286 lines
9.5 KiB
Python
286 lines
9.5 KiB
Python
"""只读类 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 <id>")
|
||
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))
|