dsh-browser-playwright-codex 0.6.5 → 0.7.0

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/CHANGELOG.md CHANGED
@@ -3,6 +3,55 @@
3
3
  Bilingual by intent: the version headings and the summary of each release are given in both
4
4
  languages, the individual entries follow the language the surrounding code and docs use.
5
5
 
6
+ ## 0.7.0 — 2026-10-19
7
+
8
+ 一句话摘要:**两个插件合成一个** —— 设置卡片并入本包;加载行从三行收成一行(这是 `dsh-client-modules` 允许本包带 client 模块的前提),独立的 `dsh-browser-toggle` 退役。
9
+
10
+ ### Breaking
11
+
12
+ - **一个包一行 loader**:`cordis.patch.yml` 现在只插 `- id: browser` / `name: 'dsh-browser-playwright-codex'`,三块配置收进嵌套的 `service` / `playwright` / `tool`。旧的 `browser-playwright`、`browser-tool` 行不再被读取;loader 整体替换 config,迁移时三节都要写全(设置卡会自动重写 home 补丁层里的那一块)。
13
+ - 版本跳到 0.7.0,因为配置形状变了。
14
+
15
+ ### Added
16
+
17
+ - **设置卡随本包发布**:`dsh.client` + `./client` 导出,卡片仍在 `/api/browser-playwright/*` 上提供总开关、页面识别方式、两个强力工具闸门与已登录站点名单。
18
+ - **单 loader 行守卫**(tests/plugin-entry.test.ts):`cordis.patch.yml` 不得再出现本包的子路径行,否则卡片会重新变成非法。
19
+
20
+ ### Changed
21
+
22
+ - **两个诊断工具改为门控**:`browser_console_messages` / `browser_network_requests` 不再常驻 —— 新增 `tool.diagnostics: false`(默认关),开启后才注册。常驻成本 **18 工具 12,755 字符 ≈ 3,543 token/轮 → 16 工具 11,005 字符 ≈ 3,057 token/轮(−486,−13.7%)**;全量注册仍是 21 工具 ≈ 4,384。
23
+ - **快照瘦身(运行时大头)**:无名无状态的 `generic` 包装层不再逐层输出(真实页面常把一个链接包在 7~8 层 div 里),显示用 `href` 去掉追踪参数(`utm_*` / `gclid` / `rsv_*` / `sa` 等)并截到 160 字符。百度结果页形状的样本:**6,169 → 2,557 字符(−58.6%)**,行数 108 → 24。refs 与点击行为不变 —— 省的是模型读到的东西。
24
+ - 闸门写入块改为**整行 config**(新标记 `# >>> dsh-browser:`),并兼容旧的 `dsh-browser-toggle` 标记就地替换 —— 避免同一个 id 被两段 patch 争抢。
25
+ - `scripts/shipped-gates.mjs` 按新行名读取出厂闸门;`scripts/build-client.mjs` 把 `client/client.js` 复制进 `lib/`。
26
+
27
+ ## 0.6.6 — 2026-10-17
28
+
29
+ 一句话摘要:**四份独立审查(文档漂移 / 代码对抗 / 发行打包 / 中性冷读)查出的问题集中修掉**——两条安全缺陷、几处"假成功/假失败"、随包自检的假承诺、发布门禁漏洞与一批文档漂移。
30
+
31
+ ### Security
32
+
33
+ - **点击白名单可被祖先 `<a>` 绕过**:`browser_click` 的 `ref` / `text` / `precise` 三条路径只校验**被点元素自身**的 href,外层 `<a href="http://外站/">` 包住它时点击会导航到站外并返回成功。现在改为「自身 → 最近祖先 `<a href>`(穿 shadow host)」,与裸坐标路径同层,统一交给 URL 策略。两处独立审查各自用本地 fixture 复现过。
34
+ - **密码/验证码/卡号曾进入 prompt**:`browser_extract` 把 `pageData()` 拼进辅助模型的 prompt,而 `pageData()` 采集输入框**当前值**——登录页的明文密码会随请求发出去(实测 prompt 里出现 `hunter2`)。现在 `type=password` 与 `autocomplete` 为 `current-password` / `new-password` / `one-time-code` / `cc-number` / `cc-csc` / `cc-exp` 的字段一律置空值,**字段本身保留**(模型仍知道"这里有个码要填")。
35
+
36
+ ### Fixed
37
+
38
+ - **填表核对两头错**:上一版把回读改成严格相等后,页面自动改写格式的输入框(全大写、千分位 `1,000`)被误判成"没写进去",而 `browser_fill` 的描述恰恰把 input-mask / currency 列为预期场景;更早的版本则相反,`ABCD→ZZZZ`、`John→Jane` 会被判成功。现在的归一化只放过"页面改写、不是我们写错":NFKC + 去大小写 + 去掉非字母数字;`1000≡1,000`、`abc≡ABC` 通过,`ABCD↔ZZZZ`、`1234↔12345` 仍然失败。contenteditable 也不再假失败(改读 `textContent`/`innerText`)。
39
+ - **挑战页识别的两处回归**(本轮自查引入的):给控件层加"页面得够空"的门控后,**正常登录页里嵌的真 reCAPTCHA / hCaptcha / Turnstile 完全不再识别**(登录页正是主要场景),已移除该门控;`CAPTCHA_ROLES` 曾删掉 `iframe`,导致名字就叫「验证码」的非厂商 iframe 不被识别,已恢复。
40
+ - **`[nothing here]` 提示三类误报**:静默页扫描深度 4 → 8 层(正文藏得深的正常页曾被判"几乎没有文字",与同一结果树里的正文自相矛盾);无标题的裸文档(`about:blank`、直接打开图片)不再提示;有挂起弹窗时不出这条(否则与 `[dialog]` 指令冲突)。
41
+ - **提示互斥**:已判定为拦截页时不再叠 `[http NNN]`;已识别为挑战时不再叠行为兜底提示。
42
+
43
+ ### Added
44
+
45
+ - **测试 159 → 220**(+61):挑战识别 50 条(含真实页面测得的门控数值与反例:讲 hCaptcha 的文章、`验证码说明` 帮助链接、`获取验证码` 按钮、百度搜索页标题)、真浏览器端到端 6 条(真实 `cf-mitigated` 头、无签名拦截面被行为兜底抓住、`interactiveOnly` 下 iframe 仍识别、单页应用改 URL 后旧响应头不复用、真实 404 显示状态码、真实访客门显示静默页提示)、安全回归 5 条(外链祖先点击被拒、密码/OTP 不进 prompt、contenteditable 成功、同长度不同值失败)、本机路径守卫。
46
+
47
+ ### Changed
48
+
49
+ - **随包自检不再在 npm 安装形态崩溃**:`scripts/dsh-browser-verify.mjs` 的「`lib/` 与 `src/` 同步」一组在没有 `src/` 时**跳过**并打印说明,退出码 0 —— 安装形态显示「全部通过 (56 项,跳过 1 项)」,仓库形态仍是「(57 项)」。README / CHANGELOG 此前承诺的"装上即可跑"现在才真的成立。
50
+ - **依赖不再全是 `*`**:`@deepseek-ai/schemastery` 固定 `^3.18.4`,peer `cordis >=4.0.4`、`dsh-llm` / `dsh-tools >=0.2.0-rc.1`。修前空项目安装会解析到 `dsh-tools 0.0.1-rc.1`、`dsh-llm 0.0.1-rc.5`(远旧于测试基线);修后解析到 `0.2.0-rc.2`。代价:不同版本元组的预发布版(如 `0.2.1-rc.*`)会收到 **peer 警告**(不是失败,pnpm 默认不严格)。
51
+ - **发布门禁补齐**:CI 增加 `npm run typecheck` 与 `npm run preflight`(与 `prepublishOnly` 对齐);CI 与 publish 都新增「`npm run build` + `git diff --exit-code -- lib`」守卫——以前先 build 再比 mtime,`src` 改完忘重建也能全绿,而 `dsh plugin add github:…` 的安装路径会拿到旧 `lib/`。
52
+ - **文档漂移**:独立审查列出 13 条硬错误 / 11 条过期 / 8 条矛盾 / 8 条缺失,全部修掉——随包文档死链由 12 条清零(改 GitHub 绝对链接)、测试计数与成本数字按实际命令重取、`npm test:live` 与 `browser-plugin/dev.cordis.yml` 这类跑不通/不存在的引用纠正、URL 白名单不再声称覆盖 `hover`(实际不覆盖)、补写 `snapshot.engine` 说明,并在 `中文说明.md` 新增 **§4.14 挑战页识别**(四层判定与两条结果提示,明确"产出提示不是阻断,认不出来就停下来问人")。许可表述收敛为:代码与说明文档 MIT,CC BY-NC-SA 仅限 `FOCUS-STEALING.md` / `MODE-TRADE-OFFS.md` 两篇分析长文。
53
+ - **`.gitignore` 补登录态**:`dsh-storage-state.json` / `browser-profiles/` / `profile/`。默认写在 `~/.dsh/browser-profiles/`(仓库外),但只要有人把 `launch.profileDir` 指进 checkout,那个文件里就是明文 cookie。
54
+
6
55
  ## 0.6.5 — 2026-10-16
