feat(去重): 跨设备「已做过」账本 —— 同一个号不会做两次 + 「谁做过了」看得见

用户场景(他原话):一台手机登录 5 个抖音号、一共 5 台手机,每个任务只让其中一个
目标号评论;每天跑一次但不知道什么时候跑完,于是"一直重复跑" → 结果
"一个手机还没评论到,一个手机都评论两次了"。

**根因不是"单设备重复",是跨设备没有共享的判断 + 进度不可见。** 所以做两件事:
① 幂等;② 把"谁做过了、还差谁"摆到台面上(不然只能靠重跑确认,而重跑又在制造重复)。

- `core/models.py`:新表 `done_mark`(迁移账本补 v7)。**判据只有 `scope_key` 的
  唯一索引**——多台设备会同时判断"没做过","先查后插"有竞态(两台都插),
  唯一索引 + `INSERT ... ON DUPLICATE KEY`/`INSERT OR IGNORE` 的**受影响行数**才原子。
- `core/dedup.py`(新):`build_key`(`任务|身份|时间桶`)/ `check` / `mark` /
  `list_marks`(带"今天做了几台/几个号"统计)/ `delete_mark` / `clear_job` / `purge_old`。
  自建 app context(照 device_pool 的 `_ctx()`),任务线程/Web/清理都不用关心。
- 任务侧两个部件(**检查在前、记账在后**):
  · `if_el` 新增条件类型 `selector_type="dedup"`:命中=这个身份做过了 → 走 then 分支。
    身份元素在 `ident_type`/`ident_value`(留空 = 用设备 serial,一号一机场景)。
  · 新步骤 `mark_done`「记为已做」(22 种步骤):放动作**成功之后**。
  拆两步的用意:动作失败就不记账,下次重跑还会重试该设备 —— 失败不丢。
- 有效期(`dedup_reset` = day/all/hours)放**任务级**:检查与记账两处各填一份的话,
  填不一致就算出两个 key、去重会**静默失效**,所以强制只配一处(编辑器顶部下拉)。
- 三条防误伤规则(都有测试兜着):
  · 身份读不到 / 身份值过长 → **不去重、当没做过照常执行**。绝不能把"读不到"
    当成空身份——那会让所有设备共用一个 key、第一台记账后其余全被误判成"做过"。
  · `kind='all'`(只做一次)的记录**永不清理**(清了等于语义失效);清理只删 day/hours。
  · 去重的两个易错点在保存时直接告警:身份元素两边不一致、有检查没记账/有记账没检查。
- 「任务 → 去重记录」新子分栏(`static/admin/dedup.js`):统计行 + 明细表 +
  删单条(那个号重跑)/ 清空任务(整批重跑)。接口 3 个(GET/delete/clear,PERM_TASKS)。
- 每日 04:23 清理(挂现有 APScheduler),`TABLE_LABELS` 补中文名(备份覆盖自动派生)。
- AI 建任务草稿校验同步:`dedup` 走自己的规则(要 ident_value、xpath 前缀校验),
  没填身份元素只警告不拦(用设备当身份是合法用法);普通条件空选择器仍然拦。
- 文档:TASK_DEV §4.6(去重专章 + App 内检测的兜底配方与它的三个局限)、
  DATA_MODEL §2.9、API 三个接口、ARCHITECTURE(分层/装配/子分栏/JS 分工/清理)、
  DEPLOY §5.2(15 张表)、步骤数 21→22 全库同步。

自测:单元 + 集成 33 项(**含 8 线程抢同一个身份、恰好一个成功**的原子性断言,
以及"all 记录不被清理""身份读不到不去重""清了能重跑")、
**真机端到端**(cs1 上"检查→动作→记账"跑两遍:第二遍被拦、换 serial 的"另一台设备"
同样被拦、删记录后能重跑)、草稿校验 5 项、GET 冒烟 56 路由 0 个 500。

