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

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 (88) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +172 -36
  3. package/annotation-capture.js +111 -0
  4. package/browser-safety.js +45 -1
  5. package/client.js +3721 -132
  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/images/tutorial-01-overview.png +0 -0
  28. package/docs/images/tutorial-02-history-dropdown.png +0 -0
  29. package/docs/images/tutorial-03-pick-element.png +0 -0
  30. package/docs/images/tutorial-04-annotate.png +0 -0
  31. package/docs/index.md +11 -7
  32. package/docs/multi-tab.md +55 -20
  33. package/docs/network-p0.md +2 -2
  34. package/docs/panel-context-menu.md +41 -0
  35. package/docs/t2-structured-gui.md +2 -2
  36. package/docs/t3-vision.md +6 -1
  37. package/docs/t4-dynamic-behavior.md +1 -0
  38. package/docs/tools.md +65 -7
  39. package/docs/tutorial.md +125 -0
  40. package/docs/verification.md +26 -2
  41. package/docs/viewport-control.md +21 -8
  42. package/index.js +3263 -251
  43. package/integration/cross-environment.js +1535 -0
  44. package/integration/diagnose.js +945 -0
  45. package/integration/tab-principal.js +493 -0
  46. package/locale/en.json +1 -1
  47. package/locale/zh.json +1 -1
  48. package/network-p0.js +24 -7
  49. package/package.json +4 -3
  50. package/select-control.js +298 -0
  51. package/skills/ui-testing-common/SKILL.md +54 -0
  52. package/skills/ui-testing-fake/SKILL.md +43 -0
  53. package/skills/{ui-testing → ui-testing-web}/SKILL.md +64 -24
  54. package/skills/{ui-testing → ui-testing-web}/manifests/declared-capabilities.json +9 -2
  55. package/skills/{ui-testing → ui-testing-web}/references/contracts/capability.md +3 -3
  56. package/skills/{ui-testing → ui-testing-web}/references/contracts/evidence.md +1 -1
  57. package/skills/{ui-testing → ui-testing-web}/references/contracts/operation.md +1 -1
  58. package/skills/{ui-testing → ui-testing-web}/references/core/fallback.md +1 -1
  59. package/skills/{ui-testing → ui-testing-web}/references/core/page-identity.md +3 -2
  60. package/skills/{ui-testing → ui-testing-web}/references/core/selection.md +1 -1
  61. package/skills/{ui-testing → ui-testing-web}/references/core/tool-routing-and-limits.md +6 -3
  62. package/skills/{ui-testing → ui-testing-web}/references/platforms/index.md +1 -1
  63. package/skills/{ui-testing → ui-testing-web}/references/platforms/web.md +2 -2
  64. package/skills/ui-testing-web/references/scenarios/annotations.md +104 -0
  65. package/skills/{ui-testing → ui-testing-web}/references/scenarios/functional.md +1 -1
  66. package/skills/{ui-testing → ui-testing-web}/references/scenarios/visual-motion.md +1 -1
  67. package/skills/{ui-testing → ui-testing-web}/references/tiers/t1-programmatic.md +2 -2
  68. package/skills/{ui-testing → ui-testing-web}/references/tiers/t3-static-visual.md +2 -2
  69. package/skills/{ui-testing → ui-testing-web}/references/tiers/t4-dynamic-visual.md +2 -1
  70. package/targets/fake/adapter.js +1163 -0
  71. package/targets/fake/fixtures.js +177 -0
  72. package/targets/web/adapter.js +1231 -0
  73. package/targets/web/provider.js +415 -0
  74. package/skills/ui-testing/references/scenarios/annotations.md +0 -87
  75. /package/skills/{ui-testing → ui-testing-web}/references/core/dynamic-visual.md +0 -0
  76. /package/skills/{ui-testing → ui-testing-web}/references/core/network-and-state.md +0 -0
  77. /package/skills/{ui-testing → ui-testing-web}/references/core/report-template.md +0 -0
  78. /package/skills/{ui-testing → ui-testing-web}/references/core/reporting.md +0 -0
  79. /package/skills/{ui-testing → ui-testing-web}/references/core/safety.md +0 -0
  80. /package/skills/{ui-testing → ui-testing-web}/references/core/verdict-examples.md +0 -0
  81. /package/skills/{ui-testing → ui-testing-web}/references/core/verdict-gate.md +0 -0
  82. /package/skills/{ui-testing → ui-testing-web}/references/core/verification.md +0 -0
  83. /package/skills/{ui-testing → ui-testing-web}/references/core/workflow.md +0 -0
  84. /package/skills/{ui-testing → ui-testing-web}/references/tiers/t2-structured.md +0 -0
  85. /package/skills/{ui-testing → ui-testing-web}/templates/capability-note.md +0 -0
  86. /package/skills/{ui-testing → ui-testing-web}/templates/platform.md +0 -0
  87. /package/skills/{ui-testing → ui-testing-web}/templates/regression-case.md +0 -0
  88. /package/skills/{ui-testing → ui-testing-web}/templates/scenario.md +0 -0
package/docs/tools.md CHANGED
@@ -1,14 +1,28 @@
1
1
  # 工具参考
2
2
 
3
- 插件向 Agent 注册 **36 个** `browser_*` 工具。工具名即调用名,返回值统一以 `{ok:true,…}` 表示工具执行完成,失败抛类型化 `BrowserError`。
3
+ 插件向 Agent 注册 **37 个** `browser_*` 工具。工具名即调用名,返回值统一以 `{ok:true,…}` 表示工具执行完成,失败抛类型化 `BrowserError`。
4
4
 
5
5
  > `ok:true` 只说明工具完成了动作,**不等于**验收通过。视觉结论看 `verdict`,网络结论看 `integrity`,动态结论看 `analysis.verdict`。
6
6
 
7
+ 工具声明与执行由 Host 配置驱动:默认只暴露当前环境的专用 schema(Web 默认 37 个);未启用/非当前环境的工具不会出现在模型请求中,旧调用与 GUI 旁路在执行时仍会被拒绝(`ADAPTER_DISABLED`,未注册的工具报 `unknown tool`)。`browser_state` 与 `browser_capabilities` 是**环境中立诊断**:任何环境(含被禁用)都保留,用于回答"支持什么/为什么被拒",它们不启动目标;被禁用环境的六轴报告为 `support=supported` + `enabled=false`,不再退化成"未注册"或 `INTERNAL`。成功/失败 envelope 额外携带 `configRevision`、`environment`、`adapterId`,用于核对本次执行使用的配置版本;`browser_capabilities.adapter` 报告 adapter 契约版本、能力矩阵、P0 family 状态、六轴就绪,以及 `declaration`:`registeredTools`(当前注册面=模型请求能看到的插件 schema)、`admittedTools`(逐 Agent 准入集合)、`isolated`(两者是否一致)。
8
+
9
+ **工具布局由设置页的面板配置作为唯一权威**:启用了哪些环境,就只注册这些环境的工具,不会额外带入其它环境的 schema。
10
+
11
+ | 面板里启用 | 注册面(模型请求可见的插件工具) |
12
+ | --- | --- |
13
+ | 只 Web | 37 个 `browser_*`(其中 `browser_state`/`browser_capabilities` 是两个环境无关诊断) |
14
+ | Web + fake(测试环境) | 上述 37 个 + `browser_fake_probe` = 38 |
15
+ | 只 fake | `browser_fake_probe` + `browser_state` + `browser_capabilities` = 3 |
16
+ | 全关 | `browser_state` + `browser_capabilities` = 2(诊断在任何环境都可用,用于回答“支持什么/为什么被拒”) |
17
+
18
+ 被禁用或不允许选择的环境,其工具**根本不注册**,因此不可能出现在任何模型请求里。`when(agent)` 仍是第二道闸门(会话级执行准入与 Skill 发现都按会话解析),但**声明面以注册范围为准**:实测本机 3080 的 `request/header` 按“已注册”构建请求,`when=false` 不会移除声明,所以 `declaration.surface=host-tool-registration`、`registration=enabled-union`;两者不一致时 `isolated=false` 并给出说明。三个环境的真实请求头实测:37 → 2 → 37(关闭/重开 Web)。`browser_fake_probe` 仅在 `DSH_UI_TESTING_TEST_HOOKS=1`/`DSH_UI_TESTING_ALLOW_TEST_ENV=1` 的测试配置中注册,不属于生产 37 工具面。
19
+
7
20
  ## 总览
