feat(账号): 账号台账(web 页 + 任务取号 + 设备端身份页);fix(剪贴板): 注入通道改走设备端 Agent
一、账号台账(新表 device_account,schema v8) - core/ledger.py:CRUD、Excel 粘贴解析(Tab 分隔 / 表头乱序缺列 / 是·否布尔 / 逐行留痕)、 按范围取号(本机台账 / 全部 / 按设备分组)、设备端账号块、dry_run 预览 - 新「账号」顶级 Tab(权限 devices):列表 + 搜索 + 设备筛选 + 增删改 + 粘贴导入(默认先预览再写,逐行显示 新增/覆盖/跳过/失败 + 留痕) - 设备池加「账号 N」列(0 个显示"未登记"),点开看该设备账号明细 - 任务「条件判断」新增 cmp_source / cmp_group:候选值可直接来自台账, 与手填值**合并(OR)**;取不到号时回落手填值,并把原因写进步骤明细与日志 (否则"永远走 else 分支"而任务照样显示成功,最难查) - 设备端契约:身份页多推 accounts_b64(base64 JSON;默认不含手机号; 超预算按整条丢且绝不算字节切),见 doc/DEVICE_AGENT.md §5.2.1 - 备份/文档红线:TABLE_LABELS 加「账号台账」;DATA_MODEL/API/ARCHITECTURE/DEPLOY/ TASK_DEV/DEVICE_AGENT/DEVELOPMENT/README 同步;顺手补上 DATA_MODEL 漏列的 done_mark ⚠ 台账里的抖音号是**纯号**,只当"比对用的候选值":绝不能拿去填「去重」的身份元素 (身份是元素原文逐字算 key,格式不同会让去重静默失效)。代码与文档都写明了。 二、剪贴板注入通道(修:平台还在调早期的独立 APK) - 改为按顺序尝试:设备端 Agent(com.example.deviceagent/.ClipActivity)→ 旧版独立 ClipInject 兜底;两个都没有时报"需要设备端 Agent"(不再只说 ClipInject) - 读回验证改成**轮询到 3 秒**:透明 Activity 要等窗口拿到焦点才写, 原来只睡 0.5s 会读到上一次的内容 → 误报"写入可能被拒"(实测内容已写入却回失败)
This commit is contained in:
+104
-1
@@ -4,7 +4,7 @@ from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
|
||||
from flask import Blueprint, jsonify, request
|
||||
|
||||
from core import device_battery, device_pool, device_discovery
|
||||
from core import device_battery, device_pool, device_discovery, ledger
|
||||
from core.adb_helper import adb_connect, adb_connect_light
|
||||
from core.logger import get_logger
|
||||
from web.auth import perm_required, PERM_DEVICES
|
||||
@@ -445,6 +445,109 @@ def api_devices_battery_scan():
|
||||
return jsonify({"ok": True, "msg": "采集已启动(约 1-3 秒)"})
|
||||
|
||||
|
||||
# ================== API:账号台账(「账号」页) ==================
|
||||
#
|
||||
# 数据在 device_account 表(见 core/ledger.py);任务侧怎么用它取号见
|
||||
# tasks/generic/task.py 的 `_resolve_cmp_value`(if_el 的 cmp_source)。
|
||||
# ⚠ 台账的抖音号只做「条件判断」的比对值,**不做去重身份**(格式不同会静默失效)。
|
||||
|
||||
@bp.route("/api/ledger")
|
||||
@perm_required(PERM_DEVICES)
|
||||
def api_ledger_list():
|
||||
"""账号列表 + 统计(?device=设备号 &q=搜索 &limit=)。"""
|
||||
device_name = (request.args.get("device") or "").strip()
|
||||
q = (request.args.get("q") or "").strip()
|
||||
try:
|
||||
limit = int(request.args.get("limit") or 500)
|
||||
except ValueError:
|
||||
limit = 500
|
||||
try:
|
||||
rows, stats = ledger.list_accounts(device_name, q, limit)
|
||||
names = ledger.device_names()
|
||||
except Exception as e:
|
||||
_log.warning(f"读取账号台账失败: {e}")
|
||||
return jsonify({"ok": False, "error": str(e)[:120]}), 503
|
||||
return jsonify({"ok": True, "accounts": rows, "stats": stats, "device_names": names})
|
||||
|
||||
|
||||
@bp.route("/api/ledger/by_device")
|
||||
@perm_required(PERM_DEVICES)
|
||||
def api_ledger_by_device():
|
||||
"""按设备分组(设备池显示"这台几个号" + 点开看明细都走它,省一次请求)。"""
|
||||
devices = ledger.by_device()
|
||||
counts = {k: v["count"] for k, v in devices.items()}
|
||||
return jsonify({"ok": True, "devices": devices, "counts": counts})
|
||||
|
||||
|
||||
@bp.route("/api/ledger", methods=["POST"])
|
||||
@perm_required(PERM_DEVICES)
|
||||
def api_ledger_add():
|
||||
"""新增一条账号。"""
|
||||
row, err = ledger.add(request.json or {})
|
||||
if err:
|
||||
return jsonify({"ok": False, "error": err}), 400
|
||||
return jsonify({"ok": True, "msg": "已新增", "account": row})
|
||||
|
||||
|
||||
@bp.route("/api/ledger/<acc_id>", methods=["PUT"])
|
||||
@perm_required(PERM_DEVICES)
|
||||
def api_ledger_update(acc_id):
|
||||
"""改一条账号。"""
|
||||
row, err = ledger.update(acc_id, request.json or {})
|
||||
if err:
|
||||
code = 404 if "不存在" in err else 400
|
||||
return jsonify({"ok": False, "error": err}), code
|
||||
return jsonify({"ok": True, "msg": "已保存", "account": row})
|
||||
|
||||
|
||||
@bp.route("/api/ledger/<acc_id>", methods=["DELETE"])
|
||||
@perm_required(PERM_DEVICES)
|
||||
def api_ledger_delete(acc_id):
|
||||
"""删一条账号(删掉之后任务就取不到这个号了)。"""
|
||||
if not ledger.delete(acc_id):
|
||||
return jsonify({"ok": False, "error": "记录不存在或删除失败"}), 404
|
||||
return jsonify({"ok": True, "msg": "已删除"})
|
||||
|
||||
|
||||
@bp.route("/api/ledger/import", methods=["POST"])
|
||||
@perm_required(PERM_DEVICES)
|
||||
def api_ledger_import():
|
||||
"""从表格粘贴文本批量导入。
|
||||
|
||||
请求:{"text": "设备号\\t手机号\\t…\\nA01\\t…", "delimiter": "\\t",
|
||||
"mode": "skip|overwrite", "dry_run": true}
|
||||
dry_run=true 只解析+判重、**一个字节都不写** —— 界面默认先预览再导入。
|
||||
返回逐行结果(line/action/device_name/douyin_id/nickname/error/warns)+ 汇总。
|
||||
"""
|
||||
data = request.json or {}
|
||||
text = data.get("text") or ""
|
||||
if not text.strip():
|
||||
return jsonify({"ok": False, "error": "粘贴的内容是空的"}), 400
|
||||
delim = data.get("delimiter") or "\t"
|
||||
if delim == "tab":
|
||||
delim = "\t"
|
||||
mode = data.get("mode") or "skip"
|
||||
dry_run = bool(data.get("dry_run"))
|
||||
try:
|
||||
rows, errors = ledger.parse_paste(text, delim)
|
||||
except Exception as e:
|
||||
_log.warning(f"账号台账解析失败: {e}")
|
||||
return jsonify({"ok": False, "error": f"解析失败: {str(e)[:120]}"}), 400
|
||||
if not rows:
|
||||
return jsonify({"ok": True, "counts": {"total": 0, "added": 0, "updated": 0,
|
||||
"skipped": 0, "failed": 0},
|
||||
"rows": [], "errors": errors, "mode": mode, "dry_run": dry_run,
|
||||
"msg": "没有可导入的行"})
|
||||
try:
|
||||
result = ledger.import_rows(rows, mode=mode, dry_run=dry_run)
|
||||
except Exception as e:
|
||||
_log.warning(f"账号台账导入失败: {e}")
|
||||
return jsonify({"ok": False, "error": str(e)[:120]}), 503
|
||||
result["errors"] = errors
|
||||
result["msg"] = ("预览完成(未写入)" if dry_run else "导入完成")
|
||||
return jsonify({"ok": True, **result})
|
||||
|
||||
|
||||
# ================== API:Tailscale 管理(仅管理员) ==================
|
||||
# 通过 Tailscale 官方 API v2 管理 tailnet 设备(列表/改名/授权/密钥不过期/删除/生成 auth key)。
|
||||
# 设备 IP 由 tailnet 自动分配,API 无法修改,列表只读展示。
|
||||
|
||||
Reference in New Issue
Block a user