From 27cfe1018e5ec9e9691e77aeaba95f96f3b72299 Mon Sep 17 00:00:00 2001 From: chendelian <116870791@qq.com> Date: Sun, 5 Apr 2026 08:36:41 +0800 Subject: [PATCH] chore: initialize account-manager skill repository --- .github/workflows/release_skill.yaml | 11 + SKILL.md | 53 + release.ps1 | 23 + scripts/__pycache__/account.cpython-312.pyc | Bin 0 -> 67974 bytes scripts/__pycache__/main.cpython-312.pyc | Bin 0 -> 76635 bytes scripts/main.py | 1654 +++++++++++++++++++ 6 files changed, 1741 insertions(+) create mode 100644 .github/workflows/release_skill.yaml create mode 100644 SKILL.md create mode 100644 release.ps1 create mode 100644 scripts/__pycache__/account.cpython-312.pyc create mode 100644 scripts/__pycache__/main.cpython-312.pyc create mode 100644 scripts/main.py diff --git a/.github/workflows/release_skill.yaml b/.github/workflows/release_skill.yaml new file mode 100644 index 0000000..229e053 --- /dev/null +++ b/.github/workflows/release_skill.yaml @@ -0,0 +1,11 @@ +name: 技能自动化发布 +on: + push: + tags: ["v*"] + +jobs: + release: + uses: admin/jiangchang-platform-kit/.github/workflows/reusable-release-skill.yaml@main + with: + artifact_platform: windows + pyarmor_platform: windows.x86_64 \ No newline at end of file diff --git a/SKILL.md b/SKILL.md new file mode 100644 index 0000000..52f7e55 --- /dev/null +++ b/SKILL.md @@ -0,0 +1,53 @@ +--- +name: 账号管理 +description: 多平台多账号管理。管理各平台账号与Chrome Profile的对应关系,供publisher类Skill调用获取账号信息。 +version: 1.0.4 +author: 深圳匠厂科技有限公司 +metadata: + openclaw: + slug: account-manager + emoji: "👤" + category: "通用" +allowed-tools: + - bash +--- + +# 账号管理 + +## 使用时机 + +当用户发送以下内容的时候触发本Skill: +- 说"初始化账号登录"、"登录搜狐账号"、"帮我登录账号" +- 说"列出账号"、"查看所有账号"、"有哪些账号" +- 说"添加账号"、"删除账号"、"获取账号信息" + +## 执行步骤 + +### 添加账号(平台用中文名;须填合法中国大陆 11 位手机号;同平台下同手机号不可重复;ID 自增,名称如「搜狐1号」) +```bash +python3 {baseDir}/scripts/main.py add 搜狐号 189xxxxxxxx +python3 {baseDir}/scripts/main.py add 知乎 13800138000 +``` +支持的平台称呼示例:搜狐号、头条号、知乎、微信公众号、Kimi、DeepSeek、豆包、通义千问、文心一言、腾讯元宝(亦支持英文键如 sohu、toutiao)。 + +### 仅打开浏览器核对是否已登录(不写数据库) +```bash +python3 {baseDir}/scripts/main.py open +``` + +### 登录并自动检测、写入数据库 +```bash +python3 {baseDir}/scripts/main.py login +``` + +### 删除账号(同时删库里的记录与 profile 用户数据目录) +```bash +python3 {baseDir}/scripts/main.py delete id +python3 {baseDir}/scripts/main.py delete platform <平台> +python3 {baseDir}/scripts/main.py delete platform <平台> <手机号> +``` + +### 列出某平台所有账号(platform 可为中文名、all 或 全部) +```bash +python3 {baseDir}/scripts/main.py list all +python3 {baseDir}/scripts/main.py list 搜狐号 \ No newline at end of file diff --git a/release.ps1 b/release.ps1 new file mode 100644 index 0000000..639081a --- /dev/null +++ b/release.ps1 @@ -0,0 +1,23 @@ +[CmdletBinding()] +param( + [string]$Prefix = "v", + [string]$Message = "正式发布", + [switch]$AutoCommit, + [switch]$RequireClean, + [string]$CommitMessage, + [switch]$DryRun +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" + +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$sharedScript = Join-Path $scriptDir "..\jiangchang-platform-kit\tools\release.ps1" +$sharedScript = [System.IO.Path]::GetFullPath($sharedScript) + +if (-not (Test-Path $sharedScript)) { + throw "Shared release script not found: $sharedScript" +} + +& $sharedScript @PSBoundParameters +exit $LASTEXITCODE diff --git a/scripts/__pycache__/account.cpython-312.pyc b/scripts/__pycache__/account.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..52257685c46d473ad12027cb911cff9a62600f4d GIT binary patch literal 67974 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 B$cX>| literal 0 HcmV?d00001 diff --git a/scripts/__pycache__/main.cpython-312.pyc b/scripts/__pycache__/main.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7489a1d9e4edef31d29ce2781f2e36c85efce238 GIT binary patch literal 76635 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