8
21
 
9
22
  | 分组 | 数量 | 工具 |
10
23
  | --- | --- | --- |
11
24
  | P0-E 多标签(tabs) | 1 | `browser_tabs` |
25
+ | P0-D 标注(annotations) | 1 | `browser_annotation` |
12
26
  | 程序化(T1) | 10 | `browser_navigate` `browser_eval` `browser_screenshot` `browser_input` `browser_network` `browser_computed_style` `browser_http_request` `browser_viewport` `browser_state` `browser_capabilities` |
13
27
  | 结构化 GUI(T2) | 7 | `browser_snapshot` `browser_click` `browser_type` `browser_scroll` `browser_fill_form` `browser_node_state` `browser_wait_for` |
14
28
  | 静态视觉(T3) | 6 | `browser_vision_image` `browser_vision_find` `browser_vision_compare` `browser_vision_act` `browser_vision_baseline` `browser_vision_check` |
@@ -21,7 +35,49 @@
21
35
 
22
36
  | 工具 | 主要参数 | 要点 |
23
37
  | --- | --- | --- |
24
- | `browser_tabs` | `action:"list"\|"current"\|"select"\|"open"\|"close"\|"wait"` `tabId?` `kind?` `source?` `timeoutMs?` `pollMs?` | `list/current/wait` 只读(wait 纯内存轮询,不启动 Chromium、不取 lease,超时 `WAIT_TIMEOUT`);`select` 原子 claim;`open` 新建 about:blank;`close` 仅本人有效 lease;`list` 每项含 `source/parentTabId/openEventId/createdAt/lastUsedAt/idleRemainingMs/manageable` 并返回 `notices`/`recentClosed`(各最多 8 条);错误 `TAB_NOT_FOUND/TAB_LIMIT/TAB_FORBIDDEN/BUSY/WAIT_TIMEOUT` |
38
+ | `browser_tabs` | `action:"list"\|"current"\|"select"\|"open"\|"close"\|"wait"\|"acquire"\|"release"\|"transfer"` `tabId?` `mode?`(仅 select,work/observe)`kind?` `source?` `timeoutMs?`(wait/acquire)`pollMs?`(wait)`expectedControlRevision?`(acquire/release/transfer 必填)`expectedOwnershipRevision?`(transfer 必填)`toSessionId?`(transfer) | `list/current/wait` 只读(wait 纯内存轮询,不启动 Chromium、不取 lease,超时 `WAIT_TIMEOUT`);`select` 只改调用者自己的选择(work 写目标 / observe 只读目标),**不 claim 租约、不切前台**;`open` 新建 about:blank;`close` 按长期归属/控制权判定(owner 或 controller,本 root Lead 亦可);`acquire/release/transfer` 仅本 root Lead,带 CAS 版本,`acquire` 先 draining 再等安全边界(不中断在途操作,超时 `BUSY` 且撤销 draining);页面组按 (workspace, provider, 团队根) 划分,child 最多 3 个按 (workspace, provider) 跨 root 合计、全局 live 8;`list` 每项含 `source/parentTabId/openEventId/createdAt/lastUsedAt/idleRemainingMs/manageable` 与 `accessVersion/rootSessionId/creatorSessionId/ownerSessionId/controllerSessionId/ownershipRevision/controlRevision/accessState/permissions`,并返回 `notices`/`recentClosed`(各最多 8 条);错误 `TAB_NOT_FOUND`(跨 root 统一,不泄露存在性)`/TAB_FORBIDDEN/TAB_CONTROL_CHANGED/TAB_OWNERSHIP_STALE/TAB_IDENTITY_UNAVAILABLE/TAB_LIMIT/BUSY(仅短暂忙)/WAIT_TIMEOUT/INVALID_ARGUMENT` |
39
+
40
+ ## 标注(P0-D)
41
+
42
+ `browser_annotation` 读写**调试视图里的面板标注文档**:每个 Runtime 一份,与面板认证的 `POST /plugins/ui-testing/annotations` 是**同一份**文档。人类画的形状和 Agent 画的形状落在同一个文档里,面板下一次 `/state` 轮询(约 500ms)就会画出来。它**不启动浏览器、不进页面串行队列、不占 single-owner lease、不写页面 `lastError`、不改页面 DOM/CSS**——只写 Host 侧的标注文档。坐标契约同[视口、画布与标注](viewport-control.md)。
43
+
44
+ | 动作 | 参数 | 要点 |
45
+ | --- | --- | --- |
46
+ | `read` | 不接受其它字段 | 返回当前文档:`revision`、`shapes`、`shapeCount`、`truncated`、`truncation`、`foreground`。**不受前台条件限制** |
47
+ | `draw` | `shapes` 1–64、`expectRevision?` | 形状**追加**到现有文档,不是替换;省略 `id` 时由 Host 从现有最大 id 继续分配 `a<N>` |
48
+ | `erase` | `ids?` 1–64、`point?`、`expectRevision?` | `ids` 与 `point` 至少给一个;`point` 为 `{x,y,radius?}`,默认半径 8 画布 px |
49
+ | `clear` | 不接受其它字段(`expectRevision?` 除外) | 清空画布:写成空形状列表 |
50
+
51
+ 写动作成功后返回写入后的同一份文档视图,字段与 `read` 相同。
52
+
53
+ 形状字段:
54
+
55
+ | 字段 | 取值 |
56
+ | --- | --- |
57
+ | `type` | 必填:`circle` / `rect` / `line` / `arrow` / `brush` / `text`,分别对应面板的圆形、矩形、线段、箭头、画笔(铅笔)、文字(用于写问题、问号等文字提示) |
58
+ | `id` | 可省略(省略则由 Host 分配);显式给出时必须匹配 `^a[1-9][0-9]{0,15}$` 且不重复 |
59
+ | `color` | `#rrggbb`,默认 `#ff3b30` |
60
+ | `strokeWidth` | 整数 1–20,默认 3(面板的「粗细」) |
61
+ | `style` | `stroke` / `fill`(面板的「外框」/「填充」),只对 `circle`/`rect` 有效;`text` 固定 `fill`,`line`/`arrow`/`brush` 固定 `stroke` |
62
+ | `fill` / `stroke` | 也接受显式布尔值;**同时**给 `style` 与 `fill`/`stroke` 报 `INVALID_ARGUMENT` |
63
+
64
+ 几何字段按类型:
65
+
66
+ | `type` | 字段 | 备注 |
67
+ | --- | --- | --- |
68
+ | `rect` | `x,y,width,height` | 左上角 + 尺寸 |
69
+ | `circle` | `cx,cy,rx,ry` | 与面板一致:包围盒内接椭圆(不是「中心 + 半径」) |
70
+ | `line` / `arrow` | `x1,y1,x2,y2` | `arrow` 的 head 由 Host 按端点与线宽派生,调用方给了也不作数 |
71
+ | `brush` | `points:[[x,y],…]` | 单笔画笔点数受下文限额约束 |
72
+ | `text` | `x,y,text,fontSize?` | `fontSize` 固定 20 |
73
+
74
+ - **橡皮擦命中规则**:`erase.point` 命中形状的判据与面板橡皮擦一致——形状几何到该点的距离 ≤ `max(strokeWidth/2, 6)` 与给定 `radius` 取大者。
75
+ - **前台硬约束**:`draw`/`erase`/`clear` 只在目标标签页**正被人类在调试视图里选中且可见**时才允许,否则**零写入**并报 `ANNOTATION_NOT_FOREGROUND`。判断依据是面板在认证请求上携带的前台标签心跳(约 1.5s 新鲜窗口):人切走标签、关掉面板、或把 DSH 标签页切到后台都会让窗口过期。目的就是 Agent 画的标注必须人能立刻看到。`read` 不受此限;可用 `browser_state.annotations.foreground = {active, ageMs, maxAgeMs}` 先判断能不能画。
76
+ - **页面必须处于运行态**:未启动页面时写标注报 `INVALID_ARGUMENT`(reason `annotation-runtime-not-running`)。
77
+ - **乐观并发**:`expectRevision` 与当前文档 `revision` 不同则报 `ANNOTATION_REVISION_CONFLICT`,零写入并返回 `currentRevision`。
78
+ - **整份替换语义**:文档是整份替换模型——人类在面板里下一次编辑会整体发布自己的形状列表(其中已包含 Agent 画的形状,因为面板会采纳 Host 文档),而人类「清除画布」会连 Agent 的形状一起清掉。
79
+ - **软限额**:与面板写面相同——文档最多 `maxShapes` 128 个形状、单笔画笔 512 点、文档总点数 8192、文字 200 字符、文档 65536 字节。Host 做确定性裁剪,并在结果里如实报 `truncated` / `truncation`。
80
+ - **证据边界**:标注是 `trusted=false` 的问题线索,不是页面事实;`browser_screenshot {includeAnnotations:true}` 会把 Agent 画的形状与人类画的形状一起叠加。
25
81
 
