import sys import json import os import sqlite3 import subprocess from datetime import datetime # Windows GBK 编码兼容修复 if sys.platform == "win32": import io sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace') sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace') BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) SKILL_SLUG = "account-manager" LEGACY_ACCOUNTS_FILE = os.path.join(BASE_DIR, "accounts.json") PLATFORM_URLS = { # 自媒体/图文平台 "sohu": "https://mp.sohu.com", "zhihu": "https://www.zhihu.com", "wechat": "https://mp.weixin.qq.com", # 大模型平台 "kimi": "https://kimi.moonshot.cn", "deepseek": "https://chat.deepseek.com", "doubao": "https://www.doubao.com", "qianwen": "https://tongyi.aliyun.com", "yiyan": "https://yiyan.baidu.com", "yuanbao": "https://yuanbao.tencent.com" } def get_data_root(): return (os.getenv("JIANGCHANG_DATA_ROOT") or os.path.join(os.path.expanduser("~"), ".jiangchang-data")).strip() def get_user_id(): uid = (os.getenv("JIANGCHANG_USER_ID") or os.getenv("OPENCLAW_USER_ID") or "").strip() return uid or "_anon" def get_skill_data_dir(): path = os.path.join(get_data_root(), get_user_id(), SKILL_SLUG) os.makedirs(path, exist_ok=True) return path def get_db_path(): return os.path.join(get_skill_data_dir(), "account-manager.db") def get_default_profile_dir(account_id): path = os.path.join(get_skill_data_dir(), "profiles", account_id) os.makedirs(path, exist_ok=True) return path def get_conn(): return sqlite3.connect(get_db_path()) def init_db(): conn = get_conn() try: cur = conn.cursor() cur.execute( """ CREATE TABLE IF NOT EXISTS accounts ( id TEXT PRIMARY KEY, name TEXT NOT NULL, platform TEXT NOT NULL, phone TEXT, profile_dir TEXT, extra_json TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL ) """ ) conn.commit() finally: conn.close() def migrate_legacy_json_if_needed(): init_db() conn = get_conn() try: cur = conn.cursor() cur.execute("SELECT COUNT(1) FROM accounts") count = cur.fetchone()[0] if count > 0: return finally: conn.close() if not os.path.exists(LEGACY_ACCOUNTS_FILE): return try: with open(LEGACY_ACCOUNTS_FILE, "r", encoding="utf-8") as f: raw = json.load(f) except Exception: return now = datetime.now().isoformat(timespec="seconds") rows = [] for platform, accounts in (raw or {}).items(): for acc in accounts: acc_id = str(acc.get("id") or "").strip() if not acc_id: continue profile_dir = (acc.get("profile_dir") or "").strip() or get_default_profile_dir(acc_id) base = { "id": acc_id, "name": str(acc.get("name") or acc_id), "platform": str(acc.get("platform") or platform), "phone": str(acc.get("phone") or ""), "profile_dir": profile_dir, } extra = {k: v for k, v in acc.items() if k not in base} rows.append( ( base["id"], base["name"], base["platform"], base["phone"], base["profile_dir"], json.dumps(extra, ensure_ascii=False) if extra else None, now, now, ) ) if not rows: return conn = get_conn() try: cur = conn.cursor() cur.executemany( """ INSERT OR REPLACE INTO accounts (id, name, platform, phone, profile_dir, extra_json, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?) """, rows, ) conn.commit() finally: conn.close() def get_account_by_id(account_id): migrate_legacy_json_if_needed() conn = get_conn() try: cur = conn.cursor() cur.execute( "SELECT id, name, platform, phone, profile_dir, extra_json FROM accounts WHERE id = ?", (account_id,), ) row = cur.fetchone() if not row: return None acc = { "id": row[0], "name": row[1], "platform": row[2], "phone": row[3] or "", "profile_dir": row[4] or get_default_profile_dir(row[0]), } if row[5]: try: extra = json.loads(row[5]) if isinstance(extra, dict): acc.update(extra) except Exception: pass return acc finally: conn.close() def cmd_list(platform="all"): """ 修改后的 list 功能,能直观打出带着【手机号】的详细台账供AI查询匹配! 支持查全部(all),或指定(sohu) """ migrate_legacy_json_if_needed() conn = get_conn() found = False try: cur = conn.cursor() if platform == "all": cur.execute("SELECT id, name, phone, platform FROM accounts ORDER BY platform, id") else: cur.execute( "SELECT id, name, phone, platform FROM accounts WHERE platform = ? ORDER BY id", (platform,), ) rows = cur.fetchall() finally: conn.close() found = False for row in rows: phone = row[2] or "未绑定手机" print(f"账号ID:{row[0]} | 名称:{row[1]} | 手机号:{phone} | 平台:{row[3]}") found = True if not found: print(f"ERROR:NO_ACCOUNTS_FOUND") def cmd_get(account_id): acc = get_account_by_id(account_id) if acc: print(json.dumps(acc, ensure_ascii=False)) return print("ERROR:ACCOUNT_NOT_FOUND") def cmd_login(account_id): """首次登录:打开浏览器,用户手动登录,登录态自动保存到Profile目录""" target = get_account_by_id(account_id) if not target: print("ERROR:ACCOUNT_NOT_FOUND") return profile_dir = target["profile_dir"] os.makedirs(profile_dir, exist_ok=True) url = PLATFORM_URLS.get(target["platform"], "https://www.google.com") print(f"正在为账号 [{target['name']}] 打开浏览器...") print(f"请在浏览器中完成登录,登录后直接关闭浏览器窗口即可。") print(f"登录态将自动保存到:{profile_dir}") # 用Playwright打开持久化浏览器,等待用户手动登录 login_script = f""" import asyncio from playwright.async_api import async_playwright async def main(): async with async_playwright() as p: browser = await p.chromium.launch_persistent_context( user_data_dir=r"{profile_dir}", headless=False, channel="chrome", # 【修改处】也加上这行 no_viewport=True, # 确保浏览器内容视口也最大化 args=["--start-maximized"] ) page = browser.pages[0] if browser.pages else await browser.new_page() await page.goto("{url}") print("浏览器已打开,请完成登录后关闭窗口...") # 移除自动超时,无限等待直到用户扫码完手动关闭 await browser.wait_for_event("close", timeout=0) print("登录态已保存!") asyncio.run(main()) """ import tempfile with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False, encoding='utf-8') as f: f.write(login_script) tmp_path = f.name os.system(f'python "{tmp_path}"') os.unlink(tmp_path) if __name__ == "__main__": if len(sys.argv) < 2: print("用法:python account.py list [platform] | get | login ") sys.exit(1) cmd = sys.argv[1] if cmd == "list": # 默认不传 platform 时打出 all cmd_list(sys.argv[2] if len(sys.argv) >= 3 else "all") elif cmd == "get" and len(sys.argv) >= 3: cmd_get(sys.argv[2]) elif cmd == "login" and len(sys.argv) >= 3: cmd_login(sys.argv[2]) else: print("参数错误") sys.exit(1)