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:
2026-09-24 16:12:58 +08:00
parent e82fd64695
commit 7ce4ae9016
24 changed files with 1455 additions and 62 deletions
+46
View File
@@ -511,6 +511,7 @@ body{background:var(--bg);font-family:var(--body);color:var(--text);font-size:14
{% endif %}
<div class="tabs">
<button class="tab active" data-tab="monitor" onclick="showTab('monitor')">监控</button>
<button class="tab" data-tab="account" onclick="showTab('account')" data-perm="devices">账号</button>
<button class="tab" data-tab="tasks" onclick="showTab('tasks')">任务</button>
<button class="tab" data-tab="logs" onclick="showTab('logs')" data-perm="logs">日志</button>
<button class="tab" data-tab="users" onclick="showTab('users')" data-perm="admin">用户</button>
@@ -581,6 +582,49 @@ body{background:var(--bg);font-family:var(--body);color:var(--text);font-size:14
<div id="jobs-grid" class="jobs-grid"></div>
</div>
<!-- ========== Tab: 账号(台账) ========== -->
<div id="tab-account" class="tab-panel">
<div class="page-title">账号</div>
<div class="page-sub">每台设备上登录的抖音账号(谁在哪台机器上、卡在不在机内、能不能发视频)</div>
<div class="toolbar">
<button class="btn btn-primary" onclick="openLedgerModal('')" data-perm="devices">新增账号</button>
<button class="btn" onclick="openLedgerImport()" data-perm="devices">粘贴导入</button>
<button class="btn" onclick="loadLedger()">刷新</button>
<select class="form-control" id="ledger-device" style="width:140px" onchange="ledgerDeviceChanged()">
<option value="">全部设备</option>
</select>
<input class="form-control list-search" id="search-ledger" placeholder="搜索设备号/账号名/抖音号/手机号...">
<span class="text-muted" id="ledger-stats"></span>
</div>
<table class="table">
<thead><tr>
<th data-sort="device_name" onclick="_lpSort('ledger','device_name')">设备号</th>
<th data-sort="nickname" onclick="_lpSort('ledger','nickname')">账号名称</th>
<th data-sort="douyin_id" onclick="_lpSort('ledger','douyin_id')">抖音号</th>
<th>手机号</th>
<th>注册时间</th>
<th>卡在机内</th>
<th>可发视频</th>
<th>简介</th>
<th>备注</th>
<th>操作</th>
</tr></thead>
<tbody id="tb-ledger"></tbody>
</table>
<div class="list-pager" id="pager-ledger"></div>
<div class="help">
这张台账有三个用处:① 人在机器旁时对照(手机端 Agent 的「身份大字页」也会显示本机账号);
② 任务的「条件判断」可以直接从这里取号(编辑器里把<b>候选值来源</b>选成「本机台账」,
就不用把几十个号手写进去);③ 设备池里能看到每台登记了几个号。<br>
⚠ 台账里的抖音号是<b>纯号</b>(如 <code>35377983067</code>),只当"比对用的候选值"。
<b>不要</b>拿它去填「去重」的身份元素 —— 身份比对的是元素原文
(<code>抖音号:35377983067</code>),格式不同会让去重<b>静默失效</b>。
</div>
</div>
<!-- ========== Tab 2: 任务 ========== -->
<div id="tab-tasks" class="tab-panel">
<div class="page-title">任务</div>
@@ -1284,6 +1328,7 @@ body{background:var(--bg);font-family:var(--body);color:var(--text);font-size:14
<thead><tr>
<th>serial</th><th>名称</th><th>型号</th><th>在线</th><th>状态</th>
<th title="设备指纹(ro.serialno):采集到之后,设备换 IP 才能被自动认领">指纹</th>
<th title="这台设备在「账号」页登记了几个抖音号(点数字看明细)">账号</th>
<th>添加时间</th><th>操作</th>
</tr></thead>
<tbody id="tb-devpool"></tbody>
@@ -1481,5 +1526,6 @@ body{background:var(--bg);font-family:var(--body);color:var(--text);font-size:14
<script src="/static/admin/steplog.js"></script>
<script src="/static/admin/recorder.js"></script>
<script src="/static/admin/dedup.js"></script>
<script src="/static/admin/ledger.js"></script>
</body>
</html>