26
82
  ## 程序化(T1)
27
83
 
@@ -29,9 +85,9 @@
29
85
  | --- | --- | --- |
30
86
  | `browser_navigate` | `url` | 只接受 HTTP(S);校验目标、DNS 与最终 URL;**不保证加载完成** |
31
87
  | `browser_eval` | `expression` | 主世界执行,返回可序列化值;页面异常以错误对象返回 |
32
- | `browser_screenshot` | `scale` `region` `persist` `out` | 显式 scale 0.1–4;省略时跟随共享 `view.scale`;`region` 为 canvas CSS px(`O=0` 时与文档坐标相同);PNG 经结构/尺寸校验;内联 base64 超过 24 MiB 时 `image=null` 并附 `image_base64_omitted` 警告(附件优先) |
33
- | `browser_input` | `events[]` | 原始鼠标/滚轮/键盘事件,单批 ≤ 200 条(超出 `INVALID_ARGUMENT`);`applied` 只是数组长度,不能证明生效 |
34
- | `browser_network` | `limit` | 轻量请求列表(默认 50、上限 300);深度分析用网络 P0 七工具 |
88
+ | `browser_screenshot` | `scale` `region` `includeAnnotations?:boolean=false` `persist` `out` | 显式 scale 0.1–4;省略时跟随共享 `view.scale`;`region` 为 canvas CSS px(`O=0` 时与文档坐标相同);`includeAnnotations:true` 按相同映射叠加 Host 已接受的六种面板标注,不含未提交草稿/工具栏,证据见 `capture.annotations`;默认截图及 T3/T4 原始源不含面板标注;PNG 经结构/尺寸校验;内联 base64 超过 24 MiB 时 `image=null` 并附 `image_base64_omitted` 警告(附件优先) |
89
+ | `browser_input` | `events[]` | 原始鼠标/滚轮/键盘事件,单批 ≤ 200 条(超出 `INVALID_ARGUMENT`);`applied` 只是数组长度,不能证明生效。**带按键状态机**:`keyDown` 记位、`keyUp` 清位,按住 Shift/Ctrl/Alt/Meta 时后续 key/mouse/wheel 自动带上对应位(页面 `e.shiftKey` 等为真),`synthesizeModifiers:false` 可显式关掉;`mouse` 事件给 `deltaX/deltaY` 时按相对位移移动一个虚拟光标(点积锁定/转视角类调用不需要知道绝对坐标,光标被 clamp 在视口内);返回 `{ok,applied,modifiers,held,cursor}`,`held` 是当前仍按住的键。导航/关闭会清空状态 |
90
+ | `browser_network` | `limit` | 轻量请求列表(默认 50、上限 300);**只读**:不取页面队列、不取也不校验单 owner 租约(owner 持租约时仍可读),因此信封里 `sessionId` 是采集会话、agent 会话见 `agentSessionId`;深度分析用网络 P0 七工具 |
35
91
  | `browser_computed_style` | `selector` `properties` | 首个匹配元素的计算样式;简写属性(如 `border-color`/`margin`/`font`)经页面 `getComputedStyle` 回退读取,未知属性不返回;不证明视觉结果 |
36
92
  | `browser_http_request` | `url` `method` `headers` `body` `timeoutMs` `maxBytes` … | 独立 HTTP(S) 客户端,不启动浏览器、不占页面租约;`ok:true` **不等于 2xx** |
37
93
  | `browser_viewport` | `action:"get"\|"set"` `mode` `x` `y` `canvasX` `canvasY` `width` `height` `scale` `expectedRevision` | `get` 是只读缓存诊断;`set` 是页面操作,走队列与租约 |
@@ -64,7 +120,7 @@
64
120
  | --- | --- |
65
121
  | `browser_vision_image` | 裁剪、缩放、预处理、统计、识别、标注并交付 PNG |
66
122
  | `browser_vision_find` | 颜色/连通域/边缘/模板确定性定位;无匹配是成功空列表 |
67
- | `browser_vision_compare` | 两图 YIQ 差异,输出标注差异图;两图必须同尺寸 |
123
+ | `browser_vision_compare` | 两图 YIQ 差异,输出标注差异图;两图必须同尺寸;几何映射不兼容时返回 `BLOCKED`/`mapping-incompatible`(显式 `bitmapOnly:true` 才降级为纯位图比较) |
68
124
  | `browser_vision_act` | 按截图坐标派发真实输入;六步校验不过则零派发 |
69
125
  | `browser_vision_baseline` | 受管基线:不可变 revision、HEAD CAS、墓碑删除 |
70
126
  | `browser_vision_check` | 回归与六类缺陷用例;整批先校验再执行 |