7
56
 
8
57
  一句话摘要:**挑战页识别不再靠一份关键词表** —— 判定改成「厂商自有的机器信号优先,认不出来就问一句『页面变了吗』」,所以新出现的验证方式也能被接住;顺带清掉漏网的 4 处本机路径,并把 npm 包里 12 个用不上的开发脚本收成 2 个。
package/NOTICE.md CHANGED
@@ -6,26 +6,25 @@ This repository mixes work from three different origins. They are **not** under
6
6
 
7
7
  ## 1. The plugin — MIT (`./LICENSE`)
8
8
 
9
- Covers everything that is part of the plugin: `src/`, `tests/`, `scripts/`, `cordis.patch.yml`, `tsconfig.json`, `package.json`, `CHANGELOG.md`, and the plugin's own documentation (`PLUGIN-README.md`, `DIALOG-POLICY.md`, `SAFETY-RULES.md`, `SNAPSHOT-RULES.md`, `codex-rules.md`).
9
+ Covers everything that is part of the plugin: `src/`, `tests/`, `scripts/`, `cordis.patch.yml`, `tsconfig.json`, `package.json`, `CHANGELOG.md`, and the plugin's own documentation (`README.md`, `SECURITY.md`, `NOTICE.md`, `PLUGIN-README.md`, `DIALOG-POLICY.md`, `SAFETY-RULES.md`, `SNAPSHOT-RULES.md`, `codex-rules.md`, `中文说明.md`, `对比与优势.md`, and the articles under `docs/articles/`).
10
10
 
11
11
  | Origin | What | Licence |
12
12
  |---|---|---|
