dsh-agent-team-ui-testing 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +170 -35
  3. package/annotation-capture.js +111 -0
  4. package/browser-safety.js +45 -1
  5. package/client.js +3709 -134
  6. package/context-menu.js +134 -0
  7. package/core/clock.js +88 -0
  8. package/core/config-store.js +566 -0
  9. package/core/config.js +892 -0
  10. package/core/contracts.js +240 -0
  11. package/core/errors.js +156 -0
  12. package/core/evidence.js +141 -0
  13. package/core/geometry.js +217 -0
  14. package/core/identity.js +180 -0
  15. package/core/index.js +24 -0
  16. package/core/p0.js +1101 -0
  17. package/core/readiness.js +175 -0
  18. package/core/registry.js +239 -0
  19. package/core/tab-access.js +1350 -0
  20. package/docs/adapters.md +160 -0
  21. package/docs/architecture.md +44 -9
  22. package/docs/capability-tiers.md +20 -4
  23. package/docs/capture-p0.md +39 -2
  24. package/docs/diagnose-reason-codes.md +157 -0
  25. package/docs/element-picker.md +24 -6
  26. package/docs/getting-started.md +47 -1
  27. package/docs/index.md +10 -7
  28. package/docs/multi-tab.md +55 -20
  29. package/docs/network-p0.md +2 -2
  30. package/docs/panel-context-menu.md +41 -0
  31. package/docs/t2-structured-gui.md +2 -2
  32. package/docs/t3-vision.md +6 -1
  33. package/docs/t4-dynamic-behavior.md +1 -0
  34. package/docs/tools.md +65 -7
  35. package/docs/verification.md +26 -2
  36. package/docs/viewport-control.md +21 -8
  37. package/index.js +3263 -251
  38. package/integration/cross-environment.js +1535 -0
  39. package/integration/diagnose.js +945 -0
  40. package/integration/tab-principal.js +493 -0
  41. package/locale/en.json +1 -1
  42. package/locale/zh.json +1 -1
  43. package/network-p0.js +24 -7
  44. package/package.json +4 -3
  45. package/select-control.js +298 -0
  46. package/skills/ui-testing-common/SKILL.md +54 -0
  47. package/skills/ui-testing-fake/SKILL.md +43 -0
  48. package/skills/{ui-testing → ui-testing-web}/SKILL.md +64 -24
  49. package/skills/{ui-testing → ui-testing-web}/manifests/declared-capabilities.json +9 -2
  50. package/skills/{ui-testing → ui-testing-web}/references/contracts/capability.md +3 -3
  51. package/skills/{ui-testing → ui-testing-web}/references/contracts/evidence.md +1 -1
  52. package/skills/{ui-testing → ui-testing-web}/references/contracts/operation.md +1 -1
  53. package/skills/{ui-testing → ui-testing-web}/references/core/fallback.md +1 -1
  54. package/skills/{ui-testing → ui-testing-web}/references/core/page-identity.md +3 -2
  55. package/skills/{ui-testing → ui-testing-web}/references/core/selection.md +1 -1
  56. package/skills/{ui-testing → ui-testing-web}/references/core/tool-routing-and-limits.md +6 -3
  57. package/skills/{ui-testing → ui-testing-web}/references/platforms/index.md +1 -1
  58. package/skills/{ui-testing → ui-testing-web}/references/platforms/web.md +2 -2
  59. package/skills/ui-testing-web/references/scenarios/annotations.md +104 -0
  60. package/skills/{ui-testing → ui-testing-web}/references/scenarios/functional.md +1 -1
  61. package/skills/{ui-testing → ui-testing-web}/references/scenarios/visual-motion.md +1 -1
  62. package/skills/{ui-testing → ui-testing-web}/references/tiers/t1-programmatic.md +2 -2
  63. package/skills/{ui-testing → ui-testing-web}/references/tiers/t3-static-visual.md +2 -2
  64. package/skills/{ui-testing → ui-testing-web}/references/tiers/t4-dynamic-visual.md +2 -1
  65. package/targets/fake/adapter.js +1163 -0
  66. package/targets/fake/fixtures.js +177 -0
  67. package/targets/web/adapter.js +1231 -0
  68. package/targets/web/provider.js +415 -0
  69. package/skills/ui-testing/references/scenarios/annotations.md +0 -87
  70. /package/skills/{ui-testing → ui-testing-web}/references/core/dynamic-visual.md +0 -0
  71. /package/skills/{ui-testing → ui-testing-web}/references/core/network-and-state.md +0 -0
  72. /package/skills/{ui-testing → ui-testing-web}/references/core/report-template.md +0 -0
  73. /package/skills/{ui-testing → ui-testing-web}/references/core/reporting.md +0 -0
  74. /package/skills/{ui-testing → ui-testing-web}/references/core/safety.md +0 -0
  75. /package/skills/{ui-testing → ui-testing-web}/references/core/verdict-examples.md +0 -0
  76. /package/skills/{ui-testing → ui-testing-web}/references/core/verdict-gate.md +0 -0
  77. /package/skills/{ui-testing → ui-testing-web}/references/core/verification.md +0 -0
  78. /package/skills/{ui-testing → ui-testing-web}/references/core/workflow.md +0 -0
  79. /package/skills/{ui-testing → ui-testing-web}/references/tiers/t2-structured.md +0 -0
  80. /package/skills/{ui-testing → ui-testing-web}/templates/capability-note.md +0 -0
  81. /package/skills/{ui-testing → ui-testing-web}/templates/platform.md +0 -0
  82. /package/skills/{ui-testing → ui-testing-web}/templates/regression-case.md +0 -0
  83. /package/skills/{ui-testing → ui-testing-web}/templates/scenario.md +0 -0
