dsh-agent-team-ui-testing 0.0.0-stage → 0.1.1
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.
- package/README.md +192 -2
- package/banner.png +0 -0
- package/banner_zh.png +0 -0
- package/browser-safety.js +249 -0
- package/capture-p0.js +910 -0
- package/client.js +7727 -0
- package/cordis.patch.yml +3 -0
- package/docs/architecture.md +111 -0
- package/docs/capability-tiers.md +64 -0
- package/docs/capture-p0.md +53 -0
- package/docs/element-picker.md +125 -0
- package/docs/getting-started.md +90 -0
- package/docs/index.md +33 -0
- package/docs/multi-tab.md +127 -0
- package/docs/network-p0.md +117 -0
- package/docs/t2-structured-gui.md +68 -0
- package/docs/t3-vision.md +79 -0
- package/docs/t4-dynamic-behavior.md +134 -0
- package/docs/tools.md +116 -0
- package/docs/verification.md +107 -0
- package/docs/viewport-control.md +81 -0
- package/dynamic-t4.js +5699 -0
- package/frame-push.js +508 -0
- package/frame-source.js +162 -0
- package/icon.png +0 -0
- package/index.js +20016 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/network-p0.js +4247 -0
- package/package.json +18 -4
- package/skills/ui-testing/SKILL.md +480 -0
- package/skills/ui-testing/manifests/declared-capabilities.json +896 -0
- package/skills/ui-testing/references/contracts/capability.md +68 -0
- package/skills/ui-testing/references/contracts/evidence.md +41 -0
- package/skills/ui-testing/references/contracts/operation.md +44 -0
- package/skills/ui-testing/references/core/dynamic-visual.md +123 -0
- package/skills/ui-testing/references/core/fallback.md +83 -0
- package/skills/ui-testing/references/core/network-and-state.md +114 -0
- package/skills/ui-testing/references/core/page-identity.md +90 -0
- package/skills/ui-testing/references/core/report-template.md +144 -0
- package/skills/ui-testing/references/core/reporting.md +92 -0
- package/skills/ui-testing/references/core/safety.md +12 -0
- package/skills/ui-testing/references/core/selection.md +26 -0
- package/skills/ui-testing/references/core/tool-routing-and-limits.md +195 -0
- package/skills/ui-testing/references/core/verdict-examples.md +96 -0
- package/skills/ui-testing/references/core/verdict-gate.md +105 -0
- package/skills/ui-testing/references/core/verification.md +19 -0
- package/skills/ui-testing/references/core/workflow.md +16 -0
- package/skills/ui-testing/references/platforms/index.md +19 -0
- package/skills/ui-testing/references/platforms/web.md +17 -0
- package/skills/ui-testing/references/scenarios/annotations.md +87 -0
- package/skills/ui-testing/references/scenarios/functional.md +205 -0
- package/skills/ui-testing/references/scenarios/visual-motion.md +170 -0
- package/skills/ui-testing/references/tiers/t1-programmatic.md +25 -0
- package/skills/ui-testing/references/tiers/t2-structured.md +61 -0
- package/skills/ui-testing/references/tiers/t3-static-visual.md +45 -0
- package/skills/ui-testing/references/tiers/t4-dynamic-visual.md +57 -0
- package/skills/ui-testing/templates/capability-note.md +23 -0
- package/skills/ui-testing/templates/platform.md +30 -0
- package/skills/ui-testing/templates/regression-case.md +15 -0
- package/skills/ui-testing/templates/scenario.md +13 -0
- package/vision-baseline.js +1254 -0
- package/vision-t3.js +2657 -0
package/README.md
CHANGED
|
@@ -1,3 +1,193 @@
|
|
|
1
|
-
#
|
|
1
|
+
# dsh-agent-team-ui-testing
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
<img src="banner.png" alt="dsh-agent-team-ui-testing — multi-platform GUI automation testing for DeepSeekHarness" width="100%">
|
|
4
|
+
|
|
5
|
+
**AI-native frontend GUI testing for DeepSeekHarness.**
|
|
6
|
+
|
|
7
|
+
Let AI agents operate, inspect, test and verify real user interfaces — with evidence humans can review.
|
|
8
|
+
|
|
9
|
+
**English** | [简体中文](README_zh.md)
|
|
10
|
+
|
|
11
|
+
> **Status: pre-release.** The Web/Chromium channel is implemented across all four tiers: 36 agent tools plus a debug panel for programmatic inspection, structured GUI actions, static visual analysis, dynamic frame/timing analysis, network capture, multi-tab page groups, shared viewport control, a hybrid canvas, an element picker and client-side annotations. Other environments are planned and stay disabled in the runtime picker. **Install:** `dsh plugin --profile web add dsh-agent-team-ui-testing`. Capabilities, limits and verification are documented in [docs/capability-tiers.md](docs/capability-tiers.md) and [docs/verification.md](docs/verification.md).
|
|
12
|
+
|
|
13
|
+
## What is this?
|
|
14
|
+
|
|
15
|
+
**dsh-agent-team-ui-testing** is a UI testing infrastructure plugin for [DeepSeekHarness (DSH)](https://github.com/deepseek-ai/deepseek-harness). It lets an AI agent — or a coordinated Agent Team — drive a real application, inspect what the application exposes, verify expected behavior, collect reviewable evidence and report actionable failures.
|
|
16
|
+
|
|
17
|
+
The runtime is built around a shared Chromium instance with one isolated page per DSH workspace, a typed tool surface, and a human-facing debug panel. The same session can be inspected programmatically, operated through structured DOM/accessibility data, captured as screenshots, or analyzed visually.
|
|
18
|
+
|
|
19
|
+
The project grows out of the earlier `dsh-agent-team-browser` idea. The shared browser workspace remains the first adapter of a broader, capability-aware UI testing runtime.
|
|
20
|
+
|
|
21
|
+
## Why build it?
|
|
22
|
+
|
|
23
|
+
Coding agents can produce UI code without being able to answer: **“Does the finished application actually work and look right?”** A 200 response, a running process or a single screenshot is not proof of a working user journey.
|
|
24
|
+
|
|
25
|
+
This project focuses on:
|
|
26
|
+
|
|
27
|
+
- **Real UI feedback** — exercise user journeys on rendered interfaces, not only source code.
|
|
28
|
+
- **Structured-first automation** — prefer APIs, DOM and accessibility data; use vision when structure is insufficient.
|
|
29
|
+
- **Evidence-based results** — record what ran, what was observed, what passed, what failed and what remains unverified.
|
|
30
|
+
- **Human control** — authorized access, scoped sessions and a panel humans can watch or take over.
|
|
31
|
+
- **Low disruption** — prefer background browser operations over hijacking the desktop foreground.
|
|
32
|
+
|
|
33
|
+
## What works today
|
|
34
|
+
|
|
35
|
+
- **Shared browser runtime** — one Chromium per host (the system Chrome/Chromium binary; the plugin does not bundle or install a browser), headless by default with an optional headed mode in the Web view (off-screen hidden window + persistent profile); each DSH workspace gets its own page, URL, viewport, history, network buffer and lease. Cookies and storage are shared by the active Chromium profile.
|
|
36
|
+
- **36 agent tools** across programmatic, structured GUI, static vision, dynamic behavior, network debugging and per-workspace tab management.
|
|
37
|
+
- **Multi-tab page group (P0-E)** — each workspace keeps a primary page plus up to three child tabs, with per-tab leases, per-agent tab selection and popup adoption kept under the same isolation rules.
|
|
38
|
+
- **Dynamic behavior (T4)** — real CDP screencast plus a screenshot-loop path, a bounded frame store with integrity counters, seven deterministic motion analyses, WAAPI motion control with restore, region-only frame delivery, and six-direction frame↔network event correlation.
|
|
39
|
+
- **Debug panel** — live preview, responsive size controls, shared viewport state, a hybrid canvas, and a client-side annotation layer that agents can read back.
|
|
40
|
+
- **Element picker** — select an element in the panel preview to append its node description to the chat as an `@` reference chip; the model receives the full text block and the page is not modified.
|
|
41
|
+
- **Evidence and safety** — typed errors, structured action evidence, validated PNG capture, managed visual baselines, network evidence and optimistic concurrency for shared state.
|
|
42
|
+
|
|
43
|
+
### Agent tools
|
|
44
|
+
|
|
45
|
+
| Group | Tools | What it covers |
|
|
46
|
+
| --- | --- | --- |
|
|
47
|
+
| **Programmatic** (10) | `browser_navigate` `browser_eval` `browser_screenshot` `browser_input` `browser_network` `browser_computed_style` `browser_http_request` `browser_viewport` `browser_state` `browser_capabilities` | Navigation, JavaScript, console/network inspection, independent HTTP requests, computed styles, screenshots, raw mouse/wheel/keyboard input, shared viewport state and capability reporting |
|
|
48
|
+
| **Page group** (1) | `browser_tabs` | Primary + up to three child tabs per workspace, per-tab leases, agent tab selection and tab lifecycle (list/current/select/open/close/wait) |
|
|
49
|
+
| **Structured GUI** (7) | `browser_snapshot` `browser_click` `browser_type` `browser_scroll` `browser_fill_form` `browser_node_state` `browser_wait_for` | Snapshot → ref targeting, trusted input, forms, node state and waits, with structured evidence and typed errors |
|
|
50
|
+
| **Static vision** (6) | `browser_vision_image` `browser_vision_find` `browser_vision_compare` `browser_vision_act` `browser_vision_baseline` `browser_vision_check` | Crop/zoom/preprocess/annotate PNGs, deterministic colour/component/edge/template finders, YIQ diff, coordinate input, managed baselines and visual assertions |
|
|
51
|
+
| **Dynamic** (5) | `browser_dynamic_session` `browser_motion_observe` `browser_motion_analyze` `browser_motion_control` `browser_dynamic_correlate` | Bounded frame capture with integrity counters, region-only frame delivery, seven motion/transition analyses, WAAPI pause/seek/rate/restore, and six-direction frame↔network correlation |
|
|
52
|
+
| **Network** (7) | `browser_network_session` `browser_network_events` `browser_network_detail` `browser_network_trace` `browser_network_control` `browser_network_evidence` `browser_network_subscribe` | Capture, timeline, analysis, business-step traces, interception/throttling/replay/mock, JSONL/HAR evidence and per-agent subscriptions |
|
|
53
|
+
|
|
54
|
+
### Debug panel
|
|
55
|
+
|
|
56
|
+
The panel is the human window into the same runtime the agent uses.
|
|
57
|
+
|
|
58
|
+
- **Live preview** of the workspace page, driven by a screencast with a polling fallback.
|
|
59
|
+
- **Size bar** — 宽屏 / 横屏 / 平板 / 手机 presets as a segmented control, X/Y canvas position, width × height, display scale, and a `拖拽 | 适应窗口 | 默认` mode selector. Preset clicks reset X/Y and keep the chosen display mode; the selected preset shows a green dot and is mutually exclusive with drag mode.
|
|
60
|
+
- **Hybrid canvas** — the viewport can be positioned like a canvas: a signed origin, negative coordinates and blank areas, a dark checkerboard in the panel (pure white in screenshots), drag-to-pan and screen-anchored wheel zoom.
|
|
61
|
+
- **Annotation mode** — circle, rectangle, line, arrow, brush, text and eraser, plus fill/outline, colour, width and clear-canvas. Drawings are anchored in canvas coordinates, the page receives no input while annotating, and the document is published so agents can read it through `browser_state.annotations`.
|
|
62
|
+
- **Shared state** — agent and panel read the same viewport; updates use `expectedRevision`, and conflicts are reported instead of silently overwriting.
|
|
63
|
+
|
|
64
|
+
### Evidence and safety
|
|
65
|
+
|
|
66
|
+
- Every structured action can return evidence: target identity, before/after state, dispatch facts, network deltas and typed errors.
|
|
67
|
+
- Screenshots are validated PNGs with explicit scale and canvas-space regions; capture can be staged or persisted under a controlled root.
|
|
68
|
+
- Visual baselines are immutable revisions with compare-and-swap HEAD updates and tombstones.
|
|
69
|
+
- Network evidence supports JSONL (lossless) and HAR 1.2 export with default redaction and explicit truncation counters.
|
|
70
|
+
- Frame evidence keeps the original `sourceTimestamp`, separate encoded and RGBA hashes, and dropped/evicted/gap counters; motion analyses are deterministic and report budget exhaustion or timeout as `BLOCKED` instead of pretending to pass.
|
|
71
|
+
- Runtime access is scoped to a DSH workspace; mutating page work is serialized through a lease and a serial queue.
|
|
72
|
+
- Navigation is restricted to approved targets (loopback by default; private ranges blocked unless explicitly enabled), with DNS, redirect and popup checks.
|
|
73
|
+
|
|
74
|
+
## How it works
|
|
75
|
+
|
|
76
|
+
```text
|
|
77
|
+
DSH Agent / Agent Team
|
|
78
|
+
│
|
|
79
|
+
▼
|
|
80
|
+
UI testing plugin runtime
|
|
81
|
+
├─ shared Chromium (headless by default; headed available in Web view)
|
|
82
|
+
├─ one page per DSH workspace (URL, viewport, history, network, lease)
|
|
83
|
+
├─ agent tool surface (36 tools)
|
|
84
|
+
└─ debug panel (preview, size bar, canvas, annotations)
|
|
85
|
+
│
|
|
86
|
+
▼
|
|
87
|
+
Observations, evidence and verdicts
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Only the Web/Chromium adapter is implemented. Desktop, Android, TUI, HarmonyOS, game and 3D adapters are planned; each must report its real capabilities instead of pretending to support the same operations.
|
|
91
|
+
|
|
92
|
+
## Capability tiers
|
|
93
|
+
|
|
94
|
+
The tool surface is organized as **selection and fallback levels**, not a mandatory pipeline. Pick the lowest-cost reliable path; escalate when it cannot produce the required evidence.
|
|
95
|
+
|
|
96
|
+
| Level | Focus | Typical use |
|
|
97
|
+
| --- | --- | --- |
|
|
98
|
+
| **T1** Programmatic | CDP, HTTP, JavaScript, network, console | API checks, network faults, page state, performance data |
|
|
99
|
+
| **T2** Structured GUI | DOM, accessibility tree, snapshot/ref, trusted input | Buttons, forms, navigation, interaction tests |
|
|
100
|
+
| **T3** Static vision | Screenshots, deterministic pixel analysis, coordinate actions | Layout, colour, occlusion, canvas, chart and style defects |
|
|
101
|
+
| **T4** Dynamic vision | Frame sequences, timing, motion reasoning | Animation, transitions, dragging and time-dependent behavior — escalates from T3 when a single image cannot prove motion |
|
|
102
|
+
|
|
103
|
+
A successful T1 call is never treated as a visual pass. UI-specific acceptance criteria still require UI-level observations.
|
|
104
|
+
|
|
105
|
+
## Getting started
|
|
106
|
+
|
|
107
|
+
**Requirements:** a DSH installation and a local Chrome/Chromium binary. The plugin does not bundle or install a browser; it launches the system binary it resolves (see [docs/getting-started.md](docs/getting-started.md)). A headed Web view additionally needs `DISPLAY` or `WAYLAND_DISPLAY` on POSIX hosts; Windows hosts always provide a window system, so headed mode needs no such variable there. The plugin requires Node.js 22+ (native `WebSocket`); the current DSH release itself requires Node.js 24.9+, so a real DSH host follows the stricter version.
|
|
108
|
+
|
|
109
|
+
The plugin ships as a DSH plugin bundle and is published on npm as `dsh-agent-team-ui-testing`. There is no compatibility matrix beyond the Node.js and DSH requirements above.
|
|
110
|
+
|
|
111
|
+
```sh
|
|
112
|
+
# npm form (recommended): follows semver, so dsh-market can prompt for updates
|
|
113
|
+
dsh plugin --profile web add dsh-agent-team-ui-testing
|
|
114
|
+
|
|
115
|
+
# floating GitHub form: also receives updates, but only when package.json version is bumped
|
|
116
|
+
dsh plugin --profile web add github:bloodarea/dsh-agent-team-ui-testing
|
|
117
|
+
|
|
118
|
+
# pinned tag: reproduces exactly one release and never receives update prompts
|
|
119
|
+
dsh plugin --profile web add github:bloodarea/dsh-agent-team-ui-testing#vX.Y.Z
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
1. Install the package into the DSH profile being used.
|
|
123
|
+
2. Restart DSH after the first install so the plugin bundle is registered.
|
|
124
|
+
3. Open the debug view from **更多 → 调试视图**, or from the plugin card in the right sidebar.
|
|
125
|
+
4. Changes to `client.js` only need a panel refresh; changes to `index.js`, the tool surface or bundled skills need a DSH restart.
|
|
126
|
+
|
|
127
|
+
For plugin development, link or copy this repository into the profile instead of installing the npm package.
|
|
128
|
+
|
|
129
|
+
Requirements, install steps and panel usage are in [docs/getting-started.md](docs/getting-started.md).
|
|
130
|
+
|
|
131
|
+
A typical agent flow is: navigate to the target, inspect state with `browser_state`/`browser_eval`, perform structured actions with the T2 tools, capture screenshots or run T3 checks, inspect network traffic with the network tools, escalate to the dynamic tools (`browser_dynamic_session` → `browser_motion_observe` → `browser_motion_analyze`/`browser_dynamic_correlate`) when the question is about motion, timing or a frame sequence, and use the shared viewport/canvas/annotation state when the task involves layout, framing or visual feedback.
|
|
132
|
+
|
|
133
|
+
## Current limits
|
|
134
|
+
|
|
135
|
+
- **Web target only.** Web/Chromium is the only implemented target environment and runs on Linux, macOS and Windows hosts. Desktop applications, Android, TUI, HarmonyOS, games and 3D are planned targets and stay disabled in the runtime picker.
|
|
136
|
+
- **Vision is deterministic, not semantic.** T3 judges single PNGs and T4 judges frame sequences with deterministic algorithms: no VLM/OCR, no semantic naming, so "what does this text mean" and "looks right to a human" stay out of scope.
|
|
137
|
+
- **T4 verification is partial.** Of the 44 dynamic-behavior criteria, 12 pass, 20 are partial, 0 fail and 12 were not run — the suite exits non-zero only because required checks were not run, not because they failed. Close/dispose restore, WebGPU rendering, media seeking/stalling, header-verified identity, 16 ms flicker coverage and long-lived connections remain unverified.
|
|
138
|
+
- **Shared browser profile.** Cookies and storage are shared by the Chromium profile; only page, URL, viewport, history, network buffer and leases are isolated per workspace.
|
|
139
|
+
- **Screenshots are not redacted.** Review their content before sharing.
|
|
140
|
+
- **Attachments are not provider visibility.** A returned image attachment does not prove the model received it at full resolution.
|
|
141
|
+
- **Network capture is partial at the edges.** Same-page iframe and dedicated workers are covered; OOPIF is partial; shared and service workers are not collected; HAR is a 1.2 subset; body/header redaction is not a general secret detector.
|
|
142
|
+
- **Viewport display is a 1× screencast.** A display scale above 100% magnifies the bitmap rather than adding pixel density; request an explicit screenshot scale when resolution matters. Resize input barriers are best-effort, while canvas offsets use a fail-closed barrier.
|
|
143
|
+
- **Shared state is optimistic.** `browser_viewport get` is a cache diagnostic; `stale` means “not recently observed”, and a lost `expectedRevision` race returns `VIEWPORT_CONFLICT` instead of replaying the patch.
|
|
144
|
+
|
|
145
|
+
## Roadmap
|
|
146
|
+
|
|
147
|
+
- Harden the Web channel and close the remaining dynamic-behavior criteria (12 not run, 20 partial).
|
|
148
|
+
- Add the cross-environment test core: typed sessions, observations, assertions and reports.
|
|
149
|
+
- Validate desktop (Windows/Linux/macOS), Android, TUI, HarmonyOS, game and 3D adapters by demand and platform feasibility.
|
|
150
|
+
- Add multi-agent scheduling, human takeover and release hardening.
|
|
151
|
+
|
|
152
|
+
## Documentation
|
|
153
|
+
|
|
154
|
+
| File | Contents |
|
|
155
|
+
| --- | --- |
|
|
156
|
+
| [docs/index.md](docs/index.md) | Documentation index |
|
|
157
|
+
| [docs/getting-started.md](docs/getting-started.md) | Requirements, install, debug panel and first calls |
|
|
158
|
+
| [docs/tools.md](docs/tools.md) | All 36 tools, parameters and error classes |
|
|
159
|
+
| [docs/capability-tiers.md](docs/capability-tiers.md) | T1–T4 selection model, P0 infrastructure and known limits |
|
|
160
|
+
| [docs/viewport-control.md](docs/viewport-control.md) | Shared viewport, hybrid canvas and annotations |
|
|
161
|
+
| [docs/capture-p0.md](docs/capture-p0.md) | Screenshots, image delivery and file lifecycle |
|
|
162
|
+
| [docs/multi-tab.md](docs/multi-tab.md) | Per-workspace tab group, per-tab leases and agent tab selection |
|
|
163
|
+
| [docs/element-picker.md](docs/element-picker.md) | Panel element picking and `@` element reference chips |
|
|
164
|
+
| [docs/network-p0.md](docs/network-p0.md) | Network capture, tracing, control and evidence |
|
|
165
|
+
| [docs/t2-structured-gui.md](docs/t2-structured-gui.md) | Snapshot/ref, trusted input and form filling |
|
|
166
|
+
| [docs/t3-vision.md](docs/t3-vision.md) | Static vision algorithms, baselines and verdicts |
|
|
167
|
+
| [docs/t4-dynamic-behavior.md](docs/t4-dynamic-behavior.md) | Frame capture, motion analysis, control and correlation |
|
|
168
|
+
| [docs/architecture.md](docs/architecture.md) | Runtime, module responsibilities and extension points |
|
|
169
|
+
| [docs/verification.md](docs/verification.md) | Reproducible tests, result interpretation and contributing |
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
## Relationship to other projects
|
|
173
|
+
|
|
174
|
+
| Project | Role |
|
|
175
|
+
| --- | --- |
|
|
176
|
+
| [`dsh-agent-team-presets`](https://github.com/bloodarea/dsh-agent-team-presets) | Reusable team and role configurations, model routes and tool policies |
|
|
177
|
+
| **`dsh-agent-team-ui-testing`** | UI testing targets, sessions, automation, evidence and governed human/agent control |
|
|
178
|
+
|
|
179
|
+
They are complementary but do not require each other; the runtime supports a single DSH agent without the presets plugin.
|
|
180
|
+
|
|
181
|
+
## Contributing
|
|
182
|
+
|
|
183
|
+
Reproducible DSH integration findings, platform feasibility tests, accessibility/DOM fallback cases, security threat models and concrete UI acceptance scenarios are the most useful contributions.
|
|
184
|
+
|
|
185
|
+
When filing an issue, include the DSH version/commit, platform and runtime versions, reproduction steps, expected and observed results, and sanitized evidence. Never upload credentials, personal device data or unredacted authenticated traffic.
|
|
186
|
+
|
|
187
|
+
## License
|
|
188
|
+
|
|
189
|
+
The repository does not currently declare a license. Add an explicit license before redistribution or reuse; third-party code incorporated later must retain its applicable notices.
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
**Repository:** [bloodarea/dsh-agent-team-ui-testing](https://github.com/bloodarea/dsh-agent-team-ui-testing) · **Maintainer:** [bloodarea](https://github.com/bloodarea) · **Status:** pre-release
|
package/banner.png
ADDED
|
Binary file
|
package/banner_zh.png
ADDED
|
Binary file
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { lookup } from 'node:dns/promises';
|
|
2
|
+
import { isIP } from 'node:net';
|
|
3
|
+
|
|
4
|
+
const TAXONOMY = {
|
|
5
|
+
NAV_FAILED: ['navigation', true], CDP_TIMEOUT: ['timeout', true],
|
|
6
|
+
CDP_DISCONNECTED: ['disconnected', true], TARGET_NOT_FOUND: ['action', false],
|
|
7
|
+
EVAL_ERROR: ['app', false], INVALID_ARGUMENT: ['input', false], BUSY: ['concurrency', true],
|
|
8
|
+
UNSUPPORTED: ['capability', false], INTERNAL: ['internal', false], ABORTED: ['cancelled', true],
|
|
9
|
+
// Headed mode without a window system is a capability refusal; it must never degrade to INTERNAL.
|
|
10
|
+
DISPLAY_UNAVAILABLE: ['capability', false],
|
|
11
|
+
// Independent HTTP client (docs/tools.md): HTTP timeouts are not CDP timeouts
|
|
12
|
+
// (they must never trigger the page "close the browser before releasing the queue" path).
|
|
13
|
+
HTTP_TIMEOUT: ['timeout', true], HTTP_FAILED: ['network', true], HTTP_REDIRECT_LIMIT: ['network', false],
|
|
14
|
+
// Workspace isolation (docs/architecture.md): these must never fall back to INTERNAL.
|
|
15
|
+
SCOPE_INVALID: ['input', false], SCOPE_NOT_FOUND: ['scope', false],
|
|
16
|
+
SCOPE_UNAVAILABLE: ['scope', true], SCOPE_FORBIDDEN: ['authorization', false],
|
|
17
|
+
RUNTIME_LIMIT: ['resource', true],
|
|
18
|
+
// Web T2 (structured GUI, docs/t2-structured-gui.md): a ref that no longer points at the node it
|
|
19
|
+
// was minted for, a selector that is not unique, a target that cannot be acted on, and a bounded
|
|
20
|
+
// wait that ran out. RETRYABLE only tells the caller a retry *may* help — a stale ref needs a new
|
|
21
|
+
// snapshot, not a replay.
|
|
22
|
+
REF_STALE: ['action', false], TARGET_NOT_UNIQUE: ['action', false],
|
|
23
|
+
NOT_INTERACTABLE: ['action', false], WAIT_TIMEOUT: ['timeout', true],
|
|
24
|
+
// Shared viewport (docs/viewport-control.md, Lead freeze 2026-11-01): an optimistic
|
|
25
|
+
// concurrency failure, not a bad argument and not a page failure. `state` + retryable=true means
|
|
26
|
+
// "read the current view and decide again"; the patch was NOT applied and page lastError is kept.
|
|
27
|
+
VIEWPORT_CONFLICT: ['state', true],
|
|
28
|
+
// P0 capture infrastructure (docs/capture-p0.md): the capture pipeline has its own taxonomy so a
|
|
29
|
+
// bad screenshot never masquerades as INTERNAL. Size/format refusals are not retryable (the same
|
|
30
|
+
// page would produce the same bytes); a failed CDP capture and a failed output write are.
|
|
31
|
+
// ABORTED/CDP_TIMEOUT/CDP_DISCONNECTED keep their existing codes for capture too.
|
|
32
|
+
CAPTURE_FAILED: ['capture', true], INVALID_PNG: ['capture', false],
|
|
33
|
+
CAPTURE_SIZE_MISMATCH: ['capture', false], CAPTURE_UPSCALED: ['capture', false],
|
|
34
|
+
CAPTURE_TOO_LARGE: ['resource', false], OUTPUT_PATH_INVALID: ['input', false],
|
|
35
|
+
OUTPUT_EXISTS: ['output', false], OUTPUT_WRITE_FAILED: ['output', true],
|
|
36
|
+
// Web T3 (static vision, docs/t3-vision.md). Every code below is fixed by the contract:
|
|
37
|
+
// a vision refusal must never degrade into INTERNAL, and a product/baseline that cannot be
|
|
38
|
+
// read must never be reported as a visual FAIL. `vision` marks "the image/source side said no",
|
|
39
|
+
// `action` marks "the live page no longer matches the screenshot", `output` marks the managed
|
|
40
|
+
// baseline root on disk, `concurrency` marks the HEAD CAS loser.
|
|
41
|
+
VISION_SOURCE_NOT_FOUND: ['vision', false], VISION_FORMAT_UNSUPPORTED: ['vision', false],
|
|
42
|
+
VISION_LIMIT_EXCEEDED: ['resource', false], VISION_TIMEOUT: ['timeout', true],
|
|
43
|
+
VISION_SIZE_MISMATCH: ['vision', false], VISION_EMPTY_COMPARISON: ['vision', false],
|
|
44
|
+
VISION_STALE_IMAGE: ['action', false], VISION_HIT_MISMATCH: ['action', false],
|
|
45
|
+
VISION_BASELINE_ROOT_UNAVAILABLE: ['output', false], VISION_BASELINE_NAME_INVALID: ['input', false],
|
|
46
|
+
VISION_BASELINE_NOT_FOUND: ['vision', false], VISION_BASELINE_EXISTS: ['output', false],
|
|
47
|
+
VISION_BASELINE_CONFLICT: ['concurrency', false],
|
|
48
|
+
VISION_BASELINE_READ_FAILED: ['output', true], VISION_BASELINE_WRITE_FAILED: ['output', true],
|
|
49
|
+
// Web P0 network collection (docs/network-p0.md). Codes, categories and retryability are
|
|
50
|
+
// fixed by the contract table: `scope` = the session/store is not there, `action` = the cited
|
|
51
|
+
// event is gone, `resource` = a declared budget, `concurrency` = the page/session is owned
|
|
52
|
+
// elsewhere or an id/step name collides, `capability` = no CDP/page or an unsupported CDP call,
|
|
53
|
+
// `output`/`input` = the evidence file side, `authorization` = cross-agent/cross-scope access.
|
|
54
|
+
NET_SESSION_NOT_FOUND: ['scope', false], NET_SESSION_ACTIVE: ['concurrency', false],
|
|
55
|
+
NET_SESSION_NOT_ACTIVE: ['scope', true], NET_CONFIG_INVALID: ['input', false],
|
|
56
|
+
NET_EVENT_NOT_FOUND: ['action', false], NET_BODY_UNAVAILABLE: ['network', true],
|
|
57
|
+
NET_QUOTA_EXCEEDED: ['resource', false], NET_RULE_INVALID: ['input', false],
|
|
58
|
+
NET_RULE_CONFLICT: ['concurrency', false], NET_RULE_NOT_FOUND: ['input', false],
|
|
59
|
+
NET_CONTROL_UNAVAILABLE: ['capability', false], NET_CONTROL_TIMEOUT: ['timeout', true],
|
|
60
|
+
NET_EXPORT_FAILED: ['output', true], NET_IMPORT_FAILED: ['input', false],
|
|
61
|
+
NET_HAR_INVALID: ['input', false], NET_SUBSCRIPTION_NOT_FOUND: ['scope', false],
|
|
62
|
+
NET_SUBSCRIPTION_FORBIDDEN: ['authorization', false], NET_INTEGRITY_FAILED: ['integrity', false],
|
|
63
|
+
NET_SCOPE_FORBIDDEN: ['authorization', false],
|
|
64
|
+
// R1-5(docs/network-p0.md):新 7 工具的参数/路径/根/写盘错误一律 NET_*,
|
|
65
|
+
// 只有 BUSY/ABORTED/CDP_*/RUNTIME_LIMIT 这些平台码可以透传。
|
|
66
|
+
NET_INVALID_ARGUMENT: ['input', false], NET_OUTPUT_ROOT_UNKNOWN: ['output', false],
|
|
67
|
+
NET_OUTPUT_PATH_INVALID: ['input', false], NET_OUTPUT_WRITE_FAILED: ['output', true],
|
|
68
|
+
// Web T4 (docs/t4-dynamic-behavior.md): the DYNAMIC_* surface is its own taxonomy — a
|
|
69
|
+
// dynamic refusal must never degrade into INTERNAL, and missing frame/clock evidence is an
|
|
70
|
+
// evidence problem, not a page failure. `scope` = the session/event is gone or belongs to
|
|
71
|
+
// another scope, `input` = pure pre-check (zero start/control), `capability` = the requested
|
|
72
|
+
// frame source/encoding/adapter is unavailable (never silently substituted), `evidence` =
|
|
73
|
+
// clock/mapping limits make an exact claim impossible (keep the events, degrade the verdict),
|
|
74
|
+
// `state` = page-generation/control conflict, `resource` = a declared budget.
|
|
75
|
+
DYNAMIC_INVALID_ARGUMENT: ['input', false], DYNAMIC_SESSION_NOT_FOUND: ['scope', false],
|
|
76
|
+
DYNAMIC_EVENT_NOT_FOUND: ['scope', false], DYNAMIC_SESSION_ACTIVE: ['concurrency', false],
|
|
77
|
+
DYNAMIC_SOURCE_UNAVAILABLE: ['capability', false], DYNAMIC_CLOCK_UNCERTAIN: ['evidence', false],
|
|
78
|
+
DYNAMIC_LIMIT_EXCEEDED: ['resource', false], DYNAMIC_TIMEOUT: ['timeout', true],
|
|
79
|
+
DYNAMIC_CONTROL_CONFLICT: ['state', false], DYNAMIC_RESTORE_FAILED: ['state', false],
|
|
80
|
+
DYNAMIC_MAPPING_INCOMPATIBLE: ['evidence', false],
|
|
81
|
+
// P0-E page group (docs/multi-tab.md): a tab that is gone (`scope`), a tab owned by another
|
|
82
|
+
// workspace (`authorization`), the per-workspace/global page capacity (`resource`) and a popup
|
|
83
|
+
// whose first document cannot be hosted (`capability`). None of them may read as INTERNAL.
|
|
84
|
+
TAB_NOT_FOUND: ['scope', false], TAB_FORBIDDEN: ['authorization', false],
|
|
85
|
+
TAB_LIMIT: ['resource', false], POPUP_UNSUPPORTED: ['capability', false],
|
|
86
|
+
// T4 extend on a stopped/retained session: the session is gone but starting a new one may
|
|
87
|
+
// recover, hence scope + retryable (docs/t4-dynamic-behavior.md §extend).
|
|
88
|
+
DYNAMIC_SESSION_NOT_ACTIVE: ['scope', true],
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/** Frozen copy of the error taxonomy: every thrown code must have an entry (tests enforce it). */
|
|
92
|
+
export const ERROR_TAXONOMY = Object.freeze({ ...TAXONOMY });
|
|
93
|
+
export class BrowserError extends Error {
|
|
94
|
+
constructor(code, message, details = {}) {
|
|
95
|
+
super(`[${code}] ${redactText(message)}`);
|
|
96
|
+
this.name = 'BrowserError';
|
|
97
|
+
this.code = code;
|
|
98
|
+
[this.category, this.retryable] = TAXONOMY[code] || TAXONOMY.INTERNAL;
|
|
99
|
+
this.details = details;
|
|
100
|
+
}
|
|
101
|
+
payload() {
|
|
102
|
+
return { code: this.code, category: this.category, retryable: this.retryable,
|
|
103
|
+
message: this.message, details: this.details };
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
export function asBrowserError(error) {
|
|
107
|
+
return error instanceof BrowserError ? error : new BrowserError('INTERNAL', String(error?.message || error));
|
|
108
|
+
}
|
|
109
|
+
export function checkAbort(signal) {
|
|
110
|
+
if (signal?.aborted) throw new BrowserError('ABORTED', '操作已取消');
|
|
111
|
+
}
|
|
112
|
+
export function abortable(promise, signal) {
|
|
113
|
+
checkAbort(signal);
|
|
114
|
+
if (!signal) return promise;
|
|
115
|
+
return new Promise((resolve, reject) => {
|
|
116
|
+
const abort = () => { cleanup(); reject(new BrowserError('ABORTED', '操作已取消')); };
|
|
117
|
+
const cleanup = () => signal.removeEventListener('abort', abort);
|
|
118
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
119
|
+
Promise.resolve(promise).then((value) => { cleanup(); resolve(value); }, (error) => { cleanup(); reject(error); });
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
const sensitive = /token|key|secret|password|auth|code|session/i;
|
|
123
|
+
export function redactUrl(raw) {
|
|
124
|
+
try {
|
|
125
|
+
const url = new URL(raw);
|
|
126
|
+
if (url.username) url.username = '***';
|
|
127
|
+
if (url.password) url.password = '***';
|
|
128
|
+
for (const key of [...url.searchParams.keys()]) if (sensitive.test(key)) url.searchParams.set(key, '***');
|
|
129
|
+
if (url.hash) url.hash = '***';
|
|
130
|
+
return url.toString();
|
|
131
|
+
} catch { return String(raw).replace(/\b(token|key|secret|password|auth|code|session)=[^&\s]*/gi, '$1=***'); }
|
|
132
|
+
}
|
|
133
|
+
export function redactText(value) {
|
|
134
|
+
return String(value).replace(/https?:\/\/[^\s<>"']+/gi, (url) => redactUrl(url));
|
|
135
|
+
}
|
|
136
|
+
function blockedAddress(address, allowPrivate) {
|
|
137
|
+
let value = address.toLowerCase().replace(/^\[|\]$/g, '');
|
|
138
|
+
if (value.startsWith('::ffff:')) {
|
|
139
|
+
const tail = value.slice(7);
|
|
140
|
+
if (isIP(tail) === 4) value = tail;
|
|
141
|
+
else {
|
|
142
|
+
const parts = tail.split(':');
|
|
143
|
+
if (parts.length === 2) {
|
|
144
|
+
const n = parseInt(parts[0], 16) * 65536 + parseInt(parts[1], 16);
|
|
145
|
+
value = [n >>> 24, (n >>> 16) & 255, (n >>> 8) & 255, n & 255].join('.');
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (isIP(value) === 4) {
|
|
150
|
+
const [a, b] = value.split('.').map(Number);
|
|
151
|
+
if (a === 0 || (a === 169 && b === 254) || a >= 224) return true;
|
|
152
|
+
return !allowPrivate && (a === 10 || (a === 172 && b >= 16 && b <= 31) || (a === 192 && b === 168));
|
|
153
|
+
}
|
|
154
|
+
if (isIP(value) === 6) {
|
|
155
|
+
if (value === '::' || /^ff/i.test(value) || /^fe[89ab]/i.test(value)) return true;
|
|
156
|
+
return !allowPrivate && /^f[cd]/i.test(value);
|
|
157
|
+
}
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
export async function validateTarget(raw, { origins = [], allowPrivate = process.env.DSH_UI_TESTING_ALLOW_PRIVATE === '1', signal, resolve = lookup } = {}) {
|
|
161
|
+
checkAbort(signal);
|
|
162
|
+
let url;
|
|
163
|
+
try { url = new URL(raw); } catch { throw new BrowserError('INVALID_ARGUMENT', '地址不是合法 URL'); }
|
|
164
|
+
if (!['http:', 'https:'].includes(url.protocol)) throw new BrowserError('INVALID_ARGUMENT', '只支持 http/https 地址');
|
|
165
|
+
if (url.username || url.password) throw new BrowserError('INVALID_ARGUMENT', '导航地址不能包含凭据');
|
|
166
|
+
const blockedOrigins = [...origins, process.env.DSH_WEB_URL].filter(Boolean).map((origin) => {
|
|
167
|
+
try { return new URL(origin).origin; } catch { return null; }
|
|
168
|
+
});
|
|
169
|
+
if (blockedOrigins.includes(url.origin)) throw new BrowserError('INVALID_ARGUMENT', '不能导航到 DSH 自身 origin');
|
|
170
|
+
const hostname = url.hostname.replace(/^\[|\]$/g, '');
|
|
171
|
+
let addresses;
|
|
172
|
+
try {
|
|
173
|
+
// `resolve` is an injection point for probes (mixed public/private DNS, zero-dispatch
|
|
174
|
+
// scenarios). Production always uses the real `dns/promises` lookup imported above.
|
|
175
|
+
addresses = isIP(hostname) ? [{ address: hostname }] : await abortable(resolve(hostname, { all: true }), signal);
|
|
176
|
+
} catch (error) {
|
|
177
|
+
if (error instanceof BrowserError) throw error;
|
|
178
|
+
throw new BrowserError('NAV_FAILED', '导航域名解析失败', { url: redactUrl(raw) });
|
|
179
|
+
}
|
|
180
|
+
if (!addresses.length || addresses.some(({ address }) => blockedAddress(address, allowPrivate))) {
|
|
181
|
+
throw new BrowserError('INVALID_ARGUMENT', '导航目标地址不在允许范围', { url: redactUrl(raw) });
|
|
182
|
+
}
|
|
183
|
+
return url.href;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Minimal cancellable CDP transport. Each settlement removes its timer and abort listener. */
|
|
187
|
+
export class Cdp {
|
|
188
|
+
constructor(url) {
|
|
189
|
+
this.url = url; this.nextId = 1; this.pending = new Map(); this.listeners = new Set(); this.closed = false;
|
|
190
|
+
}
|
|
191
|
+
connect({ signal, timeoutMs = 30000 } = {}) {
|
|
192
|
+
checkAbort(signal);
|
|
193
|
+
return new Promise((resolve, reject) => {
|
|
194
|
+
const ws = new WebSocket(this.url);
|
|
195
|
+
this.ws = ws;
|
|
196
|
+
let settled = false;
|
|
197
|
+
const finish = (error) => {
|
|
198
|
+
if (settled) return;
|
|
199
|
+
settled = true;
|
|
200
|
+
clearTimeout(timer); signal?.removeEventListener('abort', abort);
|
|
201
|
+
if (error) { this.close(); reject(error); } else resolve(this);
|
|
202
|
+
};
|
|
203
|
+
const abort = () => finish(new BrowserError('ABORTED', 'CDP 连接已取消'));
|
|
204
|
+
const timer = setTimeout(() => finish(new BrowserError('CDP_TIMEOUT', 'CDP 连接超时')), timeoutMs);
|
|
205
|
+
signal?.addEventListener('abort', abort, { once: true });
|
|
206
|
+
ws.onopen = () => finish();
|
|
207
|
+
ws.onerror = () => finish(new BrowserError('CDP_DISCONNECTED', 'CDP 连接失败'));
|
|
208
|
+
ws.onclose = () => {
|
|
209
|
+
this.closed = true;
|
|
210
|
+
finish(new BrowserError('CDP_DISCONNECTED', 'CDP 连接已关闭'));
|
|
211
|
+
this.rejectPending();
|
|
212
|
+
};
|
|
213
|
+
ws.onmessage = (event) => {
|
|
214
|
+
let message;
|
|
215
|
+
try { message = JSON.parse(event.data); } catch { return; }
|
|
216
|
+
if (message.id !== undefined) {
|
|
217
|
+
const entry = this.pending.get(message.id);
|
|
218
|
+
if (entry) entry.finish(message.error ? new BrowserError('INTERNAL', message.error.message || 'CDP error') : null, message.result);
|
|
219
|
+
} else for (const listener of this.listeners) listener(message);
|
|
220
|
+
};
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
send(method, params = {}, { signal, timeoutMs = 30000, sessionId } = {}) {
|
|
224
|
+
if (this.closed) return Promise.reject(new BrowserError('CDP_DISCONNECTED', 'CDP 连接已关闭', { method }));
|
|
225
|
+
if (signal?.aborted) return Promise.reject(new BrowserError('ABORTED', '操作已取消'));
|
|
226
|
+
const id = this.nextId++;
|
|
227
|
+
return new Promise((resolve, reject) => {
|
|
228
|
+
const abort = () => finish(new BrowserError('ABORTED', '操作已取消', { method }));
|
|
229
|
+
const finish = (error, value) => {
|
|
230
|
+
if (!this.pending.delete(id)) return;
|
|
231
|
+
clearTimeout(timer); signal?.removeEventListener('abort', abort);
|
|
232
|
+
if (error) reject(error); else resolve(value);
|
|
233
|
+
};
|
|
234
|
+
const timer = setTimeout(() => finish(new BrowserError('CDP_TIMEOUT', 'CDP 超时: ' + method)), timeoutMs);
|
|
235
|
+
this.pending.set(id, { finish, timer, method });
|
|
236
|
+
signal?.addEventListener('abort', abort, { once: true });
|
|
237
|
+
try { this.ws.send(JSON.stringify({ id, method, params, ...(sessionId ? { sessionId } : {}) })); }
|
|
238
|
+
catch {
|
|
239
|
+
finish(new BrowserError('CDP_DISCONNECTED', 'CDP 发送失败'));
|
|
240
|
+
this.close();
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
rejectPending() {
|
|
245
|
+
for (const entry of [...this.pending.values()]) entry.finish(new BrowserError('CDP_DISCONNECTED', 'CDP 连接已关闭', { method: entry.method }));
|
|
246
|
+
}
|
|
247
|
+
on(listener) { this.listeners.add(listener); return () => this.listeners.delete(listener); }
|
|
248
|
+
close() { this.closed = true; this.rejectPending(); try { this.ws?.close(); } catch { /* already closed */ } }
|
|
249
|
+
}
|