dsh-browser-verify 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.
- package/README.md +9 -15
- package/README.zh.md +21 -31
- package/cordis.patch.yml +7 -13
- package/lib/cli.js +1 -1
- package/lib/{driver-DxMldTYf.js → driver-Dwuu4MgP.js} +35 -1
- package/lib/index.js +3 -3
- package/lib/types/browser/scenario.d.ts +9 -0
- package/package.json +5 -1
- package/src/browser/scenario.ts +41 -1
- package/src/tools/index.ts +2 -2
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ verification is just tool calls.
|
|
|
18
18
|
## Quick start
|
|
19
19
|
|
|
20
20
|
```sh
|
|
21
|
-
dsh plugin --profile web add dsh-browser-verify@0.1.
|
|
21
|
+
dsh plugin --profile web add dsh-browser-verify@0.1.3
|
|
22
22
|
```
|
|
23
23
|
|
|
24
24
|
1. **Install** with the command above (or see [Install](#install)).
|
|
@@ -44,7 +44,7 @@ state?"; add `browser_screenshot` when you need to see the layout, or
|
|
|
44
44
|
|
|
45
45
|
| Tool | Purpose |
|
|
46
46
|
|---|---|
|
|
47
|
-
| `browser_open` | Open a URL in a fresh scenario (headless Chromium, default viewport 390×844 @2x) and report title / HTTP status / visible-text summary / console errors. Optional `waitSelector` waits for a key element before returning, and optional inline `mocks` intercept APIs **before** the first navigation — for pages that boot against mocked data. |
|
|
47
|
+
| `browser_open` | Open a URL in a fresh scenario (headless Chromium, default viewport 390×844 @2x) and report title / HTTP status / visible-text summary / console errors. Without `waitSelector` it waits for the page to render-settle (two identical consecutive visible-text samples, capped at ~3s) before snapshotting, so it never returns the boot/skeleton frame; loading-state noise (`加载中...` etc.) is filtered out of the summary. Optional `waitSelector` waits for a key element before returning, and optional inline `mocks` intercept APIs **before** the first navigation — for pages that boot against mocked data. |
|
|
48
48
|
| `browser_mock` | Register a playwright-glob route (`**/api/*.do*`) returning your JSON, then auto-reload the page to show the mocked state — the quickest way to verify empty / error / abnormal states without touching the backend. Duplicate patterns are rejected with a hint. |
|
|
49
49
|
| `browser_assert` | The cheapest and most precise check: wait for a CSS selector, verify its count and contained text, return `{pass, count, actualText, elapsedMs}`. A mismatch is `pass:false` (with the diff), never a throw — so failure is a first-class result, not an error you debug. |
|
|
50
50
|
| `browser_screenshot` | Capture the current page (viewport or full page) and **auto-project the image block into the model context** — the model sees the layout without any file handling. Reports dimensions, sha256, and `identicalToPrevious:true` when the shot is byte-identical to the previous one (page probably not refreshed). |
|
|
@@ -69,7 +69,7 @@ browser_screenshot
|
|
|
69
69
|
## Install
|
|
70
70
|
|
|
71
71
|
```sh
|
|
72
|
-
dsh plugin --profile web add dsh-browser-verify@0.1.
|
|
72
|
+
dsh plugin --profile web add dsh-browser-verify@0.1.3
|
|
73
73
|
```
|
|
74
74
|
|
|
75
75
|
The version is pinned on purpose: pnpm 11 holds back packages published in the
|
|
@@ -92,17 +92,6 @@ or point the plugin at an existing binary via `DSH_BROWSER_VERIFY_CHROMIUM`
|
|
|
92
92
|
(see [Environment variables](#environment-variables)). Without either, the
|
|
93
93
|
first `browser_open` fails with an actionable install hint.
|
|
94
94
|
|
|
95
|
-
### From a source checkout
|
|
96
|
-
|
|
97
|
-
```sh
|
|
98
|
-
pnpm install && pnpm build && pnpm pack # → dsh-browser-verify-<version>.tgz
|
|
99
|
-
dsh plugin --profile web add ./dsh-browser-verify-<version>.tgz
|
|
100
|
-
# or: dsh plugin --profile web add /path/to/dsh-browser-verify (build first — lib/ is gitignored)
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
The tarball ships a prebuilt `lib/`, so no build step runs on the installing
|
|
104
|
-
machine.
|
|
105
|
-
|
|
106
95
|
## Environment variables
|
|
107
96
|
|
|
108
97
|
| Variable | Default | Meaning |
|
|
@@ -131,7 +120,7 @@ machine.
|
|
|
131
120
|
|
|
132
121
|
## Testing status
|
|
133
122
|
|
|
134
|
-
|
|
123
|
+
41 unit tests (fully offline — no browser needed), strict typecheck, and a
|
|
135
124
|
per-file ≥90% statement coverage gate. Verified **end-to-end in the real DSH
|
|
136
125
|
web GUI** on dsh 0.1.2-alpha.4: a two-state loop (empty + normal) against a
|
|
137
126
|
live uni-app H5 (hhhweb) in 6 tool calls, with screenshots auto-projected and
|
|
@@ -163,6 +152,11 @@ CLI (harness-free debug path): `node lib/cli.js --url <u> [--mock <file.json>]
|
|
|
163
152
|
[--wait-selector <sel>] [--assert <sel>] [--screenshot] [--persist <dir>]
|
|
164
153
|
[--viewport <WxH>]`.
|
|
165
154
|
|
|
155
|
+
Contributors can install the local build with
|
|
156
|
+
`dsh plugin --profile web add ./dsh-browser-verify-<version>.tgz` after
|
|
157
|
+
`pnpm build`; the tarball ships a prebuilt `lib/`, so no build step runs on
|
|
158
|
+
the installing machine.
|
|
159
|
+
|
|
166
160
|
## License & attribution
|
|
167
161
|
|
|
168
162
|
Apache-2.0. Architecture and implementation notes for agents and
|
package/README.zh.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
[English](README.md) | 中文
|
|
4
4
|
|
|
5
5
|
[](LICENSE)
|
|
6
|
-
[](package.json)
|
|
6
|
+
[](package.json)
|
|
7
7
|
|
|
8
8
|
**给你的 DeepSeek Harness 一双看网页的眼睛** —— 只读浏览器验证,≤4 次工具调用完成:打开、mock、断言、截图。
|
|
9
9
|
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
## 快速上手
|
|
13
13
|
|
|
14
14
|
```sh
|
|
15
|
-
dsh plugin --profile web add dsh-browser-verify@0.1.
|
|
15
|
+
dsh plugin --profile web add dsh-browser-verify@0.1.3
|
|
16
16
|
```
|
|
17
17
|
|
|
18
18
|
1. **安装**(更多方式见 [安装](#安装))。
|
|
@@ -34,12 +34,12 @@ browser_assert selector=".empty-wrap" text="暂无可用缴费服务"
|
|
|
34
34
|
|
|
35
35
|
## 它做什么
|
|
36
36
|
|
|
37
|
-
| 工具
|
|
38
|
-
|
|
39
|
-
| `browser_open`
|
|
40
|
-
| `browser_mock`
|
|
41
|
-
| `browser_assert`
|
|
42
|
-
| `browser_screenshot` | 截取当前页面(视口或整页),**自动以 image block 投影到模型上下文**——模型直接"看见"版式,无需任何文件处理。返回尺寸、sha256;与上一张完全一致时 `identicalToPrevious:true`(疑似页面未刷新)。
|
|
37
|
+
| 工具 | 用途 |
|
|
38
|
+
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
39
|
+
| `browser_open` | 在全新场景中打开 URL(无头 Chromium,默认视口 390×844 @2x),返回 title / HTTP 状态 / 可见文本摘要 / console 错误。不传 `waitSelector` 时默认等待页面**渲染稳定**后再采样(连续两次相同可见文本即稳定,上限约 3s),不会采到启动骨架帧;`加载中...` 等加载态文案自动过滤。可选 `waitSelector` 等待关键元素出现后再返回;可选内联 `mocks` 在**首次导航前**拦截接口——用于一启动就依赖 mock 数据的页面。 |
|
|
40
|
+
| `browser_mock` | 注册 playwright glob 路由(如`**/api/*.do*`)返回指定 JSON,并自动 reload 页面展示 mock 状态——不碰后端最快验证空态/异常态。重复 pattern 会提示报错。 |
|
|
41
|
+
| `browser_assert` | 最省 token 也最精确的验证:等待 CSS 选择器出现,校验数量与包含文本,返回`{pass, count, actualText, elapsedMs}`。不满足时返回 `pass:false`(附差异),**绝不抛错**——失败是一等公民的结果,而不是要调试的异常。 |
|
|
42
|
+
| `browser_screenshot` | 截取当前页面(视口或整页),**自动以 image block 投影到模型上下文**——模型直接"看见"版式,无需任何文件处理。返回尺寸、sha256;与上一张完全一致时 `identicalToPrevious:true`(疑似页面未刷新)。 |
|
|
43
43
|
|
|
44
44
|
先 `browser_assert` 再 `browser_screenshot`:断言更便宜,截图留给必须判断
|
|
45
45
|
渲染效果的时刻。
|
|
@@ -61,11 +61,10 @@ browser_screenshot
|
|
|
61
61
|
## 安装
|
|
62
62
|
|
|
63
63
|
```sh
|
|
64
|
-
dsh plugin --profile web add dsh-browser-verify@0.1.
|
|
64
|
+
dsh plugin --profile web add dsh-browser-verify@0.1.3
|
|
65
65
|
```
|
|
66
66
|
|
|
67
|
-
版本故意钉死:pnpm 11 会暂缓 24 小时内新发布的包,裸写 `add
|
|
68
|
-
dsh-browser-verify`(latest)会在发布当天装到上一个版本。`--profile web`
|
|
67
|
+
版本故意钉死:pnpm 11 会暂缓 24 小时内新发布的包,裸写 `add dsh-browser-verify`(latest)会在发布当天装到上一个版本。`--profile web`
|
|
69
68
|
是本部署的 GUI profile——如果不同请换成你自己的 profile 名。
|
|
70
69
|
|
|
71
70
|
要求 **dsh ≥ 0.1.2-alpha.1**。
|
|
@@ -81,23 +80,13 @@ npx playwright install chromium # 需要 playwright-core@1.62.0
|
|
|
81
80
|
或通过 `DSH_BROWSER_VERIFY_CHROMIUM` 指向已有二进制(见
|
|
82
81
|
[环境变量](#环境变量))。两者都没有时,第一次 `browser_open` 会给出可操作的安装提示。
|
|
83
82
|
|
|
84
|
-
### 源码 checkout 安装
|
|
85
|
-
|
|
86
|
-
```sh
|
|
87
|
-
pnpm install && pnpm build && pnpm pack # → dsh-browser-verify-<version>.tgz
|
|
88
|
-
dsh plugin --profile web add ./dsh-browser-verify-<version>.tgz
|
|
89
|
-
# 或:dsh plugin --profile web add /path/to/dsh-browser-verify (需先 build——lib/ 不入库)
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
tgz 内已含预构建 `lib/`,安装机无需再构建。
|
|
93
|
-
|
|
94
83
|
## 环境变量
|
|
95
84
|
|
|
96
|
-
| 变量
|
|
97
|
-
|
|
98
|
-
| `DSH_BROWSER_VERIFY_CHROMIUM` | *(未设置)* | Chromium 完整路径;优先于缓存探测。路径错误时启动即报可操作提示。
|
|
99
|
-
| `DSH_BROWSER_VERIFY_TIMEOUT`
|
|
100
|
-
| `DSH_BROWSER_VERIFY_IDLE_MS`
|
|
85
|
+
| 变量 | 默认 | 含义 |
|
|
86
|
+
| ------------------------------- | ------------ | -------------------------------------------------------------------------------- |
|
|
87
|
+
| `DSH_BROWSER_VERIFY_CHROMIUM` | *(未设置)* | Chromium 完整路径;优先于缓存探测。路径错误时启动即报可操作提示。 |
|
|
88
|
+
| `DSH_BROWSER_VERIFY_TIMEOUT` | `10000` | `browser_open` 页面加载路径的墙钟预算(ms),含 wait-selector 与 mock reload。 |
|
|
89
|
+
| `DSH_BROWSER_VERIFY_IDLE_MS` | `600000` | 空闲回收窗口(ms),超时后浏览器实例自动关闭;插件 dispose 时强制清理。 |
|
|
101
90
|
|
|
102
91
|
## 可靠性与清理
|
|
103
92
|
|
|
@@ -108,12 +97,11 @@ tgz 内已含预构建 `lib/`,安装机无需再构建。
|
|
|
108
97
|
```sh
|
|
109
98
|
rm -rf "$(node -p 'require("os").tmpdir()')/dsh-browser-verify-*"
|
|
110
99
|
```
|
|
111
|
-
|
|
112
100
|
- **错误即接口**——所有错误以 `browser-verify: ` 前缀开头、以可操作建议结尾;验证"失败"是结果(`pass:false`),不是异常。
|
|
113
101
|
|
|
114
102
|
## 测试状态
|
|
115
103
|
|
|
116
|
-
|
|
104
|
+
41 个单测(完全离线,无需浏览器)、严格 typecheck、每文件 ≥90% 语句覆盖率闸门。已在 **dsh 0.1.2-alpha.4 真实 GUI 端到端验证**:对一个真实 uni-app H5(hhhweb),空态+正常态两态闭环共 6 次调用,截图自动投影,无临时目录残留、无僵尸进程。
|
|
117
105
|
|
|
118
106
|
## 已知限制
|
|
119
107
|
|
|
@@ -133,9 +121,11 @@ pnpm vitest run --coverage # discover / cleanup / attachments 每文件 ≥90%
|
|
|
133
121
|
scripts/smoke.sh # 两态端到端;需要参考应用 dev server 在 :5173
|
|
134
122
|
```
|
|
135
123
|
|
|
136
|
-
CLI(免 harness 的调试路径):`node lib/cli.js --url <u> [--mock <file.json>]
|
|
137
|
-
|
|
138
|
-
|
|
124
|
+
CLI(免 harness 的调试路径):`node lib/cli.js --url <u> [--mock <file.json>] [--wait-selector <sel>] [--assert <sel>] [--screenshot] [--persist <dir>] [--viewport <WxH>]`。
|
|
125
|
+
|
|
126
|
+
贡献者可本地构建后安装:`pnpm build` 后
|
|
127
|
+
`dsh plugin --profile web add ./dsh-browser-verify-<version>.tgz`——tgz 内已含
|
|
128
|
+
预构建 `lib/`,安装机无需再构建。
|
|
139
129
|
|
|
140
130
|
## 许可与署名
|
|
141
131
|
|
package/cordis.patch.yml
CHANGED
|
@@ -1,17 +1,11 @@
|
|
|
1
|
-
# dsh-browser-verify bundle patch:
|
|
2
|
-
# tools
|
|
3
|
-
#
|
|
1
|
+
# dsh-browser-verify bundle patch: registers the four read-only browser
|
|
2
|
+
# verification tools (browser_open / browser_mock / browser_assert /
|
|
3
|
+
# browser_screenshot) in the profile's tool roster. Applied automatically as a
|
|
4
|
+
# profile bundle layer (dsh.bundle.patch in package.json).
|
|
4
5
|
#
|
|
5
|
-
# Install
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
# # or, during development, add the checkout directory directly (pnpm link):
|
|
9
|
-
# dsh plugin --profile web add <path-to-this-directory>
|
|
10
|
-
#
|
|
11
|
-
# Host half only: the node half (exports ".") registers browser_open /
|
|
12
|
-
# browser_mock / browser_assert / browser_screenshot. No client half, no
|
|
13
|
-
# settings card. Configuration is env-var only (DSH_BROWSER_VERIFY_*); do NOT
|
|
14
|
-
# add a `config:` block here.
|
|
6
|
+
# Install: dsh plugin --profile web add dsh-browser-verify@<version>
|
|
7
|
+
# Host half only — no client half, no settings card. Configuration is env-var
|
|
8
|
+
# only (DSH_BROWSER_VERIFY_*); do NOT add a `config:` block here.
|
|
15
9
|
- insert:
|
|
16
10
|
- id: browser-verify
|
|
17
11
|
name: 'dsh-browser-verify'
|
package/lib/cli.js
CHANGED
|
@@ -91,6 +91,19 @@ const MAX_VISIBLE = 8;
|
|
|
91
91
|
const MAX_VISIBLE_LEN = 40;
|
|
92
92
|
const MAX_ERRORS = 5;
|
|
93
93
|
const MAX_ERROR_LEN = 120;
|
|
94
|
+
/** Poll interval and cap for the default render-settled wait (no waitSelector). */
|
|
95
|
+
const SETTLE_INTERVAL_MS = 250;
|
|
96
|
+
const SETTLE_CAP_MS = 3e3;
|
|
97
|
+
/**
|
|
98
|
+
* Loading-state noise (uni-app showLoading / boot toasts) filtered out of the
|
|
99
|
+
* visible summary. Anchored full-string patterns only, so business states like
|
|
100
|
+
* 加载失败 / 加载更多 are never affected.
|
|
101
|
+
*/
|
|
102
|
+
const NOISE_TEXT_PATTERN = /^(加载中|正在加载|请稍候|loading)[.…]{0,3}$/i;
|
|
103
|
+
/** True for transient loading-state text that should not pollute the summary. */
|
|
104
|
+
function isNoiseText(text) {
|
|
105
|
+
return NOISE_TEXT_PATTERN.test(text);
|
|
106
|
+
}
|
|
94
107
|
function assertNoMockConflict(patterns, next) {
|
|
95
108
|
if (patterns.includes(next)) throw new Error(`browser-verify: 拦截 pattern 已存在: ${next}(已有: ${patterns.join(", ")})。请先 browser_open 重开场景或用不同的 urlPattern。`);
|
|
96
109
|
}
|
|
@@ -110,7 +123,7 @@ function summarizeVisibleText(texts) {
|
|
|
110
123
|
const out = [];
|
|
111
124
|
for (const raw of texts) {
|
|
112
125
|
const trimmed = raw.trim();
|
|
113
|
-
if (trimmed === "") continue;
|
|
126
|
+
if (trimmed === "" || isNoiseText(trimmed)) continue;
|
|
114
127
|
const reduced = trimmed.length > MAX_VISIBLE_LEN ? trimmed.slice(0, MAX_VISIBLE_LEN) : trimmed;
|
|
115
128
|
if (seen.has(reduced)) continue;
|
|
116
129
|
seen.add(reduced);
|
|
@@ -159,6 +172,7 @@ var Scenario = class {
|
|
|
159
172
|
timeout
|
|
160
173
|
});
|
|
161
174
|
if (opts.waitSelector !== void 0) await this.page.waitForSelector(opts.waitSelector, { timeout });
|
|
175
|
+
else await this.waitUntilRendered(timeout);
|
|
162
176
|
const texts = await this.page.evaluate(VISIBLE_TEXT_SCRIPT);
|
|
163
177
|
return {
|
|
164
178
|
title: await this.page.title(),
|
|
@@ -169,6 +183,26 @@ var Scenario = class {
|
|
|
169
183
|
elapsedMs: Date.now() - started
|
|
170
184
|
};
|
|
171
185
|
}
|
|
186
|
+
/**
|
|
187
|
+
* Default settle wait: two consecutive identical non-empty visible-text
|
|
188
|
+
* samples (250ms apart) mean the render stopped changing. Bounded by
|
|
189
|
+
* min(timeoutMs, 3s) — pages that never settle (polling/animations) fall
|
|
190
|
+
* through to the current snapshot instead of burning the whole budget.
|
|
191
|
+
*/
|
|
192
|
+
async waitUntilRendered(timeoutMs) {
|
|
193
|
+
const cap = Math.min(timeoutMs, SETTLE_CAP_MS);
|
|
194
|
+
const deadline = Date.now() + cap;
|
|
195
|
+
let prev = null;
|
|
196
|
+
for (;;) {
|
|
197
|
+
const sig = (await this.page.evaluate(VISIBLE_TEXT_SCRIPT)).join("\0");
|
|
198
|
+
if (sig !== "") {
|
|
199
|
+
if (prev !== null && sig === prev) return;
|
|
200
|
+
prev = sig;
|
|
201
|
+
}
|
|
202
|
+
if (Date.now() >= deadline) return;
|
|
203
|
+
await new Promise((resolve) => setTimeout(resolve, SETTLE_INTERVAL_MS));
|
|
204
|
+
}
|
|
205
|
+
}
|
|
172
206
|
async addMock(rule) {
|
|
173
207
|
assertNoMockConflict([...this.mocks.keys()], rule.urlPattern);
|
|
174
208
|
const status = rule.status ?? 200;
|
package/lib/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as BrowserDriver } from "./driver-
|
|
1
|
+
import { t as BrowserDriver } from "./driver-Dwuu4MgP.js";
|
|
2
2
|
import { exec } from "node:child_process";
|
|
3
3
|
import { readdir, rm, stat } from "node:fs/promises";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
@@ -111,7 +111,7 @@ function registerBrowserTools(ctx) {
|
|
|
111
111
|
});
|
|
112
112
|
ctx.tools.register(defineTool({
|
|
113
113
|
name: "browser_open",
|
|
114
|
-
description: "在无头浏览器中打开一个页面并返回页面状态(标题/状态码/可见文本摘要/console 错误)用于验证前端页面;可选 waitSelector
|
|
114
|
+
description: "在无头浏览器中打开一个页面并返回页面状态(标题/状态码/可见文本摘要/console 错误)用于验证前端页面;可选 waitSelector 等待关键元素出现(不传时自动等待页面渲染稳定后返回,加载态文案如「加载中...」自动过滤),默认视口 390×844 @2x(移动端形态)。可传 mocks 在打开时拦截接口(用于启动即依赖接口数据的页面)。验证顺序:先 browser_assert 做 DOM 断言,确需看版式再 browser_screenshot。",
|
|
115
115
|
parameters: {
|
|
116
116
|
url: {
|
|
117
117
|
type: "string",
|
|
@@ -165,7 +165,7 @@ function registerBrowserTools(ctx) {
|
|
|
165
165
|
},
|
|
166
166
|
waitSelector: {
|
|
167
167
|
type: "string",
|
|
168
|
-
description: "
|
|
168
|
+
description: "可选:等待该选择器出现后再返回;不传则自动等待页面渲染稳定后返回(最多数秒)"
|
|
169
169
|
},
|
|
170
170
|
timeoutMs: {
|
|
171
171
|
type: "number",
|
|
@@ -23,6 +23,8 @@ export interface AssertResult {
|
|
|
23
23
|
actualText: string | null;
|
|
24
24
|
elapsedMs: number;
|
|
25
25
|
}
|
|
26
|
+
/** True for transient loading-state text that should not pollute the summary. */
|
|
27
|
+
export declare function isNoiseText(text: string): boolean;
|
|
26
28
|
export declare function assertNoMockConflict(patterns: string[], next: string): void;
|
|
27
29
|
export declare function normalizeCountSpec(count: number | {
|
|
28
30
|
min: number;
|
|
@@ -48,6 +50,13 @@ export declare class Scenario {
|
|
|
48
50
|
waitSelector?: string;
|
|
49
51
|
timeoutMs?: number;
|
|
50
52
|
}): Promise<OpenResult>;
|
|
53
|
+
/**
|
|
54
|
+
* Default settle wait: two consecutive identical non-empty visible-text
|
|
55
|
+
* samples (250ms apart) mean the render stopped changing. Bounded by
|
|
56
|
+
* min(timeoutMs, 3s) — pages that never settle (polling/animations) fall
|
|
57
|
+
* through to the current snapshot instead of burning the whole budget.
|
|
58
|
+
*/
|
|
59
|
+
private waitUntilRendered;
|
|
51
60
|
addMock(rule: {
|
|
52
61
|
urlPattern: string;
|
|
53
62
|
json: unknown;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-browser-verify",
|
|
3
3
|
"description": "Read-only browser verification tools for the DeepSeek Harness web GUI: browser_open / browser_mock / browser_assert / browser_screenshot — verify a page (H5/desktop) in ≤4 tool calls with mock interception, DOM assertions, and screenshots that auto-project into the model context.",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.3",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "pnpm@11.7.0",
|
|
7
7
|
"engines": {
|
|
@@ -31,6 +31,10 @@
|
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"playwright-core": "1.62.0"
|
|
33
33
|
},
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"@deepseek-ai/dsh-attachment": "^0.1.2-alpha.4",
|
|
36
|
+
"@deepseek-ai/dsh-tools": "^0.1.2-alpha.4"
|
|
37
|
+
},
|
|
34
38
|
"devDependencies": {
|
|
35
39
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
36
40
|
"@deepseek-ai/dsh-attachment": "^0.1.2-alpha.4",
|
package/src/browser/scenario.ts
CHANGED
|
@@ -34,6 +34,20 @@ const MAX_VISIBLE_LEN = 40
|
|
|
34
34
|
const MAX_ERRORS = 5
|
|
35
35
|
const MAX_ERROR_LEN = 120
|
|
36
36
|
const MAX_DIFF_LEN = 120
|
|
37
|
+
/** Poll interval and cap for the default render-settled wait (no waitSelector). */
|
|
38
|
+
const SETTLE_INTERVAL_MS = 250
|
|
39
|
+
const SETTLE_CAP_MS = 3000
|
|
40
|
+
/**
|
|
41
|
+
* Loading-state noise (uni-app showLoading / boot toasts) filtered out of the
|
|
42
|
+
* visible summary. Anchored full-string patterns only, so business states like
|
|
43
|
+
* 加载失败 / 加载更多 are never affected.
|
|
44
|
+
*/
|
|
45
|
+
const NOISE_TEXT_PATTERN = /^(加载中|正在加载|请稍候|loading)[.…]{0,3}$/i
|
|
46
|
+
|
|
47
|
+
/** True for transient loading-state text that should not pollute the summary. */
|
|
48
|
+
export function isNoiseText(text: string): boolean {
|
|
49
|
+
return NOISE_TEXT_PATTERN.test(text)
|
|
50
|
+
}
|
|
37
51
|
|
|
38
52
|
export function assertNoMockConflict(patterns: string[], next: string): void {
|
|
39
53
|
if (patterns.includes(next)) {
|
|
@@ -52,7 +66,7 @@ export function summarizeVisibleText(texts: string[]): string[] {
|
|
|
52
66
|
const out: string[] = []
|
|
53
67
|
for (const raw of texts) {
|
|
54
68
|
const trimmed = raw.trim()
|
|
55
|
-
if (trimmed === '') continue
|
|
69
|
+
if (trimmed === '' || isNoiseText(trimmed)) continue
|
|
56
70
|
const reduced = trimmed.length > MAX_VISIBLE_LEN ? trimmed.slice(0, MAX_VISIBLE_LEN) : trimmed
|
|
57
71
|
if (seen.has(reduced)) continue
|
|
58
72
|
seen.add(reduced)
|
|
@@ -106,6 +120,11 @@ export class Scenario {
|
|
|
106
120
|
const response = await this.page.goto(opts.url, { waitUntil: 'domcontentloaded', timeout })
|
|
107
121
|
if (opts.waitSelector !== undefined) {
|
|
108
122
|
await this.page.waitForSelector(opts.waitSelector, { timeout })
|
|
123
|
+
} else {
|
|
124
|
+
// No explicit selector: wait for the page to render-settle instead of
|
|
125
|
+
// snapshotting the boot frame (SPAs like uni-app show skeleton/loading
|
|
126
|
+
// right after domcontentloaded — a 500ms snapshot reads empty).
|
|
127
|
+
await this.waitUntilRendered(timeout)
|
|
109
128
|
}
|
|
110
129
|
const texts = await this.page.evaluate(VISIBLE_TEXT_SCRIPT) as string[]
|
|
111
130
|
return {
|
|
@@ -118,6 +137,27 @@ export class Scenario {
|
|
|
118
137
|
}
|
|
119
138
|
}
|
|
120
139
|
|
|
140
|
+
/**
|
|
141
|
+
* Default settle wait: two consecutive identical non-empty visible-text
|
|
142
|
+
* samples (250ms apart) mean the render stopped changing. Bounded by
|
|
143
|
+
* min(timeoutMs, 3s) — pages that never settle (polling/animations) fall
|
|
144
|
+
* through to the current snapshot instead of burning the whole budget.
|
|
145
|
+
*/
|
|
146
|
+
private async waitUntilRendered(timeoutMs: number): Promise<void> {
|
|
147
|
+
const cap = Math.min(timeoutMs, SETTLE_CAP_MS)
|
|
148
|
+
const deadline = Date.now() + cap
|
|
149
|
+
let prev: string | null = null
|
|
150
|
+
for (;;) {
|
|
151
|
+
const sig = (await this.page.evaluate(VISIBLE_TEXT_SCRIPT) as string[]).join('\u0000')
|
|
152
|
+
if (sig !== '') {
|
|
153
|
+
if (prev !== null && sig === prev) return
|
|
154
|
+
prev = sig
|
|
155
|
+
}
|
|
156
|
+
if (Date.now() >= deadline) return
|
|
157
|
+
await new Promise(resolve => setTimeout(resolve, SETTLE_INTERVAL_MS))
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
121
161
|
async addMock(rule: { urlPattern: string; json: unknown; status?: number; reload?: boolean; timeoutMs?: number }): Promise<string[]> {
|
|
122
162
|
assertNoMockConflict([...this.mocks.keys()], rule.urlPattern)
|
|
123
163
|
const status = rule.status ?? 200
|
package/src/tools/index.ts
CHANGED
|
@@ -24,7 +24,7 @@ export function registerBrowserTools(ctx: Context): void {
|
|
|
24
24
|
|
|
25
25
|
ctx.tools.register(defineTool({
|
|
26
26
|
name: 'browser_open',
|
|
27
|
-
description: '在无头浏览器中打开一个页面并返回页面状态(标题/状态码/可见文本摘要/console 错误)用于验证前端页面;可选 waitSelector
|
|
27
|
+
description: '在无头浏览器中打开一个页面并返回页面状态(标题/状态码/可见文本摘要/console 错误)用于验证前端页面;可选 waitSelector 等待关键元素出现(不传时自动等待页面渲染稳定后返回,加载态文案如「加载中...」自动过滤),默认视口 390×844 @2x(移动端形态)。可传 mocks 在打开时拦截接口(用于启动即依赖接口数据的页面)。验证顺序:先 browser_assert 做 DOM 断言,确需看版式再 browser_screenshot。',
|
|
28
28
|
parameters: {
|
|
29
29
|
url: { type: 'string', required: true, description: '页面地址,如 http://localhost:5173/hweb/pages/...' },
|
|
30
30
|
viewport: {
|
|
@@ -50,7 +50,7 @@ export function registerBrowserTools(ctx: Context): void {
|
|
|
50
50
|
},
|
|
51
51
|
description: '可选:页面启动前注册的接口拦截(glob urlPattern + json,如 [{urlPattern: "**/api/*.do*", json: {...}}])',
|
|
52
52
|
},
|
|
53
|
-
waitSelector: { type: 'string', description: '
|
|
53
|
+
waitSelector: { type: 'string', description: '可选:等待该选择器出现后再返回;不传则自动等待页面渲染稳定后返回(最多数秒)' },
|
|
54
54
|
timeoutMs: { type: 'number', description: `加载超时,默认 ${envTimeoutMs()}ms` },
|
|
55
55
|
},
|
|
56
56
|
output: {
|