@@ -0,0 +1,160 @@
1
+ # 三类 adapter:契约、接入与坑点
2
+
3
+ 面向两类读者:给本插件接一个新调试环境的人(Android、Linux 桌面、小程序……),以及维护 `core/` / `integration/` 的人。这里只写当前实现与当前边界;不写过程记录。
4
+
5
+ 一句话分层:**TargetAdapter 管"目标在哪、还能不能用",CapabilityAdapter 管"这个操作能不能做、做了什么、证据是什么",P0Provider 管"把平台数据翻译成公共数据模型"**。三者消费同一份不可变配置快照,串在一条调用链上。
6
+
7
+ ## 1. 一个工具调用经过什么
8
+
9
+ ```text
10
+ Agent 工具调用
11
+ │
12
+ ├─ 组合根:解析 ConfigSnapshot(按会话/工作区/全局)→ 未启用或能力缺失 → ADAPTER_DISABLED
13
+ ├─ TargetAdapter.createSession() → TargetSession → listTargets()/bind(targetId) → 得到目标与 generation
14
+ ├─ CapabilityAdapter.invoke({ operation, args, session, snapshot, executionClass, context })
15
+ │ └─ P0 家族操作 → P0Provider.<family>.<method>() → 数据 + 证据 envelope
16
+ ├─ 组合根:executionClass 决定是否进串行队列 / ensure / 租约
17
+ └─ TargetSession.dispose()(每次调用即建即销的簿记对象)
18
+ ```
19
+
20
+ `executionClass`(`page | readOnly | hostOnly | networkRead | diagnostic | independentHttp | tabs`)由**组合根**决定,adapter 只能声明与校验,不能改变分支——这是"只读诊断不进队列、不取租约"这类行为的唯一来源。
21
+
22
+ ## 2. 三类 adapter 的职责边界
23
+
24
+ | 类型 | 必须实现 | 只属于它 | 不得做 |
25
+ | --- | --- | --- | --- |
26
+ | **TargetAdapter** | `manifest`、`probe`、`createSession`、`dispose` | 目标发现/绑定/关闭、目标 `generation`、连接与后台任务归属 | 定义工具语义、做业务操作、启动用户没启用的目标 |
27
+ | **CapabilityAdapter** | `manifest`、`operationStatus`、`invoke`、`preflight`、`dispose` | 操作清单与现场可用性、参数前置校验、证据 envelope | `bind`/`close` 目标、改 `executionClass`、自己起浏览器 |
28
+ | **P0Provider** | `manifest`、`familyStatus`、五个家族 surface、`dispose` | A–E 家族的数据模型转换与平台引用(`transportRef`) | `bind`/`close`(P0-E 只给 `list` 与租约)、伪造缺失能力 |
29
+
30
+ 三者的 `manifest` 都以 `contractVersion`(当前 `1`)开头;版本不一致的环境在就绪报告里是 `support=unsupported`,不会被当成可用。
31
+
32
+ ### 2.1 TargetAdapter
33
+
34
+ ```js
35
+ {
36
+ manifest: { contractVersion, id, class: 'target', environment, displayName, capabilities, dependencies, visibility },
37
+ async probe({ scope, snapshot, signal }) -> ReadinessReport, // 六轴,见 §4
38
+ async createSession({ scope, snapshot, signal }) -> TargetSession,
39
+ async dispose() -> void,
40
+ }
41
+ ```
42
+
43
+ `TargetSession` 对组合根暴露:
44
+
45
+ | 成员 | 语义 |
46
+ | --- | --- |
47
+ | `sessionId` / `environment` / `scopeId` | 调用身份;`sessionId` 用于租约归属与网络归属 |
48
+ | `generation` | 目标代次。**`bind()` 成功后递增**;旧代次的引用(帧、标注、租约)必须一律失效 |
49
+ | `configRevision` | 创建时消费的配置 revision |
50
+ | `target` | 当前绑定目标 `{ targetId, generation, ... }` 或 `null` |
51
+ | `listTargets()` / `bind(targetId)` | 发现与绑定;目标不存在 → `TARGET_NOT_BOUND`,已关闭 → `TARGET_GONE` |
52
+ | `close()` / `dispose()` | 关闭/释放。两者都要幂等 |
53
+
54
+ 要点:**TargetSession 是"每次工具调用一份"的簿记对象**,不是页面本身的生命周期。页面/连接/帧源/网络采集器属于平台实现(Web 里是 `Runtime`),由组合根与 `Runtime.close()` 负责。把两者的生命周期混在一起,会出现"一次调用结束后把用户的页面关掉"或"环境切换时页面没被释放"两种对称的 bug。
55
+
56
+ ### 2.2 CapabilityAdapter
57
+
58
+ ```js
59
+ {
60
+ manifest: { contractVersion, id, class: 'capability', environment, capabilities, operations },
61
+ operationStatus({ session, snapshot }) -> { [operation]: { status, reason, constraints } },
62
+ preflight(args, operation) -> void, // 纯校验,零副作用
63
+ async invoke({ operation, args, session, snapshot, signal, executionClass, context }) -> { value, evidence },
64
+ async dispose() -> void,
65
+ }
66
+ ```
67
+
68
+ - `operation` 是**环境无关的操作 id**(Web 是 `browser_*` 名字,映射到 `web.t*` 能力);`context` 携带本次调用的平台句柄(页面 runtime、调用方 `exec`、`actionId` 等),**必须原样往下传**(见 §5 坑 1)。
69
+ - 工具返回给用户的结果形状由**工具契约**决定,不由 adapter 决定;P0 家族的公共数据模型(帧、网络事件、显示空间、标注文档、目标身份/租约)才走中立工厂。
70
+ - `preflight` 必须在任何 I/O 之前跑完:非法参数要做到"零 CDP、零文件、零派发"。
71
+
72
+ ### 2.3 P0Provider(A–E)
73
+
74
+ | 家族 | 内容 | 方法 |
75
+ | --- | --- | --- |
76
+ | A | 采集(截图/帧/图像校验) | `screenshot`、`capture`、`product`、`readFrame`、`validateImage` |
77
+ | B | 网络(会话/事件/详情/拦截控制/链路/证据/订阅) | `start`、`stop`、`status`、`reset`、`events`、`detail`、`control`、`trace`、`evidence`、`subscribe` |
78
+ | C | 显示空间(视图投影与几何) | `read`、`set`、`transform` |
79
+ | D | 画布/标注 | `annotationRead`、`annotationWrite` |
80
+ | E | 目标模型与租约 | `list`、`leaseAcquire`、`leaseRelease`、`leaseStatus` |
81
+
82
+ - **家族可以缺省**。没有网络能力的平台把 B 报成 `support=unsupported`,A/C/D/E 照常工作;"可选"是契约的一部分,不是降级。
83
+ - 两组词表不能混用:`support` 回答"工程支持吗" = `supported | unsupported | planned`;`availability` 回答"现在能用吗" = `available | unavailable | not-probed | busy`。就绪报告里的 `capability` 轴是二者的并集词表(多出 `partial` / `unsupported`)。未探测就是 `not-probed`,不得写成 `available`;"支持但此刻不可用"要分别报。
84
+ - 每次成功调用都要带证据 envelope,字段固定:`version / scope / session / target / generation / configRevision / operation / source / capturedAt`;缺一个就是 `EVIDENCE_INCOMPLETE`。
85
+ - 租约必须同时带 `targetId` 与整数 `generation`;`acquire/release/status` 是三个不同操作,不能接成同一个只读投影。
86
+
87
+ ## 3. 配置如何约束三类 adapter
88
+
89
+ - 组合根解析出**不可变** `ConfigSnapshot`:`{ revision, environment, adapterId, provider, executionAdapterId, enabledProviders, providerConfig, enabledEnvironments, capabilities, environmentConfig, targetId, source }`。深度冻结 + 按 revision 记忆化,同一 revision 内同一 Agent 拿到的是同一个对象。
90
+ - **`environment` 与 `adapterId` 仍是目标环境身份(Web 恒为 `"web"`)**;浏览器身份放在 `provider` / `executionAdapterId`(`web-<provider>`)里。一个字段不兼任两件事。
91
+ - `invoke` 收到 `snapshot.revision` 与 `session.configRevision` 不一致时抛 `CONFIG_STALE`,**不静默用旧配置执行**。
92
+ - `enabledEnvironments` 决定"允许启用谁";`environmentConfig` 是该环境的连接参数。设置页只提交意图,Host 校验 + CAS 写入 + 返回权威 `revision`。
93
+ - 工具声明面 = **已启用环境的并集 + 两个环境无关诊断**(`browser_state`、`browser_capabilities`)。被禁用的环境其工具根本不注册,因此不会出现在模型请求里。
94
+
95
+ ## 3.1 provider 维度:facade → driver
96
+
97
+ 同一个目标环境可以有多个**运行环境**(provider = 实际执行端,如 `chrome` / `edge`)。注册面不变:每环境仍是一个 TargetAdapter / CapabilityAdapter / P0Provider 组成的 facade,注册表不需要复合键。
98
+
99
+ ```text
100
+ registry.resolve(snapshot) → web facade(environment/adapterId = "web")
101
+ └─ resolveWebDriver(snapshot.provider) → chrome driver | edge driver
102
+ └─ 未实现 / 未知 provider → ADAPTER_UNSUPPORTED
103
+ ```
104
+
105
+ - **派发靠快照,不靠全局开关。** facade 在 `invoke`/`createSession` 前解析 driver,并把 `provider` / `executionAdapterId` 一并交给平台 dispatch;37 个 `browser_*` 名字只注册一次,切换 provider 不改变工具集合。
106
+ - **禁止静默换浏览器。** driver 解析结果只有两种:可执行,或带 `reason` 的阻断。被禁用是 `ADAPTER_DISABLED`(`environment-disabled` / `provider-disabled`),未实现/未知是 `ADAPTER_UNSUPPORTED`(`driver-pending` / `provider-unsupported` / `unknown-provider`),依赖问题属于 `DEPENDENCY_MISSING`(`executable-missing` / `override-invalid` / `identity-unverified` / `identity-mismatch`)。这些 `reason` 都在 `details` 里,不属于对外错误码。
107
+ - **厂商参数化。** 可执行文件候选、覆盖变量(Chrome `DSH_UI_TESTING_CHROME`、Edge `DSH_UI_TESTING_EDGE`)、profile 目录、启动参数都按 provider 注入;Chrome 的覆盖不会被 Edge 继承,反之亦然。一个提供者的覆盖无效时直接报错,不尝试另一个厂商。
108
+ - **产品身份校验。** 传输建立后用 `Browser.getVersion` 的产品串核对厂商(规则表:Chrome 必须含 `Chrome/` 且不含 `Edg/`;Edge 必须含 `Edg/`)。Edge 拿不到产品串按 `identity-unverified` 拒绝;Chrome 保持机会式(拿到就核对,拿不到不阻断)。
109
+ - **运行时归属按 provider。** 页面容器是 `hosts[provider]`,entry 键为 `(provider, workspace)`(`<provider>\u0000<baseKey>`)。切换 provider 后旧 `targetId`、旧 ref、旧租约一律失效;关闭一个 provider 不影响另一个;页面总预算按 provider 求和后仍与同一个 `maxPages` 比较,拆 host 不放宽上限。
110
+ - **GUI/预览读侧。** 明确绑定(binding)时用 provider 前缀键精确命中;其余读路径回落到"该工作区最近使用的 provider",响应里带 `provider` 与 `providerResolved: "binding" | "recent"`。**预览面板按 provider 分栏尚未实现**。
111
+
112
+ ## 4. 就绪(六轴)怎么写才算真实
113
+
114
+ `probe()` 返回 `support / enabled / dependency / connection / target / capability` 六个独立轴,每轴带 `status`、`reason`、`checkedAt`(`expiresAt` 可选),能测到的还要带 `target` / `evidence`:
115
+
116
+ - `probe()` **不得启动目标**,必须有超时;启动/连接属于显式操作。
117
+ - `dependency`:可执行文件/服务是否存在;版本没读到就 `version: null`,不编造。Web 的 evidence 还带 `provider` 与解析出的 `path`,并**用该 provider 自己的候选清单**探测(不借用另一个厂商的路径);未实现的 provider 不探测,直接报 `not-probed` + 专属 reason。
118
+ - Web 的每张 provider 卡片读自己的结果:`readiness.environments.web.providers.<provider>.axes`;被禁用的 provider 不探测、不启动。
119
+ - `connection`:只在确有 transport 时 `ok`;有宿主进程但没有页面会话是 `not-probed / host-idle-no-transport`。
120
+ - `target`:配置目标与实际绑定目标不一致时报 `stale / identity-mismatch`(带 expected/actual),不要报 `bound`。
121
+ - `capability`:**逐项探测的并集**(Web 是结构树 / 输入 / 网络采集 / 帧源),全过 `available`、部分 `partial`、全不过 `unavailable`,明细放 `evidence.checks`。
122
+ - 被禁用或不允许选择的环境**不探测**,对应轴报 `not-probed / environment-disabled`——"工程支持"与"用户启用"是两件事,前者仍应是 `supported`。
123
+
124
+ ## 5. 坑点(都是这轮实测踩到的)
125
+
126
+ 1. **Provider 派发必须透传 `context`。** 只传 `{ args, session, snapshot, signal }` 时,平台 dispatch 拿不到页面 runtime,只能回 `PROVIDER_UNAVAILABLE`;表面"接好了",实际一条数据都取不到。
127
+ 2. **`when` 不等于声明隔离。** 本机组合按"已注册"构建模型请求(实测 `request/header`:`when=false` 的工具仍在声明里,调用也仍能进到插件)。隔离靠**注册范围**:只注册已启用环境的工具。`when` 是第二道闸门,不能当唯一防线。
128
+ 3. **`TargetSession` 不是页面生命周期。** 每次调用即建即销;页面/连接/帧源/采集器归平台实现,由 `close()` 释放。
129
+ 4. **租约要 `targetId + generation`。** 少了 generation 就无法判断"这条租约属于哪一代目标",重绑后旧租约会被当成有效。
130
+ 5. **目标重建必须让旧引用失效。** 帧、标注、租约、文本 ref 都要按 `targetId + generation` 校验,否则出现"数据属于 t1、证据写 t2"。
131
+ 6. **平台错误码不要在边界上坍缩。** 适配器只认得中立码,`NET_*` / `VISION_*` / `OUTPUT_*` 这类平台码必须原样透出(保留 cause 或平台码),否则调用方拿到的是 `INTERNAL`,无法分类。
132
+ 7. **缺能力要报 `unsupported`,不能假装。** 没有网络家族就报 `unsupported`,不要返回空数组冒充"没有事件";没有服务 worker 采集就写明 `not-collected`。
133
+ 8. **取消与关闭走同一条清理链。** 取消后必须释放连接/帧源/网络/租约(Web 里是 `ABORTED → close()`);只把状态位改掉会留后台任务。
134
+ 9. **按需加载。** Web 的帧源/采集/视觉/网络模块与三面 adapter 在首次真正需要时才创建;禁用环境时既不加载也不启动 Chromium。
135
+ 10. **假环境只是契约夹具。** `targets/fake/` 的标注存储是单槽、目标是内存对象;用它证明的是"契约形状与准入/证据/清理",**不能**据此声称任何真实平台可用,也不能把它当作会话级数据隔离的证据。
136
+ 11. **证据字段缺一个就失败。** `P0_EVIDENCE_FIELDS` 是硬约束;为了"顺手"塞进工具结果里的额外字段不要污染中立 envelope。
137
+ 12. **`executionClass` 不许 adapter 改。** 只读诊断若被当成页面操作,会多出 ensure/队列/租约,直接改变用户可见行为。
138
+
139
+ ## 6. 接入一个新平台的步骤
140
+
141
+ 1. 在 `targets/<platform>/` 实现 TargetAdapter、CapabilityAdapter、P0Provider(P0 家族可只实现有能力的部分)。
142
+ 2. 在平台 manifest 里声明能力 id,并把 `operation` 映射到这些 id;不要复用 Web 的工具名。
143
+ 3. 在 `integration/cross-environment.js` 注册三类 adapter,并让配置启用该环境(`enabledEnvironments` + `environments.<id>.enabled`)。
144
+ 4. `probe()` 按 §4 返回六轴;先做到"冷态全部 `not-probed`、不启动目标"。
145
+ 5. 用假环境做对照跑一遍契约(配置 revision 一致、generation、证据、可选能力、取消/清理),再接真实平台。
146
+ 6. 跑文档里列出的验证命令,并把"假适配器契约通过"与"真实平台验证"分开记录。
147
+
148
+ ## 7. 验证入口
149
+
150
+ ```bash
151
+ node --test tests/contracts/*.test.mjs # 契约层:adapter/fake/P0/配置/就绪/provider 派发
152
+ node --test tests/contracts/web-provider-config.test.mjs # provider 配置与选择链(含旧配置规范化)
153
+ node --test tests/contracts/web-provider-runtime.test.mjs # driver 门禁、host 隔离、页面预算、providerResolved
154
+ node --test tests/contracts/web-provider-edge.test.mjs # Edge 厂商参数化、覆盖变量、产品身份判定
155
+ node tests/acceptance/cross-environment.mjs # 配置 CAS、六轴、GUI 旁路、不启动浏览器
156
+ node tests/acceptance/foundation-isolation.mjs # 逐项阻断复核(含 A08 面板为权威、A09 Skill 隔离)
157
+ node tests/acceptance/run.mjs # 真实无头 Chromium:37 工具面与 T1~T4 回归
158
+ ```
159
+
160
+ 新增或修改 adapter 后,至少要让契约套件与 `foundation-isolation` 通过;运行方式与套件清单见 [verification.md](verification.md)。
@@ -8,22 +8,56 @@
8
8
  package.json DSH bundle 清单(dsh.bundle.patch + dsh.client,platform: web)
