butubb
|
7ce4ae9016
|
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 会读到上一次的内容 → 误报"写入可能被拒"(实测内容已写入却回失败)
|
2026-09-24 16:12:58 +08:00 |
|
butubb
|
876224f876
|
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 拉必然冲突——这份是超集,合一次两份都进。)
|
2026-09-24 10:55:29 +08:00 |
|
butubb
|
621d48c800
|
feat(日志): 任务步骤明细表 + 「日志 → 步骤明细」面板(按设备/任务/时间过滤、按运行归组、导出 CSV)
文本日志只能 grep,"这台设备这次运行为什么失败"翻起来很费劲。新增一张
**结构化**的步骤明细表,把"哪一步、什么类型、哪个选择器、结果、耗时"落库。
- core/models.py:新增 task_step_log(run_id/job/设备/step_path/selector/
result/detail/duration_ms),索引 run_id、created_at、(serial,created_at)、
(job_id,created_at);
- core/step_log.py(新):**专用写线程 + 有界队列**批量落库——步骤执行是热路径,
任务线程只 put_nowait(实测 9000 次入队 31ms),队列满丢弃并计数,绝不阻塞;
另有保留期清理(默认 14 天,每日 04:13 + 每次启动);
- tasks/generic/task.py:_exec_one 记一条(异常=error / handler 返回 False=miss /
未知类型=unknown / 概率未触发=skip);_exec_steps 维护路径栈得到 "2.1.3"
这样的嵌套位置;**单次运行封顶 2000 条**——forever 循环任务否则会写爆表;
- 运行上下文 ctx(run_id/job_id/job_name/device_name)由 TaskManager 生成,
经 create_worker(serial, params, ctx=None) 传入 worker(扩展点向后兼容);
- 接口:/api/step_logs(过滤+分页)、/runs(按运行归组)、/filters(下拉选项)、
/download(CSV,带 BOM);
- 前端:日志页拆成「文件日志 / 步骤明细」子分栏 + static/admin/steplog.js。
红线:新表自动进备份覆盖清单(SUMMARY_TABLES 由元数据派生),已补
TABLE_LABELS 中文标签,导出实测 14 张表、coverage_missing 为空。
文档:DATA_MODEL §2.8/§1、API §10.2、ARCHITECTURE §1.1/§2.2/§3.1、
DEVELOPMENT §5.2 与红线表、DEPLOY §5.2、TASK_DEV §8.3、README。
|
2026-09-16 08:58:43 +08:00 |
|
butubb
|
97cec6e211
|
feat(通知): 系统级 Webhook 通知子系统(事件目录 + 可插拔适配器 + 防刷屏)
平台此前出了问题只能靠人盯页面。现在各组件统一走 `notifier.notify(事件, **字段)`,
推到企业微信 / 自建服务;**所有可通知点都登记进事件目录,默认全关,用户按 webhook 勾选**。
## 架构(`core/notifier.py` + `core/notify_events.py`)
业务线程调 `notify()` → 只做内存操作(读配置快照/匹配订阅/入队)→ 返回;
后台 1 个 dispatcher(聚合 + 每 hook 限流 + 折叠摘要)+ 3 个 sender(真实 HTTP、退避重试)
负责真正发出去。硬约束:**notify 零 DB、零 HTTP、零阻塞、异常不冒泡**——所以任务线程里
可以直接调(不用 app_context、不用 try/except),但**必须放在所有 `with self._lock` 之外**。
- **事件目录 36 条**(任务批次/单设备/设备/Worker/业务/安装/系统/AI),支持 `task.*` 通配订阅;
语义分工避免重复告警:`worker.*` 是单次尝试级,`task.device.success/failed` 是唯一权威结论。
- **适配器可插拔**:`wecom`(markdown,按 4096 **字节**截断、超限不截半个汉字)+
`json`(模板占位符,替换值按 JSON 转义,保存前干跑校验);钉钉/飞书留了插槽(前端置灰)。
- **防打爆四层**:聚合窗口(默认 30s,同批次合并成一条并带样本)→ 令牌桶限流(默认 18/分,
对齐企微硬限 20)→ 被限流的**折叠成摘要不丢弃** → 有界队列背压。取舍:失败通知最多延迟
一个窗口,换来群不被刷屏。
## 安全与存储
- 配置只落 `app_meta.notify_webhooks` 一个键(**不建表** → 不涉及备份覆盖红线)。
- URL 本身就是凭据(企微 `?key=`)→ 接口回显/发送记录/日志一律 `mask_url()/scrub()`;
编辑时留空即不修改;secret 永不回显。DATA_MODEL 的明文凭据告警补上了这一条。
- 发送记录:内存环形缓冲 200 条(重启清空)+ 独立 `logs/notify.log`。
## 接入点(每个都放在锁外、不改 return 顺序)
task_manager(批次开始/结束用新增的 `_BatchTracker` 统一在 finally 计数、单设备成功/失败/
离线/重试/停止/抢占/归还/cron 停止)、device_worker 心跳看门狗、generic 任务选择器连续失效、
apk 安装开始/完成、设备上下线(**状态沿检测**,只报新变化)、备份导出/恢复、经验巡检、
用户登录、服务启停。
## 前端
系统 Tab 新增「通知 / Webhook」子分栏:多条 webhook 列表(URL 打码)+ 编辑弹窗(格式/URL/
密钥/事件勾选树带 ★建议/聚合/限流/自定义模板/预览)+ 发送测试 + 发送记录。
## 自测
- 进程内逻辑 10 组断言全绿:聚合合并、限流+折叠、无配置/全局关静默丢弃、未知事件、
内部异常不外泄、JSON 转义(标题含引号换行仍合法)、URL/异常消息脱敏、配置校验。
- 端到端(假 webhook 接收端)17 项断言全绿:真实事件投递(user.login / task.batch.no_device)、
企微请求体形状、**HTTP 200 + errcode 93000 判为失败**、500 重试 3 次、记录里 URL 打码。
- 韧性:webhook 指向黑洞地址时登录耗时 100~114ms(基线 107~133ms,**异步隔离生效**);
配置写成坏 JSON 服务照常启动、通知静默不发、日志有 error(服务端实测后已复原)。
- 页面:系统 → 通知 面板/弹窗/36 个事件复选框/预览全部正常,无 JS 报错。
- 自测数据已清理(webhook、自建任务、写坏又复原的配置键)。
文档:新增 doc/NOTIFY.md(事件表/配置/格式约束/防刷屏/加事件三步骤/排障)并登记进 doc/README;
API.md §2.11;DATA_MODEL 的 app_meta 键表与明文凭据告警;ARCHITECTURE 线程表/分层/扩展点;
根 README 功能索引与日志表。
|
2026-09-15 13:55:14 +08:00 |
|
butubb
|
dad1af8b0b
|
feat: 设备端应用商店(平台侧)—— Agent 直连拉清单/下载/安装,绕开 MIUI 的 USB 安装拦截
背景:`adb install` 在 MIUI 上被「USB 安装」拦下(USER_RESTRICTED),必须人工点确认;
而**由设备上的 App 自己调系统安装器**走的是普通应用安装流程,不触发那道拦截。
平台侧实现(设备端 Agent APK 在另一个仓库,契约见 doc/DEVICE_AGENT.md):
- web/device_agent_api.py(新):
* 设备侧 `/api/device/agent/{bootstrap,apk/<id>,report}` —— 无登录会话,靠
`X-Device-Token`(常量时间比对)鉴权;`X-Device-Fingerprint` 让平台认出是哪台设备
(换 IP 也认得出,并把平台分配的名称回给它);**默认关闭,关闭时统一 404**,
不暴露接口存在性;只读清单与文件,不返回任何配置/密钥
* 管理侧 `/api/agent-store/{config,logs}` —— 登录 + 应用管理权限;启用时自动生成令牌,
可一键重置(旧令牌立即失效)
- core/models.py:新增 `device_install_log`(下载/安装记录,自动裁剪保留 500 条)
- 应用管理页新增「📱 设备端应用商店」面板:开关 / 接入地址 / 令牌(复制·重置)/ 安装记录;
面板里写明它与「平台批量推送」的分工
- doc/DEVICE_AGENT.md(新):**两个仓库共享的接口契约** —— 接入流程、三个设备接口的
完整规格与错误码、adb 指令协议(剪贴板/身份显示/配置下发)、版本兼容约定、安全须知;
API.md、DATA_MODEL.md、doc/README.md 索引同步
验证:
- 后端 9 组用例:默认关闭 404、无/错令牌 401、指纹认出设备名、清单、下载、上报与失败
原因落库、关闭后重新 404、重置令牌后旧令牌立即失效、未登录管理端被拒 —— 全通过
- 真实浏览器:面板渲染、点开关 → 自动生成令牌 → 设备端带令牌拉到清单且 device.name=A08、
无 JS 报错;测试后已把开关还原为关闭
- 全量回归 105 项通过(42 GET + 62 写探测 + 7 关键业务)
|
2026-09-13 14:40:38 +08:00 |
|
butubb
|
4899e66c68
|
refactor: 备份/恢复改为「SQLite 归档 + 单事务整库替换」——不再换文件,也不再依赖 SQLite 运行时
SQLite 从"运行时数据库"降级为"备份交换格式":导出把当前库(MySQL 或 SQLite)
整库写成一份 SQLite 归档,恢复则是启动时在单个事务里 DELETE + INSERT。
前端三个接口的契约一行未改。
- core/system_backup.py 重写:
* dump_to_sqlite()(旧名 snapshot_db 保留为别名)取代在线备份 API:MySQL 下把
这条连接提到 REPEATABLE READ,保证 12 张表读的是同一时刻
* zip 结构不变(users.db + manifest.json + apks/),既有老备份继续可导入;不依赖
任何外部二进制(sqlite3 是标准库,python:slim 容器里现成)
* manifest 增加 db_backend / deployment_env / source_db_id,用于识别备份来源环境
* apply_restore:安全网从裸 pre_restore_*.db 升级为 pre_restore_*.zip(可直接再导入
回滚);跨环境导入默认硬停(需 force_env_mismatch=true)
* consume_pending_restore:启动时单事务整库替换,任何一步失败 rollback,当前数据
完好(比换文件更安全);坏归档挪 restore_failed_* 且不阻塞启动
* 归档库收尾时 checkpoint 回单文件模式并清掉 -wal/-shm(zip 只打包主文件)
- web_server.py:consume 从 init_db 之前移到之后(现在是事务替换,需要表与 app context)
- config.py:BACKUP_DIR / RESTORE_STAGING_DIR / RESTORE_PENDING_DIR 支持环境变量覆盖
—— 自动化测试必须把恢复目录指到临时位置,否则测试造的"待生效恢复任务"会在服务
下次重启时被当成用户的操作消费掉
- core/db_config.py:SQLite 回退模式也写库环境标签(备份要能标出来源环境)
- web/system_api.py + static/admin/system.js + templates/admin/monitor.html:
预览显示来源/当前环境,跨环境时出现「允许跨环境导入」勾选项;文案同步
验证(隔离副本,绝不碰真实库):导出→zip 结构与 manifest 正确;预览带来源环境与
告警;应用→生成 pre_restore zip + 归档就位;跨环境 apply 被拒;模拟重启→数据回到
导出时刻、经验库/动作库完好;塞入坏归档→挪 restore_failed_*、服务照常启动、数据未变。
|
2026-09-13 10:34:58 +08:00 |
|
butubb
|
c834349fac
|
refactor: schema 收敛到 ORM metadata——5 张裸表升模型,方言无关的建表/补列/唯一索引
原来建表有三套来源(ORM create_all + 自建 SCHEMA_MIGRATIONS + agent_api 里的裸
CREATE TABLE)。这在 SQLite 下能活是因为 SQLite 的 DDL 极宽容;换 MySQL 后
agent_* 四张表的 `INTEGER PRIMARY KEY AUTOINCREMENT`、`TEXT DEFAULT ''`、
`TEXT PRIMARY KEY` 会全部建不出来,而失败被 `except: pass` 吞掉 —— 表现为
「经验库/动作库/会话功能静默失灵、日志里什么都看不到」。本期把它收敛成一套。
- core/models.py:
* 新增 5 个 ORM 模型:AppMeta / AgentExperience / ExperienceAudit /
AgentAction / AgentConversation(列名沿用历史,`app_meta.key` 在 MySQL 里是
保留字,ORM 属性名用 k,读写统一走 db_config.meta_get/meta_set)
* 新增 _long_text()(MySQL 用 MEDIUMTEXT,裸 TEXT 只有 64KB)与 _DOUBLE()
(评分别用单精度 FLOAT);_migrate_schema 不再执行 DDL,只维护版本账本,
SCHEMA_MIGRATIONS 的历史建表/加列条目 SQL 置 None
* 新增 _sync_columns():用 sqlalchemy.inspect 比对模型与实表补缺列,取代原来
靠 "duplicate column name" 报错文本判断的写法(换方言就失效)
* _ensure_unique_indexes() 按方言分叉:MySQL 5.7 没有过滤索引,改用
「虚拟生成列 + 唯一索引」复刻"空值不参与唯一约束"的语义
- web/agent_api.py: 删 4 段裸 CREATE TABLE;_ensure_* 收敛为 _ensure_tables()
(db.create_all 薄封装,失败必记日志);app_meta 读写与 INSERT OR IGNORE
改为方言中立
- core/device_discovery.py: app_meta 读写改用同一助手
- core/system_backup.py: SUMMARY_TABLES 由 db.metadata 派生 —— 备份覆盖红线
从"靠人记"变成结构上不可能漏;CURRENT_SCHEMA_VERSION 改从 models 导入
验证(均在 users.db 的一致快照副本上,不碰真实库):
空库 → 自建 12 张表 + 默认管理员 + 唯一索引 + schema_version=6;
老库 → 逐表行数与真实库完全一致,app_meta 键未变;
接口 → health/devices/jobs/groups/agent(经验库/动作库/会话) 全 200,
会话增删与备份导出(12 张表、无覆盖缺失)正常。
|
2026-09-13 10:30:15 +08:00 |
|
butubb
|
d40c867d6f
|
fix: 备份覆盖补全(动作库/系统配置入清单)+ 覆盖自检与未登记表告警;发布流程/红线文档更新
问题(2026-09-10 用户反馈):动作库"没有备份"。实测导出 zip 里 agent_action 数据其实在
(导出是 users.db 全库快照),但清单/预览没列它 → 看起来像没备份。同类还有 app_meta。
修复(core/system_backup.py):
- SUMMARY_TABLES 补 agent_action(动作库) 与 app_meta(系统配置) + 中文标签;
- 导出侧**覆盖自检**:登记表若在快照缺失 → manifest.coverage_missing + 日志告警;
- 导入侧**反向自检**:备份含未登记表 → 预览告警提示登记(extra_tables);
- 顶部注释写明新增持久化表必须登记(红线)。
其余:
- monitor.html:数据备份面板文案改为明列全部业务表 + 指引(预览见表行数 / 新增表须登记);
- doc/DEPLOY.md §3.5:新增「备份覆盖清单(红线)」小节(含清单与两侧自检说明);
- doc/DEVELOPMENT.md:§5.6 增「备份覆盖红线」;§6 发布流程重写为分支流程
(本机建分支 → 用户确认 → 合 dev → dev 整体就绪 → 合 main → 220 部署,附部署命令);
- doc/ARCHITECTURE.md §3.6:补备份覆盖登记提示。
实测:导出清单 12 表(含动作库 3 行、系统配置 8 行),coverage_missing 空;上传预览同样显示、
extra_tables 空。
|
2026-09-10 21:03:41 +08:00 |
|
butubb
|
55b1b74944
|
feat: 系统数据备份导出/导入后端——sqlite 在线快照 + apk 打包导出;上传校验预览→应用(自动快照当前库)+ 重启生效
导出:sqlite3 在线备份 API 对 data/users.db 做一致快照 → zip(users.db +
manifest.json:schema_version/逐表行数/apk 清单)+ 可选 data/apks/*.apk。
导出文件读入内存(BytesIO)发送后即删磁盘副本,避免 Windows 流式句柄锁残留。
导入:上传 zip/db → 暂存校验(integrity + 必需表 app_meta/user/task_job/
device_group + schema 版本提示)→ 确认应用:先自动快照当前库到
data/backups/pre_restore_*.db,再把备份落到 data/restore_pending/,由
web_server.py 在 init_db 之前 consume 换库——TaskManager 启动时才读库入内存、
Windows 不能热替换正被持有的库文件,故导入必须重启生效。
新增 web/system_api.py(export/preview/apply,全 @admin_required)与目录常量
BACKUP_DIR/RESTORE_STAGING_DIR/RESTORE_PENDING_DIR;.gitignore 排除运行产物。
|
2026-09-09 14:49:40 +08:00 |
|