381 lines
14 KiB
Python
381 lines
14 KiB
Python
"""账号用例:list / get / add / delete / pick / set-login-status(含终端输出,供 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_logged_in_id,
|
||
fetch_pick_web_candidate_id,
|
||
fetch_row_platform_phone_conn,
|
||
get_account_by_id,
|
||
has_duplicate_phone_conn,
|
||
insert_account_row,
|
||
mark_login_status,
|
||
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,
|
||
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))
|
||
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_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()
|
||
picked = fetch_pick_logged_in_id(key)
|
||
|
||
if picked is None:
|
||
print("ERROR:NO_LOGGED_IN_ACCOUNT 该平台暂无已登录账号(login_status=1)。")
|
||
print("请先 list 查看账号 id,再执行:python main.py login <id>")
|
||
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_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()
|
||
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)
|
||
|
||
|
||
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")
|