9
9
  cordis.patch.yml 注册插件:id = ui-testing-browser
10
10
  index.js Host:BrowserHost、RuntimeRegistry、租约与串行队列、scoped HTTP 路由、
11
- 36 个工具注册、共享 view、标注存储、网络采集、T4 会话、
12
- 固定 loopback 调试端口与 scoped `ui.devtools` 投影
11
+ 37 个工具注册、共享 view、标注存储、网络采集、T4 会话、
12
+ 固定 loopback 调试端口与 scoped `ui.devtools` 投影;Web 实现与组合根
13
+ integration/ 跨环境组合根:权威配置、adapter registry、工具准入、逐次 adapter 调度、
14
+ `/config` `/config/select` `/readiness` 路由与 Skill provider
15
+ core/ 环境中立契约:身份/几何/时钟/证据/错误、配置解析与持久化、registry、
16
+ 六轴就绪、P0-A~E 中立模型(`core/p0.js`)
17
+ targets/web/ Web TargetAdapter / CapabilityAdapter / P0Provider(注入 Runtime,不直接发 CDP)
18
+ targets/fake/ 仅测试的确定性假环境(test-only,不是真实平台实现)
13
19
  browser-safety.js 错误类型与分类、CDP 封装、取消与定时器清理、URL 脱敏、目标/DNS 校验
14
20
  capture-p0.js PNG 结构/CRC/解码、像素统计、尺寸校验、映射投影、原子安全写入
15
21
  network-p0.js 网络事件与会话、配额、分析、链路、规则、JSONL/HAR、订阅(零依赖)
16
22
  vision-t3.js PNG 编解码、裁剪/缩放/预处理/标注、YIQ 差异、识别、映射、产物存储
17
23
  vision-baseline.js 受管基线:scope 根、不可变 revision、HEAD CAS、墓碑