@@ -104,7 +160,7 @@
104
160
  | 错误码 | 含义 |
105
161
  | --- | --- |
106
162
  | `INVALID_ARGUMENT` | 参数不合法(含未知字段、互斥字段同时出现) |
107
- | `BUSY` | 当前工作区页面被其他 owner 占用 |
163
+ | `BUSY` | 该页队列正被其他会话的**写**操作占用(短暂忙;只读观察不占写租约,权限问题不会报 BUSY) |
108
164
  | `REF_STALE` | ref 所属快照或文档已失效,需要重新 snapshot |
109
165
  | `NOT_INTERACTABLE` | 目标不可见/被遮挡/持续运动,零派发 |
110
166
  | `TARGET_NOT_UNIQUE` | selector 命中多个元素 |
@@ -112,5 +168,7 @@
112
168
  | `VISION_*` | 视觉源、尺寸、基线或比较错误 |
113
169
  | `DYNAMIC_*` | 动态会话、预算、控制或关联错误 |
114
170
  | `NET_*` | 网络会话、事件、规则、证据或订阅错误 |
171
+ | `ANNOTATION_NOT_FOREGROUND` | 目标标签页不是人类此刻选中且可见的前台标签,写标注零写入 |
172
+ | `ANNOTATION_REVISION_CONFLICT` | `expectRevision` 与当前文档 `revision` 不一致,写标注零写入并返回 `currentRevision` |
115
173
 
116
174
  完整错误列表与字段以各功能文档为准;失败时 `details` 携带结构化证据,不要把错误文本当成结论。
