import sys import json import os import sqlite3 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" 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, login_status INTEGER DEFAULT 0, last_login_at TEXT, extra_json TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL ) """ ) _ensure_column(cur, "accounts", "login_status", "INTEGER DEFAULT 0") _ensure_column(cur, "accounts", "last_login_at", "TEXT") conn.commit() finally: conn.close() def _ensure_column(cur, table_name, col_name, ddl): cur.execute(f"PRAGMA table_info({table_name})") cols = [row[1] for row in cur.fetchall()] if col_name not in cols: cur.execute(f"ALTER TABLE {table_name} ADD COLUMN {col_name} {ddl}") def get_account_by_id(account_id): init_db() conn = get_conn() try: cur = conn.cursor() cur.execute( "SELECT id, name, platform, phone, profile_dir, login_status, last_login_at, 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]), "login_status": int(row[5] or 0), "last_login_at": row[6], } if row[7]: try: extra = json.loads(row[7]) if isinstance(extra, dict): acc.update(extra) except Exception: pass return acc finally: conn.close() def cmd_list(platform="all"): """ 修改后的 list 功能,能直观打出带着【手机号】的详细台账供AI查询匹配! 支持查全部(all),或指定(sohu) """ init_db() conn = get_conn() found = False try: cur = conn.cursor() if platform == "all": cur.execute( "SELECT id, name, phone, platform, login_status FROM accounts ORDER BY platform, id" ) else: cur.execute( "SELECT id, name, phone, platform, login_status FROM accounts WHERE platform = ? ORDER BY id", (platform,), ) rows = cur.fetchall() finally: conn.close() found = False for row in rows: phone = row[2] or "未绑定手机" login_text = "已登录" if int(row[4] or 0) == 1 else "未登录" print(f"账号ID:{row[0]} | 名称:{row[1]} | 手机号:{phone} | 平台:{row[3]} | 状态:{login_text}") 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_add(account_id, name, platform, phone="", profile_dir=""): init_db() platform = (platform or "").strip().lower() if platform not in PLATFORM_URLS: print(f"ERROR:INVALID_PLATFORM (支持: {', '.join(sorted(PLATFORM_URLS.keys()))})") return account_id = (account_id or "").strip() name = (name or "").strip() or account_id if not account_id: print("ERROR:INVALID_ACCOUNT_ID") return profile_dir = (profile_dir or "").strip() or get_default_profile_dir(account_id) os.makedirs(profile_dir, exist_ok=True) now = datetime.now().isoformat(timespec="seconds") conn = get_conn() try: cur = conn.cursor() cur.execute( """ INSERT INTO accounts (id, name, platform, phone, profile_dir, login_status, last_login_at, extra_json, created_at, updated_at) VALUES (?, ?, ?, ?, ?, 0, NULL, NULL, ?, ?) ON CONFLICT(id) DO UPDATE SET name = excluded.name, platform = excluded.platform, phone = excluded.phone, profile_dir = excluded.profile_dir, updated_at = excluded.updated_at """, (account_id, name, platform, phone, profile_dir, now, now), ) conn.commit() finally: conn.close() print(f"✅ 已保存账号:{account_id}({platform})") print("ℹ️ 请继续执行登录:python account.py login <账号ID>") 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 exit_code = os.system(f'python "{tmp_path}"') os.unlink(tmp_path) if exit_code == 0: _mark_login_success(account_id) print("✅ 登录状态已标记为已登录") else: print("⚠️ 浏览器登录流程异常退出,登录状态未更新") def _mark_login_success(account_id): now = datetime.now().isoformat(timespec="seconds") conn = get_conn() try: cur = conn.cursor() cur.execute( "UPDATE accounts SET login_status = 1, last_login_at = ?, updated_at = ? WHERE id = ?", (now, now, account_id), ) conn.commit() finally: conn.close() if __name__ == "__main__": if len(sys.argv) < 2: print("用法:python account.py add [phone] [profile_dir] | list [platform] | get | login ") sys.exit(1) cmd = sys.argv[1] # 兼容写反顺序:python account.py sohu list if len(sys.argv) >= 3 and sys.argv[2] == "list" and cmd in PLATFORM_URLS: cmd_list(cmd) elif cmd in PLATFORM_URLS and len(sys.argv) == 2: cmd_list(cmd) elif cmd == "add" and len(sys.argv) >= 5: cmd_add( sys.argv[2], sys.argv[3], sys.argv[4], sys.argv[5] if len(sys.argv) >= 6 else "", sys.argv[6] if len(sys.argv) >= 7 else "", ) elif 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)