18
24
  dynamic-t4.js T4 纯核心:帧存储、事件日志与时钟锚点、七类分析、六向关联、JPEG 解码
25
+ context-menu.js 页面右键事务、最终取消状态观测、默认菜单投影与短期动作令牌
19
26
  frame-source.js 最新帧缓存与统计(与传输、UI 解耦)
20
27
  frame-push.js 同源 WebSocket 推帧:握手、合并推送、背压丢帧、鉴权
21
28
  client.js 全部面板 UI(React,无 JSX)与传输层;DevTools 独立窗口
22
- skills/ui-testing/ 内置 Agent 操作手册
29
+ skills/ui-testing-web/ Web 环境 Agent 操作手册
30
+ skills/ui-testing-common/ 跨环境公共规则(证据、判定、报告、安全)
31
+ skills/ui-testing-fake/ test-only 假环境说明(生产默认不可发现)
23
32
  tests/ 验收套件与确定性靶机
24
33
  ```
25
34
 
26
- `index.js` 只负责编排与 CDP;纯算法与状态机都在独立模块里,可脱离浏览器单测。
35
+ `index.js` 只负责 Web 实现、编排与 CDP;纯算法与状态机都在独立模块里,可脱离浏览器单测。`core/` 不 import `index.js`、`targets/` 或任何平台实现;`targets/web/` 只 import `core/` 与 Node 内置模块,平台能力通过注入的 dispatch 使用。
36
+
37
+ ## 跨环境契约
38
+
39
+ 三类 adapter 是调用方向不同的契约,源码目录 `core / targets / engines` 只是责任边界,不等同于三类 adapter。接口形状、接入步骤与完整坑点见 [三类 adapter:契约、接入与坑点](adapters.md);下面只是摘要:
40
+
41
+ | 类型 | 职责 | 所有权 |
42
+ | --- | --- | --- |
43
+ | **TargetAdapter** | 环境入口:声明工程支持、发现/绑定/关闭目标、生命周期、生成 TargetSession、汇总就绪 | 连接、后台任务、目标 generation |
44
+ | **CapabilityAdapter** | 按能力层声明操作清单与现场可用性;执行操作并返回标准化观察+证据 | 操作准入、证据 envelope;不拥有目标 |
45
+ | **P0Provider** | 跨能力提供 P0-A~E:帧/图像、网络、显示空间、画布/标注、目标模型与租约原语 | 数据模型与平台转换;P0-E 只提供只读投影与租约,不 bind/close |
46
+
47
+ - 三类 adapter 消费**同一份不可变 ConfigSnapshot**;`invoke` 时 revision 不一致抛 `CONFIG_STALE`,不静默用旧配置执行。
48
+ - `executionClass`(`page|readOnly|hostOnly|networkRead|diagnostic|independentHttp|tabs`)由组合根决定是否进串行队列/ensure;adapter 不得改变该分支。
49
+ - 目标发现/绑定/关闭只归 TargetAdapter;P0-E provider 的 `targets` 仅 `list/leaseAcquire/leaseRelease/leaseStatus`。
50
+ - 平台协议(CDP、DOM、WAAPI 等)只出现在 `targets/web/` 或对应平台目录,不泄漏到 `core/`。
51
+
52
+ ## 配置驱动执行与上下文
53
+
54
+ - **配置真值在 Host**:设置页只提交意图,Host 校验、CAS 写入并返回权威 `revision`。默认文件为 `${DSH_HOME}/ui-testing/config.json`,可用 `DSH_UI_TESTING_CONFIG_DIR` 覆盖;原子写、0600、跨进程锁与损坏回退。
55
+ - **作用域全序**:session 自身 > 继承父的有效快照(`parentSession`,≤8)> workspace > global;只暴露当前环境的专用 schema/Skill,`enabledEnvironments` 只决定“允许启用”。
56
+ - **工具准入**:`ctx.tools.register(definition, { when })` 在 step 组装时排除非当前环境/未启用能力的工具;执行入口与 GUI 页面写路由再次校验,旧调用与面板旁路不能绕过。
57
+ - **Skill 隔离**:`ctx.skills.registerProvider` 的 `list()` 与 `get()` 都按环境过滤;公共规则在 `ui-testing-common`,Web 扩展在 `ui-testing-web`,test-only 假环境在 `ui-testing-fake`。已注入历史的正文不会被开关自动移除,切换只对后续轮次生效。
58
+ - **六轴就绪**:`support / enabled / dependency / connection / target / capability` 独立返回,带 `checkedAt`、`configRevision`、`reason`、`target`、`evidence`;冷态目标为 `unbound`、能力为 `not-probed`,不伪造版本或连接。每个轴都带实测证据:`connection` 只在确有 transport 时为 `ok`(有宿主进程但无页面会话是 `not-probed/host-idle-no-transport`);`target` 在配置目标与实际绑定目标不一致时报 `stale/identity-mismatch`;`capability` 是逐项探测的并集(结构树 / 输入 / 网络采集 / 帧源),全部通过为 `available`、部分为 `partial`、全不通过为 `unavailable`,`evidence.checks` 记录每一项状态。被禁用或未选择的环境不做探测(依赖/连接/能力为 `not-probed/environment-disabled`),因此打开设置页不会扫描未启用的环境。
59
+ - **按需加载**:Web 专用模块(帧源、采集、T3 视觉、基线、P0 网络)与 Web 三面 adapter 在首次真正需要时才加载/创建;Web 被禁用时既不加载这些模块,也不创建 adapter、不启动 Chromium。
60
+ - **声明面按注册范围隔离**:插件的工具声明由注册面决定(已启用环境的并集 + 中立诊断),逐 Agent 的 `when` 是第二道闸门。实测按 step 准入不改变当前组合的模型请求声明,因此“某环境被禁用后其 schema 不再出现在请求里”由注册范围保证,而不是靠 `when` 隐藏。
27
61
 
28
62
  ## 运行时模型
29
63
 
@@ -31,7 +65,7 @@ tests/ 验收套件与确定性靶机
31
65
  DSH Agent / Agent Team
32
66
  │ 工具调用
33
67
  ▼
34
- BrowserHost ── 一个 Chromium(默认无头;Web 视图可切有头)/ 一个 profile
68
+ BrowserHosts[provider] ── 每个运行环境一个浏览器(默认无头;Web 视图可切有头)/ 每个 provider 一份 profile
35
69
  │
36
70
  ├─ Runtime(每个 DSH 工作区一个 primary)
37
71
  │ ├─ 独立 page:URL、视口、历史、网络缓冲
@@ -46,18 +80,19 @@ DSH Agent / Agent Team
46
80
  ```
47
81
 
48
82
  - **隔离粒度**:页面、URL、视口、历史、网络缓冲、租约按 Runtime 隔离;同一 DSH 工作区的 primary 与 popup child 共享 profile,但各有独立页面状态。**Cookie 与存储由 profile 共享**,不是登录态隔离。
49
- - **内部标签页**:真人触发的 `target=_blank` / `window.open` 会把原生 popup target 接管为同工作区的 child Runtime(不重新发请求),面板在地址栏下方增加标签;child 由 binding 路由,primary 保留原页。自动弹窗、无真人输入、非法目标与超容量仍阻断。每 Agent 会话独立 selectedTab:现有 `browser_*` 工具默认操作该会话选中的标签(无选择时 primary),`browser_tabs` 负责 list/current/select/open/close;GUI active 独立,GUI 切标签不会改变 Agent selection。
83
+ - **内部标签页**:真人触发的 `target=_blank` / `window.open` 会把原生 popup target 接管为同工作区的 child Runtime(不重新发请求),面板在地址栏下方增加标签;child 由 binding 路由,primary 保留原页。自动弹窗、无真人输入、非法目标与超容量仍阻断。每 Agent 会话独立保存两个选择:写工具用 workSelection(无选择时 primary),读工具优先用 observationSelection(未设置时回退 workSelection),`browser_tabs` 负责 list/current/select/open/close/wait/acquire/release/transfer。页面组按 (workspace, provider, 团队根) 划分:同 root 共享一组页,跨 root 的目标一律 not-found 语义;谁能写由 root → 角色 → 控制权/版本决定,15 秒租约只表示"短暂忙"。GUI active 独立,GUI 切标签不会改变 Agent 的选择。
50
84
  - **串行化**:所有会改动页面的操作走该 Runtime 的串行队列;只读诊断(`browser_state`、`browser_capabilities`、`browser_viewport get`、离线基线读取)不进队列、不占租约。
