dsh-builtin-browser 0.4.7 → 0.4.8
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 +28 -0
- package/README.en.md +62 -491
- package/README.md +44 -470
- package/client.js +10 -10
- package/cordis.patch.yml +4 -4
- package/desktop-bridge/plugin-browser-bridge.js +1 -1
- package/docs/README.md +8 -4
- package/docs/architecture.md +82 -11
- package/docs/development.md +38 -0
- package/docs/limitations.md +49 -0
- package/docs/tool-reference.md +42 -42
- package/docs/user-guide.md +93 -37
- package/docs/why-browser.md +3 -3
- package/lib/browser/types.d.ts +2 -2
- package/lib/browser-electron/entry.d.ts +2 -2
- package/lib/browser-electron/entry.js +1 -1
- package/lib/browser-electron/history-store.d.ts +1 -1
- package/lib/browser-electron/history-store.js +1 -1
- package/lib/browser-electron/host-main.js +3 -3
- package/lib/browser-electron/provider.d.ts +9 -9
- package/lib/browser-electron/provider.js +14 -14
- package/lib/browser-electron/remote-host.js +1 -1
- package/lib/browser-electron/settings-store.d.ts +4 -4
- package/lib/index.d.ts +1 -1
- package/lib/index.js +1 -1
- package/lib/tool-browser/element-fields.d.ts +4 -4
- package/lib/tool-browser/element-fields.js +6 -6
- package/lib/tool-browser/index.d.ts +6 -6
- package/lib/tool-browser/index.js +161 -139
- package/package.json +2 -2
- package/scripts/verify-host-compat.mjs +6 -6
- package/src/browser/types.ts +2 -2
- package/src/browser-electron/entry.ts +2 -2
- package/src/browser-electron/history-store.ts +1 -1
- package/src/browser-electron/host-main.ts +3 -3
- package/src/browser-electron/provider.ts +15 -15
- package/src/browser-electron/remote-host.ts +1 -1
- package/src/browser-electron/settings-store.ts +4 -4
- package/src/index.ts +1 -1
- package/src/tool-browser/element-fields.ts +6 -6
- package/src/tool-browser/index.ts +164 -140
package/client.js
CHANGED
|
@@ -64,15 +64,15 @@ window.__ModuleLoader__.load({ id: "dsh-builtin-browser", factory: (require) =>
|
|
|
64
64
|
"browser.edge": "本机 Edge",
|
|
65
65
|
"browser.auto": "自动(优先 Chrome,其次 Edge)",
|
|
66
66
|
"browser.hint": "选本机浏览器时会以**独立 profile** 启动它:不会打开、占用或修改你日常的窗口、书签与登录状态,关闭也不会关掉你自己的浏览器;代价是它看不到你日常浏览器里已登录的站点,需要时请在那个窗口里登录一次(登录态会留在插件自己的 profile 里)。桌面端默认由官方侧栏承载页面,此选项优先于侧栏。",
|
|
67
|
-
"credentials.hint": "关闭后
|
|
67
|
+
"credentials.hint": "关闭后 dsh_browser_auth 的导出会被拒绝,报 BROWSER_AUTH_DISABLED —— 导出读的是整台机器共享的 cookie 罐(按当前页所属站点收敛)。它只管**读**:写入登录状态(restore)归下面「Agent 能做什么」里的写入开关。桌面端请注意:Agent 驱动的是侧栏里那一个页面(人机同页),因此它与该页面共用同一个 partition —— 这也正是它能读到登录态的原因,属于有意接受的沙箱边界变化;关闭本项即拒绝读取。",
|
|
68
68
|
"actions.title": "Agent 能做什么",
|
|
69
|
-
"actions.lead": "与
|
|
69
|
+
"actions.lead": "与 dsh_browser_restrict 不同:那一层是模型自己设的软护栏,它随时可以解除;这里的三项由你在设置里决定,模型手里的工具改不了它们。",
|
|
70
70
|
"actions.allowExecute": "允许在页面里执行脚本",
|
|
71
|
-
"actions.allowExecute.hint": "
|
|
71
|
+
"actions.allowExecute.hint": "dsh_browser_execute 能在当前页面里运行任意 JavaScript —— 它的能力上限就是这个页面能做的事。关闭后该工具一律被拒(BROWSER_EXECUTE_DISABLED),模型手里的工具没有一个能改这项设置。但也别把它当成锁:设置文档由本机 HTTP 端点读写,同源页面可以写它 —— 这不是凭据系统。",
|
|
72
72
|
"actions.allowDownload": "允许下载文件到磁盘",
|
|
73
|
-
"actions.allowDownload.hint": "
|
|
73
|
+
"actions.allowDownload.hint": "dsh_browser_download 会把 URL 下载到 downloadDir 内(带登录态;仍需通过目录准入,不覆盖已有文件)。关闭后一律被拒(BROWSER_DOWNLOAD_DISABLED)。截图另存走的是同一套目录准入,不受此项影响。",
|
|
74
74
|
"actions.allowCredentialWrite": "允许写入登录状态",
|
|
75
|
-
"actions.allowCredentialWrite.hint": "
|
|
75
|
+
"actions.allowCredentialWrite.hint": "dsh_browser_auth 的 restore 会向任意域写入 cookie。关闭后只能导出、不能导入(BROWSER_AUTH_WRITE_DISABLED);读取仍由上面「凭据」那一项控制。",
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
const en = {
|
|
@@ -112,15 +112,15 @@ window.__ModuleLoader__.load({ id: "dsh-builtin-browser", factory: (require) =>
|
|
|
112
112
|
"browser.edge": "Installed Edge",
|
|
113
113
|
"browser.auto": "Automatic (Chrome first, then Edge)",
|
|
114
114
|
"browser.hint": "Choosing an installed browser launches it with a **separate profile**: your everyday windows, bookmarks and logins are never opened, locked or modified, and closing the plugin never closes your browser. The trade-off is that it does not see sites you are already signed into there — sign in once in that window and the session stays in the plugin's own profile. On the desktop the shell's sidebar normally carries the page; this setting outranks it.",
|
|
115
|
-
"credentials.hint": "Off makes
|
|
115
|
+
"credentials.hint": "Off makes dsh_browser_auth refuse the export, reporting BROWSER_AUTH_DISABLED — an export reads the machine-wide shared cookie jar (narrowed to the current page's site). It governs READING only: writing login state (restore) answers to the write switch under \"What the agent may do\" below. On the desktop, note that the agent drives the sidebar's own page (one page for both parties), so it shares that page's partition — which is exactly why it can reach the login state. That is a deliberate sandbox-boundary change; this switch is how you refuse it.",
|
|
116
116
|
"actions.title": "What the agent may do",
|
|
117
|
-
"actions.lead": "Unlike
|
|
117
|
+
"actions.lead": "Unlike dsh_browser_restrict, which is the model's own soft guardrail and can be lifted by the model at any time, these three are yours: the model's own tools cannot change them.",
|
|
118
118
|
"actions.allowExecute": "Run scripts in the page",
|
|
119
|
-
"actions.allowExecute.hint": "
|
|
119
|
+
"actions.allowExecute.hint": "dsh_browser_execute runs arbitrary JavaScript in the active tab — its reach is everything that page can do. Off refuses the tool outright (BROWSER_EXECUTE_DISABLED), and no tool the model holds can change this setting. Do not read that as a lock, though: the document is served over a local HTTP endpoint and a same-origin page can write it. This is not a credential system.",
|
|
120
120
|
"actions.allowDownload": "Download files to disk",
|
|
121
|
-
"actions.allowDownload.hint": "
|
|
121
|
+
"actions.allowDownload.hint": "dsh_browser_download fetches a URL into downloadDir (with login state; still subject to the directory admission gate, never overwriting an existing file). Off refuses it outright (BROWSER_DOWNLOAD_DISABLED). Saving a screenshot goes through the same directory gate and is not covered by this switch.",
|
|
122
122
|
"actions.allowCredentialWrite": "Write login state",
|
|
123
|
-
"actions.allowCredentialWrite.hint": "
|
|
123
|
+
"actions.allowCredentialWrite.hint": "dsh_browser_auth's restore writes cookies to arbitrary domains. Off allows export but not import (BROWSER_AUTH_WRITE_DISABLED); reading stays under the Credentials switch above.",
|
|
124
124
|
}
|
|
125
125
|
|
|
126
126
|
/** Read or patch the host-owned settings document. */
|
package/cordis.patch.yml
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
# though every tool works — hence the inert root row under the bare package name.
|
|
9
9
|
#
|
|
10
10
|
# The browser seam (ctx.browser) is always mounted, and the Electron provider plus
|
|
11
|
-
# the
|
|
11
|
+
# the dsh_browser_* tools are always registered. The CARRIER is chosen at run time:
|
|
12
12
|
#
|
|
13
13
|
# 1. a browser the user selected in settings (their installed Chrome / Edge),
|
|
14
14
|
# launched with a plugin-owned profile;
|
|
@@ -47,14 +47,14 @@
|
|
|
47
47
|
# Every field is optional, so an empty mapping uses the defaults:
|
|
48
48
|
#
|
|
49
49
|
# allowedActions: operator-level allow-list of browser tool names, applied to every
|
|
50
|
-
# task in the deployment (a task can still narrow it further with
|
|
51
|
-
# READ_ONLY_TOOLS and
|
|
50
|
+
# task in the deployment (a task can still narrow it further with dsh_browser_restrict).
|
|
51
|
+
# READ_ONLY_TOOLS and dsh_browser_restrict are never blocked, whatever the list says.
|
|
52
52
|
# timeoutMs: cooperative per-call budget in ms (default 60000).
|
|
53
53
|
# tabTools: whether the tab-management tools are registered (default true).
|
|
54
54
|
#
|
|
55
55
|
# To restrict a deployment, replace this with, for example:
|
|
56
56
|
# config:
|
|
57
|
-
# allowedActions: [
|
|
57
|
+
# allowedActions: [dsh_browser_snapshot, dsh_browser_a11y, dsh_browser_content, dsh_browser_click]
|
|
58
58
|
#
|
|
59
59
|
# The settings panel's "What the agent may do" switches are a SEPARATE layer with the
|
|
60
60
|
# same property — the model cannot lift them either — and they need no patch file at
|
|
@@ -500,7 +500,7 @@ async function handle(request) {
|
|
|
500
500
|
//
|
|
501
501
|
// The comment above said that; the code below did not enforce it. It closed every webview
|
|
502
502
|
// whose title matched — and with no filter, EVERY webview — regardless of which session held
|
|
503
|
-
// it. So one session's `
|
|
503
|
+
// it. So one session's `dsh_browser_reset_session` could destroy another session's page, which is
|
|
504
504
|
// the worst version of the bleed: not merely seeing someone else's tab, but closing it.
|
|
505
505
|
//
|
|
506
506
|
// Ownership is now the outer bound: only guests this owner holds are candidates at all, and
|
package/docs/README.md
CHANGED
|
@@ -8,11 +8,15 @@
|
|
|
8
8
|
| 安装、配置、日常使用与常见问题 | [用户指南](user-guide.md) |
|
|
9
9
|
| 全部 34 个工具的参数、输出与示例 | [工具参考](tool-reference.md) |
|
|
10
10
|
| 了解 seam / provider / 工具三层与自托管实现 | [架构说明](architecture.md) |
|
|
11
|
+
| 知道哪些事**做不到**、以及为什么 | [已知限制](limitations.md) |
|
|
12
|
+
| 构建、测试、复验宿主兼容性 | [开发](development.md) |
|
|
11
13
|
| 回到项目首页 | [README](../README.md) |
|
|
12
14
|
|
|
13
15
|
## 各篇概览
|
|
14
16
|
|
|
15
|
-
- **[为什么做共享真实浏览器](why-browser.md)**
|
|
16
|
-
- **[用户指南](user-guide.md)**
|
|
17
|
-
- **[工具参考](tool-reference.md)**
|
|
18
|
-
- **[架构说明](architecture.md)**
|
|
17
|
+
- **[为什么做共享真实浏览器](why-browser.md)** —— 定位、设计理念、与 Playwright 等无头方案的对比、边界与取舍。
|
|
18
|
+
- **[用户指南](user-guide.md)** —— 环境要求、安装、配置、操作纪律、FAQ、故障排查、验证过的版本、更新方式(两端不同)、以及从 0.4.7 升级的改名说明。
|
|
19
|
+
- **[工具参考](tool-reference.md)** —— 每个 `dsh_browser_*` 工具的参数、输出、守卫与示例。
|
|
20
|
+
- **[架构说明](architecture.md)** —— 三层结构、`ElectronBrowserViewHost` 接缝、四种载体的分工(桌面侧栏 / 本机 Chrome-Edge / `electronViewHost` / 自托管)、自托管 RPC 子进程、截图通道、Electron 版本选择。
|
|
21
|
+
- **[已知限制](limitations.md)** —— 截图、登录态、权限门控、下载路径、载体行为的边界,以及本插件**不提供**的东西。
|
|
22
|
+
- **[开发](development.md)** —— 构建与测试命令、代码结构、宿主兼容性复验、peer 范围的 semver 语义。
|
package/docs/architecture.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
## 三层结构
|
|
4
4
|
|
|
5
5
|
```
|
|
6
|
-
agent (
|
|
6
|
+
agent (dsh_browser_* 工具)
|
|
7
7
|
→ ctx.browser (seam, dsh-builtin-browser/browser)
|
|
8
8
|
→ dsh-builtin-browser/browser-electron (provider)
|
|
9
9
|
→ ElectronBrowserViewHost (由宿主外壳提供)
|
|
@@ -37,24 +37,24 @@ agent (browser_* 工具)
|
|
|
37
37
|
|
|
38
38
|
### 工具层(`src/tool-browser/`)
|
|
39
39
|
|
|
40
|
-
34 个 `
|
|
40
|
+
34 个 `dsh_browser_*` 工具,按**调用方任务**(`exec.agent.id`)维护独立浏览器会话:
|
|
41
41
|
|
|
42
42
|
- 会话缓存 `sessionsByTask`:同一任务复用同一会话,并发首开去重;
|
|
43
|
-
- `
|
|
43
|
+
- `dsh_browser_reset_session` 关闭本任务会话并遗忘映射(即使 close 抛错也清除,下次调用重建);
|
|
44
44
|
- 会话生命周期绑定 agent 作用域 ctx:agent(DSH 会话)销毁时自动关闭对应浏览器会话,任务结束后不再泄漏窗口/视图;
|
|
45
45
|
- 会话/开启动态/白名单都是**每插件实例(每 context)作用域**的,多 context 互不共享、互不污染;
|
|
46
|
-
- `
|
|
47
|
-
- 输出 schema 与返回值严格一致(DSH 运行时会校验,`additionalProperties: false` 下多一个字段都会报错):元素与节点的字段集在 `element-fields.ts` 里**只声明一次**,item schema 与投影都由它导出 —— 手写白名单漏一个字段会**静默丢数据**、多一个会让**整个结果被拒**,同源之后这两个方向都不可能单独出错(`
|
|
46
|
+
- `dsh_browser_restrict` 维护本任务白名单,守卫所有非只读工具;
|
|
47
|
+
- 输出 schema 与返回值严格一致(DSH 运行时会校验,`additionalProperties: false` 下多一个字段都会报错):元素与节点的字段集在 `element-fields.ts` 里**只声明一次**,item schema 与投影都由它导出 —— 手写白名单漏一个字段会**静默丢数据**、多一个会让**整个结果被拒**,同源之后这两个方向都不可能单独出错(`dsh_browser_a11y` 的 `selector` 与 `dsh_browser_screenshot` 的 `width`/`height` 都是这么丢的)。
|
|
48
48
|
|
|
49
49
|
### 输出尺寸(工具层)
|
|
50
50
|
|
|
51
51
|
工具输出花的是调用方的上下文,所以工具层对几处返回设了明确上限,并在截断时**说明截断**:
|
|
52
52
|
|
|
53
|
-
- `
|
|
54
|
-
- `
|
|
55
|
-
- `
|
|
56
|
-
- `
|
|
57
|
-
- `
|
|
53
|
+
- `dsh_browser_a11y` 默认 150 个节点(上限 5000)、一层一个空格缩进、`states` 只省略字面量 `enabled`(`unchecked`、`collapsed` 等仍会打印);
|
|
54
|
+
- `dsh_browser_snapshot` 与 `dsh_browser_a11y` 在元素有 `id`/`name` 时给出 `{#id}` / `{[name=x]}` 引用选择器(页面脚本一直算,工具层此前丢掉;输出 schema 必须声明该字段,否则 `additionalProperties: false` 会拒绝整个结果);
|
|
55
|
+
- `dsh_browser_content` 的上限按格式:html/json 50 000,txt/markdown 20 000,`maxChars` 可覆盖;
|
|
56
|
+
- `dsh_browser_execute` 的返回值上限 50 000 字符;
|
|
57
|
+
- `dsh_browser_scrape` 输出紧凑 JSON;`dsh_browser_history` 默认只渲染最近 20 条且不回显参数(`verbose: true` 才带);`dsh_browser_visited` 的时间戳精确到分钟、会话标记只在变化时打印一次。
|
|
58
58
|
|
|
59
59
|
## 自托管实现(纯 `dsh web`)
|
|
60
60
|
|
|
@@ -74,7 +74,7 @@ RemoteElectronViewHost ──TCP JSON-RPC──▶ host-main.js
|
|
|
74
74
|
- **稳健性**:子进程/套接字都有 `error` 监听(否则未捕获事件会炸掉整个 DSH 进程);子进程退出自动重启,已物化的视图句柄在下次调用时**自动重建并重试一次**(会话跨崩溃存活,仅页面状态丢失,不再出现 "browser host is not running" 僵尸态);物化失败可重试;下载仅限 HTTP(S)、`savePath` 必须绝对路径(`downloadDir` 已配置时用它;未配置时按序探测 —— 存在的 `XDG_DOWNLOAD_DIR` → home 下第一个存在的 `Downloads`/`下载`/`下載` → 回退 `~/Downloads`,首次使用时创建)、流式限流 + Content-Length 提前拒绝、256MB 上限与 60s 超时,文件由**子进程直接落盘**(临时文件 + rename,不再经 RPC 传 base64);cookie 导出/恢复有 30s 超时,两者受**不同**的门控:导出是**读**动作,读 `credentials.allowRead`,关闭时抛 `BROWSER_AUTH_DISABLED`;恢复是**写**动作,读 `actions.allowCredentialWrite`,关闭时抛 `BROWSER_AUTH_WRITE_DISABLED`(允许读取的操作者因此仍能拒绝写入)。有原生方法时优先用它,否则走 CDP 的 `Network.getCookies` / `Storage.setCookies`(三种载体都可用);**导出的范围收敛到本会话当前所在页面的 URL**,页面 URL 取不到时**拒绝而不是放宽**(CDP 载体报 `BROWSER_AUTH_SCOPE_UNKNOWN`,自托管侧栏按 view 自己的 URL 过滤);`cookies.get`/`cookies.set`(后者每条 cookie 一次)与下载的 `Runtime.evaluate` 都套同一套 `COMMAND_TIMEOUT_MS` —— 这三条曾经是无上限的 await,会让串行操作队列永久停在那一条上;
|
|
75
75
|
- **视图可见性**:多标签/多会话时,`showView` 隐藏其他视图并把目标视图置顶(remove+re-add),确保用户看到的是活动标签;目标是当前可见视图时**跳过** remove/re-add(否则每次操作都闪烁);窗口标题实时显示当前可见会话的任务标识 + 页面标题/URL(`showView` 携带会话 label,子进程读 `getTitle`/`getURL`);窗口 resize 时视图 bounds 自动跟随;
|
|
76
76
|
- **孤儿防护**:父进程断开时子进程自动退出,不留僵尸窗口;
|
|
77
|
-
- **cookie 落盘**:子进程使用独立 userData 目录(`<DSH_HOME>/dsh-builtin-browser-host`),登录态跨重启保留(另有 `
|
|
77
|
+
- **cookie 落盘**:子进程使用独立 userData 目录(`<DSH_HOME>/dsh-builtin-browser-host`),登录态跨重启保留(另有 `dsh_browser_auth` 手动导出/恢复)。
|
|
78
78
|
|
|
79
79
|
## 关键设计决策
|
|
80
80
|
|
|
@@ -86,3 +86,74 @@ RemoteElectronViewHost ──TCP JSON-RPC──▶ host-main.js
|
|
|
86
86
|
| 独立 userData | 多实例争用默认目录导致 GPU 缓存/会话锁冲突 |
|
|
87
87
|
| withTimeout 全覆盖 | 卡死的 CDP 调用必须能被工具超时兜底 |
|
|
88
88
|
| 输出 schema 严格匹配 | DSH 运行时会校验返回值,多字段即报错 |
|
|
89
|
+
|
|
90
|
+
## 载体与桌面外壳的分工
|
|
91
|
+
|
|
92
|
+
插件会自动选载体,也可以在设置里指定。共四种,优先级是:**设置里显式选择的本机浏览器 > 桌面端侧栏 > 自托管**。
|
|
93
|
+
|
|
94
|
+
### ① 桌面端:驱动官方侧栏的页面(人机同页)
|
|
95
|
+
|
|
96
|
+
DSH Desktop 是两层结构:Electron 外壳 + 一个 `--expose-internals` 的 **Node 模式宿主**(插件就跑在宿主里,**没有 Electron API**)。0.2 移除了 `electronViewHost`,宿主与外壳之间也没有任何承载视图的通道,所以插件**借道一条小桥**:外壳主进程里跑一个 loopback + token 的 bridge,把侧栏浏览器的 guest(就是界面上那个页面)的 CDP 交给插件。
|
|
97
|
+
|
|
98
|
+
结果是:**agent 操作的页面就是人看的页面**,插件不再 spawn 自己的 Electron,也不再出现第二个窗口。
|
|
99
|
+
|
|
100
|
+
**页面落在哪个会话,由发起的会话决定,与屏幕上显示谁无关。** 外壳为每个会话各挂一个侧栏,而插件的渲染侧半边按**会话 id** 打开那一会话自己的面板(DSH 的 `openTabIn`),不读"当前显示的是谁" —— 所以"在 A 里让 agent 开页面、随后切到 B"不会把页面开到 B 去,结构上也没有通往别人侧栏的路径。
|
|
101
|
+
|
|
102
|
+
> **一条来自外壳的行为**:外壳只渲染**当前显示会话**的侧栏。因此当发起会话不是屏幕上那个时,面板会被建立(它的标签确实进了那个会话),但页面要等该会话被显示时才附着上来。表现是:切走后在 A 里再让 agent 开页面,会得到一句"面板已放置,页面只能在被显示时附着",而 A 的侧栏此后一直是它自己的样子 —— **那是提示,不是串扰**。
|
|
103
|
+
|
|
104
|
+
安装这条桥(改的是**已安装的桌面端**,所以做成可重放):
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
node desktop-bridge/install.mjs # 幂等;首次会备份 main.js.before-bridge
|
|
108
|
+
node desktop-bridge/install.mjs --revert # 回滚
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
> **桌面端升级后要重跑一次 `install.mjs`** —— 升级会替换 `resources/app/`,bridge 随之消失。bridge 不在时插件自动退回自托管,功能不会中断,只是会多出一个独立窗口。
|
|
112
|
+
|
|
113
|
+
> **⚠️ 沙箱边界的变化(请明示知悉)**
|
|
114
|
+
>
|
|
115
|
+
> 官方侧栏浏览器的前提是:侧栏里的页面**不被外部读取** —— 它有独立的 partition,且宿主拒绝跨站内容访问。让 Agent 驱动该 guest,等于**有意打破这个前提**:
|
|
116
|
+
>
|
|
117
|
+
> - Agent 能读到你在侧栏访问的**任何页面的内容**(这正是"人机同页"的意义);
|
|
118
|
+
> - Agent 能读到该 partition 中的 **Cookie / 登录态**,`dsh_browser_auth` 可将其导出(由设置项控制);
|
|
119
|
+
> - 反过来,你在侧栏里的操作与 Agent 的操作**作用于同一个页面**,可能互相影响(Agent 不会主动覆盖你的输入,但导航会改变双方看到的内容)。
|
|
120
|
+
>
|
|
121
|
+
> 这是"人机同页"的必然代价。我们认为值得(它把"Agent 在一个你看不见的窗口里操作"变成"你能看着它操作并随时接手"),但你有权知道它存在 —— 因此也提供了开关:**「凭据访问」关闭后 `dsh_browser_auth` 的导出会被拒绝(`BROWSER_AUTH_DISABLED`)—— 它只管读;写入登录态(`restore`)另有「允许写入登录状态」开关,报 `BROWSER_AUTH_WRITE_DISABLED`,两者独立**;**「视觉策略」设为纯非视觉后,任何依赖截图定位的坐标点击都会被拒绝**。不想接受这个边界变化时,把 desktop profile 里的插件移除即可回到"独立窗口"的旧形态。
|
|
122
|
+
|
|
123
|
+
### ② 用你自己的浏览器(Chrome / Edge)
|
|
124
|
+
|
|
125
|
+
设置里可以把载体改成**本机已安装的 Chrome 或 Edge**(`browser.channel`:`bundled` / `auto` / `chrome` / `edge`)。做法与 Codex Browser Use 一致:以 `--remote-debugging-port=0` 启动,读浏览器自己写下的 `DevToolsActivePort` 得到端口,再全程走 CDP(用 Node 22 内置的 `WebSocket`,**不新增依赖**)。
|
|
126
|
+
|
|
127
|
+
**你自己的数据不会被碰**:插件用的是**独立 profile**(`$DSH_HOME/dsh-builtin-browser-host/<chrome|edge>-profile`),不会打开、占用或修改你日常的窗口、书签与登录状态;插件退出也不会关掉你的浏览器。
|
|
128
|
+
|
|
129
|
+
**浏览器死了之后会自己回来**:这个载体会观察子进程的退出并清掉**已失效的会话映射**(不只是缓存里的连接),所以重启后的浏览器是**可用的**,而不是"活着但每条命令都报 Session not found"。重新拉起前会先 kill 旧进程并**最多等 3 秒**它真正退出(Windows 上 `kill` 是异步的,不等就会把新进程交给旧实例、然后被误报成"路径不是可运行的浏览器"),并**删掉上一轮写下的 `DevToolsActivePort`、只认本次启动之后写入的那份**(否则会连到别的 Chromium 或 `node --inspect` 上,发布一个自己既不属于、也杀不掉的客户端)。
|
|
130
|
+
|
|
131
|
+
反过来,**页面慢不等于浏览器死了**:命令**超时**只把连接标记为"存疑",下一次调用先用 `Browser.getVersion` 探一次再决定是否丢弃;只有**socket 已关闭**才直接重启 —— 因此一次慢页面不会让插件杀掉你正在用的浏览器窗口、连你打开的标签和填到一半的表单一起丢掉。
|
|
132
|
+
|
|
133
|
+
同一套判活也用在桌面侧栏那条桥上,但判据不同:桥的端点文件由外壳的 keep-alive **每 15 秒重写一次**,所以一个**超过一分钟没被刷新**的端点被判定为墓碑(那是退出的外壳留下的最后一份写入),插件据此报告载体已消失并换回自托管 —— 而不是对着一个没人监听的端口一直发命令。
|
|
134
|
+
|
|
135
|
+
**登录态怎么办**:
|
|
136
|
+
|
|
137
|
+
- `cookies.persist` **开**(默认)→ 上面那个固定 profile 会保留,**重启 DSH 后仍是登录状态**;`dsh_browser_auth` 也照常可导出/恢复该 profile 的 Cookie。
|
|
138
|
+
- `cookies.persist` **关** → 每次用**临时 profile**,释放浏览器时整个目录被删除,不留登录痕迹。**进程被强杀时释放路径跑不到**,所以下一次启动会**先清扫**上一轮残留的临时 profile —— 只清"属主进程已不在 **且** 一小时没有写入"的那些,免得删掉仍在运行的孤儿浏览器正在用的目录。
|
|
139
|
+
- 代价要说清:独立 profile **看不到**你日常浏览器里已登录的站点 —— 在插件打开的窗口里登录一次即可,之后登录态就存在它自己的 profile 里。
|
|
140
|
+
|
|
141
|
+
### ③ 有 `electronViewHost` 的宿主(旧版桌面外壳)
|
|
142
|
+
|
|
143
|
+
直接使用外壳提供的视图。
|
|
144
|
+
|
|
145
|
+
### ④ 没有外壳(纯 `dsh web`)
|
|
146
|
+
|
|
147
|
+
自托管 —— spawn 插件自带的 Electron 窗口,功能照常。
|
|
148
|
+
|
|
149
|
+
> 可见视图与列布局始终属于宿主外壳;插件只负责 seam、provider 与工具。各形态下**工具集、浏览历史、设置栏、可视化鼠标、收尾语义完全一致**,差别只在页面由谁承载。
|
|
150
|
+
|
|
151
|
+
### 载体缺失时
|
|
152
|
+
|
|
153
|
+
分两种处理:`自动` 的语义就是"哪个都行",找不到 Chrome/Edge 时**记一条警告并继续用内置**;**明确选了某个浏览器**而它没装时则不再只写日志 —— 插件改挂一个只会解释原因的载体,**每次命令都向调用方报出找不到的是哪个浏览器、查过哪些名字与位置,以及三条出路**(装上它 / 用 `DSH_BROWSER_CHROME_PATH`、`DSH_BROWSER_EDGE_PATH` 指定路径 / 把载体改回 `内置` 或 `自动`),不会让人对着一条关于 Electron 的错误去猜真正的原因。
|
|
154
|
+
|
|
155
|
+
### 载体切换是实时的
|
|
156
|
+
|
|
157
|
+
`browser.channel` 在设置里改动后**当场生效**,不需要重启 DSH:设置路由在写入改变 channel 时重跑一次载体选择,换掉旧载体并释放它。响应里带 `carrierRechosen: true` 与"只存了"区分开。
|
|
158
|
+
|
|
159
|
+
**代价明说**:换载体意味着**关掉旧载体上开着的页面** —— 换浏览器本来就是这个意思。如果你改设置时正好开着东西,那些页面会消失;这是刻意的(比留一个看不见的载体继续活着强,否则会有两个浏览器实例、两套登录态,而用户只知道自己改了一个设置)。
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# 开发
|
|
2
|
+
|
|
3
|
+
## 构建与测试
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
npm run build # 类型检查 + 构建到 lib/
|
|
7
|
+
npm test # tsc -p tsconfig.json + node --test "tests/*.test.mjs"
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
测试用**假 host**(`tests/` 里的 stub),**不需要 Electron、也不需要网络** —— 所以它可以在 CI 与任何开发机上跑。
|
|
11
|
+
|
|
12
|
+
## 代码结构
|
|
13
|
+
|
|
14
|
+
| 目录 | 职责 |
|
|
15
|
+
| --- | --- |
|
|
16
|
+
| `src/browser/` | `ctx.browser` seam 与全部请求/结果类型 |
|
|
17
|
+
| `src/browser-electron/` | provider 与三种载体实现 —— 桌面侧栏桥(`desktop-bridge-host.ts`)、本机浏览器(`system-browser.ts`)、自托管子进程(`host-main.ts`);传输层 `bridge-connection.ts` 与 RPC 操作词汇表 `rpc-ops.ts`;以及设置、历史、虚拟光标 |
|
|
18
|
+
| `src/tool-browser/` | 模型侧 `dsh_browser_*` 工具;元素/节点字段集 `element-fields.ts`(item schema 与投影同源,一处声明) |
|
|
19
|
+
| `src/types/` | electron 环境类型(shim,避免强制依赖 electron 类型) |
|
|
20
|
+
| `desktop-bridge/` | 装进桌面端的那条 bridge 与幂等安装脚本(`install.mjs`) |
|
|
21
|
+
| `tools/` | 运维脚本(如 `install-web-plugin.mjs`:钉版本 → 安装 → 修复 profile → 复验) |
|
|
22
|
+
| `docs/` | 本套文档 |
|
|
23
|
+
|
|
24
|
+
## 复验宿主兼容性
|
|
25
|
+
|
|
26
|
+
DSH 升级后想确认"有没有把我们弄坏",不用启动浏览器:
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
node scripts/verify-host-compat.mjs
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
它用某个 profile 里**真实安装**的 `@deepseek-ai/*` 包组装一个宿主,加载本插件的编译产物、经宿主自己的 `defineTool` 注册工具、再用一个假 provider 真调两个工具(`dsh_browser_session` / `dsh_browser_a11y`)。默认读 `$DSH_HOME/profiles` 下的 profile,可用 `DSH_PROFILE` / `DSH_HOME` 指定。
|
|
33
|
+
|
|
34
|
+
## 关于 peer 范围的 semver 语义
|
|
35
|
+
|
|
36
|
+
插件声明 `>=0.1.1-rc.2 <0.3.0`。这个范围在 **semver 默认语义**下**不匹配**任何带预发布标签的版本(如 `0.2.1-alpha.1`)—— 这是规范行为:预发布只被"同一 major.minor.patch 元组"的比较器允许。
|
|
37
|
+
|
|
38
|
+
DSH 判定插件兼容性时用的是 `includePrerelease: true`(`packages/boot/app-boot/src/plugin-compatibility.ts`),因此这些预发布宿主**实际都在范围内**。若某个外部工具用默认语义得出"不兼容",那是它的语义与 DSH 不同,不是这里的声明写错了。
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# 已知限制
|
|
2
|
+
|
|
3
|
+
这些是本插件**有意接受**或**受宿主约束**的边界。它们不是待办事项:写在这里是因为"知道为什么"能省掉一次排查。
|
|
4
|
+
|
|
5
|
+
## 截图
|
|
6
|
+
|
|
7
|
+
- JPEG 在**自托管**与**本机 Chrome / Edge** 上可用;**桌面端侧栏**走外壳的 `webContents.debugger`,其 Electron 的 CDP JPEG 编码会挂起,因此该载体下请求 JPEG 会返回 PNG。
|
|
8
|
+
- 降采样(`maxWidth`/`maxHeight`)**三种载体都支持**(经 CDP `clip.scale`,或自托管走原生 `capturePage`)。
|
|
9
|
+
- 自托管截图优先走 Electron 原生 `capturePage`(CDP `captureScreenshot` 在多视图下会挂起);截图前自动把目标标签置顶。
|
|
10
|
+
- `fullPage` 在部分主机的**软件合成**下不稳定 —— 请求 `fullPage` 时**原生 `capturePage` 路径被整体跳过**(它没有捕获滚动区以外内容的能力),因此**三种载体都走 CDP 的 `captureBeyondViewport`**。也就是说 `fullPage` 的不稳定性与载体无关,任何载体都可能碰到;只有视口截图才优先走原生路径。
|
|
11
|
+
|
|
12
|
+
## 验证与登录
|
|
13
|
+
|
|
14
|
+
- 人机验证(CAPTCHA)无法自动解决:快照会标注检测到的挑战,此时应请用户在共享窗口中人工完成,而不是反复重试。
|
|
15
|
+
- `dsh_browser_auth` 的 cookie 往返**不保留** `hostOnly`/`sameSite` 字段(host-only cookie 恢复后变成 domain cookie)。
|
|
16
|
+
- `flush` **只导出当前页所属站点的 cookie**,不是整台机器的共享 cookie 罐 —— 范围按本会话所在页面的 URL 收敛;页面没有可用 URL 时**直接拒绝**,而不是扩大范围去读全部域(自托管侧栏载体报"无法确定 cookie 范围",CDP 载体报 `BROWSER_AUTH_SCOPE_UNKNOWN`)。
|
|
17
|
+
- `dsh_browser_restrict` 的白名单**也管着 `dsh_browser_auth`**,因为 `restore` 是向任意域写 cookie 的动作,不是观察。
|
|
18
|
+
- 自托管浏览器的 cookie 在磁盘上**以明文存储**(Electron 默认行为);需要加密落盘的部署应在宿主层接入系统钥匙串 / DPAPI。
|
|
19
|
+
|
|
20
|
+
## 权限与门控
|
|
21
|
+
|
|
22
|
+
- `dsh_browser_restrict` 是防误操作的**软护栏**,不是安全边界:模型可以自行解除白名单。要一个**模型拿工具改不掉**的限制,用设置页「Agent 能做什么」的三个动作开关(它们写在设置文档里,`dsh_browser_*` 工具没有一个会写它),或部署级的 `tool-browser` 配置 `allowedActions`(见 `cordis.patch.yml` 的注释)。
|
|
23
|
+
- **这条边界要说准**:设置走的是本机 HTTP 端点,写入要求带同源 `Origin`(裸 HTTP 客户端因此写不进去),但**同源页面仍然可以写** —— 这是"插件暴露了一个 HTTP 端点"的固有代价,它**不是凭据系统**,面板上的说明也是这么写的。
|
|
24
|
+
- 设置文档**存在但读不懂**(JSON 损坏、写到一半、权限读不到)时,四个**门控**开关(`credentials.allowRead` 与三个动作开关)一律按**关**处理,其余设置取默认值 ——"读不透操作者的意图"不等于"操作者什么都允许"。文件**不存在**(首次运行)仍按默认值。单个门控字段类型不对(`"false"` 字符串、`null`、数字)同样按**关**;门控所在的**小节**整个写成非对象(如 `actions: false`)也按**关**,而不是把里面的每个开关当成"缺席"、各取放行的默认值。
|
|
25
|
+
|
|
26
|
+
## 下载与保存路径
|
|
27
|
+
|
|
28
|
+
- `dsh_browser_download` 在页面上下文内 `fetch`(带登录态),受同源/CORS 约束;仅允许 HTTP(S) 目标。
|
|
29
|
+
- `savePath` 必须为**绝对路径**且位于 `downloadDir` 内(默认系统下载目录,自动识别 `Downloads`/`下载`/`下載` 与 `XDG_DOWNLOAD_DIR`,可用 `downloadDir` 覆盖),**不覆盖已存在文件**;`dsh_browser_screenshot` 的 `savePath` 走同一准入门。
|
|
30
|
+
- 单文件上限 **256MB**(流式限流,按 Content-Length 提前拒绝),文件由浏览器子进程直接落盘(临时文件 + 原子改名)。
|
|
31
|
+
- 准入还会解析路径的**真实归属**:`downloadDir` 里的符号链接无法把写入带出目录,悬空链接同样被拒(它读作"名字已被占用",而不是"路径空闲")。
|
|
32
|
+
|
|
33
|
+
## 载体行为
|
|
34
|
+
|
|
35
|
+
- **侧栏载体不上报"用户操作"事件**:人在那个页面里点击是外壳自己的事件,而 bridge 没有用于回报它的操作。因此依赖该事件的功能(如自定义的接管提示)在桌面端侧栏下不会触发;换到自托管载体则可以。
|
|
36
|
+
- **本机 Chrome / Edge 载体使用插件自己的 profile**:插件用它自己的用户数据目录启动浏览器(`$DSH_HOME/dsh-builtin-browser-host/<chrome|edge>-profile`),所以你日常浏览器里的书签、扩展与登录态**不会自动继承** —— 这是刻意的,避免插件操作与你的个人会话混在一起;登录态是否跨重启保留由 `cookies.persist` 决定。
|
|
37
|
+
- 页面弹窗(`window.open` / `target=_blank`)不再覆盖当前视图:HTTP(S) 弹窗会在同一会话窗口**新开一个标签页**并计入历史,原页面与 opener 上下文保留;非 HTTP(S) 弹窗(空 URL 弹窗承接、`mailto:`、自定义协议)仍**放行原生窗口**,交给系统处理 —— 这类弹窗不纳入会话模型。
|
|
38
|
+
- 自托管浏览器子进程崩溃(或宿主 DSH 重启)后会自动重启;崩溃前已打开的会话在**下一次调用时自动重建** —— 仅页面状态丢失,无需手动 `dsh_browser_reset_session`。`dsh_browser_reset_session` 仍可用于主动重置。
|
|
39
|
+
- 新视图创建前会先有界加载 `about:blank`(3 秒上限,保证视图一存在就有可响应的渲染进程),宿主侧命令另有 20s 有界超时。
|
|
40
|
+
- 子进程 stderr 与退出码/信号落到 `$DSH_HOME/logs/dsh-builtin-browser-host.log`;写之前若该文件已超过 2 MiB,则**丢弃旧内容、只写入一行带时间戳的轮转记录**(形如 `<ISO 时间戳> log rotated: previous content exceeded 2097152 bytes and was discarded`)—— 旧内容就此消失不留副本,但那一行证明"曾发生过轮转",纯 `dsh web` 自托管可据此自助排查崩溃循环。
|
|
41
|
+
|
|
42
|
+
## 不提供的东西
|
|
43
|
+
|
|
44
|
+
- **无痕模式(`privateMode`)未实现**:它需要 Electron 的 session 分区能力,属于宿主层,本插件不承诺。
|
|
45
|
+
- **本插件不提供任何浏览器界面**:地址栏、标签条、侧栏面板都不是插件画的。桌面端的浏览器界面是**外壳自带的官方侧栏**,我们只是借它的页面来驱动;自托管载体下画窗口的是插件拉起的那个 Electron 子进程,那是载体本身而非插件 UI。别把"侧栏"或"浏览器列"当成插件能力。
|
|
46
|
+
|
|
47
|
+
## Electron 运行时
|
|
48
|
+
|
|
49
|
+
- Electron 随插件安装;但 **Electron 44+ 不再随安装下载二进制**(约 100MB,需网络)—— 插件探测是**纯文件系统**、不触发其懒下载,二进制缺失时首次使用会报错并提示先 `npx install-electron`;也可用 `ELECTRON_PATH` 预装指定的二进制。
|
package/docs/tool-reference.md
CHANGED
|
@@ -1,96 +1,96 @@
|
|
|
1
1
|
# 工具参考
|
|
2
2
|
|
|
3
|
-
全部 34 个 `
|
|
3
|
+
全部 34 个 `dsh_browser_*` 工具。守卫列:✅ 表示该动作受 `dsh_browser_restrict` 白名单约束;**「– 豁免」表示无论白名单怎么写都不拦截**(豁免集合为 `READ_ONLY_TOOLS`,`src/tool-browser/index.ts` 导出:**只观察的工具** —— `snapshot`/`a11y`/`content`/`scrape`/`screenshot`/`get_value`/`wait`/`challenge`/`list_tabs`/`session`/`history`/`visited` —— 加上 **解除限制 / 从坏状态恢复的工具** `restrict`/`reset_session`/`reset`;`restrict` 在其中,否则限制到空的任务再也解不开);**「–」表示该工具只观察、不做动作**,故没有可拦的东西。两者之外的工具都受白名单约束 —— 例如 `dsh_browser_close_tab`、`dsh_browser_auth` 都不属于豁免集合(`dsh_browser_auth` 的 `restore` 会向任意域写 cookie,是动作,不是观察)。
|
|
4
4
|
|
|
5
5
|
## 页面与导航
|
|
6
6
|
|
|
7
7
|
| 工具 | 参数 | 输出 | 守卫 | 说明 |
|
|
8
8
|
| --- | --- | --- | --- | --- |
|
|
9
|
-
| `
|
|
10
|
-
| `
|
|
11
|
-
| `
|
|
12
|
-
| `
|
|
13
|
-
| `
|
|
14
|
-
| `
|
|
9
|
+
| `dsh_browser_open` | `url`(必填), `newTab?` | 快照(url/title/elements/truncated/challenge) | ✅ | 打开 URL,返回带编号元素的快照;`newTab: true` 在新标签打开 |
|
|
10
|
+
| `dsh_browser_wait` | `timeoutMs?`, `url?`, `selector?` | `{ ready, reason }` | – | 等待页面加载完成(可选期望 URL / CSS 选择器);未就绪不抛错,返回原因 |
|
|
11
|
+
| `dsh_browser_snapshot` | – | 快照 | – | 交互元素(输入框/按钮/链接)编号清单,供定位与点击;穿透同源 iframe 与 Shadow DOM,iframe 内元素标注 `frame`。元素有 `id` 或 `name` 时,每行末尾附**可直接引用**的 `{#id}` / `{[name=x]}`(推导不出来时该段不出现),可原样作为 `dsh_browser_click`/`dsh_browser_type` 的 `target {by:"css", value:"…"}` |
|
|
12
|
+
| `dsh_browser_a11y` | `includeHidden?`, `maxNodes?`(10-5000,**默认 150**), `coords?` | `{ url, title?, count, nodes[], truncated }` | – | 无障碍树:每个交互节点的 `role`/`name`/`value`/`states`/`depth`/`tag`/`selector`,优先 Chrome `computedRole`/`computedName`,穿透同源 iframe 与 Shadow DOM。**默认不返回坐标**(需要像素落点时传 `coords: true`;要按文本驱动目标,用节点的 `name` 配 `dsh_browser_click` 的 `target {by:"text"}`)。`states` **省略字面量 `enabled`(`unchecked`、`collapsed` 等仍会打印)** —— 没有 `states=` 就是 enabled;缩进为一层一个空格 |
|
|
13
|
+
| `dsh_browser_content` | `format`(html/markdown/txt/json,必填), `selector?`, `maxChars?`, `timeoutMs?` | `{ content, truncated }` | – | 抓取页面内容;`selector` 限定区域。**字符上限按格式**(默认):`html`/`json` **50 000**,`txt`/`markdown` **20 000**,单次调用用 `maxChars` 覆盖;被截断时结果末尾提示如何收窄。`json` 返回 `{"html": "<元素 outerHTML>", "tag": "<标签名>"}` —— 不是空对象(DOM 节点自身没有可枚举属性,`JSON.stringify` 一个元素只会得到 `{}`) |
|
|
14
|
+
| `dsh_browser_challenge` | – | `{ blocked, kind?, reason?, hint? }` | – | 检测人机验证(CAPTCHA/Cloudflare/reCAPTCHA/hCaptcha/Turnstile);阻塞时请用户处理 |
|
|
15
15
|
|
|
16
16
|
## 页面操作
|
|
17
17
|
|
|
18
18
|
| 工具 | 参数 | 输出 | 守卫 | 说明 |
|
|
19
19
|
| --- | --- | --- | --- | --- |
|
|
20
|
-
| `
|
|
21
|
-
| `
|
|
22
|
-
| `
|
|
23
|
-
| `
|
|
24
|
-
| `
|
|
25
|
-
| `
|
|
26
|
-
| `
|
|
27
|
-
| `
|
|
28
|
-
| `
|
|
29
|
-
| `
|
|
30
|
-
| `
|
|
31
|
-
| `
|
|
32
|
-
| `
|
|
33
|
-
| `
|
|
34
|
-
| `
|
|
20
|
+
| `dsh_browser_execute` | `script`(必填), `args?` | `{ ok, value? / exception? }` | ✅ | 在页面执行 JS;脚本以 `return` 开头或作为表达式;`args` 以 `arguments[0..n]` 传入。**返回值上限 50 000 字符**,超出时结果里写明"截断于 50 000 / 实际 N 字符"(要完整内容请让表达式返回更小的值)。受设置「Agent 能做什么 → 允许在页面里执行脚本」门控,关闭后一律被拒(`BROWSER_EXECUTE_DISABLED`),且没有任何工具能把它打开 |
|
|
21
|
+
| `dsh_browser_click` | `target?`(css/text/xpath), `x?`, `y?`(target 与坐标二选一) | `{ clicked }` | ✅ | 语义目标点击:按 `target` 定位、滚动到视口中央再点中心;或视口坐标点击(配合截图做视觉定位) |
|
|
22
|
+
| `dsh_browser_type` | `text`(必填), `target?` | `{ typed }` | ✅ | 输入文本;传 `target` 先聚焦该元素(CDP `Input.insertText`) |
|
|
23
|
+
| `dsh_browser_key` | `key`(必填,枚举) | `{ pressed }` | ✅ | 按命名按键:Enter/Tab/Escape/Backspace/Delete/方向键/Home/End/PageUp/PageDown/Space |
|
|
24
|
+
| `dsh_browser_scroll` | `deltaX?`, `deltaY?`, `selector?`, `toTop?`, `toBottom?` | `{ scrolled }` | ✅ | 滚动页面:像素增量 / 选择器定位 / 顶部底部 |
|
|
25
|
+
| `dsh_browser_back` | – | `{ back }` | ✅ | 页面历史后退一步(无前项时为空操作) |
|
|
26
|
+
| `dsh_browser_forward` | – | `{ forward }` | ✅ | 页面历史前进一步(无后项时为空操作) |
|
|
27
|
+
| `dsh_browser_refresh` | – | `{ refreshed }` | ✅ | 刷新当前页(等价浏览器刷新按钮) |
|
|
28
|
+
| `dsh_browser_fill` | `fields`(必填,数组), `submit?` | `{ fields[], submitted }` | ✅ | 批量填表;字段按 `selector`/`name`/`label`/`placeholder` 匹配,值支持字符串/数字/布尔;单个字段失败不影响其余;`submit: true` 提交表单 |
|
|
29
|
+
| `dsh_browser_set_value` | `target`(必填), `value`(必填) | `{ method, value }` | ✅ | 单个控件设值(按 css/text/xpath 定位);原生 setter + input/change,React 受控输入可用;select 按值/文本 |
|
|
30
|
+
| `dsh_browser_check` | `target`(必填), `checked?` | `{ checked }` | ✅ | 勾选/取消勾选 checkbox 或 radio(按 target 定位) |
|
|
31
|
+
| `dsh_browser_select` | `target`(必填), `optionValue?`/`optionText?`/`optionIndex?`(三选一) | `{ value, text }` | ✅ | 选中 `<select>` 的某个选项(按 target 定位) |
|
|
32
|
+
| `dsh_browser_clear` | `target`(必填) | `{ cleared }` | ✅ | 清空输入/文本域/contenteditable,或取消勾选 checkbox/radio |
|
|
33
|
+
| `dsh_browser_get_value` | `target`(必填) | `{ value?, checked?, selectedText? }` | – 豁免 | 读取元素当前值,用于操作后验证 |
|
|
34
|
+
| `dsh_browser_scrape` | `item`(必填), `fields`(必填,映射), `timeoutMs?` | `{ count, items[] }` | – 豁免 | 结构化提取:容器选择器 + 字段映射(如 `{"title": "h3", "url": "a@href"}`);静态 CSS 查询、不执行任意代码、CSP 安全;等待 item 出现(默认 5s)。**结果是最紧凑的 JSON**(不缩进),条目数在最前面 |
|
|
35
35
|
|
|
36
36
|
## 标签与会话
|
|
37
37
|
|
|
38
38
|
| 工具 | 参数 | 输出 | 守卫 | 说明 |
|
|
39
39
|
| --- | --- | --- | --- | --- |
|
|
40
|
-
| `
|
|
41
|
-
| `
|
|
42
|
-
| `
|
|
43
|
-
| `
|
|
44
|
-
| `
|
|
45
|
-
| `
|
|
40
|
+
| `dsh_browser_list_tabs` | – | `{ tabs: [{ id, url, active }] }` | – | 当前会话的标签列表(输出 schema 只有 `tabs`,**没有 `session` 字段**;会话标识请用 `dsh_browser_session`) |
|
|
41
|
+
| `dsh_browser_switch_tab` | `tabId`(必填) | `{ switched }` | ✅ | 按 id 切换标签;自托管下同步切换可见视图 |
|
|
42
|
+
| `dsh_browser_close_tab` | `tabId`(必填) | `{ closed }` | ✅ | 关闭标签;关闭活动标签后激活下一个。**不属于只读豁免集合,受白名单约束**;`tabId` 只在本会话内查找(接受 `tab:<uuid>` 或裸 uuid),陈旧 id 不会关掉别的任务的标签页 |
|
|
43
|
+
| `dsh_browser_reset` | – | `{ reset }` | – 豁免 | 关闭本任务所有标签,回到一个空白标签(列入 `READ_ONLY_TOOLS`:限制到空的任务也必须能复位,否则白名单成了陷阱) |
|
|
44
|
+
| `dsh_browser_session` | – | `{ session, tabs[] }` | – | 查看本任务的浏览器会话与标签 |
|
|
45
|
+
| `dsh_browser_reset_session` | – | `{ reset }` | – 豁免 | 关闭并重建本任务的浏览器会话(崩溃/卡死后恢复;同上,永远可用) |
|
|
46
46
|
|
|
47
47
|
## 历史与下载
|
|
48
48
|
|
|
49
49
|
| 工具 | 参数 | 输出 | 守卫 | 说明 |
|
|
50
50
|
| --- | --- | --- | --- | --- |
|
|
51
|
-
| `
|
|
52
|
-
| `
|
|
53
|
-
| `
|
|
54
|
-
| `
|
|
51
|
+
| `dsh_browser_history` | `verbose?` | `{ entries[] }` | – | 操作日志(最新在后),含 seq/action/ok/params/result/error。**默认只渲染最近 20 条,且不显示每条的参数**(参数就是你刚发出去的),首行先给总条数,免得把尾部当成全部;要看参数时传 `verbose: true`。返回的完整 `entries[]` 不受条数限制 |
|
|
52
|
+
| `dsh_browser_visited` | `limit`(默认 30,上限 200), `domain`(主机名包含匹配), `query`(URL 或标题包含匹配), `session`(按来源会话标签过滤) | `{ count, entries[] }` | – | **持久化浏览历史**(访问过的页面,最新在前):与 cookie 同址落盘(`$DSH_HOME/dsh-builtin-browser-host/history.jsonl`),关闭浏览器与重启 DSH 后仍在;上限默认 5000 条或 90 天(设置文档里的 `history.maxEntries` / `history.maxAgeDays` 可改)。重开某条用 `dsh_browser_open`;与 `dsh_browser_history`(会话内操作日志,随会话消失)是两件事;可在设置里关闭记录。渲染时**时间戳精确到分钟**,会话标记**只在切换时打印一次并取 8 位前缀** |
|
|
53
|
+
| `dsh_browser_replay` | `seq`(必填) | `{ replayed }` | ✅ | 按序号回放某一步(navigate/execute/click/type/scroll/key) |
|
|
54
|
+
| `dsh_browser_download` | `url`(必填), `savePath`(必填) | `{ path }` | ✅ | 带会话 cookie 下载到本地(仅 http(s);`savePath` 必须为绝对路径且位于 `downloadDir` 内——默认系统下载目录,自动识别 `Downloads`/`下载`/`下載` 与 `XDG_DOWNLOAD_DIR`;不覆盖已有文件;上限 256MB,受 CORS 约束;由子进程直接落盘)。受设置「允许下载文件到磁盘」门控(`BROWSER_DOWNLOAD_DISABLED`) |
|
|
55
55
|
|
|
56
56
|
## 登录态与安全
|
|
57
57
|
|
|
58
58
|
| 工具 | 参数 | 输出 | 守卫 | 说明 |
|
|
59
59
|
| --- | --- | --- | --- | --- |
|
|
60
|
-
| `
|
|
61
|
-
| `
|
|
60
|
+
| `dsh_browser_auth` | `action`(flush/restore,必填), `cookies?` | `{ cookies[]? / restored? }` | ✅ | 导出/恢复 cookie —— **三种载体都可用**:自托管走原生会话,桌面侧栏与本机 Chrome/Edge 走 CDP 的 `Network.getCookies` / `Storage.setCookies`;flush 返回 cookie 列表,restore 带列表写回。**flush 只导出当前页所属站点的 cookie**(不是整台机器的共享 cookie 罐),范围按本会话所在页面的 URL 收敛;**范围未知时直接拒绝而不是放宽**(自托管侧栏载体报"cookie 范围未知",CDP 载体报 `BROWSER_AUTH_SCOPE_UNKNOWN`)。`restored` 反映真的提交了多少(无法构成有效 URL 的 cookie 会被丢弃并报出条数)。**受白名单约束**:`restore` 会向任意域写 cookie,是动作而非观察,因此该工具**不在只读豁免集合**里。**两个门控是分开的**:`flush`(读)受设置「凭据 / 允许读取 cookies」管,关闭时抛 `BROWSER_AUTH_DISABLED`;`restore`(写)受「允许写入登录状态」管,关闭时抛 `BROWSER_AUTH_WRITE_DISABLED` —— 关掉读不会连带拒绝写,反之亦然 |
|
|
61
|
+
| `dsh_browser_restrict` | `allowed?` | `{ restrictedTo[] }` | – | 设置动作白名单;空列表解除;**只校验名字是否以 `dsh_browser_` 开头**(写错的前缀会报错,`dsh_browser_typo` 这种拼错但前缀合法的名字会被接受、等同于拦掉该名字,不额外报错);守卫按名字匹配,因此白名单里列什么名字就只放行什么名字。**软护栏**——模型可自行解除,非安全边界 |
|
|
62
62
|
|
|
63
63
|
## 截图
|
|
64
64
|
|
|
65
65
|
| 工具 | 参数 | 输出 | 守卫 | 说明 |
|
|
66
66
|
| --- | --- | --- | --- | --- |
|
|
67
|
-
| `
|
|
67
|
+
| `dsh_browser_screenshot` | `fullPage?`, `savePath?`, `format?`(png/jpeg), `quality?`, `maxWidth?`, `maxHeight?` | `{ dataUrl, path?, width?, height? }` | – | 截图;PNG 默认,**JPEG 在自托管与本机 Chrome/Edge 上都可用**(自托管在原生 `capturePage` 路径编码,本机浏览器经 CDP 声明 `supportsCdpJpeg`;**桌面侧栏**的 Electron CDP JPEG 编码器会挂起,该载体下 JPEG 请求返回 PNG);`maxWidth`/`maxHeight` 等比缩放**三种载体都支持**;`fullPage` 会跳过原生 `capturePage`、三种载体统一走 CDP `captureBeyondViewport`;`savePath` 落盘供视觉模型读取(与 `dsh_browser_download` 同一准入门:必须位于 `downloadDir` 内且不覆盖已有文件) |
|
|
68
68
|
|
|
69
69
|
## 常用组合
|
|
70
70
|
|
|
71
71
|
**调研一个网站**
|
|
72
72
|
```
|
|
73
|
-
|
|
73
|
+
dsh_browser_open https://site → dsh_browser_wait(url=...) → dsh_browser_content format=markdown → dsh_browser_snapshot → 逐页浏览
|
|
74
74
|
```
|
|
75
75
|
|
|
76
76
|
**登录并下载文件**
|
|
77
77
|
```
|
|
78
|
-
|
|
79
|
-
等待跳转 →
|
|
78
|
+
dsh_browser_open https://site/login → dsh_browser_fill(用户名/密码) submit=true →
|
|
79
|
+
等待跳转 → dsh_browser_download(url, savePath)
|
|
80
80
|
```
|
|
81
81
|
|
|
82
82
|
**表单填写(React/Vue 页面)**
|
|
83
83
|
```
|
|
84
|
-
|
|
84
|
+
dsh_browser_snapshot → dsh_browser_fill(fields=[{name:'email',value:'a@b.c'},{label:'密码',value:'***'}], submit=true)
|
|
85
85
|
```
|
|
86
86
|
|
|
87
87
|
**误操作恢复**
|
|
88
88
|
```
|
|
89
|
-
|
|
89
|
+
dsh_browser_reset_session → dsh_browser_open(重新开始)
|
|
90
90
|
```
|
|
91
91
|
|
|
92
92
|
**遇到验证码**
|
|
93
93
|
```
|
|
94
|
-
|
|
95
|
-
|
|
94
|
+
dsh_browser_challenge → 阻塞 → 提示用户:"页面出现人机验证,请在共享浏览器窗口完成,完成后告诉我" →
|
|
95
|
+
dsh_browser_snapshot 复查
|
|
96
96
|
```
|