@angelky7/dsh-browser-control 0.0.0-stage → 1.1.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/README.en.md ADDED
@@ -0,0 +1,317 @@
1
+ # DSH Browser Control
2
+
3
+ <p align="center">
4
+ <img src="extension/icons/icon128.png" width="100" alt="DSH Browser Control">
5
+ </p>
6
+
7
+ <p align="center">
8
+ <a href="README.md">简体中文</a> · <a href="README.en.md">English</a>
9
+ </p>
10
+
11
+ <p align="center">
12
+ <a href="https://github.com/angelky7/dsh-browser-control/releases"><img src="https://img.shields.io/github/v/release/angelky7/dsh-browser-control" alt="release"></a>
13
+ <a href="https://github.com/angelky7/dsh-browser-control/blob/main/LICENSE"><img src="https://img.shields.io/github/license/angelky7/dsh-browser-control" alt="license"></a>
14
+ <a href="https://awesome-dsh-plugin.com"><img src="https://awesome-dsh-plugin.com/badge.svg" alt="Awesome DSH Plugin"></a>
15
+ <a href="https://developer.chrome.com/docs/extensions/develop/migrate/mv2-deprecation-timeline"><img src="https://img.shields.io/badge/Chrome-MV3-yellow" alt="chrome mv3"></a>
16
+ <a href="https://github.com/topics/dsh-plugin"><img src="https://img.shields.io/badge/DSH-Plugin-purple" alt="dsh plugin"></a>
17
+ <img src="https://img.shields.io/badge/CDP-powered-orange" alt="cdp">
18
+ <img src="https://img.shields.io/badge/tools-11-red" alt="11 browser tools">
19
+ <img src="https://img.shields.io/badge/tests-29%2F29-brightgreen" alt="tests">
20
+ </p>
21
+
22
+ A Chrome extension + DeepSeek Harness plugin that lets AI agents drive your real browser like a human.
23
+
24
+ <p align="center">
25
+ <img src="assets/banner.png" width="480" alt="DSH Browser Control — a whale searching Google with a mouse">
26
+ </p>
27
+
28
+ ## What is this
29
+
30
+ Not a headless browser, not Puppeteer — your **real Chrome**, with your logins and cookies. The AI drives tabs through the Chrome DevTools Protocol while you watch every step on screen.
31
+
32
+ ```
33
+ You say one sentence to the AI
34
+ ↓
35
+ Agent calls browser_* tools
36
+ ↓
37
+ DSH plugin (WebSocket bridge)
38
+ ↓
39
+ Chrome extension (CDP)
40
+ ↓
41
+ Your real browser performs the action
42
+ ↓
43
+ Result returns to the Agent
44
+ ```
45
+
46
+ ## How it differs from MCP browser solutions
47
+
48
+ Browsers via MCP (Playwright MCP, Puppeteer MCP, browser-use…) share one trait: they launch a **fresh browser instance they downloaded themselves**. This project takes the other road:
49
+
50
+ | | This project | Playwright / Puppeteer MCP |
51
+ |---|---|---|
52
+ | Browser | The real Chrome you are using | Separate auto-downloaded instance |
53
+ | Logins / Cookies | ✅ Fully inherited, no re-login | ❌ Fresh profile every time |
54
+ | CAPTCHAs / QR login | Rarely hit — your sessions stay logged in | Frequently stuck at login walls |
55
+ | Visibility | Live on your screen, tabs the agent opens land in a `DSH` group, grab the mouse anytime | Headless or separate window |
56
+ | Environment deps | No Node / npx / Python needed | Needs npx or uvx runtime |
57
+ | Setup | Load extension + settings toggle | Edit MCP client JSON config |
58
+ | Disk usage | Reuses existing Chrome, zero extra | Downloads hundreds of MB |
59
+ | Integration depth | Native dsh plugin (status page / cleanup button; whether a settings card exists depends on the host version, see below) | Generic MCP server |
60
+
61
+ In one line: **for "use MY browser" tasks (logged-in Bilibili, Zhihu, admin panels), use this project; for generic cross-browser test automation, use MCP.**
62
+
63
+ ## Download
64
+
65
+ | File | Purpose |
66
+ |---|---|
67
+ | [DSH-Browser-Control-v1.0.19.zip](https://github.com/angelky7/dsh-browser-control/releases/download/v1.0.19/DSH-Browser-Control-v1.0.19.zip) | Chrome extension (unzip and load; standard zip header, Explorer can expand it directly) |
68
+ | [dsh-browser-control-plugin-v1.0.19.zip](https://github.com/angelky7/dsh-browser-control/releases/download/v1.0.19/dsh-browser-control-plugin-v1.0.19.zip) | dsh plugin (offline fallback; online installs use Option A/B) |
69
+
70
+ ## Install the Chrome extension (30 seconds)
71
+
72
+ Download the zip → unzip to a fixed folder (don't delete it) → open `chrome://extensions` → enable Developer mode → click "Load unpacked" → pick the unzipped folder.
73
+
74
+ Whale icon in the toolbar = success. Requires Chrome 116+.
75
+
76
+ ## Install the dsh plugin
77
+
78
+ 📦 This package is a bundle (`package.json` points `dsh.bundle.patch` at `cordis.patch.yml`). A successful `dsh plugin add` registers it under the profile's `dsh.profile.bundles`; a restart loads it.
79
+
80
+ Prerequisite: `dsh plugin` forwards to pnpm, so pnpm must be on PATH; the target profile is initialized automatically on first use.
81
+
82
+ ### Option A: install from npm (recommended)
83
+
84
+ ```bash
85
+ # Install from the npm registry and register it with the profile
86
+ dsh plugin --profile web add @angelky7/dsh-browser-control
87
+ ```
88
+
89
+ If you manage the profile's node_modules yourself, plain npm works there too:
90
+
91
+ ```bash
92
+ npm install @angelky7/dsh-browser-control
93
+ ```
94
+
95
+ > **A version published minutes ago may not install yet, and that is not a broken package.** `dsh plugin add` pins the dependency to an **exact version**, while pnpm resolves it from **your configured registry** — for many users that is `registry.npmmirror.com`, which syncs from the official registry with a delay (measured: the official registry had 1.0.10 published while the mirror's `latest` still said 1.0.8). If you hit `ERR_PNPM_NO_MATCHING_VERSION ... while fetching it from https://registry.npmmirror.com/`, pick one:
96
+ >
97
+ > 1. Use the official registry for this install: `dsh plugin --profile web add @angelky7/dsh-browser-control@<version> --registry=https://registry.npmjs.org/`
98
+ > 2. Use the offline asset from the Release, `dsh-browser-control-plugin-v<version>.zip`: unzip it, then `dsh plugin --profile web add file:<the unzipped directory>`
99
+ > 3. Wait for the mirror and retry: `curl -s https://registry.npmmirror.com/@angelky7/dsh-browser-control` — once the version appears there and `dist-tags.latest` moved, the plain command works
100
+ >
101
+ > **A second failure looks similar but has the opposite cause — do not touch your registry for it.** dsh profiles ship a 24-hour package-age supply-chain gate (`minimumReleaseAge: 1440` in `node_modules/.pnpm-workspace-state-v1.json`). A version younger than that is rejected with `ERR_PNPM_NO_MATURE_MATCHING_VERSION`, and the message quotes `was published at …, within the minimumReleaseAge cutoff (…)` — the version **does** exist, it is just too new. Waiting past the cutoff is the only fix. Discriminator: `NO_MATCHING_VERSION` = your registry does not have that version; `NO_MATURE_MATCHING_VERSION` = it has it, but the version is younger than 24h.
102
+
103
+ ### Option B: install from GitHub or a local directory
104
+
105
+ ```bash
106
+ # Straight from GitHub
107
+ dsh plugin --profile web add "github:angelky7/dsh-browser-control#v1.0.19"
108
+
109
+ # Local checkout for debugging (note: the explicit file: prefix is required)
110
+ dsh plugin --profile web add "file:D:\path\to\dsh-browser-control"
111
+ ```
112
+
113
+ Restart DSH to load it. Uninstall:
114
+
115
+ ```bash
116
+ dsh plugin --profile web remove @angelky7/dsh-browser-control
117
+ ```
118
+
119
+ > ⚠️ Always use the `file:` prefix for local directories. Bare / relative paths
120
+ > are treated as the `link:` protocol by pnpm, which does not materialize into
121
+ > node_modules top-level under hoisted layouts and fails to resolve at boot.
122
+
123
+ After installing and restarting, the bridge is on by default (v1.0.6+); no manual enable step. The status page at http://127.0.0.1:9777/ confirms the listener is up.
124
+
125
+ > To opt out: in your profile override layer `$DSH_HOME/profiles/<name>/cordis.patch.yml`, write
126
+ > `- id: browser-bridge` with `config: { enabled: false }` (see [dsh-config/README.md](dsh-config/README.md)).
127
+ > Do not change the in-package `cordis.patch.yml` back to `enabled: false` — that value overrides the
128
+ > schema default and shows up as "nothing listens on 9777 after install" (issue #4).
129
+
130
+ ### Option C: copy into the harness tree (legacy, v1.0.2 and earlier)
131
+
132
+ ```bash
133
+ git clone https://github.com/angelky7/dsh-browser-control.git
134
+ cd dsh-browser-control
135
+ git checkout v1.0.2 # legacy layout lives at the v1.0.2 tag
136
+ ./install.sh /path/to/deepseek-harness
137
+ ```
138
+
139
+ The script only copies plugin files into place — **you still need the three manual config edits**, then restart dsh:
140
+
141
+ Download [`dsh-browser-bridge-plugin-v1.0.2.zip`](https://github.com/angelky7/dsh-browser-control/releases/download/v1.0.2/dsh-browser-bridge-plugin-v1.0.2.zip) and unzip into `deepseek-harness/packages/web/browser-bridge/`.
142
+
143
+ Then add three pieces of config:
144
+
145
+ 1. In `packages/bundle/base/package.json` dependencies:
146
+
147
+ ```json
148
+ "@deepseek-ai/dsh-browser-bridge": "workspace:^"
149
+ ```
150
+
151
+ 2. In `cordis.patch.yml` plugins list:
152
+
153
+ ```yaml
154
+ - id: browser-bridge
155
+ name: '@deepseek-ai/dsh-browser-bridge'
156
+ config:
157
+ enabled: false
158
+ ```
159
+
160
+ 3. In `tsconfig.host.json` references:
161
+
162
+ ```json
163
+ { "path": "./packages/web/browser-bridge" }
164
+ ```
165
+
166
+ Restart dsh. Details in [dsh-config/README.md](dsh-config/README.md) (this path targets the pre-v1.0.3 layout, where `enabled: false` really was the default).
167
+
168
+ ## Usage
169
+
170
+ 1. It is already on after install (v1.0.6+); no manual enable step
171
+ 2. The extension connects automatically (port 9777, default token dsh-local)
172
+ 3. Talk in natural language; the agent drives the browser
173
+
174
+ ### Whether a settings card exists depends on the host (issue #5)
175
+
176
+ The published bundle ships only the host-side `lib/index.js`; there is no browser-side module, so the card is not drawn by this plugin:
177
+
178
+ | Host | Settings page | How to change config |
179
+ |---|---|---|
180
+ | dsh 0.1.7+ / 0.2.x (dsh-settings 0.1.7-rc.2 onwards) | Yes — the host projects this plugin's own `Config` schema into a section | Settings page, or the profile override layer |
181
+ | dsh 0.1.2 and earlier | No entry (no client half claims the namespace, so nothing renders) | Profile override layer only: `$DSH_HOME/profiles/<name>/cordis.patch.yml` |
182
+
183
+ The config file works on both; see [dsh-config/README.md](dsh-config/README.md).
184
+
185
+ Visit `http://127.0.0.1:9777/` for connection status.
186
+
187
+ ### Read-only mode and a separate script switch
188
+
189
+ In your profile overlay, on `browser-bridge`:
190
+
191
+ ```yaml
192
+ config:
193
+ readOnly: true # reads only: click/input/press/nav/scroll/tabs/emulate/… are refused
194
+ allowScript: false # also turn off browser_evaluate (arbitrary JS in the page)
195
+ ```
196
+
197
+ Refused commands are **never sent to the browser** (zero side effects), come back with
198
+ `blocked_by_policy`, and say which field to change. `readOnly` alone does **not** remove arbitrary
199
+ script execution — `browser_evaluate` can do everything the other tools can, which is why it is a
200
+ separate switch. These two govern the tool surface the model drives; the HTTP face (token-gated) is
201
+ your own channel and is unaffected.
202
+
203
+ ### What agent tabs look like (automatic DSH group)
204
+
205
+ Tabs the agent **opens** are collected into a blue `DSH` tab group, one per window, so at a glance you can tell which pages a machine is driving and sweep them away afterwards:
206
+
207
+ - `browser_tabs` `action: "ungroup"` dissolves the group (pass `windowId` for one window, omit it for all) — it releases the grouping, it does not close the tabs.
208
+ - `browser_tabs` `action: "open"` with `group: false` keeps a new tab ungrouped; `action: "group"` pulls an already-existing loose tab in.
209
+ - Grouping is a nicety, never a prerequisite: on an old Chrome, without the `tabGroups` permission, or when Chrome's own tab bookkeeping wins the race, **opening the tab still succeeds** and the result carries `grouped: false` plus a reason. Grouping never changes page content, so retrying it is safe.
210
+ - `browser_tabs` `action: "close"` takes one tab (`tabId`) or a batch (`tabIds`), and a batch goes out as **one command**. The result accounts for every id — `closed` / `alreadyGone` (already gone = idempotent success, safe to retry) / `failed` (with the error) — so a partial failure is never swallowed, and single and batch share one shape.
211
+
212
+ ### One-click freeze: the extension popup's lock
213
+
214
+ Click the extension icon and hit **锁定** in the middle of the button row — a single click is enough.
215
+ Every browser command is then refused before it runs (click, input, press, navigation, script
216
+ execution), while the WebSocket stays up: a caller sees "reachable but frozen", not a dropped link,
217
+ so it does not retry what looks like a network failure.
218
+
219
+ - There is exactly one way out: click the same button (now **已锁定**) in that popup. The bridge has no
220
+ unlock command and neither does the tool surface.
221
+ - The flag lives in `chrome.storage.local`, so an MV3 service-worker eviction cannot silently unfreeze it.
222
+ - If that flag cannot be **read**, the extension freezes anyway (fail closed): a panic switch that is
223
+ believed-on but actually off is worth nothing. The refusal and the popup then say "lock state unreadable"
224
+ instead of blaming you for a lock you never pressed, and one click in the popup still writes a definite state.
225
+ - The toolbar badge shows a red `LOCK` while frozen instead of a reassuring green `ON`.
226
+ - Why this lives in the client rather than the bridge: the bridge token sits in the profile's
227
+ `cordis.patch.yml`, which the agent can read *and* rewrite. A freeze that only a human can lift has to
228
+ be enforced somewhere the agent cannot reach.
229
+
230
+ ### Calling the HTTP face from a script
231
+
232
+ `/api/command` and `/api/cleanup` now require the token — the same value the extension presents on the WebSocket:
233
+
234
+ ```bash
235
+ curl -X POST http://127.0.0.1:9777/api/command \
236
+ -H 'Authorization: Bearer <your token>' \
237
+ -H 'Content-Type: application/json' \
238
+ -d '{"command":"nav","params":{"url":"https://example.com"}}'
239
+ ```
240
+
241
+ `?token=<your token>` and `X-Token: <your token>` work too. Missing or wrong token is a 401; `/` and `/api/status` stay unauthenticated read-only. What used to work without a token no longer does (issue #3 / #6). The cleanup button on the status page asks for the token once and keeps it in the browser session only.
242
+
243
+ ## Tools
244
+
245
+ | Tool | Purpose |
246
+ |---|---|
247
+ | `browser_navigate` | Navigate to a URL |
248
+ | `browser_read` | Read page text/HTML — the text itself, in an `offset`/`limit` window |
249
+ | `browser_snapshot` | Page snapshot → the ref list itself (`mode:"ax"` reads Chrome's accessibility tree: role + accessible name) |
250
+ | `browser_click` | Click an element (by ref / selector) |
251
+ | `browser_type` | Type into inputs |
252
+ | `browser_press` | Send keyboard keys (Enter, Tab, arrows, and combinations like "Ctrl+A" / "Shift+ArrowRight"; one press inserts exactly once) |
253
+ | `browser_scroll` | Scroll the page |
254
+ | `browser_wait` | Wait for an element / text / page condition (up to 120s, v1.0.9+) |
255
+ | `browser_find` | Query elements by CSS selector, returning count and viewport rects (read-only, v1.0.9+) |
256
+ | `browser_dialog` | Read or set the native-dialog policy accept/dismiss/manual, plus the recent dialog log (v1.0.9+) |
257
+ | `browser_hold` | Put or remove a cancellable take-over overlay on a tab (tells the user, stops mis-clicks); `covered:false` says why it could not cover the page. A soft layer, not a security boundary |
258
+ | `browser_tabs` | Tab management (list/open/close/activate/group/ungroup; see "What agent tabs look like") |
259
+ | `browser_evaluate` | Run arbitrary JS |
260
+ | `browser_screenshot` | Capture page screenshot |
261
+ | `browser_console_log` | Captured console entries, listed with their text (v1.0.7+) |
262
+ | `browser_network_log` | Captured HTTP requests, listed with method/status/URL (v1.0.7+) |
263
+ | `browser_network_clear` | Clear the network log buffer on its own (v1.0.7+) |
264
+ | `browser_pdf` | Export the current page as PDF (v1.0.7+) |
265
+ | `browser_emulate` | Switch to a device viewport (mobile / desktop / custom, v1.0.7+) |
266
+ | `browser_cleanup` | Clean up temp files |
267
+ | `browser_info` | Which browser and extension build is attached (run it first when debugging, v1.0.9+) |
268
+
269
+ 21 tools in total. `browser_wait`, `browser_find`, `browser_dialog` and `browser_info` existed inside the extension up to v1.0.8 but were never registered as tools, so the agent could not reach them; `browser_network_clear` existed in code and was simply missing from this table. `verify/verify_surface.mjs` now reconciles the extension command table, the commands the plugin calls, this table and the built bundle, so any drift fails `npm run verify`.
270
+
271
+ ## Architecture
272
+
273
+ ```
274
+ Chrome browser
275
+ └─ DSH Browser Control extension (MV3)
276
+ └─ chrome.debugger (CDP)
277
+ └─ WebSocket ──────→ DSH plugin (browser-bridge)
278
+ └─ browser_* tools → Agent
279
+ ```
280
+
281
+ **Key design:**
282
+ - Extension dials out to the bridge (no native messaging host)
283
+ - Off by default; enabled manually from Settings
284
+ - Persistent debugger attachment — banner stays visible during control
285
+ - Listens on 127.0.0.1 only; the command surface (WebSocket and HTTP) shares one token, and no cross-origin access is granted
286
+ - The wire protocol has its own version, bumped only when the frame contract changes; a mismatch is named on the status page
287
+
288
+ ## Verified
289
+
290
+ | Scenario | Result |
291
+ |---|---|
292
+ | Baidu search → extract result titles | ✅ |
293
+ | Bilibili user search → send DM | ✅ |
294
+ | Bilibili search → count video cards + screenshot | ✅ (real browser, 1.0.x era) |
295
+ | `npm run verify` harness: config resolution, three host shapes, HTTP auth and upgrade origin, published-bundle behaviour | ✅ 2026-10-10, see [verify/README.md](verify/README.md) |
296
+ | `verify_tab_groups` harness: group creation / reuse / backoff retry / stale cache / per-window dissolve / graceful degrade | ✅ 2026-10-11, node:vm plus a stateful fake `chrome.tabs`/`chrome.tabGroups` — **not re-run in a real Chrome** |
297
+ | `verify_extension_e2e` harness: real bridge + real extension code + real WebSocket end to end (frame contract, command reachability, error classification) | ✅ 2026-10-11 — only `chrome.*` is faked; visuals and real CDP behaviour still need a live browser |
298
+ | `verify_cdp_retry` harness: session recovery after Chrome steals the attach, replay must not re-run side effects, bounded retries (6 scenarios) | ✅ 2026-10-11, real `background.js` against a stateful fake `chrome.debugger`; cross-checked on a merge-preview build |
299
+ | `verify_render_contract` harness: every tool's real return value goes through the host's own output validator **and** its own `render`, asserting the model-facing text actually contains the data the call asked for | ✅ 2026-10-12, real bridge + fake extension + host `validateJsonSchemaValue`; this layer had **zero coverage** before, which is how two defect classes survived 12 / 5 released versions |
300
+ | **Real host × real browser**: local dsh `0.2.0-rc.2` mounted this plugin from an isolated `DSH_HOME`, the user's actual Chrome attached to the bridge, and 9 read-only tools ran through the real code path | ✅ 2026-10-12, Chrome 153 + extension 1.0.10: `browser_read` returned 1,341 chars of real page text, `browser_snapshot` returned 14 ref-bearing lines, zero output-schema violations; **this is also the run that exposed an older extension silently ignoring `mode:"ax"`** (local extension was 1.0.10, predating the 1.0.11 AX snapshot) — the plugin now reports `axFallback` explicitly |
301
+ | `tsc --noEmit` + `publint` | ✅ 2026-10-10 |
302
+
303
+ There have been no unit tests since v1.0.4 removed `tests/`; the three browser rows above are historical runs that were not repeated for this change, while every claim in this release comes from the repeatable harness under `verify/`.
304
+
305
+ ## Changelog
306
+
307
+ See [CHANGELOG.md](CHANGELOG.md).
308
+
309
+ ## License
310
+
311
+ This project is licensed under the **GNU Affero General Public License v3.0 (AGPL-3.0)**.
312
+
313
+ - **Personal / academic / non-commercial use**: completely free — use, modify, and distribute freely under AGPL-3.0 terms
314
+ - **Enterprise / commercial use**: AGPL-3.0 treats networked use as distribution, requiring derivative code to be published. If you want to embed this project in closed-source products or build a SaaS on it without open-sourcing, contact the author for a **commercial license** (terms negotiated separately)
315
+ - **Commercial licensing inquiries**: [GitHub Issues](https://github.com/angelky7/dsh-browser-control/issues) or email **caob2333@outlook.com**
316
+
317
+ See the [LICENSE](LICENSE) file for the full license text (AGPL-3.0).
package/README.md CHANGED
@@ -1,3 +1,311 @@
1
- # Temporary Holding Version
1
+ # DSH Browser Control
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ <p align="center">
4
+ <img src="extension/icons/icon128.png" width="100" alt="DSH Browser Control">
5
+ </p>
6
+
7
+ <p align="center">
8
+ <a href="README.md">简体中文</a> · <a href="README.en.md">English</a>
9
+ </p>
10
+
11
+ <p align="center">
12
+ <a href="https://github.com/angelky7/dsh-browser-control/releases"><img src="https://img.shields.io/github/v/release/angelky7/dsh-browser-control" alt="release"></a>
13
+ <a href="https://github.com/angelky7/dsh-browser-control/blob/main/LICENSE"><img src="https://img.shields.io/github/license/angelky7/dsh-browser-control" alt="license"></a>
14
+ <a href="https://awesome-dsh-plugin.com"><img src="https://awesome-dsh-plugin.com/badge.svg" alt="Awesome DSH Plugin"></a>
15
+ <a href="https://developer.chrome.com/docs/extensions/develop/migrate/mv2-deprecation-timeline"><img src="https://img.shields.io/badge/Chrome-MV3-yellow" alt="chrome mv3"></a>
16
+ <a href="https://github.com/topics/dsh-plugin"><img src="https://img.shields.io/badge/DSH-Plugin-purple" alt="dsh plugin"></a>
17
+ <img src="https://img.shields.io/badge/CDP-powered-orange" alt="cdp">
18
+ <img src="https://img.shields.io/badge/tools-11-red" alt="11 browser tools">
19
+ <img src="https://img.shields.io/badge/tests-29%2F29-brightgreen" alt="tests">
20
+ </p>
21
+
22
+ Chrome 浏览器扩展 + DeepSeek Harness 插件,让 AI Agent 像人一样操控你的真实浏览器。
23
+
24
+ <p align="center">
25
+ <img src="assets/banner.png" width="480" alt="DSH Browser Control — a whale searching Google with a mouse">
26
+ </p>
27
+
28
+ ## 这是什么
29
+
30
+ 不是无头浏览器,不是 Puppeteer——是你的**真实 Chrome**,带着你的登录态、你的 cookies。AI 通过 Chrome DevTools Protocol 驱动标签页,你可以在屏幕上看到每一步操作。
31
+
32
+ ```
33
+ 你对 AI 说一句话
34
+ ↓
35
+ Agent 调用 browser_* 工具
36
+ ↓
37
+ DSH 插件(WebSocket 桥)
38
+ ↓
39
+ Chrome 扩展(CDP 驱动)
40
+ ↓
41
+ 你的真实浏览器执行操作
42
+ ↓
43
+ 结果返回给 Agent
44
+ ```
45
+
46
+ ## 和 MCP 浏览器方案的区别
47
+
48
+ 市面上已经有 Playwright MCP、Puppeteer MCP、browser-use 等,它们的共同点:启动一个**自己下载的全新浏览器实例**。本项目走的是另一条路:
49
+
50
+ | | 本项目 | Playwright / Puppeteer MCP |
51
+ |---|---|---|
52
+ | 浏览器 | 你正在用的真实 Chrome | 自动下载的独立实例 |
53
+ | 登录态 / Cookies | ✅ 全部继承,无需重新登录 | ❌ 每次全新 profile |
54
+ | 过验证码 / 扫码登录 | 你的会话已经登录,基本不遇到 | 经常卡在登录墙 |
55
+ | 可见性 | 屏幕上实时可见,agent 新建的标签自动归进 `DSH` 分组,随时鼠标接管 | 无头运行或独立窗口 |
56
+ | 环境依赖 | 无需 Node / npx / Python | 需要 npx 或 uvx 运行时 |
57
+ | 接入方式 | 加载扩展 + 设置页开关 | 编辑 MCP 客户端 JSON 配置 |
58
+ | 磁盘占用 | 复用现有 Chrome,零新增 | 额外下载数百 MB 浏览器 |
59
+ | 集成深度 | dsh 原生插件(状态页 / 清理按钮;设置页卡片由宿主版本决定,见下) | 通用 MCP server |
60
+
61
+ 一句话:**要 AI 用"你自己的"浏览器干活(已登录的 B 站、知乎、淘宝后台),用本项目;要做跨浏览器、跨应用的通用自动化测试,用 MCP。**
62
+
63
+ ## 下载
64
+
65
+ | 文件 | 说明 |
66
+ |---|---|
67
+ | [DSH-Browser-Control-v1.0.19.zip](https://github.com/angelky7/dsh-browser-control/releases/download/v1.0.19/DSH-Browser-Control-v1.0.19.zip) | Chrome 扩展(解压后加载;标准 zip 头,资源管理器可直接展开) |
68
+ | [dsh-browser-control-plugin-v1.0.19.zip](https://github.com/angelky7/dsh-browser-control/releases/download/v1.0.19/dsh-browser-control-plugin-v1.0.19.zip) | dsh 插件(离线兜底,在线装直接用方式 A/B) |
69
+
70
+ ## 安装 Chrome 扩展(30 秒)
71
+
72
+ 下载 zip → 解压到固定文件夹(别删)→ Chrome 打开 `chrome://extensions` → 开启「开发者模式」→ 点「加载已解压的扩展程序」→ 选解压后的文件夹。
73
+
74
+ 工具栏出现鲸鱼图标 → 绿点呼吸 = 已连接。需要 Chrome 116+。
75
+
76
+ ## 安装 dsh 插件
77
+
78
+ 📦 本包是 bundle 包(package.json 中 `dsh.bundle.patch` 指向 `cordis.patch.yml`)。`dsh plugin` 安装成功后会自动把它加入 profile 的 `dsh.profile.bundles`,重启即加载。
79
+
80
+ 前置:`dsh plugin` 转发给 pnpm,需要 pnpm 在 PATH 上;首次使用会自动初始化目标 profile。
81
+
82
+ ### 方式 A:从 npm 安装(推荐)
83
+
84
+ ```bash
85
+ # 通过 dsh plugin 从 npm registry 安装并自动注册到 profile
86
+ dsh plugin --profile web add @angelky7/dsh-browser-control
87
+ ```
88
+
89
+ 如果自行管理 profile 的 node_modules,也可以在对应目录中直接使用 npm 安装:
90
+
91
+ ```bash
92
+ npm install @angelky7/dsh-browser-control
93
+ ```
94
+
95
+ > **刚发布的新版本可能一时装不上,那不是插件坏了。** `dsh plugin add` 会把依赖钉成**确切版本号**,而 pnpm 用的是**你自己配置的源**——国内默认常常是 `registry.npmmirror.com`,它同步官方源有延迟(实测 1.0.10 在官方源已发布时,镜像的 `latest` 还停在 1.0.8)。撞上 `ERR_PNPM_NO_MATCHING_VERSION ... while fetching it from https://registry.npmmirror.com/` 时有三条路:
96
+ >
97
+ > 1. 这一次显式走官方源:`dsh plugin --profile web add @angelky7/dsh-browser-control@<版本> --registry=https://registry.npmjs.org/`
98
+ > 2. 用 Release 里的离线包 `dsh-browser-control-plugin-v<版本>.zip`,解压后 `dsh plugin --profile web add file:<解压目录>`
99
+ > 3. 等镜像同步后重试原命令:`curl -s https://registry.npmmirror.com/@angelky7/dsh-browser-control`,看到该版本、且 `dist-tags.latest` 也跟上即可
100
+ >
101
+ > **另一种装不上长得像、原因完全不同,别改 registry。** dsh 的 profile 自带 24 小时「包龄」供应链闸门(`node_modules/.pnpm-workspace-state-v1.json` 里的 `minimumReleaseAge: 1440`)。发布未满 24 小时的版本被它拦下时报的是 `ERR_PNPM_NO_MATURE_MATCHING_VERSION`,而且报文里会直接写 `was published at …, within the minimumReleaseAge cutoff (…)`——版本是**存在**的,只是太新。这种只有等满 cutoff 时刻这一条路(改 registry、离线包都不必)。两个码的判据:`NO_MATCHING_VERSION` = 你配置的源里没有这个版本;`NO_MATURE_MATCHING_VERSION` = 有,但没满 24 小时。
102
+
103
+ ### 方式 B:从 GitHub 或本地目录安装
104
+
105
+ ```bash
106
+ # 直接从 GitHub 安装
107
+ dsh plugin --profile web add "github:angelky7/dsh-browser-control#v1.0.19"
108
+
109
+ # 本地目录调试(注意:必须显式 file: 前缀)
110
+ dsh plugin --profile web add "file:D:\path\to\dsh-browser-control"
111
+ ```
112
+
113
+ 重启 DSH 后生效。卸载:
114
+
115
+ ```bash
116
+ dsh plugin --profile web remove @angelky7/dsh-browser-control
117
+ ```
118
+
119
+ > ⚠️ 本地目录请用 `file:` 前缀。裸路径 / 相对路径会被 pnpm 当作 `link:` 协议,
120
+ > 在 hoisted 布局下不会物化到 node_modules 顶层,导致启动时无法解析该包。
121
+
122
+ 安装并重启后,桥接默认开启(v1.0.6+),不需要再去设置里手动启用。状态页 http://127.0.0.1:9777/ 可看到服务已监听。
123
+
124
+ > 想关掉默认开启:在你的 profile 覆盖层 `$DSH_HOME/profiles/<名字>/cordis.patch.yml` 里写
125
+ > `- id: browser-bridge` + `config: { enabled: false }` 即可(写法见 [dsh-config/README.md](dsh-config/README.md))。
126
+ > 注意包内 `cordis.patch.yml` 里那份 `enabled: true` 不要改回 false —— 那会盖掉 schema 默认值,
127
+ > 表现为装上后 9777 无人监听(issue #4)。
128
+
129
+ ### 方式 C:复制进 harness 源码树(旧方式,v1.0.2 及以前)
130
+
131
+ ```bash
132
+ git clone https://github.com/angelky7/dsh-browser-control.git
133
+ cd dsh-browser-control
134
+ git checkout v1.0.2 # 旧布局在 v1.0.2 tag
135
+ ./install.sh /你的路径/deepseek-harness
136
+ ```
137
+
138
+ 脚本只负责把插件文件复制到位,**完成后仍需手动改三处配置**,改完重启 dsh 才会生效:
139
+
140
+ 下载 [`dsh-browser-bridge-plugin-v1.0.2.zip`](https://github.com/angelky7/dsh-browser-control/releases/download/v1.0.2/dsh-browser-bridge-plugin-v1.0.2.zip),解压到 deepseek-harness 的 `packages/web/browser-bridge/`。
141
+
142
+ 然后补充三处配置:
143
+
144
+ 1. `packages/bundle/base/package.json` 的 dependencies 加:
145
+
146
+ ```json
147
+ "@deepseek-ai/dsh-browser-bridge": "workspace:^"
148
+ ```
149
+
150
+ 2. `cordis.patch.yml` 的 plugins 列表加:
151
+
152
+ ```yaml
153
+ - id: browser-bridge
154
+ name: '@deepseek-ai/dsh-browser-bridge'
155
+ config:
156
+ enabled: false
157
+ ```
158
+
159
+ 3. `tsconfig.host.json` 的 references 加:
160
+
161
+ ```json
162
+ { "path": "./packages/web/browser-bridge" }
163
+ ```
164
+
165
+ 重启 dsh。详细说明见 [dsh-config/README.md](dsh-config/README.md)(注意这一条路径针对 v1.0.2 及以前的旧布局,当时的默认就是 `enabled: false`)。
166
+
167
+ ## 使用
168
+
169
+ 1. 装好即默认开启(v1.0.6+),不需要手动启用
170
+ 2. Chrome 扩展自动连接(端口 9777,Token 默认 dsh-local)
171
+ 3. 对话说自然语言,Agent 自动操控浏览器
172
+
173
+ ### 「设置 → 插件」里有没有这张卡片,取决于宿主版本(issue #5)
174
+
175
+ npm 发布的 bundle 只含宿主侧 `lib/index.js`,没有浏览器端模块,所以卡片不是插件自己画的:
176
+
177
+ | 宿主 | 设置页 | 改配置的办法 |
178
+ |---|---|---|
179
+ | dsh 0.1.7+ / 0.2.x(dsh-settings 0.1.7-rc.2 起) | 有——宿主按插件自身 `Config` schema 自动投影出这一节 | 设置页,或 profile 覆盖层 |
180
+ | dsh 0.1.2 及更早 | 没有条目(没有客户端半区认领命名空间,宿主就不渲染) | 只能改 profile 覆盖层 `$DSH_HOME/profiles/<名字>/cordis.patch.yml` |
181
+
182
+ 两边都能改的就是配置文件,写法见 [dsh-config/README.md](dsh-config/README.md)。
183
+
184
+ 访问 `http://127.0.0.1:9777/` 查看连接状态。
185
+
186
+ ### 不想让它点,只想让它看:只读档与脚本开关
187
+
188
+ profile 覆盖层里给 `browser-bridge` 配两项(完整写法见 [dsh-config/README.md](dsh-config/README.md#两个闸门)):
189
+
190
+ ```yaml
191
+ config:
192
+ readOnly: true # 只读:点击/输入/按键/导航/滚动/标签页操作/视口模拟全拒
193
+ allowScript: false # 再把 browser_evaluate(在页面里跑任意 JS)关掉
194
+ ```
195
+
196
+ 被拦的命令**不会发到浏览器**(零副作用),错误码 `blocked_by_policy`,文案写明改哪个字段。
197
+ 注意 `readOnly` 单独打开**不等于**拿掉了任意脚本能力:`browser_evaluate` 能做其它工具能做的所有事,
198
+ 所以「能不能执行脚本」是单独一项。这两个开关只管模型那条工具面,HTTP 面(带 token)不受影响。
199
+
200
+ ### agent 开的标签长什么样(自动归进 DSH 分组)
201
+
202
+ Agent **新建**的标签页会自动收进当前窗口的一个 `DSH` 蓝色标签分组里(每个窗口一组),所以你扫一眼就知道哪些页面是机器开的、哪些是自己开的,事后也能一把清掉:
203
+
204
+ - `browser_tabs` `action: "ungroup"` 拆掉本窗口的组(带 `windowId` 只拆那一个窗口,不带则所有窗口)——只拆标签归属,不关标签。
205
+ - `browser_tabs` `action: "open"` 加 `group: false` 可以显式让新标签留在组外;`action: "group"` 能把早就存在的散标签拉进来。
206
+ - 归组是锦上添花:Chrome 版本太旧、权限没给、或刚建标签就被用户拖动导致失败时,**开标签本身照常成功**,结果里带 `grouped: false` 和原因。分组动作不改页面内容,重试安全。
207
+ - `browser_tabs` `action: "close"` 一次可以关一个(`tabId`)或一批(`tabIds`);一批在链路上只发**一条命令**。结果把每个 id 的去向都写出来:`closed` / `alreadyGone`(早就没了=幂等成功,重试安全)/ `failed`(带原因)——部分失败不会被吞掉,单数与批量同一个形状。
208
+
209
+ ### 一键冻结:扩展弹窗里的「锁定」
210
+
211
+ 点扩展图标,按钮行中间那个「锁定」,**单击即生效**:此后扩展对模型**拒绝一切浏览器操作**
212
+ (点击、输入、按键、导航、执行脚本全拦),而 WebSocket 连接照常——调用方看到的是
213
+ 「连得上但被冻结」,不是掉线,所以不会误当作网络问题去重试。
214
+
215
+ - 解锁只有一条路:**在同一个弹窗里点「已锁定」**。桥接侧没有任何解锁命令,模型的工具面里也没有。
216
+ - 冻结状态落在 `chrome.storage.local`,所以 MV3 把 service worker 回收掉也不会「自己解冻」。
217
+ - 万一**读不出**这个状态,扩展按「已冻结」处理(fail-closed):一个以为锁着、其实没锁的
218
+ panic switch 等于没有。这时拒绝文案与弹窗都会写「锁状态读不出来」,不会说成「已被用户锁定」,
219
+ 而你照样能在弹窗里点一下把状态写明确。
220
+ - 图标徽标在上锁期间显示红色 `LOCK`,不会继续显示绿色的 `ON` 骗人。
221
+ - 为什么客户端而不是桥接侧管这件事:桥的 token 就写在 profile 的 `cordis.patch.yml` 里,模型读得到也改得动;
222
+ 把「冻结」放在浏览器里、且只留一个人工通道,才真的挡得住。
223
+
224
+ ### 用脚本直连 HTTP(curl 等)
225
+
226
+ `/api/command` 与 `/api/cleanup` 现在要求 Token,用的就是扩展连 WS 时那个值:
227
+
228
+ ```bash
229
+ curl -X POST http://127.0.0.1:9777/api/command \
230
+ -H 'Authorization: Bearer <你的 token>' \
231
+ -H 'Content-Type: application/json' \
232
+ -d '{"command":"nav","params":{"url":"https://example.com"}}'
233
+ ```
234
+
235
+ 也可以写 `?token=<你的 token>` 或 `X-Token: <你的 token>`。不带、或带错,一律 401;`/` 与 `/api/status` 仍是免鉴权的只读信息。之前「不带 Token 也能调通命令面」的行为已经失效(issue #3 / #6)。状态页上的清理按钮会要求你把 Token 输一次,只记在浏览器会话里。
236
+
237
+ ## 工具清单
238
+
239
+ | 工具 | 功能 |
240
+ |---|---|
241
+ | `browser_navigate` | 导航到 URL |
242
+ | `browser_read` | 读取页面文本/HTML,正文直接给模型(`offset`/`limit` 窗口,长文分页) |
243
+ | `browser_snapshot` | 页面快照 → 逐条列出 ref 的交互树(`mode:"ax"` 走 Chrome 无障碍树,拿 role + 可访问名) |
244
+ | `browser_click` | 点击元素(by ref / selector) |
245
+ | `browser_type` | 在输入框填入文本 |
246
+ | `browser_press` | 模拟键盘按键(回车、Tab、方向键,以及 "Ctrl+A" / "Shift+ArrowRight" 这类组合键;一次按键只插入一次) |
247
+ | `browser_scroll` | 滚动页面 |
248
+ | `browser_wait` | 等元素出现 / 等文本出现 / 等条件为真(最长 120 秒,v1.0.9+) |
249
+ | `browser_find` | 按 CSS 选择器查元素,返回数量与位置尺寸(只读,v1.0.9+) |
250
+ | `browser_dialog` | 读改原生弹窗应答策略 accept/dismiss/manual,并回最近弹窗记录(v1.0.9+) |
251
+ | `browser_hold` | 在某个标签页上加/撤一层可取消的接管浮层(告知用户 + 防误触);`covered:false` 时说明盖不住的原因。软层,不是安全边界 |
252
+ | `browser_tabs` | 标签页管理(列表/新建/关闭/切换/归组/拆组;见「agent 开的标签长什么样」) |
253
+ | `browser_evaluate` | 执行任意 JS |
254
+ | `browser_screenshot` | 截取页面截图 |
255
+ | `browser_console_log` | 抓取页面 console 日志并逐条给出正文(v1.0.7+) |
256
+ | `browser_network_log` | 抓取 HTTP 请求/响应并逐条给出方法/状态/URL(v1.0.7+) |
257
+ | `browser_network_clear` | 单独清空网络日志缓冲(v1.0.7+) |
258
+ | `browser_pdf` | 当前页导出 PDF(v1.0.7+) |
259
+ | `browser_emulate` | 切设备视口(移动 / 桌面 / 自定义,v1.0.7+) |
260
+ | `browser_cleanup` | 清理临时文件 |
261
+ | `browser_info` | 报告实际连上的浏览器与扩展版本(排障先跑它,v1.0.9+) |
262
+
263
+ 共 21 个工具。`browser_wait` / `browser_find` / `browser_dialog` / `browser_info` 在 v1.0.8 及以前是「扩展里已实现、但没注册成工具」,agent 调不到;`browser_network_clear` 则是代码里有、这张表漏了。这四条现在由 `verify/verify_surface.mjs` 做三方对账(扩展命令表 ↔ 插件调用 ↔ 文档表 ↔ 构建产物),漂移会直接让 `npm run verify` 失败。
264
+
265
+ ## 架构
266
+
267
+ ```
268
+ Chrome 浏览器
269
+ └─ DSH Browser Control 扩展 (MV3)
270
+ └─ chrome.debugger (CDP)
271
+ └─ WebSocket ──────→ DSH 插件 (browser-bridge)
272
+ └─ browser_* 工具 → Agent
273
+ ```
274
+
275
+ **关键设计:**
276
+ - 扩展主动外连桥(不需要 native messaging host)
277
+ - 默认开启(v1.0.6+),装上即监听,不需要手动启用
278
+ - 持久 debugger 附着——控制期间横幅始终显示
279
+ - 仅监听 127.0.0.1;命令面(WS 与 HTTP)共用同一个 Token,跨源一律不放行
280
+ - 协议版本独立于版本号:帧契约变了才升,不匹配时状态页直接写出该升级哪一侧
281
+
282
+ ## 已验证
283
+
284
+ | 场景 | 结果 |
285
+ |---|---|
286
+ | 百度搜索 → 提取结果标题 | ✅(1.0.x 时期实机) |
287
+ | B 站搜索用户 → 发私信 | ✅(1.0.x 时期实机) |
288
+ | B 站搜索 → 统计视频卡片 + 截图 | ✅(1.0.x 时期实机) |
289
+ | 实测台 `npm run verify`:配置解析、三种宿主形状、HTTP 鉴权与升级来源、发布产物行为 | ✅ 2026-10-10,见 [verify/README.md](verify/README.md) |
290
+ | 实测台 `verify_tab_groups`:agent 标签归组的建组 / 复用 / 退避重试 / 缓存失效 / 按窗口拆组 / 无权限降级 | ✅ 2026-10-11,node:vm + 有状态假 `chrome.tabs`/`chrome.tabGroups`,**未在真实 Chrome 里重跑** |
291
+ | 实测台 `verify_extension_e2e`:真桥 + 真扩展代码 + 真 WebSocket 端到端(帧契约、命令可达、错误分类) | ✅ 2026-10-11,只有 `chrome.*` 是假的;视觉与真实 CDP 行为仍需实机确认 |
292
+ | 实测台 `verify_cdp_retry`:CDP 会话被抢走后的恢复 / 重放不许把副作用做两遍 / 重试有界(6 场景) | ✅ 2026-10-11,真实 `background.js` + 有状态假 `chrome.debugger`;另用合并预览文件交叉验证过 |
293
+ | 实测台 `verify_render_contract`:每条工具的真实返回值同时喂给宿主的输出校验器与它自己的 `render`,断言「模型可见文本里有这次调用真正要的数据」 | ✅ 2026-10-12,真桥 + 假扩展 + 宿主 `validateJsonSchemaValue`;这一层此前**零覆盖**,所以两类缺陷活了 12 / 5 个版本 |
294
+ | **真实宿主 × 真实浏览器**:本机 dsh `0.2.0-rc.2` 用隔离 `DSH_HOME` 挂载本插件,用户的真实 Chrome 连上桥,9 条只读工具全部走真实代码路径 | ✅ 2026-10-12,Chrome 153 + 扩展 1.0.10:`browser_read` 交出 1341 字符真实正文、`browser_snapshot` 交出 14 条带 ref 的清单、输出 schema 零违规;**也正是这一跑发现了旧扩展会静默忽略 `mode:"ax"`**(本机扩展是 1.0.10,早于 AX 快照的 1.0.11)——已改成插件侧显式回报 `axFallback` |
295
+ | `tsc --noEmit` + `publint` | ✅ 2026-10-10 |
296
+
297
+ 仓库自 v1.0.4 起不再有单测目录,上面三条浏览器场景是历史实机记录,本版未重跑;本版结论都来自 `verify/` 里可重复执行的实测台。
298
+
299
+ ## 更新日志
300
+
301
+ 见 [CHANGELOG.md](CHANGELOG.md)。
302
+
303
+ ## 许可证 · License
304
+
305
+ 本项目采用 **GNU Affero General Public License v3.0 (AGPL-3.0)**。
306
+
307
+ - **个人 / 学术 / 非商业用途**:完全免费,在遵守 AGPL-3.0 的前提下自由使用、修改、分发
308
+ - **企业 / 商业用途**:AGPL-3.0 要求通过网络使用本软件也构成"分发",必须公开衍生代码。若企业在闭源产品中嵌入、基于本项目构建 SaaS 服务而不愿开源,需要联系作者获取**商业许可**(另行协商授权条款)
309
+ - **商业许可咨询**:[GitHub Issues](https://github.com/angelky7/dsh-browser-control/issues) 或邮箱 **caob2333@outlook.com**
310
+
311
+ 完整许可证文本见 [LICENSE](LICENSE) 文件(AGPL-3.0)。
@@ -0,0 +1,14 @@
1
+ # dsh-browser-control: the bridge listens by default (since v1.0.6 the schema
2
+ # default is `true`; this patch must not contradict it — an explicit `false` here
3
+ # overrode that default and made a fresh install look broken, see issue #4).
4
+ # To opt out, add to your profile patch:
5
+ # - merge:
6
+ # - id: browser-bridge
7
+ # config: { enabled: false }
8
+ - insert:
9
+ - id: browser-bridge
10
+ name: '@angelky7/dsh-browser-control'
11
+ config:
12
+ enabled: true
13
+ port: 9777
14
+ token: dsh-local