51
85
  - **租约**:同一标签同一时刻只有一个 owner;不同标签可由不同 Agent 并行,同标签冲突返回 `BUSY`。
52
86
  - **懒启动**:只有真正需要页面的动作才启动 Chromium;诊断类工具不启动。
87
+ - **运行环境(provider)归属**:页面容器按 provider 分开(`hosts[provider]`),entry 键是 `(provider, workspace)`,所以同一工作区在不同 provider 下是两套互不影响的页面/租约/ref;页面总预算按 provider 求和后仍受同一个 `maxPages` 约束。目标环境身份(`environment` / `adapterId`)不因此改变,仍是 `"web"`。跨 provider 的预览分栏尚未实现。
53
88
 
54
89
  ### 面板 HTTP 路由
55
90
 
56
91
  认证 scoped GUI 使用同一前缀 `/plugins/ui-testing` 的 HTTP 路由(legacy 客户端按既有闸处理):
57
92
 
58
93
  - 读:`GET /state`、`GET /frame`、`GET /metrics`;
59
- - 页面操作(串行队列 + single-owner lease):`POST /ensure`、`/navigate`、`/reload`、`/history`、`/viewport`、`/input`、`/eval`、`/screenshot`、`/tabs`、`/close`;
60
- - 标注唯一写面:`POST /annotations`;
94
+ - 页面操作(串行队列 + single-owner lease):`POST /ensure`、`/navigate`、`/reload`、`/history`、`/viewport`、`/input`、`/context-menu`、`/context-action`、`/eval`、`/screenshot`、`/tabs`、`/close`;其中 `/screenshot` 的 `{includeAnnotations:true}` 复用 Agent 截图的离屏合成器(`capture.annotations` 随响应返回),供标注栏「截图」按钮把页面+标注交给聊天框附件;空 body 与 `{canvas:true}` 的旧回复形状不变;
95
+ - 标注写面:`POST /annotations` 是面板写面(Agent 侧写同一份文档走 `browser_annotation` 工具,不经 HTTP 路由);
61
96
  - 只读元素选取:`POST /element`(文档 CSS px → 命中元素;不 ensure、不进串行队列、不占租约、不写 `lastError`、不记录客户端指标;`GET /element` 固定 405)。语义与限制见 [选元素加入聊天框](element-picker.md)。
62
97
 
63
98
  只读诊断(`/state`、`/metrics`、`/element` 与离线基线读取)不进页面队列、不占租约。
@@ -82,7 +117,7 @@ DSH Agent / Agent Team
82
117
  ## 证据与状态
83
118
 
84
119
  - **共享 view**:面板与 Agent 读写同一对象,用 `expectedRevision` 做 CAS。
85
- - **标注**:独立文档,按工作区存放,唯一写面是认证的 `POST /annotations`。
120
+ - **标注**:独立文档,按工作区存放;人类在面板经认证的 `POST /annotations` 写,Agent 经 `browser_annotation` 写**同一份**文档(前台标签心跳门禁 + `expectRevision` CAS)。
86
121
  - **视觉产物**:按 scope 隔离、有配额、重启失效。
87
122
  - **网络证据**:JSONL 保真、HAR 为 1.2 子集;只写持久根。
88
123
  - **帧**:有界存储,带淘汰与空档计数。
@@ -32,26 +32,42 @@ P0 是四项互相独立的能力,不能相互顶替:
32
32
  | P0-A | 图片输入流与截图采集 | [capture-p0.md](capture-p0.md) |
33
33
  | P0-B | 网络抓包与业务链路 | [network-p0.md](network-p0.md) |
34
34
  | P0-C | 共享视口控制 | [viewport-control.md](viewport-control.md) |
35
- | P0-D | 混合画布与客户端标注 | [viewport-control.md](viewport-control.md) |
35
+ | P0-D | 混合画布与客户端标注(人画、Agent 经 `browser_annotation` 读写同一份文档) | [viewport-control.md](viewport-control.md) |
36
36
  | P0-E | 多标签页/每会话选页 | [multi-tab.md](multi-tab.md) |
37
37
 
38
- 工具面共 36 个:10 T1 + 1 P0-E(`browser_tabs`)+ 7 T2 + 6 T3 + 7 network + 5 T4。
38
+ 工具面共 37 个:10 T1 + 1 P0-E(`browser_tabs`)+ 1 P0-D(`browser_annotation`)+ 7 T2 + 6 T3 + 7 network + 5 T4。
39
39
 
40
40
  ## 各层已实现范围
41
41
 
42
- - **T1**:导航、JS 执行、截图、原始输入、轻量网络列表、计算样式、独立 HTTP 请求、共享视口、状态与能力发现。轻量 `browser_network` 只提供元数据环形缓冲;深度抓包、正文与控制属于 P0-B 的七个工具,两者并存、语义不互相顶替。
42
+ - **T1**:导航、JS 执行、截图、原始输入、轻量网络列表、计算样式、独立 HTTP 请求、共享视口、状态与能力发现。轻量 `browser_network` 只提供元数据环形缓冲,且**只读**(不取页面队列与单 owner 租约);深度抓包、正文与控制属于 P0-B 的七个工具,两者并存、语义不互相顶替。
43
43
  - **T2**:snapshot → ref 定位 → 可信输入 → 证据 → 等待/节点状态,覆盖点击、输入、滚动、表单与状态读取。
44
44
  - **T3**:纯 JavaScript、零 npm 依赖的图像处理、确定性识别、YIQ 差异、坐标输入、受管基线、回归与六类缺陷用例。
45
45
  - **T4**:真实 CDP screencast 与 screenshot-loop 两条帧源、有界帧存储、七类确定性分析、WAAPI 运动控制、六向事件关联。
46
46
  - **P0-B**:采集(HTTP/HTTPS 请求响应、WebSocket、SSE、同页 iframe 与 dedicated worker)、时序、分析、业务链路、控制、证据导出与多 Agent 订阅。
47
+ - **P0-D**:混合画布与标注文档——面板画布/标注读写、`browser_screenshot {includeAnnotations:true}` 叠加,以及 Agent 侧 `browser_annotation`(`read`/`draw`/`erase`/`clear`,写同一份文档,带前台标签门禁与 `expectRevision` CAS)。
48
+
49
+ ## 跨环境适配状态
50
+
51
+ 三类 adapter 契约(TargetAdapter / CapabilityAdapter / P0Provider)已落地,Web 是唯一真实实现;配置驱动工具 schema、Skill 目录/正文与执行准入。`targets/fake/` 只是 test-only 契约夹具,不是真实平台。
52
+
53
+ | 环境 | 工程支持 | 说明 |
54
+ | --- | --- | --- |
55
+ | Web / Chromium | supported | 37 个 `browser_*` 工具、T1–T4 与 P0;唯一可执行的真实目标环境 |
56
+ | Web 运行环境 `chrome` | supported | 默认 provider;可执行文件、覆盖变量、profile、身份校验均已参数化 |
57
+ | Web 运行环境 `edge` | supported(代码)/ 依赖视本机而定 | 与 Chrome 复用同一套 CDP 实现,候选路径、`DSH_UI_TESTING_EDGE`、profile、产品身份校验独立;本机没有 Edge 二进制时依赖轴报 `failed / executable-missing`,真机拉起未实测 |
58
+ | Web 运行环境 `firefox` | unsupported | 配置里可写入开关,但没有驱动;设置页置灰,执行返回 `ADAPTER_UNSUPPORTED` |
59
+ | fake(test-only) | supported(仅测试) | 确定性夹具,用于验证契约/隔离/证据;生产配置默认拒绝,不得称为真实平台 |
60
+ | Linux/Windows/macOS 桌面、Android、TUI、HarmonyOS、小程序、游戏、3D | planned / unsupported | 尚无 adapter 与真实探针;设置页显示 planned,运行环境选择器不启用 |
47
61
 
48
62
  ## 已知限制
49
63
 
50
- - **仅 Web 目标环境。** Web / Chromium 是唯一实现的目标环境,可运行在 Linux、macOS 与 Windows 宿主上;桌面应用、Android、TUI、HarmonyOS、游戏与 3D 尚无适配器,运行时选择器保持禁用。
64
+ - **仅 Web 目标环境。** Web / Chromium 是唯一实现的目标环境,可运行在 Linux、macOS 与 Windows 宿主上;桌面应用、Android、TUI、HarmonyOS、小程序、游戏与 3D 尚无适配器,运行时选择器保持禁用。跨环境契约与配置隔离已落地,但不等于这些平台已支持。
51
65
  - **视觉是确定性的,不是语义的。** 没有 VLM/OCR、没有语义命名;「这段文字是什么意思」「人看着对不对」不在范围内。
