"""账号用例:list / get / add / delete / pick-web(含终端输出,供 CLI 调用)。""" import json import os import shutil import sys import time from db.accounts_repo import ( count_platform_accounts_conn, default_name_for_platform, delete_account_by_id_conn, delete_account_platform_phone_conn, delete_accounts_by_platform_conn, fetch_delete_row_by_id_conn, fetch_ids_for_list_json, fetch_ids_profile_for_platform_conn, fetch_list_rows, fetch_pick_web_candidate_id, fetch_row_platform_phone_conn, get_account_by_id, has_duplicate_phone_conn, insert_account_row, normalize_account_id, ) from db.connection import get_conn, init_db from util.logging_config import get_skill_logger from util.platforms import ( PLATFORM_URLS, _PLATFORM_PRIMARY_CN, _is_valid_cn_mobile11, _normalize_phone_digits, _platform_list_cn_for_help, resolve_platform_key, ) from util.runtime_paths import _runtime_paths_debug_text, get_default_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_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 rows = fetch_list_rows(key, int(limit)) 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, 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"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)) ids = fetch_ids_for_list_json(key, lim) 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_web(platform_input: str): """ 供 llm-manager 等:取该平台用于网页自动化的账号候选。 按 updated_at、created_at 倒序选一条,与库内「登录态」无关;实际是否已登录由 wait-login / 页面 DOM 判定。 成功: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() picked = fetch_pick_web_candidate_id(key) if picked is None: print("ERROR:NO_ACCOUNT 该平台在账号库中没有任何记录,请先执行 add。") return acc = get_account_by_id(picked) 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 = int(time.time()) phone_store = phone_norm conn = get_conn() try: if has_duplicate_phone_conn(conn, key, phone_store): log.warning("add_duplicate platform=%s phone_suffix=%s", key, phone_store[-4:]) print( f"ERROR:DUPLICATE_PHONE_PLATFORM 该平台下已存在手机号 {phone_store},请勿重复添加。" ) return next_idx = count_platform_accounts_conn(conn, key) + 1 name = default_name_for_platform(key, next_idx) profile_dir = get_default_profile_dir(key, phone_store, None) new_id = insert_account_row(conn, name, key, phone_store, profile_dir, url, now) 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 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: row = fetch_delete_row_by_id_conn(conn, int(aid)) 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 delete_account_by_id_conn(conn, 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: rows = fetch_ids_profile_for_platform_conn(conn, key) if not rows: log.warning("delete_by_platform_empty platform=%s", key) print("ERROR:NO_ACCOUNTS_FOUND 该平台下没有账号记录。") return delete_accounts_by_platform_conn(conn, 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: row = fetch_row_platform_phone_conn(conn, key, phone_norm) if not row: print("ERROR:ACCOUNT_NOT_FOUND 该平台下无此手机号。") return rid, name, profile_dir = row delete_account_platform_phone_conn(conn, 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)