lemonmmice/dsh-agent-toolchain--plugins-dsh-ui-drive ↗★ 0
dsh-ui-drive
通过Windows UIA驱动桌面客户端并截图进行视觉复核 适合需要自动化测试、UI驱动及视觉验证闭环的开发任务。
安裝
此插件尚未提供可驗證的 bundle,或相容性檢查未通過。請先閱讀倉庫說明。 閱讀完整 README ↗
說明文件
閱讀完整 README ↗dsh-ui-drive
DSH(DeepSeek Harness)的 UI 自验驱动插件:通过 Windows UIA 程序化操作正在运行的桌面客户端并截图留证, 支撑「改完代码 → 启动/驱动客户端到目标页面 → 截图 → 视觉复核」的自验闭环。
应用授权、回放与视觉兜底
ui_status 返回应用身份:exeCanonical、binaryName、productName、aumid、
publisherName 和 publisherVerified。发布者来自验证成功的 Authenticode 签名;
company 只是版本信息。无签名、无 AUMID、读取失败分别由 signatureStatus / aumidStatus 表示。
DSH_UI_APP_POLICY 的 allow/deny 规则支持这些身份字段和 windowHandle / aid,所有指定条件同时匹配。
显式 deny、冲突、损坏的策略不能被授权 ID 覆盖。
授权由本机运维接口管理,路径前缀为 /api/dsh-ui-drive:
POST /approvals/grant:Content-Type: application/json,请求体上限 16 KiB,读取超时 5 秒。GET /approvals:查看当前宿主的会话授权和磁盘上的持久授权。DELETE /approvals/{id}:撤销授权。
授权请求示例:
{
"scope": "session",
"sessionId": "verify-dialog",
"identity": { "exe": "C:/Apps/Client.exe" },
"actions": ["click", "clickat"],
"ttlMs": 900000
}
once 消费一次;session 绑定当前宿主内的会话;persistent 有有效期且跨宿主重启保留;
permanent 保留到撤销(也可显式设置有效期)。默认有效期为 15 分钟。
空应用身份会被拒绝,持久化失败会返回错误。磁盘文件由 DSH_UI_APPROVAL_FILE 配置,
默认 ~/.dsh-agent-toolchain/ui-approvals.json,每次使用重新读取以看见撤销。
once / session 不写磁盘,DSH 与 MCP 是不同宿主,不能互用内存授权;持久授权可共享文件。
UI Agent 工具只消费 approvalId 和 sessionId,未提供签发或撤销工具;本机回环运维接口不构成操作系统级隔离。
未传授权 ID 时保留已有 allowSideEffects + 应用规则行为,授权 ID 不替代 allowSideEffects=true。
锁屏检查在 PowerShell 真正输入前执行,覆盖单次、常驻和流程路径,也覆盖鼠标移动和滚轮。
desktop_locked / desktop_secure / desktop_unknown 默认拒绝输入;只读状态查询仍可使用。
运维显式设置 DSH_UI_ALLOW_LOCKED=1 或 true 仅允许已确认锁屏的桌面,安全桌面和未知状态仍拒绝。
desktopState 由系统查询,动作参数不能伪造它。
每次单步返回 actionId,观测返回 observationId、observationRecord(含 digest / frameHash)。
副作用证据升级为 v3,并能校验原 v2 证据。ui_flow 同时写 steps.json 和 replay.json,
返回 replayId / replayPath;ui_replay(replayPath, allowSideEffects, approvalId?, sessionId?) 可重新运行。
回放校验内容哈希、应用身份与步骤字段,重新经过当前授权和桌面检查;哈希用于发现内容变化,不是授权签名。
输入值脱敏后需要重新提供,完整 ${cred:name} 占位符可保留;临时 PID / 窗口句柄绑定的记录不能直接重放。
新执行生成新的 replayId,sourceReplayId 指向来源。授权或视觉流程逐步执行,普通流程仍使用批量引擎。
failFast 在执行器内停止后续步骤;结果未知或策略拒绝也停止受控流程。
ui_drive / ui_act 的 click、doubleclick 支持 visualFallback=true(默认关闭)。
UIA 确认没找到目标才尝试视觉定位;不对超时或执行结果未知的动作重试。
visualTarget 可提供目标描述,visualMinConfidence 默认 0.78(范围 0.5–1)。
截图使用已有 describe-image 视觉服务,会发送到配置的模型端点;敏感焦点或焦点未知时停止。
兜底只支持未限定容器/索引/正则的目标,要求截图物理像素与窗口坐标一致、窗口及前后帧哈希不变。
坐标越界、低置信度、窗口移动或画面变化都会拒绝点击。最终 clickat 仍经过授权;使用授权 ID 时,
actions 需同时覆盖原动作和 clickat。结果中的 visualFallback 明确记录尝试、拒绝原因或点击坐标。
能力
Agent 工具(host 侧注册)
| 工具 | 说明 |
|---|---|
| `ui_status` | 目标客户端进程/主窗口状态(只读) |
| `ui_launch` | 启动客户端并等待主窗口;视觉即返:完成后自动截图+视觉模型描述界面(返回 `uiState.description`) |
| `ui_observe` | 只读观察(推荐入口):find / read(含输入框真实值)/ state(窗口+焦点+交互控件快照)/ windows / waitfor / expectwindow / expecttext / waitany / shot |
| `ui_act` | 真实操作:click / setvalue / key / type / drag;`observe=true` 动作后附带界面快照;需 `allowSideEffects=true` |
| `ui_windows` | 列出该进程所有顶层窗口(登录窗口/弹窗/主窗口各自一行,只读) |
| `ui_state` | 界面快照:当前窗口 + 焦点元素 + 交互控件清单(只读) |
| `ui_drive` | 通用单步入口(等价 ui_observe + ui_act 的并集,保留兼容) |
| `ui_tree` | 进程内视觉树 dump(真实类型 + Name + AutomationId + DataContext 类型,只读深查) |
| `ui_flow` | 步骤序列自验:find/click/setvalue/key/type/drag/read/state/windows/shot/wait/waitfor/expect/expectwindow/expecttext/waitany,统计 passed/failed,证据落盘 steps.json |
ui_replay | 校验并执行 ui_flow 的 replay.json,重新检查应用身份、授权与桌面状态 |
| `ui_live` | 实时看见(agent 专用):`action=start|stop|status|frame|wait` 后台循环抓「窗口内容」帧(默认 1500ms,不抢前台、不恢复最小化);`frame` 返回 latest.png 路径 + 帧 hash + 控件状态摘要,`wait` 可阻塞等画面变化;敏感帧(焦点=密码/验证码)默认不出 path(`allowSensitive=true` 才给) |
动态界面(登录、验证码、按界面情况分支)的正确用法——不是预排固定点击序列,而是「看一步再做下一步」:
```text ui_observe(action="windows") # 现在是登录窗还是主窗?有哪些弹窗? ui_observe(action="state", match="登录|验证码") # 焦点在哪、按钮什么文案、输入框填了没 ui_act(action="setvalue", aid="phoneBox", value="138…", allowSideEffects=true) # 受限输入框用 setvalue ui_act(action="click", name="获取验证码", allowSideEffects=true, observe=true) # 动作后直接带回界面快照 ui_observe(action="waitany", ms=30000, conds=[ {kind:"window", titleRe:"主界面", label:"success"}, {kind:"text", textRe:"密码错误|不能为空", label:"error"}, {kind:"window", titleRe:"登录", label:"still-here"}]) # 一次押注三支,返回命中的那支 ```
跨窗口能力是关键:登录成功 = 登录窗关闭 + 主窗出现(本客户端没有成功弹窗), 所以 `expectwindow` / `waitany(kind="window")` 才是判定登录结果的唯一可靠信号。
输入正确性(实测踩过的坑):本客户端登录页手机号框 `PreviewKeyDown` 只放行数字键, `key` 的剪贴板粘贴会被静默吃掉——所以受限输入框用 `setvalue`(ValuePattern 绕开按键过滤), 且 `key/type` 写完会回读校验,值没进去直接 `ok:false`(不再假成功)。
凭据:用 `${cred:name}` 占位符,驱动进程从自己的环境变量 `DSH_CRED_name` 展开, 密码不经过模型上下文、不落证据;密码/验证码类控件的值在 read/state 里只回长度(``)。
硬护栏(驱动层强制,不是提示词):命中「按名硬拒」名单的控件一律拒绝(默认名单为空 = 默认不拦任何控件,拦哪些由 `DSH_UI_DENY_RE` 指定), `allowSideEffects=true` 也解锁不了;副作用超时返回 `unknown` 且绝不重放。
视觉闭环:主模型不读图也能视觉复核——截图 → 插件内置视觉模型(复用 `describe-image` 配置)描述界面, 或截图复制到 `/.dsh-ui-evidence/` 后用 `describe_image` 深度复核。
Web 路由(仅回环 127.0.0.1):
- `GET /api/dsh-ui-drive/status` — 插件/客户端状态
- `GET /api/dsh-ui-drive/evidence` — 证据目录列表
- `GET /api/dsh-ui-drive/evidence/{id}` — 某次自验的 steps.json
- `GET /api/dsh-ui-drive/evidence/{id}/files/{file}` — 截图/日志原文
- `POST /api/dsh-ui-drive/shot` — 立即截图直出 PNG
安全边界
- 点击/输入 = 真实操作,可能落库
- 插件硬护栏:click / setvalue / key / type / drag 必须显式 `allowSideEffects=true` 才执行
- 驱动层硬拒绝:命中「按名硬拒」名单的控件(按控件名/AutomationId 匹配),传 true 也点不动
- 输入回读校验:key/type/setvalue 写完回读控件真实值,不一致即 `ok:false`(防「假成功」)
- 凭据不落地:`${cred:name}` 占位符 + 密码/验证码控件值只回长度;敏感值在输出/证据里打码
- 超时不重放:副作用动作在常驻进程超时时返回 `unknown`,绝不自动重试(避免点两次/输两次)
- 软约束(systemPrompt 公告):「保存/删除/清空/导出」点击前报按钮名给用户确认; 命中「按名硬拒」名单的控件一律不点;优先只读验证;定位卡住三步即停,不盲点轰炸
环境变量
| 项目 | 说明 |
|---|---|
| `DSH_UI_PROC_NAME` | 目标客户端进程名(如 `MyClient`),必配 |
| `DSH_UI_WINDOW_NAME` | 主窗口标题,必配 |
| `DSH_UI_CLIENT_EXE` | 客户端 exe 绝对路径(ui_launch 用),必配 |
| `DSH_UI_EVIDENCE_DIR` | 证据目录,默认 `~/.dsh-agent-toolchain/ui-evidence` |
| `DSH_SNOOP_DIR` | Snoop 注入器目录(ui_tree 探针用),必配 |
| `DSH_UI_POWERSHELL` | PowerShell 路径,默认 Windows PowerShell 5.1 |
安装
`~/.dsh/profiles/web/cordis.patch.yml` 追加:
```yaml
- insert:
- id: ui-drive name: './plugins/dsh-ui-drive/index.js' ```
重启 DSH 生效。
实时性(2026-09 优化)
UI 驱动最早的瓶颈是「每个动作新起一个 PowerShell 进程」:进程启动 + 脚本解析 + 5 个 UIA 程序集加载 = 每步 ~900ms 固定成本,10 步的自验流程就要等 9 秒, 控件点击根本谈不上「实时」。现在分三层消除这份成本:
| 路径 | 做法 | 实测(本机,13 步流程 / 单动作) |
|---|---|---|
ui_flow 批量执行 | 整个步骤序列交给一个进程(ui-drive-batch.ps1),程序集只加载一次、主窗口只解析一次 | 13 步 11.9s → 1.6s(7.6x,~120ms/步) |
ui_drive 常驻进程 | 插件内维护一个 serve 模式 PowerShell(-Serve,stdin/stdout JSON 协议),启动成本只付一次 | 单动作 p50 886ms → 30ms(~30x) |
ui_status 快路径 | -Status 模式不加载 UIA,只用 Win32 取主窗口句柄/矩形 | ~1000ms → ~400ms |
其他配套改动:
- 默认
waitMs1200 → 250ms;find/read/shot/expect步不再做无意义静默等待 find改用 UIA 原生FindAll+AndCondition,不再手写遍历整棵树的循环- 常驻进程空闲超时自动退出(
DSH_UI_SERVE_IDLE_MS,默认 5 分钟),插件卸载时回收 DSH_UI_SERVE=0可关闭常驻进程,退回一次性进程路径(排查用)
实现
index.js— host 插件入口:工具注册 + systemPrompt 公告 + 证据路由lib/driver.mjs— PowerShell 进程封装(常驻进程协议、批量执行、超时、杀进程树、输出解析)lib/vision.mjs— 界面截图视觉描述(复用 describe-image 配置)scripts/ui-drive.ps1— UIA 单步驱动(find/click/setvalue/key/read/shot/status)scripts/ui-drive-batch.ps1— 批量执行(-StepsFile)+ 常驻进程(-Serve)+ 快路径状态(-Status)scripts/ui-probe.ps1+probe/UiProbe.cs— 进程内只读视觉树探针(复用 Snoop 注入器)test/flow-batch.test.mjs— 批量引擎离线单测(护栏、降级、步骤文件规则)
环境变量(补充)
| 项目 | 说明 |
|---|---|
DSH_UI_SERVE | 0 关闭常驻进程(默认开启) |
DSH_UI_SERVE_IDLE_MS | 常驻进程空闲回收毫秒,默认 300000 |
DSH_UI_LIVE_DIR | ui_live 帧目录,默认 ~/.dsh-agent-toolchain/ui-live(可指向大盘/独立卷) |
DSH_UI_LOCK | 客户端进程级锁开关,0 关闭(默认开启,防多 agent 同时驱动同一客户端) |
DSH_UI_LOCK_STALE_MS | 锁的过期毫秒(持有者崩溃后自动失效),默认 120000 |
DSH_UI_LOCK_WAIT_MS | 抢锁等待上限毫秒,默认 30000 |
行为变更与修复(2026-09-10)
本轮回灌把长期只存在于本机运行副本里的修复同步进仓库,逐条如下:
read不再偶发返回 0 行(可靠性根因修复)Get-ControlTypeName原先直接$el.Current.ControlType.ProgrammaticName.Replace(...): 界面重绘/切页瞬间 UIA 会枚举到ControlType=null的瞬时元素,.Replace()打在 null 上抛 「不能对 Null 值表达式调用方法」,整次枚举崩掉 → 0 行(会被误读成「客户端没响应」)。 现在:该函数全程 try/catch 降级为Unknown;read分支逐元素 try/catch 并缓存$cur快照; 带match却读到 0 行时自动重试一次(返回字段新增attempts)。read输出新增help="…"与控件尺寸WxH,且match同时命中 Name 与 HelpText WPF 在AutomationProperties.HelpText为空时会回落ToolTip,所以只显示图标的按钮 (Name 为空、语义只在 ToolTip)现在可以直接按 help 读到。注意:click/find的match目前仍只匹配 Name(见下「已知缺口」)。- 只读动作不再改变窗口状态
旧实现每次调用都无条件
ShowWindow(hwnd, SW_RESTORE):对已最大化窗口等价于「还原成浮窗」 (用户肉眼可见,也是坐标漂移的根因)。现在用IsIconic门控:只有真的最小化才恢复; 只有输入/截图类动作才抢前台。 - 截图只写证据目录
shot/capture一律写DSH_UI_EVIDENCE_DIR(默认~/.dsh-agent-toolchain/ui-evidence), 不再往 workspace/仓库复制副本——仓库只放代码证据。 - 新增
ui_live模块(lib/live.mjs) 后台循环抓帧 +wait等变化;warmSend的killOnTimeout=false语义保证后台循环超时 不会误杀常驻进程;插件卸载时先停 live 循环再回收常驻进程(顺序固定,避免双 timer/孤儿进程)。
2026-09-10 第二批修复
click/find/waitfor的match现在同时匹配 Name 与 HelpText(新增Test-MatchText助手, 逐元素 try/catch 保护)→ 空 Name、语义只在 ToolTip 的图标按钮(放大/缩小/筹码…)现在可以直接click match="放大",不必再退化成坐标点击。- 工具层补齐坐标类动作文档:
ui_drive/ui_act现列出clickat/doubleclick/move/wheel,ui_observe列出move/wheel/capture/state-live。此前脚本层已支持这些动作,但工具描述里 没写,agent 无从知道,只能绕到自带 harness 里发坐标点击——这是「能用但没人知道」型的缺口。 文档同时标注:坐标类动作脆弱(窗口一移动就失效),优先用find/click+match。
2026-09-10 第三批修复:观测完整性必须报数(B-1)
问题:上一批把 read 的假空真凶(Get-ControlTypeName 在 ControlType=null 上抛异常 → 整次枚举
崩成 0 行)改成了逐元素 try/catch + continue。但静默 continue 是新一轮假空:调用方拿到一份
「变少了的控件清单」,却不知道少了几行、为什么少 —— 「观测不完整」和「界面真的没有」分不开。
现在:
read/state结果恒带skipped=N:本次枚举里「读不到元素状态」而被跳过的数量 (元素失效Item抛异常 /Current抛异常 / 元素与状态为 null)。 类型白名单、IsOffscreen、match过滤、同名去重这些正常过滤一律不计数——报数只报真正的观测失败。skipped > 0时同结果附带warn(⚠ 跳过 N 个读不到状态的元素,本次清单不完整(原因…)——不要把 「没读到」当成「界面上没有」),由lib/render.mjs渲染进ui_drive/ui_observe/ui_state的可见文本;ui_flow的每一步 transcript 同样带skipped/warn`,步进日志会打印「(跳过 N 个…)」。skipped=null= 该路径没有回报(未知),不等于 0:绝不把「不知道」伪装成「观测完整」。state路径顺带补齐逐元素容错:Get-InteractiveLines原先裸取$el.Current,界面重绘瞬间会被 单个坏元素打断成 0 行(与read那处根因同源)。现在同样逐元素容错并计 skipped。- 一次性回退脚本
ui-drive.ps1的read同步修:它的Current.ControlType.ProgrammaticName.Replace(...)还是旧写法(同样能崩成 0 行),现在逐元素容错并输出协议行SKIPPED(+ 最多一条SKIPREASON), driver 侧解析成skipped/skippedReasons。 - 渲染层(
renderDrive/renderState)抽到lib/render.mjs:渲染文本是 agent 唯一看得见的契约,必须能离线单测 (index.js依赖宿主@deepseek-ai/dsh-tools,普通 node 进程 import 不到)。顺带修好ui_observe(action=state)过去因renderDrive没有state分支而回落成「完成」不显示清单的问题。
单测:test/read-skips.test.mjs(离线,不需要客户端/不启动 PowerShell,已进 CI 11 → 12 道闸门),
覆盖「报数 / 零值不误报 / 未回报为 null / flow 每步透传 / 渲染可见 / 回退脚本 SKIPPED 协议行」21 项断言。
现场验证(脚本层真路径,非单测):test/read-skips-live.ps1 起一个自建 WPF 窗口,分两阶段跑
scripts/ui-drive-batch.ps1 的 read/state:
- 阶段 A(窗口静止):清单必须非空(
count>0)——证明正常路径没被改坏、skipped不是假警报; - 阶段 B(窗口高频变动:虚拟化列表滚动 + 尾部控件拆建):必须回报
skipped>=1, 并给出原因(实测最稳定的瞬态形态是「整次枚举失败:目标元素的对应 UI 不再可用」)—— 这正是 B-1 的核心:读不到的元素要报数,而不是静默少几行; - 任何一轮
ok!=true都算失败(异常必须显式暴露,不能被当成「界面为空」)。
用法:
powershell -NoProfile -ExecutionPolicy Bypass -File test\read-skips-live.ps1
# 退出码 0 = 两阶段都通过;1 = 条件未满足;2 = 环境问题(窗口起不来)
实测记录(2026-09-10 夜):阶段 A
count=320;阶段 B 3/8 轮skipped=1(含原因),无异常轮。
2026-09-11 补完 B-1 的另一半:0 行必须永远有解释(空枚举)
问题(2026-09-11 上午实测,用自建重绘窗口压 read/state):UIA 在界面重绘/最小化瞬间
会返回空集合——FindAll(Descendants) 连续 4 次返回 0 个元素,既不抛异常、也没有元素被过滤。
旧实现于是回报「读到 0 个控件」,调用方只能读成「界面上没有控件」——这正是摩擦清单 #1
「read 偶发返回 0 行,把整晚判据都污染了」的真身之一:skipped 只覆盖异常路径,覆盖不到这种静默空。
现在:
read/state新增scanned=(本次枚举扫到多少个元素)与offscreen=(其中因不可见被过滤的数量)。 于是「0 行」永远解释得清:scanned=0= 这次没看到;scanned>0 且 offscreen=scanned= 元素在但都不可见;skipped>0= 有元素读不到状态。- 枚举为 0 时自动重试(最多 3 次、间隔 200ms):重绘通常一两帧就恢复,重试把假空消灭在源头;
带
match却 0 行的旧重试逻辑保留。 - 重试后仍是 0 元素 → driver 生成 warn:
⚠ 本次枚举返回 0 个元素(UIA 给了空集合,通常是界面正在重绘或窗口刚切换):这不等于「界面上没有控件」…。 - 现场验证脚本新增硬判据:任何一轮只要
count=0,必须满足「有 skipped / scanned=0 / offscreen>0」之一, 否则记为unexplainedRounds并判失败——静默 0 行不再可能蒙混过关。
2026-09-10 第四批修复:重启类调用不再挂死(B-2)
问题:昨夜整晚卡在「客户端重启调用」里 2.5 小时,只靠日志时间戳才发现。根因形态是: 常驻进程不会退出、也不会报错,只是把请求吞掉 —— 只看退出码的看门狗等于没有看门狗。
现在:
- 僵死看门狗(定位已澄清,见 2026-09-11 复核节):判据是「最近一次成功响应的时间」
(
warm.lastOkAt由每个成功响应刷新),不是退出码;阈值取max(DSH_UI_STALL_MS, 最老排队请求自己的超时)—— 它只可能比请求自己的超时更晚动手。 也就是说:卡住请求的主防线是「请求自身超时 + 只读重试」,看门狗是「Node 侧定时器丢失」时的兜底, 并对外提供诊断(stalls/lastStallReason/lateResponses)。之所以不保留「固定阈值抢杀」, 是因为复核指出那会误杀合法长动作(例如调用方给了 180s 超时的截图)。 - 失败重试(只读动作):
read/find/state/windows等只读动作超时后自动重试一次 (新进程 + ready 握手 + 重新解析窗口);只有重试这一腿受 20s 上限约束,整次调用最坏 仍是「首次超时(默认 90s)+ 握手(≤15s)+ 重试(≤20s)」。只读重放无副作用,重试是安全的。 - 副作用动作绝不重放:
click/setvalue/key/type/drag在看门狗重启或请求超时时, 一律返回{ok:false, unknown:true}并提示「先复核控件状态再决定」,绝不走回退路径重跑。 status超时 ≠ 未运行:状态查询超时改为返回{running:false, unknown:true, error:'status 超时…'},ui_status渲染成「客户端状态未知」——把「查询卡住」误报成「客户端没起来」会导致反复重启客户端(踩过)。- 启动/重启调用有硬上限:
ui_launch的整段调用受waitMs硬上限约束(最小 3s), 每次轮询都把剩余预算传给status(),任何一次卡住的状态查询都拖不垮整个调用; 返回里带polls/statusTimeouts心跳,便于区分「客户端真没起来」与「轮询自身被拖慢」。 - 诊断出口:
warmStatus()现在带lastOkAt/stalls/lastStallAt/lastStallReason。
环境变量:DSH_UI_STALL_MS(僵死判定阈值,默认 90000ms)。
单测:test/restart-watchdog.test.mjs(离线,假 serve 进程模拟「活着但不回请求」),
15 项断言覆盖「看门狗判僵死 + 只读重试 + 副作用不重放 + status 未知语义 + launch 硬上限」。
2026-09-11 跨模型独立复核修正(Codex + Claude 各出一份报告)
两份独立复核报告是本机工作文档,未随仓库分发,且本机副本现已不在盘上(去向未查清, 见 docs/prior-art.md)。 幸存下来、也是唯一算数的记录,是下面这份"已确认并已修"的清单 + 对应的测试断言:
truncated硬编码false(两方都判「对调用方撒谎」)→ 如实透传res.truncated, 并在截断时补returned=(实际返回行数)。补了零覆盖的单测。- 看门狗固定阈值会误杀合法长动作 → 阈值改为
max(DSH_UI_STALL_MS, 最老请求自身超时); 同时把 README/注释里「看门狗判僵死」的定位改准(主防线是请求超时 + 重试)。 launch的Math.max(3000, waitMs)覆盖调用方显式值 → 改为只挡非法值(≤0/NaN),下限 500ms; 显式waitMs=1000现在就是 1s(有单测)。- read 的
$cur.*属性访问未全包 try/catch(异常会整步失败而不是记 skipped)→ 逐元素函数体 全包 try/catch,异常计入skipped并带原因;与state路径口径一致。 - 空枚举修复在一次性回退路径(
DSH_UI_SERVE=0)缺失(Claude 的 N3)→ui-drive.ps1的 read 补齐「缓冲输出 + 空枚举重试 ≤3 次 + 回报SCANNED n」,driver 侧解析SCANNED→ 回退路径 同样会给出空枚举 warn(补了单测)。 - 两条路径对同一坏元素口径不一(Claude 的 N4)→
ControlType为 null 的瞬时元素在两条路径 都按「类型未知 → 白名单过滤」处理,不计 skipped。 - 重试的
killOnTimeout与首次不一致 → 重试保持与首次相同的killOnTimeout(避免把 「宁可放弃这一帧也不杀进程」偷偷变成「延迟若干秒后照杀」)。 - 注释把「重试腿 20s」写成「总时长 20s」(Claude 的 N2)→ 措辞改正,并把「整次调用最坏时长」 的构成写清。
复核中未被采纳的两条(附我的反证):
- Codex「
/T会连带杀 MSBuild/驱动」:Claude 给出反证并被我核实 ——killClientProcess在 MSBuildspawn之前await完成,且 MSBuild / UI 驱动 PowerShell 都不是客户端的子进程,/T够不到它们。红线命中点只有「按镜像名误杀同名实例」这一条(已修)。 - Claude 的 N1「
capture会走 warm 路径并被重试破坏killOnTimeout:false护栏」:路径不成立 ——capture在BATCH_ONLY_ACTIONS里(lib/driver.mjs),根本不走 warm 路径(实测seq不增长、 结果来自批量路径)。不过该条促成的第 7 项修正仍然保留(万一路由变化也不会踩)。
已知缺口(尚未修复,欢迎 PR)
- 证据目录无按会话聚合与上限,长跑会堆积大量时间戳目录。
read/state的skipped只在批量/常驻引擎路径有值;一次性回退路径解析老脚本时可能为null(未知)。