# N15 Selector Report Hardening Design 日期:2026-07-09 ## 1. 背景 N14 已在 `main` 上合并并推送,提供了离线 UIA selector 报告能力: - `internal/uiaselector/report.go`:报告模型、catalog report generator、JSON/Markdown renderer。 - `cmd/uia-selector-report/main.go`:离线 CLI,只接受 dump 文件路径和输出报告路径。 - `scripts/verify-n14-uia-selector-report.ps1`:运行测试、生成报告并做基础安全扫描。 N14 最终 review 留下两个非阻塞建议: 1. `StrictExitAmbiguous` 分支缺少显式单测。 2. verifier 的输出安全检查仍以字符串扫描为主,并且运行 CLI 前没有清理旧输出文件。 N15 的目标是把这些 review 建议收口为一个小型加固节点,不扩展产品能力,不接触 live UI,不新增 helper op,不新增 MCP tool,不扩展 live action surface。 ## 2. 目标 N15 只做报告安全与验证加固: 1. 增加 `StrictExitAmbiguous` 显式单测,锁定 `ambiguous_count > 0` 时严格模式返回 `3`。 2. 加固 `scripts/verify-n14-uia-selector-report.ps1`,避免旧报告掩盖 CLI 输出回归。 3. 增加输出文件检查:必须存在、必须是文件、大小必须大于 0。 4. 增加 JSON 字段 allowlist 检查,确认 `selectors[].matches[]` 只包含允许的结构字段。 5. 保持 N14 报告格式和 CLI 参数不变。 ## 3. 非目标 N15 不做以下事项: ```text 新增 selector 修改 selector catalog 修改报告 JSON/Markdown 格式 新增 CLI 参数 新增 helper op 新增 MCP tool 连接内网 iSphere live dump 当前窗口 使用 hwnd/process 参数 点击控件 输入文本 打开会话 搜索联系人 发送消息 发送文件 接收文件 读取聊天内容 导出完整 visible text ``` N15 也不清理本地分支。分支清理可以在 N15 合并后单独执行。 ## 4. 推荐方案 采用“小范围加固”方案,只修改两个文件: ```text internal/uiaselector/report_test.go scripts/verify-n14-uia-selector-report.ps1 ``` 不修改: ```text native/ISphereWinHelper/Program.cs internal/tools/winhelper.go cmd/isphere-mcp/main.go cmd/uia-selector-report/main.go internal/uiaselector/report.go ``` 理由:N14 的实现逻辑已经通过最终验证和 review。N15 只需要补测试和 verifier 防回归能力,不需要改变生产代码路径。 ## 5. Ambiguous strict-mode 测试设计 在 `internal/uiaselector/report_test.go` 中新增测试: ```text TestStrictModeClassifiesAmbiguousMatches ``` 测试构造一个最小 UIA tree: ```text root Window frmMain child Pane duplicate child Pane duplicate ``` 测试 catalog 只包含一个 read-only selector: ```text id: duplicate_panel allowed_use: read_only_locate required: automation_id: duplicate control_type: Pane framework_id: WinForm ``` 期望: ```text report.OK == true report.CatalogSize == 1 report.MatchedCount == 1 report.UnmatchedCount == 0 report.AmbiguousCount == 1 report.Selectors[0].MatchCount == 2 StrictExitCode(report) == StrictExitAmbiguous ``` 该测试只使用内存构造的 `Node`,不依赖 N12R fixture,也不涉及 visible text。 ## 6. Verifier 加固设计 ### 6.1 清理旧输出 在 `go run ./cmd/uia-selector-report` 之前删除旧报告: ```powershell Remove-Item -LiteralPath $jsonPath, $mdPath -Force -ErrorAction SilentlyContinue ``` 目的:如果未来 CLI 回归为“退出 0 但未写新文件”,旧报告不会让后续存在性检查误通过。 ### 6.2 文件存在和非空检查 替换当前 `Test-Path` 检查为更严格的文件检查: ```powershell $jsonItem = Get-Item -LiteralPath $jsonPath -ErrorAction Stop if (-not $jsonItem.PSIsContainer -eq $false) { ... } if ($jsonItem.Length -le 0) { ... } ``` 实际实现应避免 PowerShell 运算优先级歧义,推荐使用清晰表达: ```powershell if ($jsonItem.PSIsContainer) { throw "JSON report path is not a file: $jsonPath" } if ($jsonItem.Length -le 0) { throw "JSON report is empty: $jsonPath" } ``` Markdown 报告同样检查。 ### 6.3 JSON match 字段 allowlist 解析 JSON 后,对每个 `selectors[].matches[]` 对象检查字段集合,只允许: ```text path control_type automation_id class_name framework_id has_name name_length ``` 禁止出现: ```text name value text visible_text ``` 实现策略: 1. 遍历 `$report.selectors`。 2. 遍历 `$selector.matches`。 3. 对每个 match 的 `$match.PSObject.Properties.Name` 做集合比较。 4. 遇到不在 allowlist 中的字段立即失败。 5. 如果字段名是 `name` 或 `value`,错误信息明确指出这是 visible text 字段泄漏。 这比单纯扫描字符串更稳,因为它检查的是 JSON 对象结构。 ### 6.4 保留字符串扫描 保留 N14 已有字符串扫描,用于发现: ```text "name" "value" 国网甘肃省电力公司 send_after_approval send_file_after_approval receive_file_after_approval open_conversation InvokePattern ValuePattern.SetValue ``` 字段 allowlist 和字符串扫描同时存在: - allowlist 负责结构化字段泄漏。 - 字符串扫描负责已知敏感文本和动作词防线。 ## 7. 测试与验证策略 N15 implementation plan 必须采用 TDD。 最小验证命令: ```powershell go test ./internal/uiaselector -run "TestStrictModeClassifiesAmbiguousMatches" -count=1 powershell -NoProfile -ExecutionPolicy Bypass -File scripts\verify-n14-uia-selector-report.ps1 go test ./... ``` 最终验证命令: ```powershell go test ./internal/uiaselector ./cmd/uia-selector-report -count=1 go test ./... powershell -NoProfile -ExecutionPolicy Bypass -File scripts\verify-n14-uia-selector-report.ps1 git diff --name-only main..HEAD -- native/ISphereWinHelper/Program.cs internal/tools/winhelper.go cmd/isphere-mcp/main.go ``` 最后一条命令期望无输出。 ## 8. 风险与缓解 ### 风险 1:PowerShell 对单元素数组展开造成遍历差异 缓解:实现遍历时使用 `@($report.selectors)` 和 `@($selector.matches)`,确保单元素和多元素都按数组处理。 ### 风险 2:allowlist 检查误报 PowerShell 内置属性 缓解:只读取 `$match.PSObject.Properties.Name`,这返回 JSON 对象自身属性,不读取方法或扩展成员。 ### 风险 3:旧输出清理误删非目标文件 缓解:只删除两个明确计算出的文件路径: ```text runs/n14-selector-report/n12r-selector-report.json runs/n14-selector-report/n12r-selector-report.md ``` 不递归删除目录。 ## 9. 验收条件 N15 可接受条件: 1. `StrictExitAmbiguous` 有显式单测。 2. verifier 运行 CLI 前删除旧 JSON/Markdown 输出文件。 3. verifier 检查 JSON/Markdown 输出存在、是文件、非空。 4. verifier 对 JSON `matches[]` 执行字段 allowlist 检查。 5. verifier 继续扫描 `"name"`、`"value"`、已知 visible text 和动作词。 6. `go test ./...` 通过。 7. `scripts\verify-n14-uia-selector-report.ps1` 通过。 8. 不修改 helper/MCP/live action 文件。 ## 10. 推荐下一步 下一步写 N15 implementation plan,拆分为两个小任务: 1. TDD 增加 ambiguous strict-mode 单测。 2. 加固 verifier 的旧输出清理、文件检查和 JSON 字段 allowlist。