(注:本分支基于 feat/if-el-multi-value,因为它俩都要改 task.py 的 STEP_TYPES 与
editor.js 的 STEP_LIB 同一区域,分开从 dev 拉必然冲突——这份是超集,合一次两份都进。)
This commit is contained in:
2026-09-24 10:55:29 +08:00
parent 5c88cc6384
commit 876224f876
21 changed files with 803 additions and 54 deletions
+6 -5
View File
@@ -47,11 +47,12 @@
| **设备池** | SQLite 清单 + adb 在线状态;支持手工添加、网段自动发现、一键重连、型号采集、启用/停用 |
| **电量监控** | 后台每分钟 `dumpsys battery`(只读)采一次电量,**大屏卡片**与**监控页设备列表**都显示(按档位着色 + ⚡ 充电中);低于阈值推 webhook 告警(**充电中不报、"插着却没充电"照报**,掉档/恢复各报一次,不刷屏)。电量只放内存不落库(见 [NOTIFY.md](doc/NOTIFY.md) §3.1) |
| **任务调度** | 手动 / cron 定时 / 定时启停;运行窗口;失败重试(含端口耗尽类的长退避) |
| **步骤编辑器** | 可视化拖拽编排 21 种步骤(含循环/条件/OCR/通知/录制回放),可打包成"自定义动作"复用 |
| **步骤编辑器** | 可视化拖拽编排 22 种步骤(含循环/条件/OCR/通知/录制回放),可打包成"自定义动作"复用 |
| **拟人化操作** | 滑动默认走弧线轨迹、位置/幅度/时长每次抖动,且**每台设备有自己的手速与习惯**(同设备风格稳定、设备之间明显不同)——批量跑时不像同步机器人(见 [TASK_DEV.md](doc/TASK_DEV.md) §8.4) |
| **录制回放** | 「录制手势」步骤 = **纯录制**:**用手指在真机上划**(读 getevent 真触屏)或在网页画面上拖,把轨迹点列原样存下来,回放时**照原路径与时长重放**——不套滑动那套方向/幅度/拟人参数 |
| **动作配置 / 录制** | 「任务 → 动作配置」:① 配**步骤默认值**(新建步骤预填:滑动时长/幅度/抖动/拟人、点击超时、等待区间…)② **录制动作**——在设备画面上点/划/按键,自动翻译成步骤(点元素记选择器、划一下记方向/幅度/时长),可存成可复用动作,也可只取一个手势回填滑动步骤 |
| **公共巡检** | 任务级的守护条件(**独立于步骤画布**):每 N 秒检查屏幕亮/熄、元素在不在、前台是不是某 App,命中就点亮/息屏/停本设备/推通知——通知标题正文自己写(见 [TASK_DEV.md](doc/TASK_DEV.md) §4.5) |
| **去重账本** | 解决"**同一个号被做两次、有的号还没做**":任务里用「条件判断→去重」+「记为已做」,把"这个身份做过了"记进一张**所有设备共享**的账本——多台手机、反复重跑都不会重复做同一个号。「任务 → 去重记录」页能看"谁做过了、还差谁",也能删记录让它重跑(见 [TASK_DEV.md](doc/TASK_DEV.md) §4.6) |
| **元素抓取** | 拉取设备 UI 元素树 → 点选回填选择器;支持"点一下"与"测选择器"真机验证 |
| **实时看屏** | MJPEG 实时流 + 点击/滑动/按键/文字输入;全屏监控大屏(`/wall`) |
| **应用管理** | APK 上传/解析/批量安装;设备已装应用与版本查询;剪贴板注入 |
@@ -157,7 +158,7 @@ auto_control/
├── tasks/ # 任务定义层
│ ├── base.py # BaseTask + _TASK_TYPES + register_task
│ └── generic/ # 通用步骤任务(task_type=generic_steps,当前唯一类型)
│ └── task.py # STEP_TYPES(21 种步骤)+ Worker + 执行器
│ └── task.py # STEP_TYPES(22 种步骤)+ Worker + 执行器
│
├── mcp_server/ # MCP Server(20 个 de_* 工具,:8033)
│ ├── mcp_server.py # 工具定义 + 平台登录 + 门控
@@ -283,14 +284,14 @@ self.set_progress(done=5, total=80, unit="视频", action_counts={"like": 3}, el
`generic_steps` 的执行内容全在 `params.steps`(JSON 数组),由步骤编辑器产出。**没有默认步骤**——空步骤任务执行时会明确报错。
**21 种步骤**(完整参数见 [doc/TASK_DEV.md](doc/TASK_DEV.md)):
**22 种步骤**(完整参数见 [doc/TASK_DEV.md](doc/TASK_DEV.md)):
| 类别 | 步骤 |
|------|------|
| 屏幕 | `screen_on` 亮屏 · `screen_off` 息屏 · `keep_screen` 保持亮屏 |
| 应用与输入 | `open_app` 打开 App · `stop_app` 结束 App · `input_text` 输入文字 · `clipboard` 剪贴板注入 · `key_event` 按键 |
| 交互 | `click` 点击元素 · `click_xy` 点击坐标 · `long_click` 长按 · `swipe` 滑动 · `swipe_until` 滑动直到元素出现 · `wait_el` 等待元素 · `wait` 等待时长 |
| 流程 | `loop` 循环块 · `group` 动作组 · `if_el` 条件判断(元素或 **OCR 识别**) |
| 流程 | `loop` 循环块 · `group` 动作组 · `if_el` 条件判断(元素 / **OCR 识别** / 屏幕状态 / 前台App / **去重**)· `mark_done` 记为已做(配套去重) |
要点:
@@ -405,7 +406,7 @@ read_log_text()`,`file` 参数走白名单,不接受任意路径。
| [doc/ARCHITECTURE.md](doc/ARCHITECTURE.md) | 架构详解(分层、装配顺序、线程模型、设备生命周期、调度链路、设计决策) |
| [doc/DATA_MODEL.md](doc/DATA_MODEL.md) | 数据模型(表结构、迁移、`app_meta`、数据目录、备份覆盖清单) |
| [doc/API.md](doc/API.md) | HTTP 接口全量说明 |
| [doc/TASK_DEV.md](doc/TASK_DEV.md) | 任务与步骤开发(21 种步骤、公共巡检、录制回放、选择器、自定义动作) |
| [doc/TASK_DEV.md](doc/TASK_DEV.md) | 任务与步骤开发(22 种步骤、公共巡检、录制回放、去重、选择器、自定义动作) |
| [doc/MCP.md](doc/MCP.md) · [doc/MCP_DESIGN.md](doc/MCP_DESIGN.md) | MCP 使用手册 / 设计文档 |
| [doc/AI_CONSOLE.md](doc/AI_CONSOLE.md) · [doc/AI_TASK_GEN.md](doc/AI_TASK_GEN.md) | AI 控制台机制 / AI 建任务设计(规划) |
| [doc/DEPLOY.md](doc/DEPLOY.md) | 部署与运维(含生产容器、备份导入、故障排查) |