%vN 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 +LOG_LOGGER_NAME = "openclaw.skill.account_manager" + +# --------------------------------------------------------------------------- +# 本地 CLI 调试:直接运行 python main.py 时,若未设置 JIANGCHANG_*,可自动注入下面默认值。 +# - 仅当 __main__ 里调用了 _apply_cli_local_dev_env() 时生效;被其它脚本 import 不会改 os.environ。 +# - 不会覆盖宿主/终端已设置的 JIANGCHANG_DATA_ROOT、JIANGCHANG_USER_ID。 +# - 开启方式(二选一): +# 1) 将 _ACCOUNT_MANAGER_CLI_LOCAL_DEV 改为 True; +# 2) 或设置环境变量 JIANGCHANG_ACCOUNT_CLI_LOCAL_DEV=1(无需改代码)。 +# 合并/发版前请关闭开关,避免把个人机器路径带进生产制品。 +# --------------------------------------------------------------------------- +_ACCOUNT_MANAGER_CLI_LOCAL_DEV = True +_ACCOUNT_MANAGER_CLI_LOCAL_DATA_ROOT = r"D:\jiangchang-data" +_ACCOUNT_MANAGER_CLI_LOCAL_USER_ID = "10032" + + +def _apply_cli_local_dev_env() -> None: + enabled = _ACCOUNT_MANAGER_CLI_LOCAL_DEV + if not enabled: + v = (os.getenv("JIANGCHANG_ACCOUNT_CLI_LOCAL_DEV") or "").strip().lower() + enabled = v in ("1", "true", "yes", "on") + if not enabled: + return + if not (os.getenv("JIANGCHANG_DATA_ROOT") or "").strip(): + os.environ["JIANGCHANG_DATA_ROOT"] = _ACCOUNT_MANAGER_CLI_LOCAL_DATA_ROOT.strip() + if not (os.getenv("JIANGCHANG_USER_ID") or "").strip(): + os.environ["JIANGCHANG_USER_ID"] = _ACCOUNT_MANAGER_CLI_LOCAL_USER_ID.strip() + + +# SQLite 无独立 DATETIME 类型:时间统一存 INTEGER Unix 秒(UTC),查询/JSON 再转 ISO8601。 +# 下列 DDL 含注释,会原样写入 sqlite_master,便于 Navicat / DBeaver 等查看建表语句。 +ACCOUNTS_TABLE_SQL = """ +/* 多平台账号表:与本地 Chromium/Edge 用户数据目录(profile)绑定,供发布类技能读取登录态 */ +CREATE TABLE accounts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, -- 账号主键(自增) + name TEXT NOT NULL, -- 展示名称,如「搜狐1号」 + platform TEXT NOT NULL, -- 平台标识,与内置 PLATFORMS 键一致,如 sohu + phone TEXT, -- 可选绑定手机号 + profile_dir TEXT, -- Playwright 用户数据目录(绝对路径);默认可读结构 profiles/<平台展示名>/<手机号>/ + url TEXT, -- 平台入口或登录页 URL + login_status INTEGER NOT NULL DEFAULT 0, -- 是否已登录:0 否 1 是(由脚本校验后写入) + last_login_at INTEGER, -- 最近一次登录成功时间,Unix 秒 UTC;未登录过为 NULL + extra_json TEXT, -- 扩展字段 JSON + created_at INTEGER NOT NULL, -- 记录创建时间,Unix 秒 UTC + updated_at INTEGER NOT NULL -- 记录最后更新时间,Unix 秒 UTC +); +""" + + +def _now_unix() -> int: + return int(time.time()) + + +def _unix_to_iso_local(ts: Optional[int]) -> Optional[str]: + """对外 JSON:本地时区 ISO8601 字符串,便于人读。""" + if ts is None: + return None + try: + return datetime.fromtimestamp(int(ts)).isoformat(timespec="seconds") + except (ValueError, OSError, OverflowError): + return None + + +# 平台唯一配置表:只改这里即可。键 = 入库的 platform;label = 帮助/提示里的展示名; +# prefix = 默认账号名「{prefix}{序号}号」,省略时等于 label; +# aliases = 额外 CLI 称呼(英文键与 label 已自动参与解析,不必重复写)。 +# 登录检测:仅看页面 DOM。匹配 anchor 的标签页上出现「未登录」选择器则未登录;否则视为已登录。 +# - _LOGIN_LOGGED_OUT_DOM_GENERIC_SELECTORS:通用「登录」按钮/链接等(可被 login_skip_generic_logged_out_dom 关闭)。 +# - login_logged_out_selectors:按平台追加;误判时也可用 login_skip_generic_logged_out_dom + 仅平台自选。 +PLATFORMS = { + "sohu": { + "url": "https://mp.sohu.com", + "label": "搜狐号", + "prefix": "搜狐", + "aliases": ["搜狐"], + }, + "toutiao": { + "url": "https://mp.toutiao.com/", + "label": "头条号", + "prefix": "头条", + "aliases": ["头条"], + }, + "zhihu": { + "url": "https://www.zhihu.com", + "label": "知乎", + "aliases": ["知乎号"], + }, + "wechat": { + "url": "https://mp.weixin.qq.com", + "label": "微信公众号", + "prefix": "微信", + "aliases": ["公众号", "微信"], + }, + "kimi": { + "url": "https://kimi.moonshot.cn", + "label": "Kimi", + "aliases": ["月之暗面"], + }, + "deepseek": { + "url": "https://chat.deepseek.com", + "label": "DeepSeek", + }, + "doubao": { + "url": "https://www.doubao.com", + "label": "豆包", + }, + "qianwen": { + "url": "https://tongyi.aliyun.com", + "label": "通义千问", + "prefix": "通义", + "aliases": ["通义", "千问"], + }, + "yiyan": { + "url": "https://yiyan.baidu.com", + "label": "文心一言", + "prefix": "文心", + "aliases": ["文心", "一言"], + }, + "yuanbao": { + "url": "https://yuanbao.tencent.com", + "label": "腾讯元宝", + "prefix": "元宝", + "aliases": ["元宝"], + }, +} + +# 未登录页共性:主行动点含「登录」——submit 按钮、Element 主按钮、Semi 文案、标题、通栏主按钮等,Playwright :has-text 可覆盖多数站点。 +_LOGIN_LOGGED_OUT_DOM_GENERIC_SELECTORS = ( + 'button:has-text("登录")', + 'role=button[name="登录"]', + 'a:has-text("登录")', + 'h4:has-text("登录")', + 'span.semi-button-content:has-text("登录")', +) + + +def _build_platform_derived(): + urls = {} + name_prefix = {} + primary_cn = {} + alias_to_key = {} + + def _register_alias(alias: str, key: str) -> None: + if not alias or not str(alias).strip(): + return + a = str(alias).strip() + if a not in alias_to_key: + alias_to_key[a] = key + lo = a.lower() + if lo not in alias_to_key: + alias_to_key[lo] = key + + for key, spec in PLATFORMS.items(): + urls[key] = spec["url"] + primary_cn[key] = spec["label"] + name_prefix[key] = (spec.get("prefix") or spec["label"]).strip() or key + _register_alias(key, key) + _register_alias(spec["label"], key) + for a in spec.get("aliases") or []: + _register_alias(a, key) + + return urls, name_prefix, primary_cn, alias_to_key + + +PLATFORM_URLS, _PLATFORM_NAME_CN, _PLATFORM_PRIMARY_CN, _PLATFORM_ALIAS_TO_KEY = _build_platform_derived() + + +def resolve_platform_key(name: str): + """将用户输入解析为内部 platform 键;无法识别返回 None。""" + if name is None: + return None + s = str(name).strip() + if not s: + return None + sl = s.lower() + if sl in PLATFORM_URLS: + return sl + return _PLATFORM_ALIAS_TO_KEY.get(s) + + +def _normalize_phone_digits(phone: str) -> str: + """从输入中提取数字串,供号段规则校验与入库去重(不对外承诺可随意夹杂符号)。""" + d = re.sub(r"\D", "", (phone or "").strip()) + if d.startswith("86") and len(d) >= 13: + d = d[2:] + return d + + +def _is_valid_cn_mobile11(digits: str) -> bool: + """中国大陆 11 位手机号:1 开头,第二位 3–9,共 11 位数字。""" + return bool(re.fullmatch(r"1[3-9]\d{9}", digits or "")) + + +def _platform_list_cn_for_help() -> str: + """帮助文案:一行中文展示名(不重复内部键)。""" + parts = [] + for k in sorted(PLATFORM_URLS.keys()): + parts.append(_PLATFORM_PRIMARY_CN.get(k, k)) + return "、".join(parts) + + +def get_data_root(): + env = (os.getenv("JIANGCHANG_DATA_ROOT") or "").strip() + if env: + return env + if sys.platform == "win32": + return r"D:\jiangchang-data" + return os.path.join(os.path.expanduser("~"), ".jiangchang-data") + + +def get_user_id(): + uid = (os.getenv("JIANGCHANG_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_skill_logs_dir() -> str: + """{DATA_ROOT}/{USER_ID}/account-manager/logs/""" + path = os.path.join(get_skill_data_dir(), "logs") + os.makedirs(path, exist_ok=True) + return path + + +def get_skill_log_file_path() -> str: + """主日志文件绝对路径(与 get_skill_logger 写入文件一致)。""" + return _skill_log_file_path() + + +def _skill_log_file_path() -> str: + """主日志文件路径;可由 JIANGCHANG_ACCOUNT_MANAGER_LOG_FILE 覆盖为绝对路径。""" + override = (os.getenv("JIANGCHANG_ACCOUNT_MANAGER_LOG_FILE") or "").strip() + if override: + parent = os.path.dirname(os.path.abspath(override)) + if parent: + os.makedirs(parent, exist_ok=True) + return os.path.abspath(override) + return os.path.join(get_skill_logs_dir(), f"{SKILL_SLUG}.log") + + +def _log_level_from_env() -> int: + v = (os.getenv("JIANGCHANG_LOG_LEVEL") or "INFO").strip().upper() + return getattr(logging, v, None) or logging.INFO + + +def get_skill_logger() -> logging.Logger: + """ + 技能级日志:单文件按日切分(TimedRotatingFileHandler midnight),UTF-8。 + 环境变量:JIANGCHANG_LOG_LEVEL(默认 INFO)、JIANGCHANG_LOG_TO_STDERR=1 时 WARNING+ 同步 stderr、 + JIANGCHANG_ACCOUNT_MANAGER_LOG_FILE 覆盖日志文件路径。 + """ + log = logging.getLogger(LOG_LOGGER_NAME) + if log.handlers: + return log + log.setLevel(_log_level_from_env()) + path = _skill_log_file_path() + fh = TimedRotatingFileHandler( + path, + when="midnight", + interval=1, + backupCount=30, + encoding="utf-8", + delay=True, + ) + fh.setFormatter( + logging.Formatter( + "%(asctime)s | %(levelname)-8s | %(name)s | %(message)s", + datefmt="%Y-%m-%dT%H:%M:%S", + ) + ) + log.addHandler(fh) + if (os.getenv("JIANGCHANG_LOG_TO_STDERR") or "").strip().lower() in ( + "1", + "true", + "yes", + "on", + ): + sh = logging.StreamHandler(sys.stderr) + sh.setLevel(logging.WARNING) + sh.setFormatter(fh.formatter) + log.addHandler(sh) + log.propagate = False + return log + + +def get_db_path(): + return os.path.join(get_skill_data_dir(), "account-manager.db") + + +def _runtime_paths_debug_text(): + """供排查「为何 list 没数据」:终端未注入环境变量时会落到 _anon 等默认目录,与网关里用的库不是同一个文件。""" + env_root = (os.getenv("JIANGCHANG_DATA_ROOT") or "").strip() or "(未设置)" + env_uid = (os.getenv("JIANGCHANG_USER_ID") or "").strip() or "(未设置→使用 _anon)" + return ( + "[account-manager] " + f"JIANGCHANG_DATA_ROOT={env_root} | " + f"JIANGCHANG_USER_ID={env_uid} | " + f"实际数据根={get_data_root()} | " + f"实际用户目录={get_user_id()} | " + f"数据库文件={get_db_path()}" + ) + + +def _maybe_print_paths_debug_cli(): + """设置 JIANGCHANG_ACCOUNT_DEBUG_PATHS=1 时,每次执行子命令前在 stderr 打印路径。""" + v = (os.getenv("JIANGCHANG_ACCOUNT_DEBUG_PATHS") or "").strip().lower() + if v in ("1", "true", "yes", "on"): + print(_runtime_paths_debug_text(), file=sys.stderr) + + +_WIN_FS_FORBIDDEN = re.compile(r'[<>:"/\\|?*\x00-\x1f]') +_WIN_RESERVED_NAMES = frozenset( + ["CON", "PRN", "AUX", "NUL"] + + [f"COM{i}" for i in range(1, 10)] + + [f"LPT{i}" for i in range(1, 10)] +) + + +def _fs_safe_segment(name: str, fallback: str) -> str: + """单级目录名:去掉 Windows 非法字符,避免末尾点/空格;空则用 fallback。""" + t = _WIN_FS_FORBIDDEN.sub("_", (name or "").strip()) + t = t.rstrip(" .") + if not t: + t = fallback + if t.upper() in _WIN_RESERVED_NAMES: + t = f"_{t}" + return t + + +def get_default_profile_dir( + platform_key: str, phone: Optional[str], account_id: Optional[int] = None +) -> str: + """ + 默认可读路径:profiles/<平台展示名>/<手机号>/(便于在资源管理器中辨认)。 + 无手机号时退化为 profiles/<平台展示名>/no_phone_/。 + """ + label = _PLATFORM_PRIMARY_CN.get(platform_key, platform_key or "account") + label_seg = _fs_safe_segment(label, _fs_safe_segment(platform_key or "", "platform")) + ph = (phone or "").strip() + if ph: + phone_seg = _fs_safe_segment(ph, f"id_{account_id}" if account_id is not None else "phone") + else: + phone_seg = _fs_safe_segment( + "", f"no_phone_{account_id}" if account_id is not None else "no_phone" + ) + path = os.path.join(get_skill_data_dir(), "profiles", label_seg, phone_seg) + 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("SELECT name FROM sqlite_master WHERE type='table' AND name='accounts'") + if not cur.fetchone(): + cur.executescript(ACCOUNTS_TABLE_SQL) + conn.commit() + finally: + conn.close() + + +def _normalize_account_id(account_id): + if account_id is None: + return None + s = str(account_id).strip() + if s.isdigit(): + return int(s) + return s + + +def get_account_by_id(account_id): + init_db() + aid = _normalize_account_id(account_id) + conn = get_conn() + try: + cur = conn.cursor() + cur.execute( + """ + SELECT id, name, platform, phone, profile_dir, url, login_status, last_login_at, extra_json, + created_at, updated_at + FROM accounts WHERE id = ? + """, + (aid,), + ) + 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 "").strip(), + "url": row[5] or PLATFORM_URLS.get(row[2], ""), + "login_status": int(row[6] or 0), + "last_login_at": _unix_to_iso_local(row[7]), + "created_at": _unix_to_iso_local(row[9]), + "updated_at": _unix_to_iso_local(row[10]), + } + if row[8]: + try: + extra = json.loads(row[8]) + if isinstance(extra, dict): + acc.update(extra) + except Exception: + pass + return acc + finally: + conn.close() + + +def _default_name_for_platform(platform: str, index: int) -> str: + cn = _PLATFORM_NAME_CN.get(platform) + if cn: + return f"{cn}{index}号" + return f"{platform}_{index}" + + +# list 表头:与 accounts 表列顺序、列名一致(便于对照库结构) +_LIST_TABLE_COLUMNS = ( + "id", + "name", + "platform", + "phone", + "profile_dir", + "url", + "login_status", + "last_login_at", + "extra_json", + "created_at", + "updated_at", +) + + +def _list_row_to_cells(row: tuple) -> list: + """将 SELECT 整行转为展示用字符串列表(与 _LIST_TABLE_COLUMNS 顺序一致)。""" + rid, name, plat, phone, pdir, url, lstat, lla, exj, cat, uat = row + return [ + str(rid) if rid is not None else "", + name if name is not None else "", + plat if plat is not None else "", + phone if phone is not None else "", + pdir if pdir is not None else "", + url if url is not None else "", + str(int(lstat)) if lstat is not None else "", + str(int(lla)) if lla is not None else "", + (exj or "").replace("\n", "\\n").replace("\r", ""), + str(int(cat)) if cat is not None else "", + str(int(uat)) if uat is not None else "", + ] + + +def _print_accounts_table(rows: list) -> None: + """表头 + 分隔线 + 每账号一行;列宽按内容对齐(终端等宽字体)。""" + headers = list(_LIST_TABLE_COLUMNS) + body = [_list_row_to_cells(r) for r in rows] + n = len(headers) + widths = [len(headers[j]) for j in range(n)] + for cells in body: + for j in range(n): + widths[j] = max(widths[j], len(cells[j])) + sep = " | " + + def fmt(cells): + return sep.join(cells[j].ljust(widths[j]) for j in range(n)) + + print(fmt(headers)) + print(sep.join("-" * widths[j] for j in range(n))) + for cells in body: + print(fmt(cells)) + + +def cmd_list(platform="all", limit: int = 10): + get_skill_logger().info("list filter=%r", platform) + init_db() + raw = (platform or "all").strip() + if not raw or raw.lower() == "all" or raw == "全部": + key = "all" + else: + key = resolve_platform_key(raw) + if not key: + get_skill_logger().warning("list_invalid_platform raw=%r", raw) + print(f"ERROR:INVALID_PLATFORM_LIST 无法识别的平台「{raw}」") + print("支持:" + _platform_list_cn_for_help()) + return + + if limit <= 0: + limit = 10 + + conn = get_conn() + try: + cur = conn.cursor() + sql = ( + "SELECT id, name, platform, phone, profile_dir, url, login_status, " + "last_login_at, extra_json, created_at, updated_at FROM accounts " + ) + if key == "all": + cur.execute(sql + "ORDER BY created_at DESC, id DESC LIMIT ?", (int(limit),)) + else: + cur.execute( + sql + "WHERE platform = ? ORDER BY created_at DESC, id DESC LIMIT ?", + (key, int(limit)), + ) + rows = cur.fetchall() + finally: + conn.close() + + found = bool(rows) + if found: + get_skill_logger().info("list_ok rows=%s key=%s", len(rows), key) + sep_line = "_" * 39 + for idx, row in enumerate(rows): + ( + aid, + name, + plat, + phone, + profile_dir, + url, + login_status, + last_login_at, + extra_json, + created_at, + updated_at, + ) = row + print(f"id:{aid}") + print(f"name:{name or ''}") + print(f"platform:{plat or ''}") + print(f"phone:{phone or ''}") + print(f"profile_dir:{profile_dir or ''}") + print(f"url:{url or ''}") + print(f"login_status:{int(login_status) if login_status is not None else ''}") + print(f"last_login_at:{int(last_login_at) if last_login_at is not None else ''}") + print(f"extra_json:{extra_json or ''}") + print(f"created_at:{int(created_at) if created_at is not None else ''}") + print(f"updated_at:{int(updated_at) if updated_at is not None else ''}") + if idx != len(rows) - 1: + print(sep_line) + print() + + if not found: + print("ERROR:NO_ACCOUNTS_FOUND") + print(_runtime_paths_debug_text(), file=sys.stderr) + + +def cmd_get(account_id): + get_skill_logger().info("get account_id=%r", account_id) + acc = get_account_by_id(account_id) + if acc: + print(json.dumps(acc, ensure_ascii=False)) + return + get_skill_logger().warning("get_not_found account_id=%r", account_id) + print("ERROR:ACCOUNT_NOT_FOUND") + print(_runtime_paths_debug_text(), file=sys.stderr) + + +def cmd_list_json(platform_input: str, limit: int = 200) -> None: + """ + 跨技能接口:stdout 仅输出一行 JSON 数组,元素结构与 get 单条一致。 + 平台可为 all/全部 或各平台中文名/英文键;按 updated_at 倒序,最多 limit 条(上限 500)。 + """ + get_skill_logger().info("list_json filter=%r limit=%s", platform_input, limit) + init_db() + raw = (platform_input or "all").strip() + if not raw or raw.lower() == "all" or raw == "全部": + key = "all" + else: + key = resolve_platform_key(raw) + if not key: + print("ERROR:INVALID_PLATFORM_LIST_JSON 无法识别的平台名称。") + print("支持:" + _platform_list_cn_for_help()) + return + lim = max(1, min(int(limit), 500)) + conn = get_conn() + try: + cur = conn.cursor() + if key == "all": + cur.execute( + "SELECT id FROM accounts ORDER BY updated_at DESC, id DESC LIMIT ?", + (lim,), + ) + else: + cur.execute( + "SELECT id FROM accounts WHERE platform = ? " + "ORDER BY updated_at DESC, id DESC LIMIT ?", + (key, lim), + ) + ids = [r[0] for r in cur.fetchall()] + finally: + conn.close() + out = [] + for aid in ids: + acc = get_account_by_id(aid) + if acc: + out.append(acc) + print(json.dumps(out, ensure_ascii=False)) + + +def cmd_pick_logged_in(platform_input: str): + """ + 机器可读跨技能接口:查询指定平台下「已登录」的一条账号(login_status=1,按 last_login_at 优先)。 + 成功:stdout 仅输出一行 JSON,结构与 get 子命令一致。 + 失败:stdout 首行以 ERROR: 开头(由调用方判断,勿解析为 JSON)。 + """ + get_skill_logger().info("pick_logged_in platform_input=%r", platform_input) + key = resolve_platform_key((platform_input or "").strip()) + if not key: + print("ERROR:INVALID_PLATFORM 无法识别的平台名称。") + print("支持:" + _platform_list_cn_for_help()) + return + + init_db() + conn = get_conn() + try: + cur = conn.cursor() + cur.execute( + """ + SELECT id FROM accounts + WHERE platform = ? AND login_status = 1 + ORDER BY (last_login_at IS NULL), last_login_at DESC + LIMIT 1 + """, + (key,), + ) + row = cur.fetchone() + finally: + conn.close() + + if not row: + print("ERROR:NO_LOGGED_IN_ACCOUNT 该平台暂无已登录账号(login_status=1)。") + print("请先 list 查看账号 id,再执行:python main.py login ") + return + + acc = get_account_by_id(row[0]) + if not acc: + print("ERROR:ACCOUNT_NOT_FOUND") + print(_runtime_paths_debug_text(), file=sys.stderr) + return + print(json.dumps(acc, ensure_ascii=False)) + + +def cmd_pick_web(platform_input: str): + """ + 供 llm-manager 等:取该平台用于网页自动化的账号候选。 + 优先 login_status=1(与 pick-logged-in 一致);若无,则取该平台 updated_at 最新的一条, + 便于「已 add 未标登录」时仍打开 profile,在浏览器内登录后继续任务。 + 成功:stdout 仅一行 JSON(与 get 一致);失败:首行 ERROR:。 + """ + get_skill_logger().info("pick_web platform_input=%r", platform_input) + key = resolve_platform_key((platform_input or "").strip()) + if not key: + print("ERROR:INVALID_PLATFORM 无法识别的平台名称。") + print("支持:" + _platform_list_cn_for_help()) + return + + init_db() + conn = get_conn() + try: + cur = conn.cursor() + cur.execute( + """ + SELECT id FROM accounts + WHERE platform = ? AND login_status = 1 + ORDER BY (last_login_at IS NULL), last_login_at DESC + LIMIT 1 + """, + (key,), + ) + row = cur.fetchone() + if not row: + cur.execute( + """ + SELECT id FROM accounts + WHERE platform = ? + ORDER BY updated_at DESC, id DESC + LIMIT 1 + """, + (key,), + ) + row = cur.fetchone() + finally: + conn.close() + + if not row: + print("ERROR:NO_ACCOUNT 该平台在账号库中没有任何记录,请先执行 add。") + return + + acc = get_account_by_id(row[0]) + if not acc: + print("ERROR:ACCOUNT_NOT_FOUND") + print(_runtime_paths_debug_text(), file=sys.stderr) + return + print(json.dumps(acc, ensure_ascii=False)) + + +def cmd_add(platform_input: str, phone: str): + """添加账号:platform 可为中文展示名或英文键;手机号必填;同平台下同手机号不可重复。""" + log = get_skill_logger() + log.info("add_attempt platform_input=%r", platform_input) + init_db() + key = resolve_platform_key((platform_input or "").strip()) + if not key: + log.warning("add_invalid_platform input=%r", platform_input) + print("ERROR:INVALID_PLATFORM 无法识别的平台名称。") + print("支持:" + _platform_list_cn_for_help()) + return + + phone_raw = (phone or "").strip() + if not phone_raw: + log.warning("add_phone_missing") + print("ERROR:PHONE_REQUIRED 手机号为必填。") + return + phone_norm = _normalize_phone_digits(phone_raw) + if not _is_valid_cn_mobile11(phone_norm): + log.warning("add_phone_invalid digits_len=%s", len(phone_norm or "")) + print( + "ERROR:PHONE_INVALID 手机号格式不正确:须为中国大陆 11 位号码," + "以 1 开头、第二位为 3~9(示例 13800138000)。" + ) + return + + url = PLATFORM_URLS[key] + now = _now_unix() + phone_store = phone_norm + + conn = get_conn() + try: + cur = conn.cursor() + cur.execute( + "SELECT id FROM accounts WHERE platform = ? AND phone = ?", + (key, phone_store), + ) + if cur.fetchone(): + log.warning("add_duplicate platform=%s phone_suffix=%s", key, phone_store[-4:]) + print( + f"ERROR:DUPLICATE_PHONE_PLATFORM 该平台下已存在手机号 {phone_store},请勿重复添加。" + ) + return + + cur.execute("SELECT COUNT(*) FROM accounts WHERE platform = ?", (key,)) + next_idx = cur.fetchone()[0] + 1 + name = _default_name_for_platform(key, next_idx) + profile_dir = get_default_profile_dir(key, phone_store, None) + cur.execute( + """ + INSERT INTO accounts (name, platform, phone, profile_dir, url, login_status, last_login_at, extra_json, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, 0, NULL, NULL, ?, ?) + """, + (name, key, phone_store, profile_dir, url, now, now), + ) + new_id = cur.lastrowid + conn.commit() + finally: + conn.close() + + print( + f"✅ 已保存账号:ID {new_id} | {name} | {_PLATFORM_PRIMARY_CN.get(key, key)} | 手机 {phone_store}" + ) + print(f"ℹ️ 登录请执行:python main.py login {new_id}") + log.info( + "add_success id=%s platform=%s profile_dir=%s", + new_id, + key, + profile_dir, + ) + + +def _remove_profile_dir(path: str) -> None: + p = (path or "").strip() + if not p or not os.path.isdir(p): + return + try: + shutil.rmtree(p) + except OSError as e: + print(f"⚠️ 未能删除用户数据目录(可手工删):{p}\n {e}", file=sys.stderr) + + +def cmd_delete_by_id(account_id) -> None: + log = get_skill_logger() + log.info("delete_by_id account_id=%r", account_id) + init_db() + aid = _normalize_account_id(account_id) + if aid is None or (isinstance(aid, str) and not str(aid).isdigit()): + log.warning("delete_invalid_id") + print("ERROR:DELETE_INVALID_ID 账号 id 须为正整数。") + return + conn = get_conn() + try: + cur = conn.cursor() + cur.execute( + "SELECT id, name, platform, phone, profile_dir FROM accounts WHERE id = ?", + (int(aid),), + ) + row = cur.fetchone() + if not row: + log.warning("delete_by_id_not_found id=%s", aid) + print("ERROR:ACCOUNT_NOT_FOUND") + print(_runtime_paths_debug_text(), file=sys.stderr) + return + rid, name, plat, phone, profile_dir = row + cur.execute("DELETE FROM accounts WHERE id = ?", (rid,)) + conn.commit() + finally: + conn.close() + _remove_profile_dir(profile_dir) + print( + f"✅ 已删除账号:ID {rid} | {name} | {_PLATFORM_PRIMARY_CN.get(plat, plat)} | 手机 {phone or '(无)'}" + ) + log.info("delete_by_id_done id=%s platform=%s", rid, plat) + + +def cmd_delete_by_platform(platform_input: str) -> None: + log = get_skill_logger() + log.info("delete_by_platform platform_input=%r", platform_input) + init_db() + key = resolve_platform_key((platform_input or "").strip()) + if not key: + log.warning("delete_by_platform_invalid_platform") + print("ERROR:INVALID_PLATFORM 无法识别的平台名称。") + print("支持:" + _platform_list_cn_for_help()) + return + conn = get_conn() + try: + cur = conn.cursor() + cur.execute( + "SELECT id, profile_dir FROM accounts WHERE platform = ?", + (key,), + ) + rows = cur.fetchall() + if not rows: + log.warning("delete_by_platform_empty platform=%s", key) + print("ERROR:NO_ACCOUNTS_FOUND 该平台下没有账号记录。") + return + cur.execute("DELETE FROM accounts WHERE platform = ?", (key,)) + conn.commit() + finally: + conn.close() + for _rid, pdir in rows: + _remove_profile_dir(pdir) + print( + f"✅ 已删除 {_PLATFORM_PRIMARY_CN.get(key, key)} 下共 {len(rows)} 条账号及对应用户数据目录。" + ) + log.info("delete_by_platform_done platform=%s count=%s", key, len(rows)) + + +def cmd_delete_by_platform_phone(platform_input: str, phone: str) -> None: + log = get_skill_logger() + log.info("delete_by_platform_phone platform_input=%r", platform_input) + init_db() + key = resolve_platform_key((platform_input or "").strip()) + if not key: + print("ERROR:INVALID_PLATFORM 无法识别的平台名称。") + print("支持:" + _platform_list_cn_for_help()) + return + phone_raw = (phone or "").strip() + if not phone_raw: + print("ERROR:PHONE_REQUIRED 手机号为必填。") + return + phone_norm = _normalize_phone_digits(phone_raw) + if not _is_valid_cn_mobile11(phone_norm): + print( + "ERROR:PHONE_INVALID 手机号格式不正确:须为中国大陆 11 位号码," + "以 1 开头、第二位为 3~9。" + ) + return + conn = get_conn() + try: + cur = conn.cursor() + cur.execute( + "SELECT id, name, profile_dir FROM accounts WHERE platform = ? AND phone = ?", + (key, phone_norm), + ) + row = cur.fetchone() + if not row: + print("ERROR:ACCOUNT_NOT_FOUND 该平台下无此手机号。") + return + rid, name, profile_dir = row + cur.execute( + "DELETE FROM accounts WHERE platform = ? AND phone = ?", + (key, phone_norm), + ) + conn.commit() + finally: + conn.close() + _remove_profile_dir(profile_dir) + print( + f"✅ 已删除账号:ID {rid} | {name} | {_PLATFORM_PRIMARY_CN.get(key, key)} | 手机 {phone_norm}" + ) + log.info("delete_by_platform_phone_done id=%s platform=%s", rid, key) + + +def _win_find_exe(candidates): + for p in candidates: + if p and os.path.isfile(p): + return p + return None + + +def resolve_chromium_channel(): + """ + Playwright channel: 'chrome' | 'msedge' | None + Windows 优先 Chrome,其次 Edge;其它系统默认 chrome(由 Playwright 解析 PATH)。 + """ + if sys.platform != "win32": + return "chrome" + + pf = os.environ.get("ProgramFiles", r"C:\Program Files") + pfx86 = os.environ.get("ProgramFiles(x86)", r"C:\Program Files (x86)") + local = os.environ.get("LocalAppData", "") + + chrome = _win_find_exe( + [ + os.path.join(pf, "Google", "Chrome", "Application", "chrome.exe"), + os.path.join(pfx86, "Google", "Chrome", "Application", "chrome.exe"), + os.path.join(local, "Google", "Chrome", "Application", "chrome.exe") if local else "", + ] + ) + if chrome: + return "chrome" + + edge = _win_find_exe( + [ + os.path.join(pfx86, "Microsoft", "Edge", "Application", "msedge.exe"), + os.path.join(pf, "Microsoft", "Edge", "Application", "msedge.exe"), + os.path.join(local, "Microsoft", "Edge", "Application", "msedge.exe") if local else "", + ] + ) + if edge: + return "msedge" + + return None + + +def _print_browser_install_hint(): + print("❌ 未检测到 Google Chrome 或 Microsoft Edge,请先安装后再登录:") + print(" • Chrome: https://www.google.com/chrome/") + print(" • Edge: https://www.microsoft.com/zh-cn/edge") + + +def _login_timeout_seconds(): + try: + return max(60, int((os.getenv("JIANGCHANG_LOGIN_TIMEOUT_SECONDS") or "300").strip())) + except ValueError: + return 300 + + +def _login_poll_interval_seconds(): + """轮询间隔,默认 1.5s。""" + try: + v = float((os.getenv("JIANGCHANG_LOGIN_POLL_INTERVAL_SECONDS") or "1.5").strip()) + return max(0.5, min(v, 10.0)) + except ValueError: + return 1.5 + + +def _login_dom_grace_seconds() -> float: + """goto 后等待再开始 DOM 判定,减轻跳转中途误判。默认 4s,JIANGCHANG_LOGIN_DOM_GRACE_SECONDS。""" + try: + v = float((os.getenv("JIANGCHANG_LOGIN_DOM_GRACE_SECONDS") or "4").strip()) + return max(0.0, min(v, 60.0)) + except ValueError: + return 4.0 + + +def _login_out_bundle_for(platform_key: str) -> dict: + """传给子进程:anchor + 未登录可见的选择器列表。""" + spec = PLATFORMS.get(platform_key, {}) if platform_key else {} + anchor = (urlparse(PLATFORM_URLS.get(platform_key, "") or "").netloc or "").lower() + spec_lo_dom = [ + str(s).strip() + for s in (spec.get("login_logged_out_selectors") or []) + if s is not None and str(s).strip() + ] + if bool(spec.get("login_skip_generic_logged_out_dom")): + logged_out_dom_selectors = list(spec_lo_dom) + else: + logged_out_dom_selectors = list(_LOGIN_LOGGED_OUT_DOM_GENERIC_SELECTORS) + spec_lo_dom + return { + "logged_out_dom_selectors": logged_out_dom_selectors, + "anchor_host": anchor, + } + + +def cmd_login(account_id): + log = get_skill_logger() + log.info("login_command account_id=%r", account_id) + target = get_account_by_id(account_id) + if not target: + log.warning("login_aborted account_not_found account_id=%r", account_id) + print("ERROR:ACCOUNT_NOT_FOUND") + return + + channel = resolve_chromium_channel() + if not channel: + log.warning("login_aborted no_chromium_channel") + _print_browser_install_hint() + return + + try: + import playwright # noqa: F401 + except ImportError: + log.error("login_aborted playwright_missing") + print("ERROR:需要 playwright:pip install playwright && playwright install chromium") + return + + profile_dir = (target.get("profile_dir") or "").strip() + if not profile_dir: + log.warning("login_aborted profile_dir_empty account_id=%s", target.get("id")) + print("ERROR:PROFILE_DIR_MISSING 库中 profile_dir 为空。") + return + os.makedirs(profile_dir, exist_ok=True) + url = (target.get("url") or "").strip() or PLATFORM_URLS.get(target["platform"], "https://www.google.com") + platform = (target.get("platform") or "").strip().lower() + timeout_sec = _login_timeout_seconds() + poll_sec = _login_poll_interval_seconds() + dom_grace_sec = _login_dom_grace_seconds() + + browser_name = "Google Chrome" if channel == "chrome" else "Microsoft Edge" + print(f"正在为账号 [{target['name']}] 打开 {browser_name} …") + print(f"访问地址:{url}") + print( + "请在窗口内完成登录。系统根据页面是否仍出现「登录」等未登录控件判断;" + "成功后自动关闭窗口并写入状态(无需手动关浏览器)。" + ) + print( + f"页面加载后先等待约 {dom_grace_sec:g} 秒再开始检测(减轻跳转误判,可用 JIANGCHANG_LOGIN_DOM_GRACE_SECONDS 调整)。" + ) + print(f"轮询间隔约 {poll_sec} 秒;全程最长 {timeout_sec} 秒。") + + # 子进程:login_detect_bundle + 多 tab DOM;日志与主进程同一文件(追加)。 + log_file = _skill_log_file_path() + log.info( + "login_browser_start account_id=%s platform=%s channel=%s timeout_sec=%s poll_sec=%s " + "dom_grace_sec=%s profile_dir=%s start_url=%s log_file=%s", + target.get("id"), + platform, + channel, + timeout_sec, + poll_sec, + dom_grace_sec, + profile_dir, + url, + log_file, + ) + runner = r"""import json, logging, sys, time +from urllib.parse import urlparse +from playwright.sync_api import sync_playwright + +def page_location_href(p): + # Prefer location.href over page.url for SPA (e.g. Sohu shell path lag). + try: + href = p.evaluate("() => location.href") + if href and str(href).strip(): + return str(href).strip() + except Exception: + pass + try: + return (p.url or "").strip() + except Exception: + return "" + +def host_matches_anchor(netloc, anchor): + h = (netloc or "").lower().strip() + a = (anchor or "").lower().strip() + if not a: + return True + if not h: + return False + return h == a or h.endswith("." + a) or a.endswith("." + h) + +def loc_first_visible(pg, selectors): + for sel in selectors: + s = str(sel).strip() + if not s: + continue + try: + loc = pg.locator(s).first + if loc.is_visible(timeout=500): + return True, s + except Exception: + pass + return False, None + +def list_pages_on_anchor(ctx, main_page, bundle): + anchor = (bundle.get("anchor_host") or "").strip().lower() + seen_ids = set() + pages = [] + for pg in list(ctx.pages or []): + try: + i = id(pg) + if i not in seen_ids: + seen_ids.add(i) + pages.append(pg) + except Exception: + pages.append(pg) + if main_page is not None: + try: + i = id(main_page) + if i not in seen_ids: + seen_ids.add(i) + pages.append(main_page) + except Exception: + pages.append(main_page) + out = [] + for pg in pages: + try: + href = page_location_href(pg) + except Exception: + continue + if not href or str(href).lower().startswith("about:"): + continue + try: + host = urlparse(href).netloc.lower() + except Exception: + continue + if anchor and not host_matches_anchor(host, anchor): + continue + out.append(pg) + return out + +def evaluate_logged_out_dom(ctx, main_page, bundle): + selectors = [ + str(x).strip() + for x in (bundle.get("logged_out_dom_selectors") or []) + if x is not None and str(x).strip() + ] + if not selectors: + return True, "no_logged_out_dom_selectors_configured" + for pg in list_pages_on_anchor(ctx, main_page, bundle): + try: + href = page_location_href(pg) + except Exception: + href = "" + hit, which = loc_first_visible(pg, selectors) + if hit: + return True, "logged_out_dom=%r url=%r" % (which, href) + return False, "" + +with open(sys.argv[1], encoding="utf-8") as f: + c = json.load(f) +bundle = c.get("login_detect_bundle") or {} +poll = float(c.get("poll_interval", 1.5)) +try: + dom_grace = float(c.get("dom_grace_sec", 4.0)) +except (TypeError, ValueError): + dom_grace = 4.0 +if dom_grace < 0: + dom_grace = 0.0 +t0 = time.time() +deadline = t0 + float(c["timeout_sec"]) +interactive_ok = False +result_path = c.get("result_path") or "" +log_path = (c.get("log_file") or "").strip() +lg = logging.getLogger("openclaw.skill.account_manager.login_child") +if log_path: + lg.handlers.clear() + lg.setLevel(logging.DEBUG) + _fh = logging.FileHandler(log_path, encoding="utf-8") + _fh.setFormatter( + logging.Formatter( + "%(asctime)s | %(levelname)-8s | %(name)s | %(message)s", + datefmt="%Y-%m-%dT%H:%M:%S", + ) + ) + lg.addHandler(_fh) + lg.propagate = False +last_eval_detail = "" +with sync_playwright() as p: + ctx = p.chromium.launch_persistent_context( + user_data_dir=c["profile_dir"], + headless=False, + channel=c["channel"], + no_viewport=True, + args=["--start-maximized"], + ) + try: + page = ctx.pages[0] if ctx.pages else ctx.new_page() + page.goto(c["url"], wait_until="domcontentloaded", timeout=60000) + if dom_grace > 0: + time.sleep(dom_grace) + if lg.handlers: + lg.info( + "goto_done initial_pages=%s poll_interval_sec=%s dom_grace_sec=%s", + len(ctx.pages or []), + poll, + dom_grace, + ) + while time.time() < deadline: + iter_start = time.time() + try: + still_logged_out, detail = evaluate_logged_out_dom(ctx, page, bundle) + ok = not still_logged_out + last_eval_detail = detail + if lg.handlers: + parts = [] + for tab in list(ctx.pages or []): + try: + href = page_location_href(tab) + pu = (tab.url or "").strip() + parts.append("href=%r playwright_url=%r" % (href, pu)) + except Exception: + parts.append("(tab_read_error)") + lg.debug( + "poll iter_start_elapsed=%.1fs deadline_in=%.1fs tabs=%s poll_interval_sec=%s logged_out=%s ok=%s detail=%s | %s", + iter_start - t0, + deadline - iter_start, + len(ctx.pages or []), + poll, + still_logged_out, + ok, + detail, + " ; ".join(parts) if parts else "no_tabs", + ) + if ok: + interactive_ok = True + if lg.handlers: + lg.info("login_detected_ok %s", detail) + break + except Exception as ex: + if lg.handlers: + lg.warning("poll_exception err=%s", ex, exc_info=True) + spent = time.time() - iter_start + time.sleep(max(0.0, poll - spent)) + if not interactive_ok: + if lg.handlers: + lg.warning("login_poll_exhausted detail=%s", last_eval_detail) + rem = max(0.0, deadline - time.time()) + if rem > 0: + try: + ctx.wait_for_event("close", timeout=rem * 1000) + except Exception: + pass + else: + if lg.handlers: + lg.info("login_success_closing_browser_immediately") + except Exception as e: + if lg.handlers: + lg.exception("login_runner_fatal err=%s", e) + print(e, file=sys.stderr) + raise + finally: + if result_path: + try: + with open(result_path, "w", encoding="utf-8") as rf: + json.dump({"interactive_ok": interactive_ok}, rf, ensure_ascii=False) + except Exception: + pass + try: + ctx.close() + except Exception: + pass +""" + cfg = { + "channel": channel, + "profile_dir": profile_dir, + "url": url, + "timeout_sec": timeout_sec, + "poll_interval": poll_sec, + "dom_grace_sec": dom_grace_sec, + "login_detect_bundle": _login_out_bundle_for(platform), + "log_file": log_file, + } + with tempfile.NamedTemporaryFile( + mode="w", suffix=".json", delete=False, encoding="utf-8" + ) as rf: + result_path = rf.name + cfg["result_path"] = result_path + with tempfile.NamedTemporaryFile( + mode="w", suffix=".json", delete=False, encoding="utf-8" + ) as jf: + json.dump(cfg, jf, ensure_ascii=False) + cfg_path = jf.name + with tempfile.NamedTemporaryFile( + mode="w", suffix=".py", delete=False, encoding="utf-8" + ) as pf: + pf.write(runner) + py_path = pf.name + proc_rc = None + try: + r = subprocess.run( + [sys.executable, py_path, cfg_path], + timeout=timeout_sec + 180, + ) + proc_rc = r.returncode + if proc_rc != 0: + print("⚠️ 浏览器进程异常退出,将仅根据已写入的检测结果更新状态") + log.warning("login_subprocess_nonzero_return code=%s", proc_rc) + except subprocess.TimeoutExpired as ex: + print("⚠️ 等待浏览器超时,将仅根据已写入的检测结果更新状态") + log.warning("login_subprocess_timeout err=%s", ex) + finally: + try: + os.unlink(cfg_path) + except OSError: + pass + try: + os.unlink(py_path) + except OSError: + pass + + interactive_ok = False + try: + with open(result_path, encoding="utf-8") as rf: + interactive_ok = bool(json.load(rf).get("interactive_ok")) + except Exception: + pass + try: + os.unlink(result_path) + except OSError: + pass + + time.sleep(0.5) + ok = interactive_ok + log.info( + "login_finished account_id=%s interactive_ok=%s subprocess_rc=%s marking_db=%s", + target.get("id"), + interactive_ok, + proc_rc, + ok, + ) + _mark_login_status(target["id"], ok) + if ok: + print("✅ 已判定登录成功,状态已写入数据库") + else: + log.warning( + "login_not_detected account_id=%s platform=%s see_log=%s", + target.get("id"), + platform, + log_file, + ) + print("⚠️ 未检测到有效登录,状态为未登录。请关闭其他占用该用户目录的浏览器后重试,或延长 JIANGCHANG_LOGIN_TIMEOUT_SECONDS 后再登录。") + print(f"ℹ️ 详细轮询日志见:{log_file}(可将 JIANGCHANG_LOG_LEVEL=DEBUG 打开更细粒度)") + + +def cmd_open(account_id): + """打开该账号的持久化浏览器,仅用于肉眼确认是否已登录;不写数据库。""" + get_skill_logger().info("open account_id=%r", account_id) + target = get_account_by_id(account_id) + if not target: + get_skill_logger().warning("open_not_found account_id=%r", account_id) + print("ERROR:ACCOUNT_NOT_FOUND") + return + + channel = resolve_chromium_channel() + if not channel: + _print_browser_install_hint() + return + + try: + import playwright # noqa: F401 + except ImportError: + print("ERROR:需要 playwright:pip install playwright && playwright install chromium") + return + + profile_dir = (target.get("profile_dir") or "").strip() + if not profile_dir: + print("ERROR:PROFILE_DIR_MISSING 库中 profile_dir 为空。") + return + os.makedirs(profile_dir, exist_ok=True) + url = (target.get("url") or "").strip() or PLATFORM_URLS.get( + target["platform"], "https://www.google.com" + ) + browser_name = "Google Chrome" if channel == "chrome" else "Microsoft Edge" + print(f"正在打开 [{target['name']}] 的 {browser_name}(仅查看,不写入数据库)") + print(f"地址:{url}") + print("请在本窗口中自行确认登录态。关闭浏览器后命令结束。") + print("需要自动检测并写入数据库时,请执行:python main.py login ") + + runner_open = r"""import json, sys +from playwright.sync_api import sync_playwright +with open(sys.argv[1], encoding="utf-8") as f: + c = json.load(f) +with sync_playwright() as p: + ctx = p.chromium.launch_persistent_context( + user_data_dir=c["profile_dir"], + headless=False, + channel=c["channel"], + no_viewport=True, + args=["--start-maximized"], + ) + try: + page = ctx.pages[0] if ctx.pages else ctx.new_page() + page.goto(c["url"], wait_until="domcontentloaded", timeout=60000) + ctx.wait_for_event("close", timeout=86400000) + except Exception as e: + print(e, file=sys.stderr) + raise + finally: + try: + ctx.close() + except Exception: + pass +""" + cfg = {"channel": channel, "profile_dir": profile_dir, "url": url} + with tempfile.NamedTemporaryFile( + mode="w", suffix=".json", delete=False, encoding="utf-8" + ) as jf: + json.dump(cfg, jf, ensure_ascii=False) + cfg_path = jf.name + with tempfile.NamedTemporaryFile( + mode="w", suffix=".py", delete=False, encoding="utf-8" + ) as pf: + pf.write(runner_open) + py_path = pf.name + try: + subprocess.run([sys.executable, py_path, cfg_path]) + finally: + try: + os.unlink(cfg_path) + except OSError: + pass + try: + os.unlink(py_path) + except OSError: + pass + + +def _mark_login_status(account_id, success: bool): + now = _now_unix() + conn = get_conn() + try: + cur = conn.cursor() + if success: + cur.execute( + "UPDATE accounts SET login_status = 1, last_login_at = ?, updated_at = ? WHERE id = ?", + (now, now, account_id), + ) + else: + cur.execute( + "UPDATE accounts SET login_status = 0, updated_at = ? WHERE id = ?", + (now, account_id), + ) + conn.commit() + finally: + conn.close() + + +def cmd_set_login_status(account_id_str: str, status_str: str) -> None: + """ + 跨技能机器接口:回写 accounts.login_status。 + 成功:stdout 首行 OK:SET_LOGIN_STATUS 或 OK:CLEARED_LOGIN_STATUS,进程退出码 0。 + 失败:stdout 首行 ERROR:...,退出码 1。 + """ + aid_s = (account_id_str or "").strip() + st_s = (status_str or "").strip() + if not aid_s.isdigit(): + print("ERROR:INVALID_ACCOUNT_ID") + sys.exit(1) + if st_s not in ("0", "1"): + print("ERROR:INVALID_STATUS 须为 0 或 1") + sys.exit(1) + aid = int(aid_s) + init_db() + if get_account_by_id(aid) is None: + get_skill_logger().warning("set_login_status_not_found account_id=%r", aid) + print("ERROR:ACCOUNT_NOT_FOUND") + sys.exit(1) + ok = st_s == "1" + _mark_login_status(aid, ok) + get_skill_logger().info("set_login_status account_id=%s success=%s", aid, ok) + print("OK:SET_LOGIN_STATUS" if ok else "OK:CLEARED_LOGIN_STATUS") + + +def _cli_print_full_usage() -> None: + """总览(未带子命令或需要完整说明时)。""" + print("用法概览(将 main.py 换为你的路径):") + print(" python main.py add <平台中文名> <手机号>") + print(" python main.py list [平台|all|全部]") + print(" python main.py pick-logged-in <平台> # 跨技能用:仅已登录账号,一行 JSON 或 ERROR") + print(" python main.py pick-web <平台> # 跨技能用:优先已登录,否则最新一条账号") + print(" python main.py list-json <平台|all> [--limit N] # 跨技能用:一行 JSON 数组,元素同 get") + print(" python main.py get ") + print(" python main.py set-login-status <0|1> # 跨技能回写登录态") + print(" python main.py open ") + print(" python main.py login ") + print(" python main.py delete id ") + print(" python main.py delete platform <平台>") + print(" python main.py delete platform <平台> <手机号>") + print(" python main.py <平台> # 等价于只列该平台") + print(" python main.py <平台> list # 同上") + print() + print("支持的平台(可用下列中文称呼,亦支持英文键如 sohu):") + print(" " + _platform_list_cn_for_help()) + + +def _cli_fail_add() -> None: + print("ERROR:CLI_ADD_MISSING_ARGS") + print() + print("【add 添加账号】参数不足。") + print(" 必填:平台名称(中文即可,如 搜狐号、知乎、微信公众号)") + print(" 必填:中国大陆 11 位手机号(1 开头,第二位 3~9),同平台下不可重复") + print() + print("示例:") + print(" python main.py add 搜狐号 13800138000") + print(" python main.py add 知乎 13900001111") + print() + print("支持的平台:" + _platform_list_cn_for_help()) + + +def _cli_fail_need_account_id(subcmd: str) -> None: + print(f"ERROR:CLI_{subcmd.upper()}_MISSING_ID") + print() + print(f"【{subcmd}】缺少必填参数:账号 id(数据库自增整数,可先 list 查看)。") + print("示例:") + print(f" python main.py {subcmd} 1") + + +def _cli_fail_delete() -> None: + print("ERROR:CLI_DELETE_MISSING_ARGS") + print() + print("【delete 删除账号】参数不足。支持三种方式:") + print(" 按 id:python main.py delete id ") + print(" 按平台(删该平台下全部):python main.py delete platform <平台中文名或英文键>") + print(" 按平台+手机号:python main.py delete platform <平台> <手机号>") + print() + print("示例:") + print(" python main.py delete id 3") + print(" python main.py delete platform 搜狐号") + print(" python main.py delete platform 头条号 18925203701") + print() + print("说明:会同时删除数据库记录与 profile 用户数据目录(若存在)。") + + +if __name__ == "__main__": + if len(sys.argv) < 2: + _cli_print_full_usage() + sys.exit(1) + + _apply_cli_local_dev_env() + get_skill_logger().info("cli_start argv=%s", sys.argv) + cmd = sys.argv[1] + _maybe_print_paths_debug_cli() + + plat_from_first = resolve_platform_key(cmd) + + if len(sys.argv) >= 3 and sys.argv[2] == "list" and plat_from_first: + cmd_list(plat_from_first) + elif plat_from_first and len(sys.argv) == 2: + cmd_list(plat_from_first) + elif cmd == "add": + if len(sys.argv) < 4: + _cli_fail_add() + sys.exit(1) + cmd_add(sys.argv[2], sys.argv[3]) + elif cmd == "list": + cmd_list(sys.argv[2] if len(sys.argv) >= 3 else "all") + elif cmd == "get": + if len(sys.argv) < 3: + _cli_fail_need_account_id("get") + sys.exit(1) + cmd_get(sys.argv[2]) + elif cmd == "set-login-status": + if len(sys.argv) < 4: + print("ERROR:CLI_SET_LOGIN_STATUS_MISSING_ARGS") + print("用法:python main.py set-login-status <0|1>") + print("说明:供 llm-manager 等脚本回写登录态;成功时 stdout 首行以 OK: 开头。") + sys.exit(1) + cmd_set_login_status(sys.argv[2], sys.argv[3]) + elif cmd == "list-json": + if len(sys.argv) < 3: + print("ERROR:CLI_LIST_JSON_MISSING_PLATFORM") + print("用法:python main.py list-json <平台|all|全部> [--limit N]") + print("说明:跨技能编排;成功时 stdout 仅一行 JSON 数组(元素与 get 一致)。") + sys.exit(1) + av = sys.argv[2:] + platform_arg = av[0] + lim = 200 + if "--limit" in av: + i = av.index("--limit") + if i + 1 < len(av): + try: + lim = int(av[i + 1]) + except ValueError: + pass + cmd_list_json(platform_arg, limit=lim) + elif cmd == "pick-logged-in": + if len(sys.argv) < 3: + print("ERROR:CLI_PICK_LOGGED_IN_MISSING_ARGS") + print("用法:python main.py pick-logged-in <平台中文名或英文键>") + print("说明:供 llm-manager 等技能查询该平台已登录账号;成功时 stdout 仅一行 JSON。") + sys.exit(1) + cmd_pick_logged_in(sys.argv[2]) + elif cmd == "pick-web": + if len(sys.argv) < 3: + print("ERROR:CLI_PICK_WEB_MISSING_ARGS") + print("用法:python main.py pick-web <平台中文名或英文键>") + print("说明:供 llm-manager 等;优先已登录账号,否则返回该平台最新一条账号(未登录也可用于打开浏览器登录)。") + sys.exit(1) + cmd_pick_web(sys.argv[2]) + elif cmd == "open": + if len(sys.argv) < 3: + _cli_fail_need_account_id("open") + sys.exit(1) + cmd_open(sys.argv[2]) + elif cmd == "login": + if len(sys.argv) < 3: + _cli_fail_need_account_id("login") + sys.exit(1) + cmd_login(sys.argv[2]) + elif cmd == "delete": + if len(sys.argv) < 4: + _cli_fail_delete() + sys.exit(1) + mode = (sys.argv[2] or "").strip().lower() + if mode == "id": + cmd_delete_by_id(sys.argv[3]) + elif mode == "platform": + if len(sys.argv) >= 5: + cmd_delete_by_platform_phone(sys.argv[3], sys.argv[4]) + else: + cmd_delete_by_platform(sys.argv[3]) + else: + print(f"ERROR:CLI_DELETE_BAD_MODE 不支持的删除方式「{sys.argv[2]}」,请使用 id 或 platform。") + _cli_fail_delete() + sys.exit(1) + else: + print(f"ERROR:CLI_UNKNOWN_OR_BAD_ARGS 子命令「{cmd}」无法识别,或参数组合不合法。") + print() + _cli_print_full_usage() + print() + print("说明:list 的筛选可为 all/全部,或上面列出的任一平台中文名。") + sys.exit(1)