52
66
  - **T4 验收未全覆盖。** 44 条动态行为门槛中 12 条通过、20 条部分通过、12 条未运行、0 条失败。close/dispose 恢复、WebGPU 渲染、媒体 seek/stall、header 校验身份、16 ms 闪白覆盖与长连接稳定性仍未验证。
53
67
  - **网络采集边界不完整。** OOPIF 为部分覆盖;shared/service worker 不采集;GUI 内部标签页可打开真人触发的 `target=_blank`(每个标签独立页面/Runtime),Agent 工具操作当前会话 selectedTab(未选择时 primary),多标签详见 [multi-tab.md](multi-tab.md),自动/未授权 popup 仍阻断,也不做跨标签聚合;HTTPS 未独立验收;`responseModification` 在本构建固定为 `false`(`modify-response` 不可用,不做 CDP 能力探测);HAR 只保证 1.2 子集。
54
68
  - **共享浏览器 profile。** Cookie 与存储由 Chromium profile 共享;只有页面、URL、视口、历史、网络缓冲与租约按工作区隔离,**不是登录态隔离**。
69
+ - **原生弹层不在页面像素里。** `alert/confirm`、浏览器默认右键菜单、文件选择器由浏览器 UI 图层绘制,不进入页面渲染面,因此面板(screencast/截图)与 `browser_screenshot` 都看不到它们。**右键提供面板菜单**:预览区内阻止 DSH Web 的菜单,向页面完整派发可信右键。页面取消默认行为时保留页面自绘菜单;未取消时显示面板菜单,覆盖后退、前进、重新加载和复制页面/链接/图片地址及选中文本。此菜单属于面板 DOM,不进入目标页截图;观测未知时不猜测显示。iframe、失效、剪贴板与功能范围见[页面右键菜单](panel-context-menu.md)。预览区之外的右键保持宿主原有行为。**单选 `<select>` 已有面板侧浮层**:点击后 Host 用 `POST /element` 只读返回选项,面板在点击点画自己的列表(↑↓ / Enter / Esc、点击外部关闭、超出边界自动上翻左移),选中后通过 `POST /select` 以 native setter + `input`/`change` 写回再补一次 `Escape` 关掉可能已打开的原生弹层。不支持浮层的形态(`multiple`、`size > 1`、没有选项)会在地址栏下方、预览区域外提示「该下拉不支持浮层,请用键盘 ↑↓ + Enter」,不覆盖页面或抢占键盘焦点,Esc 可关闭提示。同进程 `<iframe>` 内的单选 `<select>` 走同一条链路(命中返回顶层文档坐标与该 frame 的 `loaderId`);跨进程 OOPIF 与 closed shadow root 内部不读取、不显示浮层。自定义下拉沿用页面自己的鼠标与键盘交互;agent 可用 `browser_fill_form` 的 `combobox` 选择原生控件。
70
+ - **面板键盘转发的是按键本身。** 除注释/选元素模式与 IME 组合期间外,按键都以 keydown + keyUp 转发到页面,带 CDP `modifiers` 位掩码(Alt=1/Ctrl=2/Meta=4/Shift=8)、物理 `code`、`virtualKeyCode` 与 `autoRepeat`,可打印字符同时带 `text/unmodifiedText`;`Ctrl/Cmd/Alt` 组合与 F1–F12、Home/End/PgUp/PgDn/Delete/Insert 都在范围内。
55
71
  - **截图不脱敏。** 分享前请检查内容;附件返回也不等于 provider 一定以原始分辨率收到图像。
56
72
  - **共享状态是乐观并发。** `browser_viewport get` 是缓存诊断,`stale` 表示「最近没观测」;`expectedRevision` 竞争失败返回 `VIEWPORT_CONFLICT` 且不自动重放。
57
73
 
@@ -8,6 +8,7 @@
8
8
  | --- | --- |
9
9
  | `scale` | 0.1–4 的连续值;省略时解析为当前共享 `view.scale`(冷启动为 1) |
10
10
  | `region` | 画布 CSS 像素矩形,宽高 ≥1;省略则截当前视口 |
11
+ | `includeAnnotations` | `boolean`,默认 `false`;`true` 时叠加当前目标已提交且经 Host 接受的面板标注 |
11
12
  | `persist` | 显式要求持久化;默认暂存 |
12
13
  | `out` | 工作区相对的 `.png` 路径,提供即视为持久化 |
13
14
 
@@ -20,12 +21,48 @@
20
21
  | `capture.request` | 实际生效的 scale、`scaleSource`(`explicit`/`view`)、`viewRevision`、region、画布空间 |
21
22
  | `capture.mapping` | 文档/画布/视口之间的映射,逐轴 `cssPerPixel`、`scrollAtCapture` |
22
23
  | `capture.canvas` | `composite` 与 `contentRect`;`O ≠ 0` 时为 `true` |
24
+ | `capture.annotations` | 未开启时为 `null`;开启时记录标注叠加状态与 Host 已接受的文档身份,字段见下 |
23
25
  | `capture.anomaly` | 全量像素统计:`uniform`、`transparent`、`maxDelta`、`averageColor`、`distinctColors` |
24
26
  | `capture.file` | `pathKind`(`relative`/`absolute`)、`persistence`、`relativePath` |
25
27
  | `warnings` | 封闭词表内的警告,不猜测 |
26
28
 
27
29
  `anomaly` 对 ≤16,000,000 像素做**全量扫描**(不是抽样),因此「纯色/空白」结论可靠;超过预算会明确拒绝而不是降级抽样。
28
30
 
31
+ ## 带面板标注的截图
32
+
33
+ 用户提到「标注」「我画的」「圈」「框」「箭头」「笔迹」等、指代调试面板标注时,先读 `browser_state.annotations` 的图形、revision 与目标上下文,再调用:
34
+
35
+ ```text
36
+ browser_screenshot { "includeAnnotations": true }
37
+ browser_screenshot { "includeAnnotations": true, "scale": 1.5, "region": { "x": -100, "y": 0, "width": 800, "height": 600 } }
38
+ ```
39
+
40
+ - 只叠加已提交且已由 Host 接受的标注文档;圆形/椭圆(`circle`)、矩形(`rect`)、线段(`line`)、箭头(`arrow`)、画笔(`brush`)、文字(`text`)的颜色、粗细与填充/外框遵循面板渲染语义。橡皮擦与清除用于编辑文档,不是额外形状。
41
+ - 面板标注栏的「截图」按钮通过 `POST /plugins/ui-testing/screenshot {includeAnnotations:true}` 拿同一份合成 PNG 交给聊天框附件(不起新标签、不落盘);
42
+ - 写这份文档的入口有**两处**:面板认证的 `POST /plugins/ui-testing/annotations`,以及 Agent 工具 `browser_annotation`(`read`/`draw`/`erase`/`clear`,`draw` 追加形状、`clear` 清空)。两者写的是同一份文档,所以 `includeAnnotations:true` 会把人类画的与 Agent 画的形状一起叠加;写动作的前台门禁、`expectRevision` 冲突与软限额见[视口、画布与标注](viewport-control.md)。
43
+ - 页面与标注使用同一 `region`/`scale` 映射和裁剪。画布点 `d` 在截图中的连续像素位置为 `(d − region.origin) / capture.mapping.cssPerPixel`(逐轴计算,通常等于乘以 scale);线宽、文字与笔迹同步缩放。负坐标和画布空白区域也沿用该映射。
44
+ - 不包含尚未提交的拖画/文字草稿、工具栏、选中控件或其它面板 UI;也不把标注写入页面 DOM。截图使用 Host 已接受的文档,不能据此声称捕获了面板此刻的本地草稿。
45
+ - 标注来源 `source.trusted=false`:它是用户指路线索,不能证明页面缺陷或授权自动点击。可以查看截图中的手写笔迹;不能可靠辨认时如实说明,不猜字。
46
+ - 默认 `browser_screenshot`、T3 的原始源图与 T4 两种帧源均不包含面板标注;`includeAnnotations` 仅属于 `browser_screenshot`,不传播到 T3/T4。T3 的 `annotations` 参数是图片后处理标记,与面板标注文档分开。
47
+
48
+ ### 标注证据
49
+
50
+ `capture.annotations` 在 `includeAnnotations` 省略或为 false 时为 `null`;开启时包含:
51
+
52
+ | 字段 | 含义 |
53
+ | --- | --- |
54
+ | `enabled` | `true`,表示本次请求开启标注叠加 |
55
+ | `composited` | 布尔值,表示是否执行标注合成;空文档为 `false`,不能据此断言某个图形在 region 内可见 |
56
+ | `space` | `canvas-css-px`,与标注文档坐标空间一致 |
57
+ | `revision` / `updatedAt` | 本次使用的 Host 已接受文档的版本与更新时间 |
58
+ | `source` | 直接沿用 state 标注文档的来源;空文档为 `null`,有来源时 `source.trusted=false` |
59
+ | `context` | 直接沿用 state 标注文档的目标上下文,用于核对文档身份 |
60
+ | `shapeCount` | 接收的形状数量;包含可能完全位于截图 region 外的形状,不等于可见数量 |
61
+ | `stale` / `staleReason` | 标注文档的新鲜度及原因,直接反映文档状态 |
62
+ | `truncated` / `truncation` | 是否及如何裁剪标注文档,直接沿用 state 的截断信息 |
63
+
64
+ 空文档也返回 `enabled:true`、`composited:false` 的证据,不能把开启参数等同于“截图里一定有标注”。若 `stale` 或 `truncated` 表示证据不完整,报告该限制;不要伪造 revision、来源心跳或未接收的图形。
65
+
29
66
  ## 文件生命周期