13
13
  | [ChenyuHeee/dsh-browser-playwright](https://github.com/ChenyuHeee/dsh-browser-playwright) v0.1.1 | The accessibility-snapshot engine (`src/injected.ts`, `src/snapshot-render.ts`), the tool family (`src/tool.ts`), the service/provider seams (`src/service.ts`, `src/playwright.ts`), the URL policy, and the test-suite shape. Copyright (c) 2026 dsh-browser-playwright contributors. | **MIT** |
14
- | This fork's merged layer | Geometric click (`browser_click` with `precise: true`) with landing-element reporting, the CDP policy layer (`src/cdp-policy.ts`), the persistent profile + login-state export (`src/runtime-state.ts`, `persistState`/`restoreState`), dialog parking, the console/network diagnostics tools, the contract/compat modules, and the fixes listed in [`CHANGELOG.md`](CHANGELOG.md) | **MIT** |
14
+ | This fork's merged layer | Geometric click (`browser_click` with `precise: true`) with landing-element reporting, the CDP policy layer (`src/cdp-policy.ts`), the persistent profile + login-state export (`src/provider.ts`, `persistState`/`restoreState`), dialog parking, the console/network diagnostics tools, the contract/compat modules, and the fixes listed in [`CHANGELOG.md`](https://github.com/miqian-nomad/dsh-browser-playwright-codex/blob/main/CHANGELOG.md) | **MIT** |
15
15
 
16
16
  **Why the plugin cannot be non-commercial:** it is a derivative of MIT-licensed work. MIT grants everyone the right to use, modify and redistribute the software **including commercially**, and a redistributor cannot add a non-commercial restriction to it. Any non-commercial claim over `src/` would be an unauthorised restriction — worse for downstream users than leaving it MIT. The MIT text in `./LICENSE` (including the upstream copyright notice) is unchanged.
17
17
 
18
18
  ## 2. This repository's analysis prose — CC BY-NC-SA 4.0 (`./LICENSE-DOCS`)
19
19
 
20
- Covers only the writing produced *for this repository*:
20
+ Covers only the two analysis documents produced *for this repository*:
21
21
 
22
- - `README.md` (this repository's landing page)
23
22
  - `FOCUS-STEALING.md`
24
23
  - `MODE-TRADE-OFFS.md`
25
24
 
26
- Share and adapt for **non-commercial** purposes with attribution and share-alike; commercial use is not permitted. Legal code: [`LICENSE-DOCS`](LICENSE-DOCS).
25
+ Share and adapt for **non-commercial** purposes with attribution and share-alike; commercial use is not permitted. Legal code: [`LICENSE-DOCS`](https://github.com/miqian-nomad/dsh-browser-playwright-codex/blob/main/LICENSE-DOCS) (the licence text lives in the repository; it is not part of the npm tarball).
27
26
 
28
- Note the deliberate boundary: the plugin's *own* documentation is not in this set — it ships with the plugin under MIT, so that the whole plugin stays redistributable on MIT terms.
27
+ Note the deliberate boundary: these two documents are **not shipped in the npm package** — only `README.md`, `CHANGELOG.md`, `SECURITY.md` and `NOTICE.md` travel with the plugin, and all four are MIT (§1). So everything an npm installer actually receives stays redistributable on MIT terms.
29
28
 
30
29
  ## 3. Third-party content that is **not** redistributed here
31
30
 
package/README.md CHANGED
@@ -2,12 +2,15 @@
2
2
 
3
3
  > [![dsh.so 安装验证 · harness 0.2.0-rc.1](https://www.dsh.so/badge/install/dsh-browser-playwright-codex@0.2.0-rc.1.svg)](https://www.dsh.so/zh/artifact/dsh-browser-playwright-codex/) [![dsh.so 风险](https://www.dsh.so/badge/dsh-browser-playwright-codex.svg)](https://www.dsh.so/zh/artifact/dsh-browser-playwright-codex/)
4
4
 
5
- **一句话**:让 AI 用你**已经登录的账号**在真实网页上干活 —— 填表、抓取列表、多标签查后台;浏览器窗口就在你桌面上、你随时看得见;遇到登录、验证码、原生弹框,它会**停下来等你**,绝不替你点「确定」。
5
+ **一句话**:让 AI 用你**已经登录的账号**在真实网页上干活 —— 填表、抓取列表、多标签查后台;浏览器窗口就在你桌面上、你随时看得见。原生弹框会被挂起、**绝不替你点「确定」**(机制级);验证码/挑战页会被识别出来,并以提示要求模型停下交给你(**提示不是阻断**)。登录/二次验证**没有探测器**,靠模型从快照看出来并停下;密码、一次性验证码、卡号字段 `browser_fill` **直接拒绝**——这类字由你本人在窗口里输。
6
6
 
7
7
  **In one line**: the agent drives a **visible, profile-backed browser window** with the logins you
8
8
  already have — filling in forms, scraping lists and working across tabs in real pages — through
9
- accessibility snapshots with stable element refs, and it stops for logins, captchas and native dialogs
10
- instead of guessing. No CSS-selector guessing, no full-DOM dumps.
9
+ accessibility snapshots with stable element refs. Native dialogs are parked and reported instead of
10
+ auto-accepted. Challenge pages are detected and reported as a note that tells the agent to stop and hand
11
+ the page back — a note, not a block. There is **no sign-in detector**, and `browser_fill` refuses
12
+ password / one-time-code / card fields outright — you type those in the visible window. No
13
+ CSS-selector guessing, no full-DOM dumps.
11
14
 
12
15
  ```sh
13
16
  # from npm
@@ -22,14 +25,16 @@ dsh plugin --profile <name> add github:miqian-nomad/dsh-browser-playwright-codex
22
25
  - **登录一次,一直用** — a profile-backed window plus an exported session state, so a closed window or
23
26
  a restarted harness comes back logged in.
24
27
  - **全程看得见,也不抢你的焦点** — a real window you can watch, minimise or take over; the provider
25
- keeps working while it is minimised ([FOCUS-STEALING.md](FOCUS-STEALING.md)).
28
+ keeps working while it is minimised ([FOCUS-STEALING.md](https://github.com/miqian-nomad/dsh-browser-playwright-codex/blob/main/FOCUS-STEALING.md)).
26
29
  - **遇事停下来叫人** — `alert` / `confirm` / `prompt` are parked and reported, answered only by an
27
- explicit `browser_dialog` call; a login, 2FA or captcha is handed back to you
28
- ([DIALOG-POLICY.md](DIALOG-POLICY.md)). A challenge page is recognised from the interceptor's own
29
- response headers first (`cf-mitigated: challenge` and friends), then from its widget, and only then
30
- from wording; if none of those match, a page that stops changing after an action is reported as a
31
- possible wall instead of being retried
32
- ([docs/articles/dialogs-and-captcha.md](docs/articles/dialogs-and-captcha.md)).
30
+ explicit `browser_dialog` call ([DIALOG-POLICY.md](https://github.com/miqian-nomad/dsh-browser-playwright-codex/blob/main/DIALOG-POLICY.md)).
31
+ A challenge page is recognised from the interceptor's own response headers first
32
+ (`cf-mitigated: challenge` and friends; `x-iinfo` / `x-datadome*` additionally need a blocking
33
+ status), then from the vendor widget's accessible name, then from wording; if none of those match,
34
+ a page that stops changing after an action is reported as a possible wall instead of being retried.
35
+ Recognition adds a note that tells the agent to stop and hand the window back to you — that note is
36
+ advice to the model, not a hard block; the mechanism-level guarantee exists for native dialogs only
37
+ ([docs/articles/dialogs-and-captcha.md](https://github.com/miqian-nomad/dsh-browser-playwright-codex/blob/main/docs/articles/dialogs-and-captcha.md)).
33
38
  - **ref 失效会明说** — per-document nonce refs; a stale ref fails fast with an explanation instead of
34
39
  silently clicking a different element.
35
40
  - **强力工具默认关闭** — `browser_evaluate` / `browser_cdp` are **not registered at all** unless your
@@ -37,9 +42,11 @@ dsh plugin --profile <name> add github:miqian-nomad/dsh-browser-playwright-codex
37
42
 
38
43
  **安全一览 / Security at a glance** — dsh.so's scanner flags this repository's **one Critical finding**, and that finding **is** the
39
44
  implementation of `browser_evaluate` (its dynamic evaluation), which this package ships switched off.
40
- [SECURITY.md](SECURITY.md) maps every finding to the gate that contains it, and explains what the plugin
41
- can and cannot do with your logins (URL policy, CDP allow-list, dialog policy, no telemetry, and it never
42
- types passwords or 2FA codes). Counts move as the scanner and the code change — their own report is the
45
+ [SECURITY.md](https://github.com/miqian-nomad/dsh-browser-playwright-codex/blob/main/SECURITY.md) says what each
46
+ finding actually is, whether a capability gate makes it reachable (two of the warnings are not gate-controlled:
47
+ cannot do with your logins (URL policy, CDP allow-list, dialog policy, no telemetry, and `browser_fill`
48
+ refuses password / one-time-code / card fields). Counts move as the scanner and the code change — their own report is the
49
+ passwords or 2FA codes). Counts move as the scanner and the code change — their own report is the
43
50
  live number.
44
51
 
45
52
  ## Install
@@ -59,9 +66,15 @@ dsh plugin --profile <name> add github:miqian-nomad/dsh-browser-playwright-codex
59
66
  dsh plugin --profile <name> add link:<absolute path to this directory>
60
67
  ```
61
68
 
62
- **Compatibility.** Built and tested against DSH `0.2.0-rc.1` (`@deepseek-ai/*` peer ranges are deliberately
63
- `*` so a newer harness still installs). After a harness upgrade, `npm run doctor` re-checks that every named
64
- export this plugin imports still exists — that is the fast way to tell "the harness moved" from "the plugin broke".
69
+ **Compatibility.** Built and tested against DSH `0.2.0-rc.1`. The dependency ranges are **floors, not
70
+ wildcards**: peers `@deepseek-ai/cordis` `>=4.0.4`, `@deepseek-ai/dsh-llm` `>=0.2.0-rc.1`,
71
+ `@deepseek-ai/dsh-tools` `>=0.2.0-rc.1`, and the direct dependency `@deepseek-ai/schemastery` `^3.18.4`. The
72
+ floors exist so an empty project cannot resolve a very old `0.0.1-rc.*` build, while a newer harness still
73
+ installs. One cost of npm's prerelease rules: a prerelease whose `[major, minor, patch]` tuple differs from
74
+ `0.2.0` (for example `0.2.1-rc.*`) is outside a `>=0.2.0-rc.1` range, so you may see a **peer warning** — a
75
+ warning, not an install failure (pnpm does not enforce peer ranges strictly by default). After a harness
76
+ upgrade, `npm run doctor` re-checks that every named export this plugin imports still exists — that is the
77
+ fast way to tell "the harness moved" from "the plugin broke".
65
78
 
66
79
  `lib/` ships in the repository (and there is deliberately no `prepare` hook), so a fresh clone loads
67
80
  without a build step — `npm run build` is only needed after editing `src/`.
@@ -77,11 +90,13 @@ Install it and ask for something that leaves a visible result — there is nothi
77
90
  > Go to my orders page on `<site>` and tell me the delivery address on the most recent order.
78
91
 
79
92
  > Fill in this contact form, but stop and show me the filled form before you submit it.
80
-
81
- What you should see: a real browser window on your desktop doing the clicking, and the page coming back to
93
+ the model as a `ref`-numbered list. The first time it meets a verification page it **reports the page back
94
+ to you** — take the window over by hand and it continues from where it left off (sign-ins have no detector:
95
+ the agent is told to stop when it sees one).
82
96
  the model as a `ref`-numbered list. The first time it meets a sign-in, a 2FA prompt or a verification page
83
- it **stops and tells you** — take the window over by hand and it continues from where it left off. The
84
- logins already in that profile are what make the rest work.
97
+ it **reports the page back to you** — take the window over by hand and it continues from where it left off.
98
+ Native dialogs are the one case that is enforced at the interface: they are parked and never auto-accepted.
99
+ The logins already in that profile are what make the rest work.
85
100
 
86
101
  Two habits worth having on day one:
87
102
 
@@ -104,7 +119,7 @@ Or pin a binary: `launch.executablePath` / `launch.channel` in the plugin config
104
119
 
105
120
  ## Tools
106
121
 
107
- The default `toolPrefix` is `browser_`. Every action returns a fresh snapshot, so refs always come from the latest result. The registered set is capability-gated: `browser_evaluate`, `browser_cdp` and `browser_extract` appear only when their capability is switched on (their schemas are resident in the system prompt on every turn, so a tool that could only answer "disabled" would be pure prompt cost). Count the live surface with `npm run cost` — the shipped `cordis.patch.yml` keeps **both power gates off** (since 0.4.7), so a profile mounting this bundle sees **18 tools**; 19 or 20 once your deployment opts into `evaluate` / `cdp`, 21 once `extract` is configured. How to opt in: [SECURITY.md](SECURITY.md) §1.
122
+ The default `toolPrefix` is `browser_`. Every action returns a fresh snapshot, so refs always come from the latest result. The registered set is capability-gated: `browser_evaluate`, `browser_cdp` and `browser_extract` appear only when their capability is switched on (their schemas are resident in the system prompt on every turn, so a tool that could only answer "disabled" would be pure prompt cost). Count the live surface with `npm run cost` — the shipped `cordis.patch.yml` keeps **both power gates off** (since 0.4.7), so a profile mounting this bundle sees **16 tools**; 17 or 18 once your deployment opts into `evaluate` / `cdp`, 19 with `diagnostics: true`, 21 once `extract` is configured. How to opt in: paste the snippet under Known Limitations below, or use the built-in Settings card.
108
123
 
109
124
  | Tool | Purpose |
110
125
  |---|---|
@@ -118,18 +133,19 @@ The default `toolPrefix` is `browser_`. Every action returns a fresh snapshot, s
118
133
  | `browser_wait` | Wait a bounded duration for lazy content |
119
134
  | `browser_tabs` / `browser_open_tab` / `browser_switch_tab` / `browser_close_tab` | Tab management; `browser_tabs` marks the `[active]` tab the other tools act on |
120
135
  | `browser_screenshot` | PNG capture stored as a durable image attachment the model can view |
121
- | `browser_extract` | Structured extraction: natural-language instruction → one parsed JSON value | **Only registered when enabled.**
122
- | `browser_evaluate` | Run one JavaScript expression in the page (disabled by default) | **Only registered when enabled.**
136
+ | `browser_extract` | Structured extraction: natural-language instruction → one parsed JSON value; **only registered when its capability is enabled** |
137
+ | `browser_evaluate` | Run one JavaScript expression in the page (disabled by default); **only registered when its capability is enabled** |
123
138
  | `browser_dialog` | Answer the native dialog blocking the page: `{ accept: true }` confirms, `false` cancels; errors with `NO_DIALOG` when nothing is pending |
124
- | `browser_cdp` | Send one raw CDP command (DOM inspection / Input simulation allow-list only; disabled by default) | **Only registered when enabled.**
139
+ | `browser_cdp` | Send one raw CDP command (DOM inspection / Input simulation allow-list only; disabled by default); **only registered when its capability is enabled** |
125
140
  | `browser_close` | Close the session's browser window; the profile keeps login state, so the next call reopens signed in |
126
- | `browser_console_messages` | Read the page console messages captured for the current page (levels, text, source) |
127
- | `browser_network_requests` | Read the network requests captured for the current page (method, URL, status, resource type) |
141
+ | `browser_console_messages` | Read the page console messages captured for the current page (levels, text, source); **only registered when `diagnostics: true`** |
142
+ | `browser_network_requests` | Read the network requests captured for the current page (method, URL, status, resource type); **only registered when `diagnostics: true`** |
128
143
 
129
144
  ### Parameter signatures
130
145
 
131
146
  Every registered tool, with the parameters its schema actually declares (`*` = required, `?` = optional).
132
- Generated from the live tool surface and pinned by `tests/tool-schema.test.ts`, so it cannot drift from the code.
147
+ Hand-maintained and pinned by `tests/tool-schema.test.ts` in both directions: a missing tool, a renamed
148
+ parameter or a wrong required flag fails the test, so it cannot drift from the code.
133
149
 
134
150
  - `browser_back` — _none_
135
151
  - `browser_cdp` — `method`*, `params`? _(gated: registered only when enabled)_
@@ -154,7 +170,7 @@ Generated from the live tool surface and pinned by `tests/tool-schema.test.ts`,
154
170
  - `browser_wait` — `ms`*
155
171
 
156
172
  > **使用铁律**: 状态检查 / 无效不盲试 / 页面内容不可信 / 登录红线 / 风控分类等 Codex 文档纪律,
157
- > 见 [`codex-rules.md`](codex-rules.md)(A 层已内嵌进各工具 description; B 层供粘贴进 DSH profile / 系统提示)。
173
+ > 见 [`codex-rules.md`](https://github.com/miqian-nomad/dsh-browser-playwright-codex/blob/main/codex-rules.md)(A 层已内嵌进各工具 description; B 层供粘贴进 DSH profile / 系统提示)。
158
174
 
159
175
  ### Snapshot example
160
176
 
@@ -172,6 +188,25 @@ Refs: 12
172
188
 
173
189
  The model then calls `browser_click` with `ref: e2`. Invisible content is excluded, node and text caps bound the token cost, and truncation is flagged in the result.
174
190
 
191
+ #### Result notes
192
+
193
+ A result can open with one or more notes before the tree. Each says what the plugin saw; none of them
194
+ claims to *prevent* anything:
195
+
196
+ - `[human verification]` — a challenge page was recognised; the line above the note names the tier
197
+ (response header / widget / wording) and the evidence, and the note tells the agent to stop and ask
198
+ you to finish it in the open window instead of retrying.
199
+ - `[http NNN]` — the main document answered a non-2xx status and no challenge was identified, so the
200
+ tree may be an error page or an interception page rather than the content that was asked for.
201
+ - `[nothing here]` — the page has no clickable elements and almost no text: a gate or visitor check, a
202
+ page that is still loading, or one that needs a person. The note says to take one more snapshot,
203
+ then ask you rather than re-sending the same navigation.
204
+ - `[no visible change]` — the action left the page identical (after one action on a nearly empty page,
205
+ or after two identical results on a page with content). It is worded as a guess on purpose: it is
206
+ what an unrecognised wall looks like.
207
+ - A landing note (which element received the click), a parked-dialog note, and an engine-fallback note
208
+ can also appear.
209
+
175
210
  An actionable element with no text of its own is named from its descendants — `<a href="/set"><span>搜索设置</span></a>` renders as `link "搜索设置" [ref=e7]`, and `<a href="/b"><img alt="图标链接"></a>` as `link "图标链接" [ref=e8]` — so a visible label and the ref that carries it always appear on the same line. A non-actionable child that merely repeats an ancestor name is folded away, so each label appears once, on the line that carries the ref; container roles are never named from their content (a `<select>` shows its chosen option, the way a text input shows its value).
176
211
 
177
212
  ## Configuration
@@ -179,45 +214,53 @@ An actionable element with no text of its own is named from its descendants —
179
214
  All tunables are patchable through the profile's `cordis.patch.yml` (later layers win per row).
180
215
 
181
216
  ```yaml
217
+ # ONE row for the whole capability (2026-10-19). A package with a client module (the
218
+ # Settings card lives here) may only be mounted once, so the three concerns are nested.
182
219
  - id: browser
220
+ name: 'dsh-browser-playwright-codex'
183
221
  config:
184
- provider: playwright # explicit provider selection
185
-
186
- - id: browser-playwright
187
- config:
188
- launch:
189
- channel: chrome # or chromium / msedge / edge
190
- headless: false # visible window you can watch and rescue
191
- persistent: true # profile-backed window, login survives close
192
- # profileDir: ~/.dsh/browser-profiles/playwright by default
193
- viewport: { width: 1280, height: 800 }
194
- navigationTimeoutMs: 30000
195
- ignoreHTTPSErrors: false
196
- allowedDomains: [] # e.g. ['github.com'] restricts navigation
197
- idleTimeoutMs: 600000 # close idle windows after 10 min; 0 = never
198
- maxSessions: 8 # LRU eviction (persistent mode: one window)
199
- snapshot:
200
- maxNodes: 500 # token budget per snapshot tree
201
- maxNameLength: 120
202
- maxTextLength: 300
203
- diff: false # true = return "+/-/~" ref diffs instead of the full tree
204
-
205
- - id: browser-tool
206
- config:
207
- toolPrefix: browser_ # model-facing tool name prefix
208
- allowEvaluate: false # browser_evaluate gate
209
- allowCdp: false # browser_cdp gate (DOM inspection / Input simulation allow-list)
210
- registerDisabledTools: false # register evaluate/cdp/extract even while off (discoverability vs resident prompt cost)
211
- interactiveOnlyDefault: false # true = browser_snapshot returns actionable elements only
212
- maxWaitMs: 60000 # browser_wait bound
213
- extract:
214
- provider: deepseek-official # optional: enables browser_extract
215
- model: deepseek-v4-flash
216
- maxInputChars: 20000 # caps the page text handed to that model
217
- maxOutputTokens: 2000
222
+ service:
223
+ provider: playwright # explicit provider selection
224
+ playwright:
225
+ launch:
226
+ channel: chrome # or chromium / msedge / edge
227
+ headless: false # visible window you can watch and rescue
228
+ persistent: true # profile-backed window, login survives close
229
+ # profileDir: ~/.dsh/browser-profiles/playwright by default
230
+ viewport: { width: 1280, height: 800 }
231
+ navigationTimeoutMs: 30000
232
+ ignoreHTTPSErrors: false
233
+ allowedDomains: [] # e.g. ['github.com'] restricts navigation
234
+ idleTimeoutMs: 0 # shipped value: never auto-close (schema default 600000 = 10 min)
235
+ maxSessions: 8 # LRU eviction (persistent mode: one window)
236
+ snapshot:
237
+ engine: legacy # 'legacy' (default) or 'aria' (Playwright ariaSnapshot, refs aligned back)
238
+ maxNodes: 500 # token budget per snapshot tree
239
+ maxNameLength: 120
240
+ maxTextLength: 300
241
+ diff: false # true = return "+/-/~" ref diffs instead of the full tree
242
+ tool:
243
+ toolPrefix: browser_ # model-facing tool name prefix
244
+ allowEvaluate: false # browser_evaluate gate
245
+ allowCdp: false # browser_cdp gate (DOM inspection / Input simulation allow-list)
246
+ registerDisabledTools: false # register gated tools even while off (discoverability vs resident prompt cost)
247
+ diagnostics: false # true = register browser_console_messages / browser_network_requests
248
+ interactiveOnlyDefault: false # true = browser_snapshot returns actionable elements only
249
+ maxWaitMs: 60000 # browser_wait bound
250
+ extract:
251
+ provider: deepseek-official # optional: enables browser_extract
252
+ model: deepseek-v4-flash
253
+ maxInputChars: 20000 # caps the page text handed to that model
254
+ maxOutputTokens: 2000
255
+ ```
218
256
  ```
219
257
 
220
- `browser_extract` needs an auxiliary LLM route (`extract.provider` + `extract.model`); without one it fails with an actionable error. With a route configured, `extract.maxInputChars` bounds the page text in that prompt, and the prompt names the cap when it truncates. A gated tool is **not registered at all** while its capability is off, so its schema never reaches the system prompt; set `registerDisabledTools: true` to keep the whole surface discoverable (the tool then answers with the error naming the switch). The shipped layer in [`cordis.patch.yml`](cordis.patch.yml) keeps `allowEvaluate` and `allowCdp` **off**, matching the schema defaults and this sample: that file travels inside the package, so anything it enabled would be enabled for everyone who installs it. Opt in per deployment, in that deployment's own patch layer. `browser_evaluate` executes arbitrary page JavaScript; `browser_cdp` stays off until `allowCdp: true`, and even then only the allow-list in `lib/cdp-policy.js` passes — network, storage, cookie, security and arbitrary-JS (`Runtime.evaluate`) commands are rejected with `CDP_DENIED`.
258
+ `snapshot.engine` picks the page-reading engine: `'legacy'` (the default) walks the injected DOM,
259
+ `'aria'` uses Playwright's official `ariaSnapshot({ mode: 'ai' })` and aligns the result back onto the
260
+ same `ref` ids; an aria tree that cannot be parsed falls back to legacy rather than handing the model an
261
+ empty tree. It lives under `playwright.snapshot`, next to `maxNodes` / `maxNameLength` / `maxTextLength` / `diff`.
262
+
263
+ `browser_extract` needs an auxiliary LLM route (`extract.provider` + `extract.model`); without one it fails with an actionable error. With a route configured, `extract.maxInputChars` bounds the page text in that prompt, and the prompt names the cap when it truncates. A gated tool is **not registered at all** while its capability is off, so its schema never reaches the system prompt; set `registerDisabledTools: true` to keep the whole surface discoverable (the tool then answers with the error naming the switch). The shipped layer in [`cordis.patch.yml`](https://github.com/miqian-nomad/dsh-browser-playwright-codex/blob/main/cordis.patch.yml) keeps `allowEvaluate` and `allowCdp` **off**, matching the schema defaults and this sample: that file travels inside the package, so anything it enabled would be enabled for everyone who installs it. Opt in per deployment, in that deployment's own patch layer. `browser_evaluate` executes arbitrary page JavaScript; `browser_cdp` stays off until `allowCdp: true`, and even then only the allow-list in `lib/cdp-policy.js` passes — network, storage, cookie, security and arbitrary-JS (`Runtime.evaluate`) commands are rejected with `CDP_DENIED`.
221
264
 
222
265
  ### Two levers that shrink what tools *return*
223
266
 
@@ -240,8 +283,8 @@ turning them on in a deployment that shares prompts across many users.
240
283
 
241
284
  ### Prefer a switch to editing config?
242
285
 
243
- The companion package [`dsh-browser-toggle`](https://github.com/miqian-nomad/dsh-browser-toggle) puts
244
- this on a Settings card: enable/disable the browser, pick the page-reading mode, and turn the two
286
+ The Settings card ships **inside this package** (it moved out of the companion package `dsh-browser-toggle`, which is retired) and puts
287
+ this on one card: enable/disable the browser, pick the page-reading mode, and turn the two
245
288
  power tools on or off — no restart, no YAML. The card also lists **which sites this browser looks
246
289
  signed in to**. Two things make that list trustworthy: cookies are attributed **first-party** — only
247
290
  domains this browser actually visited are candidates, because a tracker's cookie belongs to the site
@@ -277,6 +320,8 @@ npm install
277
320
  npm test # full suite: engine/action integration on real Chrome, policy, rendering,
278
321
  # schema assembly, plus the realistic scenario suites below
279
322
  npm run verify # naming/folding + click/hover/dialog acceptance; self-contained, prints its own count
323
+ # (57 in a checkout; from an npm install it prints 56 + "1 skipped", because the
324
+ # `lib/`-vs-`src/` check has no `src/` to compare against)
280
325
  npm run verify:live # same, plus a live sample of a real page (needs network)
281
326
  npm run doctor # does the installed harness still export everything this plugin imports?
282
327
  npm run cost # resident prompt cost of the tool schemas, per surface
@@ -288,6 +333,10 @@ but `lib/` and the harness: `node scripts/dsh-browser-verify.mjs` (the acceptanc
288
333
  `node scripts/doctor.mjs` (re-checks the harness exports after an upgrade). The rest are repository-only
289
334
  and run from a checkout — `measure-prompt-cost.mjs` reads `src/`, so it cannot work from a tarball.
290
335
 
336
+ One package-manager note: `package.json` declares `packageManager: pnpm@10.31.0`, while the commands above
337
+ (and CI) use **npm** with the committed `package-lock.json` (`npm ci`). Pick one and stay in it — mixing the
338
+ two in a checkout gives you two lockfiles and an ambiguous install.
339
+
291
340
  ### Test suites
292
341
 
293
342
  | Suite | Command | What it covers |
@@ -295,15 +344,15 @@ and run from a checkout — `measure-prompt-cost.mjs` reads `src/`, so it cannot
295
344
  | Unit + engine integration | `npm test` (included) | Snapshot rendering, schema assembly, URL policy, provider actions on real Chrome |
296
345
  | Realistic scenarios | `npm run test:scenarios` | Scripted-agent journeys over a realistic local store fixture: guest checkout with coupon and validation, login/session persistence, multi-tab research, lazy content, infinite scroll, modal dialogs, popups, screenshots, policy, owner isolation, idle disposal, snapshot budgets — at both the `BrowserSession` level and the model-facing `ctx.tools.execute` level (with fake attachments/LLM services) |
297
346
  | Regression verify | `npm run verify` | The executable spec of snapshot naming/folding plus ref/text click-hover, the pending-dialog protocol and the tool surface (expected names derived from `contract.ts`) |
298
- | Live smoke | `npm test:live` | Opt-in checks against the real internet (example.com, Wikipedia REST). Skipped unless `DSH_BROWSER_LIVE=1`; not part of CI |
347
+ | Live smoke | `npm run test:live` | Opt-in checks against the real internet (example.com, Wikipedia REST). Skipped unless `DSH_BROWSER_LIVE=1`; not part of CI |
299
348
 
300
349
  The scenario suites are the plugin's most realistic tests: they drive the browser the way an agent does — snapshot, find a `ref`, act, re-snapshot — and therefore double as executable documentation of the model experience.
301
350
 
302
- From a DeepSeek Harness source checkout, load this plugin from TypeScript:
303
-
304
- ```sh
305
- pnpm dsh web --patch ./browser-plugin/dev.cordis.yml
306
- ```
351
+ From a DeepSeek Harness source checkout, the bundle layer to load is this repository's
352
+ [`cordis.patch.yml`](https://github.com/miqian-nomad/dsh-browser-playwright-codex/blob/main/cordis.patch.yml) — the
353
+ same file the package ships. Register the checkout through your profile with
354
+ `dsh plugin --profile <name> add link:<absolute path to this directory>` (above); this repository carries no
355
+ separate dev patch file.
307
356
 
308
357
  ## Model Experience
309
358
 
@@ -319,11 +368,23 @@ Measured with ``npm run cost`` (per-turn, resident in the system prompt):
319
368
 
320
369
  | Surface | Tools | Schema characters | ≈ tokens/turn |
321
370
  |---|---|---|---|
322
- | Schema defaults (`Config({})`, no patch layer) | 18 | 12,755 | ≈ 3,543 |
323
- | Shipped bundle layer (this package's `cordis.patch.yml`: both power gates off) | 18 | 12,755 | ≈ 3,543 |
324
- | Everything registered (`registerDisabledTools: true`, plus `extract`) | 21 | 15,608 | ≈ 4,336 |
325
-
326
- Three things changed here, and only one of them was about size. (1) The click family was rewritten so each rule is stated once instead of three times: -390 characters, with every rule, example and rationale pinned by `tests/tool-descriptions.test.ts`, so a trim that eats one fails the build. (2) A gated tool is registered only while its capability is live — `browser_extract` on this deployment: -456 characters ≈ 127 tokens/turn. (3) Four descriptions were corrected to match the code they describe (dialog blocking, the gated diagnostics `browser_fill` points at, persistent-mode `browser_close`, and the click family’s landing note), which added 211 characters back because the old wording promised behaviour the implementation did not have. Net: 18,065 → 17,886 characters with everything registered; the shipped layer's total is identical to the schema-defaults row above, because it keeps both power gates off — those schemas are not registered at all and cost nothing. What is left is per-tool prose rather than repetition, so a further cut trades away real guidance.
371
+ | Schema defaults (`Config({})`, no patch layer) | 16 | 11,179 | ≈ 3,105 |
372
+ | Shipped bundle layer (this package's `cordis.patch.yml`: both power gates off) | 16 | 11,179 | ≈ 3,105 |
373
+ | Everything registered (`registerDisabledTools: true`, plus `extract`) | 21 | 15,782 | ≈ 4,384 |
374
+
375
+ This paragraph describes the 0.6.0 change; the table above is today's measurement. Three things changed
376
+ there, and only one of them was about size. (1) The click family was rewritten so each rule is stated once
377
+ instead of three times: -390 characters, with every rule, example and rationale pinned by
378
+ `tests/tool-descriptions.test.ts`, so a trim that eats one fails the build. (2) A gated tool is registered only
379
+ while its capability is live: `browser_extract`'s schema alone is about 456 characters (≈ 127 tokens/turn),
380
+ which a deployment that leaves `extract` unconfigured does not pay — a separate comparison, so it is not part
381
+ of the net below. (3) Four descriptions were corrected to match the code they describe (dialog blocking, the
382
+ gated diagnostics `browser_fill` points at, persistent-mode `browser_close`, and the click family’s landing
383
+ note), which added 211 characters back because the old wording promised behaviour the implementation did not
384
+ have. Net for that release: 18,065 → 17,886 characters with everything registered. The shipped layer's total
385
+ is identical to the schema-defaults row above, because it keeps both power gates off — those schemas are not
386
+ registered at all and cost nothing. What is left is per-tool prose rather than repetition, so a further cut
387
+ trades away real guidance.
327
388
 
328
389
  #### KV Cache effect
329
390
 
@@ -335,15 +396,18 @@ The page state itself (DOM, cookies, storage) lives in the browser context and n
335
396
 
336
397
  ## Further reading
337
398
 
338
- Written for people deciding whether to hand an agent their browser:
399
+ Written for people deciding whether to hand an agent their browser. These live in the GitHub repository
400
+ (`docs/articles/`); the npm tarball ships only `README.md`, `CHANGELOG.md`, `SECURITY.md` and `NOTICE.md`, so the
401
+ links below go to GitHub rather than to files inside the package:
339
402
 
340
- - [让 AI 用你已经登录的账号干活](docs/articles/login-state.md) — why session cookies are the hard part, and
403
+ - [让 AI 用你已经登录的账号干活](https://github.com/miqian-nomad/dsh-browser-playwright-codex/blob/main/docs/articles/login-state.md) — why session cookies are the hard part, and
341
404
  what the export does to a minimised window.
342
- - [AI 开着浏览器抢你的焦点:三个根因](docs/articles/focus-stealing.md) — all three traced into source, with a
343
- 60-line reproduction script that needs no part of this plugin.
344
- - [遇到验证码和弹窗,为什么它不该替你点「确定」](docs/articles/dialogs-and-captcha.md) — mechanism-level
345
- dialog parking, and the same philosophy behind stale refs failing loudly.
346
- - [官方 Discussions 发帖草稿](docs/articles/discussions-post.md) — a compatibility report in the format the
405
+ - [AI 开着浏览器抢你的焦点:三个根因](https://github.com/miqian-nomad/dsh-browser-playwright-codex/blob/main/docs/articles/focus-stealing.md) — all three traced into source, with a
406
+ 113-line reproduction script that needs no part of this plugin.
407
+ - [遇到验证码和弹窗,为什么它不该替你点「确定」](https://github.com/miqian-nomad/dsh-browser-playwright-codex/blob/main/docs/articles/dialogs-and-captcha.md) — mechanism-level
408
+ dialog parking, what the challenge detector does and does not guarantee, and the same philosophy behind
409
+ stale refs failing loudly.
410
+ - [官方 Discussions 发帖草稿](https://github.com/miqian-nomad/dsh-browser-playwright-codex/blob/main/docs/articles/discussions-post.md) — a compatibility report in the format the
347
411
  community asks for, ready to paste.
348
412
 
349
413
  ## Known Limitations and Deferred Work
@@ -356,19 +420,22 @@ Written for people deciding whether to hand an agent their browser:
356
420
  - **The package ships both power gates OFF, and that is deliberate** — `cordis.patch.yml` is part of the package, so anything it enables is enabled for *everyone who installs this*. It therefore sets `allowEvaluate: false` and `allowCdp: false` (the code defaults), and each deployment opts in through its **own profile patch** (`~/.dsh/profiles/<name>/cordis.patch.yml`). A package that shipped them on would be handing every installer a backdoor they never asked for. Snippet:
357
421
  ```yaml
358
422
  # dsh replaces an entry's config object wholesale — list every key you rely on.
359
- - id: browser-tool
423
+ - id: browser
360
424
  config:
361
- toolPrefix: 'browser_'
362
- allowEvaluate: true # registers browser_evaluate
363
- allowCdp: true # registers browser_cdp (needs a full DSH restart)
364
- registerDisabledTools: false
365
- maxWaitMs: 60000
425
+ service: { provider: playwright }
426
+ playwright: { launch: { headless: false, persistent: true }, idleTimeoutMs: 0, maxSessions: 8, snapshot: { diff: false } }
427
+ tool:
428
+ toolPrefix: 'browser_'
429
+ allowEvaluate: true # registers browser_evaluate
430
+ allowCdp: true # registers browser_cdp (needs a full DSH restart)
431
+ registerDisabledTools: false
432
+ maxWaitMs: 60000
366
433
  ```
367
434
  - **Chromium family only** — Firefox/WebKit channels are not probed; providers are swappable if another engine is needed.
368
435
  - **Extract needs a dedicated model route** — it does not reuse the main request's route; misconfiguration fails loudly at call time.
369
- - **JS-driven navigations are not statically checkable** — `allowedDomains` covers `browser_navigate`, `browser_open_tab`, every click path (`browser_click`: ref, `precise`, raw-coordinate and `mode:"hover"` modes) and `browser_switch_tab`, but a button whose handler runs `location.href = …` can still leave the allowed hosts, and a tab that is already sitting on a disallowed host stays open (the agent simply refuses to drive it); deploy an external network guard for hard isolation.
436
+ - **JS-driven navigations are not statically checkable** — `allowedDomains` covers `browser_navigate`, `browser_open_tab`, every navigating click (`browser_click`: `ref`, `text`, `precise` and raw-coordinate modes) and `browser_switch_tab`; `mode: "hover"` reveals content without activating anything, so it is not checked against the URL policy. A button whose handler runs `location.href = …` can still leave the allowed hosts, and so can a `<form>` whose `action` points at another host (pressing its submit button is a navigation this check does not statically see), and a tab that is already sitting on a disallowed host stays open (the agent simply refuses to drive it); deploy an external network guard for hard isolation.
370
437
  - **Stability waits are bounded by design** — a perpetually animating element falls back to a forced click on its current rect rather than stalling; a page that never settles is snapshotted after the cap. This is intentional: the agent must never hang.
371
- - **Challenge detection reads the responses it has actually seen** — the response-header tier applies to a document response this session observed after it attached to the tab. A tab that was already sitting on a challenge page when the session took it over falls through to the widget and wording tiers and, failing those, to the no-progress backstop: it reports a *possible* wall instead of guessing the vendor.
438
+ - **Challenge detection reads the responses it has actually seen** — the response-header tier applies to a document response this session observed after it attached to the tab. A tab that was already sitting on a challenge page when the session took it over falls through to the widget and wording tiers and, failing those, to the no-progress backstop: it reports a *possible* wall instead of guessing the vendor. A page that is not recognised still never goes silent: a non-2xx document carries an `[http NNN]` note, and a page with no clickable elements and almost no text carries a `[nothing here]` note.
372
439
 
373
440
  ### Ref and lifecycle safety (fixed)
374
441
 
@@ -388,11 +455,15 @@ MIT
388
455
  <!-- changelog:today -->
389
456
  ## 更新记录
390
457
 
391
- (此列表只记 **minor 里程碑**;每个补丁版本见 [CHANGELOG.md](CHANGELOG.md)。)
458
+ (此列表只记 **minor 里程碑**;每个补丁版本见 [CHANGELOG.md](https://github.com/miqian-nomad/dsh-browser-playwright-codex/blob/main/CHANGELOG.md)。)
459
+
460
+ ### 0.7.0(2026-10-19)
461
+
462
+ - **两个插件合成一个(破坏性)**:设置卡片并入本包(`dsh.client` + `./client`),`dsh-browser-toggle` 退役;`cordis.patch.yml` 从三行 loader 收成**一行** —— `- id: browser` 加嵌套的 `service` / `playwright` / `tool`。这是硬约束:`dsh-client-modules` 拒绝一个包被多个 loader 行挂载,所以带 client 的包只能有一个入口。
392
463
 
393
464
  ### 0.6.0(2026-10-16)
394
465
 
395
- - **工具面三合一(破坏性)**:`browser_click` / `browser_hover` / `browser_click_at` 合并成一个 `browser_click` —— `mode: "hover"` 是原来的悬停、`precise: true` 是原来的几何中点兜底、`x`+`y` 是裸坐标。常驻 prompt **净减 16.5%(约六分之一)**:现在是 **18 个工具 12,755 字符 ≈ 3,543 token/轮**,全量注册(含三个闸门工具)21 个;规则一条没丢,守卫改成"合并后的工具必须保住原来三个的全部规则"。
466
+ - **工具面三合一(破坏性)**:`browser_click` / `browser_hover` / `browser_click_at` 合并成一个 `browser_click` —— `mode: "hover"` 是原来的悬停、`precise: true` 是原来的几何中点兜底、`x`+`y` 是裸坐标。常驻 prompt **净减 16.5%(约六分之一)**(当时的值;当前成本见上面的成本表),全量注册(含三个闸门工具)21 个;规则一条没丢,守卫改成"合并后的工具必须保住原来三个的全部规则"。
396
467
  - **`browser_fill` 接受 `value` 作 `text` 的别名**:模型按语义猜参数名不再第一次就撞墙;缺值时给的是指路错误(同时点出两种写法,并说明要点可见标签该用 `browser_click`)。
397
468
  - **设置页多了一节「已登录的站点」(只读)**:按域名列出"像登录态"的 cookie —— 只列你在浏览器里**真的访问过**的域(第三方统计/广告域排除),判据用 OWASP 的会话特征(httpOnly 且会话级/值像令牌),**名字与条数不再作为依据**。不显示 cookie 的名字和值、不写磁盘、**模型看不到**;同一家公司的域折叠成一行(如 `京东系(18 个域)`),每行写明"这个域名是什么"(离线对照表 → 网页自己的标题 → 域名,认不出不编)。
398
469
  - **`GET /state` 补上防伪头**:四个路由里唯一一个没校验 `x-dsh-plugin` 的;新增的 `GET /logins` 同样强制该头(这份名单是用户的登录站点)。
@@ -419,7 +490,7 @@ MIT
419
490
 
420
491
  ### 0.3.0(2026-09-26)
421
492
 
422
- - **源码化**:插件重新变成有源码的工程 —— `src/` 10 个 TypeScript 模块是唯一真源,`lib/` 由 `npm run build` 生成(10 个 .js + 10 个 .d.ts),不再手改产物。
493
+ - **源码化**:插件重新变成有源码的工程 —— `src/` 10 个 TypeScript 模块(当时;现在 `src/` 是 32 个 `.ts`,`lib/` 是 32 个 `.js` + 32 个 `.d.ts`)是唯一真源,`lib/` 由 `npm run build` 生成,不再手改产物。
423
494
  - **代码规范**:引入 Prettier 配置(`.prettierrc.json`),`npm run format` / `npm run format:check` 统一格式;严格类型检查 0 错误。
424
495
  - **新增两只诊断工具**:`browser_console_messages`(控制台报错)与 `browser_network_requests`(网络请求),用于"点了没反应"时快速定位原因。
425
496
  - **接口与实现同步**:`BrowserSession` 补齐 9 个实现里已有的方法;`errors.ts` 补齐 12 个实际在用的错误码;`BrowserSnapshot` 补 `dialogNote` / `landingNote`;`TabInfo` 补 `active`;`runtime-state` / `cdp-policy` 新增类型声明。