feat(发布计划): 改按名字发(文件名 名字_日期_编号)+ 账号管理整页下线 + 自动切号一轮发完
发布计划不再绑设备、不再依赖账号台账:上传文件名 `名字_日期_编号`(标题行 `标题 || 正文_名字_日期_编号`),运行时读本机当前抖音昵称取计划;靠循环块 「一轮把本机待发的号跑完」自动切号,一直发到发完。 · 解析/配对换键:右锚定认名字(不许含下划线、不许纯数字 —— 不合法直接拒收并说清原因), 删掉三处"手机号必须命中台账"的校验,加索引 ix_video_plan_name_date(启动时补建) · 新步骤三个:读本机账号名字 / 切换账号(**按名字点控件**、切完回读校验、遇登录页停本设备)/ 循环本机待发的号(试过不回头、跑一轮没进展就换号、停止·超时·上限三个干净出口) · 发布骨架改 18 步:读名字 → 校验账号(cmp_source=account_target)→ 推送(**推送前重读昵称核对**) → 抖音流程 → 标记结果;push_release 的 account 默认 name · 账号管理整页下线:删 core/ledger.py(serial_of 迁到 core/device_pool.py)、6 个 /api/ledger*、 「账号」页与 ledger.js、设备池「账号」列、设备端账号块;发布计划升为一级页签; device_account 表与 42 行历史数据保留、仍在备份清单内(**停用不等于删数据**) · 人工入口「按名字找设备」:新增 core/account_probe.py(只读探测、跳过在跑任务的设备) + POST /api/video_plan/push_by_account,没设备登着的号逐条报出来; 推送时把"实际是谁发的"写进 posted_serial / posted_device_name · 通知:新增 task.account.switch_failed / task.account.need_login · schema v12(posted_*)/ v13(device.cur_account_name / cur_name_at) · 文档同步:TASK_DEV / API / DATA_MODEL / ARCHITECTURE / NOTIFY / DEVICE_AGENT / DEPLOY / DEVELOPMENT / README / .env.example(两个抖音控件选择器抓一次填 .env) · 新增 scripts/tests/:6 套用例 + run_all.py(临时库 + 假设备,不碰真实数据、不连真机)
This commit is contained in:
+1
-105
@@ -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, ledger
|
||||
from core import device_battery, device_pool, device_discovery
|
||||
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,110 +445,6 @@ 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