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
@@ -0,0 +1,415 @@
1
+ /**
2
+ * Web P0Provider: platform side of the neutral P0 families A-E.
3
+ *
4
+ * The provider owns no protocol. It validates arguments, calls the injected `p0Dispatch`, and
5
+ * normalizes whatever the platform returns through the neutral factories in `core/p0.js`. That
6
+ * keeps CDP/DOM/screencast details out of `core/` while giving T1-T4 one stable data model.
7
+ *
8
+ * `p0Dispatch` may be:
9
+ * - a function: `p0Dispatch(family, method, { args, session, snapshot, signal })` (canonical), or
10
+ * - a per-family map `{ A: { screenshot }, B: { events } }` / `{ A: fn }` (test seam).
11
+ * A family the dispatch cannot serve is reported `unsupported`; it is never faked.
12
+ */
13
+
14
+ import {
15
+ AdapterError,
16
+ CONTRACT_VERSION,
17
+ ERROR_CODES,
18
+ createEvidenceEnvelope,
19
+ } from '../../core/index.js';
20
+ import {
21
+ P0_AVAILABILITY,
22
+ P0_EVIDENCE_FIELDS,
23
+ P0_FAMILIES,
24
+ P0_SUPPORT,
25
+ createAnnotationDoc,
26
+ createDisplaySpace,
27
+ createFrame,
28
+ createLease,
29
+ createNetworkEvent,
30
+ createTargetIdentity,
31
+ } from '../../core/p0.js';
32
+
33
+ /** Method inventory per P0 family; mirrors M0 §3 and the shipped provider surface. */
34
+ export const WEB_P0_METHODS = Object.freeze({
35
+ // `capture`/`product` are the page-capture entry points the tool layer uses (scale/region and
36
+ // the composited PNG product); they stay platform-shaped and skip neutral normalization.
37
+ A: Object.freeze(['screenshot', 'capture', 'product', 'validateImage', 'readFrame']),
38
+ B: Object.freeze(['start', 'stop', 'status', 'reset', 'events', 'detail', 'control', 'trace', 'evidence', 'subscribe']),
39
+ C: Object.freeze(['read', 'transform', 'set']),
40
+ D: Object.freeze(['annotationRead', 'annotationWrite']),
41
+ // P0-E owns target model projection and leases only; bind/close belong to the TargetAdapter.
42
+ E: Object.freeze(['list', 'leaseAcquire', 'leaseRelease', 'leaseStatus']),
43
+ });
44
+
45
+ /** Family -> gating capability id (web catalog). */
46
+ const FAMILY_CAPABILITY = Object.freeze({
47
+ A: 'web.p0.capture',
48
+ B: 'web.p0.network',
49
+ C: 'web.p0.display',
50
+ D: 'web.p0.canvas',
51
+ E: 'web.p0.targets',
52
+ });
53
+
54
+ /** Capability ids owned by this provider; the adapter catalog is the authority. */
55
+ export const WEB_P0_CAPABILITIES = Object.freeze(Object.values(FAMILY_CAPABILITY));
56
+
57
+ const KNOWN_ERROR_CODES = new Set(Object.values(ERROR_CODES));
58
+
59
+ const FAMILY_KEYS = Object.freeze(Object.values(P0_FAMILIES));
60
+
61
+ function isPlainObject(value) {
62
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
63
+ }
64
+
65
+ function nullableString(value) {
66
+ return typeof value === 'string' && value.length > 0 ? value : null;
67
+ }
68
+
69
+ function capabilityListOf(snapshot) {
70
+ const list = snapshot?.capabilities;
71
+ return Array.isArray(list) ? list.filter((id) => typeof id === 'string') : null;
72
+ }
73
+
74
+ function configRevisionOf(snapshot) {
75
+ const revision = snapshot?.revision ?? snapshot?.configRevision;
76
+ return Number.isInteger(revision) ? revision : null;
77
+ }
78
+
79
+ function toAdapterError(error, { operation }) {
80
+ if (error instanceof AdapterError) return error;
81
+ const code = typeof error?.code === 'string' && KNOWN_ERROR_CODES.has(error.code)
82
+ ? error.code
83
+ : ERROR_CODES.INTERNAL;
84
+ const message = error instanceof Error && error.message
85
+ ? error.message
86
+ : typeof error === 'string' && error.length > 0
87
+ ? error
88
+ : 'web p0 provider operation failed';
89
+ const details = { operation };
90
+ if (isPlainObject(error?.details)) Object.assign(details, error.details);
91
+ if (error instanceof Error) details.causeName = error.name;
92
+ return new AdapterError(code, message, {
93
+ details,
94
+ cause: error instanceof Error ? error : undefined,
95
+ });
96
+ }
97
+
98
+ function requireStringArg(args, field, operation) {
99
+ if (typeof args[field] !== 'string' || args[field].length === 0) {
100
+ throw new AdapterError(ERROR_CODES.INVALID_ARGUMENT, `${operation} requires '${field}'`, {
101
+ details: { operation, field },
102
+ });
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Argument checks stay deliberately shallow: the provider guarantees the neutral shape, not the
108
+ * platform payload. Anything the platform needs to reject must surface as a platform error.
109
+ */
110
+ function validateArgs(family, method, args) {
111
+ const operation = `${family}.${method}`;
112
+ if (!isPlainObject(args)) {
113
+ throw new AdapterError(ERROR_CODES.INVALID_ARGUMENT, `${operation} args must be a plain object`, {
114
+ details: { operation, field: 'args', type: Array.isArray(args) ? 'array' : typeof args },
115
+ });
116
+ }
117
+ if (operation === 'B.detail' && args.requestId === undefined && args.seq === undefined) {
118
+ throw new AdapterError(ERROR_CODES.INVALID_ARGUMENT, 'B.detail requires requestId or seq', {
119
+ details: { operation, fields: ['requestId', 'seq'] },
120
+ });
121
+ }
122
+ if (operation === 'E.leaseAcquire') {
123
+ requireStringArg(args, 'targetId', operation);
124
+ }
125
+ if (operation === 'E.leaseRelease' || operation === 'E.leaseStatus') {
126
+ requireStringArg(args, 'leaseId', operation);
127
+ }
128
+ }
129
+
130
+ function listFrom(raw, key) {
131
+ if (Array.isArray(raw)) return raw;
132
+ if (isPlainObject(raw) && Array.isArray(raw[key])) return raw[key];
133
+ return [];
134
+ }
135
+
136
+ /** Normalize a platform result through the neutral factories; never invent missing values. */
137
+ function normalizeP0Value(family, method, raw) {
138
+ const operation = `${family}.${method}`;
139
+ switch (operation) {
140
+ case 'A.screenshot':
141
+ case 'A.readFrame':
142
+ return createFrame(isPlainObject(raw) ? raw : {});
143
+ case 'A.capture':
144
+ case 'A.product':
145
+ // Platform-shaped payloads: the tool contract owns their fields, so the provider must not
146
+ // re-shape them into a neutral frame.
147
+ return raw ?? null;
148
+ case 'A.validateImage':
149
+ if (isPlainObject(raw)) return raw;
150
+ throw new AdapterError(ERROR_CODES.INTERNAL, 'A.validateImage returned a non-object result', {
151
+ details: { operation },
152
+ });
153
+ case 'B.events': {
154
+ const events = listFrom(raw, 'events').map((event) => createNetworkEvent(isPlainObject(event) ? event : {}));
155
+ return isPlainObject(raw) && Array.isArray(raw.events) ? { ...raw, events } : events;
156
+ }
157
+ case 'B.detail':
158
+ return createNetworkEvent(isPlainObject(raw) ? raw : {});
159
+ case 'B.start':
160
+ case 'B.stop':
161
+ case 'B.status':
162
+ case 'B.reset':
163
+ case 'B.control':
164
+ case 'B.trace':
165
+ case 'B.evidence':
166
+ case 'B.subscribe':
167
+ case 'C.set':
168
+ return raw ?? null;
169
+ case 'C.read':
170
+ case 'C.transform':
171
+ return createDisplaySpace(isPlainObject(raw) ? raw : {});
172
+ case 'D.annotationRead':
173
+ case 'D.annotationWrite':
174
+ return createAnnotationDoc(isPlainObject(raw) ? raw : {});
175
+ case 'E.list': {
176
+ const targets = listFrom(raw, 'targets').map((target) => createTargetIdentity(isPlainObject(target) ? target : {}));
177
+ return isPlainObject(raw) && Array.isArray(raw.targets) ? { ...raw, targets } : targets;
178
+ }
179
+ case 'E.leaseAcquire':
180
+ return createLease(isPlainObject(raw) ? raw : {});
181
+ case 'E.leaseRelease':
182
+ return raw ?? null;
183
+ case 'E.leaseStatus': {
184
+ if (!isPlainObject(raw)) return raw ?? null;
185
+ return typeof raw.leaseId === 'string' || typeof raw.targetId === 'string'
186
+ ? createLease(raw)
187
+ : raw;
188
+ }
189
+ default:
190
+ return raw ?? null;
191
+ }
192
+ }
193
+
194
+ /**
195
+ * Create the web P0Provider.
196
+ *
197
+ * @param {object} [options]
198
+ * @param {Function|object} [options.p0Dispatch] Injected platform dispatch. Missing -> all five
199
+ * families report `support: 'unsupported'`.
200
+ * @param {object} [options.log]
201
+ * @param {() => number} [options.now]
202
+ */
203
+ export function createWebP0Provider({ p0Dispatch, log, now = Date.now } = {}) {
204
+ const manifest = {
205
+ contractVersion: CONTRACT_VERSION,
206
+ id: 'web.p0',
207
+ class: 'p0',
208
+ environment: 'web',
209
+ families: [...FAMILY_KEYS],
210
+ capabilities: [...WEB_P0_CAPABILITIES],
211
+ };
212
+
213
+ let disposed = false;
214
+
215
+ function assertNotDisposed() {
216
+ if (disposed) {
217
+ throw new AdapterError(ERROR_CODES.ADAPTER_DISABLED, 'web p0 provider is disposed', {
218
+ details: { providerId: manifest.id },
219
+ });
220
+ }
221
+ }
222
+
223
+ function familySupport(family) {
224
+ if (typeof p0Dispatch === 'function') {
225
+ const declared = Array.isArray(p0Dispatch.families) ? p0Dispatch.families : null;
226
+ return declared === null || declared.includes(family)
227
+ ? P0_SUPPORT.SUPPORTED
228
+ : P0_SUPPORT.UNSUPPORTED;
229
+ }
230
+ if (isPlainObject(p0Dispatch)) {
231
+ const handler = p0Dispatch[family];
232
+ if (typeof handler === 'function') return P0_SUPPORT.SUPPORTED;
233
+ if (isPlainObject(handler) && WEB_P0_METHODS[family].some((method) => typeof handler[method] === 'function')) {
234
+ return P0_SUPPORT.SUPPORTED;
235
+ }
236
+ }
237
+ return P0_SUPPORT.UNSUPPORTED;
238
+ }
239
+
240
+ function methodSupported(family, method) {
241
+ if (typeof p0Dispatch === 'function') return familySupport(family) === P0_SUPPORT.SUPPORTED;
242
+ const handler = p0Dispatch?.[family];
243
+ if (typeof handler === 'function') return true;
244
+ return isPlainObject(handler) && typeof handler[method] === 'function';
245
+ }
246
+
247
+ function callP0(family, method, payload) {
248
+ if (typeof p0Dispatch === 'function') return p0Dispatch(family, method, payload);
249
+ const handler = p0Dispatch[family];
250
+ if (typeof handler === 'function') return handler(method, payload);
251
+ return handler[method](payload);
252
+ }
253
+
254
+ async function invokeFamilyMethod(family, method, { args = {}, session, snapshot, signal, context = null, raw = false } = {}) {
255
+ assertNotDisposed();
256
+ const operation = `${family}.${method}`;
257
+ if (familySupport(family) === P0_SUPPORT.UNSUPPORTED || !methodSupported(family, method)) {
258
+ throw new AdapterError(
259
+ ERROR_CODES.CAPABILITY_UNSUPPORTED,
260
+ `web p0 family ${family} does not provide '${method}'`,
261
+ { details: { operation, family, method } },
262
+ );
263
+ }
264
+ validateArgs(family, method, args);
265
+ if (!isPlainObject(session)) {
266
+ throw new AdapterError(ERROR_CODES.TARGET_NOT_BOUND, `${operation} requires a target session`, {
267
+ details: { operation },
268
+ });
269
+ }
270
+ if (session.closed === true) {
271
+ throw new AdapterError(ERROR_CODES.TARGET_GONE, `web session ${String(session.sessionId)} is closed`, {
272
+ details: { operation, sessionId: session.sessionId ?? null },
273
+ });
274
+ }
275
+ const capabilities = capabilityListOf(snapshot);
276
+ if (capabilities !== null && !capabilities.includes(FAMILY_CAPABILITY[family])) {
277
+ throw new AdapterError(
278
+ ERROR_CODES.ADAPTER_DISABLED,
279
+ `capability '${FAMILY_CAPABILITY[family]}' is not enabled for '${operation}'`,
280
+ { details: { operation, capability: FAMILY_CAPABILITY[family] } },
281
+ );
282
+ }
283
+
284
+ let platformResult;
285
+ try {
286
+ // `context` carries the invocation-scoped handles the platform dispatch needs (the page
287
+ // runtime, the calling exec, the action meta). Dropping it made the production dispatch
288
+ // unusable: it could only ever answer PROVIDER_UNAVAILABLE (A01).
289
+ platformResult = await callP0(family, method, {
290
+ args,
291
+ session,
292
+ snapshot,
293
+ signal,
294
+ context,
295
+ // Routing facts for the composition root (§4.2): the P0 call must run on the selected
296
+ // provider's runtime. `null` for a snapshot that predates the provider field.
297
+ provider: typeof snapshot?.provider === 'string' ? snapshot.provider : null,
298
+ executionAdapterId: typeof snapshot?.executionAdapterId === 'string'
299
+ ? snapshot.executionAdapterId
300
+ : null,
301
+ });
302
+ } catch (error) {
303
+ throw toAdapterError(error, { operation });
304
+ }
305
+
306
+ // `raw: true` keeps the platform payload (used by the tool layer, whose public result shape
307
+ // is its own contract); the neutral model is the default for the neutral surface.
308
+ const value = raw === true ? (platformResult ?? null) : normalizeP0Value(family, method, platformResult);
309
+ const evidence = createEvidenceEnvelope({
310
+ scope: session.scopeId ?? null,
311
+ session: session.sessionId ?? null,
312
+ target: session.target?.targetId ?? null,
313
+ generation: Number.isInteger(session.generation) ? session.generation : null,
314
+ configRevision: configRevisionOf(snapshot) ?? (Number.isInteger(session.configRevision) ? session.configRevision : null),
315
+ operation,
316
+ source: 'web.p0',
317
+ capturedAt: typeof now === 'function' ? now() : Date.now(),
318
+ });
319
+ if (typeof evidence === 'object' && evidence !== null) {
320
+ for (const field of P0_EVIDENCE_FIELDS) {
321
+ if (!Object.hasOwn(evidence, field)) {
322
+ throw new AdapterError(ERROR_CODES.EVIDENCE_INCOMPLETE, `evidence is missing '${field}'`, {
323
+ details: { operation, missing: field },
324
+ });
325
+ }
326
+ }
327
+ }
328
+
329
+ log?.debug?.('web.p0.invoke', { operation });
330
+ return { value, evidence };
331
+ }
332
+
333
+ function buildFamilySurface(family) {
334
+ const surface = {};
335
+ for (const method of WEB_P0_METHODS[family]) {
336
+ surface[method] = (input = {}) => invokeFamilyMethod(family, method, input);
337
+ }
338
+ return Object.freeze(surface);
339
+ }
340
+
341
+ return {
342
+ manifest,
343
+
344
+ familyStatus({ session, snapshot } = {}) {
345
+ const capabilities = capabilityListOf(snapshot);
346
+ const out = {};
347
+ for (const family of FAMILY_KEYS) {
348
+ const support = familySupport(family);
349
+ if (support === P0_SUPPORT.UNSUPPORTED) {
350
+ out[family] = {
351
+ support,
352
+ availability: P0_AVAILABILITY.UNAVAILABLE,
353
+ reason: 'provider-missing',
354
+ capability: FAMILY_CAPABILITY[family],
355
+ };
356
+ continue;
357
+ }
358
+ if (!isPlainObject(session)) {
359
+ out[family] = {
360
+ support,
361
+ availability: P0_AVAILABILITY.NOT_PROBED,
362
+ reason: 'no-session',
363
+ capability: FAMILY_CAPABILITY[family],
364
+ };
365
+ continue;
366
+ }
367
+ if (session.closed === true) {
368
+ out[family] = {
369
+ support,
370
+ availability: P0_AVAILABILITY.UNAVAILABLE,
371
+ reason: 'target-gone',
372
+ capability: FAMILY_CAPABILITY[family],
373
+ };
374
+ continue;
375
+ }
376
+ if (capabilities === null) {
377
+ out[family] = {
378
+ support,
379
+ availability: P0_AVAILABILITY.NOT_PROBED,
380
+ reason: 'capability-unknown',
381
+ capability: FAMILY_CAPABILITY[family],
382
+ };
383
+ continue;
384
+ }
385
+ if (!capabilities.includes(FAMILY_CAPABILITY[family])) {
386
+ out[family] = {
387
+ support,
388
+ availability: P0_AVAILABILITY.UNAVAILABLE,
389
+ reason: 'capability-disabled',
390
+ capability: FAMILY_CAPABILITY[family],
391
+ };
392
+ continue;
393
+ }
394
+ out[family] = {
395
+ support,
396
+ availability: P0_AVAILABILITY.AVAILABLE,
397
+ reason: null,
398
+ capability: FAMILY_CAPABILITY[family],
399
+ };
400
+ }
401
+ return out;
402
+ },
403
+
404
+ capture: buildFamilySurface('A'),
405
+ network: buildFamilySurface('B'),
406
+ display: buildFamilySurface('C'),
407
+ canvas: buildFamilySurface('D'),
408
+ targets: buildFamilySurface('E'),
409
+
410
+ async dispose() {
411
+ if (disposed) return;
412
+ disposed = true;
413
+ },
414
+ };
415
+ }
@@ -1,87 +0,0 @@
1
- # 读取面板标注(P0-D annotations)
2
-
3
- 用户说「看我标的」「我圈的是哪里」「这个箭头指的是什么」时用本页。标注是**用户在调试面板里画的问题线索**,Agent 通过结构化文档读取,不通过截图。
4
-
5
- ## 1. 唯一读取入口
6
-
7
- - `browser_state()` 返回顶层 `annotations`(每个**工作区**一份,与已认证 `/state` 的 `ui.annotations` 同源;旧 Host 无此字段 → 按 `unsupported` 处理,不要假装读到)。
8
- - `browser_capabilities()` 的 `annotations` 段声明读取方式、`space`(恒为 `canvas-css-px`)与 `limits`;**工具面仍是 30**,没有"标注工具"可调。
9
- - **标注不进截图、也不进 T3**:`browser_screenshot` 的 `capture.composite:true` 只表示"页面帧合成到白画布",与标注无关。若在截图里找不到用户画的线,这是设计如此,不是 bug。
10
-
11
- ## 2. 文档形状
12
-
13
- ```
14
- annotations = {
15
- version: 1, space: 'canvas-css-px', revision, updatedAt,
16
- source: { kind:'human-panel', panelId, surface:'main'|'sidebar', clientRevision, trusted:false },
17
- context: { runtimeId, pageGeneration, documentEpoch, documentKey, addressDigest },
18
- view: { canvasX, canvasY, x, y, width, height, scale, contentWidth, contentHeight, viewRevision, frameEpoch },
19
- shapes: [...], truncated, truncation:{droppedShapes,droppedPoints,shortenedTextCodeUnits},
20
- stale, staleReason
21
- }
22
- ```
23
-
24
- - `source.trusted` **恒为 false**:标注是线索,不是页面事实。
25
- - `view` 是**发布那一刻**的客户端视图快照;`context.documentKey` 是文档身份,导航/刷新会轮换并清空(v3 起跨页签/跨会话保留,见下)。
26
-
27
- ## 3. 图形与字段
28
-
29
- | `type` | 字段 | 备注 |
30
- | --- | --- | --- |
31
- | `rect` | `x,y,width,height` | 左上角 + 尺寸 |
32
- | `circle` | `cx,cy,rx,ry` | PS 默认语义:按下点与落点为**包围盒两角**的内接椭圆,不是"中心+半径" |
33
- | `line` | `x1,y1,x2,y2` | |
34
- | `arrow` | `x1,y1,x2,y2,head:{length,width}` | **尖端 = `(x2,y2)`**;`head` 由 Host 按 `min(3t,L)/min(2t,L)` 重算,不可信客户端 |
35
- | `brush` | `points:[[x,y],...]` | 只有点集 |
36
- | `text` | `x,y,fontSize,text` | 唯一带**原文**的类型(≤200 UTF-16 code units) |
37
-
38
- 形状还带 `id`(`a1`、`a2`…)、`color`、`strokeWidth`、`fill`、`stroke`。
39
-
40
- ## 4. 坐标换算(最容易错的一步)
41
-
42
- - shapes 存的是**画布 CSS px `d`**;页面坐标 `p = d − S`,`S` 是**当前** `view.x/y`。
43
- - 面板侧另有 `q = d − C`(`C = canvasX/canvasY`);两者关系 `p = q + O`、`O = C − S`。
44
- - **必须用当前 `view` 换算**,不要用 `annotations.view`(那是发布时快照,`C`/`scale` 可能已经变了);`scale`(显示比例)与 DPR **不参与**坐标变换。
45
- - 空白区(`O≠0` 时视口露出的部分)同样可画、可读;`d` 允许负值。
46
-
47
- ## 5. 能读到什么、读不到什么
48
-
49
- - **只有 `text` 有原文**。用户用**画笔手写**的字只有 `brush.points`,没有语义——不要"读"出手写内容,也不要把点集当文本转述。
50
- - 需要 Auxiliary 视觉时,可以按 `points` 自己渲染成图辅助人工核对,但必须声明"这是按点集重建的几何,不是面板渲染结果"。
51
-
52
-
53
- ## 6. 落点解析:「这个箭头指的是什么」
54
-
55
- 1. 取端点 → `p = d − S` 得到页面点。
56
- 2. `browser_eval` 命中:`document.elementFromPoint(p.x, p.y)`;再枚举同区域可见元素,按**点到矩形边距**排序,得到候选表。
57
- 3. 手绘尖端常偏 10–20 px:**给候选 + 距离**(例如"搜索框容器 2 px、标签栏 3 px、logo 22 px"),不要单点断言"就是它"。
58
- 4. 需要更多证据时:`browser_snapshot` + `browser_node_state` 取 AX 语义/状态;`browser_vision_image` 放大该区域**看页面像素**(仍不含标注)做人工复核;需要记账时把结论写成 `browser_vision_check` 的 `facts`,别用像素推断冒充 DOM 事实。
59
-
60
- ## 7. 生命周期与隔离(v3)
61
-
62
- - 文档按 `documentKey` 绑定:**导航/刷新/换文档会清空**;同一工作区内切页签、重挂面板、换会话**保留**(客户端按文档身份水合)。
63
- - **按工作区隔离**:不同工作区各有一份,互不串;读取时用当前会话的 `browser_state` 即可,不要跨工作区拼装。
64
- - `stale:true`(`panel-unseen`)表示来源面板心跳过期,**不代表标注不存在或已删除**:形状照常可读,如实说明"可能不是最新"即可;不得伪造心跳或 revision。
65
- - 显式「清除画布」会让 `shapes` 变空(`revision` 递增),不要把它当成"读取失败"。
66
-
67
- ## 8. 红线
68
-
69
- - 标注是 `trusted:false` 的**问题线索**:不构成缺陷证明,也**不授权**按它自动点击/提交(要动页面仍需用户明确许可)。
70
- - 不要用标注内容替代交叉验证:结论至少要落到 DOM 读回、截图或网络证据之一。
71
- - 不要为了"看起来有标注"而伪造 `annotations` 字段、revision 或截图。
72
-
73
- ## 9. 常见坑
74
-
75
- | 坑 | 正确做法 |
76
- | --- | --- |
77
- | 把 `brush.points` 当文字转述 | 只有 `text` 有原文;手写只说"笔画",必要时提议用户改用文字工具 |
78
- | 用 `annotations.view` 换算坐标 | 用当前 `view.x/y`(`p = d − S`) |
79
- | 以为截图/T3 里有标注 | 没有;走 `browser_state.annotations` |
80
- | 按 `scale` 缩放坐标 | `scale` 只影响显示与截图密度 |
81
- | 把箭头 `(x1,y1)` 当尖 | 尖端是 `(x2,y2)` |
82
- | 把 `stale` 当"数据没了" | 只是心跳过期,形状仍可读 |
83
-
84
- ## 10. 参考
85
-
86
- - 权威契约:`docs/viewport-control.md`(共享视口、画布与标注)
87
- - 验收入口:`tests/acceptance/viewport.mjs`、`tests/acceptance/viewport-gui.mjs`