dsh-builtin-browser 0.4.1 → 0.4.2
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 +61 -23
- package/README.md +59 -23
- package/client.js +60 -5
- package/cordis.patch.yml +21 -0
- package/desktop-bridge/plugin-browser-bridge.js +12 -4
- package/docs/architecture.md +12 -2
- package/docs/tool-reference.md +12 -12
- package/docs/user-guide.md +10 -3
- package/docs/why-browser.md +1 -1
- package/lib/browser/types.d.ts +25 -1
- package/lib/browser-electron/bridge-connection.d.ts +31 -5
- package/lib/browser-electron/bridge-connection.js +143 -21
- package/lib/browser-electron/entry.d.ts +1 -1
- package/lib/browser-electron/entry.js +61 -35
- 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 +25 -0
- package/lib/browser-electron/history-store.js +51 -4
- package/lib/browser-electron/host-main.js +75 -27
- package/lib/browser-electron/provider.d.ts +44 -1
- package/lib/browser-electron/provider.js +379 -48
- package/lib/browser-electron/remote-host.d.ts +52 -3
- package/lib/browser-electron/remote-host.js +244 -23
- package/lib/browser-electron/settings-route.d.ts +28 -0
- package/lib/browser-electron/settings-route.js +28 -0
- package/lib/browser-electron/settings-store.d.ts +40 -0
- package/lib/browser-electron/settings-store.js +107 -11
- package/lib/browser-electron/system-browser.d.ts +71 -3
- package/lib/browser-electron/system-browser.js +311 -40
- package/lib/tool-browser/index.js +142 -33
- package/package.json +5 -3
- package/scripts/verify-host-compat.mjs +195 -0
- package/src/browser/types.ts +25 -1
- package/src/browser-electron/bridge-connection.ts +142 -21
- package/src/browser-electron/entry.ts +58 -33
- package/src/browser-electron/ephemeral-profile.ts +152 -0
- package/src/browser-electron/history-store.ts +51 -4
- package/src/browser-electron/host-main.ts +105 -35
- package/src/browser-electron/provider.ts +401 -51
- package/src/browser-electron/remote-host.ts +261 -36
- package/src/browser-electron/settings-route.ts +46 -0
- package/src/browser-electron/settings-store.ts +121 -11
- package/src/browser-electron/system-browser.ts +302 -26
- package/src/tool-browser/index.ts +143 -35
package/client.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Browser settings panel (client half): the switches that decide how the shared
|
|
3
3
|
* browser behaves — history retention, login-state persistence, presentation,
|
|
4
|
-
* vision strategy and
|
|
4
|
+
* vision strategy, credential access, and which ACTIONS the agent may take —
|
|
5
|
+
* the one group of switches written where no tool can reach them.
|
|
5
6
|
*
|
|
6
7
|
* The host half owns the document and serves it at `/dsh-builtin-browser/settings`;
|
|
7
8
|
* this panel only renders it and sends patches, so the same file stays the single
|
|
@@ -62,7 +63,15 @@ window.__ModuleLoader__.load({ id: "dsh-builtin-browser", factory: (require) =>
|
|
|
62
63
|
"browser.edge": "本机 Edge",
|
|
63
64
|
"browser.auto": "自动(优先 Chrome,其次 Edge)",
|
|
64
65
|
"browser.hint": "选本机浏览器时会以**独立 profile** 启动它:不会打开、占用或修改你日常的窗口、书签与登录状态,关闭也不会关掉你自己的浏览器;代价是它看不到你日常浏览器里已登录的站点,需要时请在那个窗口里登录一次(登录态会留在插件自己的 profile 里)。桌面端默认由官方侧栏承载页面,此选项优先于侧栏。",
|
|
65
|
-
"credentials.hint": "关闭后 browser_auth
|
|
66
|
+
"credentials.hint": "关闭后 browser_auth 的导出会被拒绝,报 BROWSER_AUTH_DISABLED —— 导出读的是整台机器共享的 cookie 罐(按当前页所属站点收敛)。它只管**读**:写入登录状态(restore)归下面「Agent 能做什么」里的写入开关。桌面端请注意:Agent 驱动的是侧栏里那一个页面(人机同页),因此它与该页面共用同一个 partition —— 这也正是它能读到登录态的原因,属于有意接受的沙箱边界变化;关闭本项即拒绝读取。",
|
|
67
|
+
"actions.title": "Agent 能做什么",
|
|
68
|
+
"actions.lead": "与 browser_restrict 不同:那一层是模型自己设的软护栏,它随时可以解除;这里的三项由你在设置里决定,模型手里的工具改不了它们。",
|
|
69
|
+
"actions.allowExecute": "允许在页面里执行脚本",
|
|
70
|
+
"actions.allowExecute.hint": "browser_execute 能在当前页面里运行任意 JavaScript —— 它的能力上限就是这个页面能做的事。关闭后该工具一律被拒(BROWSER_EXECUTE_DISABLED),模型手里的工具没有一个能改这项设置。但也别把它当成锁:设置文档由本机 HTTP 端点读写,同源页面可以写它 —— 这不是凭据系统。",
|
|
71
|
+
"actions.allowDownload": "允许下载文件到磁盘",
|
|
72
|
+
"actions.allowDownload.hint": "browser_download 会把 URL 下载到 downloadDir 内(带登录态;仍需通过目录准入,不覆盖已有文件)。关闭后一律被拒(BROWSER_DOWNLOAD_DISABLED)。截图另存走的是同一套目录准入,不受此项影响。",
|
|
73
|
+
"actions.allowCredentialWrite": "允许写入登录状态",
|
|
74
|
+
"actions.allowCredentialWrite.hint": "browser_auth 的 restore 会向任意域写入 cookie。关闭后只能导出、不能导入(BROWSER_AUTH_WRITE_DISABLED);读取仍由上面「凭据」那一项控制。",
|
|
66
75
|
}
|
|
67
76
|
|
|
68
77
|
const en = {
|
|
@@ -101,7 +110,15 @@ window.__ModuleLoader__.load({ id: "dsh-builtin-browser", factory: (require) =>
|
|
|
101
110
|
"browser.edge": "Installed Edge",
|
|
102
111
|
"browser.auto": "Automatic (Chrome first, then Edge)",
|
|
103
112
|
"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.",
|
|
104
|
-
"credentials.hint": "Off makes browser_auth refuse
|
|
113
|
+
"credentials.hint": "Off makes 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.",
|
|
114
|
+
"actions.title": "What the agent may do",
|
|
115
|
+
"actions.lead": "Unlike 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.",
|
|
116
|
+
"actions.allowExecute": "Run scripts in the page",
|
|
117
|
+
"actions.allowExecute.hint": "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.",
|
|
118
|
+
"actions.allowDownload": "Download files to disk",
|
|
119
|
+
"actions.allowDownload.hint": "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.",
|
|
120
|
+
"actions.allowCredentialWrite": "Write login state",
|
|
121
|
+
"actions.allowCredentialWrite.hint": "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.",
|
|
105
122
|
}
|
|
106
123
|
|
|
107
124
|
/** Read or patch the host-owned settings document. */
|
|
@@ -158,7 +175,7 @@ window.__ModuleLoader__.load({ id: "dsh-builtin-browser", factory: (require) =>
|
|
|
158
175
|
/** The settings page section. */
|
|
159
176
|
function SettingsRoot() {
|
|
160
177
|
const t = translate
|
|
161
|
-
const [
|
|
178
|
+
const [rawSettings, setSettings] = useState(null)
|
|
162
179
|
const [meta, setMeta] = useState({ path: "" })
|
|
163
180
|
const [error, setError] = useState("")
|
|
164
181
|
const [busy, setBusy] = useState(false)
|
|
@@ -185,13 +202,23 @@ window.__ModuleLoader__.load({ id: "dsh-builtin-browser", factory: (require) =>
|
|
|
185
202
|
.then(() => setBusy(false))
|
|
186
203
|
}, [])
|
|
187
204
|
|
|
188
|
-
if (
|
|
205
|
+
if (rawSettings === null) {
|
|
189
206
|
return h("div", { style: styles.wrap }, [
|
|
190
207
|
h("p", { key: "l", style: styles.lead }, t("loading")),
|
|
191
208
|
error !== "" ? h("p", { key: "e", style: styles.error }, `${t("failed")} ${error}`) : null,
|
|
192
209
|
])
|
|
193
210
|
}
|
|
194
211
|
|
|
212
|
+
// A document served by an OLDER host has no `actions` section — which is exactly the state
|
|
213
|
+
// an update leaves behind when the host has not been restarted yet (the client bundle is
|
|
214
|
+
// read at request time, the host's code at startup). Rendering `settings.actions.allowExecute`
|
|
215
|
+
// off that threw, and the throw took the whole panel with it: the section did not merely show
|
|
216
|
+
// stale values, it refused to open. Fill the gap with the defaults an older host behaves by,
|
|
217
|
+
// which is also the honest reading — it has no such switch, so "on" is what it does.
|
|
218
|
+
const settings = rawSettings.actions === undefined
|
|
219
|
+
? { ...rawSettings, actions: { allowExecute: true, allowDownload: true, allowCredentialWrite: true } }
|
|
220
|
+
: rawSettings
|
|
221
|
+
|
|
195
222
|
const section = (titleKey, children) => h("div", { key: titleKey, style: styles.group }, [
|
|
196
223
|
h("p", { key: "t", style: styles.groupTitle }, t(titleKey)),
|
|
197
224
|
...children,
|
|
@@ -289,6 +316,34 @@ window.__ModuleLoader__.load({ id: "dsh-builtin-browser", factory: (require) =>
|
|
|
289
316
|
]),
|
|
290
317
|
]),
|
|
291
318
|
|
|
319
|
+
section("actions.title", [
|
|
320
|
+
h("p", { key: "lead", style: styles.hint }, t("actions.lead")),
|
|
321
|
+
h(Toggle, {
|
|
322
|
+
key: "allowExecute",
|
|
323
|
+
labelKey: "actions.allowExecute",
|
|
324
|
+
hintKey: "actions.allowExecute.hint",
|
|
325
|
+
checked: settings.actions.allowExecute,
|
|
326
|
+
busy,
|
|
327
|
+
onChange: value => patch({ actions: { allowExecute: value } }),
|
|
328
|
+
}),
|
|
329
|
+
h(Toggle, {
|
|
330
|
+
key: "allowDownload",
|
|
331
|
+
labelKey: "actions.allowDownload",
|
|
332
|
+
hintKey: "actions.allowDownload.hint",
|
|
333
|
+
checked: settings.actions.allowDownload,
|
|
334
|
+
busy,
|
|
335
|
+
onChange: value => patch({ actions: { allowDownload: value } }),
|
|
336
|
+
}),
|
|
337
|
+
h(Toggle, {
|
|
338
|
+
key: "allowCredentialWrite",
|
|
339
|
+
labelKey: "actions.allowCredentialWrite",
|
|
340
|
+
hintKey: "actions.allowCredentialWrite.hint",
|
|
341
|
+
checked: settings.actions.allowCredentialWrite,
|
|
342
|
+
busy,
|
|
343
|
+
onChange: value => patch({ actions: { allowCredentialWrite: value } }),
|
|
344
|
+
}),
|
|
345
|
+
]),
|
|
346
|
+
|
|
292
347
|
section("credentials.title", [
|
|
293
348
|
h(Toggle, {
|
|
294
349
|
key: "allowRead",
|
package/cordis.patch.yml
CHANGED
|
@@ -40,3 +40,24 @@
|
|
|
40
40
|
|
|
41
41
|
- id: tool-browser
|
|
42
42
|
name: dsh-builtin-browser/tool-browser
|
|
43
|
+
# The tool layer's own switches. The `config` key is what makes them reachable at all:
|
|
44
|
+
# without it nothing at the plugin level could set them, so the operator allow-list the
|
|
45
|
+
# READMEs describe was unreachable in every factory deployment.
|
|
46
|
+
#
|
|
47
|
+
# Every field is optional, so an empty mapping uses the defaults:
|
|
48
|
+
#
|
|
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 browser_restrict).
|
|
51
|
+
# READ_ONLY_TOOLS and browser_restrict are never blocked, whatever the list says.
|
|
52
|
+
# timeoutMs: cooperative per-call budget in ms (default 60000).
|
|
53
|
+
# tabTools: whether the tab-management tools are registered (default true).
|
|
54
|
+
#
|
|
55
|
+
# To restrict a deployment, replace this with, for example:
|
|
56
|
+
# config:
|
|
57
|
+
# allowedActions: [browser_snapshot, browser_a11y, browser_content, browser_click]
|
|
58
|
+
#
|
|
59
|
+
# The settings panel's "What the agent may do" switches are a SEPARATE layer with the
|
|
60
|
+
# same property — the model cannot lift them either — and they need no patch file at
|
|
61
|
+
# all. The list above restricts every task in a deployment; those three switches
|
|
62
|
+
# restrict what any task may do on this installation, and need no YAML.
|
|
63
|
+
config: {}
|
|
@@ -101,7 +101,7 @@ async function handle(request) {
|
|
|
101
101
|
const id = Number(request.id)
|
|
102
102
|
if (!Number.isFinite(id)) throw new Error('cdp requires a numeric id')
|
|
103
103
|
const result = await sendCdp(id, String(request.method ?? ''), request.params)
|
|
104
|
-
return { ok: true, result }
|
|
104
|
+
return { ok: true, result, bridgeRequestId: request.bridgeRequestId }
|
|
105
105
|
}
|
|
106
106
|
if (op === 'ensureSidebar') {
|
|
107
107
|
const url = request.url === undefined ? '' : String(request.url)
|
|
@@ -313,7 +313,7 @@ async function handle(request) {
|
|
|
313
313
|
})()`,
|
|
314
314
|
returnByValue: true,
|
|
315
315
|
})
|
|
316
|
-
return { ok: true, result: folded?.result?.value }
|
|
316
|
+
return { ok: true, result: folded?.result?.value, bridgeRequestId: request.bridgeRequestId }
|
|
317
317
|
}
|
|
318
318
|
throw new Error(`unknown op ${JSON.stringify(op)}`)
|
|
319
319
|
}
|
|
@@ -394,8 +394,16 @@ export function start() {
|
|
|
394
394
|
if (typeof request.op !== 'string' || request.op === '') continue
|
|
395
395
|
}
|
|
396
396
|
void handle(request)
|
|
397
|
-
|
|
398
|
-
|
|
397
|
+
// The request id is stamped HERE, at the single point where every answer is written,
|
|
398
|
+
// rather than on the handful of returns that happened to remember it.
|
|
399
|
+
//
|
|
400
|
+
// The parent matches replies to requests by this id; when it is missing the parent
|
|
401
|
+
// marks the whole connection id-less and serialisable, which drops every reply back
|
|
402
|
+
// to arrival order and makes it reject in-flight requests when a second call
|
|
403
|
+
// arrives. Only two of this bridge's nine reply paths carried the field, so most
|
|
404
|
+
// operations permanently degraded the connection they used.
|
|
405
|
+
.then(answer => socket.write(JSON.stringify({ ...answer, bridgeRequestId: request.bridgeRequestId }) + '\n'))
|
|
406
|
+
.catch(error => socket.write(JSON.stringify({ ok: false, error: String(error?.message ?? error), bridgeRequestId: request.bridgeRequestId }) + '\n'))
|
|
399
407
|
}
|
|
400
408
|
})
|
|
401
409
|
socket.on('error', () => { /* client went away */ })
|
package/docs/architecture.md
CHANGED
|
@@ -43,9 +43,19 @@ agent (browser_* 工具)
|
|
|
43
43
|
- `browser_reset_session` 关闭本任务会话并遗忘映射(即使 close 抛错也清除,下次调用重建);
|
|
44
44
|
- 会话生命周期绑定 agent 作用域 ctx:agent(DSH 会话)销毁时自动关闭对应浏览器会话,任务结束后不再泄漏窗口/视图;
|
|
45
45
|
- 会话/开启动态/白名单都是**每插件实例(每 context)作用域**的,多 context 互不共享、互不污染;
|
|
46
|
-
- `browser_restrict`
|
|
46
|
+
- `browser_restrict` 维护本任务白名单,守卫所有非只读工具;
|
|
47
47
|
- 输出 schema 与返回值严格一致(DSH 运行时会校验,`additionalProperties: false` 下多一个字段都会报错)。
|
|
48
48
|
|
|
49
|
+
### 输出尺寸(工具层)
|
|
50
|
+
|
|
51
|
+
工具输出花的是调用方的上下文,所以工具层对几处返回设了明确上限,并在截断时**说明截断**:
|
|
52
|
+
|
|
53
|
+
- `browser_a11y` 默认 150 个节点(上限 5000)、一层一个空格缩进、`states` 只列非默认项;
|
|
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` 的时间戳精确到分钟、会话标记只在变化时打印一次。
|
|
58
|
+
|
|
49
59
|
## 自托管实现(纯 `dsh web`)
|
|
50
60
|
|
|
51
61
|
没有桌面外壳时,`RemoteElectronViewHost` 接管:
|
|
@@ -61,7 +71,7 @@ RemoteElectronViewHost ──TCP JSON-RPC──▶ host-main.js
|
|
|
61
71
|
- **协议**:本机 loopback TCP,每行一个 JSON(`{ id, op, ... }` ↔ `{ id, ok, result|err }`);
|
|
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 超时,且两者都受设置里 `credentials.allowRead` 门控 —— 关闭时抛 `BROWSER_AUTH_DISABLED`(导出**与恢复**都拒绝),有原生方法时优先用它,否则走 CDP 的 `
|
|
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`(导出**与恢复**都拒绝),有原生方法时优先用它,否则走 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,24 +40,24 @@
|
|
|
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 后仍在;上限 5000 条或 90 天。重开某条用 `browser_open`;与 `browser_history`(会话内操作日志,随会话消失)
|
|
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 天。重开某条用 `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,是动作而非观察,因此该工具**不在只读豁免集合**里。设置里「允许读取 cookies / 导出登录状态」关闭时,flush 与 restore **都**抛 `BROWSER_AUTH_DISABLED`。导出受设置「凭据」门控(`BROWSER_AUTH_DISABLED`),写入受「允许写入登录状态」门控(`BROWSER_AUTH_WRITE_DISABLED`)—— 两者独立 |
|
|
61
61
|
| `browser_restrict` | `allowed?` | `{ restrictedTo[] }` | – | 设置动作白名单;空列表解除;**只校验名字是否以 `browser_` 开头**(写错的前缀会报错,`browser_typo` 这种拼错但前缀合法的名字会被接受、等同于拦掉该名字,不额外报错);守卫按名字匹配,因此白名单里列什么名字就只放行什么名字。**软护栏**——模型可自行解除,非安全边界 |
|
|
62
62
|
|
|
63
63
|
## 截图
|
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,7 +35,7 @@ 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` | 是否注册标签管理工具 |
|
|
@@ -68,7 +67,7 @@ dsh plugin --profile web add <本仓库路径>
|
|
|
68
67
|
- `browser_session` 查看本任务的会话与标签;
|
|
69
68
|
- `browser_reset_session` 关闭并重建本任务的会话(崩溃或卡死后用它恢复)。
|
|
70
69
|
|
|
71
|
-
登录态(cookie)为所有任务共享;可用 `browser_auth`
|
|
70
|
+
登录态(cookie)为所有任务共享;可用 `browser_auth` 导出/恢复,重启后不丢。注意 **`flush` 只导出当前页所属站点的 cookie**(按本会话所在页面的 URL 收敛),页面 URL 未知时**直接拒绝**而不是扩大范围去读整台机器的 cookie 罐;`restore` 会向任意域写 cookie,所以 `browser_auth` **同样受 `browser_restrict` 白名单约束**,不属于只读豁免。
|
|
72
71
|
|
|
73
72
|
## FAQ
|
|
74
73
|
|
|
@@ -86,12 +85,20 @@ DSH Desktop 上②命中即可用(0.1.18+ 插件自带 electron 包;44+ 二进
|
|
|
86
85
|
**Q:浏览器窗口不见了?**
|
|
87
86
|
窗口标题为 `dsh-browser`(自托管)。若子进程崩溃(或宿主 DSH 重启)会自动重启;崩溃前已打开的会话在**下一次调用时自动重建**——仅页面状态丢失,无需手动 `browser_reset_session`。`browser_reset_session` 仍可用于主动重置。
|
|
88
87
|
|
|
88
|
+
**Q:设置里的 `contentMaxChars` 改了没反应?**
|
|
89
|
+
因为它**已经没有任何代码在读** —— 那是内容上限改为按格式固定之后的遗留键(html/json 50 000,txt/markdown 20 000)。上限请用单次调用的 `maxChars` 覆盖。
|
|
90
|
+
|
|
91
|
+
**Q:本机 Chrome / Edge 被关掉之后还能继续用吗?**
|
|
92
|
+
能。该载体观察进程退出并清掉**已失效的会话映射**,下一次调用会**重新拉起**浏览器并继续工作;重新拉起前会先 kill 旧进程并**最多等 3 秒**它真正退出,且**只认本次启动写下的 `DevToolsActivePort`**(被杀掉的浏览器没机会清理那个文件,照读会连到别的 Chromium 或 `node --inspect` 上)。反过来,页面只是**慢**不会让插件杀掉你的窗口:命令超时只把连接标记为存疑,下一次调用先用 `Browser.getVersion` 探一次再决定是否丢弃,只有 socket 已关闭才重启。
|
|
93
|
+
|
|
89
94
|
**Q:下载报 CORS 错误?**
|
|
90
95
|
`browser_download` 在页面上下文内 `fetch`,受同源/CORS 约束;跨域文件请先在同源页面内操作,或直接请求用户提供。仅支持 HTTP(S) URL;`savePath` 必须为绝对路径且位于 `downloadDir` 内(默认系统下载目录,自动识别 `Downloads`/`下载`/`下載`),不覆盖已有文件;`browser_screenshot` 的 `savePath` 受同一套限制。
|
|
91
96
|
|
|
92
97
|
**Q:如何禁止 agent 乱点?**
|
|
93
98
|
`browser_restrict` 设置白名单(如只允许 `browser_snapshot`/`browser_content`);传空列表解除。注意它是防误操作的**软护栏**,模型可自行解除,不是安全边界。
|
|
94
99
|
|
|
100
|
+
要一个**模型自己解不掉**的限制,用设置页「Agent 能做什么」的三个开关(执行页面脚本 / 下载文件到磁盘 / 写入登录状态):它们写在设置文档里,而没有任何工具能写那个文档,所以模型只能被拒 —— 这与 `browser_restrict` 的区别不是"谁更强",而是"谁能改"。部署级(对所有人生效)还可以用 `tool-browser` 的 `allowedActions` 配置,见 `cordis.patch.yml` 的注释。
|
|
101
|
+
|
|
95
102
|
## 故障排查
|
|
96
103
|
|
|
97
104
|
| 现象 | 可能原因 | 处理 |
|
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
|
}
|
|
@@ -366,6 +374,14 @@ export interface BrowserA11yNode {
|
|
|
366
374
|
readonly depth: number;
|
|
367
375
|
/** Element tag name (lowercase). */
|
|
368
376
|
readonly tag: string;
|
|
377
|
+
/**
|
|
378
|
+
* A CSS selector for this node, when one can be derived from its id or name.
|
|
379
|
+
*
|
|
380
|
+
* Empty for the many elements that have neither. Present because a role and a name are not
|
|
381
|
+
* always enough to target a node: `{by: 'text'}` matches an accessible name only loosely,
|
|
382
|
+
* and this is what lets a caller point at the exact element it was shown.
|
|
383
|
+
*/
|
|
384
|
+
readonly selector: string;
|
|
369
385
|
/** Viewport-relative center, for coordinate fallbacks. */
|
|
370
386
|
readonly x: number;
|
|
371
387
|
readonly y: number;
|
|
@@ -460,7 +476,15 @@ export interface BrowserProvider {
|
|
|
460
476
|
/**
|
|
461
477
|
* Open a new session. The provider mints the session id and prepares its
|
|
462
478
|
* backing surface (a view, a headless page, and so on). Sessions are isolated from
|
|
463
|
-
* each other
|
|
479
|
+
* each other in what they OWN — tabs, the active tab, history, the view — so one
|
|
480
|
+
* session must not be visible to any other session's caller.
|
|
481
|
+
*
|
|
482
|
+
* That isolation stops at the browser's own state, and is not meant to go further:
|
|
483
|
+
* cookies, logins and site storage are deliberately shared by every session, because
|
|
484
|
+
* every session drives the same browser and "sign in once, use it everywhere" is the
|
|
485
|
+
* point of using a real one. Reads that hand that state to a caller (see
|
|
486
|
+
* `flushAuth`) are scoped to the page in question, which is least exposure rather
|
|
487
|
+
* than a claim that the jar is per-session.
|
|
464
488
|
* @param label - optional human-readable label (e.g. the calling DSH task
|
|
465
489
|
* id) the provider may surface on the visible surface (window title) so a
|
|
466
490
|
* human can tell which task's page is currently shown.
|
|
@@ -27,22 +27,44 @@ export interface BridgeEndpoint {
|
|
|
27
27
|
/**
|
|
28
28
|
* A reusable connection to the bridge.
|
|
29
29
|
*
|
|
30
|
-
* Requests
|
|
31
|
-
*
|
|
32
|
-
*
|
|
30
|
+
* Requests may be in flight CONCURRENTLY, so answers are correlated by id rather than by
|
|
31
|
+
* position: `call()` stamps a unique {@link REQUEST_ID_FIELD} on the request and `onData()`
|
|
32
|
+
* resolves the entry that carries the echoed id. A bridge that does not echo it is still
|
|
33
|
+
* supported — its answer is handed to the oldest outstanding request, which is the
|
|
34
|
+
* arrival-order rule the protocol states. Once such an answer has been seen, the connection
|
|
35
|
+
* stops keeping two requests in flight (a second one re-dials), so position can never be the
|
|
36
|
+
* only thing distinguishing two callers' answers. An answer that matches nothing is never
|
|
37
|
+
* handed to whichever request happens to be first: the socket is dropped instead.
|
|
38
|
+
*
|
|
39
|
+
* A broken socket is discarded so the next call dials again — a dead connection must never
|
|
40
|
+
* become a dead plugin.
|
|
33
41
|
*/
|
|
34
42
|
export declare class BridgeConnection {
|
|
35
43
|
private readonly endpoint;
|
|
36
44
|
private socket;
|
|
37
45
|
private buffer;
|
|
46
|
+
/** Outstanding requests by id. */
|
|
47
|
+
private readonly pending;
|
|
48
|
+
/** Ids of the requests sent on the current socket, oldest first. */
|
|
38
49
|
private readonly queue;
|
|
50
|
+
private nextId;
|
|
51
|
+
/** True once any reply arrived without an id: this bridge does not echo request ids. */
|
|
52
|
+
private idLessBridge;
|
|
53
|
+
/** True once this connection may only have one request in flight at a time. */
|
|
54
|
+
private serialisable;
|
|
39
55
|
/**
|
|
40
56
|
* @param endpoint - the shell's published bridge endpoint.
|
|
41
57
|
*/
|
|
42
58
|
constructor(endpoint: BridgeEndpoint);
|
|
43
59
|
/**
|
|
44
60
|
* Send one request, reusing the socket if it is still healthy.
|
|
45
|
-
*
|
|
61
|
+
*
|
|
62
|
+
* Requests may overlap: the request is written as soon as it is queued, so two concurrent
|
|
63
|
+
* callers have two requests in flight. Each carries a unique {@link REQUEST_ID_FIELD}, and
|
|
64
|
+
* `onData()` resolves the entry that carries the echoed id — which is what makes
|
|
65
|
+
* correlation independent of arrival order. A bridge that does not echo the field (the
|
|
66
|
+
* shipped shell today) still works by arrival order; see {@link onData}.
|
|
67
|
+
* @param request - the request body (the token and the request id are added here).
|
|
46
68
|
* @param timeoutMs - how long to wait for the answer before dropping the socket.
|
|
47
69
|
* @returns the bridge's answer.
|
|
48
70
|
*/
|
|
@@ -51,8 +73,12 @@ export declare class BridgeConnection {
|
|
|
51
73
|
close(): void;
|
|
52
74
|
/** The live socket, connecting and authenticating it on first use. */
|
|
53
75
|
private ensureSocket;
|
|
54
|
-
/**
|
|
76
|
+
/** Tear the connection down for an event raised by `source`, unless it is already gone. */
|
|
77
|
+
private socketFailed;
|
|
78
|
+
/** Resolve queued requests by id, or in order when the bridge does not echo one. */
|
|
55
79
|
private onData;
|
|
80
|
+
/** Forget one request (its answer can no longer be matched). */
|
|
81
|
+
private forget;
|
|
56
82
|
/** Tear the socket down, failing pending work. */
|
|
57
83
|
private reset;
|
|
58
84
|
}
|