@@ -0,0 +1,125 @@
1
+ # 新手图文教程:从打开调试视图到让 Agent 真正动手
2
+
3
+ 这篇教程只用**一个本地静态页**演示完整闭环:打开调试视图 → 加载页面 → 让 Agent 看见并操作 → 拿到可复核的证据。全程不需要写代码,也不需要配置模型。
4
+
5
+ > 截图来自一个**只装了本插件**的隔离 DSH 实例(独立 `DSH_HOME`、临时端口、一次性 Chromium),所以画面里没有其它插件的影响。
6
+
7
+ ## 0. 前置条件(一次性)
8
+
9
+ 1. 已安装 DSH,并且本机有 Chrome / Chromium(插件不自带浏览器)。
10
+ 2. 把插件装进要用的 profile,然后**重启一次 DSH**:
11
+
12
+ ```sh
13
+ dsh plugin --profile web add dsh-agent-team-ui-testing
14
+ ```
15
+
16
+ 3. 确认工具面在:随便开一个会话,问 Agent「你现在有哪些 browser_ 工具」,应能列出 37 个(T1 程序化、T2 结构化 GUI、T3 静态视觉、T4 动态行为、网络 P0、多标签页、标注)。
17
+
18
+ ## 1. 打开调试视图
19
+
20
+ 两个入口,效果相同:
21
+
22
+ - 左侧 **更多 → 调试视图**;
23
+ - 右侧边栏的插件卡片。
24
+
25
+ ![调试视图面板全貌:地址栏、标签栏与页面预览](images/tutorial-01-overview.png)
26
+
27
+ 从上到下依次是:
28
+
29
+ | 区域 | 用途 |
30
+ | --- | --- |
31
+ | 顶部一行 | 后退 / 前进 / 刷新、**地址栏**、最近访问、转到、标签页、标注、尺寸调整、选元素、更多 |
32
+ | 标签栏 | 一个标签一个页面;真人点击页面里的 `target=_blank` 会在这里新开一个 |
33
+ | 预览区 | 目标页的实时画面,鼠标键盘都可以直接操作 |
34
+ | 底部状态栏 | 空闲 / Agent 操作中、帧率、帧间隔 |
35
+
36
+ ## 2. 在地址栏打开要调试的页面
37
+
38
+ 把地址粘进地址栏按 <kbd>Enter</kbd>,或点它右边的 **转到** 按钮(等价于回车)。这里用 `http://127.0.0.1:8199/index.html` 这个演示页。
39
+
40
+ ![地址栏右侧的三个控件:最近访问、转到、标签页开关](images/tutorial-02-history-dropdown.png)
41
+
42
+ - **红色框 = 最近访问**:点开下拉,按栈展示最近 7 条访问过的地址(最近的在上,重复访问只置顶)。历史**按工作区隔离**,换个工作区就是另一份记录;点任意一条直接跳过去。
43
+ - **蓝色框 = 转到**:等同回车提交地址栏里的内容,校验与报错跟回车完全一致。
44
+ - **橙色框 = 下拉展开后的样子**:标题里的 `3/7` 表示已有 3 条、上限 7 条。
45
+
46
+ 页面加载后,预览区就是**真的**那个页面的实时画面:在这里点击、输入、滚轮滚动,页面收到的都是真实事件。
47
+
48
+ ## 3. 让 Agent 先「看」,再「动」
49
+
50
+ 新手最容易犯的错是让 Agent 直接点。正确顺序是**先观察、再定位、最后动作**,典型对话:
51
+
52
+ ```text
53
+ 你:打开 http://127.0.0.1:8199/index.html,看看页面上有哪些控件
54
+ Agent:browser_navigate { url } → browser_snapshot
55
+ (拿到 ref 列表:textbox "订单号" [ref=e2]、button "查询" [ref=e3] …)
56
+ 你:在订单号里填 A-1001,然后点查询
57
+ Agent:browser_fill_form { fields: [{ ref: "e2", type: "textbox", value: "A-1001" }] }
58
+ browser_click { ref: "e3" }
59
+ browser_node_state / browser_eval 回读结果
60
+ ```
61
+
62
+ 三条经验:
63
+
64
+ 1. **先 snapshot 拿 ref**:ref 形如 `e2`,每次导航或刷新都会失效,失效就重新 snapshot。
65
+ 2. **动作后必须回读**:`browser_click` 返回 `dispatched:true` 只说明事件发出去了,页面是否真的变了要自己再读一次。
66
+ 3. **点不动是结论,不是障碍**:元素被遮挡、持续运动、在 iframe 里,工具会明确报 `NOT_INTERACTABLE`,不要换个坐标硬点。
67
+
68
+ ## 4. 把元素直接发给 Agent:选元素
69
+
70
+ 不知道元素叫什么、又不想手写 selector 时,用工具栏的**选元素**:
71
+
72
+ ![选元素模式:悬浮在「查询」按钮上,浮层显示 button#submit](images/tutorial-03-pick-element.png)
73
+
74
+ 1. 点工具栏的准星图标进入选元素模式(页面零输入,滚轮仍可滚动);
75
+ 2. 鼠标划过预览,被命中的元素会出现蓝框与 `标签#id` 标签;
76
+ 3. 点一下,元素信息会作为 `@` 引用塞进聊天框,直接继续提问即可;
77
+ 4. <kbd>Esc</kbd> 退出。
78
+
79
+ ## 5. 画标注、截图给 Agent
80
+
81
+ 需要指位置时别用文字描述——直接画:
82
+
83
+ ![标注工具条:圆形、矩形、线段、箭头、画笔、文字、橡皮擦,以及填充/外框、颜色、粗细与截图](images/tutorial-04-annotate.png)
84
+
85
+ - 标注按**画布坐标**锚定,可以画在画面外的空白带上;
86
+ - 标注模式下页面不接收指针与键盘输入,<kbd>Esc</kbd> 退出;
87
+ - 工具条最后的**截图**按钮,会把「当前画面 + 已提交标注」合成一张 PNG **直接放进聊天框**,Agent 立刻能看到你圈的是哪块;
88
+ - Agent 也能反过来读/写这份标注文档(`browser_annotation`),但只有你正在调试视图里看着这个标签页时才允许写。
89
+
90
+ ## 6. 需要证据时
91
+
92
+ | 你想要 | 用哪个 |
93
+ | --- | --- |
94
+ | 一张可复核的截图 | `browser_screenshot { out: "output/capture/step1.png" }` |
95
+ | 断言颜色/位置/差异 | T3:`browser_vision_image` / `browser_vision_compare` / `browser_vision_check` |
96
+ | 接口为什么 404/500、请求发了几次 | 网络 P0:`browser_network_session start` → `browser_network_events` / `browser_network_detail` |
97
+ | 动画卡不卡、有没有闪烁 | T4:`browser_dynamic_session` → `browser_motion_observe` → `browser_motion_analyze` |
98
+ | 页面变了什么 | `browser_snapshot` 前后对比,`browser_state` 看 URL/标题/视口 |
99
+
100
+ ## 7. 常见问题
101
+
102
+ **点了页面没反应?**
103
+ 先看工具栏:**标注**、**选元素**、**尺寸调整**三者互斥,任一开启时页面不接收普通输入。<kbd>Esc</kbd> 或再点一次按钮退出。
104
+
105
+ **画面提示「画面同步中,暂时无法操作」?**
106
+ 视口刚改过尺寸,面板在等新帧对齐(最长约 1 秒),等下或点「重试同步」。这期间工具会拒绝派发,而不是点到错的位置。
107
+
108
+ **地址栏历史里没有我刚在别的窗口访问的地址?**
109
+ 历史按工作区隔离,不同工作区各有一份,这是设计行为。
110
+
111
+ **改了 `client.js` 没生效?**
112
+ 面板要刷新一次;改 `index.js`、工具 schema 或内置 Skill 则需要重启 DSH。
113
+
114
+ **面板里看不到原生弹窗(alert / 文件选择器)?**
115
+ 它们由浏览器 UI 层绘制,不在页面画面里,面板无法显示也无法操作。
116
+
117
+ **Agent 说页面在「别的标签页」?**
118
+ 每个 Agent 会话有自己的选中标签,互不抢占;用 `browser_tabs` 查看和切换,真人切标签不会改变 Agent 的选择。
119
+
120
+ ## 下一步
121
+
122
+ - [快速开始](getting-started.md):安装、设置页、环境选择
123
+ - [工具参考](tools.md):37 个工具的参数与错误分类
124
+ - [能力分层与限制](capability-tiers.md):什么时候该用 T1 / T2 / T3 / T4
125
+ - [选元素加入聊天框](element-picker.md)、[视口与标注](viewport-control.md)、[网络调试](network-p0.md)
@@ -14,6 +14,14 @@
14
14
  node tests/acceptance/run.mjs # 主验收:工具面、能力报告、渲染与 lossless 投影
15
15
  node tests/acceptance/viewport.mjs # 共享视口与画布
16
16
  node tests/acceptance/capture-p0.mjs # 截图采集
17
+ node tests/acceptance/annotation-capture.mjs # includeAnnotations:六形状、region/scale 映射与标注文档证据
18
+ node tests/acceptance/game-target.mjs # 人工靶机 tests/manual-target/game-target.html 的自评在真实浏览器里跑到 8/8
19
+ node tests/acceptance/input-state.mjs # browser_input 状态机:多键按住/修饰键合成、相对鼠标、光标探测
20
+ node --test tests/contracts/panel-cursor.test.mjs # 预览光标:尾沿查询、换帧重试、迟到响应与生命周期失效
21
+ node --test tests/contracts/panel-address-bar.test.mjs # 地址栏历史:栈语义、7 条上限、持久化与接线
22
+ node tests/acceptance/panel-address-gui.mjs # 真实面板:历史下拉顺序/去重/持久化、Esc 关闭与「转到」按钮
23
+ node tests/acceptance/gui-annot-shot.mjs # 面板「截图」:/screenshot 的 includeAnnotations 合成、旧回复形状与拒绝
24
+ node tests/acceptance/annotation-agent.mjs # Agent 标注读写:四类动作、样式映射、前台门禁、限额与 revision CAS
17
25
  node tests/acceptance/t2-structured.mjs # 结构化 GUI
18
26
  node tests/acceptance/t3-vision.mjs # 静态视觉
19
27
  node tests/acceptance/t4-dynamic.mjs # 动态行为 44 条门槛
@@ -23,6 +31,7 @@ node tests/acceptance/network-p0.mjs # 网络 P0
23
31
  node tests/acceptance/network-chain.mjs # 电商链路场景
24
32
  node tests/acceptance/devtools.mjs # DevTools 独立窗口:Host 投影、真实连接、共存与端口重试
25
33
  node tests/acceptance/pick-element-host.mjs # 选元素 Host:坐标空间/命中/详情/可见区边界/只读语义
34
+ node tests/acceptance/context-menu-host.mjs # 页面右键:真实取消状态/iframe/默认菜单动作/过期令牌
26
35
  node tests/acceptance/workspace-isolation.test.mjs
27
36
  node tests/acceptance/p0e-multitab.mjs # P0-E Host:per-tab BUSY、browser_tabs、容量、T2 ref
28
37
  node tests/acceptance/p0e-multitab-gui.mjs # P0-E GUI:Agent A/B、active 状态、+、BUSY 提示
@@ -31,6 +40,19 @@ node --test tests/dynamic-t4.test.mjs # T4 纯核心单测
31
40
  node --test tests/network-p0.test.mjs # 网络纯核心单测
32
41
  node tests/acceptance/check-skill.mjs # Skill 结构、引用、工具名与清单一致性
33
42
  node tests/acceptance/check-skill-registry.mjs # 真实 SkillRegistry 注册与加载
43
+ node --test tests/contracts/*.test.mjs # 跨环境契约:core/config/P0/fake/Web adapter
44
+ node --test tests/contracts/web-provider-config.test.mjs # provider 配置形状、选择链、旧配置兼容、revision/CAS
45
+ node --test tests/contracts/web-provider-runtime.test.mjs # provider 门禁、hosts[provider]、页面预算、providerResolved
46
+ node --test tests/contracts/web-provider-edge.test.mjs # Edge 候选/覆盖变量/身份判定(注入 platform 与 env,不启动进程)
47
+ node --test tests/contracts/isolation.test.mjs # 真实 ToolsService/SkillRegistry 的 schema 与 Skill 隔离
48
+ node tests/acceptance/cross-environment.mjs # 配置 CAS/持久化/六轴就绪/GUI 旁路/不启动浏览器
49
+ node tests/acceptance/foundation-isolation.mjs # A03/A04/A05/A08/A09/A10/A11/A12/A13/A14 逐项阻断复核(离线,不发模型请求)
50
+ node tests/acceptance/settings-config-gui.mjs # 隔离 DSH GUI:设置页读取/写入/revision/六轴/两层开关
51
+ node tests/acceptance/audit-isolated-agent.mjs --group g1 --prompt <prompt.txt> \
52
+ --expect-tools browser_navigate,browser_snapshot # 隔离 DSH 实例里跑真实 Agent 轮次并判定 browser_* 结果
53
+ node tests/acceptance/audit-payload-capture.mjs # 本地录制代理抓真实模型请求体(工具声明/Skill 目录)
54
+ node tests/acceptance/audit-a05-annotations.mjs # 标注写面(面板 scoped 路由)与目标绑定探针
55
+ node tests/acceptance/audit-a11-scope.mjs # 会话级环境选择:控制面/工具快照/声明层一致性
34
56
  ```
35
57
 
36
58
  **串行执行**:这些套件各自启动 Chromium 与靶机,并行会让端口、profile 与 CPU 互相干扰,计数不可信。
@@ -72,9 +94,11 @@ node tests/acceptance/verify-3081.mjs # 隔离宿主 HTTP 集成
72
94
 
73
95
  `devtools.mjs` 在 fake ctx 上驱动真实无头 Chromium:验证 `ui.devtools` 的 scoped-only 投影、固定 loopback 端口与精确 allow-origin、DevTools 前端真实连接(shadow DOM 中出现目标标记)、screencast 与 T2 共存,以及端口冲突/端点身份不符时的有界重试。`devtools-gui.mjs` 自己启动私有隔离 DSH 与无头 Chrome,验证真实「更多 → 打开调试工具」点击、`opener=null`/无 Referer、弹窗被拦截时的本地提示、标注模式禁用与预览共存。它只证明弹窗 target 与内容,不证明真实桌面窗口外观;GUI 内 T2 与非 loopback GUI 按 `NOT_RUN` 原样报告,不要用推断补成通过。
74
96
 
75
- `pick-element-host.mjs` 在 fake-Cordis harness 上驱动真实无头 Chromium 与仓库 fixture:先实证 `DOM.getNodeForLocation` 是文档坐标(且只命中当前可见区)、`DOM.getBoxModel` 是根帧视口坐标,再断言固定/滚动/空白/越界命中、detail 的 selector/selectorQuality、outerHTML/text 的显式截断与长度字段、role/name、shadow(open/closed)、跨帧 `reason:"frame"`、主帧未知 fail-closed、`scroll`/`loaderId` 回显与导航一致性,以及只读语义(不进 `run` 队列、不 ensure、不占租约、不写 `lastError`、不加页)。要求项计 140 条,`NOT_RUN` 仅保留 iframe 内部元素与 SSR 文档两项。不要用「返回了元素」代替「与真实 DOM 读数一致」。
97
+ `pick-element-host.mjs` 在 fake-Cordis harness 上驱动真实无头 Chromium 与仓库 fixture:先实证 `DOM.getNodeForLocation` 是文档坐标(且只命中当前可见区)、`DOM.getBoxModel` 是根帧视口坐标,再断言固定/滚动/空白/越界命中、detail 的 selector/selectorQuality、outerHTML/text 的显式截断与长度字段、role/name、shadow(open/closed)、嵌套 same-process frame 命中(顶层视口 rect、该 frame 的 `loaderId`、select 投影与写回、伪造 loaderId 的 `stale-target`)、主帧未知 fail-closed、`scroll`/`loaderId` 回显与导航一致性,以及只读语义(不进 `run` 队列、不 ensure、不占租约、不写 `lastError`、不加页)。要求项计 145 条,`NOT_RUN` 只保留 SSR 文档一项(仓库内没有该 fixture)。不要用「返回了元素」代替「与真实 DOM 读数一致」。
76
98
 
77
- `pick-element-gui.mjs` 自启私有隔离 DSH(临时 `DSH_HOME`、随机端口,绝不用 3080/3081)并另开无头 Chrome:从右侧边栏 guide 的 `debug-view` 打开面板、地址栏导航到 `/pick-element.html`,断言入口三态与互斥(标注/尺寸调整双向)、悬浮框的 `data-utb-pick-*` 与按面板 `view`/图片几何计算的屏幕矩形误差 ≤2px、`detail:false` 的 hover 不带 selector、点击后 `[data-composer-input]` 的 `@` 元素引用 chip(紧凑标签 + 模型文本由 codec.serialize 提供,经测试钩子核对完整块)、强制降级时的完整文本块、发送后聊天记录的文本规则(块→紧凑 chip 的匹配与注册,含长 selector/截断样本)、成功提示与模式退出、聊天框焦点、Esc/指针离开、滚轮滚动后按新画布原点重取、以及退出后真实点击使 fixture 计数器 1→页面输入恢复;悬停/选元素期间页面计数保持 0。`NOT_RUN`(画布平移 O≠0、主面板 `recent`、DPR≠1/非 loopback、iframe/closed shadow 选取)必须原样报告。
99
+ `pick-element-gui.mjs` 默认自启私有隔离 DSH(临时 `DSH_HOME`、随机端口,绝不用 3080/3081)并另开无头 Chrome:从右侧边栏 guide 的 `debug-view` 打开面板、地址栏导航到 `/pick-element.html`,断言入口三态与互斥(标注/尺寸调整双向)、悬浮框的 `data-utb-pick-*` 与按面板 `view`/图片几何计算的屏幕矩形误差 ≤2px、`detail:false` 的 hover 不带 selector、点击后 `[data-composer-input]` 的 `@` 元素引用 chip(紧凑标签 + 模型文本由 codec.serialize 提供,经测试钩子核对完整块)、强制降级时的完整文本块、发送后聊天记录的文本规则(块→紧凑 chip 的匹配与注册,含长 selector/截断样本)、成功提示与模式退出、聊天框焦点、Esc/指针离开、滚轮滚动后按新画布原点重取、以及退出后真实点击使 fixture 计数器 1→页面输入恢复;悬停/选元素期间页面计数保持 0。`NOT_RUN`(画布平移 O≠0、主面板 `recent`、DPR≠1/非 loopback、closed shadow 选取)必须原样报告;iframe 内 `<select>` 的浮层与写回由 `--select-only` 的 S16–S17a 真实覆盖,页面右键菜单由同一次运行覆盖(可信 `contextmenu`、页面自绘菜单可见可点、默认区显示面板菜单、重新加载/复制/Esc/离图释放、预览内不出现宿主菜单、预览外仍冒泡),契约见[页面右键菜单](panel-context-menu.md)。
100
+
101
+ 下拉浮层可单独验收:`node tests/acceptance/pick-element-gui.mjs --select-only`。默认仍使用隔离 Host;显式设置 `DSH_SELECT_GUI_URL` 可连接已有 Host,`DSH_SELECT_GUI_LOG` 指向其启动日志,`DSH_SELECT_GUI_SESSION` 选择会话标题,`DSH_SELECT_GUI_RETURN_URL` 可在验收后恢复页面地址。已有 Host 模式会导航选定会话的内嵌页面,不重启或关闭该 Host;认证仅保留在内存。检查覆盖真实 `/element` 响应、面板选项、重复 value、禁用项/组、键盘兜底、焦点转移、200 项上限、导航后旧目标拒绝,以及 Ctrl+A 和字符输入;同一次运行还会用 `/panel-context.html` 验证页面自绘菜单与默认面板菜单、实际刷新/复制、Esc/离图释放和预览内外的宿主菜单隔离;加 `--context-only` 可单独运行右键 GUI 分支。浮层来自面板 DOM,不能用目标页面截图代替面板截图。
78
102
 
79
103
  `popup-tabs.mjs` 额外覆盖 Web 视图的 无头/有头 切换(默认无头、切到有头后 Host `ui.display.mode=headed`、离屏窗口已隐藏 `display.hidden=true`、有头能打开页面、持久 profile 存在、切回无头可继续打开页面)。有头模式默认离屏并尝试从窗口管理器隐藏,但测试仍可能影响桌面环境,默认记 `NOT_RUN`;设 `DSH_UI_TESTING_HEADED_TEST=1` 才实际运行。
80
104
 
@@ -1,6 +1,6 @@
1
1
  # 视口、画布与标注
2
2
 
3
- 本页描述三件共享同一份状态的能力:**共享视口**(尺寸、滚动、显示比例)、**混合画布**(带符号的原点与空白区域)和**客户端标注**(人画、Agent 读)。
3
+ 本页描述三件共享同一份状态的能力:**共享视口**(尺寸、滚动、显示比例)、**混合画布**(带符号的原点与空白区域)和**客户端标注**(人和 Agent 读写同一份文档)。
4
4
 
5
5
  ## 共享视口
6
6
 
@@ -32,19 +32,28 @@ view = { version, mode, x, y, canvasX, canvasY, width, height, scale, source, re
32
32
 
33
33
  - `canvasX/canvasY` 是视口左上角在文档画布中的坐标,可为负或超出内容边界。
34
34
  - 当 `canvasX ≠ x` 时存在偏移 `O = C − S`;偏移区域在面板中显示为深色棋盘格,在截图与 T3 中是**纯白 `#ffffff`**。
35
- - `O = 0` 时截图与 T3 同旧行为字节/像素一致;`O ≠ 0` 时先截真实页面帧再白底合成,并披露 `capture.canvas.composite=true`(可能重编码)。
35
+ - 未开启标注叠加时,`O = 0` 的截图与 T3 同旧行为字节/像素一致;`O ≠ 0` 时先截真实页面帧再白底合成,并披露 `capture.canvas.composite=true`(可能重编码)。显式开启 `includeAnnotations:true` 时还会绘制已接受的标注文档,不能据此承诺原页面 PNG 字节不变。
36
36
  - 面板支持拖拽平移与以屏幕光标为锚点的滚轮缩放;拖拽写画布坐标,页面本身不接收这些输入。
37
37
  - `canvasX/canvasY` 与 `x/y` 不能出现在同一个 patch(`INVALID_ARGUMENT`)。
38
38
  - 空白区域不存在于 DOM 中;`O ≠ 0` 时坐标输入 fail-closed(宁可短暂「点不动」也不误派发)。
39
39
 
40
40
  ## 标注
41
41
 
42
- - 工具:圆形、矩形、线段、箭头、画笔、文字、橡皮擦;另有填充/外框、颜色、粗细与清除画布。
42
+ - 工具:圆形、矩形、线段、箭头、画笔、文字、橡皮擦;另有填充/外框、颜色、粗细、**截图**与清除画布。
43
+ - **截图按钮**(标注栏、紧接「粗细」之后):把**当前页面连同已接受的标注**合成一张 PNG,直接作为附件放进本会话的聊天框(`createDrafts` + `addAttachments`,字节随消息发送,不落盘、不占页面队列之外的东西)。它走 Host 的 `POST /screenshot {includeAnnotations:true}`,与 `browser_screenshot {includeAnnotations:true}` 是**同一条离屏合成路径**,所以像素与证据一致;成功时不弹任何提示(附件缩略图本身就是结果),只有失败才在按钮旁给出具体原因。截图前若还有待发布的笔画会先 flush(≤260ms),避免少一笔;聊天框处于发送中(busy)时按钮报「聊天框正忙」并**立即释放**已注册的附件,不留孤儿。
43
44
  - 锚定在**画布坐标**中,可画在空白区域;标注模式下页面不接收任何指针或键盘输入。
44
- - 标注不进入页面 DOM、不进入截图与 T3 分析,是一份独立文档。
45
- - Agent 读取:`browser_state.annotations`(或认证 `/state` 的 `ui.annotations`),返回图形列表与 `revision`。
45
+ - 标注是独立文档,不进入页面 DOM;默认截图、T3 原始源图与 T4 帧源不含面板标注。`browser_screenshot {includeAnnotations:true}` 可叠加当前目标已提交且经 Host 接受的六种形状(`circle/rect/line/arrow/brush/text`),渲染语义与面板一致。
46
+ - Agent 读取:`browser_state.annotations`(或认证 `/state` 的 `ui.annotations`),返回图形列表与 `revision`。用户提到标注、画、圈、框、箭头或笔迹等指代面板标注时,先读该文档,再用 `browser_screenshot {includeAnnotations:true}` 查看像素;手写内容无法可靠辨认时如实说明,不猜字。
47
+ - **Agent 写入**:`browser_annotation` 读写**同一份**标注文档(每个 Runtime 一份),四类动作 `read` / `draw` / `erase` / `clear`。用途是「你圈一下 / 标给我看 / 我指的这个在这儿」——把结论画到人眼前的调试视图上,或给人类已有的标注补充意图(文字标注写问题、问号等提示)。
48
+ - `draw` 的 `shapes` **追加**到现有文档(1–64 个/次),不是替换;`erase` 用 `ids` 和/或 `point{x,y,radius?}`(默认半径 8 画布 px,命中规则与面板橡皮擦一致:形状到点的距离 ≤ `max(strokeWidth/2,6)` 与 `radius` 取大者);`clear` 清空画布。
49
+ - 形状与面板工具的对应关系:`circle` 圆形、`rect` 矩形、`line` 线段、`arrow` 箭头、`brush` 画笔(铅笔)、`text` 文字。「颜色」→ `color`(`#rrggbb`,默认 `#ff3b30`);「粗细」→ `strokeWidth`(整数 1–20,默认 3);「外框/填充」→ `style:"stroke"|"fill"`(也接受 `fill`/`stroke` 布尔,二者不可同时给),只对 `circle`/`rect` 有效,`text` 固定 `fill`、`line`/`arrow`/`brush` 固定 `stroke`。`id` 可省略(Host 从现有最大 id 继续分配 `a<N>`),`arrow` 的 head 与 `text.fontSize`(固定 20)由 Host 决定。
50
+ - **整份替换语义**:文档按整份替换发布。人类在面板里下一次编辑会整体提交自己的形状列表(其中已包含 Agent 画的形状,因为面板会采纳 Host 文档),人类「清除画布」会连 Agent 的形状一起清掉。写动作可用 `expectRevision` 做乐观并发,不匹配报 `ANNOTATION_REVISION_CONFLICT`(零写入,返回 `currentRevision`)。
51
+ - **前台硬约束**:`draw`/`erase`/`clear` 只在目标标签页正被人类在调试视图里选中且可见时才允许,否则零写入并报 `ANNOTATION_NOT_FOREGROUND`;依据是面板在认证请求上携带的前台标签心跳(约 1.5s 新鲜窗口),人切走标签、关掉面板或把 DSH 标签页切到后台都会让它过期。先看 `browser_state.annotations.foreground = {active, ageMs, maxAgeMs}` 再决定画不画。`read` 不受此限;页面未运行(未启动)时写标注报 `INVALID_ARGUMENT`(reason `annotation-runtime-not-running`)。
52
+ - **软限额与裁剪**:与面板写面相同(`maxShapes` 128、单笔画笔 512 点、文档总点数 8192、文字 200 字符、文档 65536 字节),Host 确定性裁剪并如实回 `truncated` / `truncation`。
53
+ - **不碰页面**:不启动浏览器、不进页面串行队列、不占 single-owner lease、不写页面 `lastError`、不改页面 DOM/CSS;只写 Host 侧文档,面板下一次 `/state` 轮询就会画出来。`includeAnnotations:true` 的截图会把 Agent 画的与人类画的形状一起叠加。
54
+ - 带标注截图与页面共用 `region`/`scale` 映射、裁剪和缩放;不包含未提交草稿、工具栏、选中控件或其它面板 UI,详见[截图与图片交付](capture-p0.md)。
46
55
  - 按工作区隔离;导航换文档时清空;跨分页与会话保留。
47
- - 写面唯一:面板通过认证的 `POST /plugins/ui-testing/annotations` 提交,带 `clientRevision` 幂等与 `409 annotation-context-stale`;Host 侧有软限额(图形数、单笔点数、总点数、文字长度、文档字节数)并做确定性裁剪。
56
+ - **写面有两处,写的是同一份文档**:面板通过认证的 `POST /plugins/ui-testing/annotations` 提交(带 `clientRevision` 幂等与 `409 annotation-context-stale`),Agent 通过 `browser_annotation` 提交;两者共用 Host 侧软限额(图形数、单笔点数、总点数、文字长度、文档字节数)与确定性裁剪。
48
57
 
49
58
  ## 截图比例
50
59
 
@@ -55,6 +64,8 @@ view = { version, mode, x, y, canvasX, canvasY, width, height, scale, source, re
55
64
 
56
65
  ## 面板交互
57
66
 
67
+ - **键盘按住语义**:预览拿到焦点后,一次 `keydown` 只向页面发一次 `keyDown`、`keyup` 单独转发,所以按住 <kbd>W</kbd>+<kbd>A</kbd> 这种连续输入在页面里真的保持按住(旧实现按下即松开,按住类交互全部失效);焦点离开预览时自动把仍按住的键补一次 `keyUp`,不会把按键卡在页面里。
68
+ - **光标形状跟随页面**:预览里移动指针时,面板按 ~120ms 节流询问 `POST /element {cursorOnly:true}` 拿到光标下元素的 computed `cursor`(`pointer`→手型、`text`→文本、`grab`/`grabbing` 等),写回预览层;只接受 CSS 关键字白名单,`cursor:url(...)` 之类永不落到样式上。指针离开、落在画布空白、或进入选元素/标注/拖拽模式时回到箭头/十字。节流窗口内保留最后指针位置并补发查询,快速移到另一磁贴后停住也会更新;换帧尚未解码或同步屏障暂时关闭时,最多等待约 1 秒再回退。离开预览、进入空白、切换模式/标签/文档或画布位置时,会取消待查询并失效旧响应,避免旧位置的光标覆盖复位。
58
69
  - 尺寸预设(宽屏 / 横屏 / 平板 / 手机)是分页选择器,选中项显示绿色小球;点击预设把 X/Y 归零并保持当前显示模式。
59
70
  - `拖拽 | 适应窗口 | 默认` 三选一互斥;「适应窗口」只是本地显示偏好,不发布比例。
60
71
  - 数值框支持滚轮:X/Y/W/H 步长 10(Shift 100、Alt 1),比例步长 5(Shift 10、Alt 1)。
@@ -65,6 +76,8 @@ view = { version, mode, x, y, canvasX, canvasY, width, height, scale, source, re
65
76
  - 页面滚动时整帧按保留偏移 `O = C − S`(`T = −O × 显示比例`)定位,不随帧采集滚动 `F` 与状态 `C` 的异步到达产生伪平移;`O = 0` 时保持旧像素不动,`O ≠ 0` 时保留合法的空白带。
66
77
  - 「更多 → 打开调试工具」打开的是 Chrome 自带 DevTools 的独立窗口,直接连接当前页面的 CDP;它不是插件工具,不受面板标注模式、串行队列或 single-owner lease 约束。device toolbar、DPR/网络模拟、断点、Console 求值等会改变页面,自动化取证前应恢复设置并重新采集。
67
78
 
79
+ 页面内容右键优先交给页面;未取消默认行为时,面板显示后退、前进、重新加载与复制相关项。页面菜单、键盘操作、iframe、失效与截图边界详见[页面右键菜单](panel-context-menu.md)。
80
+
68
81
  ## 内部标签页
69
82
 
70
83
  - 面板在**地址栏下方**提供内部标签栏;一个标签对应一个浏览器页面,由「标注」左侧开关控制显隐,单标签时也占一行(默认显示)。它不是新的 DSH 侧边栏分页。
@@ -72,10 +85,10 @@ view = { version, mode, x, y, canvasX, canvasY, width, height, scale, source, re
72
85
  - 标签栏可与标注栏、尺寸栏等横向栏上下叠加;「标注」按钮左侧的标签页开关控制显示/隐藏,单标签下同样生效,隐藏只收起栏,页面与标签状态保留。
73
86
  - 关闭标签会回收该页面的 Runtime;关闭当前标签后回到原页面或剩下的标签。每个工作区 primary + 最多 3 个 child(共 4 个),并计入全局 8 页上限;标签栏最右 `+` 新建空白标签,工具栏不常驻 `+`(标签栏隐藏时先打开标签栏再新建)。
74
87
  - 自动弹窗、没有真人输入、目标地址被导航策略拒绝或超容量时仍会阻断,并在面板给出提示;不会把这些 target 交给 Agent 工具。
75
- - 所有页面共享同一个浏览器 profile(Cookie、localStorage、cache);这是浏览器语义,不是登录态隔离。Agent 的 `browser_*` 工具默认操作当前会话 selectedTab(无选择 primary),不会因为 GUI 切换标签而改目标;`browser_tabs` 提供 list/current/select/open/close。
88
+ - 所有页面共享同一个浏览器 profile(Cookie、localStorage、cache);这是浏览器语义,不是登录态隔离。Agent 的 `browser_*` 工具默认操作当前会话的选择(写用 workSelection、读优先 observationSelection,均无设置时 primary),不会因为 GUI 切换标签而改目标;`browser_tabs` 提供 list/current/select/open/close/wait/acquire/release/transfer,跨 root 的目标一律 not-found 语义。
76
89
 
77
90
  ## 限制
78
91
 
79
92
  - 实时预览是 1× screencast;宽高变更后的输入屏障只是 best-effort(≤250 ms),画布偏移使用 fail-closed 屏障。
80
93
  - 画布是视图层合成,不是页面级无限画布;`fixed`/`sticky` 元素会随整帧平移。
81
- - 截图与 T3 不包含标注层;标注只通过 `browser_state.annotations` 读取。
94
+ - 面板标注栏的「截图」按钮与 `browser_screenshot` 的 `includeAnnotations:true` 是同一份合成结果;默认截图、T3 原始源图与 T4 帧源不包含面板标注,带标注截图须显式传 `includeAnnotations:true`,并结合 `browser_state.annotations` 核对文档身份与 revision。标注来源 `source.trusted=false`,不能替代页面事实或独立验证。