dsh-builtin-browser 0.4.1 → 0.4.5
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 +1699 -0
- package/README.en.md +114 -33
- package/README.md +108 -32
- package/client.js +478 -29
- package/cordis.patch.yml +21 -0
- package/desktop-bridge/install.mjs +66 -6
- package/desktop-bridge/plugin-browser-bridge.js +426 -167
- package/docs/architecture.md +14 -4
- package/docs/tool-reference.md +13 -13
- package/docs/user-guide.md +58 -3
- package/docs/why-browser.md +1 -1
- package/lib/browser/types.d.ts +34 -3
- package/lib/browser-electron/bridge-connection.d.ts +49 -26
- package/lib/browser-electron/bridge-connection.js +185 -22
- package/lib/browser-electron/desktop-bridge-host.d.ts +69 -36
- package/lib/browser-electron/desktop-bridge-host.js +151 -86
- package/lib/browser-electron/entry.d.ts +1 -1
- package/lib/browser-electron/entry.js +133 -40
- package/lib/browser-electron/ephemeral-profile.d.ts +37 -0
- package/lib/browser-electron/ephemeral-profile.js +146 -0
- package/lib/browser-electron/history-store.d.ts +36 -2
- package/lib/browser-electron/history-store.js +76 -7
- package/lib/browser-electron/host-main.js +86 -28
- package/lib/browser-electron/provider.d.ts +106 -3
- package/lib/browser-electron/provider.js +407 -60
- package/lib/browser-electron/remote-host.d.ts +52 -3
- package/lib/browser-electron/remote-host.js +259 -25
- package/lib/browser-electron/rpc-ops.d.ts +18 -0
- package/lib/browser-electron/rpc-ops.js +36 -0
- package/lib/browser-electron/settings-route.d.ts +28 -0
- package/lib/browser-electron/settings-route.js +68 -0
- package/lib/browser-electron/settings-store.d.ts +40 -0
- package/lib/browser-electron/settings-store.js +160 -23
- package/lib/browser-electron/system-browser.d.ts +102 -12
- package/lib/browser-electron/system-browser.js +411 -51
- package/lib/index.js +0 -1
- package/lib/tool-browser/element-fields.d.ts +145 -0
- package/lib/tool-browser/element-fields.js +152 -0
- package/lib/tool-browser/index.js +149 -74
- package/package.json +10 -4
- package/scripts/verify-host-compat.mjs +195 -0
- package/src/browser/types.ts +34 -3
- package/src/browser-electron/bridge-connection.ts +179 -22
- package/src/browser-electron/desktop-bridge-host.ts +146 -84
- package/src/browser-electron/entry.ts +132 -39
- package/src/browser-electron/ephemeral-profile.ts +152 -0
- package/src/browser-electron/history-store.ts +81 -9
- package/src/browser-electron/host-main.ts +115 -36
- package/src/browser-electron/provider.ts +492 -69
- package/src/browser-electron/remote-host.ts +276 -38
- package/src/browser-electron/rpc-ops.ts +41 -0
- package/src/browser-electron/settings-route.ts +87 -0
- package/src/browser-electron/settings-store.ts +173 -24
- package/src/browser-electron/system-browser.ts +408 -39
- package/src/index.ts +3 -2
- package/src/tool-browser/element-fields.ts +167 -0
- package/src/tool-browser/index.ts +155 -76
package/docs/architecture.md
CHANGED
|
@@ -43,8 +43,18 @@ agent (browser_* 工具)
|
|
|
43
43
|
- `browser_reset_session` 关闭本任务会话并遗忘映射(即使 close 抛错也清除,下次调用重建);
|
|
44
44
|
- 会话生命周期绑定 agent 作用域 ctx:agent(DSH 会话)销毁时自动关闭对应浏览器会话,任务结束后不再泄漏窗口/视图;
|
|
45
45
|
- 会话/开启动态/白名单都是**每插件实例(每 context)作用域**的,多 context 互不共享、互不污染;
|
|
46
|
-
- `browser_restrict`
|
|
47
|
-
- 输出 schema 与返回值严格一致(DSH 运行时会校验,`additionalProperties: false` 下多一个字段都会报错)。
|
|
46
|
+
- `browser_restrict` 维护本任务白名单,守卫所有非只读工具;
|
|
47
|
+
- 输出 schema 与返回值严格一致(DSH 运行时会校验,`additionalProperties: false` 下多一个字段都会报错):元素与节点的字段集在 `element-fields.ts` 里**只声明一次**,item schema 与投影都由它导出 —— 手写白名单漏一个字段会**静默丢数据**、多一个会让**整个结果被拒**,同源之后这两个方向都不可能单独出错(`browser_a11y` 的 `selector` 与 `browser_screenshot` 的 `width`/`height` 都是这么丢的)。
|
|
48
|
+
|
|
49
|
+
### 输出尺寸(工具层)
|
|
50
|
+
|
|
51
|
+
工具输出花的是调用方的上下文,所以工具层对几处返回设了明确上限,并在截断时**说明截断**:
|
|
52
|
+
|
|
53
|
+
- `browser_a11y` 默认 150 个节点(上限 5000)、一层一个空格缩进、`states` 只省略字面量 `enabled`(`unchecked`、`collapsed` 等仍会打印);
|
|
54
|
+
- `browser_snapshot` 与 `browser_a11y` 在元素有 `id`/`name` 时给出 `{#id}` / `{[name=x]}` 引用选择器(页面脚本一直算,工具层此前丢掉;输出 schema 必须声明该字段,否则 `additionalProperties: false` 会拒绝整个结果);
|
|
55
|
+
- `browser_content` 的上限按格式:html/json 50 000,txt/markdown 20 000,`maxChars` 可覆盖;
|
|
56
|
+
- `browser_execute` 的返回值上限 50 000 字符;
|
|
57
|
+
- `browser_scrape` 输出紧凑 JSON;`browser_history` 默认只渲染最近 20 条且不回显参数(`verbose: true` 才带);`browser_visited` 的时间戳精确到分钟、会话标记只在变化时打印一次。
|
|
48
58
|
|
|
49
59
|
## 自托管实现(纯 `dsh web`)
|
|
50
60
|
|
|
@@ -58,10 +68,10 @@ RemoteElectronViewHost ──TCP JSON-RPC──▶ host-main.js
|
|
|
58
68
|
DeferredRemoteView(物化缓存) webContents.debugger(CDP)
|
|
59
69
|
```
|
|
60
70
|
|
|
61
|
-
- **协议**:本机 loopback TCP,每行一个 JSON(`{ id, op, ... }` ↔ `{ id, ok, result|err }`);
|
|
71
|
+
- **协议**:本机 loopback TCP,每行一个 JSON(`{ id, op, ... }` ↔ `{ id, ok, result|err }`);op 名清单只写在 `rpc-ops.ts` 一处,并由一个读**子端 switch 与父端调用两侧源码**的测试断言一致 —— 单侧改名因此会当场失败,而不是编译通过、运行时报 "unknown op";
|
|
62
72
|
- **认证**:每次 spawn 生成随机 token,经 **stdin 首行 + `DSH_BROWSER_RPC_TOKEN` 环境变量双通道**传给子进程(绝不进 argv,避免进程列表泄露;Windows GUI 进程收不到 piped stdin,环境变量兜底),子进程首条消息必须回传该 token(`hello`);服务端只接受**一个**连接,其余连接直接断开——本机其他进程无法伪冒子进程或注入回复;
|
|
63
73
|
- **Electron 定位**:① `ELECTRON_PATH`(显式覆盖,优先于一切自动发现)→ ② 随插件安装的 electron 包(纯文件系统探测,含 pnpm store 布局,不触发 44+ 懒下载)→ ③ 锚点与 pnpm store 中**版本最新**者(33.x 有合成器缺陷,建议 ≥ 40)→ ④ 当前进程是**裸** Electron(dev 模式)时复用宿主二进制(`process.execPath`)→ ⑤ 进程祖先树中的裸 Electron 二进制(Windows 用 PowerShell CIM 查询,仅在其它路径全部落空时才执行)。**打包应用不参与复用**:旁有 `resources/app.asar` 的可执行文件(如 DSH Desktop.exe)不能按脚本参数拉起——spawn 会启动应用本体并秒退(单实例锁,即 "browser host exited (code=0)"),一律跳过;
|
|
64
|
-
- **稳健性**:子进程/套接字都有 `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
|
|
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,会让串行操作队列永久停在那一条上;
|
|
65
75
|
- **视图可见性**:多标签/多会话时,`showView` 隐藏其他视图并把目标视图置顶(remove+re-add),确保用户看到的是活动标签;目标是当前可见视图时**跳过** remove/re-add(否则每次操作都闪烁);窗口标题实时显示当前可见会话的任务标识 + 页面标题/URL(`showView` 携带会话 label,子进程读 `getTitle`/`getURL`);窗口 resize 时视图 bounds 自动跟随;
|
|
66
76
|
- **孤儿防护**:父进程断开时子进程自动退出,不留僵尸窗口;
|
|
67
77
|
- **cookie 落盘**:子进程使用独立 userData 目录(`<DSH_HOME>/dsh-builtin-browser-host`),登录态跨重启保留(另有 `browser_auth` 手动导出/恢复)。
|
package/docs/tool-reference.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 工具参考
|
|
2
2
|
|
|
3
|
-
全部 34 个 `browser_*` 工具。守卫列:✅ 表示该动作受 `browser_restrict` 白名单约束;**「– 豁免」表示无论白名单怎么写都不拦截**(豁免集合为 `READ_ONLY_TOOLS`,`src/tool-browser/index.ts` 导出:**只观察的工具** —— `snapshot`/`a11y`/`content`/`scrape`/`screenshot`/`get_value`/`wait`/`challenge`/`list_tabs`/`session`/`history`/`visited
|
|
3
|
+
全部 34 个 `browser_*` 工具。守卫列:✅ 表示该动作受 `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` 在其中,否则限制到空的任务再也解不开);**「–」表示该工具只观察、不做动作**,故没有可拦的东西。两者之外的工具都受白名单约束 —— 例如 `browser_close_tab`、`browser_auth` 都不属于豁免集合(`browser_auth` 的 `restore` 会向任意域写 cookie,是动作,不是观察)。
|
|
4
4
|
|
|
5
5
|
## 页面与导航
|
|
6
6
|
|
|
@@ -8,16 +8,16 @@
|
|
|
8
8
|
| --- | --- | --- | --- | --- |
|
|
9
9
|
| `browser_open` | `url`(必填), `newTab?` | 快照(url/title/elements/truncated/challenge) | ✅ | 打开 URL,返回带编号元素的快照;`newTab: true` 在新标签打开 |
|
|
10
10
|
| `browser_wait` | `timeoutMs?`, `url?`, `selector?` | `{ ready, reason }` | – | 等待页面加载完成(可选期望 URL / CSS 选择器);未就绪不抛错,返回原因 |
|
|
11
|
-
| `browser_snapshot` | – | 快照 | – | 交互元素(输入框/按钮/链接)编号清单,供定位与点击;穿透同源 iframe 与 Shadow DOM,iframe 内元素标注 `frame` |
|
|
12
|
-
| `browser_a11y` | `includeHidden?`, `maxNodes?`(10-5000
|
|
13
|
-
| `browser_content` | `format`(html/markdown/txt/json,必填), `selector?`, `maxChars?`, `timeoutMs?` | `{ content, truncated }` | – | 抓取页面内容;`selector`
|
|
11
|
+
| `browser_snapshot` | – | 快照 | – | 交互元素(输入框/按钮/链接)编号清单,供定位与点击;穿透同源 iframe 与 Shadow DOM,iframe 内元素标注 `frame`。元素有 `id` 或 `name` 时,每行末尾附**可直接引用**的 `{#id}` / `{[name=x]}`(推导不出来时该段不出现),可原样作为 `browser_click`/`browser_type` 的 `target {by:"css", value:"…"}` |
|
|
12
|
+
| `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` 配 `browser_click` 的 `target {by:"text"}`)。`states` **省略字面量 `enabled`(`unchecked`、`collapsed` 等仍会打印)** —— 没有 `states=` 就是 enabled;缩进为一层一个空格 |
|
|
13
|
+
| `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
14
|
| `browser_challenge` | – | `{ blocked, kind?, reason?, hint? }` | – | 检测人机验证(CAPTCHA/Cloudflare/reCAPTCHA/hCaptcha/Turnstile);阻塞时请用户处理 |
|
|
15
15
|
|
|
16
16
|
## 页面操作
|
|
17
17
|
|
|
18
18
|
| 工具 | 参数 | 输出 | 守卫 | 说明 |
|
|
19
19
|
| --- | --- | --- | --- | --- |
|
|
20
|
-
| `browser_execute` | `script`(必填), `args?` | `{ ok, value? / exception? }` | ✅ | 在页面执行 JS;脚本以 `return` 开头或作为表达式;`args` 以 `arguments[0..n]`
|
|
20
|
+
| `browser_execute` | `script`(必填), `args?` | `{ ok, value? / exception? }` | ✅ | 在页面执行 JS;脚本以 `return` 开头或作为表达式;`args` 以 `arguments[0..n]` 传入。**返回值上限 50 000 字符**,超出时结果里写明"截断于 50 000 / 实际 N 字符"(要完整内容请让表达式返回更小的值)。受设置「Agent 能做什么 → 允许在页面里执行脚本」门控,关闭后一律被拒(`BROWSER_EXECUTE_DISABLED`),且没有任何工具能把它打开 |
|
|
21
21
|
| `browser_click` | `target?`(css/text/xpath), `x?`, `y?`(target 与坐标二选一) | `{ clicked }` | ✅ | 语义目标点击:按 `target` 定位、滚动到视口中央再点中心;或视口坐标点击(配合截图做视觉定位) |
|
|
22
22
|
| `browser_type` | `text`(必填), `target?` | `{ typed }` | ✅ | 输入文本;传 `target` 先聚焦该元素(CDP `Input.insertText`) |
|
|
23
23
|
| `browser_key` | `key`(必填,枚举) | `{ pressed }` | ✅ | 按命名按键:Enter/Tab/Escape/Backspace/Delete/方向键/Home/End/PageUp/PageDown/Space |
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
| `browser_select` | `target`(必填), `optionValue?`/`optionText?`/`optionIndex?`(三选一) | `{ value, text }` | ✅ | 选中 `<select>` 的某个选项(按 target 定位) |
|
|
32
32
|
| `browser_clear` | `target`(必填) | `{ cleared }` | ✅ | 清空输入/文本域/contenteditable,或取消勾选 checkbox/radio |
|
|
33
33
|
| `browser_get_value` | `target`(必填) | `{ value?, checked?, selectedText? }` | – 豁免 | 读取元素当前值,用于操作后验证 |
|
|
34
|
-
| `browser_scrape` | `item`(必填), `fields`(必填,映射), `timeoutMs?` | `{ count, items[] }` | – 豁免 | 结构化提取:容器选择器 + 字段映射(如 `{"title": "h3", "url": "a@href"}`);静态 CSS 查询、不执行任意代码、CSP 安全;等待 item 出现(默认 5s) |
|
|
34
|
+
| `browser_scrape` | `item`(必填), `fields`(必填,映射), `timeoutMs?` | `{ count, items[] }` | – 豁免 | 结构化提取:容器选择器 + 字段映射(如 `{"title": "h3", "url": "a@href"}`);静态 CSS 查询、不执行任意代码、CSP 安全;等待 item 出现(默认 5s)。**结果是最紧凑的 JSON**(不缩进),条目数在最前面 |
|
|
35
35
|
|
|
36
36
|
## 标签与会话
|
|
37
37
|
|
|
@@ -40,31 +40,31 @@
|
|
|
40
40
|
| `browser_list_tabs` | – | `{ tabs: [{ id, url, active }] }` | – | 当前会话的标签列表(输出 schema 只有 `tabs`,**没有 `session` 字段**;会话标识请用 `browser_session`) |
|
|
41
41
|
| `browser_switch_tab` | `tabId`(必填) | `{ switched }` | ✅ | 按 id 切换标签;自托管下同步切换可见视图 |
|
|
42
42
|
| `browser_close_tab` | `tabId`(必填) | `{ closed }` | ✅ | 关闭标签;关闭活动标签后激活下一个。**不属于只读豁免集合,受白名单约束**;`tabId` 只在本会话内查找(接受 `tab:<uuid>` 或裸 uuid),陈旧 id 不会关掉别的任务的标签页 |
|
|
43
|
-
| `browser_reset` | – | `{ reset }` |
|
|
43
|
+
| `browser_reset` | – | `{ reset }` | – 豁免 | 关闭本任务所有标签,回到一个空白标签(列入 `READ_ONLY_TOOLS`:限制到空的任务也必须能复位,否则白名单成了陷阱) |
|
|
44
44
|
| `browser_session` | – | `{ session, tabs[] }` | – | 查看本任务的浏览器会话与标签 |
|
|
45
|
-
| `browser_reset_session` | – | `{ reset }` |
|
|
45
|
+
| `browser_reset_session` | – | `{ reset }` | – 豁免 | 关闭并重建本任务的浏览器会话(崩溃/卡死后恢复;同上,永远可用) |
|
|
46
46
|
|
|
47
47
|
## 历史与下载
|
|
48
48
|
|
|
49
49
|
| 工具 | 参数 | 输出 | 守卫 | 说明 |
|
|
50
50
|
| --- | --- | --- | --- | --- |
|
|
51
|
-
| `browser_history` |
|
|
52
|
-
| `browser_visited` | `limit`(默认 30,上限 200), `domain`(主机名包含匹配) | `{ count, entries[] }` | – | **持久化浏览历史**(访问过的页面,最新在前):与 cookie 同址落盘(`$DSH_HOME/dsh-builtin-browser-host/history.jsonl`),关闭浏览器与重启 DSH
|
|
51
|
+
| `browser_history` | `verbose?` | `{ entries[] }` | – | 操作日志(最新在后),含 seq/action/ok/params/result/error。**默认只渲染最近 20 条,且不显示每条的参数**(参数就是你刚发出去的),首行先给总条数,免得把尾部当成全部;要看参数时传 `verbose: true`。返回的完整 `entries[]` 不受条数限制 |
|
|
52
|
+
| `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` 可改)。重开某条用 `browser_open`;与 `browser_history`(会话内操作日志,随会话消失)是两件事;可在设置里关闭记录。渲染时**时间戳精确到分钟**,会话标记**只在切换时打印一次并取 8 位前缀** |
|
|
53
53
|
| `browser_replay` | `seq`(必填) | `{ replayed }` | ✅ | 按序号回放某一步(navigate/execute/click/type/scroll/key) |
|
|
54
|
-
| `browser_download` | `url`(必填), `savePath`(必填) | `{ path }` | ✅ | 带会话 cookie 下载到本地(仅 http(s);`savePath` 必须为绝对路径且位于 `downloadDir` 内——默认系统下载目录,自动识别 `Downloads`/`下载`/`下載` 与 `XDG_DOWNLOAD_DIR`;不覆盖已有文件;上限 256MB,受 CORS 约束;由子进程直接落盘) |
|
|
54
|
+
| `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
|
-
| `browser_auth` | `action`(flush/restore,必填), `cookies?` | `{ cookies[]? / restored? }` | ✅ | 导出/恢复 cookie —— **三种载体都可用**:自托管走原生会话,桌面侧栏与本机 Chrome/Edge 走 CDP 的 `
|
|
60
|
+
| `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
61
|
| `browser_restrict` | `allowed?` | `{ restrictedTo[] }` | – | 设置动作白名单;空列表解除;**只校验名字是否以 `browser_` 开头**(写错的前缀会报错,`browser_typo` 这种拼错但前缀合法的名字会被接受、等同于拦掉该名字,不额外报错);守卫按名字匹配,因此白名单里列什么名字就只放行什么名字。**软护栏**——模型可自行解除,非安全边界 |
|
|
62
62
|
|
|
63
63
|
## 截图
|
|
64
64
|
|
|
65
65
|
| 工具 | 参数 | 输出 | 守卫 | 说明 |
|
|
66
66
|
| --- | --- | --- | --- | --- |
|
|
67
|
-
| `browser_screenshot` | `fullPage?`, `savePath?`, `format?`(png/jpeg), `quality?`, `maxWidth?`, `maxHeight?` | `{ dataUrl, path? }` | – | 截图;PNG 默认,**JPEG 在自托管与本机 Chrome/Edge 上都可用**(自托管在原生 `capturePage` 路径编码,本机浏览器经 CDP 声明 `supportsCdpJpeg`;**桌面侧栏**的 Electron CDP JPEG 编码器会挂起,该载体下 JPEG 请求返回 PNG);`maxWidth`/`maxHeight` 等比缩放**三种载体都支持**;`fullPage` 会跳过原生 `capturePage`、三种载体统一走 CDP `captureBeyondViewport`;`savePath` 落盘供视觉模型读取(与 `browser_download` 同一准入门:必须位于 `downloadDir` 内且不覆盖已有文件) |
|
|
67
|
+
| `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` 落盘供视觉模型读取(与 `browser_download` 同一准入门:必须位于 `downloadDir` 内且不覆盖已有文件) |
|
|
68
68
|
|
|
69
69
|
## 常用组合
|
|
70
70
|
|
package/docs/user-guide.md
CHANGED
|
@@ -22,7 +22,6 @@ dsh plugin --profile web add <本仓库路径>
|
|
|
22
22
|
| 行 | 子路径 | 角色 |
|
|
23
23
|
| --- | --- | --- |
|
|
24
24
|
| `dsh-builtin-browser` | 包名本身 | **惰性根行**:不注册任何东西(空 `apply()`),只用来声明包名 —— 宿主的客户端插件扫描靠精确包名读到 `dsh.client`,少了这行设置栏不会出现 |
|
|
25
|
-
| --- | --- | --- |
|
|
26
25
|
| `browser` | `dsh-builtin-browser/browser` | `ctx.browser` 能力 seam(始终挂载) |
|
|
27
26
|
| `browser-electron` | `dsh-builtin-browser/browser-electron` | Electron CDP provider |
|
|
28
27
|
| `tool-browser` | `dsh-builtin-browser/tool-browser` | `browser_*` 模型侧工具 |
|
|
@@ -36,10 +35,11 @@ dsh plugin --profile web add <本仓库路径>
|
|
|
36
35
|
| `browser-electron` | `viewHost` | 对象 | 可选 | 宿主提供的 `ElectronBrowserViewHost`(通常 `!!js ctx.get('electronViewHost')`)。**不传时插件自己选载体**:桌面端驱动官方侧栏、否则自托管;设置里显式选择的 Chrome/Edge 优先于两者 |
|
|
37
36
|
| `browser-electron` | `httpOnly` | 布尔 | `true` | 仅允许 HTTP(S) 导航;`file:`/`data:` 等拒绝 |
|
|
38
37
|
| `browser-electron` | `snapshotMaxElements` | 数字 | `60` | 快照最多收录的交互元素数 |
|
|
39
|
-
| `browser-electron` | `contentMaxChars` | 数字 | `
|
|
38
|
+
| `browser-electron` | `contentMaxChars` | 数字 | 未设置(按格式分档) | 内容抓取的字符上限。**不设**时按格式取分档默认(html 50 000、json 50 000、txt 20 000、markdown 20 000);**设了**就是操作者的值,覆盖分档默认。单次调用的 `maxChars` 优先级最高 |
|
|
40
39
|
| `browser-electron` | `downloadDir` | 字符串 | 系统下载目录(自动识别 `Downloads`/`下载`/`下載`,或 `XDG_DOWNLOAD_DIR`) | 限定 `browser_download` 与 `browser_screenshot` 保存路径必须位于该目录内且不覆盖已有文件;默认收敛到系统下载目录,可改沙箱目录 |
|
|
41
40
|
| `tool-browser` | `timeoutMs` | 数字 | `60000` | 工具协作超时(ms) |
|
|
42
41
|
| `tool-browser` | `tabTools` | 布尔 | `true` | 是否注册标签管理工具 |
|
|
42
|
+
| `tool-browser` | `allowedActions` | 字符串数组 | 不限制 | **部署级**动作白名单,对该部署里的每个任务生效(单个任务仍可用 `browser_restrict` 再收窄);`READ_ONLY_TOOLS` 与 `browser_restrict` 无论列表怎么写都不拦。`cordis.patch.yml` 默认给的是空 `config: {}`(等于不限制) |
|
|
43
43
|
|
|
44
44
|
## 快速上手(给 agent 的提示词示例)
|
|
45
45
|
|
|
@@ -55,12 +55,59 @@ dsh plugin --profile web add <本仓库路径>
|
|
|
55
55
|
|
|
56
56
|
## 操作纪律
|
|
57
57
|
|
|
58
|
+
**先确认视觉策略。** 设置里的 `vision.strategy` 决定坐标点击能不能用,**而且只决定这一件事**:
|
|
59
|
+
|
|
60
|
+
- **`auto`(默认)**:坐标与语义都允许,适合能读图的模型。
|
|
61
|
+
- **`nonVisual`**:**坐标点击被明确拒绝**,报 `BROWSER_NON_VISUAL_COORDINATES`,错误信息直接给出改法 —— 传语义 target,如 `target { by: "text", value: "Sign in" }`。**这是配置,不是故障,不要重试同一次坐标点击。**
|
|
62
|
+
|
|
63
|
+
**两种策略下 DOM 查询、无障碍树、文本提取与结构化抓取完全一致** —— 所以下面这些纪律对**所有模型**都成立,不是因为谁看不了图。
|
|
64
|
+
|
|
65
|
+
**定位:先用语义,再考虑坐标**
|
|
66
|
+
|
|
67
|
+
- **`browser_a11y` 是理解页面的首选**,它给出每个交互节点的**角色**(button/textbox/…)与**可访问名称**,比读 DOM 更接近"这页面上有什么";`browser_snapshot` 给出可点击元素的**引用与坐标**。
|
|
68
|
+
- **两者都带 `selector`**:无 id 且无 name 时是空串。**有 `selector` 就优先用它**,它比坐标稳定 —— 页面滚动、布局变化都不会让它失效,而坐标会。
|
|
58
69
|
- **优先用 DOM 语义而非坐标**:表单提交优先 `form.requestSubmit()`;点击优先 `element.click()`;坐标点击是最后手段。
|
|
70
|
+
- **`browser_click` 的 `target`(css/text)本质上就是语义定位**,能用它就别自己算坐标 —— 它会把元素滚进视野并在中心点击,坐标抖动、像素比、iframe 偏移都由它处理。
|
|
59
71
|
- **选中正确的元素**:页面常有隐藏副本(如移动端按钮),用 `browser_execute` 过滤可见元素(`getBoundingClientRect()` 宽高 > 0、`getComputedStyle` 非 `display:none`),再取坐标。
|
|
60
72
|
- **取坐标后立即点击**:中间不要插入其他操作(填表、滚动会移动元素,旧坐标立即失效)。
|
|
61
73
|
- **点击前验证命中**:`document.elementFromPoint(x, y)` 确认该坐标确实是目标元素,再执行真实点击。
|
|
62
74
|
- **DPR 注意**:CDP 输入使用 CSS 像素;高 DPI 屏上若点击落空,用 `elementFromPoint` 校准,不要盲试坐标。
|
|
63
75
|
|
|
76
|
+
**截图与内容:先问"我要的是判断还是像素"**
|
|
77
|
+
|
|
78
|
+
- **能用文本就用文本。** `browser_snapshot` / `browser_a11y` / `browser_content` 给出同一页面的文本表示;**没有图像输入能力的模型看截图什么也得不到**,只会花钱。截图留给布局、图表、设计核对这类**必须看像素**的事。
|
|
79
|
+
- **`browser_screenshot` 带 `maxWidth`/`maxHeight` 时,结果里会返回 `width`/`height`**(实际像素尺寸)。需要控制成本时用它确认降采样生效了,而不是自己解码 data URL。
|
|
80
|
+
- **`browser_content` 的 `maxChars` 是按格式分档的**(html/json 默认 5 万,其余 2 万),单次传 `maxChars` 优先于设置里的 `contentMaxChars`。
|
|
81
|
+
|
|
82
|
+
**动作开关:三个门是操作者(人)的,不是你的**
|
|
83
|
+
|
|
84
|
+
设置页里的**执行页面脚本**、**下载文件到磁盘**、**写入登录态** 由人控制,**任何 `browser_*` 工具都改不了它们**。关掉时对应调用会明确报错,一眼能认出:
|
|
85
|
+
|
|
86
|
+
| 谁被关 | 报的错误码 | 哪些工具会遇到 |
|
|
87
|
+
| --- | --- | --- |
|
|
88
|
+
| 执行页面脚本 | `BROWSER_EXECUTE_DISABLED` | `browser_execute`、以及需要页内脚本的工具 |
|
|
89
|
+
| 下载文件到磁盘 | `BROWSER_DOWNLOAD_DISABLED` | `browser_download` |
|
|
90
|
+
| 写入登录态 | `BROWSER_AUTH_WRITE_DISABLED` | `browser_auth` 的 `restore` |
|
|
91
|
+
| 读取 cookie | `BROWSER_AUTH_DISABLED` | `browser_auth` 的 `flush` |
|
|
92
|
+
|
|
93
|
+
**看到这些码不要重试、不要绕路** —— **它是人的决定,不是可以克服的故障**,直接告诉人"需要他在设置里打开"。
|
|
94
|
+
|
|
95
|
+
另外几个容易认错来源的码:`BROWSER_DOWNLOAD_BLOCKED` / `BROWSER_SCREENSHOT_BLOCKED` 是**保存路径被沙箱拒绝**(不是开关问题,换路径而不是找开关);`BROWSER_DOWNLOAD_UNSUPPORTED` 是**该载体没有这个能力**(比如非自托管浏览器)。
|
|
96
|
+
|
|
97
|
+
`browser_restrict` **不一样** —— 那是**你自己**设的临时白名单,你有权设也有权解除;它不该被用来绕过上面三个门。
|
|
98
|
+
|
|
99
|
+
**失败怎么读**
|
|
100
|
+
|
|
101
|
+
- **`CDP error: ...` 是浏览器答了但拒绝**(比如元素已经不在)—— 多半是页面变了,重新快照再试,不要盲目重复同一动作。
|
|
102
|
+
- **超时 / 连接类错误**才是浏览器可能有问题;这类错误会触发一次自动探测,通常下一条命令就能恢复。
|
|
103
|
+
- **`BROWSER_SESSION_UNKNOWN`** 说明会话没了(浏览器被替换过);`browser_reset_session` 重建本任务的会话。
|
|
104
|
+
- **同一个动作连续失败两次就换策略**:换 `selector`、换语义定位、或者用 `browser_snapshot` 重新看页面 —— **不要第三次用同样的坐标**。
|
|
105
|
+
|
|
106
|
+
**`browser_history` 与动作集**
|
|
107
|
+
|
|
108
|
+
- `browser_history` **默认只给 20 条**,要更多自己传 `limit`。
|
|
109
|
+
- **可重放的动作有六种**:`navigate` / `click` / `type` / `scroll` / `key` / `execute`。不是每种历史记录都能 `browser_replay`,遇到不可重放的会明确报错。
|
|
110
|
+
|
|
64
111
|
## 多任务并行
|
|
65
112
|
|
|
66
113
|
每个 DSH 会话(任务)拥有独立的浏览器会话(独立标签页与历史),并发任务互不干扰:
|
|
@@ -68,7 +115,7 @@ dsh plugin --profile web add <本仓库路径>
|
|
|
68
115
|
- `browser_session` 查看本任务的会话与标签;
|
|
69
116
|
- `browser_reset_session` 关闭并重建本任务的会话(崩溃或卡死后用它恢复)。
|
|
70
117
|
|
|
71
|
-
登录态(cookie)为所有任务共享;可用 `browser_auth`
|
|
118
|
+
登录态(cookie)为所有任务共享;可用 `browser_auth` 导出/恢复,重启后不丢。注意 **`flush` 只导出当前页所属站点的 cookie**(按本会话所在页面的 URL 收敛),页面 URL 未知时**直接拒绝**而不是扩大范围去读整台机器的 cookie 罐;`restore` 会向任意域写 cookie,所以 `browser_auth` **同样受 `browser_restrict` 白名单约束**,不属于只读豁免。
|
|
72
119
|
|
|
73
120
|
## FAQ
|
|
74
121
|
|
|
@@ -86,12 +133,20 @@ DSH Desktop 上②命中即可用(0.1.18+ 插件自带 electron 包;44+ 二进
|
|
|
86
133
|
**Q:浏览器窗口不见了?**
|
|
87
134
|
窗口标题为 `dsh-browser`(自托管)。若子进程崩溃(或宿主 DSH 重启)会自动重启;崩溃前已打开的会话在**下一次调用时自动重建**——仅页面状态丢失,无需手动 `browser_reset_session`。`browser_reset_session` 仍可用于主动重置。
|
|
88
135
|
|
|
136
|
+
**Q:设置里的 `contentMaxChars` 改了没反应?**
|
|
137
|
+
先确认改的是哪一层。优先级从高到低是:**单次调用的 `maxChars`** > **配置里的 `contentMaxChars`** > **按格式的分档默认**(html/json 50 000,txt/markdown 20 000)。`contentMaxChars` 的作用就是给这个部署换一个默认值:不设时才用分档,设了就盖过分档。若改了没反应,通常是那一次调用自己传了 `maxChars`。
|
|
138
|
+
|
|
139
|
+
**Q:本机 Chrome / Edge 被关掉之后还能继续用吗?**
|
|
140
|
+
能。该载体观察进程退出并清掉**已失效的会话映射**,下一次调用会**重新拉起**浏览器并继续工作;重新拉起前会先 kill 旧进程并**最多等 3 秒**它真正退出,且**只认本次启动写下的 `DevToolsActivePort`**(被杀掉的浏览器没机会清理那个文件,照读会连到别的 Chromium 或 `node --inspect` 上)。反过来,页面只是**慢**不会让插件杀掉你的窗口:命令超时只把连接标记为存疑,下一次调用先用 `Browser.getVersion` 探一次再决定是否丢弃,只有 socket 已关闭才重启。
|
|
141
|
+
|
|
89
142
|
**Q:下载报 CORS 错误?**
|
|
90
143
|
`browser_download` 在页面上下文内 `fetch`,受同源/CORS 约束;跨域文件请先在同源页面内操作,或直接请求用户提供。仅支持 HTTP(S) URL;`savePath` 必须为绝对路径且位于 `downloadDir` 内(默认系统下载目录,自动识别 `Downloads`/`下载`/`下載`),不覆盖已有文件;`browser_screenshot` 的 `savePath` 受同一套限制。
|
|
91
144
|
|
|
92
145
|
**Q:如何禁止 agent 乱点?**
|
|
93
146
|
`browser_restrict` 设置白名单(如只允许 `browser_snapshot`/`browser_content`);传空列表解除。注意它是防误操作的**软护栏**,模型可自行解除,不是安全边界。
|
|
94
147
|
|
|
148
|
+
要一个**模型自己解不掉**的限制,用设置页「Agent 能做什么」的三个开关(执行页面脚本 / 下载文件到磁盘 / 写入登录状态):它们写在设置文档里,而没有任何工具能写那个文档,所以模型只能被拒 —— 这与 `browser_restrict` 的区别不是"谁更强",而是"谁能改"。部署级(对所有人生效)还可以用 `tool-browser` 的 `allowedActions` 配置,见 `cordis.patch.yml` 的注释。
|
|
149
|
+
|
|
95
150
|
## 故障排查
|
|
96
151
|
|
|
97
152
|
| 现象 | 可能原因 | 处理 |
|
package/docs/why-browser.md
CHANGED
|
@@ -32,7 +32,7 @@ LLM agent 需要"上网"。传统做法是给 agent 一个**无头浏览器**(Pl
|
|
|
32
32
|
|
|
33
33
|
## 设计取舍
|
|
34
34
|
|
|
35
|
-
- **登录态共享**:所有任务共享同一份 cookie(符合"用户登录一次,agent 到处可用")
|
|
35
|
+
- **登录态共享**:所有任务共享同一份 cookie(符合"用户登录一次,agent 到处可用");需要搬运时用 `browser_auth` 手动导出/恢复 —— 它的 `flush` **只导出当前页所属站点的 cookie**(按本会话所在页面的 URL 收敛,页面 URL 未知时拒绝而不是放宽),所以"导出"是逐站点的一步,不是一次性打包整个 cookie 罐。
|
|
36
36
|
- **任务级会话隔离**:每个 DSH 会话(任务)拥有独立的浏览器会话(标签页与历史),并发任务互不抢页面;自托管载体下**每个会话连窗口都各自一份**(窗口按会话 id 分组,窗口标题带任务标识),侧栏与本机浏览器载体则是同一个宿主里切换当前可见视图。
|
|
37
37
|
- **装好即用**:不依赖桌面外壳。有外壳时嵌入外壳视图;纯 `dsh web` 时插件自托管——自己拉起 Electron 窗口,通过本机 TCP JSON-RPC 驱动。
|
|
38
38
|
- **边界清晰**:可见视图、浏览器列布局属于宿主外壳;插件只负责 seam、provider 与工具,不含任何 UI。
|
package/lib/browser/types.d.ts
CHANGED
|
@@ -240,6 +240,14 @@ export interface BrowserWaitResult {
|
|
|
240
240
|
export interface BrowserScreenshotResult {
|
|
241
241
|
/** Base64 data URL of the capture (PNG or JPEG per the request). */
|
|
242
242
|
readonly dataUrl: string;
|
|
243
|
+
/** Pixel size of the image in `dataUrl`, when the capture was measured and scaled.
|
|
244
|
+
*
|
|
245
|
+
* Present when the request carried `maxWidth`/`maxHeight` and the layout metrics were
|
|
246
|
+
* readable: the scaling happens inside CDP through a clip, and without this the only way to
|
|
247
|
+
* learn what came back was to decode the data URL. Undefined means "not measured", which is
|
|
248
|
+
* also what an unscaled capture reports. */
|
|
249
|
+
readonly width?: number;
|
|
250
|
+
readonly height?: number;
|
|
243
251
|
/** File path the capture was also saved to, when the request asked for it. */
|
|
244
252
|
readonly path?: string;
|
|
245
253
|
}
|
|
@@ -358,14 +366,29 @@ export interface BrowserA11yNode {
|
|
|
358
366
|
readonly role: string;
|
|
359
367
|
/** Accessible name (aria-label, label text, placeholder, own text, …). */
|
|
360
368
|
readonly name: string;
|
|
361
|
-
/**
|
|
362
|
-
|
|
369
|
+
/**
|
|
370
|
+
* Current value of input-like nodes; absent when the node has none.
|
|
371
|
+
*
|
|
372
|
+
* The provider produces `null` for "not applicable" and the tool layer drops it, so absence
|
|
373
|
+
* and null mean the same thing here — which is what the output schema declares (`string`,
|
|
374
|
+
* not nullable). Saying `string | null` described the provider's internal shape rather than
|
|
375
|
+
* the contract, and the two disagreed at every call.
|
|
376
|
+
*/
|
|
377
|
+
readonly value?: string;
|
|
363
378
|
/** ARIA/DOM states: enabled/disabled/checked/unchecked/expanded/… */
|
|
364
379
|
readonly states: readonly string[];
|
|
365
380
|
/** DOM depth (within its document), for reconstructing the tree. */
|
|
366
381
|
readonly depth: number;
|
|
367
382
|
/** Element tag name (lowercase). */
|
|
368
383
|
readonly tag: string;
|
|
384
|
+
/**
|
|
385
|
+
* A CSS selector for this node, when one can be derived from its id or name.
|
|
386
|
+
*
|
|
387
|
+
* Empty for the many elements that have neither. Present because a role and a name are not
|
|
388
|
+
* always enough to target a node: `{by: 'text'}` matches an accessible name only loosely,
|
|
389
|
+
* and this is what lets a caller point at the exact element it was shown.
|
|
390
|
+
*/
|
|
391
|
+
readonly selector: string;
|
|
369
392
|
/** Viewport-relative center, for coordinate fallbacks. */
|
|
370
393
|
readonly x: number;
|
|
371
394
|
readonly y: number;
|
|
@@ -460,7 +483,15 @@ export interface BrowserProvider {
|
|
|
460
483
|
/**
|
|
461
484
|
* Open a new session. The provider mints the session id and prepares its
|
|
462
485
|
* backing surface (a view, a headless page, and so on). Sessions are isolated from
|
|
463
|
-
* each other
|
|
486
|
+
* each other in what they OWN — tabs, the active tab, history, the view — so one
|
|
487
|
+
* session must not be visible to any other session's caller.
|
|
488
|
+
*
|
|
489
|
+
* That isolation stops at the browser's own state, and is not meant to go further:
|
|
490
|
+
* cookies, logins and site storage are deliberately shared by every session, because
|
|
491
|
+
* every session drives the same browser and "sign in once, use it everywhere" is the
|
|
492
|
+
* point of using a real one. Reads that hand that state to a caller (see
|
|
493
|
+
* `flushAuth`) are scoped to the page in question, which is least exposure rather
|
|
494
|
+
* than a claim that the jar is per-session.
|
|
464
495
|
* @param label - optional human-readable label (e.g. the calling DSH task
|
|
465
496
|
* id) the provider may surface on the visible surface (window title) so a
|
|
466
497
|
* human can tell which task's page is currently shown.
|
|
@@ -1,22 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Transport for the desktop shell's browser bridge: one long-lived connection to a
|
|
3
|
-
* loopback TCP service.
|
|
4
|
-
*
|
|
5
|
-
* WHY THIS IS ITS OWN MODULE
|
|
6
|
-
* The bridge used to be reached with a fresh connection per request, which measured
|
|
7
|
-
* at ~25ms per call (TCP handshake + token exchange) against ~0.2ms when the socket
|
|
8
|
-
* is reused. A single browser action issues several CDP commands, so that cost was
|
|
9
|
-
* multiplied on every tool call — the difference between a snappy agent and a
|
|
10
|
-
* visibly sluggish one. Reuse is therefore a property of the transport, and keeping
|
|
11
|
-
* it here means the view host above does not have to think about sockets at all.
|
|
12
|
-
*
|
|
13
|
-
* PROTOCOL
|
|
14
|
-
* The first line on a connection carries the token; the service marks the socket
|
|
15
|
-
* authenticated and ignores that line as a command. Requests and answers are then
|
|
16
|
-
* newline-delimited JSON, one answer per request, in order.
|
|
17
|
-
*
|
|
18
|
-
* @module dsh-browser/browser-electron/bridge-connection
|
|
19
|
-
*/
|
|
20
1
|
/** Where the shell publishes its bridge endpoint. */
|
|
21
2
|
export interface BridgeEndpoint {
|
|
22
3
|
readonly port: number;
|
|
@@ -27,22 +8,60 @@ export interface BridgeEndpoint {
|
|
|
27
8
|
/**
|
|
28
9
|
* A reusable connection to the bridge.
|
|
29
10
|
*
|
|
30
|
-
* Requests
|
|
31
|
-
*
|
|
32
|
-
*
|
|
11
|
+
* Requests may be in flight CONCURRENTLY, so answers are correlated by id rather than by
|
|
12
|
+
* position: `call()` stamps a unique {@link REQUEST_ID_FIELD} on the request and `onData()`
|
|
13
|
+
* resolves the entry that carries the echoed id. A bridge that does not echo it is still
|
|
14
|
+
* supported — its answer is handed to the oldest outstanding request, which is the
|
|
15
|
+
* arrival-order rule the protocol states. Once such an answer has been seen, the connection
|
|
16
|
+
* stops keeping two requests in flight (a second one re-dials), so position can never be the
|
|
17
|
+
* only thing distinguishing two callers' answers. An answer that matches nothing is never
|
|
18
|
+
* handed to whichever request happens to be first: the socket is dropped instead.
|
|
19
|
+
*
|
|
20
|
+
* A broken socket is discarded so the next call dials again — a dead connection must never
|
|
21
|
+
* become a dead plugin.
|
|
33
22
|
*/
|
|
34
23
|
export declare class BridgeConnection {
|
|
35
|
-
private
|
|
24
|
+
private endpoint;
|
|
25
|
+
private readonly endpointPath?;
|
|
36
26
|
private socket;
|
|
37
27
|
private buffer;
|
|
28
|
+
/** Outstanding requests by id. */
|
|
29
|
+
private readonly pending;
|
|
30
|
+
/** Ids of the requests sent on the current socket, oldest first. */
|
|
38
31
|
private readonly queue;
|
|
32
|
+
private nextId;
|
|
33
|
+
/** True once any reply arrived without an id: this bridge does not echo request ids. */
|
|
34
|
+
private idLessBridge;
|
|
35
|
+
/** True once this connection may only have one request in flight at a time. */
|
|
36
|
+
private serialisable;
|
|
39
37
|
/**
|
|
40
38
|
* @param endpoint - the shell's published bridge endpoint.
|
|
39
|
+
* @param endpointPath - where that endpoint is republished, so a moved port can be followed.
|
|
40
|
+
*/
|
|
41
|
+
constructor(endpoint: BridgeEndpoint, endpointPath?: string | undefined);
|
|
42
|
+
/**
|
|
43
|
+
* Re-read the published endpoint, and adopt it if the shell has moved.
|
|
44
|
+
*
|
|
45
|
+
* The bridge binds an ephemeral port and picks a NEW one whenever its server is recreated. The
|
|
46
|
+
* endpoint file is rewritten every fifteen seconds, but a host that adopted the old one keeps
|
|
47
|
+
* dialling a dead port forever: measured here as `connect ECONNREFUSED 127.0.0.1:52276` while
|
|
48
|
+
* the live bridge sat on 52411. Discarding the socket is not enough — the comment below says a
|
|
49
|
+
* dead connection must never become a dead plugin, and a stale PORT makes exactly that happen.
|
|
50
|
+
*
|
|
51
|
+
* Cheap and safe: one small file read per call, no socket work.
|
|
52
|
+
*
|
|
53
|
+
* @returns true when a different endpoint was adopted.
|
|
41
54
|
*/
|
|
42
|
-
|
|
55
|
+
private refreshEndpoint;
|
|
43
56
|
/**
|
|
44
57
|
* Send one request, reusing the socket if it is still healthy.
|
|
45
|
-
*
|
|
58
|
+
*
|
|
59
|
+
* Requests may overlap: the request is written as soon as it is queued, so two concurrent
|
|
60
|
+
* callers have two requests in flight. Each carries a unique {@link REQUEST_ID_FIELD}, and
|
|
61
|
+
* `onData()` resolves the entry that carries the echoed id — which is what makes
|
|
62
|
+
* correlation independent of arrival order. A bridge that does not echo the field (the
|
|
63
|
+
* shipped shell today) still works by arrival order; see {@link onData}.
|
|
64
|
+
* @param request - the request body (the token and the request id are added here).
|
|
46
65
|
* @param timeoutMs - how long to wait for the answer before dropping the socket.
|
|
47
66
|
* @returns the bridge's answer.
|
|
48
67
|
*/
|
|
@@ -51,8 +70,12 @@ export declare class BridgeConnection {
|
|
|
51
70
|
close(): void;
|
|
52
71
|
/** The live socket, connecting and authenticating it on first use. */
|
|
53
72
|
private ensureSocket;
|
|
54
|
-
/**
|
|
73
|
+
/** Tear the connection down for an event raised by `source`, unless it is already gone. */
|
|
74
|
+
private socketFailed;
|
|
75
|
+
/** Resolve queued requests by id, or in order when the bridge does not echo one. */
|
|
55
76
|
private onData;
|
|
77
|
+
/** Forget one request (its answer can no longer be matched). */
|
|
78
|
+
private forget;
|
|
56
79
|
/** Tear the socket down, failing pending work. */
|
|
57
80
|
private reset;
|
|
58
81
|
}
|