30
67
 
31
68
  - **默认暂存**:写入插件实例级暂存目录,返回绝对路径,实例销毁时清理,不污染工作区。
@@ -42,12 +79,12 @@
42
79
  | `OUTPUT_EXISTS` | 目标路径已存在 |
43
80
  | `OUTPUT_PATH_INVALID` | 持久根不可用或路径越界 |
44
81
  | `CAPTURE_FAILED` | CDP 截图失败或 PNG 校验不通过 |
45
- | `BUSY` | 工作区页面被其他 owner 占用 |
82
+ | `BUSY` | 该页队列正被其他会话的写操作占用(短暂忙,不是权限答案) |
46
83
 
47
84
  ## 边界
48
85
 
49
86
  - 截图成功**不等于**视觉验收通过;需要结论请用 T3 的 `verdict`。
50
87
  - 截图内容不脱敏;分享前请自行检查。
51
88
  - 返回图片附件不等于 provider 一定以原始分辨率收到图像。
52
- - 画布合成路径会重编码,因此 `O ≠ 0` 时不要断言 PNG 字节稳定;需要字节级稳定时保持 `O = 0`。
89
+ - 画布合成或标注叠加可能重编码,不承诺 PNG 字节稳定;需要保留原页面直接截图路径时保持 `O = 0` 且 `includeAnnotations:false`。
53
90
  - 高 DPI 只提升 DOM/CSS/文字与矢量的光栅密度,Canvas/WebGL 的内部位图不会变清晰。
