dsh-builtin-browser 0.1.22 → 0.2.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 +50 -3
- package/README.md +51 -4
- package/client.js +298 -0
- package/cordis.patch.yml +24 -5
- package/docs/README.md +1 -1
- package/docs/architecture.md +1 -1
- package/docs/tool-reference.md +2 -1
- package/lib/browser/runtime.d.ts +18 -1
- package/lib/browser/runtime.js +28 -1
- package/lib/browser/types.d.ts +33 -0
- package/lib/browser-electron/entry.js +115 -0
- package/lib/browser-electron/history-store.d.ts +71 -0
- package/lib/browser-electron/history-store.js +151 -0
- package/lib/browser-electron/host-main.d.ts +1 -0
- package/lib/browser-electron/host-main.js +88 -1
- package/lib/browser-electron/provider.d.ts +105 -4
- package/lib/browser-electron/provider.js +226 -14
- package/lib/browser-electron/remote-host.d.ts +29 -6
- package/lib/browser-electron/remote-host.js +114 -24
- package/lib/browser-electron/settings-store.d.ts +87 -0
- package/lib/browser-electron/settings-store.js +160 -0
- package/lib/browser-electron/virtual-cursor.d.ts +42 -0
- package/lib/browser-electron/virtual-cursor.js +71 -0
- package/lib/index.d.ts +16 -2
- package/lib/index.js +18 -2
- package/lib/tool-browser/index.js +72 -2
- package/package.json +20 -6
- package/src/browser/runtime.ts +37 -1
- package/src/browser/types.ts +31 -0
- package/src/browser-electron/entry.ts +135 -0
- package/src/browser-electron/history-store.ts +162 -0
- package/src/browser-electron/host-main.ts +83 -1
- package/src/browser-electron/provider.ts +267 -19
- package/src/browser-electron/remote-host.ts +114 -21
- package/src/browser-electron/settings-store.ts +198 -0
- package/src/browser-electron/virtual-cursor.ts +82 -0
- package/src/index.ts +20 -2
- package/src/tool-browser/index.ts +71 -1
package/README.en.md
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
| --- | --- |
|
|
19
19
|
| Why a shared real browser, and how it differs from headless approaches | [Why a shared real browser](docs/why-browser.md) |
|
|
20
20
|
| Installation, configuration, day-to-day use | [User guide](docs/user-guide.md) |
|
|
21
|
-
| All
|
|
21
|
+
| All 34 tools: parameters, output, examples | [Tool reference](docs/tool-reference.md) |
|
|
22
22
|
| How the seam / provider / tools layers and self-hosting work | [Architecture](docs/architecture.md) |
|
|
23
23
|
| Documentation index and README split | [Docs index](docs/README.md) |
|
|
24
24
|
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
|
|
29
29
|
- **A real view, not a relay**: the browser is a native `WebContentsView`; the human sees every step the agent takes and can grab control at any time;
|
|
30
30
|
- **Install-and-use**: with a desktop shell the shell's embedded view is used; on plain `dsh web` the plugin **self-hosts** — it spawns its own Electron window with zero extra configuration;
|
|
31
|
-
- **One plugin, one toolset**: after install the agent automatically gets
|
|
31
|
+
- **One plugin, one toolset**: after install the agent automatically gets 34 `browser_*` tools (open, a11y tree, wait, semantic/coordinate interaction, scroll, back/forward, batch and single-control form filling, keys, structured scraping, screenshot, download, auth management…).
|
|
32
32
|
|
|
33
33
|
In one sentence: **installing the plugin gives you a real browser that is shared with the user and drivable by the agent.**
|
|
34
34
|
|
|
@@ -117,6 +117,26 @@ See the full list in [Tool reference](#tool-reference).
|
|
|
117
117
|
<p><code>browser_screenshot</code> supports <code>savePath</code> to write the PNG to disk, ready for vision models (modlens etc.) to locate elements visually.</p>
|
|
118
118
|
</td>
|
|
119
119
|
</tr>
|
|
120
|
+
<tr>
|
|
121
|
+
<td width="50%" valign="top">
|
|
122
|
+
<h3>Searchable browsing history</h3>
|
|
123
|
+
<p><code>browser_visited</code> reads the <b>persistent</b> record of visited pages (stored beside the browser profile, so it survives closing the browser and restarting DSH), filterable by domain and reopenable. It is a different thing from <code>browser_history</code>, which is the session's operation log.</p>
|
|
124
|
+
</td>
|
|
125
|
+
<td width="50%" valign="top">
|
|
126
|
+
<h3>Synthetic cursor</h3>
|
|
127
|
+
<p>While the agent works, a virtual pointer and click ripple are drawn inside the page — <b>its appearance means the agent has taken over that tab</b>. DOM-level actions (set value, check, select) also show a landing point. Can be switched off in settings.</p>
|
|
128
|
+
</td>
|
|
129
|
+
</tr>
|
|
130
|
+
<tr>
|
|
131
|
+
<td width="50%" valign="top">
|
|
132
|
+
<h3>A "Browser" section in Settings</h3>
|
|
133
|
+
<p>Keep history / keep cookies / auto-expand the side panel / close the browser when a session ends / show the cursor / vision strategy / allow credential reads — switches take effect <b>immediately</b>, no restart.</p>
|
|
134
|
+
</td>
|
|
135
|
+
<td width="50%" valign="top">
|
|
136
|
+
<h3>Unambiguous teardown</h3>
|
|
137
|
+
<p><b>Closing the browser window ends that session</b> (the next open is a clean one) while browsing history and login state survive; <b>collapsing the interface is only a collapse</b> and the browser keeps running in the background.</p>
|
|
138
|
+
</td>
|
|
139
|
+
</tr>
|
|
120
140
|
</table>
|
|
121
141
|
|
|
122
142
|
## Why this plugin
|
|
@@ -135,6 +155,7 @@ See the full list in [Tool reference](#tool-reference).
|
|
|
135
155
|
| `browser_snapshot` | Numbered inventory of interactive elements (inputs/buttons/links; pierces same-origin iframes and Shadow DOM) | – |
|
|
136
156
|
| `browser_a11y` | Accessibility tree: semantic role/name/value/states + coordinates per interactive node (pierces same-origin iframes and Shadow DOM) | – |
|
|
137
157
|
| `browser_execute` | Run JS in the page; args arrive as `arguments[0..n]` | ✅ |
|
|
158
|
+
| `browser_visited` | Read the persistent browsing history (visited pages, filterable by domain / capped); reopen with `browser_open` | – |
|
|
138
159
|
| `browser_content` | Fetch the page as html / markdown / txt / json (selector, maxChars, timeoutMs) | – |
|
|
139
160
|
| `browser_click` | Click a semantic target (`target`: css/text/xpath, scrolls into view and clicks center) or viewport coordinates (vision-located) | ✅ |
|
|
140
161
|
| `browser_type` | Type text (optionally focusing a `target` element first; CDP `Input.insertText`) | ✅ |
|
|
@@ -243,6 +264,25 @@ The browser's **visible view**, the **browser column layout**, and the **column-
|
|
|
243
264
|
|
|
244
265
|
> The plugin declares `electron >= 30`; it has **only been verified on Windows** (macOS/Linux untested, not yet promised).
|
|
245
266
|
|
|
267
|
+
## Updating (the two hosts differ)
|
|
268
|
+
|
|
269
|
+
The plugin has one installation per host, and the two are updated separately — **updating one does not update the other**.
|
|
270
|
+
|
|
271
|
+
**Desktop (DSH Desktop)**
|
|
272
|
+
- The plugin is a dependency of the desktop profile (usually `$DSH_HOME/profiles/desktop`). Updating means moving that dependency to the new version and then **restarting DSH Desktop**, which is when the settings panel and the tools pick up the new code.
|
|
273
|
+
- The browser engine is the desktop app's own Electron; the plugin never downloads a second copy.
|
|
274
|
+
- Upgrading the desktop app itself does not carry the plugin along; update it as described above.
|
|
275
|
+
|
|
276
|
+
**Web (`dsh web`)**
|
|
277
|
+
- The plugin is a dependency of the web profile (`$DSH_HOME/profiles/web`); **restart `dsh web`** after updating.
|
|
278
|
+
- There is no desktop shell, so the shared browser is self-hosted by the plugin: the first install may need an Electron binary. If the package manager's build allow-list blocked it (pnpm v10+ blocks `electron`'s postinstall), run `npx install-electron` once to fetch it.
|
|
279
|
+
- Update it the same way you installed it (npm package `dsh-builtin-browser`, the GitHub repo `wqty123/dsh-browser`, or a local directory).
|
|
280
|
+
|
|
281
|
+
**What is the same on both**
|
|
282
|
+
- The toolset (34 `browser_*` tools), the "Browser" section in Settings, and how browsing history and cookies persist are identical; only the carrier of the browser window differs (desktop shell vs. plugin-hosted).
|
|
283
|
+
- Upgrading loses no data: history and settings live in `$DSH_HOME/dsh-builtin-browser-host/` (`history.jsonl`, `settings.json`), and login state sits in the same profile directory.
|
|
284
|
+
- If history behaves unexpectedly after an upgrade, check **Settings → Browser**: history defaults to **on**, one-time auto-expand defaults to **on**, and closing the browser when a session ends defaults to **off**.
|
|
285
|
+
|
|
246
286
|
## Known limitations
|
|
247
287
|
|
|
248
288
|
- JPEG screenshots are available only on the self-hosted native path (`capturePage` `toJPEG`); the desktop shell's CDP fallback stays PNG (CDP JPEG hangs on Electron 43).
|
|
@@ -310,8 +350,15 @@ Code layout:
|
|
|
310
350
|
| 17 | 2026-09-16 | **Screenshot savePath confined + localized download dir** (issue #13): `browser_screenshot` wrote straight to `writeFileSync` — anywhere the process could reach, silently replacing existing files (bypassing the read-only sandbox's write protection) → one shared `admitSavePath` gate for downloads AND screenshots (absolute, inside `downloadDir`, never overwriting an existing file), plus parent-directory creation for screenshots; the default download directory is no longer hardcoded to `~/Downloads` but probed in order: `downloadDir` → `XDG_DOWNLOAD_DIR` → `~/Downloads` / `~/下载` / `~/下載` → fallback (a Chinese desktop needs no configuration) |
|
|
311
351
|
|
|
312
352
|
| **0.1.22** | 2026-09-16 | **Release**: the macOS binary-probe fix (issues #9 / #14) and rounds 15–17 (issue #11 toolbar SyntaxError / #10 self-hosted trio / #13 screenshot savePath + download dir) ship as **0.1.22** (build clean, **35/35 tests pass**, tag `v0.1.22`) |
|
|
353
|
+
| Round 18 | 2026-09-20 | **Windows on-device trio + probe self-healing + locate verdicts** (measured on a real self-hosted `dsh web` host; defects ①–④ were all the "CDP answered success, the page received nothing" kind): ① Chromium's `CalculateNativeWinOcclusion` marks the plugin window HIDDEN while another window covers it — the page stops producing frames and **every** synthesized mouse/key event is dropped by the renderer (`CanReceiveInput()` false) while CDP replies `{}` → the child appends `disable-features=CalculateNativeWinOcclusion` before `app.whenReady()` (win32 only); ② `click()` had no leading `mouseMoved`, so the first click on a fresh view was routed away and lost → now move→press→release; ③ a fresh view holds no web focus, so the FIRST `browser_key` of a session vanished → new host `focus` op (optional `focus?()` on the view handle), `key()` focuses best-effort before dispatch and waits 80ms only when focus had to move (focus lands asynchronously; a key dispatched in the same turn is still dropped); ④ `available()` cached a FAILED Electron probe for the host's lifetime while provider selection runs once per process, so an Electron that arrived after DSH started was never adopted → successes stay cached, failures re-probe after a cooldown (`DSH_BROWSER_PROBE_RETRY_MS`, default 30s), and `resolveProvider()` now distinguishes "no provider registered" from "registered but reports itself unavailable" with the matching remedy; ⑤ **a failed locate was masked by the outer timeout** — the in-page locate script polls for its whole budget and answers only afterwards, while the outer wait used the SAME budget, so `browser: click timed out after 10000ms` won the race and the in-page verdict never got out; a css/xpath **parse error** also reported as "not found yet", polling a selector that can never become valid. Now a parse error is terminal and names itself (`invalid CSS selector "…" / invalid XPath …`), the outer wait gives the in-page answer 2 s of transport grace, and a miss reports the strategy the provider assumed (`by` defaults to `"by":"css"`) plus the time spent; `scrape`'s item selector fails the same way at once. Same call: before `click timed out after 10000ms`, after `element not found: {"value":"Learn more","by":"css"} (looked for 10000ms)`. 7 new regression tests (**47/47 pass**), 17/17 end-to-end steps against the real host, plus 4/4 locate-verdict steps |
|
|
354
|
+
|
|
355
|
+
| 19 | 2026-10-01 | **DSH 0.2 compatibility**: DSH moved to the 0.2 line (`@deepseek-ai/dsh@0.2.0-rc.2`, with `dsh-llm`/`dsh-tools`/`dsh-system-prompt` following to `0.2.0-rc.2`), while our declaration `>=0.1.1-rc.1 <0.2.0` shut 0.2 out → verified the plugin's (narrow) runtime dependency surface against a **real 0.2.0-rc.2 host** (`cordis` Context/Service, `dsh-tools` defineTool, `dsh-llm` HarnessError, `schemastery`) and found no breaking change: session, navigation, snapshot and the screenshot trio (outside-path refused / legal write / overwrite refused) all pass → peer ranges for the three dsh packages widened to `>=0.1.1-rc.2 <0.3.0`, `dsh.compatibility.dsh` widened to `>=0.1.1-rc.1 <0.3.0`, and `dshReleases` gained `0.2.0-rc.1`/`0.2.0-rc.2` = compatible |
|
|
356
|
+
| **0.1.23** | 2026-10-01 | **Release**: round 18 (PR #15: the Windows synthesized-input trio + Electron probe self-healing + locate verdicts) and round 19 (DSH 0.2 compatibility) ship as **0.1.23** (build clean, **47/47 tests pass**, tag `v0.1.23`) |
|
|
357
|
+
| Round 20 | 2026-10-01 | **Browsing history / settings panel / synthetic cursor / teardown**: (1) **persistent browsing history** (`history-store`: append-only JSONL beside the browser profile; capped at 5000 entries or 90 days, whichever comes first; a damaged line loses only itself) plus the new **`browser_visited`** tool (tool count **33 → 34**), reopening via `browser_open`; (2) a "Browser" section in Settings (hand-written client bundle registered into `settings.section` with `order: 60`, below the host's own rows) served by `GET/PUT /dsh-builtin-browser/settings` (same-origin guard, 64 KiB cap) over a `settings-store` document (per-field validation, unknown keys dropped, malformed file falls back to defaults) — **switches take effect immediately** because the provider reads the document on every use; (3) an in-page **synthetic cursor** (inline styles + Web Animations, so a page's `style-src` CSP cannot drop it; `buildTargetScript` now attaches the element centre as `__point`, which gives click / type / setValue / check / select / clear **a landing point**) — **its appearance means the agent has taken over that tab**; (4) **closing a window ends its session**: on `closed` the host releases every view's `webContents` (a BrowserWindow does not destroy child views, so each window would otherwise leak a renderer) and reports `viewClosed`; the provider ends that session and the seam gains `exists()` so the tool layer re-checks a cached session — the next call gets a **clean session** while browsing history and login state survive. 21 new tests (**68/68 pass**) |
|
|
358
|
+
| Round 20 addendum | 2026-10-01 | **Root cause of the invisible settings panel, plus crash diagnostics**: (1) On a real host the panel did not appear — the cause is that all three `cordis.patch.yml` rows were registered under **subpath** names (`dsh-builtin-browser/browser`), while the host's client-module scan resolves a row's specifier to a **package root** (`exactPackageSpecifier` returns `undefined` as soon as it sees a `/`), leaving the package with no row to read its `dsh.client` from. Fixed by adding an inert root row under the bare package name and giving the root entry `export const name` plus an empty `apply()`. Measured: boot entries 67 → **68**, the panel appears below "规则设定", and switches persist immediately. (2) Three host-log defects fixed: ISO timestamps, both paths plus their existence recorded before spawning, and a 2 MiB rotation that leaves a dated marker instead of wiping the file (which is how weeks of history vanished); the `exit` line gained `pid=` and `entryExists=` so "present at spawn, absent at exit" names an installation replaced underneath a running host. (3) Test pollution of the real log fixed: three spawn-based tests restored `DSH_HOME` in a `finally` while `dispose()` kills children asynchronously, so their synthetic exit lines landed in the operator's real log — now isolated at module scope. **70/70 tests** |
|
|
359
|
+
| **0.2.0** | 2026-10-01 | **Release**: round 20 (persistent browsing history / the "Browser" settings section / synthetic cursor / teardown semantics) and its addendum ship as **0.2.0** — tool count **33 → 34** (new `browser_visited`), plus a new "Browser" section in Settings. Build clean, **70/70 tests pass**, tag `v0.2.0` |
|
|
313
360
|
|
|
314
|
-
> The npm badge at the top is the authority on the registry's latest version: `0.1.
|
|
361
|
+
> The npm badge at the top is the authority on the registry's latest version: `0.1.23` is committed and tagged; if the badge still reads `0.1.22`, that version is not published yet.
|
|
315
362
|
|
|
316
363
|
## Acknowledgements
|
|
317
364
|
|
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
| --- | --- |
|
|
19
19
|
| 了解插件为什么存在、与无头方案的区别 | [为什么做共享真实浏览器](docs/why-browser.md) |
|
|
20
20
|
| 安装、配置与日常使用 | [用户指南](docs/user-guide.md) |
|
|
21
|
-
| 全部
|
|
21
|
+
| 全部 34 个工具的参数、输出与示例 | [工具参考](docs/tool-reference.md) |
|
|
22
22
|
| 了解 seam / provider / 工具三层与自托管实现 | [架构说明](docs/architecture.md) |
|
|
23
23
|
| 查看全部文档与 README 分工 | [文档索引](docs/README.md) |
|
|
24
24
|
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
|
|
29
29
|
- **真实视图,而非转播**:浏览器是原生 `WebContentsView`,用户直接看到 agent 在做什么,随时可以上手接管;
|
|
30
30
|
- **装好即用**:有桌面外壳时嵌入外壳视图;纯 `dsh web` 也能**自托管**——插件自己拉起一个 Electron 窗口,不需要任何额外配置;
|
|
31
|
-
- **一插件即一套工具**:安装后 agent 自动获得
|
|
31
|
+
- **一插件即一套工具**:安装后 agent 自动获得 34 个 `browser_*` 工具(打开、查看、无障碍树、等待、语义/坐标操作、滚动、回退、批量/单控件填表、按键、结构化提取、截图、下载、登录态管理……)。
|
|
32
32
|
|
|
33
33
|
一句话:**安装插件 = 获得一个与用户共享、可被 agent 驱动的真实浏览器。**
|
|
34
34
|
|
|
@@ -117,6 +117,26 @@ dsh plugin --profile web add <本仓库路径>
|
|
|
117
117
|
<p><code>browser_screenshot</code> 支持 <code>savePath</code> 直接落盘 PNG,交给视觉模型(modlens 等)做基于视觉的元素定位。</p>
|
|
118
118
|
</td>
|
|
119
119
|
</tr>
|
|
120
|
+
<tr>
|
|
121
|
+
<td width="50%" valign="top">
|
|
122
|
+
<h3>可查的浏览历史</h3>
|
|
123
|
+
<p><code>browser_visited</code> 读取<b>持久化</b>的访问记录(与 cookie 同址落盘,关闭浏览器、重启 DSH 后仍在),可按域名过滤并重新打开任意一条。它与 <code>browser_history</code>(会话内的操作日志)是两件事。</p>
|
|
124
|
+
</td>
|
|
125
|
+
<td width="50%" valign="top">
|
|
126
|
+
<h3>可视化鼠标</h3>
|
|
127
|
+
<p>agent 操作时在页面内绘制虚拟光标与点击涟漪 —— <b>光标出现即表示它已接管该标签页</b>;DOM 级操作(填值/勾选/选择)同样有落点。可在设置里关闭。</p>
|
|
128
|
+
</td>
|
|
129
|
+
</tr>
|
|
130
|
+
<tr>
|
|
131
|
+
<td width="50%" valign="top">
|
|
132
|
+
<h3>设置页里的「浏览器」栏</h3>
|
|
133
|
+
<p>历史与 cookie 是否保留、侧栏是否自动展开、会话结束时是否关闭浏览器、是否显示光标、视觉策略、是否允许读取凭据 —— 开关<b>即时生效</b>,无需重启。</p>
|
|
134
|
+
</td>
|
|
135
|
+
<td width="50%" valign="top">
|
|
136
|
+
<h3>收尾明确</h3>
|
|
137
|
+
<p><b>关掉浏览器窗口 = 结束该会话</b>(下次打开是干净的新会话),浏览历史与登录状态保留;<b>收起界面只是收起</b>,浏览器继续在后台运行。</p>
|
|
138
|
+
</td>
|
|
139
|
+
</tr>
|
|
120
140
|
</table>
|
|
121
141
|
|
|
122
142
|
## 为什么选它
|
|
@@ -135,6 +155,7 @@ dsh plugin --profile web add <本仓库路径>
|
|
|
135
155
|
| `browser_snapshot` | 交互元素(输入框/按钮/链接)带编号清单(穿透同源 iframe 与 Shadow DOM) | – |
|
|
136
156
|
| `browser_a11y` | 无障碍树:每个交互节点的语义角色/名称/值/状态 + 坐标(穿透同源 iframe 与 Shadow DOM) | – |
|
|
137
157
|
| `browser_execute` | 在页面执行 JS;参数以 `arguments[0..n]` 传入 | ✅ |
|
|
158
|
+
| `browser_visited` | 读取持久化浏览历史(访问过的页面,可按域名过滤/限量);重开用 `browser_open` | – |
|
|
138
159
|
| `browser_content` | 以 html / markdown / txt / json 抓取页面(selector、maxChars、timeoutMs) | – |
|
|
139
160
|
| `browser_click` | 点击:语义目标(`target`: css/text/xpath,滚动到元素并点中心)或视口坐标(配合截图视觉定位) | ✅ |
|
|
140
161
|
| `browser_type` | 输入文本(可先按 `target` 聚焦元素;CDP `Input.insertText`) | ✅ |
|
|
@@ -211,7 +232,7 @@ agent (browser_* 工具)
|
|
|
211
232
|
|
|
212
233
|
- **seam 层**(`browser` 行)提供 `ctx.browser` 服务:provider 注册、会话生命周期、错误码,与具体实现解耦;
|
|
213
234
|
- **provider 层**(`browser-electron` 行)通过 `ElectronBrowserViewHost` 接缝操作视图(创建/销毁/显示/`sendCommand`),由真实外壳用 Electron 对象实现;
|
|
214
|
-
- **工具层**(`tool-browser` 行)提供模型侧的
|
|
235
|
+
- **工具层**(`tool-browser` 行)提供模型侧的 34 个 `browser_*` 工具,按调用方任务(DSH 会话)维护独立的浏览器会话。
|
|
215
236
|
|
|
216
237
|
**自托管模式**:没有桌面外壳时,插件自己拉起一个 Electron 子进程(`host-main.js`),通过本机 TCP JSON-RPC 驱动。RPC 带随机 token 认证,token 经 **stdin + 环境变量双通道**传递——Windows 上 Electron 是 GUI 子系统进程、收不到 piped stdin,环境变量兜底保证握手稳定。子进程崩溃会自动重启;优先使用随插件安装的 electron 包(**打包应用如 DSH Desktop.exe 不会被误当作可复用二进制**,避免 spawn 秒退);截图优先走 Electron 原生 `capturePage`(CDP 截图在多视图下会挂起);Electron 的定位顺序见下(33.x 有合成器缺陷,建议 ≥ 40;44+ 的 electron 包不再随安装自动下载二进制,首次使用若缺失会按报错提示先 `npx install-electron`,需联网)。
|
|
217
238
|
|
|
@@ -243,6 +264,25 @@ agent (browser_* 工具)
|
|
|
243
264
|
|
|
244
265
|
> 插件声明 `electron >= 30`;**当前仅在 Windows 环境实测**(macOS/Linux 未验证,暂不承诺)。
|
|
245
266
|
|
|
267
|
+
## 更新方式(两端不同)
|
|
268
|
+
|
|
269
|
+
插件在两种宿主里各有一份安装,更新路径也不同 —— **更新其中一端不会连带更新另一端**。
|
|
270
|
+
|
|
271
|
+
**桌面端(DSH Desktop)**
|
|
272
|
+
- 插件是桌面端 profile 里的依赖(profile 目录通常是 `$DSH_HOME/profiles/desktop`)。更新它 = 把该 profile 里的依赖更新到新版本,然后**重启 DSH Desktop**,客户端设置栏与工具才会换成新代码。
|
|
273
|
+
- 浏览器内核由桌面端自带的 Electron 提供,插件不会再下载一份 Electron。
|
|
274
|
+
- 桌面端自身的升级不会自动带来插件升级,需要按上面的方式单独更新。
|
|
275
|
+
|
|
276
|
+
**Web 端(`dsh web`)**
|
|
277
|
+
- 插件是 web profile 里的依赖(`$DSH_HOME/profiles/web`),更新后**重启 `dsh web`** 生效。
|
|
278
|
+
- Web 端没有桌面外壳,共享浏览器由插件自托管拉起:首次安装可能需要 Electron 二进制;若包管理器的构建白名单拦下了它(pnpm v10+ 会拦 `electron` 的 postinstall),执行一次 `npx install-electron` 补上即可。
|
|
279
|
+
- 更新方式与你首次安装它时一致(按 npm 包名 `dsh-builtin-browser`、按 GitHub 仓库 `wqty123/dsh-browser`,或本地目录)。
|
|
280
|
+
|
|
281
|
+
**两端一致的体验**
|
|
282
|
+
- 工具集(34 个 `browser_*`)、设置页里的「浏览器」栏、浏览历史与 cookie 的持久化行为完全相同;差别只在浏览器窗口由谁承载(桌面外壳 / 插件自托管)。
|
|
283
|
+
- 升级不会丢数据:浏览历史与设置都在 `$DSH_HOME/dsh-builtin-browser-host/`(`history.jsonl`、`settings.json`),登录状态在同一 profile 目录里。
|
|
284
|
+
- 升级后如果历史记录不符合预期,先去「设置 → 浏览器」确认这些开关:历史默认**开**、侧栏自动展开默认**开**、会话结束时自动关闭浏览器默认**关**。
|
|
285
|
+
|
|
246
286
|
## 已知限制
|
|
247
287
|
|
|
248
288
|
- JPEG 截图仅自托管原生路径可用(`capturePage` 的 `toJPEG`);桌面壳的 CDP 回退路径仍为 PNG(CDP JPEG 在 Electron 43 上挂起)。
|
|
@@ -310,8 +350,15 @@ npm run build
|
|
|
310
350
|
| 第十七轮 | 2026-09-16 | **截图 savePath 收敛 + 下载目录本地化**(issue #13):`browser_screenshot` 原先直接 `writeFileSync`,可写进程可达的任意路径并**静默覆盖**已有文件(等于绕过只读沙箱的写保护)→ 抽出唯一下载/截图共用准入门 `admitSavePath`(绝对路径 + `downloadDir` 内 + **不覆盖已存在文件**),截图补父目录自动创建;默认下载目录不再写死 `~/Downloads`,按序探测 `downloadDir` → `XDG_DOWNLOAD_DIR` → `~/Downloads`/`~/下载`/`~/下載` → 回退(中文桌面免配置) |
|
|
311
351
|
|
|
312
352
|
| **0.1.22** | 2026-09-16 | **发布**:macOS 二进制探测修复(issue #9 / #14)与第十五~十七轮(issue #11 工具栏 SyntaxError / #10 自托管三连 / #13 截图 savePath + 下载目录)随 **0.1.22** 收录(构建零错误、**35 项测试全绿**,tag `v0.1.22`) |
|
|
353
|
+
| 第十八轮 | 2026-09-20 | **Windows 真机三连 + 探测自愈 + 定位判词**(真实 `dsh web` 自托管宿主实测,缺陷①~④同为「CDP 报成功、页面没收到」):① Windows 的 `CalculateNativeWinOcclusion` 把被遮挡的插件窗口判定为 HIDDEN → 整站停帧,且每条合成鼠标/键盘事件被渲染端静默丢弃(CanReceiveInput=false)而 CDP 回 `{}` → 子进程 ready 前追加 `disable-features=CalculateNativeWinOcclusion`(仅 win32);② `click()` 缺前置 `mouseMoved`,新视图第一次点击落空 → 改为 move→press→release;③ 新视图从未持有 web focus,`browser_key` 第一次调用无效 → 宿主新增 `focus` op(view handle 可选 `focus?()`),`key()` 派发前 best-effort 聚焦,并在焦点需要移动时等 80ms(焦点落地异步,同轮派发的键仍会被丢);④ `available()` 把**失败**探测按宿主生命周期永久缓存,而 provider 选择每进程一次 → Electron 晚于 DSH 到位就永远接不上 → 成功仍缓存、失败 30s 冷却后重探(`DSH_BROWSER_PROBE_RETRY_MS`),`resolveProvider()` 报错区分「一个都没注册」与「注册了但自报不可用」并附处置;⑤ **定位失败被外层超时掩盖** —— 页内定位脚本会把预算轮询到底才回答,外层却用同一个预算,于是 `browser: click timed out after 10000ms` 抢走了页内早已写好的判词;而 css/xpath **解析失败**(选择器根本不合法)也被写成「还没找到」,对着永远不可能匹配的东西把预算轮完 → 解析失败即刻终止并报 `invalid CSS selector "…" / invalid XPath …`;外层给页内判词 2s 传输余量;未命中判词补上提供方实际采用的策略(`by` 缺省即 `"by":"css"`)与实际耗时;`scrape` 的 item 选择器同样即刻失败(同一个漏写 `by` 的调用:修前 `click timed out after 10000ms`,修后 `element not found: {"value":"Learn more","by":"css"} (looked for 10000ms)`);新增 7 条回归测试(**47/47 全绿**),真实宿主端到端 **17/17** + 定位判词 **4/4** |
|
|
354
|
+
|
|
355
|
+
| 第十九轮 | 2026-10-01 | **DSH 0.2 兼容性**:DSH 进入 0.2 线(`@deepseek-ai/dsh@0.2.0-rc.2`,peer 包 `dsh-llm`/`dsh-tools`/`dsh-system-prompt` 同步到 `0.2.0-rc.2`),而原声明 `>=0.1.1-rc.1 <0.2.0` 把 0.2 挡在门外 → 在**真实 0.2.0-rc.2 宿主**上实测插件的运行时依赖面(`cordis` 的 Context/Service、`dsh-tools` 的 defineTool、`dsh-llm` 的 HarnessError、`schemastery`),确认无破坏性变更:会话、导航、快照、截图三态(越界拒绝/合法写入/覆盖拒绝)全部通过 → `peerDependencies` 三个 dsh 包范围改为 `>=0.1.1-rc.2 <0.3.0`,`dsh.compatibility.dsh` 放宽为 `>=0.1.1-rc.1 <0.3.0`,`dshReleases` 增加 `0.2.0-rc.1`/`0.2.0-rc.2` = compatible |
|
|
356
|
+
| **0.1.23** | 2026-10-01 | **发布**:第十八轮(PR #15:Windows 合成输入三连 + Electron 探测自愈 + 定位判词)与第十九轮(DSH 0.2 兼容)随 **0.1.23** 发布(构建零错误、**47 项测试全绿**,tag `v0.1.23`) |
|
|
357
|
+
| 第二十轮 | 2026-10-01 | **浏览历史 / 设置栏 / 可视化鼠标 / 收尾语义**:①新增**持久化浏览历史**(`history-store`:追加式 JSONL,落在 cookie 同一 profile 目录;5000 条或 90 天先到者为准;损坏行只丢该行)+ 新工具 **`browser_visited`**(工具数 **33 → 34**),重新打开沿用 `browser_open`;②新增设置页「浏览器」栏(手写客户端 bundle 注册到 `settings.section`,`order: 60` 排在宿主自带栏目下方)+ `GET/PUT /dsh-builtin-browser/settings`(同源防护、64 KiB 上限)+ 设置文档(`settings-store`:字段逐个校验、未知键丢弃、损坏文件按默认值),**开关即时生效**(provider 每次读取,无需重启);③新增页面内**虚拟光标**(内联样式 + Web Animations 以规避页面 `style-src` CSP;`buildTargetScript` 统一附加元素中心 `__point`,于是 click / type / setValue / check / select / clear **都有落点**),**光标出现即代表 agent 已接管该标签页**;④**关掉窗口 = 结束该会话**:窗口 `closed` 时释放其全部视图的 `webContents`(BrowserWindow 不连带销毁子视图,否则每个窗口泄漏一个渲染进程)并上报 `viewClosed`,provider 结束对应会话,seam 新增 `exists()` 供工具层核验会话缓存 —— 下一次调用得到**干净的新会话**,浏览历史与登录状态保留;新增 21 条测试(**68/68 全绿**) |
|
|
358
|
+
| 第二十轮补记 | 2026-10-01 | **客户端设置栏"隐形"的根因 + 崩溃诊断**:①真机验证发现设置栏不出现 —— 根因是 `cordis.patch.yml` 三行全用**子路径名**(`dsh-builtin-browser/browser`)注册,而宿主的客户端模块扫描只接受**精确包名**(`exactPackageSpecifier` 遇 `/` 即返回 `undefined`),于是这个包在客户端侧没有任何行可供读取 `dsh.client`;修复 = 增加一行以**包名**注册的惰性根行 + 给根入口补 `export const name` 与空 `apply()`;实测启动图条目 67 → **68**、设置栏出现并排在「规则设定」下方、开关即时落盘。②宿主日志三处缺陷修复:加 **ISO 时间戳**、spawn 前记录 **两条路径与存在性**、2 MiB 轮转改为**留时间戳标记**(原先整体清空,几周历史就是这样消失的);`exit` 行加 `pid=`/`entryExists=`,使"启动时存在、退出时不存在"自动命名"安装被就地替换"。③修复**测试污染真实日志**:三个 spawn 类测试因 `dispose()` 异步杀子进程,exit 行写在 `finally` 恢复 `DSH_HOME` 之后 → 改为模块级隔离。测试 **70/70** |
|
|
359
|
+
| **0.2.0** | 2026-10-01 | **发布**:第二十轮(浏览历史持久化 / 设置页「浏览器」栏 / 可视化鼠标 / 收尾语义)及其补记随 **0.2.0** 发布 —— 工具数 **33 → 34**(新增 `browser_visited`),设置页新增「浏览器」栏。构建零错误、**70/70 测试全绿**,tag `v0.2.0` |
|
|
313
360
|
|
|
314
|
-
> registry 上的最新版本以顶部 npm 徽章为准:本次 `0.1.
|
|
361
|
+
> registry 上的最新版本以顶部 npm 徽章为准:本次 `0.1.23` 已入库并打 tag;若徽章仍显示 `0.1.22`,说明该版本尚未 publish。
|
|
315
362
|
|
|
316
363
|
## 特别感谢
|
|
317
364
|
|
package/client.js
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser settings panel (client half): the switches that decide how the shared
|
|
3
|
+
* browser behaves — history retention, login-state persistence, presentation,
|
|
4
|
+
* vision strategy and credential access.
|
|
5
|
+
*
|
|
6
|
+
* The host half owns the document and serves it at `/dsh-builtin-browser/settings`;
|
|
7
|
+
* this panel only renders it and sends patches, so the same file stays the single
|
|
8
|
+
* source of truth (and hand-editable when no panel is reachable).
|
|
9
|
+
*
|
|
10
|
+
* Registered into the settings page's `settings.section` slot with an order above
|
|
11
|
+
* the host's own rows, so it appears under them. Plain ModuleLoader bundle: no
|
|
12
|
+
* build step, matching how the host loads plugin client halves.
|
|
13
|
+
*/
|
|
14
|
+
window.__ModuleLoader__.load({ id: "dsh-builtin-browser", factory: (require) => {
|
|
15
|
+
var module = { exports: {} }
|
|
16
|
+
var exports = module.exports
|
|
17
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" })
|
|
18
|
+
|
|
19
|
+
const react = require("react")
|
|
20
|
+
const h = react.createElement
|
|
21
|
+
const { useCallback, useEffect, useState } = react
|
|
22
|
+
|
|
23
|
+
const name = "dsh-builtin-browser"
|
|
24
|
+
const inject = ["slots", "locale"]
|
|
25
|
+
const NS = "settings.dsh-builtin-browser"
|
|
26
|
+
const ROUTE = "/dsh-builtin-browser/settings"
|
|
27
|
+
let translate = (key) => key
|
|
28
|
+
|
|
29
|
+
const zh = {
|
|
30
|
+
nav: "浏览器",
|
|
31
|
+
title: "浏览器(Browser Use)",
|
|
32
|
+
lead: "这些开关控制共享浏览器如何工作;改动立即生效,不需要重启 DSH。",
|
|
33
|
+
loading: "读取设置…",
|
|
34
|
+
failed: "读取设置失败:",
|
|
35
|
+
retry: "重试",
|
|
36
|
+
saved: "已保存",
|
|
37
|
+
path: "设置文件",
|
|
38
|
+
"history.title": "浏览历史",
|
|
39
|
+
"history.enabled": "保留浏览历史",
|
|
40
|
+
"history.hint": "记录访问过的页面;关闭浏览器后仍可查询并重新打开,关闭此开关后不再记录新访问。",
|
|
41
|
+
"cookies.title": "登录状态",
|
|
42
|
+
"cookies.persist": "保留 cookies",
|
|
43
|
+
"cookies.hint": "关闭后每次启动浏览器都是未登录状态。",
|
|
44
|
+
"ui.title": "展示",
|
|
45
|
+
"ui.autoExpandOnce": "侧栏自动展开(每轮一次)",
|
|
46
|
+
"ui.autoExpandOnce.hint": "Agent 首次操作时自动展开;你手动收起后,本轮不再打扰。",
|
|
47
|
+
"ui.closeWithSession": "会话结束时关闭浏览器",
|
|
48
|
+
"ui.closeWithSession.hint": "关闭会释放浏览器进程;浏览历史与登录状态会保留。",
|
|
49
|
+
"ui.virtualCursor": "显示可视化鼠标",
|
|
50
|
+
"ui.virtualCursor.hint": "让 Agent 正在操作的位置可见(出现即代表它在接管该标签页)。",
|
|
51
|
+
"vision.title": "视觉策略",
|
|
52
|
+
"vision.strategy": "默认策略",
|
|
53
|
+
"vision.auto": "Auto(按模型能力与页面情况自动切换)",
|
|
54
|
+
"vision.nonVisual": "纯非视觉(DOM / 无障碍树)",
|
|
55
|
+
"vision.hint": "模型不具备图像理解能力时必须使用非视觉方式。",
|
|
56
|
+
"credentials.title": "凭据",
|
|
57
|
+
"credentials.allowRead": "允许 Agent 读取 cookies / 导出登录状态",
|
|
58
|
+
"credentials.hint": "关闭后 browser_auth 会被拒绝。",
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const en = {
|
|
62
|
+
nav: "Browser",
|
|
63
|
+
title: "Browser (Browser Use)",
|
|
64
|
+
lead: "These switches decide how the shared browser behaves; changes apply immediately, no DSH restart.",
|
|
65
|
+
loading: "Loading settings…",
|
|
66
|
+
failed: "Could not load settings:",
|
|
67
|
+
retry: "Retry",
|
|
68
|
+
saved: "Saved",
|
|
69
|
+
path: "Settings file",
|
|
70
|
+
"history.title": "Browsing history",
|
|
71
|
+
"history.enabled": "Keep browsing history",
|
|
72
|
+
"history.hint": "Records visited pages so they can be found and reopened after the browser is closed; off means no new visits are recorded.",
|
|
73
|
+
"cookies.title": "Login state",
|
|
74
|
+
"cookies.persist": "Keep cookies",
|
|
75
|
+
"cookies.hint": "Off makes every browser start a signed-out session.",
|
|
76
|
+
"ui.title": "Presentation",
|
|
77
|
+
"ui.autoExpandOnce": "Auto-expand the side panel (once per task)",
|
|
78
|
+
"ui.autoExpandOnce.hint": "Expands when the agent first operates; after you collapse it, this task stays quiet.",
|
|
79
|
+
"ui.closeWithSession": "Close the browser when the session ends",
|
|
80
|
+
"ui.closeWithSession.hint": "Closing releases the browser process; history and login state survive.",
|
|
81
|
+
"ui.virtualCursor": "Show the synthetic cursor",
|
|
82
|
+
"ui.virtualCursor.hint": "Makes the agent's operating position visible (its presence means the agent has taken over that tab).",
|
|
83
|
+
"vision.title": "Vision strategy",
|
|
84
|
+
"vision.strategy": "Default strategy",
|
|
85
|
+
"vision.auto": "Auto (switch by model capability and page)",
|
|
86
|
+
"vision.nonVisual": "Non-visual only (DOM / a11y tree)",
|
|
87
|
+
"vision.hint": "A model without image input always uses non-visual operation.",
|
|
88
|
+
"credentials.title": "Credentials",
|
|
89
|
+
"credentials.allowRead": "Allow the agent to read cookies / export login state",
|
|
90
|
+
"credentials.hint": "Off makes browser_auth refuse.",
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Read or patch the host-owned settings document. */
|
|
94
|
+
async function callSettings(patch) {
|
|
95
|
+
const response = await fetch(ROUTE, patch === undefined
|
|
96
|
+
? { method: "GET", headers: { accept: "application/json" } }
|
|
97
|
+
: {
|
|
98
|
+
method: "PUT",
|
|
99
|
+
headers: { "content-type": "application/json" },
|
|
100
|
+
body: JSON.stringify(patch),
|
|
101
|
+
})
|
|
102
|
+
const data = await response.json().catch(() => undefined)
|
|
103
|
+
if (data === undefined || data.ok !== true) {
|
|
104
|
+
throw new Error((data && data.error) || `HTTP ${response.status}`)
|
|
105
|
+
}
|
|
106
|
+
return data
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const styles = {
|
|
110
|
+
wrap: { display: "flex", flexDirection: "column", gap: "14px", padding: "4px 2px", fontSize: "13px" },
|
|
111
|
+
lead: { opacity: 0.75, lineHeight: 1.6, margin: 0 },
|
|
112
|
+
group: { border: "1px solid rgba(128,128,128,.28)", borderRadius: "8px", padding: "10px 12px" },
|
|
113
|
+
groupTitle: { margin: "0 0 8px", fontSize: "13px", fontWeight: 600 },
|
|
114
|
+
row: { display: "flex", gap: "10px", alignItems: "flex-start", padding: "5px 0" },
|
|
115
|
+
box: { marginTop: "2px" },
|
|
116
|
+
label: { display: "flex", flexDirection: "column", gap: "2px", cursor: "pointer" },
|
|
117
|
+
hint: { opacity: 0.68, lineHeight: 1.5 },
|
|
118
|
+
select: { marginTop: "4px", padding: "4px 6px", borderRadius: "6px", background: "transparent", color: "inherit", border: "1px solid rgba(128,128,128,.4)" },
|
|
119
|
+
status: { opacity: 0.7, minHeight: "18px" },
|
|
120
|
+
error: { color: "#e5534b" },
|
|
121
|
+
pathLine: { opacity: 0.6, wordBreak: "break-all", lineHeight: 1.5 },
|
|
122
|
+
button: { padding: "3px 10px", borderRadius: "6px", border: "1px solid rgba(128,128,128,.4)", background: "transparent", color: "inherit", cursor: "pointer" },
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** One boolean switch bound to a settings path. */
|
|
126
|
+
function Toggle(props) {
|
|
127
|
+
const t = translate
|
|
128
|
+
return h("div", { style: styles.row }, [
|
|
129
|
+
h("input", {
|
|
130
|
+
key: "i",
|
|
131
|
+
type: "checkbox",
|
|
132
|
+
style: styles.box,
|
|
133
|
+
checked: props.checked,
|
|
134
|
+
disabled: props.busy,
|
|
135
|
+
onChange: event => props.onChange(event.target.checked),
|
|
136
|
+
}),
|
|
137
|
+
h("label", { key: "l", style: styles.label, onClick: () => props.onChange(!props.checked) }, [
|
|
138
|
+
h("span", { key: "t" }, t(props.labelKey)),
|
|
139
|
+
h("span", { key: "h", style: styles.hint }, t(props.hintKey)),
|
|
140
|
+
]),
|
|
141
|
+
])
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** The settings page section. */
|
|
145
|
+
function SettingsRoot() {
|
|
146
|
+
const t = translate
|
|
147
|
+
const [settings, setSettings] = useState(null)
|
|
148
|
+
const [meta, setMeta] = useState({ path: "" })
|
|
149
|
+
const [error, setError] = useState("")
|
|
150
|
+
const [busy, setBusy] = useState(false)
|
|
151
|
+
const [saved, setSaved] = useState(false)
|
|
152
|
+
|
|
153
|
+
const load = useCallback(() => {
|
|
154
|
+
setError("")
|
|
155
|
+
callSettings().then(data => {
|
|
156
|
+
setSettings(data.settings)
|
|
157
|
+
setMeta({ path: data.path || "" })
|
|
158
|
+
}).catch(failure => setError(String(failure && failure.message ? failure.message : failure)))
|
|
159
|
+
}, [])
|
|
160
|
+
|
|
161
|
+
useEffect(() => { load() }, [load])
|
|
162
|
+
|
|
163
|
+
const patch = useCallback(next => {
|
|
164
|
+
setBusy(true)
|
|
165
|
+
setError("")
|
|
166
|
+
callSettings(next).then(data => {
|
|
167
|
+
setSettings(data.settings)
|
|
168
|
+
setSaved(true)
|
|
169
|
+
setTimeout(() => setSaved(false), 1200)
|
|
170
|
+
}).catch(failure => setError(String(failure && failure.message ? failure.message : failure)))
|
|
171
|
+
.then(() => setBusy(false))
|
|
172
|
+
}, [])
|
|
173
|
+
|
|
174
|
+
if (settings === null) {
|
|
175
|
+
return h("div", { style: styles.wrap }, [
|
|
176
|
+
h("p", { key: "l", style: styles.lead }, t("loading")),
|
|
177
|
+
error !== "" ? h("p", { key: "e", style: styles.error }, `${t("failed")} ${error}`) : null,
|
|
178
|
+
])
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const section = (titleKey, children) => h("div", { key: titleKey, style: styles.group }, [
|
|
182
|
+
h("p", { key: "t", style: styles.groupTitle }, t(titleKey)),
|
|
183
|
+
...children,
|
|
184
|
+
])
|
|
185
|
+
|
|
186
|
+
return h("div", { style: styles.wrap }, [
|
|
187
|
+
h("p", { key: "lead", style: styles.lead }, t("lead")),
|
|
188
|
+
|
|
189
|
+
section("history.title", [
|
|
190
|
+
h(Toggle, {
|
|
191
|
+
key: "enabled",
|
|
192
|
+
labelKey: "history.enabled",
|
|
193
|
+
hintKey: "history.hint",
|
|
194
|
+
checked: settings.history.enabled,
|
|
195
|
+
busy,
|
|
196
|
+
onChange: value => patch({ history: { enabled: value } }),
|
|
197
|
+
}),
|
|
198
|
+
]),
|
|
199
|
+
|
|
200
|
+
section("cookies.title", [
|
|
201
|
+
h(Toggle, {
|
|
202
|
+
key: "persist",
|
|
203
|
+
labelKey: "cookies.persist",
|
|
204
|
+
hintKey: "cookies.hint",
|
|
205
|
+
checked: settings.cookies.persist,
|
|
206
|
+
busy,
|
|
207
|
+
onChange: value => patch({ cookies: { persist: value } }),
|
|
208
|
+
}),
|
|
209
|
+
]),
|
|
210
|
+
|
|
211
|
+
section("ui.title", [
|
|
212
|
+
h(Toggle, {
|
|
213
|
+
key: "autoExpandOnce",
|
|
214
|
+
labelKey: "ui.autoExpandOnce",
|
|
215
|
+
hintKey: "ui.autoExpandOnce.hint",
|
|
216
|
+
checked: settings.ui.autoExpandOnce,
|
|
217
|
+
busy,
|
|
218
|
+
onChange: value => patch({ ui: { autoExpandOnce: value } }),
|
|
219
|
+
}),
|
|
220
|
+
h(Toggle, {
|
|
221
|
+
key: "closeWithSession",
|
|
222
|
+
labelKey: "ui.closeWithSession",
|
|
223
|
+
hintKey: "ui.closeWithSession.hint",
|
|
224
|
+
checked: settings.ui.closeWithSession,
|
|
225
|
+
busy,
|
|
226
|
+
onChange: value => patch({ ui: { closeWithSession: value } }),
|
|
227
|
+
}),
|
|
228
|
+
h(Toggle, {
|
|
229
|
+
key: "virtualCursor",
|
|
230
|
+
labelKey: "ui.virtualCursor",
|
|
231
|
+
hintKey: "ui.virtualCursor.hint",
|
|
232
|
+
checked: settings.ui.virtualCursor,
|
|
233
|
+
busy,
|
|
234
|
+
onChange: value => patch({ ui: { virtualCursor: value } }),
|
|
235
|
+
}),
|
|
236
|
+
]),
|
|
237
|
+
|
|
238
|
+
section("vision.title", [
|
|
239
|
+
h("div", { key: "row", style: styles.row }, [
|
|
240
|
+
h("label", { key: "l", style: styles.label }, [
|
|
241
|
+
h("span", { key: "t" }, t("vision.strategy")),
|
|
242
|
+
h("select", {
|
|
243
|
+
key: "s",
|
|
244
|
+
style: styles.select,
|
|
245
|
+
value: settings.vision.strategy,
|
|
246
|
+
disabled: busy,
|
|
247
|
+
onChange: event => patch({ vision: { strategy: event.target.value } }),
|
|
248
|
+
}, [
|
|
249
|
+
h("option", { key: "auto", value: "auto" }, t("vision.auto")),
|
|
250
|
+
h("option", { key: "nonVisual", value: "nonVisual" }, t("vision.nonVisual")),
|
|
251
|
+
]),
|
|
252
|
+
h("span", { key: "h", style: styles.hint }, t("vision.hint")),
|
|
253
|
+
]),
|
|
254
|
+
]),
|
|
255
|
+
]),
|
|
256
|
+
|
|
257
|
+
section("credentials.title", [
|
|
258
|
+
h(Toggle, {
|
|
259
|
+
key: "allowRead",
|
|
260
|
+
labelKey: "credentials.allowRead",
|
|
261
|
+
hintKey: "credentials.hint",
|
|
262
|
+
checked: settings.credentials.allowRead,
|
|
263
|
+
busy,
|
|
264
|
+
onChange: value => patch({ credentials: { allowRead: value } }),
|
|
265
|
+
}),
|
|
266
|
+
]),
|
|
267
|
+
|
|
268
|
+
h("div", { key: "status", style: styles.status }, [
|
|
269
|
+
saved ? h("span", { key: "s" }, t("saved")) : null,
|
|
270
|
+
error !== "" ? h("span", { key: "e", style: styles.error }, ` ${t("failed")} ${error} `) : null,
|
|
271
|
+
error !== "" ? h("button", { key: "r", style: styles.button, onClick: load }, t("retry")) : null,
|
|
272
|
+
]),
|
|
273
|
+
|
|
274
|
+
meta.path !== "" ? h("div", { key: "path", style: styles.pathLine }, `${t("path")}: ${meta.path}`) : null,
|
|
275
|
+
])
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function apply(ctx) {
|
|
279
|
+
ctx.effect(() => ctx.locale.register(NS, { zh, en }), "dsh-builtin-browser: dictionaries")
|
|
280
|
+
const t = ctx.locale.bind(NS)
|
|
281
|
+
translate = t
|
|
282
|
+
ctx.slots.inject("settings.section", () => ctx.slots.register({
|
|
283
|
+
name: "settings.section",
|
|
284
|
+
id: "dsh-builtin-browser",
|
|
285
|
+
// Above the rows the host ships (its furthest sits at 40), so this panel
|
|
286
|
+
// lands beneath them in the settings page.
|
|
287
|
+
order: 60,
|
|
288
|
+
label: () => t("nav"),
|
|
289
|
+
locale: NS,
|
|
290
|
+
inject: () => ({ t }),
|
|
291
|
+
}, SettingsRoot))
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
exports.name = name
|
|
295
|
+
exports.inject = inject
|
|
296
|
+
exports.apply = apply
|
|
297
|
+
return module.exports
|
|
298
|
+
}})
|
package/cordis.patch.yml
CHANGED
|
@@ -1,10 +1,29 @@
|
|
|
1
1
|
# dsh-builtin-browser bundle patch: mounts the shared-browser capability into a
|
|
2
|
-
# profile composition.
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
2
|
+
# profile composition.
|
|
3
|
+
#
|
|
4
|
+
# Row names matter beyond loading: the host's client-module scan resolves a row's
|
|
5
|
+
# specifier to a PACKAGE ROOT and reads `dsh.client` from there. A row named after
|
|
6
|
+
# a subpath (`dsh-builtin-browser/browser`) is not a package specifier and is
|
|
7
|
+
# skipped by design, which would leave the settings panel permanently absent even
|
|
8
|
+
# though every tool works — hence the inert root row under the bare package name.
|
|
9
|
+
#
|
|
10
|
+
# The browser seam (ctx.browser) is always mounted, and the Electron provider plus
|
|
11
|
+
# the browser_* tools are always registered: when a host provides an embedded view
|
|
12
|
+
# seam the provider drives that view, and otherwise it self-hosts its own Electron
|
|
13
|
+
# window, so installing the plugin is enough for the browser to work anywhere.
|
|
14
|
+
# DSH 0.2 no longer provides `electronViewHost` (the desktop shell's embedded
|
|
15
|
+
# view), so `ctx.get('electronViewHost')` currently resolves to undefined and
|
|
16
|
+
# every surface self-hosts. The row keeps reading it on purpose: a host that
|
|
17
|
+
# reintroduces such a seam is then picked up without a code change.
|
|
7
18
|
- insert:
|
|
19
|
+
# Root row. The host's client-module scan resolves a loader row's specifier
|
|
20
|
+
# to a PACKAGE ROOT; a row named after a subpath is skipped, and this package
|
|
21
|
+
# would then have no row to read its `dsh.client` declaration from — the
|
|
22
|
+
# browser would work while its settings panel never appeared. This row is
|
|
23
|
+
# inert (see src/index.ts) and exists only to name the package.
|
|
24
|
+
- id: dsh-builtin-browser
|
|
25
|
+
name: dsh-builtin-browser
|
|
26
|
+
|
|
8
27
|
- id: browser
|
|
9
28
|
name: dsh-builtin-browser/browser
|
|
10
29
|
|
package/docs/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
| --- | --- |
|
|
7
7
|
| 了解插件为什么存在、与无头方案的区别 | [为什么做共享真实浏览器](why-browser.md) |
|
|
8
8
|
| 安装、配置、日常使用与常见问题 | [用户指南](user-guide.md) |
|
|
9
|
-
| 全部
|
|
9
|
+
| 全部 34 个工具的参数、输出与示例 | [工具参考](tool-reference.md) |
|
|
10
10
|
| 了解 seam / provider / 工具三层与自托管实现 | [架构说明](architecture.md) |
|
|
11
11
|
| 回到项目首页 | [README](../README.md) |
|
|
12
12
|
|
package/docs/architecture.md
CHANGED
|
@@ -37,7 +37,7 @@ agent (browser_* 工具)
|
|
|
37
37
|
|
|
38
38
|
### 工具层(`src/tool-browser/`)
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
34 个 `browser_*` 工具,按**调用方任务**(`exec.agent.id`)维护独立浏览器会话:
|
|
41
41
|
|
|
42
42
|
- 会话缓存 `sessionsByTask`:同一任务复用同一会话,并发首开去重;
|
|
43
43
|
- `browser_reset_session` 关闭本任务会话并遗忘映射(即使 close 抛错也清除,下次调用重建);
|
package/docs/tool-reference.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 工具参考
|
|
2
2
|
|
|
3
|
-
全部
|
|
3
|
+
全部 34 个 `browser_*` 工具。守卫列:✅ 表示该动作受 `browser_restrict` 白名单约束;只读工具永不拦截。
|
|
4
4
|
|
|
5
5
|
## 页面与导航
|
|
6
6
|
|
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
| 工具 | 参数 | 输出 | 守卫 | 说明 |
|
|
50
50
|
| --- | --- | --- | --- | --- |
|
|
51
51
|
| `browser_history` | – | `{ entries[] }` | – | 操作日志(最新在后),含 seq/action/ok/params/result/error |
|
|
52
|
+
| `browser_visited` | `limit`(默认 30,上限 200), `domain`(主机名包含匹配) | `{ count, entries[] }` | – | **持久化浏览历史**(访问过的页面,最新在前):与 cookie 同址落盘(`$DSH_HOME/dsh-builtin-browser-host/history.jsonl`),关闭浏览器与重启 DSH 后仍在;上限 5000 条或 90 天。重开某条用 `browser_open`;与 `browser_history`(会话内操作日志,随会话消失)是两件事;可在设置里关闭记录 |
|
|
52
53
|
| `browser_replay` | `seq`(必填) | `{ replayed }` | ✅ | 按序号回放某一步(navigate/execute/click/type/scroll/key) |
|
|
53
54
|
| `browser_download` | `url`(必填), `savePath`(必填) | `{ path }` | ✅ | 带会话 cookie 下载到本地(仅 http(s);`savePath` 必须为绝对路径且位于 `downloadDir` 内——默认系统下载目录,自动识别 `Downloads`/`下载`/`下載` 与 `XDG_DOWNLOAD_DIR`;不覆盖已有文件;上限 256MB,受 CORS 约束;由子进程直接落盘) |
|
|
54
55
|
|
package/lib/browser/runtime.d.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { Context, Service } from '@deepseek-ai/cordis';
|
|
10
10
|
import z from '@deepseek-ai/schemastery';
|
|
11
|
-
import type { BrowserA11yRequest, BrowserA11yResult, BrowserCheckRequest, BrowserClearRequest, BrowserClickRequest, BrowserContentRequest, BrowserContentResult, BrowserDownloadRequest, BrowserElementTarget, BrowserExecuteRequest, BrowserExecuteResult, BrowserFillRequest, BrowserFillResult, BrowserGetValueRequest, BrowserGetValueResult, BrowserHistoryEntry, BrowserNavigateRequest, BrowserOpenRequest, BrowserProvider, BrowserScrapeRequest, BrowserScrapeResult, BrowserScreenshotRequest, BrowserScreenshotResult, BrowserSelectRequest, BrowserSelectResult, BrowserSessionId, BrowserSetValueRequest, BrowserSetValueResult, BrowserSnapshotResult, BrowserTab, BrowserTypeRequest, BrowserWaitRequest, BrowserWaitResult, BrowserScrollRequest, BrowserKeyRequest, BrowserChallenge, ExportedCookie } from './types.js';
|
|
11
|
+
import type { BrowserA11yRequest, BrowserA11yResult, BrowserCheckRequest, BrowserClearRequest, BrowserClickRequest, BrowserContentRequest, BrowserContentResult, BrowserDownloadRequest, BrowserElementTarget, BrowserExecuteRequest, BrowserExecuteResult, BrowserFillRequest, BrowserFillResult, BrowserGetValueRequest, BrowserGetValueResult, BrowserHistoryEntry, BrowserNavigateRequest, BrowserOpenRequest, BrowserProvider, BrowserScrapeRequest, BrowserScrapeResult, BrowserScreenshotRequest, BrowserScreenshotResult, BrowserSelectRequest, BrowserSelectResult, BrowserSessionId, BrowserSetValueRequest, BrowserSetValueResult, BrowserSnapshotResult, BrowserTab, BrowserTypeRequest, BrowserWaitRequest, BrowserWaitResult, BrowserScrollRequest, BrowserKeyRequest, BrowserChallenge, ExportedCookie, VisitedPage } from './types.js';
|
|
12
12
|
export { BrowserError, } from './types.js';
|
|
13
13
|
export type { BrowserA11yNode, BrowserA11yRequest, BrowserA11yResult, BrowserChallenge, BrowserCheckRequest, BrowserClearRequest, BrowserClickRequest, BrowserContentFormat, BrowserElementTarget, BrowserGetValueRequest, BrowserGetValueResult, BrowserScrapeField, BrowserScrapeRequest, BrowserScrapeResult, BrowserSelectRequest, BrowserSelectResult, BrowserSetValueRequest, BrowserSetValueResult, BrowserWaitRequest, BrowserWaitResult, BrowserScrollRequest, BrowserKeyRequest, BrowserContentRequest, BrowserContentResult, BrowserDownloadRequest, BrowserExecuteRequest, BrowserExecuteResult, BrowserFillField, BrowserFillRequest, BrowserFillResult, BrowserHistoryEntry, BrowserNavigateRequest, BrowserOpenRequest, BrowserProvider, BrowserScreenshotRequest, BrowserScreenshotResult, BrowserSessionId, BrowserSnapshotElement, BrowserSnapshotResult, BrowserTab, BrowserTypeRequest, ExportedCookie, } from './types.js';
|
|
14
14
|
declare module '@deepseek-ai/cordis' {
|
|
@@ -118,6 +118,23 @@ export declare class BrowserRuntime extends Service {
|
|
|
118
118
|
detectChallenge(session: BrowserSessionId, signal?: AbortSignal): Promise<BrowserChallenge>;
|
|
119
119
|
/** Return the session's chronological operation log through the provider. */
|
|
120
120
|
history(session: BrowserSessionId): Promise<readonly BrowserHistoryEntry[]>;
|
|
121
|
+
/**
|
|
122
|
+
* Persisted visits, newest first — the record that outlives the browser
|
|
123
|
+
* process, as opposed to {@link history}'s session-scoped operation log.
|
|
124
|
+
* @param options - result cap and an optional hostname filter.
|
|
125
|
+
* @returns the retained visits (empty when history recording is disabled).
|
|
126
|
+
*/
|
|
127
|
+
visited(options?: {
|
|
128
|
+
readonly limit?: number;
|
|
129
|
+
readonly domain?: string;
|
|
130
|
+
}): readonly VisitedPage[];
|
|
131
|
+
/**
|
|
132
|
+
* Whether a session is still live. Sessions can end outside the tool layer
|
|
133
|
+
* (the human closed its window), so a cached id must be re-checked before
|
|
134
|
+
* reuse; a provider that does not implement the check counts as live.
|
|
135
|
+
* @param session - the session id to test.
|
|
136
|
+
*/
|
|
137
|
+
exists(session: BrowserSessionId): boolean;
|
|
121
138
|
/** Replay one recorded operation by sequence number through the provider. */
|
|
122
139
|
replay(session: BrowserSessionId, seq: number): Promise<void>;
|
|
123
140
|
/** Download a URL to a local file through the provider. */
|