"""只读类 CLI:list / get / list-json / pick-*。""" import json import sys from am_db import get_account_by_id, get_conn, init_db from am_logging import get_skill_logger from am_platforms import _platform_list_cn_for_help, resolve_platform_key from am_runtime_paths import _runtime_paths_debug_text # 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))