@@ -0,0 +1,157 @@
1
+ # 诊断状态码(中英对照)
2
+
3
+ 诊断页(设置页每张卡片的「诊断」)上出现的每个状态码都在这里对照:**英文码是机器契约,中文是给人看的**。
4
+
5
+ ## 两种文字分居两处
6
+
7
+ **界面上只显示中文**;英文码是给调用方 agent 的,只出现在属性与 NDJSON 里,不挤进可见文案。
8
+
9
+ | 位置 | 界面可见(中文) | 给 agent 的英文 |
10
+ | --- | --- | --- |
11
+ | 节点状态 | `警告 · 未找到可执行文件` | `data-uts-node-state="WARN"`、`data-uts-stage-reason="executable-missing"` |
12
+ | 总览胶囊 | `警告 · 运行环境与所选 provider 不一致` | `data-uts-overall="WARN"`、`data-uts-overall-reason="provider-mismatch"` |
13
+ | 各模块诊断状态(六轴)的原因 | `未选择该运行环境` | `data-uts-axis-reason-code="provider-not-selected"` |
14
+ | 能力就绪的检查项(一行一项,最多三行,多余用 `+n`) | `只读求值:跳过` / `目标列表:跳过` / `快照:跳过 +3` | `data-uts-axis-checks-en="readonly-eval: skip · target-list: skip …"`、`data-uts-axis-checks-truncated` |
15
+ | 诊断日志 | `第 4 段 · 警告 · 没找到该浏览器的可执行文件` | `data-uts-event-operation="stage-4"`、`data-uts-event-reason="executable-missing"`、`data-uts-event-status="WARN"` |
16
+ | NDJSON 事件流 | — | 事件对象里的 `reason` 原样是英文 |
17
+
18
+ 表里没有的码不猜:界面回落到原样显示英文码(不会显示错误的中文),这样宿主以后新增码也不会误导用户。
19
+
20
+ 诊断日志左侧那一列同理:界面写 `本轮开始` / `第 3 段` / `本轮结束`,底层 id 仍是 `run-start` / `stage-3` / `run-end`(读 `data-uts-event-operation`)。
21
+
22
+ 节点卡片里的文案会被截断(状态区固定两行):把鼠标停在节点上,提示里按行列出角色·名称、职责、完整状态、原因码、阻塞阶段与耗时,被省略的内容都能读到。
23
+
24
+ ## 术语对照(中文界面 / 英文底层)
25
+
26
+ 下面这些词在界面上**一律用中文**;英文原样只出现在 `data-uts-*`、NDJSON 与代码里,供 agent 与排障使用。译文按上下文取用户能看懂的词,不做字对字直译。
27
+
28
+ | 英文(底层) | 界面中文 | 说明 |
29
+ | --- | --- | --- |
30
+ | `run` | 本轮 | 一轮完整诊断;界面上说「本轮」 |
31
+ | `Host` | 宿主 | 运行本插件的 DSH 进程 |
32
+ | `Failed to fetch` | 连不上宿主(请求失败) | 浏览器原话、对用户没有信息量,换成说清对象 |
33
+ | `stage-start` | 阶段开始 | 某一段开始检查 |
34
+ | `stage-end` | 阶段结束 | 某一段检查结束 |
35
+ | `readonly-eval` | 只读求值 | 在页面里执行只读表达式 |
36
+ | `target-list` | 目标列表 | 标签页 / 目标清单 |
37
+ | `snapshot` | 快照 | 结构化快照 |
38
+ | `screenshot` | 截图 | 截图 |
39
+ | `computed-style` | 计算样式 | 读页面的计算样式 |
40
+ | `network-read` | 网络记录 | 读最近几条网络请求 |
41
+ | `pass` | 通过 | 检查项通过 |
42
+ | `fail` | 失败 | 检查项失败 |
43
+ | `skip` | 跳过 | 检查项被跳过 |
44
+ | `ok` | 通过 | 就绪轴通过(与 pass 同译) |
45
+ | `PASS` | 正常 | 整段结论通过 |
46
+
47
+ ## 五个结果状态(同样中英双语)
48
+
49
+ 状态词与原因码一样,界面上**只写中文**,例如 `警告 · 浏览器未运行`;大写状态码只在 `data-uts-node-state` / `data-uts-overall` 等属性里。
50
+
51
+ | 状态(机器) | 中文(界面) | 含义 |
52
+ | --- | --- | --- |
53
+ | `PASS` | 正常 | 这一段真的通过了 |
54
+ | `WARN` | 警告 | 这一段跑完但有问题,后面跟原因码(如 `browser-not-running`) |
55
+ | `FAIL` | 异常 | 这一段失败,链路在这里断 |
56
+ | `SKIPPED` | 跳过 | 这一段没执行(上游已失败或 provider 不匹配),带 `blockedBy` |
57
+ | `UNKNOWN` | 未知 | 这一段没给出结论(例如超时、被取消) |
58
+
59
+ 这五个词只表示**本轮诊断已经得出的结论**:`警告` 后面跟的原因码就是结论本身(「警告 · 未找到可执行文件」= 查出来缺可执行文件,不是等你操作)。
60
+
61
+ 还没跑出结论时不借用五态的词,也**没有机器码**(它们只是本页的说明):**待诊断**(这一屏还没跑过 run)、**诊断未完成**(run 没能跑出结果,错误另起一行)。详见 [快速开始](getting-started.md)。
62
+
63
+ > 面板四块的标题现在是**调试流程 / 各模块诊断状态 / 诊断日志 / 步骤职责**(步骤按 1–5 编号),右上角原来那几条英文装饰(`HOST RUN · STAGED CHECKS`、`THIS RUN · <runId>`、`HOST STAGES · 8 行`、`LOGICAL LAYERS`)已去掉;同样的信息在 `data-uts-run-id`、`data-uts-axis-source`、`data-uts-event-count` 这些属性里仍可读到。
64
+
65
+ ## ① 诊断 run 的五个阶段
66
+
67
+ 阶段编号:① 宿主集成/工具注册/调用通道、② 插件 Core/配置/调度、③ 通信接口探测、④ 执行后端与依赖、⑤ 目标列表/绑定/连接。
68
+
69
+ | 状态码(英文,机器契约) | 中文(界面显示) | 出现位置 |
70
+ | --- | --- | --- |
71
+ | `scope-unresolved` | 无法确定当前作用域(不知道要查哪个工作区) | 全部阶段(作用域未定时) |
72
+ | `registration-unreadable` | 读取工具注册信息失败 | 阶段 1 |
73
+ | `registration-broken` | 工具声明与实际注册对不上 | 阶段 1 |
74
+ | `tool-call-not-attempted` | 没能发起工具调用 | 阶段 1 |
75
+ | `tool-call-failed` | 工具调用失败 | 阶段 1 |
76
+ | `tool-not-admitted` | 本作用域未放行这些工具 | 阶段 1 |
77
+ | `config-invalid` | 宿主配置无效 | 阶段 2 |
78
+ | `adapter-unresolved` | 没取到该运行环境的执行适配器 | 阶段 2 |
79
+ | `no-runtime-entry` | 插件还没建立运行时入口 | 阶段 2、阶段 3 |
80
+ | `busy` | 插件正在处理别的操作,稍后再试 | 阶段 3 |
81
+ | `browser-not-running` | 浏览器未运行 | 阶段 3 |
82
+ | `provider-mismatch` | 实际在跑的浏览器与所选运行环境不一致 | 阶段 3 |
83
+ | `cdp-protocol-error` | 浏览器调试协议返回错误 | 阶段 3 |
84
+ | `cancelled` | 本轮检查已被取消 | 全部阶段(被取消) |
85
+ | `executable-missing` | 没找到该浏览器的可执行文件 | 阶段 4、六轴「依赖可用」 |
86
+ | `identity-mismatch` | 启动起来的浏览器不是所选的那一个 | 阶段 4 |
87
+ | `identity-unverified` | 拿不到浏览器版本信息,无法确认身份 | 阶段 4 |
88
+ | `blocked-by-provider-mismatch` | 上游运行环境不一致,这一段没执行 | 阶段 5 |
89
+ | `blocked-by-stage-1` | 第 1 段失败,这一段没执行 | 阶段 2–5(前一段失败时) |
90
+ | `blocked-by-stage-3` | 第 3 段未通过,这一段没执行 | 阶段 5 |
91
+ | `stage-timeout` | 这一段超时,没拿到结果 | 全部阶段(该段超时) |
92
+ | `target-list-failed` | 读取标签页列表失败 | 阶段 5 |
93
+ | `no-bound-target` | 还没有绑定调试目标 | 阶段 5 |
94
+ | `readonly-eval-failed` | 只读求值失败(在页面里跑只读脚本出错) | 阶段 5 |
95
+ | `readonly-eval-unexpected` | 只读求值的结果不是预期的 1 | 阶段 5 |
96
+ | `target-not-in-list` | 目标已不在标签页列表里(可能已被关闭) | 阶段 5 |
97
+ | `capability-partial` | 部分能力不可用 | 阶段 5 |
98
+ | `cold` | 浏览器还没启动 | 阶段 4、六轴「依赖可用」 |
99
+ | `host-idle-no-transport` | 宿主空闲,没有可用的传输通道 | 阶段 4、六轴「依赖可用」 |
100
+ | `session-missing` | 调试会话已不存在 | 阶段 4、六轴「连接可信」 |
101
+ | `aborted` | 已中止 | 阶段 3、阶段 4(被中止) |
102
+
103
+ ## ② 就绪六轴的来源(`not-probed` 等)
104
+
105
+ 诊断页第二块「各模块诊断状态」就是这六轴(工程支持 / 目标绑定 / 用户启用 / 能力就绪 / 依赖可用 / 连接可信)。它们来自 Host 的就绪快照或本次 run;没有 run 结论时回退快照,此时原因码是下面这些。
106
+
107
+ | 状态码(英文,机器契约) | 中文(界面显示) | 出现位置 |
108
+ | --- | --- | --- |
109
+ | `not-probed` | 未探测 | 六轴(未探测) |
110
+ | `probe-failed` | 探测失败 | 六轴「依赖可用」 |
111
+ | `environment-disabled` | 环境总开关已关闭 | 六轴(环境总开关关闭) |
112
+ | `adapter-not-registered` | 该环境还没接入适配器 | 六轴「工程支持」「能力就绪」 |
113
+ | `no-capabilities` | 没有上报能力清单 | 六轴「能力就绪」 |
114
+ | `no-runtime` | 宿主里还没有这个环境的运行时 | 六轴「连接可信」 |
115
+ | `no-target` | 还没有目标 | 六轴「目标绑定」 |
116
+ | `target-not-bound` | 目标未绑定 | 六轴「目标绑定」 |
117
+ | `target-unbound` | 目标已解绑 | 六轴「目标绑定」 |
118
+ | `target-gone` | 调试目标已消失 | 六轴「目标绑定」 |
119
+ | `cdp-not-attached` | 还没连上浏览器调试端口 | 六轴「连接可信」 |
120
+ | `collector-missing` | 缺少网络采集器 | 六轴「连接可信」 |
121
+ | `frame-source-missing` | 没有可用的帧来源 | 六轴「连接可信」 |
122
+ | `input-unsupported` | 该浏览器后端不支持模拟输入 | 六轴「连接可信」 |
123
+ | `provider-not-selected` | 未选择该运行环境 | 六轴(该运行环境未选中) |
124
+ | `provider-disabled` | 该运行环境已停用 | 六轴(该运行环境停用) |
125
+ | `provider-driver-pending` | 该运行环境的驱动待接入 | 六轴「工程支持」 |
126
+ | `provider-not-implemented` | 该运行环境尚未实现 | 六轴「工程支持」 |
127
+ | `provider-dependency-not-verified` | 还没验证该运行环境的依赖 | 六轴「依赖可用」 |
128
+ | `provider-missing` | 没有这个运行环境 | 六轴(该运行环境不存在) |
129
+ | `driver-pending` | 该运行环境的驱动待接入 | 六轴「工程支持」「依赖可用」 |
130
+ | `unknown-provider` | 没有这个运行环境 | 六轴(运行环境不存在) |
131
+ | `provider-unsupported` | 该运行环境不受支持 | 六轴「工程支持」 |
132
+ | `override-invalid` | 指定的浏览器路径不可用 | 六轴「依赖可用」 |
133
+ | `capability-disabled` | 该能力已关闭 | 六轴「能力就绪」 |
134
+ | `capability-unknown` | 未知的能力名 | 六轴「能力就绪」 |
135
+ | `no-session` | 没有调试会话 | 六轴「连接可信」 |
136
+
137
+ ## ③ 页面自身造的两个码
138
+
139
+ 不是 Host 的码,只是本页在没有结论时的说明。
140
+
141
+ | 状态码(英文,机器契约) | 中文(界面显示) | 出现位置 |
142
+ | --- | --- | --- |
143
+ | `no-run` | 还没有诊断结果 | 总览胶囊与节点(页面自身) |
144
+ | `run-in-progress` | 诊断进行中 | 总览胶囊(页面自身) |
145
+
146
+ ## 动态族
147
+
148
+ | 模式 | 中文 | 说明 |
149
+ | --- | --- | --- |
150
+ | `tool-call-<ERROR_CODE>` | 工具调用失败 | 阶段 ① 用真实 handler 调一次 `browser_capabilities` 失败时,把工具错误码小写拼在后面,例如 `tool-call-tool_not_found` |
151
+ | `stage-<n>` | 该段没有给出原因 | 阶段 ⑤ 判断目标绑定时,该段既没 PASS 也没写原因,Host 用这个兜底 |
152
+
153
+ ## 怎么维护
154
+
155
+ 1. 客户端词表在 [`client.js`](../client.js) 的 `DIAG_REASON_ZH`;
156
+ 2. 本文件的表格必须逐条一致;
157
+ 3. `tests/contracts/diagnose-status-text.test.mjs` 会强制三件事:客户端表与本表相同、Host 侧能吐出的诊断原因码全部有中文、没有重复码。改词表就同时改这两处,否则测试失败。