butubb
f660b1031c
Merge branch 'feat/if-el-text-compare'——条件判断支持「拿选中元素的文本和设定的值比」(等于/不等于/包含/不包含)
2026-09-24 10:07:24 +08:00
butubb
34a03db1b9
feat(条件判断): 支持「选中元素 → 拿它的文本和设定的值比」(等于/不等于/包含/不包含)
...
需求原话:条件判断需要能选择元素,比如我设置了一个抖音号,让它采集这个元素的
文本和我设的值做条件判断。
原来的条件判断只会判「元素在不在」(找到走 then、没找到走 else),没法判"内容对不对"。
现在加两个可选参数:
- `cmp_op`:等于 / 不等于 / 包含 / 不包含(`==`/`!=`/`contains` 这类别名也认);
留空 = 老行为,**完全向后兼容**(现有任务不用改)。
- `cmp_value`:要比对的值。
填了就变成:先按选择器取到元素文本 → 和 cmp_value 比 → 比对结果才是命中与否。
用户那个例子就是:元素 `com.ss.android.ugc.aweme:id/506` 的文本是
`抖音号:35377983067`,选「包含」、值填 `35377983067` 即可;
要抓"登录的不是这个号"就用「不等于」。
- 读取只走 `get_text()`(只读,2 秒超时),不点击、不改状态。
- **两条防误判规则**(都写进文档了):
· 元素**没找到**时一律算未命中(走 else)——"没读到"绝不能被当成"和我设的不一样",
否则界面还没加载出来就会误判成"账号被换了";
· 元素在但文本为空时按空文本参与比对(用于抓"栏位是空的"),日志会打出实际读到的值。
- 比对**不污染「选择器健康」统计**:统计用的仍是"元素在不在"——"找到了但值不对"
是条件按预期走了 else,不是选择器失效,不该攒出「连续未命中」告警。
- 支持比对的类型:所有元素类选择器 + `foreground`(比前台包名)+ `ocr`(比 OCR 命中的那段字)。
`screen`(亮/熄)没有文本可比,设了会忽略并告警。
- 前端:条件判断面板多一块「文本比对」(运算符下拉 + 比对的值),
只在元素/OCR/前台App 条件下出现;保存前校验"选了运算符却没填值"和"screen 上设了比对"。
- AI 建任务草稿校验(`core/task_draft.py`)同步拦"运算符拼错""有运算符没值"——
执行器对认不出的运算符是"一律不命中",不拦就会变成悄悄走 else。
自测:比对函数 16 项(含别名、空值、None、拼错运算符)、`_exec_if_el` 全分支 20 项
(含"没找到+不等于"这个关键边界、健康统计用存在性、screen 忽略比对、u2 抛异常不炸)、
**真机验证**(cs1 上真读一个 TextView 的文本再比对:包含/等于/不等于 全对,
元素不存在时不误判)、草稿校验 5 项、GET 路由冒烟 55 个 0 个 500。
(测试里踩到一个坑记一笔:u2 的 `XPathSelector.exists` 是纯 bool 属性,而
`UiObject.exists` 是"可调用的 Exists 对象"(`bool()` 即时判断、`exists(timeout=)` 带等待),
两套语义不能想当然统一——单测的假设备一开始没模拟对,是测试写错了不是代码写错了。)
2026-09-24 09:22:32 +08:00
butubb
bf432b4f56
Merge branch 'fix/device-name-display'——选择设备与所有通知显示设备名(不再显示 IP)+ 修 /locate 500/XSS
2026-09-24 09:17:15 +08:00
butubb
8fdde978ea
fix(设备名): 选择设备与所有通知都显示名称而不是 IP
...
用户反馈:① 元素抓取的选择设备列表显示的是 IP;② 所有 webhook 通知里都是 IP;
要都显示设备名。
- 通知链路统一补名字(调用点不用动):`core/notifier.py`
· 新增 `set_device_name_resolver()` + `fill_device_names()`,在 `notify()` 与
`build_message()`(预览/测试发送也走它)里把 `serial` 补成 `device_name`、
把 `serials`/`devices` 列表逐项换成名字。
· 补一处就全带名字了——标题主体、字段表、聚合样本认的都是 `device_name`。
· **做成"纯内存回调"是刻意的**:notify() 的硬红线是零 DB,不能为了取个名字去查库
(那等于在业务线程里加一次阻塞查询)。
· 降级规则:调用点自己传了 device_name 就用它的;查不到名字(没命名/不在池里)
保留原地址;解析器缺失或抛异常都只是降级,绝不影响发送。
- `core/device_pool.py`:维护 `serial → 名称` 内存快照——`init_app` 同步刷一次、
增删改名/迁址后各刷一次(改名立刻生效)、`device-names` 线程每 60s 兜底刷一次
(覆盖整库恢复这类进程外改动)。`name_of()` 只读内存,可在通知路径上安全调用。
- `web_server.py`:装配层接上 `notifier.set_device_name_resolver(device_pool.name_of)`。
- 元素抓取/测试此步骤的设备列表:`GET /api/uiauto/devices` 的 `name` 改用**平台名**,
前端 `editor.js` 新增共用的 `_devCard()`——名字做主标题(粗体),
`型号 · 地址` 作副标题。
· 池外设备**退回地址而不是 uiautodev 的 name**:实测那份 name 是设备 codename
(一柜子机器全叫 "earth"),拿它认设备等于没名字,地址至少唯一。
· 「测试此步骤」的设备列表原来连状态角标都没有,一并统一成同一个卡片。
- 顺带修一个**既有 bug(不是本次需求)**:`/locate` 设备端定位页从 ce47a5b 那次
web 蓝图拆分起就一直 500——拆分时漏掉了 `render_template_string` 与
`markupsafe.escape as _esc` 两个 import。后者不只是缺个名字:定位页把 query 里的
serial 拼进 HTML,而 `render_template_string` 的模板名是 "<template>"、
**不会自动转义**,所以那还是个反射 XSS。已显式转义(已用 `<img onerror>` 验证)。
自测:device_pool 快照/改名即时生效、fill_device_names 全分支(含列表、
未命名保留地址、不在池保留地址、解析器缺失/抛异常降级)、真消息渲染断言
**通篇不含 IP**;GET 路由冒烟 54 个 0 个 500;前端语法 + 卡片渲染截图核对。
2026-09-24 08:55:10 +08:00
butubb
beb51637dd
feat(设备): 电量显示(大屏 + 监控页)+ 低电量 webhook 告警
...
需求:大屏和监控页都要能看到每台设备的电量,并且"低于多少电量就发通知"。
数据从哪来:`adb -s <serial> shell dumpsys battery`(**只读**,实测 0.2~0.35s/台,
11 台并发一轮 0.44s)。设备只要在 `adb devices` 里就说明 transport 现成,
**不需要 connect**——所以连空闲设备也能查。这是本模块敢每轮扫全量的前提,
也是这条只读路径与"空闲设备不主动 connect"红线(DEVELOPMENT.md §2 #3)的边界,
已写进文档免得后人误加 connect。
- core/device_battery.py(新):后台 daemon 线程 `device-battery`(照 device_discovery
的 init_app/shutdown 模式),默认 60s 一轮,采「设备池 ∩ 在线」(与 list_ready 同口径,
adb 可见但不归平台管的设备不上屏也不告警)。
· 结果**只放内存**(不落库 → 不建表、不动备份覆盖清单),离线设备保留最后读数;
· 读失败不清缓存:list_devices 失败时若照清会把整份缓存抹掉、大屏全体变「—」;
· 配置存 `app_meta.device_battery`(照 step_defaults:出厂值 + 白名单 + 范围钳制 +
只落与出厂值不同的字段),改阈值不用重启(线程每轮重读配置)。
- 告警:事件 `device.battery.low` / `device.battery.recovered`(默认关,去通知页勾订阅)。
**按档位做状态沿**(0 正常 / 1 低 / 2 严重):掉档才报、严重再报一次、恢复报一次;
迟滞 +5% 避免在阈值上下反复告警;同档位每 6h 重复提醒一次。
"充电中"看的是 `status:` 行(2/5),**不是"插着电"**——所以"插着却没充电"
(劣质线/温控/满电停充 status:4)仍会告警,那正是最该知道的情况(实测 fleet 里
就有一台 8% 插着 AC 但 `Max charging current: 0`)。
- GET /api/status 每台设备多一个 `battery`:`{level,charging,at,tier}`。
**tier 由后端算好**,前端只按它上色——阈值只在「工具 → 设备发现 → 电量监控」一处定义,
免得 JS 里再判一遍、两边不一致。
- 前端:大屏卡片型号行右侧加电量徽标(绿/黄/红 + ⚡ 充电中,离线置灰),
`cardHtml`/`renderGrid` 增量更新/脏检查白名单三处都改了(漏一处就不刷新);
监控页设备表新增可排序的「电量」列(空表 colspan 11 顺带对齐了)。
- 接口:GET/POST /api/devices/battery[/settings] + POST /scan(需设备权限,
与 /api/devices/discovery/* 同权限档)。
自测:单元 30 项(解析真机输出/档位/状态机/钳制,含 scale=255、无 status 行、
垃圾输出等边界)、集成 27 项(临时库自建设备池:缓存/通知/离线保留/删设备清理/
配置往返/阈值改动即时生效)、真实 11 台设备一轮 0.44s 全读到、API 端到端
(登录/权限/钳制/越界/REST 往返/页面 DOM)、两份前端各自语法检查 + 渲染用例。
2026-09-24 08:37:42 +08:00
butubb
773cfbb763
Merge branch 'feat/gesture-record'——录制手势改为纯轨迹回放(手机 getevent 真手指录制)
2026-09-20 15:28:30 +08:00
butubb
73895f7233
feat(任务): 录制手势改成**纯录制回放**(真手指轨迹)+ 手机端 getevent 录制
...
用户反馈:录制出来的滑动又被套上滑动那套逻辑(方向/幅度/拟人重新生成),
"导致滑动还是不顺畅"。录制就该是录制——按录下的路径与时间原样重放。
- 新步骤 `gesture`「录制手势」:存完整轨迹点列 `[[x,y,t_ms],…]`,
回放时原样交给设备(`d.swipe_points(points, duration)`),不做任何加工。
与 `swipe` 是两套东西(滑动是参数化的,录制是点列)。
- core/gesture.py(新):
· 手机端录制 `PhoneRecorder`:`getevent` 读**真触屏**设备(自动挑 fts_ts 这类、
排除 uinput/vitural-sar 等合成节点),解析两套协议(BTN_TOUCH / ABS_MT_TRACKING_ID);
**合成的注入事件不会出现在真触屏节点上**(实测),所以录到的只有人手的动作;
· 点列清洗:按 ≥16ms 抽稀但**末点必留**(快划时不能把收尾丢了);
· 回放**一次调用**而不是逐点注入:实测这台设备单次触摸 RPC ≈190ms,
逐点回放 20 点要 3.8 秒——只能让设备自己插值,"时间"由点密度还原。
- 接口:POST /api/gesture/record/{start,stop}(需设备权限;重复开始返回 409)。
- 「录制手势」步骤卡片:录到就回填(手机上录 / 网页上录),显示点数/时长;
**滑动步骤上的「录制手势」按钮已移除**(按用户要求,录制不再走滑动逻辑)。
- 录制前自动唤醒设备:**息屏时 u2 抓 UI 树会从 3 秒退化到 65 秒**(实测),
截图也是黑的——这是本次排查最耗时的坑,已写进文档。
自测:假设备单测(点列/时长/抽稀/失败退回直线,10 项);
无头浏览器 + CDP 真模拟拖动 → 录到 14 点/605ms 并按实际轨迹回填;
手机录制接口起停/重复拦截;
真机回放:用录下的轨迹跑任务 → 步骤 `gesture ok`。
⚠️ 手机端"真手指轨迹"这一段需要人真的去划才能端到端验证(我没有手指)。
文档:TASK_DEV §3.2(两套东西对比 + 三个坑)、步骤表 21 种、API、README。
2026-09-20 15:04:21 +08:00
butubb
f62bc8da28
Merge branch 'dev'——动作配置(步骤默认值 + 动作录制器)
2026-09-20 14:34:31 +08:00
butubb
97e2883fec
Merge branch 'feat/action-studio'——动作配置分栏(步骤默认值)+ 动作录制器
2026-09-20 14:34:14 +08:00
butubb
bd51306a2a
feat(任务): 「动作配置」分栏——步骤默认值 + 动作录制器
...
需求:滑动要能"我自己录制";在自定义动作旁边加一个分栏,能配动作默认值、
能录制操作当动作、能自己建立动作。
**步骤默认值**(core/step_defaults.py,存 app_meta.step_defaults 单键,不建表):
- 覆盖 滑动(方向/时长区间/幅度/抖动/拟人)、点击(等待超时)、长按(时长/超时)、
等待(区间)、按键、输入文字(模式/候选文案/清空);
- **只影响之后新建的步骤**,已有步骤不动;新建时 `_makeStep` 用出厂值打底 + 默认值覆盖;
- 只存与出厂值不同的字段(以后调出厂默认,没配过的能跟着走);
- 字段白名单 + 范围夹取(SPEC):越界夹边界、非法值丢弃 → 手改 JSON 塞脏值也进不来。
**动作录制器**(static/admin/recorder.js):
- 在设备画面上点/划/按键/输入 → 自动翻译成步骤:
点→命中元素记「点击元素」(选择器)、没命中退化成「点击坐标」(百分比);
划→「滑动」方向/幅度/时长全按你实际操作算;停顿≥1s 可选记成「等待」(±15%);
- 两个入口:动作配置页「录制动作」(存成自定义动作,进动作库复用)、
步骤编辑器滑动那步的「录制手势」(只取手势回填该步骤);
- 画面+元素树走 /api/uiauto/snapshot 一次取齐,**每次操作后自动刷新**,
所以下一次点击用的是最新的树;执行仍走 /api/screen/{tap,swipe,key,text}(与大屏同一套)。
接口:GET/POST /api/step_defaults(读登录、写需 tasks 权限)。
文档:TASK_DEV §3.1(含翻译规则表与三条实现口径)、API §2.3、DATA_MODEL §5、README。
自测(无头浏览器 + CDP 真模拟拖动):默认值保存→回读→新建滑动步骤确实预填 0.42;
录制器取到画面与元素树;模拟"按住往上划"→ 录出
{direction:up, distance_ratio:0.5, duration_min:0.47, humanize:true}(幅度/时长按实际操作算);
恢复出厂链路 0.6 通过;页面无 JS 报错。
2026-09-20 14:32:35 +08:00
butubb
cc72591773
Merge branch 'dev'——拟人滑动(每台设备自己的手感)+ 任务级公共巡检
2026-09-16 13:14:02 +08:00
butubb
9a44eeba95
Merge branch 'feat/task-patrol'——任务级公共巡检(含与拟人滑动的合并:import、能力表两处冲突已解)
2026-09-16 13:10:05 +08:00
butubb
77eb53b4b2
Merge branch 'feat/human-swipe'——滑动拟人化 + 每台设备自己的手感
2026-09-16 13:09:31 +08:00
butubb
c29516cff5
feat(任务): 任务级「公共巡检」——独立于步骤画布的守护条件(含 webhook 通知)
...
需求:任务编辑器里能单独配"这个任务每隔 N 秒检查一次"——熄屏就点亮、某个元素
出现就通知、掉出 App 就停本设备;通知标题正文要能自己写。
配置与执行分离(这是本次的关键设计):
- **配置是任务级的**(`params.watchers`),在任务编辑器单独一块,不进步骤画布;
- **执行是穿插的**:worker 每执行完一步、以及长等待的每个分片,看一眼哪个巡检
到点了。不起线程 → 不需要并发模型,也不会和主流程抢屏幕(两边同时点屏幕会
互相打断)。代价是精度受步长影响(某步卡 30s,巡检最多晚 30s),已在文档写明。
- core/patrol.py(新):检查项/动作注册表(CHECKS/ACTIONS)+ evaluate/act。
检查:屏幕熄灭/亮着、元素存在/不存在、前台是/不是某 App;
动作:只通知、点亮、息屏、停止本设备。屏幕走 `dumpsys power`(0.3s,
不用 d.info——那玩意在部分设备要 14s),前台走 d.app_current()(0.7s)。
- tasks/generic/task.py:`_maybe_patrol` / `_run_patrol`(冷却、命中记一条
步骤明细、发通知);**文案在动作之前渲染**——点亮后 {screen} 就成了"亮屏",
用户要看的是"发现熄屏,已点亮"。
- 任务编辑器新增「公共巡检」块(static/admin/tasks.js)+ 样式;保存进 params.watchers。
- 通知:新增事件 `task.patrol.hit`(巡检命中)与 `task.notify.custom`(步骤发通知);
给了 title 就用它当标题(不再拼前缀),level 字段可点名级别。
顺带(巡检需要的原语,也可单独用):
- if_el 条件判断支持 `selector_type=screen`(亮/熄)与 `foreground`(前台包名);
非元素条件不参与「选择器健康」统计(否则会攒出假的"选择器失效"告警)。
- 新增两个步骤:`notify`(发自定义通知)、`stop_self`(停本设备,记"被停止"
而不是失败,不触发重试)。步骤类型 18 → 20,相关文档计数一并更新。
修 bug:`wait` 步骤在巡检耗时超过剩余时间后 `sleep(负数)` 抛
"sleep length must be non-negative"(真机联调抓到,已 clamp 到 0)。
自测:假设备单测 12 组(命中/冷却/间隔/元素/前台/停止/静默/异常不炸);
真机联调:熄屏→点亮(False→True)+ 通知文案正确、掉出抖音按间隔命中 5 次、
长等待里穿插生效且步骤回到 ok、清理后用户通知配置原样恢复。
文档:TASK_DEV §4.5(含两个可抄的例子)与步骤表/条件类型、NOTIFY §3、README。
2026-09-16 13:02:40 +08:00
butubb
cf075ba4e1
feat(任务): 滑动拟人化 + 每台设备有自己的手感(core/humanize.py)
...
用户反馈"滑动太像机器人""批量执行时每台设备都一样"。
core/humanize.py(新)分两个层次:
- **每次不同**:起止点/幅度/时长抖动、弧线方向随机——最容易被识别的不是"慢",
是"每次都一模一样";
- **每台设备不同**:由 crc32(serial) 派生稳定的"性格"(手速 0.82~1.32、
幅度 0.86~1.16、弧度、常用横坐标 ±9% 屏宽、停顿 0.80~1.35)。同设备多次运行
风格一致,不同设备明显不同——13 台批量跑看着像 13 个人各刷各的。
用独立 Random 实例播种,不碰全局 random(多线程 worker 会打乱取值顺序)。
轨迹用二次贝塞尔走 d.swipe_points(曲线),异常时自动退回直线 d.swipe。
**点数固定 4 个**:swipe_points 在慢设备上每多一个点约多 1 秒(实测 2 点
1.4s / 6 点 6.0s / 10 点 10.6s),设备自己会插值几十步,4 点已足够弯。
顺带修一个真机上的老毛病:滑动原本每次都要读 d.info,而它在部分设备上要
**14 秒**。改用 humanize.screen_size()(走 window_size,同设备 0.6s,缓存
120s)——所以哪怕多了弧线,真机单次滑动反而从 ~15s 降到 ~5s。
- tasks/generic/task.py:swipe / swipe_until 走拟人(新增 distance_ratio、
jitter、humanize 参数,默认开);swipe_until 每轮停顿也抖动;wait 步骤新增
可选 vary_pace(默认关,按设备节奏缩放 0.8~1.35 倍);
- static/admin/editor.js:滑动类步骤参数面板加"幅度/抖动/拟人轨迹"+ 说明;
- 文档:TASK_DEV.md §8.4(含"点数别调大""别用 d.info"两个坑)、步骤表、
README 能力表与结构。
自测:假设备单测(曲线/抖动/设备间差异/退化路径/不越界/尺寸缓存)+ 真机联调
(4 次滑动全部 ok,标注"弧线"、坐标每次不同、对照的 humanize=false 仍是直线)。
2026-09-16 12:39:47 +08:00
butubb
92d71c0e27
Merge branch 'dev'——日志筛选/下载 + 任务步骤明细 + 通知降噪
2026-09-16 10:32:57 +08:00
butubb
243df3071b
Merge branch 'fix/notify-noise'——通知降噪:多设备批次不逐台报成功、批次汇总瘦身、设备只显示名字
2026-09-16 10:32:51 +08:00
butubb
16e3e162b2
fix(通知): 多设备批次不再逐台报成功;批次汇总瘦身并列出失败设备;设备只显示名字
...
用户反馈:一条任务覆盖 13 台设备,每台成功都推一条,群里被刷屏;批次汇总里
「任务:抖音养号」与标题重复、「样本:抖音养号」毫无信息量、「stopped」还是英文。
- task_manager:
· **多设备批次不发 `task.device.success`**(批次汇总里已有成功台数;单台任务照发);
失败仍逐台发——那是少数,且要知道是哪台;
· `_BatchTracker` 收集失败设备(`名字(型号):原因`,最多 5 条)并加进批次汇总;
· 设备级事件带 `model`:型号取自**设备池快照**(批次开始时一次查好带下去),
不是 worker 每次连接现采的(那次 `d.info()` 可能超时 → 空),也不查库;
- notifier:
· 聚合窗口口径修正:事件声明 `agg_window=0`(批次结束/服务启停/备份恢复等低频
高危事件)**不再被 hook 的窗口拖住**——原先事件级设置完全失效;
· 「样本」行只在真的合并了多条(>1)时出现,且按**设备**维度写(合并多台时写
任务名每条都一样);与标题重复的字段行(「任务:x」)不再重复渲染;
· 设备名与地址同时存在时**只显示名字**(IP 是给日志看的);
· 补 `stopped`/`failed_devices` 中文标签(原先直接显示英文 key);
- notify_events:批次事件补 `skipped`/`failed_devices` 字段,设备事件补 `model`。
实测(dev 真机 + 假接收端):
批次 2 台 → 只收到 batch.started/finished,无 device.success,汇总 总数2/成功1/跳过1;
单台任务 → 收到 device.success,显示「设备:cs1 / 型号:M2010J19SC」不含 IP。
文档:doc/NOTIFY.md §3(多设备批次怎么发 + 批次消息样例)、§6(窗口口径与注意事项)。
2026-09-16 09:47:25 +08:00
butubb
49544b1985
Merge branch 'feat/task-step-log'——任务步骤明细表 + 日志页「步骤明细」面板
2026-09-16 09:10:24 +08:00
butubb
8879da72b3
Merge branch 'feat/log-query'——日志页:关键字/级别/时间过滤 + 下载(含滚动历史)
2026-09-16 09:10:18 +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
2c32c397c8
feat(日志): 日志页支持关键字/级别/时间过滤 + 下载(含滚动历史)
...
「日志」页原来只能 tail 固定行数、且文件下拉写死了 4 项(新增的 notify.log
根本选不到)。本次:
- core/logger.py 新增读取接口:list_log_files()(模块文件 + .1/.2 滚动历史,
白名单)、query_log()(关键字/最低级别/时间过滤,返回结构化行)、
read_log_text()(导出)。
· 时间/级别过滤对**续行**用继承值(traceback 缩进行本身没有前缀),
按 ERROR 筛不会把堆栈拆散;
· 过滤按"最近 N 条命中"返回,保持文件原顺序;
- GET /api/logs 换新协议(rows/files/matched/scanned/truncated),
新增 GET /api/logs/download(附件下载,支持同样过滤;无一条件时整文件);
- 顺带修 **目录穿越**:原实现 os.path.join(_LOG_DIR, file) 可用 ../../ 逃逸,
现在 file 必须命中白名单;
- 顺带修 web/admin_api.py 里 _log 未定义(用户增删改的日志行会 NameError);
- 前端:文件下拉改为按接口渲染、加关键字(命中高亮)/级别/时间范围/下载/重置,
自动刷新时不再把正在上翻的用户拽回底部。
文档:doc/API.md §10.1(参数与返回)、README 日志节。
2026-09-16 08:44:19 +08:00
butubb
6fa90a505c
Merge branch 'dev'
2026-09-15 14:32:11 +08:00
butubb
3b6ec8c98f
feat(通知): 加钉钉 / 飞书两种格式(含各自的加签算法)
...
## 两家签名算法不一样,照抄必错
| | 钉钉 | 飞书 |
|---|---|---|
| HMAC key | `secret` | `"{timestamp}\n{secret}"` |
| 被签内容 | `"{timestamp}\n{secret}"` | 空 |
| timestamp | 毫秒 | 秒 |
| 拼在哪 | URL query | JSON body |
| 结果 | Base64 再 urlencode | Base64 |
| 出错码 | `errcode 310000` | `code 19021` |
实现为 `BaseAdapter.sign_request()` 钩子(默认不动),两家各写各的;
`core/notifier.py` 的 `_post_once` 在发请求前调用它。
## 另外
- 钉钉:markdown 消息(title+text);成功码 0;默认限流 15/分(官方 20/分,**超限会被限 10 分钟**,留余量)
- 飞书:交互式卡片(header 颜色按事件级别 + markdown 元素);成功码 0;默认 60/分
- `PLANNED_FORMATS` 清空(下拉里不再有置灰项)
- **修一个泄漏**:`mask_url` 原来只打码 query 参数——**飞书的 token 在 URL 路径里**(`/hook/<token>`)
→ 接口回显会漏出去。现在同时处理路径 token(≥20 位随机串)与 Slack 的 `/services/T…/B…/X…` 三段。
## 验证(都跑过)
- **签名对拍官方示例**:钉钉逐字节一致(含 urlencode,`%2B` 级别)、飞书的 key/message 口径一致;
时间戳钉住后比对,不靠"自己跟自己一致"
- 成功码按格式:wecom/dingtalk/feishu 的 0 与 bark 的 200 分别判成功,各自的错误码判失败
- 端到端 15 项:钉钉/飞书在假接收端上真发(URL 带签名、body 带 timestamp/sign、卡片是 markdown 元素、
毫秒 vs 秒的时间戳),签名错时正确判失败(310000 / 19021),token 在回显里被打码
2026-09-15 14:29:11 +08:00
butubb
515cb5d580
docs: 根 README 结构表/前端 JS 加载清单补上 notify(含 notify_api/notifier/taskgen/notify.js,并修正文件数)
2026-09-15 14:05:44 +08:00
butubb
24ea289c15
feat(通知): 加 Bark 格式 + 格式切换时界面提示全部跟着变
...
## 用户报的第二件事:格式改了,提示没跟着改
弹窗里的 URL 示例/说明、密钥标签、限流说明都写死成企业微信了——选「通用 JSON」时
占位还是 `qyapi.weixin.qq.com`,限流还写着「企微硬限 20」。现在这些文案挂在**适配器**上
(`url_hint/url_help/secret_label/secret_help/limit_help`),由接口随格式下发,切格式即时更新;
限流值在用户没手动改过时也跟着格式的推荐值走(企微 20 / 通用 JSON 60 / Bark 60)。
## 新格式:Bark(iOS 推送)
- `POST {url}`,body `{title, body, markdown, group, level[, device_key]}`
—— `markdown` 传富文本、`body` 传纯文本兜底(老版本 App 不认 markdown 字段时也能看清)
- URL 两种填法都支持:直接粘 Bark 复制的那串(`https://api.day.app/ <key>`,key 在路径里),
或填 `https://api.day.app/push ` + 把 key 填到「设备 Key」(作为 device_key 发送)
- **成功判定按格式**:Bark 是 `code==200`(企业微信是 `errcode==0`)→ 新增
`BaseAdapter.ok_codes`,`_post_once` 用它判定。少了这一步 Bark 的每次成功都会被误判成失败
- 正文按 2048 字节截断(走 APNs,体量有限)
## 顺带
- 通用 JSON 的 `secret` 现在会作为 `X-Webhook-Secret` 请求头发出(原先填了没用)
- `_formats()` 改为序列化适配器元信息,新增格式只改一处
## 验证
- 格式切换 14 项断言全绿:三种格式的 URL 示例/密钥标签/密钥说明/限流说明/模板区/限流默认值
全部跟着切换;下拉里 Bark 出现在已实现区
- Bark 端到端 7 项:`code=200` 判成功、`code=400` 判失败(含重试)、请求体带
device_key/title/body/markdown/group、URL 里的 key 在接口回显里被打码
- 文档:NOTIFY.md §5 的格式表补 Bark 列(URL 怎么填 / 成功码 / 约束),并说明
"提示文案挂在适配器上,别写死在页面里"
2026-09-15 14:05:33 +08:00
butubb
1e223e847b
chore(通知): 补上漏提交的 web_server.py 接线(init_app 时序 + 启动/停止通知)
...
上一轮的 git add 路径列表漏了 web_server.py——功能本身在跑(文件在磁盘上),
但提交里缺这一段,别人拉下来会因为没调 init_app 而收不到任何通知。
2026-09-15 13:59:13 +08:00
butubb
9a6c0db65c
fix(通知): 新建/编辑弹窗缺「保存/关闭」整条操作栏(点进去出不来、也存不了)
...
用户报:新建 webhook 弹窗**没有关闭按钮**——点进去不想建就出不来了(只能刷新页面)。
顺着查发现更严重的一处:**底部操作栏整个漏了**——「创建/保存」「预览」「取消」全都没有,
只是靠"以为有保存按钮所以先点开看看"才没被立刻发现:其实填完也提交不了。
修(static/admin/notify.js + monitor.html):
- 弹窗顶部加「✕ 关闭」,底部加「创建/保存 · 预览请求体 · 取消」操作栏
- 四条关闭路径都可用:✕ / 取消 / **Esc** / **点空白处**(overlay 的 onclick)
同批复核出的其它问题:
- 通配订阅区被我上一版写成永远隐藏(`display:none` 且没人改回来)→ 恢复可见
(任何格式都适用,标签补成「通配订阅(可选,任何格式都适用)」)
- `_notifyFormatChanged` 里残留 `(f === 'json' || true)` 的无意义判断 → 清掉
- 工具条上一个 input 写了**两个 style 属性**(后者会被浏览器忽略)→ 合并成一个
复核方式(这次全部**走页面按钮**,不再只测接口):
- UI 全流程 13 项:四条关闭路径 + 底部按钮存在 + 页面创建(列出/打码 URL/事件数)
+ 编辑回填与改名 + 发送测试(假接收端真收到)+ 页面删除,无 JS 报错
- 通配订阅 6 项:区可见 / 非法通配被拒 / 芯片渲染 / 保存后列出 / 编辑回填 / 清理
- 接口边界 10 项:提交打码 URL 不覆盖真实值、只传 enabled 的局部更新、非法模板被拒、
预览字节数、limit 非法值兜底、404 分支、未登录被挡
教训:交付物是界面时,自测必须走界面点一遍——上一轮我用接口建的 webhook,
把「按钮不存在」整类问题漏掉了。
2026-09-15 13:59:00 +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
5e0a7d5556
fix(屏幕): 一键息屏不再唤醒已眠设备 + 「保持亮屏」对不充电的设备生效
...
用户反馈两条:
1. 监控页「一键息屏」像是把设备**唤醒**了(按钮行为"反");
2. 任务里的「保持亮屏」没用。
根因:
1. `POST /api/device/screen_all` 的 off 分支发 `input keyevent 26`(KEYCODE_POWER)——
那是电源键**开关**:对亮着的设备是熄屏,对**已经息屏的设备反而是唤醒**。
2. `keep_screen` 只发 `svc power stayon true`,它管的是「**充电时**屏幕常亮」
(stay_on_while_plugged_in)。设备走 WiFi 跑任务、没插充电器 → 完全不生效。
修法:
- 息屏改用 `KEYCODE_SLEEP(223)`(单向:只熄不亮)。
- 保持亮屏改成把系统**息屏超时**顶到最大(`settings put system screen_off_timeout
2147483647`)+ 顺带 `svc power stayon true` + 立刻 `KEYCODE_WAKEUP` 唤醒一次;
`mode=off` 时写回原值(进入时读一次记在内存;进程重启丢了记录就写回 10 分钟兜底,
`settings get` 返回 "null" 的机型也走兜底)。
- 文案/文档同步:编辑器里的步骤说明、TASK_DEV 步骤表、API.md 的接口行为。
自测(按用户要求不动机器):py_compile / node --check 通过;用假设备对象断言命令序列——
保持亮屏发 `settings get` → `stayon true` → `put …2147483647` → `keyevent 224` 并记住原值;
恢复写回原值 60000 且清空备份;原值为 null 时兜底 600000。
2026-09-15 13:24:28 +08:00
butubb
afdea347ee
Merge branch 'dev'
2026-09-14 10:34:13 +08:00
butubb
6f14529db0
docs(流程): 明确「修复也走分支流程,含线上紧急修复」
...
2026-09-14 当天两次 APK 安装修复(615f9cf / 7b6c838)因为"生产卡着"直接提交在 dev 上、
紧接着合 main 部署,跳过了 ① 切分支 与 ③ 交负责人确认(把"合 dev 确认"与"部署确认"
揉成了一次)。负责人指出后补齐这条约束。
补充内容(§1.2 git 铁律):
- 紧急修复照样从 dev 切 fix/xxx,不允许直接 commit/push 到 dev
- 第 ② 步自测要跑通本机服务,不能只有脚本级验证
- 第 ③ 步确认要点名两件事:「是否合 dev」与「是否合 main + 部署生产」
- 线上正卡住时先恢复(运维动作,经确认即可),再按流程走修复
2026-09-14 10:33:43 +08:00
butubb
b11d36a52e
Merge branch 'dev'
2026-09-14 10:29:26 +08:00
butubb
7b6c8386af
fix(APK 安装): 推送卡死会永久挂住整批安装(新增停滞看门狗 + 收尾兜底)
...
用户报:220 上一个安装任务卡住不动。(现场:rednote 169.6MB 装 6 台,
5 台成功,192.168.20.203 卡在「推送中 67%(114.0/169.6 MB)」9 分钟没动静——
那台设备当时正跑着任务,adb 流量撞在一起把推送链路卡住了。)
两个叠加的缺陷:
1. 分块推送的 `proc.stdin.write()` **没有任何超时**:链路一卡就永久阻塞,
安装线程挂死。
2. 批处理的 `as_completed(timeout=...)` 超时后异常直接冒泡出去(`with
ThreadPoolExecutor` 退出时还会 wait 卡住的线程),`finished=True` 永远没被
置位 —— 于是界面上永远是"安装中",且后续安装全被「已有安装任务正在进行」
挡住(用户看到的"卡住")。
修法(core/apk_manager.py):
- 新增**停滞看门狗**:盯着"已写字节数"是否推进,`_PUSH_STALL_TIMEOUT`(90s) 没进展
就 kill 掉 adb,阻塞中的写立刻以异常返回 → 退回 `adb push` 重传。
(为什么不用非阻塞写/管道 select:`os.set_blocking` 是 Unix 专有,开发机是 Windows,
看门狗两边都能用。)
- 批处理改成 try/except/finally:超时或异常时把还没结果的设备标失败,**无论如何**
都置 `finished=True`;`pool.shutdown(wait=False)` 不再为卡住的线程陪等。
验证:
- 假 adb 模拟"永不读 stdin"(链路卡死)→ 4 秒识别停滞 → kill → 回退 push 成功,
全程 6.1s(旧代码在这里永久挂死)。
- 真机正常路径不受影响:5.9MB 推送 1.7s、设备侧字节数一致。
2026-09-14 10:28:22 +08:00
butubb
75c63db836
Merge branch 'dev'
2026-09-14 09:10:27 +08:00
butubb
615f9cf0d5
fix(APK 安装): 「推送不完整」是误报——adb 退出 ≠ 设备写完
...
用户报:安装 APK 显示「推送不完整(设备 3605447 / 本地 5933150 字节)」。
排查(220 现场 + 复现):这不是传输失败,是**检查时机太早**。
`adb exec-in` 把数据交给设备侧(adbd → shell → `cat >`)后,缓冲里的数据还在继续落盘:
同一份 5.9MB 的 APK,adb 刚返回时 stat 读到 3710967,紧接着 4264448,几秒后才是完整的
5933150(逐步实测)。旧实现只量一次就判失败,于是同一份文件在多台设备上"失败"了 8 次,
断点各不相同(2.7M/3.3M/3.6M/3.9M)—— 典型误报指纹。
修法(core/apk_manager.py):
- 新增 `_wait_remote_size()`:收尾**轮询**设备文件大小直到长齐(默认最长 20s);
stat 取不到(机型差异)时返回 None,照旧跳过校验,不误报。
- 只有真不齐才退回 `adb push` 重传(原来直接判失败)。
- `_push_fallback` 也补一次大小校验:adb push 虽同步,但设备空间不足/被并发覆盖时
"1 file pushed" 也可能是残缺的。
影响:这个误报不只是显示难看——它会让整台设备的安装直接失败(明明是好的包)。
2026-09-14 09:08:37 +08:00
butubb
2456a9d010
Merge branch 'dev'
2026-09-14 08:54:57 +08:00
butubb
5dc15e128e
fix(AI 建任务): 切走再切回来不再重置/重复探索回放
...
现象(用户报):离开「AI 建任务」子页再回来,回放与步骤计数像是被重置了。
真因与修法(`static/admin/taskgen.js`):
1. 每次进入子页 `initTaskGen → tgRestore` 都会重新订阅 SSE。已在盯同一轮时这是多余的:
服务端 `_Fanout` 带历史缓冲,会把这一轮**从头补发**一遍 → 回放区里卡片变两份、
步骤计数从 0 重数。现在用 `_tgRunId` 记当前订阅,同一轮直接复用连接;
换了一轮/刚刷新过页面才清空回放区重新订阅(历史缓冲会把已发生的事件补回来)。
2. 跟随画面的 MJPEG 长连接在切 Tab 时可能被浏览器挂起(画面定格)——
切回来时重新拉一次流(`tgRearmLive`)。
实测(真机,12 步探索):切走 6 秒再切回 → 卡片 11→14 无重复、状态继续计数;
跑完后再切两次 → 14 张卡片与草稿预览均保持、跟随画面正常、无 JS 报错。
2026-09-14 08:25:35 +08:00
butubb
ed9e8bacb1
feat(AI 建任务): 直接创建 + 草稿沉淀 + MCP de_snapshot;修「建任务页收不到 done」
...
用户报的"探索完无法点击创建任务"真因:一个 run 的事件原先只有**一条** queue.Queue,
聊天页与建任务页同时开着时两个 EventSource 会**瓜分**它——建任务页的回放卡在中间、
`done` 被聊天页取走 → 永远等不到草稿,页面上自然没有可点的"创建"。
一、修(根因 + 表现)
- `web/agent_api.py` 新增 `_Fanout`:**每个订阅者一个专属队列**,多开页面各看各的,
还带单轮事件缓冲(晚订阅/刷新重连也能补齐回放,终止事件一定送达)。
实测两路订阅者收到完全一致的 1039 条事件(含 done)。
- `static/admin/agent.js`:断线重连的兜底订阅也按 `mode` 让开(此前漏了这一处)。
二、补齐上一批的三项
- **「直接创建」**:`POST /api/agent/task_draft/create`(草稿体只在服务端、创建前再校验一次、
成功后清草稿避免重复建)+ 草稿预览里的「✓ 直接创建任务」按钮 + 「探索完直接创建任务」勾选框。
- **草稿沉淀经验/动作**:designer 轮次也走 `_distill_experience/_distill_actions`,
但**只在草稿通过校验时**(没走通的试错不入库,免得把误点当经验)。
- **MCP `de_snapshot`**(第 20 个工具):截图+元素树一次取齐(省一次来回、不会因界面在动而错位),
附带 `screen_state`/`unstable`;两套提示词都改为优先用它。
平台侧 `/api/uiauto/snapshot` 随之多返回 `screen_state`。
三、文档
- AI_TASK_GEN §10:§10.3 记两个 bug 的真因与修法、§10.4 三项标完成、§10.5 剩余项。
- AI_CONSOLE(扇出语义、多页面同时看一轮)、API(task_draft/create、snapshot 字段)、
MCP/MCP_DESIGN/staffdeck/README/ARCHITECTURE:工具数 19→20 + de_snapshot 条目。
- backlog:记一条新发现的缺陷——`mcp_server/platform_client._login()` 会把"登录页 200"
当成登录成功(现场进程缺 `MCP_PLATFORM_PASS` 时表现为含糊的 platform_unavailable)。
自测:真机浏览器端到端(勾上"探索完直接创建")→ 探索 12 步 → 草稿 → 自动建任务成功;
`de_snapshot` 直连真机校验;校验器 21 条用例、扇出单元用例、本地工具契约用例全绿。
自测产生的任务/草稿已全部清理(未碰用户既有数据)。
2026-09-14 08:14:11 +08:00
butubb
46e6ea1f37
feat(AI 建任务): AI 自己在真机探索 → 写出可调度任务 → 人工确认入库
...
AI 控制台下新增子分栏「🧭 AI 建任务」:描述要做什么(例:建一个跑 2 小时的任务、自动刷
某 App、随机点赞),AI 用 de_* 工具自己在设备上探索(看屏/读元素树/点按验证),把走通的
路径写成一条任务草稿,经服务端校验后交人在步骤编辑器里核对/手改/试跑,保存才入库。
链路:POST /api/agent/run{mode:"designer", settings}
→ Agent 自探 → 本地工具 submit_task(draft)
→ core/task_draft 校验(失败把 errors 回灌模型让它改)
→ 只暂存(运行态 + app_meta.agent_task_draft,**不落库**)
→ SSE done{mode,draft,warnings} → 页面草稿预览 → openTaskModal(null, prefill) 预填
关键实现
- core/task_draft.py(新):把执行器的"静默跳过点"(未知 type/空 selector/空 children/
嵌套>5/节点>60/cron 非法/必填缺失)前移成显式 error——POST /api/jobs 对 params 是盲存的,
执行器又静默跳过错误步骤,没有这道闸门就是"任务建好了、跑起来什么都没做"。
归一化兜底任务名/target/schedule/retry/时长;页面填的设置以 overrides 优先于模型。
故意**不比执行器更严**:loop_mode 近义值归一(count→rounds)、缺 max_iterations 补默认 10
(执行器本来就默认)——实测卡太死会把一轮探索耗在改字段上。
有副作用的步骤(评论/发送/购买/删除…)只警告并把触发概率压到 30%(编辑器可改回)。
- mcp_agent/agent.py:双系统提示词(CHAT/DESIGNER)+ 平台级本地工具
(LOCAL_TOOL_SPECS,不进 MCP)+ 每工具调用上限 40 + designer 输出上限 8192 +
**json.loads 容错**(草稿被截断时给模型可读错误,而不是整轮失败)。
- web/agent_api.py:mode/settings 透传、submit_task 处理器(app_context 内校验+暂存)、
done 带 draft、GET /api/agent/task_draft{,+POST,/clear}(草稿走 app_meta,不新建表)。
- 前端:static/admin/taskgen.js + #agent-sub-taskgen 子面板(showSubTab 机制);
tasks.js 的 openTaskModal(jobId, prefill) + 信封归一化 + 唯一 draftKey;
agent.js 按 mode 门控(一个 run 只有一个事件队列,两个 EventSource 会互相瓜分事件)。
顺带修掉一个 chat 也踩的协议 bug:一轮里同时调 de_screenshot 与别的工具时,截图图像会被
插在两条 tool 消息之间 → 模型侧判"工具回应不足"直接 400。改为本轮 tool 消息发完再附图像,
_repair_tool_messages 也改成只数**连续**的 tool 消息。
真机实测(Redmi 22120RN86C,设置页):8 步探索(含 tap_text 验证)→ submit_task 一次通过 →
草稿 8 个顶层步骤(screen_on/open_app/wait_el/click/wait/key_event…)、max_duration 1800、
无 click_xy、3 条 evidence;页面恢复草稿 + 预填编辑器 + 提示块渲染均正常,无 JS 报错。
自测数据已清理(草稿已丢弃、未创建任何任务)。
文档:AI_TASK_GEN.md 状态改「P0 已实现」+ §10 实现记录(差异/护栏/未做项)、AI_CONSOLE.md
(子分栏、designer 分支、SSE done 负载、app_meta 键)、API.md、DATA_MODEL.md、ARCHITECTURE.md、
DEVELOPMENT.md(自测入口)、README.md 索引、backlog 勾掉 P0。
2026-09-13 23:08:59 +08:00
butubb
0bc713137d
feat(抓取): 选择器优先语义化(同 id 多实例用 @text 限定,而非序号)+ 修掉两类"死选择器"
...
语义消歧(B 项):主属性在整棵树里重复时,先找第二个属性把目标单独圈出来 ——
//*[@resource-id="x" and @text="我"] (次属性 text > content-desc > class,
单个不够就两两组合)
标 semantic:true + via;只有组合也分不开(列表里同 id 同文字)才退回
(//*[@resource-id="x"])[k] (标 indexed,前端黄标提醒脆弱)
抖音底部导航正是这个场景:tab 个数随灰度版本变(4 个 ↔ 3 个),序号必然错位。
顺带修掉两个结构性缺陷(给上面做验证时逐条 lxml 求值发现的,均非本次引入):
1. 结构步进把 class 当标签名 —— dump 的 XML 标签**一律是 <node>**,class 在
@class 上,所以 //FrameLayout[1]/… 这类路径**永远零命中**;改 *[@class="…"][n]
2. 兜底结构路径用 @index 定位兄弟 —— 实测同级 index 会重复(状态栏/内容区/
导航栏三个兄弟全是 index="0");改按子节点位置 //hierarchy/*[1]/*[2]
前端:抓取列表把 text/content-desc 排到最前并加粗上色(最稳的定位依据);
序号型从蓝标改**黄标 ⚠ 序号 k/n**,语义型给**绿标 ✓ 语义**;属性页新增
「选择器稳定性」一行说明这个选择器靠什么定位、会不会因界面变化失效。
真机实测(192.168.20.100,248 个元素):
精确命中目标 221 → 247 | 死选择器 26 → 0 | 语义型 0 → 26(序号型 141 → 115)
「我」的语义选择器经 /api/steps/test 真机点击 → 命中 ✓
文档:TASK_DEV §5.3/5.4(含两个 XPath 坑)、API §8(suggested 字段表 +
snapshot 行)、research/U2_ELEMENT_SELECTORS §五/§六、backlog ②标记完成。
2026-09-13 22:05:47 +08:00
butubb
4ea777a55a
fix(抓取): 元素框跟着图片走(ResizeObserver)+ 界面放大 + 右侧层级/属性/颜色三页签
...
用户报的准确现象:**点某些元素会让画面变大,但框没跟着变大 → 错位**。
根因:预览图是 `max-width:100%` —— 窗口够宽时恒为原始尺寸(我这儿复现不出),
但窗口窄时图片被压小;此时任何**布局微动**都会改变预览列宽度 → 图片重新缩放,
而框是上一次算好的**像素坐标**,不会跟着变。触发布局微动的正是"点元素":
`.el-picker-item.active` 用 `border-left:3px`(占宽度)→ 列表变宽 → 预览变窄 →
图片变小 ✗。实测(视口压到 900):图片从 720 → 363,框仍停在 720 的位置。
修法:
1. **ResizeObserver 盯着图片**:显示尺寸一变就重算所有框 —— 窗口缩放、面板宽度
变化、滚动条出现…都会自动跟上,这是"完全不错位"的兜底保证。
2. 高亮一律改用 **outline / box-shadow**(不占布局)→ 选中元素不再引发任何重排。
3. 弹窗放大到 **98vw × 88vh**(用户要"整个界面大一点"),图片在宽窗口下 1:1。
4. 右侧改成和 uiauto.dev 一样的**三页签**:
- **层级**:元素树(原列表,缩进 + 搜索)
- **属性**:选中元素的全部属性(建议选择器/class/resource-id/text/desc/package/
clickable/bounds/深度)
- **颜色**:鼠标在图上移动实时取**像素色**;选中元素显示**元素中心色**
5. 交互对齐云检查器:**点层级条目 = 选中**(看属性/颜色/高亮,不关窗),
回填改由每行的「✓ 填入」按钮触发。
实测(视口 900 窄窗口):图片 720→363、框同步变 363 ✓;点元素不再错位 ✓;
三页签可用、属性/取色正确(选中元素中心 #f7f7f7)✓;无 JS 报错 ✓。
2026-09-13 21:30:41 +08:00
butubb
37a2b1c59b
feat(抓取): 元素检查器改成本地版"大图 + 一次取齐"——复刻云检查器的体验
...
用户要的是 uiauto.dev 那个检查器的体验(左边大图点元素、右边层级树、选完回填),
并希望本地复刻(云页面跨域,拿不到它的选中结果,没法自动回传)。
- core/uiauto_helper.py: 新增 `snapshot(serial)`
* **一个 u2 连接背靠背** dump_hierarchy() + screenshot():截图与元素树同源同刻,
不再像原来那样分两个接口取(中间隔着 dump 本身的 1.3~1.8 秒)
* `_xml_to_node()`:把 u2 的 XML 节点转成 uiautodev 那套结构,直接复用既有的
选择器建议逻辑(不写第二遍)
* **双截图校验**:dump 前后各截一张,差异明显就标 `unstable`,让前端明确提示
"界面在变化中,请停在静止界面再抓",而不是悄悄给一个可能错位的框
- web/tasks_api.py: 新增 `GET /api/uiauto/snapshot`(登录 + 设备权限)
- static/admin/editor.js + templates/admin/monitor.html:
* 抓取弹窗从 900px 加宽到 1280px,预览列从"固定 320px"改为铺满左侧 →
**图片按原始分辨率 1:1 显示**(实测 720x1650 缩放 1.000),元素框严丝合缝;
原来缩到 294px 时框全挤在一起,看着就像错位
* 工具栏显示 `720x1650 · 247 个元素 · 2370ms`;界面在变时顶部弹黄色提示条
* 鼠标在图上移动时高亮"最深命中"的元素框,便于确认真要点哪个
实测:抓取弹窗 1:1 显示、222 个框逐一贴合元素、无 JS 报错;
`unstable` 在静止界面为 false。
2026-09-13 21:17:12 +08:00
butubb
accd06df4b
docs(research): 章节编号理顺(三·补 → 四,建议改动 → 五)
2026-09-13 21:02:01 +08:00
butubb
ee559786e7
docs(research): 补「抓取弹窗老是错位」的真因——截图与元素树不是同一时刻(附实测)
...
用户澄清"是抓取的窗口老是会错位"。把几种常见猜测逐一实测排除:
两条通道元素树不一致(✗ 逐项相同)、CSS 缩放没跟着算(✗ 缩放比正确、
框位置与理论值一致)、窗口 resize(✗ 预览列固定 320px)、列表索引错位
(✗ indexOf 保住原始序号)。
真因是取数时序:
/api/uiauto/screenshot 0.4s + /api/uiauto/elements 1.8s(两次请求)= 间隔约 1.8s;
换成原生 u2 背靠背也仍有约 1.4s(dump 本身就要 1.3~1.8s,设备端开销改不动)。
界面只要在动(信息流/视频/动画),这两秒就足以让元素位置全变 → 框永远落在旧位置。
因为是**每次都发生**,所以表现为"老是错位"而不是偶发。
解法(写进文档待排期):①新增 /api/uiauto/snapshot 一个请求取齐(也正是
"用原始 u2"的做法,间隔降到 ~1.4s);②抓取前后各截一张做校验,不一致就
明确提示"界面在变化中,请停在静止界面再抓",而不是悄悄给一个错位的框。
2026-09-13 21:01:53 +08:00
butubb
a94f63f0bf
docs: 元素选择器研究——「点不到按钮」的根因是序号型选择器(附实测证据与解法)
...
用户反馈任务步骤老是点不到元素,怀疑"执行用的 u2"和"抓取用的 uiautodev"两条
通道不一致。建 research 分支实测,结论与假设相反:
1) **两条通道其实是一致的**:同设备同屏各 dump 一次,节点数 330/330、
id 个数逐项相同 —— 都是同一份 UiAutomation 树,不存在"看到的不一样"。
顺带纠正一处过时注释:uiautodev 的 `rect` 就是像素(`bounds` 才是归一化)。
2) **真凶是序号型选择器**:复现「抖音→我页面」,底部 `0qf` 只有 **3 个**
(首页/消息/我 —— 「朋友」tab 是灰度功能,有的账号/设备没有),
而任务里写死 `(…0qf…)[4]` → 第 4 个不存在 → 必然点空。
同一选择器在 4 tab 设备上碰巧对、在 3 tab 设备上必错 —— 这就是"时好时坏"。
3) **解法(实测有效)**:同一 id 多实例时用**文字/描述限定**:
`//*[@resource-id="…0qf" and @text="我"]` → 命中并把设备带进我页面(jy- 出现)。
文档里还列了同类限定条件的优先级与四条待排期改动(抓取器优先产语义选择器、
对带序号的选择器加提示、存量任务批量复核、运行期 dump 同 id 实例数)。
2026-09-13 20:56:37 +08:00
butubb
ef71943a3c
Merge branch 'dev' into main——本批:APK 安装进度/静默安装接口/应用列表合并/设备名显示/配置二维码/监控页排序与按钮
...
dev 侧 6 个提交整体合入 main(对应之前按红线撤回的商店功能,现已在 dev 验收)。
2026-09-13 17:37:56 +08:00
butubb
524ae20c49
Reapply "feat: 设备端应用商店(平台侧)"
...
This reverts commit bf5071b14d .
2026-09-13 17:37:32 +08:00
butubb
6fd9e6f8f3
chore: 容器依赖守卫补 qrcode——设备配置二维码用了它,不加的话容器重启不会安装(守卫只看固定模块列表)
2026-09-13 17:36:03 +08:00
butubb
3422b4f265
feat: 监控页按名称排序 + 按钮改「看屏 / 打开设备端 Agent」;设备名匹配补 USB 设备
...
用户反馈的三件事:
1) **监控页设备排序**改成按**名称**(A01/A02/…),没名字的排最后按地址。
之前是 adb 返回的顺序,看着像按 IP 排,同型号多台时对不上号。
排序放在 `/api/status`(后端),大屏页一并受益。
2) **操作按钮改名 + 换实现**:
- 「截图」→「**看屏**」(行为不变,就是查看设备实时画面)
- 「定位」→「**打开设备端 Agent**」:改成让设备上的 Agent 弹身份大字页
(名称/IP/平台地址),替代原来"推 HTML 让设备浏览器打开定位页"那套
(那套要设备装浏览器、页面还依赖设备能访问平台,backlog A1/A2 一直没修)
* 新接口 `POST /api/agent/show-info {serial, close?}`;没装 Agent 时明确报出来
* **先点亮屏幕 + 解除锁屏再拉起** —— 设备息屏时 am start 会把大字页开在锁屏后面,
人看到的还是一片黑(实测踩过)
* 平台地址取操作者当前访问地址;若是 localhost 则换成本机局域网地址
(设备上显示 127.0.0.1 毫无意义)
3) **安装应用的选择设备界面仍显示裸序列号**:USB 设备在 adb 里的 serial 就是
`ro.serialno`,等于池里那条网络记录的**指纹** —— 之前只按 serial 匹配,插着 USB
时会冒出一个没名字的 `s8o7nrt8pbzhfadq`。现在按 serial 或指纹任一命中都取名称,
三处(剪贴板注入 / 安装弹窗 / 安装进度表)统一。
顺带修掉几个自己踩出来的坑:
- `device_pool.list_configured()` 返回的是 **serial 字符串列表**,我当成字典用了
(`.get("serial")` 抛异常被 except 吞掉 → 名字全空)→ 改用 `list_devices()`
- `web/device_agent_api.py` 用了 `PERM_DEVICES` 但没导入 → 服务直接起不来(502)
- Agent 自检里的版本号是写死的常量(升到 1.4 还显示 1.0)→ 改成读包信息
- `doc/API.md` 章节编号(插了一节没把后面整体下移,出现两个 §16)
验证:安装弹窗里 USB 那台现在显示「A07」(指纹匹配);监控页按钮为
「看屏 / 打开设备端 Agent」;对装/未装 Agent 的设备分别得到大字页 / 明确报错;
大字页实测显示 A07 + 192.168.20.250:18050 + Agent v1.4。
2026-09-13 17:18:30 +08:00
butubb
3f041668ec
feat: 设备配置二维码——设备池里出码,Agent 扫一下配好
...
配合设备端 Agent 新增的「扫码配置」:人在机架前、设备还没接进平台时,
不用插线也不用在手机上打字输地址/令牌(还容易漏指纹)。
- `GET /api/devices/qrcode?serial=`:生成配置二维码(PNG data-url),内容为
{"v":1,"server":…,"token":…,"fingerprint":…,"name":…}
* 前置:设备端商店已启用(令牌从那儿来),否则 400 并说明去哪儿开
* `warn` 非空必须显示:①用 localhost 打开平台时,二维码里的地址手机会连不上
(提示改用局域网地址,_lan_ip() 优先挑 192.168/10.x,**不用** gethostbyname ——
那会返回 Tailscale 的 100.x 或 docker 的 172.17,手机根本连不上)
②设备还没采到指纹 → 扫完平台认不出是哪台
- 设备池每行加「二维码」按钮 → 弹窗显示二维码 + 设备名/地址 + 可展开看原始内容
- requirements 加 qrcode(纯 Python,配合已有的 Pillow 出 PNG,不引前端二维码库)
- 文档:API.md §6.2.2 新增、§6.2.1 名称呈现表补齐(剪贴板/安装弹窗/版本管理/
安装进度四处);DEVICE_AGENT.md §5.3 新增二维码内容格式(设备端契约)
验证:二维码用 cv2.QRCodeDetector 解回来与 payload 完全一致;手机上实测——
扫 A07 的码 → 相机启动 → 解码 → 保存 → 立刻拉清单,平台认出「A07」。
2026-09-13 16:42:53 +08:00
butubb
17b6624a34
docs: 契约补 '请平台静默装' 接口(§3.4)——设备端可选实现
...
设备端自己调系统安装器在 MIUI 上要过好几步确认,而平台走 adb 装是静默的
(实测升级 2.7s、全新安装 7.9s)。接口已实现,写进两个仓库共享的契约:
受理后平台异步安装、设备端轮询版本变化判断结果;平台装不了时返回 ok=false,
设备端退回本机安装。
2026-09-13 16:30:48 +08:00
butubb
25e4fbb053
feat: 应用列表按包名合并 + 剪贴板/安装/版本管理显示设备名
...
两件事都是"列表看不清楚"的问题:
1) **同一个应用传多个版本会并排显示多行**(v1.2 / v1.3 各占一行),看着像重复。
现在按 **package_name 合并成一个应用一行**,显示最新版:
- `apk_manager.list_latest()`:版本按 version_code 排序取最新,其余版本放进
`versions` 字段;解析不出包名的 APK 各自独立成行(不硬合并)
- 管理页版本列显示 `1.3(另有 1 个旧版本)`,多一个「清理旧版本」按钮
- 「删除」语义改为**删掉这个应用的所有版本**(行=应用),另给
`?scope=older` 只清理旧版本;老的单条删除行为保留(不带 scope)
- **设备端商店也走同一个列表** —— 手机上不会再看到同一个 App 的 v1.2/v1.3 两行
2) **设备只显示地址**(`192.168.20.100:5555`),同型号多台时分不清是哪一台。
三处补齐设备名(平台给设备起的名字,如 A08),统一「名称 · 地址」:
- 剪贴板注入的设备勾选列表(`/api/adb/devices` 经 `_merged_device_list` 带 name)
- 应用管理的安装弹窗(`/api/apks/install/devices` 带 name)
- 应用版本管理表格(`/api/tools/appver` 结果里带 name)
- 顺带:安装进度表里的「设备」列原来显示**型号**(而且是 STF 时代残留的空 dict),
改为显示设备名
没名字的设备(不在池里)自动回退只显示地址,不显示空占位
验证:上传同包名 v1.1/v1.2/v1.3 三个版本 → 列表只出现一行(v1.3,标注共 4 个版本)、
设备端 bootstrap 也只剩 2 个应用;`scope=older` 只留最新、`scope=package` 整包删除;
三个面板在浏览器里渲染无 JS 报错,devText 有名字/没名字两种形态都正确。
2026-09-13 16:29:38 +08:00
butubb
fa07ed0804
feat(platform): 设备端商店支持"请平台静默装"—— 设备请求、平台走 adb 落地
...
背景:设备端自己调系统安装器在 MIUI 上要过「继续 → 勾选未经安全检测 → 继续更新」
甚至 ICP 备案检查好几步;而平台用 adb(推送 + pm install)装是**静默**的,实测
升级 2.7s、全新安装 7.9s、336MB 大包也没弹过一次框。
所以给设备端商店补一个"请求平台装"的出口:用户还是在手机上选应用(应用商店体验),
真正干活的是平台,几秒钟装完、零弹窗。
- web/device_agent_api.py 新增 `POST /api/device/agent/install`:
设备带令牌调它 → 平台复用应用管理的安装器(`apk_mgr.install`,本身已是
推送 + pm install、并发 5 台)→ 返回是否受理;失败时设备端自行退回本机安装
- 鉴权与设备识别沿用同一套(令牌 + 指纹),未登记设备拒绝并说明原因
注:设备端 APK 那边的对应按钮改动已暂停(用户决定先不动 Agent),
这个接口属于平台侧能力,先落在本次分支里,等 Agent 恢复时直接可用。
2026-09-13 15:38:14 +08:00
butubb
cdf06e1929
fix: 大 APK 推送到设备时显示实时进度(原来是整段黑箱干等)
...
问题(用户反馈):从网页点「安装」到设备,包大的时候(抖音 336MB)只显示
一句「正在推送 APK...」,传了两分多钟全程没有任何进度,像卡死了。
根因:推送用的是 `adb push`,它**只在结束时**吐一行汇总
("1 file pushed, 3.0 MB/s"),中间过程什么都不给。
改法:分块写 + 实时进度
- core/apk_manager.py 新增 `_push_with_progress()`:改用 `adb exec-in "cat > 文件"`
自己按 1MB 分块喂数据,每块更新一次状态(限流 0.8s 一次):
`推送中 62%(211.0/336.1 MB · 2.6 MB/s · 剩约 48s)`
传完校验设备上文件大小;中途断流/exec-in 不可用时**退回原来的 adb push**(保证兼容)
- 代价:实测速度 2.6 vs 3.1 MB/s(慢 17%),换来看得见的进度,划算
- 「正在设备上安装...」补上预期时间说明 —— `pm install` 在设备端解包
(336MB 实测 ~65s),系统没给进度接口,只能给预期值
- static/admin/apps.js:安装进度轮询 3s → 1.5s;表格里再加一条细进度条
(大包要一两分钟,光有文字不够直观)
- doc/API.md:把安装状态机的两段式进度与耗时参考写进接口说明
实测(抖音 v40.4.0 336.1MB → 192.168.20.100,单台):
推送 60%→100% 每 2 秒一档,速度稳定 2.6 MB/s,134s 传完;
设备端安装 65s;端到端 ~200s,全程有进度可看。
全量回归 106 项通过。
2026-09-13 15:38:14 +08:00
butubb
bf5071b14d
Revert "feat: 设备端应用商店(平台侧)"
...
按 git 红线撤回:该功能未经 dev 验收就被合并进 main(是我提交时没切分支、
又把自己的自测当成了用户验收)—— main 必须保持"已验收可部署"的状态。
功能本身没问题,代码仍在 **dev**(dad1af8)与 feature 分支上,等设备端 Agent
写出来、端到端验收通过后,再从 dev 合并回 main。
main 内容已回到 66632bb(git diff 66632bb HEAD 为空)。
2026-09-13 14:44:00 +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
66632bb7d6
chore: .gitignore 忽略 .env.*(含密钥的备份文件),显式放行 .env.example
...
220 上切 MySQL 前留的 .env.bak_before_mysql 含生产 WEB_SECRET_KEY 与新加的
DB_PASSWORD,git status 显示为未跟踪 —— 一次 git add . 就会把生产密钥提交进去。
2026-09-13 13:11:41 +08:00
butubb
5f02edb5c3
feat: 浏览器标签名带环境前缀(dev-设备自动化后台 / 设备自动化后台)
...
同时开 dev 和生产的标签页时分不清谁是谁。标题取 web_server 注入的
db_env(非 prod 才加前缀),login / monitor / wall 三个页面一致。
2026-09-13 13:03:37 +08:00
butubb
c6fda262db
fix: 回归脚本两处"自己坏掉"的问题——GBK 控制台崩在汇总、写死过期设备串
...
在 MySQL dev 库上跑全量回归时暴露:
1. 汇总里的 ⚠/❌ /✅ 在 Windows GBK 控制台抛 UnicodeEncodeError,而且崩在打印
汇总那一步 —— 探测其实全跑完了,看起来却像脚本挂了(stdout 重设为 UTF-8)
2. `100.100.10.11:5555` 是 Tailscale 时代的地址,设备早换了:路径参数与两个关键
POST 都拿它当目标 → 每次回归要等好几轮 30s adb connect 超时,还误报
"关键 POST 测试步骤(wait) 失败"。改为**运行期从设备池挑一台启用设备**
结果(对 MySQL dev 库):99 项检查全通过(38 GET + 60 写探测 + 7 关键业务),
仅 3 条 Tailscale 未配置的业务提示。
2026-09-13 11:05:44 +08:00
butubb
da77d8eb6b
fix: 回归脚本 Windows 可用 + 加"禁止对生产库跑"安全闸;迁移脚本去掉 GBK 控制台会崩的符号
...
- scripts/regression_test.py:
* signal.alarm 加 hasattr 守卫(Windows 上直接 AttributeError 退出,
backlog A4)→ 本机终于能跑"一条命令扫全接口 500"
* 新增安全闸:.env 声明 prod、或目标库 app_meta.deployment_env=prod 时
**拒绝运行**(exit 3)。本脚本会发真实写请求(设备池增删/剪贴板注入/
亮灭屏),跑在生产配置上就是拿正式数据做实验
- scripts/migrate_sqlite_to_mysql.py: stdout 重设为 UTF-8 并去掉 ✔/✘ 符号。
实测在 GBK 控制台里打印 ✔ 会抛 UnicodeEncodeError,而且崩在"写库标签"之前,
看起来像迁移失败(数据其实已搬完)
- doc/backlog/TODO.md: A4 移入已完成;gitignore 条目更新(mcp_audit.log 已补,
uiauto.pid 仍缺);登记 MySQL 迁移与回归安全闸
实测(对 192.168.2.27 的 auto_control_dev):数据迁移 12 张表逐表 SHA-256 一致;
应用直连 MySQL 全部接口 200,设备名/指纹唯一索引语义与 SQLite 一致(空值可重复、
非空重复被拦、大小写敏感 A08≠a08);备份导出→预览→应用往返正常。
2026-09-13 10:48:36 +08:00
butubb
f5fc96c300
feat: 页面顶部环境徽标(防混库第 4 层)
...
启动日志和横幅只在日志里;用户在界面上操作时也该一眼看出连的是哪个库。
生产显示红底 'PROD · <库名@主机/库>',开发显示灰底 'DEV',鼠标悬停给出完整目标。
环境信息由 core/db_config.env_badge() 提供(不含密码),web_server 注入 Jinja 全局。
2026-09-13 10:38:13 +08:00
butubb
429aaf6283
feat: SQLite → MySQL 数据迁移脚本 + 建库建账号 SQL(P3)
...
- scripts/migrate_sqlite_to_mysql.py(新增):源库完整性校验 → 列宽审计(SQLite 不强制
长度,MySQL 严格模式下超长直接报错,先拦下)→ 建目标 schema(复用平台的 create_all
+ _sync_columns + 唯一索引)→ 逐表单事务搬行 → 逐表行数 + 全行 SHA-256 比对 →
写库环境标签与 schema_version。支持 --dry-run / --mode verify / --schema-only;
--env prod 必须 --allow-prod(+ 交互确认或 --yes);目标库已登记别的环境时拒绝
- scripts/sql/init_mysql_5.7.sql(新增):建 auto_control / auto_control_dev 两个库
(utf8mb4 + utf8mb4_bin)+ 专用账号与授权
- doc/DEPLOY.md §7.2:补 SQLite→MySQL 迁移流程与注意事项
比对口径踩坑记录:验证哈希必须两边都从**驱动层原生读**。走 ORM/Core 的 typed
select 会把 Boolean 读成 True/False,而源库 SQLite 原始读出来是 1/0 —— 三张带
布尔列的表(user/device/task_job)会全部误报"不一致"。现在统一用原生 SQL 回读 +
值规范化(bool→int、整数值浮点→int、bytes→str)。
自测(目标用 SQLite 代跑,MySQL 专属部分待 P4 真库验证):12 张表逐表 SHA-256 一致、
重复迁移幂等、verify 模式通过。
2026-09-13 10:37:15 +08:00
butubb
2a3775ffd5
docs: 同步 P5 备份机制重写——DEPLOY §5 / API §13 / DATA_MODEL §7 / DEVELOPMENT §4.3
...
- DEPLOY §5.1: 说明 SQLite 现在是「备份交换格式」,恢复走单事务整库替换,
跨环境导入默认拒绝;§5.2 覆盖清单改为「由 metadata 派生」;§5.3 三个目录
支持环境变量覆盖及原因
- API §13: 导出/预览/应用三个接口的字段补齐(db_backend/deployment_env/
source_db_id/source_env/current_env、force_env_mismatch、pre_restore zip)
- DATA_MODEL §7: pre_restore 改 .zip、补目录可覆盖说明
- DEVELOPMENT §4.3: 补三个备份目录环境变量
2026-09-13 10:35:26 +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
b145a007eb
docs: 同步 P1/P2——DATA_MODEL 重写引擎/建表/迁移/唯一索引/覆盖清单章节,ARCHITECTURE 更新装配顺序与数据库决策
...
- DATA_MODEL §1: 引擎改为「MySQL(正式)/SQLite(回退)」+ 连接参数 + 环境与库名绑定表;
表清单注明 12 张全部是 ORM 模型(原 5 张裸表已并入)
- §4 重写: 建表/补列以模型为准(create_all + _sync_columns),补 §4.2.1 说明
排序规则为何必须 utf8mb4_bin;唯一索引补 MySQL 的「生成列 + 唯一索引」方案
- §5 补 deployment_env/deployment_id/deployment_claimed_at 三个键
- §6 覆盖清单改为「由 metadata 派生」,红线只剩补 TABLE_LABELS
- ARCHITECTURE: 装配阶段 B/C/D 更新(db_config 装配 + 环境校验横幅)、
§3.2 并发描述按方言改写、§7 数据库决策表改写
2026-09-13 10:31:09 +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
f22263ab45
feat: 数据库连接层改造——库目标由 .env 装配 + 环境防呆(迁 MySQL 第一步)
...
为把数据库从单文件 SQLite 迁到 MySQL 5.7 铺路。本期不改后端:
DB_HOST 为空时仍走 SQLite,本地开发无感。
- config.py: 新增 DEPLOY_ENV(默认 dev)与 DB_HOST/PORT/USER/PASSWORD/NAME/
CHARSET/COLLATION、DATABASE_URL、两个逃生阀(DB_ALLOW_ENV_MISMATCH /
DB_ALLOW_SQLITE_FALLBACK)
- core/db_config.py(新增): URI 组装;按方言分叉的引擎参数(utf8mb4、
pool_pre_ping、pool_recycle=1800、READ COMMITTED、STRICT_TRANS_TABLES);
连接探活;app_meta 方言中立读写(MySQL 里 key 是保留字,需反引号)
- 防混库三层: ①库名与环境绑定(dev→auto_control_dev / prod→auto_control)
②库标签 app_meta.deployment_env 与 .env 声明比对 ③启动横幅打印当前库
(生产用 WARNING 级)。不符直接拒绝启动并说明两边分别是什么
- web_server.py: 硬编码 sqlite URI → db_config;配置错在装配期就 exit 2;
init_db 之后跑库标签校验 + 横幅
- core/models.py: PRAGMA 监听器加 sqlite 类型守卫——它挂在 Engine 基类上,
MySQL 连接执行 PRAGMA 会直接导致建连失败
- requirements.txt 加 PyMySQL;scripts/start.sh 依赖守卫加 pymysql,
并在 exec 前打印 DEPLOY_ENV/DB_NAME/DB_HOST
- .env.example 新增「数据库」段;DEVELOPMENT.md §4.1/4.2、DEPLOY.md §2.2 同步
验证: 用 DATABASE_URL 指向 users.db 的一致快照副本跑通主要只读接口
(health/devices/jobs/pool/groups/discovery/summary 全 200,app_meta 读写正常);
DEPLOY_ENV=prod 且无 DB_HOST 时退出码 2;开 DB_ALLOW_SQLITE_FALLBACK 后可回退。
2026-09-13 10:27:14 +08:00
butubb
933225784b
chore: .gitignore 补 data/*.log——MCP 审计日志落在 data/ 下,git add . 会误提交
2026-09-13 08:24:34 +08:00
butubb
64fadde568
fix: 断联设备表「换地址」按钮传参丢失(缺 data-serial)+ 删除任务/分组兼容内存不一致
...
- tools.js:断联设备表的「换地址」按钮漏了 data-serial 属性,onclick 里
this.dataset.serial 是 undefined → 接口报「缺少 old_serial 或 new_serial」。
设备池表里的同名按钮是传字面量的,所以只有断联表这条路径坏。
顺带给 relocatePoolDev 加参数防呆(取不到地址时给明确提示,不静默)
- task_manager.delete_job / delete_group:原先只在"内存里有"时才删库行,
内存与库不一致时(手工插行、上次异常退出、旧版本漏删)会出现"删不掉的
任务/分组"(API 404 但库行还在、重启复活)。改为无论内存有没有都尝试删库行
自测:浏览器验证断联表两个按钮都带 data-serial;relocate 接口端到端(上一轮已验);
删除接口在前端删除分组/任务后 DB 与内存一致
2026-09-11 11:23:19 +08:00
butubb
79cdf61ed2
feat: 自动认领开关 + 全站按设备名称显示
...
一、指纹匹配自动认领(可选,默认关)
- 发现设置新增「指纹匹配自动认领」勾选(app_meta: discovery_auto_claim,默认 0)
- 打开后:扫描发现某设备指纹与池中已有记录一致(同一台换了 IP)→ 自动迁移记录到新地址
并同步分组/任务引用,零点击;关闭时维持"识别自动 + 人工点一次确认"
- 默认关的原因:认领会改写分组/任务引用(数据结构变动),交人工确认更稳妥
- 扫描结果与状态行会显示本轮自动认领了几台
二、设备名称在界面上呈现(凡选择/展示设备处都显示名称)
- 新增前端 helper `devText(name, serial)`(base.js):有名称→「名称 · serial」
- 监控页设备表:名称加粗为主、地址作副行(未命名显示橙色提醒);任务概况的覆盖设备
chip 也优先显示名称(tooltip 保留完整地址)
- AI 控制台:目标设备下拉、实时画面设备下拉、目标/运行中提示都带名称(serial→name 映射)
- 任务编辑器「指定设备」下拉、分组编辑的设备勾选列表:带名称
- 后端 `/api/devices` 新增 `items`([{serial,name,model}],`devices` 保持兼容);
`/api/agent/devices` 增加 `name` 字段
- MCP `de_list_devices` 返回 `name`,并在工具说明与 Agent 系统提示里要求"汇报用名称、
调工具用 serial"
文档:API.md(items/name/auto_claim + §6.2.1 名称呈现表)、MCP.md(工具返回)
自测(全通过):自动认领端到端(开开关→扫描→自动迁址 + 名称保留 + 分组/任务引用同步 +
待连接池清理 + 开关默认关且可持久化);名称显示浏览器验证(监控页/覆盖设备 chip/AI 目标与
观看下拉/任务编辑器/分组弹窗/发现设置开关);设备指纹与人工认领回归
2026-09-11 11:16:24 +08:00
butubb
0a1b4d6122
feat: 设备身份改为「名称 + 指纹」——更换 IP 自动认领,分组/任务引用自动同步
...
背景:设备池原先拿 serial(IP)当身份。设备一换 IP(DHCP 重新分配)旧记录就成了连不上的
僵尸条目(表现为"断联·自动重连中"但设备并没关机),分组与 serial 模式的任务还吊着死地址。
2026-09-11 实际发生:.70 变成 .71、.72 消失,平台两个条目永远连不上。
实现
- **设备名称必填且唯一**:加入设备必须填名称;库层面用部分唯一索引兜底
(ux_device_name / ux_device_fingerprint,WHERE 非空 → 兼容历史空值),管理页可改名
- **设备指纹**(ro.serialno):网络设备添加/确认/扫描/采集型号时自动读取;
身份三层拆分——名称(人可读,稳定)、指纹(机器识别,稳定)、serial(当前地址,可变)
- **自动认领**:添加或确认设备时指纹命中池中已有记录 → 迁移原记录到新地址
(名称/型号/备注/启用状态/添加时间全保留),不新增条目
- **人工认领** `POST /api/devices/pool/relocate`:旧地址已断联、指纹没采过时的兜底——
人工指认"这条就是那台,现在在 X",迁移并同步引用
- **引用同步**:认领/迁址时把 device_group.serials 与 task_job.target.serial 的旧地址
换成新地址。⚠️ 必须同时改**内存**:分组/任务在 TaskManager 里另有内存副本且调度用内存对象,
只改库不重启不生效 → device_pool 迁址后回调 TaskManager.sync_device_serial
(装配层用 set_move_hook 注册;device_pool 不能反向 import task_manager,会循环依赖)
- **前端**:待连接池新增「识别」列(指纹命中时提示"≈ 名称(原 IP)",按钮变「认领为 X」);
设备池新增「名称/指纹」列与「改名/换地址」操作;断联设备表也加「换地址」(用户看到断联就在这里)
- 添加设备接口改用 adb_connect_light(单次短超时),避免不可达 IP 让请求卡 30s+;重名校验提前到 adb 之前
文档:API.md §6 重写(设备身份/认领/新接口)、DATA_MODEL.md(新列 + v5/v6 迁移 + 唯一索引)、
ARCHITECTURE.md §4.0(身份三层与引用同步的内存坑)、DEPLOY.md 排查表加"断联但没关机"条目
自测(全通过):名称必填/唯一/改名/重名拒绝(含库层面约束);指纹采集(真实读到 .71 的
gy7lskwkkvj7c6b6);自动认领(指纹命中→迁址+保留名称+带指纹+未命中不误判);人工认领
(迁址+名称保留+分组与任务引用同步);浏览器验证设备池/断联表/待连接池三个界面
2026-09-11 11:03:38 +08:00
butubb
24d57d3b96
docs: doc/ 全量重整——按现状重写并建立文档索引;项目统一更名 auto_control
...
背景:文档长期落后于代码(Tab 数、任务类型、接口示例等多处与现状不符),
且信息分散重复。这次按当前代码状态逐篇重写,并建立统一的文档体系。
新增
- doc/README.md:文档总索引(文档地图 / 推荐阅读路径 / **文档维护约定**)
- doc/DATA_MODEL.md:数据模型(7 张模型表 + 5 张非模型表、迁移机制、app_meta 键、
数据目录、备份覆盖清单与双向自检)
- doc/AI_CONSOLE.md:AI 控制台机制(会话与 SSE、经验库/动作库蒸馏与召回、巡检、
Markdown 渲染、推理链、token 统计、故障排查)
重写(按现状,去掉过时与重复)
- README.md:7 个 Tab、18 种步骤、设备生命周期、调度/窗口语义、常见问题;修掉
「6 个 Tab / 分组为顶级 Tab」等过时内容与损坏的目录树
- doc/ARCHITECTURE.md:补启动装配顺序(import 期副作用、A~G 七阶段)、线程与锁清单、
设备状态机、调度全链路、前端结构与实时通道、设计决策、**已知缺陷与踩坑清单**、扩展点
- doc/API.md:按蓝图重建「接口总索引」(107 条路由含鉴权)+ 分域详细说明 +
非 JSON 响应汇总 + 错误分支速查
- doc/TASK_DEV.md:18 种步骤全表(参数/默认值/语义)、容器与公共参数、
选择器与 XPath 序号语义、抓取器建议规则、新增任务类型骨架
- doc/DEPLOY.md:容器入口 start.sh 三件事、发布流程与检查清单、备份覆盖红线、
按现象分类的故障排查
- doc/DEVELOPMENT.md:流程/红线/本地开发/**测试与写测试的约定**/配置速查/文档同步
- doc/MCP.md:19 个工具的参数级清单、坐标空间、写门控三连、安全与审计
- doc/MCP_DESIGN.md、doc/AI_TASK_GEN.md:标注设计 vs 实现现状,补交叉链接
- doc/backlog/TODO.md:新增「已知缺陷」小节(含复现与影响)+ 已完成留档
- .env.example:按代码实际读取的键重写(补 USB/DISCOVERY/MCP/AGENT,删死配置)
其它
- 项目名统一 auto_control:README/文档/scripts/pack.py 产物名;代码内的
doc 章节引用(templates/admin/monitor.html)同步更新
- 校验:16 篇文档 156 条相对链接全部可解析;文档中的关键数字与代码核对一致
(19 个 MCP 工具 / 18 种步骤 / 12 张备份表 / 1 种任务类型)
2026-09-10 22:19:18 +08:00
butubb
f4b5316436
chore: 去掉 generic_steps 的默认步骤 + 重试失败带上真实原因
...
- tasks/generic/task.py:DEFAULT_PARAMS 只留 max_duration,**不再内置示例步骤**
(步骤只能由前端编辑器产出;此前默认的"打开抖音→循环看视频"只有后端在用,
编辑器新建任务本来就是空的,等于埋了个和界面不一致的隐性默认值)
- 空步骤不再静默空跑:worker 立即抛错「通用步骤任务没有可执行步骤…」,
设备表「最近错误」能看到(此前会走完 0 步当成功)
- core/task_manager.py:重试耗尽的 last_error 现在带上最后一次的真实失败原因,
不再只有"重试N次失败"(用户看不到为什么失败);max_attempts=1 显示"执行失败"
而非费解的"重试1次失败";长度截断 200 字符
- 文档:TASK_DEV §2.10 说明无默认步骤 + 空步骤会报错;API.md 的 task_types 示例
改 default_params={"max_duration":0},POST /api/jobs 补「steps 要一起传」提示
自测(全通过):default_params 无 steps;无步骤任务真跑一次 → 设备错误信息完整含原因;
有步骤任务(wait 1s)真跑一次 → status=done 无错误;临时任务验完已删,任务集合复原
2026-09-10 21:50:18 +08:00
butubb
98cdc39224
chore: 删除抖音养号任务类型,平台只保留 generic_steps
...
tasks/douyin/(task_type=douyin_nurture)整体删除,唯一任务类型是 generic_steps。
所有默认值/文档/接口示例同步改成 generic_steps:
- tasks/:删 douyin 包;__init__ 只注册 generic;base.py/generic 注释改为照 generic 抄
- 默认值:core/models.py(列默认 + 旧 JSON 迁移默认)、core/task_manager.py TaskJob、
web/tasks_api.py 建任务默认、static/admin/tasks.js 新建任务默认
- core/task_manager.py:去掉 douyin 专属的"清理废弃 comment 参数"迁移块,改为**启动时告警**
仍残留已删类型的任务(只告警不改数据);run_job_now 对已删类型直接返回明确错误,
不再"报已触发、线程里静默失败"
- 清理残留:douyin_running 状态位(无任何读取方)、core/__init__、core/actions/*、
core/logger.py 注释里的抖音示例
- 文档:README(特性/目录树/类型表/参数表/示例)、TASK_DEV(目录树/注册说明/模板引用)、
ARCHITECTURE(注册示例/action 注册表示例)、API.md(task_types 与任务 JSON 示例)、
AI_TASK_GEN(P1 去掉 douyin 预设)、DEVELOPMENT
- 注:示例里"抖音"作为**App 名**(MCP 列应用、AI 建任务的需求举例)保留,与任务类型无关
自测(全部通过):类型列表只剩 generic_steps;建任务不传类型默认 generic_steps;传
douyin_nurture 被 400 拒;库里塞残留旧类型任务 → 启动日志告警 + 执行返回明确错误 +
不自动删用户数据;前端新建任务下拉 1 项且默认选中、界面建任务成功;监控页卡片两个按钮 +
覆盖设备正常。临时任务/数据验完已清理,任务集合复原。
2026-09-10 21:43:08 +08:00
butubb
86c65236a2
fix: 监控页任务概况去掉「编辑/删除」,只留执行/停用 + 显示覆盖设备
...
监控页的任务卡片原来带「编辑」,但 openTaskModal 依赖 _taskTypes/_groupsList/
_devicesList,这三个列表只在任务 Tab 的 loadTasks() 里加载——从监控页直接点编辑,
下拉框是空的(这就是"编辑有问题")。按需求把编辑/删除从监控页移除(编辑统一去任务 Tab),
每张卡片只留:执行任务 + 停用任务/启用任务。
并给卡片加「覆盖设备」一行:
- 后端 /api/jobs(含 POST/PUT 返回的 job)新增 coverage = {mode, serials, total}:
按 target 定义解析(all=设备池全量 / group=分组∩设备池 / serial=该设备),
不做离线过滤、不写调度日志(该接口每 5s 轮询),与调度用的 resolve_serials 口径差异已写入文档
- 前端用已在手的 /api/status 标注在线状态,渲染成彩色 chip:
在线=蓝、运行中=黄+⏳ 、离线或不在池=灰划线+✕,完整 serial/型号放 tooltip
- 文档:API.md §5 补 coverage 字段与三种模式口径、ARCHITECTURE §5.1 补卡片说明
自测:coverage 三种模式端到端(含分组过滤池外 serial)全通过;Edge headless 验卡片
(2 个按钮、无编辑/删除、chip 文案/样式/tooltip)全通过;离线分支与任务 Tab 编辑器回归通过
2026-09-10 21:15:23 +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
4b5b836d31
feat: AI 控制台回答支持 Markdown 渲染 + 推理链可折叠 + token 用量显示
...
- markdown.js(新增,无 CDN 依赖):轻量 Markdown 渲染(标题/列表含嵌套/表格/
代码块/引用/链接…),先 esc() 转义再套标记,模型输出的 HTML 只当文本显示
- agent.js:回答改走 Markdown;推理链改为 <details> 可折叠(流式时展开、正文开始
自动收起、手动点过后不再自动改);单条消息 token 脚注 + 顶栏「本会话累计」
- monitor.html:消息结构加 .reasoning/.agent-usage、顶栏 token 徽标、md 相关样式,
引入 markdown.js(base.js 之后、agent.js 之前)
- mcp_agent/agent.py:请求带 stream_options.include_usage,按「每次模型调用」累计
usage(末尾 chunk),on_usage 回调吐累计值;网关不认该参数(400/422/点名)时
自动降级重试一次
- web/agent_api.py:SSE 新增 usage 事件、done 带 usage;推理链与用量随会话落库
(_REASONING_KEEP=6000 截断),回灌模型时只取 role/content
- 文档:API.md(usage 事件/done/会话消息字段)、ARCHITECTURE §5.4.1、DEVELOPMENT
前端 JS 清单
自测:假模型端点单测 3/3(正常/降级/多轮累加);Edge headless 全链路 27 项全通过
(真实 Flask+SSE+SQLite,含 XSS 转义、刷新后回看);Markdown 渲染器 18 用例全通过
2026-09-10 18:22:20 +08:00
butubb
1c2b440dce
docs: 新增 doc/backlog/TODO.md(待完成项)并登记进文档索引
...
收录:adb 远程终端目标切换(本机/220)、未命中要明确提示、uiautodev 超时 8s→30s、
序号型选择器/界面就绪防呆、动作库后续(合并面板/预制件)、MCP 平台级工具与 de_screen_text、
经验召回改进、命名统一、STF 容器状态矛盾、MCP 白名单语义缺口等。
2026-09-10 15:31:42 +08:00
butubb
972db13f81
fix: 经验/动作蒸馏不再静默丢弃(关推理 + 质量门槛 + 截断容忍)+ 前端会话显示 ID
...
问题:蒸馏模型把 token 预算烧在 reasoning 上 → content 为空/被截断(finish_reason=length),
代码只读 content → 经验与动作被静默丢弃("使用小红书找苏州饭店"跑完什么都没存,动作侧日志
'原始输出 0 字符')。
修复(web/agent_api.py):
- 蒸馏调用统一加 "thinking": {"type":"disabled"}(该代理支持;实测关掉后 reasoning=0、
配方 3/3 合格)——关键修复
- 配方:纯文本问法 + _recipe_ok 质量门槛(过短/含省略号占位丢弃,避免把提示词示例当真配方;
曾因提示词里写了占位示例,模型照抄成 "1. …\n2. …" 存进库)+ 空则重试一次;
仅动作提炼允许回退 reasoning_content(配方不回退,防思考草稿污染)
- 动作:JSON 输出 + _loads_lenient 截断容忍(逐对象抢救)+ {action,params} 形状归一 +
输入/产出限量(≤10 步、≤3 动作×4 步);失败日志带样本
- 前端 agent.js:会话列表显示会话 ID 前 8 位(等宽小字),点击复制完整 ID(便于引用 conv=<id>)
- doc/ARCHITECTURE.md:§3.8 序号语义 + §5.4 蒸馏健壮性与会话 ID 说明
实测:真机复跑同一句需求 → 经验已保存(配方 196 字符)+ 动作经验已保存 2 条
2026-09-10 15:31:39 +08:00
butubb
3f44cd1491
fix: 元素选择器序号语义 + 抓取弹窗直接点击测试
...
- uiauto_helper: 同属性多实例的选择器由 `//*[@id="x"][k]` 改为 `(//*[@id="x"])[k]`——
前者在 XPath 里是"父节点内排第 k",多实例时 [2..n] 全部失配(实测抖音底部 4 个同 id tab:
仅 [1] 可用),任务里表现为"未找到元素"但界面上元素明明存在。
- tasks/generic/task.py: 新增 _norm_legacy_xpath,执行前把**历史遗留**的
`//*[@attr=…][k]` 窄范围纠正为带括号形式(只改前缀,结构路径 …/FrameLayout[2] 的
兄弟序号保持不动)——已存任务无需重抓即可恢复。
- editor.js: 抓取弹窗每条元素新增「▶ 点一下」(按 bounds 中心真点一次,/api/screen/tap
snap=1 吸附)与「✓ 测选择器」(用将填入的选择器跑 /api/steps/test 验证命中),
点击后自动刷新截图;底部加用法提示。
- doc/TASK_DEV.md:写明 xpath 序号必须整体加括号 + 旧形态自动纠正 + 优先文字/唯一 id。
2026-09-10 15:31:33 +08:00
butubb
8072f4a380
docs: 补充动作库文档——ARCHITECTURE 加 agent_action 表与「AI 控制台记忆面板」小节、agent_api 职责含动作库;DEVELOPMENT 幂等建表清单加 agent_action
2026-09-10 14:03:09 +08:00
butubb
91b2785c6b
feat: 动作库查看/管理(接口 + AI 控制台「 🎬 动作库」面板)+ 未沉淀灰卡提示
...
- 后端:GET /api/agent/actions(列表)、POST /api/agent/actions/delete、
POST /api/agent/actions/save(新增/编辑,steps 走 _sanitize_actions 校验:
type 白名单 + 必填 + **拒绝坐标 click_xy**),均 @admin_required
- 前端:AI 控制台右上角新增「🎬 动作库」按钮与模态框——列出 名称/app/别名/
步骤摘要(含元素定位)/命中次数/更新时间;支持编辑(表单 + steps JSON)、删除、
手动新建;保存失败(如写坐标)即时 toast 原因
- 未沉淀提示:本轮无可沉淀动作时也推送一张 🧠 动作经验 灰卡
「本轮未沉淀动作(步骤以坐标定位为主,缺少可复用的元素定位信息)」
- doc/API.md:登记三个新接口
实测:列表返回自动沉淀的动作;保存合法动作 200;提交 click_xy 步骤被 400 拒绝;
删除生效
2026-09-10 13:58:38 +08:00
butubb
104964aa53
feat: 动作经验库(agent_action)——成功步骤蒸馏命名动作(带元素定位/禁坐标)+ 执行前召回注入
...
- 新表 agent_action(name/app/aliases/params/steps/preconditions/hits/时间),
独立于人工维护的 custom_action(2B 决策):AI 自学动作不污染手建动作
- 沉淀:任务成功后从**成功**工具轨迹(_ACTION_TOOLS: open_app/tap_text/tap_element/
type_text/clipboard/swipe/press_key/wake/sleep)用模型蒸馏为命名动作;steps 用
编辑器 schema,**必须元素定位**(xpath/text/resourceId/description…),
**显式剔除 click_xy 等坐标类**;on_tool 记录带 result 的结构化轨迹以判成败
- 兼容模型形状漂移:顶层 {action,params} 自动归一为 {name,steps};宽容 JSON 解析
(围栏/尾逗号/中文引号/坏对象逐条抢救),实测模型常返回带语法错误的 JSON
- 召回:执行前按动作名/别名命中(或相似度≥0.34)取 top3,注入 system prompt
「可复用动作」段(含元素定位),模型可跳过重新探索;hits 回写
- 文档同步:ARCHITECTURE §3.6(agent_action 表)、API.md(🧠 动作经验 伪卡片 + 动作库
说明)、AI_TASK_GEN P1(沉淀进展)
实测:跑「打开抖音,点搜索」→ 沉淀「打开抖音」;下一轮同指令命中并注入;日志
「命中可复用动作 1 个」「动作提炼: 轨迹 5 步, 成功可沉淀 1 步」「动作经验已保存 1 条」
2026-09-10 13:53:04 +08:00
butubb
4e71f79a10
fix: 经验库命中数算错(单条恒显示 0)+ 蒸馏前缀误杀 + 命中卡片带摘要
...
- _find_experiences 改为返回 (注入文本, 命中配方列表):此前卡片用
exp_ctx.count('\n- ') 计数,单条恒为 0、两条为 1(永远少 1),用户看到
「命中 0 条」误判为没参考;现用 len(列表) 得真实条数并附配方摘要
- 新增 _clean_recipe():清洗模型可能加的「操作配方:」前缀,替换旧判定
"配方" not in recipe[:50]——该判定与蒸馏提示词(要求以「操作配方」作答)
自相矛盾,模型照做即被整条丢弃(静默),是经验存不下来的主因
- doc/API.md:SSE step 说明补 🧠 伪卡片语义(N 为实际条数 + 摘要)
实测:同一指令由「命中 0 条」变为「命中 1 条同类历史经验,已注入参考:<摘要>」
2026-09-10 13:41:10 +08:00
butubb
19560aba3e
docs: 数字员工知识库升级到平台同级——补操作纪律/状态检查/SOP
...
- KNOWLEDGE_BASE.md 重写:§0 快速开始;§5 操作纪律(与内置 AI 控制台等价的 10 条:
先看设备/先看屏/定位三段优先级/吸附验证/输入两拍/每步验证/无变化不重点/如实汇报/
效率/连续 6 步无进展停止);§6 开跑前状态检查 7 项(含「屏幕是否点亮/解锁」:
用 screen_state+画面判断,黑屏先 de_wake 再重截,未确认亮屏不许点按);§7 执行中
状态判据(息屏/锁屏、页面到位判据表、加载抖动、幂等重放注意);§8 SOP 五阶段+闸门
(P0 澄清→P1 预检→P2 到起点→P3 观察-行动-验证循环→P4 收尾还原→P5 固定汇报口径)
含卡住判定表(2 步换策略/6 步停);§9 异常处置速查;§10 效率预算
- JOB_SPEC.md §4 SOP 改为指向知识库 §5-§8 + 五阶段概览,两份不打架
2026-09-10 13:41:06 +08:00
butubb
357fe98e22
fix: AI 控制台 MCP 不可达给出明确文案——不再显示 SDK 含糊报错
...
MCP 客户端(streamable_http)在工具服务不可达/返回非 MCP 响应时只抛
'Server returned an error response',用户无法判断原因。改为:
- 后端 web/agent_api.py:_execute 包裹 _load_tools/run_stream,识别连接类错误
(Server returned an error response/ConnectError/refused 等)后抛明确文案:
'MCP server(8033) 不可达:无法加载设备工具(<url>)。请确认 MCP server 已启动…'
- 前端 static/admin/agent.js 加 _friendlyAgentError() 兜底映射并去掉
'RuntimeError:' 之类前缀,三处错误展示统一使用
- 文档同步:MCP.md(依赖提示+本机启动命令)、API.md(SSE error 文案说明)、
DEPLOY.md(故障排查新增一行)
实测:停掉 MCP 复现 → 前端显示明确文案;启动 MCP 后 AI 控制台正常完成任务
2026-09-10 10:28:57 +08:00
butubb
4e67764589
docs: 新增 StaffDeck 数字员工知识库与岗位说明
...
- doc/staffdeck/KNOWLEDGE_BASE.md:接入方式(MCP http://<host>:8033/mcp 首选/REST 备选)、服务端配置项、19 个 de_* 工具清单(读写分类)、通用约定(serial/坐标空间/busy 占用锁/错误码)、推荐操作模式与常见配方、红线、当前边界
- doc/staffdeck/JOB_SPEC.md:岗位描述、看板摘要(指标口径+文本/JSON 汇报模板)、岗位执行约束(L0-L3 授权分级/硬红线/操作规范/失败重试/审计)、SOP 工作流、应拒绝与转人工清单
- doc/DEVELOPMENT.md:§7 文档索引与 §5.6 同步映射登记这两份
2026-09-10 10:28:53 +08:00
butubb
fd829a6063
docs: doc/ 全量同步 dev 现状——API 目录补全(AI 控制台/系统备份/自动发现等)、去 STF 过时口径、补 generic_steps 与配置键速查;确立「功能/配置改动须同步文档」红线
...
- doc/API.md:补方法/路径标题,权限分层修正,新增 AI 控制台(/api/agent/*)、系统备份(/api/system/backup/*)、设备自动发现(/api/devices/discovery/*)、tap_text/summary/health/devices-apps 等整节端点,去 STF 残留
- doc/TASK_DEV.md:STF 时代描述清理;新增 §2.10 generic_steps(18 节点与必填/嵌套/静默跳过语义)、§2.11 自定义动作与单步测试、/api/jobs 盲存校验语义、resolve_serials/抢占语义、模板构造函数签名修正
- doc/DEPLOY.md:数据备份改为推荐「系统→数据备份」功能并说明重启生效目录,端口表 STF7100→MCP8033,补 start.sh 生产链路与 MCP_PLATFORM_PASS 同步,故障排查去 STF
- doc/MCP.md:加「现状边界」(平台级任务 CRUD 未 MCP 化,规划见 AI_TASK_GEN §9),busy/平台会话说明,MCP_ALLOWED_SERIALS 语义纠正
- doc/MCP_DESIGN.md:加实现现状对照、错误码、独立容器改演进备选、里程碑状态、API 映射表按实现重写
- doc/ARCHITECTURE.md:Tab/子分栏/线程模型/数据表/蓝图表去 STF,补 device_discovery/agent/system_backup/经验巡检等
- doc/DEVELOPMENT.md:新增 §5.6「改动必须同步文档」红线、§2.3 配置键速查、蓝图化新增 API 流程、文档索引补登记
- doc/STF_REMOVAL.md:加历史记录状态横幅
- doc/AI_TASK_GEN.md:新增 AI 建任务设计稿(含 §9 需转 MCP 工具分层)
2026-09-09 16:05:56 +08:00
butubb
470c76221e
feat: 顶栏新增「系统」栏目——数据备份导出/导入恢复子分栏(仅管理员)
...
monitor.html:导航 .tabs 追加「系统」(data-perm=admin) + #tab-system 面板,
内含「数据备份 / 导入恢复」两个子分栏;base.js _activeSubs 记忆子分栏并在
showTab 里切回;system.js:导出(含 APK 勾选→zip 下载)、导入(选文件→
校验预览:文件/schema/表行数/告警→确认应用,原生 confirm),全程含
「全量快照含密钥」警告与重启生效提示。
2026-09-09 14:49:40 +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
butubb
65bab7e25c
fix: schema 迁移幂等兜底——create_all 已建列而 schema_version 未记录时重复 ALTER 报 duplicate column,遇错回滚并照记版本自愈
...
典型场景:create_all 已按当前模型把列/表直接建好(如 perms、device.model),
而 schema_version 又因历史中断没记录,导致每次启动重复 ALTER 报错。
duplicate column name 说明列已存在=迁移目标已达成:回滚本次语句后仍记录
版本号,一次启动即自愈;其它异常才中止本批迁移。
2026-09-09 14:16:15 +08:00
butubb
4c0572f73c
fix: 设备自动发现本机IP枚举——git-bash hostname -I 报错炸线程,改 bytes 接收 + getaddrinfo 兜底
...
Windows 上 git-bash 的 coreutils hostname 不支持 -I,会把 GBK 报错写进
stderr;text=True 在 subprocess 后台读线程里 utf-8 严格解码会直接炸线程
(主线程 try/except 接不住异步线程异常)。抽 _local_ips():优先 hostname -I
且用 bytes 接收 errors=ignore 解码,不支持/失败时 getaddrinfo 枚举兜底。
扫描网段展开时剔除本机自身 IP,避免探测到自己 5555。
2026-09-09 14:16:13 +08:00
butubb
caa646e89c
fix: 会话落库/加载缺 app context——后台线程 db 操作必须包 _flask_app.app_context()(历史加载与完成写回两处),否则静默失败(消息不落库、标题不生成)
2026-09-06 15:14:30 +08:00
butubb
a77635c925
feat: AI 控制台历史会话(DeepSeek 式)——①agent_conversation 表:会话持久化(单表 JSON 消息),一轮 run 完成后 user/assistant 自动落库、新会话以首条消息作标题;②会话 API:列表/新建/详情/删除/重命名;③run 绑定 conversation_id:history 从会话加载(多轮上下文延续)、完成写回;④前端三栏布局:左侧会话栏(新建/列表/高亮/悬停删除/轮数时间)+ 聊天 + 实时画面,切换会话即切换上下文,刷新后自动恢复当前会话与消息
2026-09-06 15:12:56 +08:00
butubb
9b8ecd63f7
fix: AI 控制台切页/刷新不丢——①EventSource onerror 不再误 endRun(断网/后台节流/瞬时抖动都会触发 onerror 而后端任务仍在跑;EventSource 自动重连 + 服务端队列保留积压事件,重连后补发,连接彻底关闭才提示可刷新恢复);②页面刷新恢复:GET /api/agent/run 增加 run_id/answer/error/history,前端 restoreAgentView 渲染历史轮次、running 时自动重订阅事件流、done/error 展示结果——切走再回来任务与对话都在
2026-09-06 09:41:02 +08:00
butubb
0f75db289d
feat: AI 目标设备必选 + 设备任务占用锁——①前端新增「 🎯 目标设备」选择器(在线设备含型号,任务运行中的设备禁选),发送必须选定设备,AI 只操作你指定的设备;②/api/agent/run 校验:serial 不在池/离线/任务 running-connecting → 拒绝(409 提示任务名);③MCP 11 个写工具加 busy 锁(_ensure_device_free,5s 缓存):任务运行中的设备 AI 一律拒绝,AI 不与任务抢设备(外部 MCP 客户端同样受保护);④运行状态端点 GET /api/agent/run + 前端 8s 轮询:多人/多窗口能看到运行中任务(发起时间/任务/设备)并可停止(stop 加确认);未选设备直接 400 引导请选择设备,去掉模型自己乱挑设备的行为
2026-09-06 09:32:11 +08:00
butubb
850d2e2e66
feat: 最大执行步骤可配置——AI 控制台 ⚙ 配置弹窗新增「最大执行步骤」输入(1-200,默认 40),存 app_meta(agent_max_steps),Agent 线程运行时覆盖 agent.s.max_steps;此前仅环境变量 AGENT_MAX_STEPS 可改
2026-09-06 09:26:53 +08:00
butubb
f7aa71cca4
fix: AI 控制台实时画面跟随 AI 所选设备——后端 step 事件 args 原为 str() 的 Python repr(单引号),前端 JSON.parse 失败导致 followSerialFromArgs 从未生效;改为保留对象序列化,前端兼容对象/字符串并修 step 卡片 args 展示(避免 [object Object])
2026-09-06 09:21:03 +08:00
butubb
edfd529c59
feat: 正式池断联设备自动重连——①扫描线程每轮顺带对断联的网络设备 adb_connect_light(幂等,连上即恢复,无需人工;此前只有启动时预连接一次,断联后不会自己回来);②断联设备不删不进待连接池(pending 语义=未授权新设备),GET discovery 返回 pool_offline(serial/型号/备注);③POST /api/devices/discovery/reconnect 手动立即重连;④发现面板加「设备池断联设备」区块(自动重连中 + 立即重连按钮)
2026-09-05 09:58:42 +08:00