dsh-browser-plus 0.0.0-stage → 0.5.1
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 +153 -0
- package/LICENSE +22 -0
- package/NOTICE.md +7 -0
- package/README.en.md +119 -0
- package/README.md +118 -2
- package/assets/dsh-browser-plus-256.png +0 -0
- package/assets/dsh-browser-plus-512.png +0 -0
- package/assets/dsh-browser-plus-small.svg +9 -0
- package/assets/dsh-browser-plus.ico +0 -0
- package/assets/dsh-browser-plus.svg +11 -0
- package/assets/readme-workspace.png +0 -0
- package/client/index.js +185 -0
- package/cordis.patch.yml +41 -0
- package/docs/MIGRATION.md +48 -0
- package/docs/README.md +22 -0
- package/docs/SOAK-CHECKLIST.md +98 -0
- package/docs/architecture.md +88 -0
- package/docs/tool-reference.md +126 -0
- package/docs/user-guide.md +137 -0
- package/docs/why-browser.md +45 -0
- package/lib/browser/runtime.d.ts +238 -0
- package/lib/browser/runtime.js +330 -0
- package/lib/browser/types.d.ts +758 -0
- package/lib/browser/types.js +18 -0
- package/lib/browser-electron/auth-cookies.d.ts +54 -0
- package/lib/browser-electron/auth-cookies.js +83 -0
- package/lib/browser-electron/chrome-state.d.ts +211 -0
- package/lib/browser-electron/chrome-state.js +12 -0
- package/lib/browser-electron/entry.d.ts +73 -0
- package/lib/browser-electron/entry.js +65 -0
- package/lib/browser-electron/fingerprint.d.ts +29 -0
- package/lib/browser-electron/fingerprint.js +42 -0
- package/lib/browser-electron/host-main.d.ts +19 -0
- package/lib/browser-electron/host-main.js +2691 -0
- package/lib/browser-electron/icon.d.ts +11 -0
- package/lib/browser-electron/icon.js +23 -0
- package/lib/browser-electron/page-chrome.d.ts +21 -0
- package/lib/browser-electron/page-chrome.js +2269 -0
- package/lib/browser-electron/provider.d.ts +767 -0
- package/lib/browser-electron/provider.js +2825 -0
- package/lib/browser-electron/remote-host.d.ts +145 -0
- package/lib/browser-electron/remote-host.js +993 -0
- package/lib/browser-electron/task-summary.d.ts +2 -0
- package/lib/browser-electron/task-summary.js +12 -0
- package/lib/browser-electron/task-thumbnail.d.ts +11 -0
- package/lib/browser-electron/task-thumbnail.js +9 -0
- package/lib/browser-electron/write-guard.d.ts +41 -0
- package/lib/browser-electron/write-guard.js +123 -0
- package/lib/client.js +185 -0
- package/lib/command-browser/index.d.ts +20 -0
- package/lib/command-browser/index.js +35 -0
- package/lib/http-browser/index.d.ts +28 -0
- package/lib/http-browser/index.js +110 -0
- package/lib/index.d.ts +27 -0
- package/lib/index.js +25 -0
- package/lib/task-todos/index.d.ts +25 -0
- package/lib/task-todos/index.js +100 -0
- package/lib/tool-browser/index.d.ts +31 -0
- package/lib/tool-browser/index.js +2026 -0
- package/package.json +120 -4
- package/screenshots.json +3 -0
- package/scripts/build-client.mjs +20 -0
- package/scripts/build-icons.mjs +80 -0
- package/scripts/capture-window.ps1 +79 -0
- package/scripts/crop-image.ps1 +20 -0
- package/scripts/smoke-browser-tools.mjs +2051 -0
- package/scripts/smoke-chrome-world.mjs +65 -0
- package/scripts/smoke-electron-host.mjs +50 -0
- package/scripts/test-orb-drag.mjs +83 -0
- package/src/browser/runtime.ts +506 -0
- package/src/browser/types.ts +741 -0
- package/src/browser-electron/auth-cookies.ts +125 -0
- package/src/browser-electron/chrome-state.ts +192 -0
- package/src/browser-electron/entry.ts +125 -0
- package/src/browser-electron/fingerprint.ts +45 -0
- package/src/browser-electron/host-main.ts +2526 -0
- package/src/browser-electron/icon.ts +26 -0
- package/src/browser-electron/page-chrome.ts +2281 -0
- package/src/browser-electron/provider.ts +3366 -0
- package/src/browser-electron/remote-host.ts +1051 -0
- package/src/browser-electron/task-summary.ts +10 -0
- package/src/browser-electron/task-thumbnail.ts +17 -0
- package/src/browser-electron/write-guard.ts +134 -0
- package/src/command-browser/index.ts +61 -0
- package/src/http-browser/index.ts +139 -0
- package/src/index.ts +65 -0
- package/src/task-todos/index.ts +114 -0
- package/src/tool-browser/index.ts +2071 -0
- package/src/types/electron-shim.d.ts +143 -0
package/client/index.js
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-browser-plus — client half.
|
|
3
|
+
*
|
|
4
|
+
* One right-sidebar tab whose whole job is to put the shared browser window on
|
|
5
|
+
* screen: picking it from the rightbar's add list opens the window, and the
|
|
6
|
+
* panel keeps a button for raising it again. It is named after the plugin and
|
|
7
|
+
* carries the plugin's own mark, so it is not confused with the product's
|
|
8
|
+
* built-in browser tab. It talks to the plugin's own
|
|
9
|
+
* HTTP route (`src/http-browser/index.ts`), because a third-party client bundle
|
|
10
|
+
* has no generated Remote surface to call a server-side method through.
|
|
11
|
+
*
|
|
12
|
+
* This file is the browser bundle's source in the client-modules factory
|
|
13
|
+
* format: running it only registers the factory, and the module body runs when
|
|
14
|
+
* the plugin is first materialized. `scripts/build-client.mjs` copies it to
|
|
15
|
+
* `lib/client.js` — it is one file with no imports beyond the platform table,
|
|
16
|
+
* so no bundler is involved.
|
|
17
|
+
*/
|
|
18
|
+
window.__ModuleLoader__.load({
|
|
19
|
+
id: 'dsh-browser-plus',
|
|
20
|
+
factory: (require) => {
|
|
21
|
+
var module = { exports: {} }
|
|
22
|
+
var exports = module.exports
|
|
23
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })
|
|
24
|
+
const React = require('react')
|
|
25
|
+
|
|
26
|
+
/** Implementation identity; also the key its body registers under. */
|
|
27
|
+
const IMPLEMENTATION_ID = 'dsh-browser-plus'
|
|
28
|
+
/** Tab kind: a page type, opened by kind and recognizing no address. */
|
|
29
|
+
const KIND = 'dsh-browser-plus'
|
|
30
|
+
const OPEN_PATH = '/api/dsh-browser-plus/open'
|
|
31
|
+
const STATUS_PATH = '/api/dsh-browser-plus/status'
|
|
32
|
+
|
|
33
|
+
const COPY = {
|
|
34
|
+
zh: {
|
|
35
|
+
title: 'DSH-Browser-Plus',
|
|
36
|
+
description: '打开共享浏览器窗口',
|
|
37
|
+
open: '打开浏览器窗口',
|
|
38
|
+
busy: '正在打开…',
|
|
39
|
+
opened: '浏览器窗口已打开。',
|
|
40
|
+
failed: '打开失败:',
|
|
41
|
+
tasks: (count) => (count > 0 ? '当前有 ' + String(count) + ' 个浏览器任务' : '还没有浏览器任务'),
|
|
42
|
+
},
|
|
43
|
+
en: {
|
|
44
|
+
title: 'DSH-Browser-Plus',
|
|
45
|
+
description: 'Open the shared browser window',
|
|
46
|
+
open: 'Open browser window',
|
|
47
|
+
busy: 'Opening…',
|
|
48
|
+
opened: 'The browser window is open.',
|
|
49
|
+
failed: 'Could not open it: ',
|
|
50
|
+
tasks: (count) => (count > 0 ? String(count) + ' browser task(s) open' : 'no browser task yet'),
|
|
51
|
+
},
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The panel's copy, read at render so a language switch needs no reload. */
|
|
55
|
+
function copy() {
|
|
56
|
+
try {
|
|
57
|
+
const declared = String(document.documentElement.getAttribute('lang') || navigator.language || '')
|
|
58
|
+
return declared.toLowerCase().indexOf('zh') === 0 ? COPY.zh : COPY.en
|
|
59
|
+
} catch (error) {
|
|
60
|
+
return COPY.zh
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** POST the open endpoint; resolve to a message, never throw at the renderer. */
|
|
65
|
+
function requestOpen() {
|
|
66
|
+
return fetch(OPEN_PATH, { method: 'POST' }).then((response) => response.json().catch(() => null).then((body) => {
|
|
67
|
+
if (!response.ok || body === null || body.ok !== true) {
|
|
68
|
+
throw new Error(body !== null && typeof body.error === 'string' ? body.error : 'HTTP ' + String(response.status))
|
|
69
|
+
}
|
|
70
|
+
return true
|
|
71
|
+
}))
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Read the task count for the status line; a failure is not worth a notice. */
|
|
75
|
+
function requestStatus() {
|
|
76
|
+
return fetch(STATUS_PATH).then((response) => response.json()).then((body) => (body !== null && body.ok === true && typeof body.tasks === 'number' ? body.tasks : null)).catch(() => null)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The plugin's own mark, inlined from assets/dsh-browser-plus.svg so the
|
|
81
|
+
* bundle keeps requiring nothing but the platform table. The plate is part
|
|
82
|
+
* of the art: it is what keeps a white outline legible on a light surface.
|
|
83
|
+
*/
|
|
84
|
+
function BrowserIcon(props) {
|
|
85
|
+
const size = props !== null && props !== undefined && props.size !== undefined ? props.size : 36
|
|
86
|
+
return React.createElement('svg', {
|
|
87
|
+
width: size,
|
|
88
|
+
height: size,
|
|
89
|
+
className: props !== null && props !== undefined ? props.className : undefined,
|
|
90
|
+
'aria-hidden': 'true',
|
|
91
|
+
viewBox: '0 0 256 256',
|
|
92
|
+
xmlns: 'http://www.w3.org/2000/svg',
|
|
93
|
+
},
|
|
94
|
+
React.createElement('rect', { x: 10, y: 10, width: 236, height: 236, rx: 54, fill: '#202124' }),
|
|
95
|
+
React.createElement('g', { transform: 'translate(20 20) scale(9)', fill: 'none', stroke: '#ffffff', strokeWidth: 1.8, strokeLinecap: 'round', strokeLinejoin: 'round' },
|
|
96
|
+
React.createElement('rect', { x: 3.25, y: 4.75, width: 17.5, height: 14.5, rx: 3.25 }),
|
|
97
|
+
React.createElement('path', { d: 'M3.25 9.4h17.5' }),
|
|
98
|
+
React.createElement('circle', { cx: 6.15, cy: 7.05, r: 0.9, fill: '#ffffff', stroke: 'none' }),
|
|
99
|
+
React.createElement('circle', { cx: 8.75, cy: 7.05, r: 0.9, fill: '#ffffff', stroke: 'none' }),
|
|
100
|
+
),
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const PANEL_STYLE = {
|
|
105
|
+
display: 'flex',
|
|
106
|
+
flexDirection: 'column',
|
|
107
|
+
gap: '10px',
|
|
108
|
+
padding: '16px',
|
|
109
|
+
color: 'inherit',
|
|
110
|
+
font: 'inherit',
|
|
111
|
+
}
|
|
112
|
+
const BUTTON_STYLE = {
|
|
113
|
+
alignSelf: 'flex-start',
|
|
114
|
+
padding: '6px 14px',
|
|
115
|
+
border: '1px solid currentColor',
|
|
116
|
+
borderRadius: '6px',
|
|
117
|
+
background: 'transparent',
|
|
118
|
+
color: 'inherit',
|
|
119
|
+
font: 'inherit',
|
|
120
|
+
cursor: 'pointer',
|
|
121
|
+
}
|
|
122
|
+
const NOTE_STYLE = { opacity: 0.7, fontSize: '12px' }
|
|
123
|
+
|
|
124
|
+
/** The panel body: it opens the window as soon as it is mounted. */
|
|
125
|
+
function BrowserPanel() {
|
|
126
|
+
const t = copy()
|
|
127
|
+
const [busy, setBusy] = React.useState(false)
|
|
128
|
+
const [note, setNote] = React.useState(null)
|
|
129
|
+
const [tasks, setTasks] = React.useState(null)
|
|
130
|
+
|
|
131
|
+
const open = React.useCallback(() => {
|
|
132
|
+
setBusy(true)
|
|
133
|
+
setNote(null)
|
|
134
|
+
requestOpen()
|
|
135
|
+
.then(() => {
|
|
136
|
+
setBusy(false)
|
|
137
|
+
setNote({ ok: true, text: t.opened })
|
|
138
|
+
return requestStatus().then((count) => setTasks(count))
|
|
139
|
+
})
|
|
140
|
+
.catch((error) => {
|
|
141
|
+
setBusy(false)
|
|
142
|
+
setNote({ ok: false, text: t.failed + String(error !== null && error !== undefined && error.message !== undefined ? error.message : error) })
|
|
143
|
+
})
|
|
144
|
+
}, [t])
|
|
145
|
+
|
|
146
|
+
// The tab itself is the affordance: opening it opens the window.
|
|
147
|
+
React.useEffect(() => {
|
|
148
|
+
open()
|
|
149
|
+
requestStatus().then((count) => setTasks(count))
|
|
150
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
151
|
+
}, [])
|
|
152
|
+
|
|
153
|
+
return React.createElement('div', { style: PANEL_STYLE },
|
|
154
|
+
React.createElement('button', { type: 'button', style: BUTTON_STYLE, disabled: busy, onClick: open }, busy ? t.busy : t.open),
|
|
155
|
+
note !== null ? React.createElement('div', { style: NOTE_STYLE, role: 'status' }, note.text) : null,
|
|
156
|
+
tasks !== null ? React.createElement('div', { style: NOTE_STYLE }, t.tasks(tasks)) : null,
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
exports.inject = ['slots', 'sidebarRightTabs']
|
|
161
|
+
exports.apply = (ctx) => {
|
|
162
|
+
const slots = ctx.get('slots')
|
|
163
|
+
const tabs = ctx.get('sidebarRightTabs')
|
|
164
|
+
if (slots === undefined || tabs === undefined) return
|
|
165
|
+
ctx.effect(() => tabs.register({
|
|
166
|
+
id: IMPLEMENTATION_ID,
|
|
167
|
+
kind: KIND,
|
|
168
|
+
priority: 'extension',
|
|
169
|
+
title: () => copy().title,
|
|
170
|
+
guide: [{
|
|
171
|
+
id: 'open',
|
|
172
|
+
order: 40,
|
|
173
|
+
title: () => copy().title,
|
|
174
|
+
description: () => copy().description,
|
|
175
|
+
icon: BrowserIcon,
|
|
176
|
+
}],
|
|
177
|
+
}), 'dsh-browser-plus:type')
|
|
178
|
+
ctx.effect(() => slots.inject('sidebar.right.pane.tab', () => slots.register({
|
|
179
|
+
name: 'sidebar.right.pane.tab',
|
|
180
|
+
key: IMPLEMENTATION_ID,
|
|
181
|
+
}, BrowserPanel)), 'dsh-browser-plus:body')
|
|
182
|
+
}
|
|
183
|
+
return module.exports
|
|
184
|
+
},
|
|
185
|
+
})
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# dsh-browser-plus bundle patch: mounts the shared-browser capability into a
|
|
2
|
+
# profile composition. The browser seam (ctx.browser) is always mounted. The
|
|
3
|
+
# Electron provider and browser_* tools are ALWAYS registered: when the host
|
|
4
|
+
# provides `electronViewHost` (the desktop shell) that embedded view is used,
|
|
5
|
+
# and otherwise the plugin self-hosts its own Electron window, so installing
|
|
6
|
+
# the plugin is enough for the browser to work anywhere.
|
|
7
|
+
- insert:
|
|
8
|
+
# Root row: no behaviour of its own, but the client module system only reads
|
|
9
|
+
# a package's `dsh.client` from a Loader row whose specifier is the EXACT
|
|
10
|
+
# package root (a subpath resolves to undefined and is skipped). Without this
|
|
11
|
+
# row the Web GUI panel is never composed, however many subpath rows exist.
|
|
12
|
+
- id: browser-plus
|
|
13
|
+
name: dsh-browser-plus
|
|
14
|
+
|
|
15
|
+
- id: browser
|
|
16
|
+
name: dsh-browser-plus/browser
|
|
17
|
+
|
|
18
|
+
- id: browser-electron
|
|
19
|
+
name: dsh-browser-plus/browser-electron
|
|
20
|
+
config:
|
|
21
|
+
viewHost: !!js ctx.get('electronViewHost')
|
|
22
|
+
|
|
23
|
+
- id: tool-browser
|
|
24
|
+
name: dsh-browser-plus/tool-browser
|
|
25
|
+
|
|
26
|
+
# Human-facing `/browser`: opens/raises the shared window without the model.
|
|
27
|
+
# Its own row on purpose — a composition without the command registry must
|
|
28
|
+
# still get the tools above.
|
|
29
|
+
- id: browser-command
|
|
30
|
+
name: dsh-browser-plus/command-browser
|
|
31
|
+
|
|
32
|
+
# The Web GUI panel's bridge: opens/raises the window from a button in the
|
|
33
|
+
# right sidebar. Needs the web server, so it is its own row too.
|
|
34
|
+
- id: browser-http
|
|
35
|
+
name: dsh-browser-plus/http-browser
|
|
36
|
+
|
|
37
|
+
# Bridges the Agent's todo list (DSH session projection) into the browser, so
|
|
38
|
+
# the floating orb can show what the Agent is working on. Its own row because
|
|
39
|
+
# it needs optional services the browser itself does not.
|
|
40
|
+
- id: browser-task-todos
|
|
41
|
+
name: dsh-browser-plus/task-todos
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# 迁移到 dsh-browser-plus
|
|
2
|
+
|
|
3
|
+
> 如果你的 DSH profile 仍装着旧版浏览器插件(package name 为 `dsh-builtin-browser`),按本指南切换到 `dsh-browser-plus` 并迁移登录态。
|
|
4
|
+
|
|
5
|
+
## 步骤
|
|
6
|
+
|
|
7
|
+
### 1. 导出当前浏览器登录态(可选但推荐)
|
|
8
|
+
|
|
9
|
+
在旧包仍生效的 DSH 会话中:
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
browser_auth action="flush" # 把返回的 cookies JSON 保存到文件
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
### 2. 安装新包(正式名 dsh-browser-plus)
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
# 从 GitHub 安装(推荐;或使用 release tarball 或源码目录)
|
|
19
|
+
dsh plugin --profile web add github:ParticleLight/dsh-browser-plus
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### 3. 移除已替换的旧包(可选)
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
dsh plugin --profile web remove dsh-builtin-browser # 如实际安装名如此
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### 4. 恢复登录态
|
|
29
|
+
|
|
30
|
+
新包会话中:
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
browser_auth action="restore" cookies=<步骤1保存的JSON>
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
> 说明:宿主 userData 目录已从 `dsh-builtin-browser-host` 改为 `dsh-browser-plus-host`
|
|
37
|
+
> (host-main.ts `app.setPath('userData', ...)`),因此 cookie 不会自动迁移,必须走一遍 export/import。
|
|
38
|
+
|
|
39
|
+
## 变更对使用者可见的部分
|
|
40
|
+
|
|
41
|
+
- 当前共 36 个 `browser_*` 工具;除输入、文件和任务工具外,新增语义导航、滚动、快照引用、任务状态与显式人机交接。
|
|
42
|
+
- **一个共享可见浏览器窗口**,每个 DSH 任务保持隔离视图、标签与历史;页面任务管理器切换任务,后台任务操作不会抢走当前页面。`browser_space label="..."` 命名浏览器任务,空参列出任务。
|
|
43
|
+
- JS 对话框**默认**自动 accept,记录见 `browser_history`(action `dialog`);需要驱动「取消」路径时用 `browser_dialog` 先设策略。
|
|
44
|
+
- Electron 锁定 **42.9.3**(43.4.1 组合器故障,勿升)。
|
|
45
|
+
|
|
46
|
+
## 重启要求
|
|
47
|
+
|
|
48
|
+
迁移 profile bundle 后请**重启 DSH Web**,确保旧 loader entry 完整卸载、新包以 `dsh-browser-plus` 身份加载(见 `docs/SOAK-CHECKLIST.md`)。
|
package/docs/README.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# dsh-browser-plus 文档索引
|
|
2
|
+
|
|
3
|
+
按"你想做什么"选择入口:
|
|
4
|
+
|
|
5
|
+
| 目标 | 入口 |
|
|
6
|
+
| --- | --- |
|
|
7
|
+
| 了解插件为什么存在、与无头方案的区别 | [为什么做共享真实浏览器](why-browser.md) |
|
|
8
|
+
| 安装、配置、日常使用与常见问题 | [用户指南](user-guide.md) |
|
|
9
|
+
| 全部 43 个工具的参数、输出与示例 | [工具参考](tool-reference.md) |
|
|
10
|
+
| 了解 seam / provider / 工具三层与自托管实现 | [架构说明](architecture.md) |
|
|
11
|
+
| 重启 DSH 后的运行时验证清单 | [SOAK-CHECKLIST](SOAK-CHECKLIST.md) |
|
|
12
|
+
| 切换到 dsh-browser-plus 与登录态搬运 | [迁移指南](MIGRATION.md) |
|
|
13
|
+
| 回到项目首页 | [README](../README.md) |
|
|
14
|
+
|
|
15
|
+
## 各篇概览
|
|
16
|
+
|
|
17
|
+
- **[为什么做共享真实浏览器](why-browser.md)** — 定位、设计理念、与 Playwright 等无头方案的对比、边界与取舍。
|
|
18
|
+
- **[用户指南](user-guide.md)** — 环境要求、安装、配置项、快速上手、操作纪律、FAQ 与故障排查。
|
|
19
|
+
- **[工具参考](tool-reference.md)** — 每个 `browser_*` 工具的参数、输出、守卫与示例。
|
|
20
|
+
- **[架构说明](architecture.md)** — 三层结构、`ElectronBrowserViewHost` 接缝、自托管 RPC 子进程、截图通道、Electron 版本选择、共享窗口任务管理器与页面 chrome。
|
|
21
|
+
- **[SOAK-CHECKLIST](SOAK-CHECKLIST.md)** — 重启 DSH 后逐项验证对话框/输入工具/等待定位/共享窗口任务管理器/稳定性(一次性通过后可放心发布)。
|
|
22
|
+
- **[迁移指南](MIGRATION.md)** — 切换到 `dsh-browser-plus` 的步骤与 cookie 搬运。
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# 重启后浸泡验证清单(SOAK-CHECKLIST)
|
|
2
|
+
|
|
3
|
+
> 前置:重启 DSH Web(使 provider/remote-host/tool-browser 新代码生效),然后在 DSH 会话中依次执行。
|
|
4
|
+
> 每个工具调用后记录结果;任何**白屏**立即停止并回滚 host-main.js 至上一提交。
|
|
5
|
+
|
|
6
|
+
## 0. 不重启也能跑的真实集成冒烟(先跑这个)
|
|
7
|
+
```bash
|
|
8
|
+
npm run smoke:browser-tools
|
|
9
|
+
```
|
|
10
|
+
用**真实 Electron 宿主 + 真实 Chromium** 驱动**真实 provider**,覆盖 **87 项**(具体数字以脚本输出的 `passed` / `failed` 为准):
|
|
11
|
+
|
|
12
|
+
- **provider 层(12 项)**:navigate / content / snapshot / screenshot / **click 三种寻址(坐标、选择器、文字)** /
|
|
13
|
+
目标缺失的错误码 / waitForElement / **scrape 并发** / **cookie 导出→文件→清除→导入往返** / listTabs。
|
|
14
|
+
- **工具层(6 项)**:真实 `apply(ctx)` 注册的工具跑在真实 provider 上 —— `browser_open` / `browser_content` /
|
|
15
|
+
`browser_click`(文字寻址) / `browser_snapshot` / `browser_scrape`(start+status) / `browser_auth`(file)。
|
|
16
|
+
每次调用还会**逐字段比对声明的输出 schema**——DSH 会在运行时校验输出,而直接调 `execute()` 绕过了它。
|
|
17
|
+
- **输入真的到达页面(10 项)**:左键 → 页面应收到 `mousedown,mouseup,click`;右键 → 页面自己的
|
|
18
|
+
`contextmenu` handler 应收到且 `button:2`;修饰键 → `shiftKey` 与 `ctrlKey` 均应为真;
|
|
19
|
+
拖拽 → 手势应跨多次移动、按在源、释放在目标;键盘 → 页面应收到 `keydown`(Enter);
|
|
20
|
+
输入法 → 聚焦的输入框内容应变成所输入文本;**`click_ref`**(文档里的主要交互方式) → 快照取 ref
|
|
21
|
+
再点,页面应收到 `mousedown,click`;双击 → 页面应收到 `dblclick`;滚动 → `scrollY` 应真的变化
|
|
22
|
+
(页面本身不够长时先注入高元素);`fill` 选 select → 其 `value` 应变成所设值。
|
|
23
|
+
**这一组全部断言「页面真的收到了」,而不是「调用返回了」** —— 后者会漏掉整类静默失效。
|
|
24
|
+
**这三项曾长期为假绿**(只验证「目标解析对了」,空串也算通过)。它们现在能通过,靠的是
|
|
25
|
+
`Emulation.setFocusEmulationEnabled` —— **Chromium 会在渲染进程自认未聚焦时丢弃合成的鼠标按压**,
|
|
26
|
+
而移动不受此门控,所以 `hover` 一直正常、掩盖了点击全废。`data:` URL 的渲染器在进程内不做这个门控,
|
|
27
|
+
因此**本地用 `data:` 页面做输入验证会得出错误的「一切正常」** —— 必须打真实站点。
|
|
28
|
+
- **并行与规模(3 项)**:双任务(A 跑抓取时 B 的 snapshot 应在毫秒级返回)/ 两个任务各持独立会话 /
|
|
29
|
+
**100 个 URL @ 并发 8**(应为 100 行、100 个不同 `seq`、0 失败,结束后标签页数回到 1)。
|
|
30
|
+
|
|
31
|
+
- 它自带 profile(`DSH_BROWSER_PLUS_USER_DATA` 指向临时目录),**不与正在运行的 DSH 抢 profile 锁**,所以可以在 DSH 运行时跑;会短暂弹出一个窗口。
|
|
32
|
+
- 退出码 0 = 全绿。任何 FAIL 都会打印期望与实际。
|
|
33
|
+
- 这是唯一覆盖「provider → RPC → host-main → CDP → Chromium」整条链路的检查:单测用的是假宿主,够不到这一层。
|
|
34
|
+
- **历史**:它第一次跑就抓到一个真 bug —— 文字匹配的候选标签表漏了 `p`,导致「正文被拆成逐字符 span」的页面(example.com 现在就是这样)匹配不到容器。
|
|
35
|
+
|
|
36
|
+
## 1. 对话框自动处理
|
|
37
|
+
- [ ] `browser_open https://example.com`(host child 全新启动,无白屏)
|
|
38
|
+
- [ ] `browser_execute` 脚本 `setTimeout(() => { window.confirm('soak'); }, 0); 'scheduled'` → 页面不卡
|
|
39
|
+
- [ ] 二次 `browser_execute Date.now()` 返回数字(confirm 已自动 accept)
|
|
40
|
+
- [ ] `browser_history` 出现 `#n dialog ok {"type":"confirm",...}`
|
|
41
|
+
|
|
42
|
+
## 2. 输入工具(GUI 受控)
|
|
43
|
+
- [ ] `browser_execute` 聚焦输入后 `browser_press_key key="Enter"` → 快照见行为变化;history 有 pressKey
|
|
44
|
+
- [ ] `browser_press_key key="a" modifiers=["ctrl"]`(键盘事件低位键 'a')
|
|
45
|
+
- [ ] `browser_double_click` 选中文本段;history 有 doubleClick
|
|
46
|
+
- [ ] `browser_hover` 导航项 → `browser_screenshot` 目视 hover 态;history 有 hover
|
|
47
|
+
- [ ] `browser_execute` 注入 `<input type=file>` → 在工作目录/临时目录建样本文件 → `browser_upload_file filePath=<该文件绝对路径>` → `browser_execute` 读 `input.files[0]?.name` 与文件同名
|
|
48
|
+
- [ ] 传根外路径(如 `C:\Windows\win.ini`)调用 `browser_upload_file` → 必须报 `BROWSER_READ_PATH_DENIED`,且页面收不到该文件
|
|
49
|
+
|
|
50
|
+
## 3. 等待与定位
|
|
51
|
+
- [ ] `browser_wait_for selector="a[href]"` 立即命中(iana.org)
|
|
52
|
+
- [ ] 动态元素:注入延时节点后 `browser_wait_for selector="#late"` 命中
|
|
53
|
+
- [ ] `browser_snapshot` 每行含 `loc=`,结果含 `snapshotId`
|
|
54
|
+
- [ ] `browser_click_ref(snapshotId, ref)` 点击快照中的链接或按钮;导航后用旧 snapshotId 再调用应明确提示重新快照
|
|
55
|
+
- [ ] `browser_scroll` 无参数向下滚动;`browser_scroll_into_view(snapshotId, ref)` 将目标滚入视口
|
|
56
|
+
- [ ] `browser_back` / `browser_forward` / `browser_reload` / `browser_stop` 分别与页面工具栏行为一致
|
|
57
|
+
|
|
58
|
+
## 4. 共享窗口、任务管理器与 space
|
|
59
|
+
- [ ] 本会话 `browser_open https://www.iana.org/` → 一个可见 `dsh-browser-plus` 窗口和对应任务视图
|
|
60
|
+
- [ ] **另一个 DSH 会话** `browser_open https://www.w3.org/` → 仍只有**一个共享窗口**,页面任务管理器显示两个隔离任务
|
|
61
|
+
- [ ] `browser_space label="奖励任务"` → 当前浏览器任务在任务管理器中显示该标签,活动时标题为 `dsh-browser-plus — 奖励任务`;history 有 setSpace
|
|
62
|
+
- [ ] `browser_space`(无参)→ 列出全部浏览器任务(key + label);不产生新窗口
|
|
63
|
+
- [ ] 在任务管理器切换两个任务 → 各自 URL/标签正确;隐藏任务的浏览器操作更新自身状态但不抢当前可见页面
|
|
64
|
+
- [ ] Agent 执行长等待时任务卡显示“执行中”;调用 `browser_handoff state=waiting-user` 后显示“等待用户”
|
|
65
|
+
- [ ] 在当前任务卡点击“接管” → 显示“用户接管”,新的 Agent 页面操作被拒绝;点击“交还 Agent”后恢复操作
|
|
66
|
+
- [ ] `browser_tasks` 的状态、控制方、标签页数和最近动作与任务卡一致
|
|
67
|
+
- [ ] 关闭共享窗口后再次 `browser_open` → 窗口重建且不残留
|
|
68
|
+
|
|
69
|
+
## 5. 增量更新与性能
|
|
70
|
+
- [ ] 打开任务和轨迹面板后连续执行 100 次轻量页面操作 → 当前任务轨迹持续追加,其他任务卡不闪烁或重建
|
|
71
|
+
- [ ] 创建至少 3 个任务、每个 2 个标签 → 后台页面不持续刷新缩略图;打开任务面板并切换当前任务后才刷新当前缩略图
|
|
72
|
+
- [ ] 保持任务面板关闭执行操作 → 无可见缩略图捕获;重新打开后当前任务缩略图按需更新
|
|
73
|
+
|
|
74
|
+
## 6. 稳定性
|
|
75
|
+
- [ ] 连续导航 5 站(example.com → bing.com → w3.org → iana.org → example.com)→ 无白屏,每窗口有且仅有一个视图
|
|
76
|
+
- [ ] 回收 Electron child(Get-CimInstance ... Stop-Process)→ 下一次工具调用自动重启、无残留窗口
|
|
77
|
+
- [ ] `browser_auth action="flush"` → cookies 数量正常(换名安装前迁移用)
|
|
78
|
+
|
|
79
|
+
## 7. 已知 deferred minors(合并后择机)
|
|
80
|
+
见 `.superpowers/sdd/2026-08-21-dsh-browser-plus-ego-features/progress.md` 的 "minor (deferred)" 行(全部为非阻塞风格/文档项)。
|
|
81
|
+
## 8. chrome 隔离世界(可选,默认关)
|
|
82
|
+
|
|
83
|
+
仅在把 `browser-electron.chromeWorld` 设为 `isolated` 后执行。这一步会改变工具栏的注入世界,必须逐项人工确认后才可切换默认值:
|
|
84
|
+
|
|
85
|
+
**可自动验证的部分**(不需要 DSH 重启,自己起一个隔离 profile 的宿主):
|
|
86
|
+
```bash
|
|
87
|
+
npm run smoke:chrome-world
|
|
88
|
+
```
|
|
89
|
+
它做 **A/B 对照**并断言:两种模式下工具栏都挂载 ✓;**默认(main)模式会把 `__dshTasks`/`__dshTrail` 泄露给页面** ✗;`isolated` 模式下两者对页面**均为 `undefined`** ✓ —— 后者正是下面第 4 条的核心断言。
|
|
90
|
+
**注意**:脚本**不**断言 `__dshBrowserTaskAction` —— 它是**异步出现**的(2.5s 与 3s 两次测量结果不同),固定等待测不准,故只记录不断言。
|
|
91
|
+
|
|
92
|
+
- [ ] `browser_open https://example.com` → 工具栏正常显示,顶部中央悬停可展开
|
|
93
|
+
- [ ] 点击「接管」→ 状态变为等待用户;点击「交还 Agent」→ 恢复(隔离世界内 binding 仍能触发 set-control-owner)
|
|
94
|
+
- [ ] 打开任务面板与轨迹面板 → 任务卡、缩略图、操作轨迹正常渲染与追加
|
|
95
|
+
- [ ] 在页面控制台执行 `[typeof window.__dshTasks, typeof window.__dshTrail, typeof window.__dshBrowserTaskAction]` → 三项**全部为 undefined**
|
|
96
|
+
- [ ] 切换任务后,旧视图的 chrome 停表(无残留定时器);切回后工具栏与面板状态正确
|
|
97
|
+
- [ ] 连续导航 5 站 → 无白屏、工具栏每次都重新出现(每次导航会新建一个隔离世界)
|
|
98
|
+
- [ ] 回收 Electron child → 下一次调用自愈后工具栏仍正常
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# 架构说明
|
|
2
|
+
|
|
3
|
+
## 三层结构
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
agent (browser_* 工具)
|
|
7
|
+
→ ctx.browser (seam, dsh-browser-plus/browser)
|
|
8
|
+
→ dsh-browser-plus/browser-electron (provider)
|
|
9
|
+
→ ElectronBrowserViewHost (由宿主外壳提供)
|
|
10
|
+
→ WebContentsView + webContents.debugger (CDP)
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### seam 层(`src/browser/`)
|
|
14
|
+
|
|
15
|
+
`BrowserRuntime` 以 Cordis Service 形式注册为 `ctx.browser`:
|
|
16
|
+
|
|
17
|
+
- **provider 注册**:`registerBrowserProvider(provider)` 登记一个实现 `BrowserProvider` 接口的 provider,重名抛 `BROWSER_DUPLICATE_PROVIDER`;disposer 挂在注册方自身的 fiber 上,插件重载时正确清理。
|
|
18
|
+
- **provider 选择**(执行期解析,不依赖顺序):
|
|
19
|
+
- 配置了 id 且注册且可用 → 该 provider
|
|
20
|
+
- 配置了 id 但未注册 → `BROWSER_PROVIDER_CONFIGURED_MISSING`
|
|
21
|
+
- 配置了 id 但不可用 → `BROWSER_PROVIDER_CONFIGURED_UNAVAILABLE`
|
|
22
|
+
- 未配置、恰一个可用 → 自动选择
|
|
23
|
+
- 未配置、多个可用 → `BROWSER_PROVIDER_AMBIGUOUS`
|
|
24
|
+
- 未配置、无可用 → `BROWSER_PROVIDER_UNAVAILABLE`
|
|
25
|
+
- 所有请求/结果类型(`BrowserProvider` 接口、`BrowserError` 错误码)定义在 `src/browser/types.ts`。
|
|
26
|
+
|
|
27
|
+
### provider 层(`src/browser-electron/`)
|
|
28
|
+
|
|
29
|
+
`ElectronBrowserProvider` 通过 `ElectronBrowserViewHost` 接缝操作视图,与 Electron 解耦:
|
|
30
|
+
|
|
31
|
+
- 会话 = 有序标签列表 + 历史;每次 `open()` 新建会话(工具层按任务缓存复用);
|
|
32
|
+
- 每个标签对应一个视图(handle);`showActive` 让宿主把活动标签的视图置顶;
|
|
33
|
+
- 页面驱动全部走 CDP:`Page.navigate` / history navigation / `Page.reload` / `Page.stopLoading` / `Runtime.evaluate` / `Input.dispatchMouseEvent` / `Input.insertText` / `Page.captureScreenshot`(兜底);
|
|
34
|
+
- 每个 tab 保留最近 10 个短生命周期快照引用;`browser_click_ref` 与 `browser_scroll_into_view` 用内部 CSS 路径、元素指纹、URL 和文档代次验证目标,变化后明确要求重新快照;
|
|
35
|
+
- **人类工具栏是页面注入 chrome**:通过 `Page.addScriptToEvaluateOnNewDocument` 在顶层文档挂载 closed Shadow DOM,不创建第二个 `WebContentsView`;
|
|
36
|
+
- **可见性不重挂**: `showView` 只切换 `setVisible`,导航、加载、标题和 resize 路径不得执行 `removeChildView` / `addChildView`;
|
|
37
|
+
- **截图优先走宿主原生 `capturePage`**(新增 `capture` 通道):CDP `captureScreenshot` 在窗口存在多个(隐藏)视图时会挂起,原生捕获对可见视图快速可靠,失败时自动回退 CDP(临时摘除其他视图保证单视图状态);
|
|
38
|
+
- **写入路径受白名单约束**:`browser_screenshot` 与 `browser_download` 落盘前经 `resolveWritePath()`(解析最深已存在祖先的真实路径,防 `..` 与符号链接逃逸),只允许 `browser-electron.writeRoots`(默认工作目录 + 系统临时目录)之内的路径;`browser_download` 复用 `admitUrl()`,与导航同一套 URL 准入;
|
|
39
|
+
- **读取路径同样受白名单约束**:`browser_upload_file` 在触碰 DOM 之前经 `resolveReadPath()` 校验(文件必须存在、按真实路径比对,链接逃逸会被拒),只允许 `browser-electron.readRoots`(默认同 `writeRoots`)之内的文件;
|
|
40
|
+
- **chrome 所在的世界可切换**:默认注入页面主世界;`browser-electron.chromeWorld: isolated` 时 chrome 改注入自己的隔离世界(`Page.createIsolatedWorld`),任务状态与 binding token 都不再落在页面可读的上下文里(binding 用 `executionContextName` 限定在该世界)。默认仍为 `main`,切换前需按 SOAK 第 8 节在真实窗口验证;
|
|
41
|
+
- **注入 chrome 的信任边界**:页面可见的轨迹经 `redactTraceParams()` 白名单脱敏(`type`→字符数、`execute`→丢弃脚本、URL→origin、路径→basename),被访问页面无法从轨迹里读走此前输入的文本或脚本;`Runtime.addBinding('__dshBrowserTaskAction')` 的每个 payload 必须携带 `createView` 生成的每视图随机 token,否则忽略,页面脚本无法伪造任务切换或控制权变更;
|
|
42
|
+
- 所有 CDP 调用都有超时兜底(`withTimeout`),避免卡死工具调用;
|
|
43
|
+
- 历史记录单调递增的 seq,截断(500 条)后不回绕;失败导航只记一条。
|
|
44
|
+
|
|
45
|
+
### 工具层(`src/tool-browser/`)
|
|
46
|
+
|
|
47
|
+
36 个 `browser_*` 工具,按**调用方任务**(`exec.agent.id`)维护独立浏览器会话:
|
|
48
|
+
|
|
49
|
+
- 会话缓存 `sessionsByTask`:同一任务复用同一会话,并发首开去重;
|
|
50
|
+
- 变更型调用经过每任务 FIFO 操作通道;相同 in-flight snapshot/content/无落盘截图会合并,避免重复 CDP 与渲染工作;
|
|
51
|
+
- `browser_tasks` 与 `browser_handoff` 暴露运行、等待用户、用户接管、失败和空闲状态;用户接管后新的变更型 Agent 调用会等待交还;
|
|
52
|
+
- `browser_reset_session` 关闭本任务会话并遗忘映射(即使 close 抛错也清除,下次调用重建);
|
|
53
|
+
- `browser_restrict` 维护**按调用任务隔离**的白名单(插件级 `allowedActions` 作为默认值,任务可为自己覆盖或解除),守卫所有非只读工具;一个任务的规则不会限制其它任务;
|
|
54
|
+
- 输出 schema 与返回值严格一致(DSH 运行时会校验,`additionalProperties: false` 下多一个字段都会报错)。
|
|
55
|
+
|
|
56
|
+
## 自托管实现(纯 `dsh web`)
|
|
57
|
+
|
|
58
|
+
没有桌面外壳时,`RemoteElectronViewHost` 接管:
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
父进程(DSH) 子进程(Electron main)
|
|
62
|
+
RemoteElectronViewHost ──TCP JSON-RPC──▶ host-main.js
|
|
63
|
+
resolveElectronPath() BrowserWindow('dsh-browser-plus')
|
|
64
|
+
ElectronChildClient WebContentsView × N
|
|
65
|
+
DeferredRemoteView(物化缓存) webContents.debugger(CDP)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
- **协议**:本机 loopback TCP,每行一个 JSON(`{ id, op, ... }` ↔ `{ id, ok, result|err }`);
|
|
69
|
+
- **Electron 定位**:优先 package-local 的精确 `42.9.3` optional dependency;其次只接受经 package metadata 验证为 `42.9.3` 的 `ELECTRON_PATH`、DSH 锚点或 pnpm store 候选;找不到即失败,绝不回退到 43.x。
|
|
70
|
+
- **稳健性**:子进程/套接字都有 `error` 监听(否则未捕获事件会炸掉整个 DSH 进程);子进程退出自动重启;物化失败可重试;下载有 64MB 上限与 60s 超时;cookie 导出/恢复有 30s 超时;
|
|
71
|
+
- **视图可见性**:所有任务键(DSH 会话)共用一个 `BrowserWindow`,每个任务有隔离视图;页面任务管理器选择可见任务,`showView` 对后台任务只更新其活动视图,不改变用户当前选择。切换仅用 `setVisible`,绝不 remove/re-add(capture 的 CDP 兜底仍只临时 detach/restore 同窗口兄弟视图);
|
|
72
|
+
- **任务状态传递**:页面首次挂载、导航重装 chrome 或任务切换时接收完整 bootstrap;常规状态、任务卡、面板和轨迹变化使用带 epoch/revision 的增量 patch。摘要中的 URL 只保留 origin,避免泄露完整路径与查询参数;
|
|
73
|
+
- **任务缩略图**:缩略图使用原生 `capturePage` 生成 JPEG data URL,最长边限制为 288px、质量 58、上限 180 KiB。仅在任务面板打开时为可见任务按需捕获,单飞、最短 2 秒间隔、32 项缓存;后台任务保留最后成功图像。
|
|
74
|
+
- **孤儿防护**:父进程断开时子进程自动退出,不留僵尸窗口;
|
|
75
|
+
- **cookie 落盘**:子进程使用独立 userData 目录(`<DSH_HOME>/dsh-browser-plus-host`),登录态跨重启保留(另有 `browser_auth` 手动导出/恢复/按域清理)。
|
|
76
|
+
|
|
77
|
+
## 关键设计决策
|
|
78
|
+
|
|
79
|
+
| 决策 | 原因 |
|
|
80
|
+
| --- | --- |
|
|
81
|
+
| 任务级会话隔离,共享 cookie | 并行任务不抢页面;登录一次到处可用 |
|
|
82
|
+
| 原生 capturePage 优先,CDP 兜底 | CDP 截图多视图挂起;原生捕获窗口未激活时失败——两通道互补 |
|
|
83
|
+
| 页面注入 chrome | 保留单视图合成树,避免第二个 WebContentsView 引发的人眼白屏 |
|
|
84
|
+
| 一个共享窗口 + 页面任务管理器 | 任务视图、标签与历史隔离;`browser_space` 命名浏览器任务,后台更新不抢可见页面 |
|
|
85
|
+
| 固定 Electron 42.9.3 | 43.4.1 合成器故障会导致截图/白屏;找不到 pin 时明确失败 |
|
|
86
|
+
| 独立 userData | 多实例争用默认目录导致 GPU 缓存/会话锁冲突 |
|
|
87
|
+
| withTimeout 全覆盖 | 卡死的 CDP 调用必须能被工具超时兜底 |
|
|
88
|
+
| 输出 schema 严格匹配 | DSH 运行时会校验返回值,多字段即报错 |
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# 工具参考
|
|
2
|
+
|
|
3
|
+
全部 43 个 `browser_*` 工具。守卫列:✅ 表示该动作受 `browser_restrict` 白名单约束(白名单**按调用任务隔离**,一个任务的规则不影响其它任务);只读工具永不拦截。
|
|
4
|
+
|
|
5
|
+
## 页面与导航
|
|
6
|
+
|
|
7
|
+
| 工具 | 参数 | 输出 | 守卫 | 说明 |
|
|
8
|
+
| --- | --- | --- | --- | --- |
|
|
9
|
+
| `browser_open` | `url`(必填), `newTab?` | 快照(snapshotId/url/title/elements/truncated/challenge) | ✅ | 打开 URL,返回带编号元素和短生命周期 `snapshotId`;`newTab: true` 在新标签打开 |
|
|
10
|
+
| `browser_snapshot` | `query?`, `limit?` | 快照 | – | 交互元素(输入框/按钮/链接)编号清单和 `snapshotId`,供精确定位。`query` 按 kind+label 做大小写不敏感过滤,**过滤发生在计上限之前**(所以能搜到第 60 个之后的元素);`limit` 1-1000,默认 60 |
|
|
11
|
+
| `browser_back` | – | `{ navigated }` | ✅ | 返回上一条历史;没有上一页时返回 `false` |
|
|
12
|
+
| `browser_forward` | – | `{ navigated }` | ✅ | 前进到下一条历史;没有下一页时返回 `false` |
|
|
13
|
+
| `browser_reload` | – | `{ reloaded }` | ✅ | 刷新当前页 |
|
|
14
|
+
| `browser_stop` | – | `{ stopped }` | ✅ | 停止当前页加载 |
|
|
15
|
+
| `browser_scroll` | `deltaX?`, `deltaY?` | `{ x,y,maxX,maxY }` | ✅ | 按 CSS 像素滚动;无参数时向下一个视口 |
|
|
16
|
+
| `browser_wait_for` | `selector?`, `text?`, `state?`, `timeoutMs?`, `visible?` | `{ found, state, selector, tag, text? }` | ✅ | 等元素或文字出现 / 消失;**选择器与文字至少给一个**;`hidden` 与 `detached` 必须给选择器(要知道等谁消失) |
|
|
17
|
+
| `browser_content` | `format`(html/markdown/txt/json,必填), `selector?`, `maxChars?`, `timeoutMs?` | `{ content, truncated }` | – | 抓取页面内容;`selector` 限定区域 |
|
|
18
|
+
| `browser_challenge` | – | `{ blocked, kind?, reason?, hint? }` | – | 检测人机验证(CAPTCHA/Cloudflare/reCAPTCHA/hCaptcha/Turnstile);阻塞时请用户处理 |
|
|
19
|
+
|
|
20
|
+
## 页面操作
|
|
21
|
+
|
|
22
|
+
| 工具 | 参数 | 输出 | 守卫 | 说明 |
|
|
23
|
+
| --- | --- | --- | --- | --- |
|
|
24
|
+
| `browser_click_ref` | `snapshotId`, `ref`(必填) | `{ clicked }` | ✅ | 以快照引用进行真实 CDP 点击;页面变化后返回过期引用错误并要求重新快照 |
|
|
25
|
+
| `browser_scroll_into_view` | `snapshotId`, `ref`, `block?` | `{ scrolled,x,y,maxX,maxY }` | ✅ | 将快照引用元素滚入可见区域 |
|
|
26
|
+
| `browser_execute` | `script`(必填), `args?` | `{ ok, value? / exception? }` | ✅ | 仅在引用、表单和原生浏览工具无法表达时执行页面 JS。`script` 可以是**表达式**,也可以是**语句体**(自动判别,语句体用 `return` 返回值,如 `const rows = [...document.querySelectorAll('a')]; return rows.length`);两种都不是时报可读的解析错误 |
|
|
27
|
+
| `browser_click` | `x?`, `y?`, `selector?`, `text?` | `{ clicked, x?, y?, target? }` | ✅ | 点击元素,**三种寻址任选其一**:① `x`+`y` 视口坐标(配合截图做视觉定位;**坐标不会自动滚动** —— 落在视口外会**明确报错**而不是静默丢弃,并报出那个点上是什么元素;覆盖图标/图片按钮/canvas);② `selector` CSS 选择器;③ `text` 可见文字(或 aria-label/value,不区分大小写)。后两者在**页内解析**并把元素滚入视野,所以「点登录按钮」**不必先 snapshot 拿 ref**(省一轮);返回 `target` 告诉你实际点到了什么。多个匹配时**最内层的可见元素胜出**(文字最短者优先,同长取更深者) |
|
|
28
|
+
| `browser_double_click` | `x?`, `y?`, `selector?`, `text?` | `{ clicked, x?, y?, target? }` | ✅ | 同上寻址方式;用于选中文本、展开忽略单击的 UI |
|
|
29
|
+
| `browser_hover` | `x?`, `y?`, `selector?`, `text?` | `{ hovered, x?, y?, target? }` | ✅ | 同上寻址方式;悬停不点击(触发 hover 态、tooltip、下拉菜单) |
|
|
30
|
+
| `browser_drag` | `from`(必填), `to`(必填), `steps?` | `{ dragged, from?, to? }` | ✅ | 拖拽:`from` 按下 → 中间移动 → 在 `to` 释放。两端都用与 `browser_click` 相同的寻址(`x`+`y` / `selector` / `text`)并先滚入视野。`steps` 默认 12、上限 60 —— 中间移动是滑块/可排序库监听的东西,**瞬移会被忽略**。用于滑块、可排序列表、canvas 编辑器。**只驱动指针式拖拽**:依赖 HTML5 拖放(`dragstart`/`drop`)的页面不会响应,那种页面请用其自带控件 |
|
|
31
|
+
| `browser_type` | `text`(必填) | `{ typed }` | ✅ | 向聚焦元素输入文本(CDP `Input.insertText`) |
|
|
32
|
+
| `browser_press_key` | `key`(必填), `modifiers?` | `{ pressed }` | ✅ | 向聚焦元素物理按键(keyDown+keyUp;Enter/Tab/F1-F12/方向键及 Ctrl+A 等修饰组合) |
|
|
33
|
+
| `browser_fill` | `fields`(必填,数组), `submit?` | `{ fields[], submitted }` | ✅ | 批量填表;字段按 `selector`/`name`/`label`/`placeholder` 匹配,值支持字符串/数字/布尔;单个字段失败不影响其余;`submit: true` 提交表单 |
|
|
34
|
+
| `browser_upload_file` | `filePath`(必填), `selector?` | `{ path }` | ✅ | 给文件输入附加本地文件(CDP `DOM.setFileInputFiles`,页面视为真实选择);缺省页面第一个 `input[type="file"]`;`filePath` 必须存在且落在 `browser-electron.readRoots` 之内,越界抛 `BROWSER_READ_PATH_DENIED` |
|
|
35
|
+
|
|
36
|
+
## 标签与会话
|
|
37
|
+
|
|
38
|
+
| 工具 | 参数 | 输出 | 守卫 | 说明 |
|
|
39
|
+
| --- | --- | --- | --- | --- |
|
|
40
|
+
| `browser_list_tabs` | – | `{ session, tabs[] }` | – | 当前会话的标签列表 |
|
|
41
|
+
| `browser_switch_tab` | `tabId`(必填) | `{ switched }` | ✅ | 按 id 切换标签;自托管下同步切换可见视图 |
|
|
42
|
+
| `browser_close_tab` | `tabId`(必填) | `{ closed }` | – | 关闭标签;关闭活动标签后激活下一个 |
|
|
43
|
+
| `browser_reset` | – | `{ reset }` | ✅ | 关闭本任务所有标签,回到一个空白标签 |
|
|
44
|
+
| `browser_session` | – | `{ session, tabs[] }` | – | 查看本任务的浏览器会话与标签 |
|
|
45
|
+
| `browser_space` | `label?` | `{ label? / spaces[] }` | – | 命名本浏览器任务或列出浏览器任务;页面任务管理器控制哪个隔离任务视图显示在共享窗口中 |
|
|
46
|
+
| `browser_tasks` | – | `{ tasks[] }` | – | 查看每个任务的状态、控制方、标签页数、最近动作和错误摘要 |
|
|
47
|
+
| `browser_handoff` | `state`(`waiting-user` / `agent`) | 当前任务状态 | – | 让 Agent 等待用户操作,或在用户交还后恢复 Agent 控制 |
|
|
48
|
+
| `browser_reset_session` | – | `{ reset }` | ✅ | 关闭并重建本任务的浏览器会话(崩溃/卡死后恢复) |
|
|
49
|
+
|
|
50
|
+
## 历史与下载
|
|
51
|
+
|
|
52
|
+
| 工具 | 参数 | 输出 | 守卫 | 说明 |
|
|
53
|
+
| --- | --- | --- | --- | --- |
|
|
54
|
+
| `browser_history` | – | `{ entries[] }` | – | 操作日志(最新在后),含 seq/action/ok/params/result/error |
|
|
55
|
+
| `browser_replay` | `seq`(必填) | `{ replayed }` | ✅ | 按序号回放某一步(navigate/execute/click/type) |
|
|
56
|
+
| `browser_download` | `url`(必填), `savePath`(必填) | `{ path }` | ✅ | 带会话 cookie 下载到本地(上限 64MB,受 CORS 约束);`savePath` 受 `writeRoots` 限制,URL 与导航共用 HTTP(S) 准入(拒绝内嵌凭据) |
|
|
57
|
+
|
|
58
|
+
## 登录态与安全
|
|
59
|
+
|
|
60
|
+
| 工具 | 参数 | 输出 | 守卫 | 说明 |
|
|
61
|
+
| --- | --- | --- | --- | --- |
|
|
62
|
+
| `browser_auth` | `action`(flush/restore/clear,必填), `cookies?`, `file?`, `domain?`, `name?`, `all?` | `{ cookies[]? / restored? / failed? / removed? , names[]? }` | ✅ | 导出/恢复/清理 cookie(自托管可用);flush 返回列表,restore 写回,clear 按 domain(含子域)与/或 name 精确删除,未限定范围时必须显式 `all: true` |
|
|
63
|
+
| `browser_restrict` | `allowed?` | `{ restrictedTo[] }` | – | 设置**本任务**的动作白名单;空列表解除本任务的限制;未知工具名报错 |
|
|
64
|
+
|
|
65
|
+
## 批量抓取
|
|
66
|
+
|
|
67
|
+
| 工具 | 参数 | 输出 | 守卫 | 说明 |
|
|
68
|
+
| --- | --- | --- | --- | --- |
|
|
69
|
+
| `browser_scrape` | `action?`(start/status/stop/list), `urls?`, `script?`, `outPath?`, `waitFor?`, `timeoutMs?`, `concurrency?`, `id?` | `{ id?, state?, total?, done?, failed?, path?, error?, jobs[]? }` | ✅ | 后台批量访问 URL,把**每页一行 JSON** 追加到文件,结果**不经模型往返**——一千条与一条的 token 成本相同。`action=start` 立即返回,用 `action=status` 轮询。每行是 `{ seq, url, ok, data }` 或 `{ seq, url, ok, error }`(`seq` = 该 URL 在输入里的下标;并发时行按**完成顺序**落盘,按 `seq` 排序即可还原),**产生即落盘**,所以 `stop` 或中断都保留已抓到的行;单页失败不终止整批(计入 `failed`)。`outPath` 受 `writeRoots` 限制并在开始时截断。批次使用**自己的标签页**(不激活,所以不会抢走你正在看的页面,也不与同任务的工具调用争用),结束后销毁。`concurrency` 默认 1、上限 8,每个 worker 占一个标签页;后台批次**跳过 250ms 的绘制等待**(它只读 DOM 不读像素),实测单页开销约 6ms。
|
|
70
|
+
|
|
71
|
+
## 截图与打印
|
|
72
|
+
|
|
73
|
+
| 工具 | 参数 | 输出 | 守卫 | 说明 |
|
|
74
|
+
| --- | --- | --- | --- | --- |
|
|
75
|
+
| `browser_screenshot` | `fullPage?`, `savePath?` | `{ dataUrl, path? }` | – | PNG 截图;`savePath` 落盘供视觉模型读取,且必须落在 `browser-electron.writeRoots` 之内 |
|
|
76
|
+
| `browser_pdf` | `savePath`(必填), `landscape?`, `printBackground?`, `paperWidth?`, `paperHeight?` | `{ path, bytes }` | ✅ | 把当前标签打印成 PDF(Chrome 的「另存为 PDF」);`savePath` 必须落在 `browser-electron.writeRoots` 之内。走宿主的 `webContents.printToPDF`,**不是** CDP 的 `Page.printToPDF` —— Electron 的 debugger 没有那个方法;`paperWidth`/`paperHeight` 是**英寸**(宿主内部换算成微米),默认 8.5×11。`printBackground` 默认 true,否则深色页面会印成白纸。 |
|
|
77
|
+
| `browser_highlight` | `selector?`, `clear?` | `{ matched, cleared, nodeId?, box? }` | ✅ | 用 DevTools 那套高亮框套住 `selector` 的第一个匹配元素,让**看着窗口的人**知道 Agent 正要动哪里;走 CDP Overlay,**不改页面 DOM**。`box` 与 `getBoundingClientRect()` 逐位一致(实测偏差 ~2e-6 px)。注意高亮框**不在页面渲染里**,所以 `browser_screenshot` 拍不到它 —— 要看它得抓真窗口。`clear: true` 撤掉;选择器没匹配到返回 `matched: false`。 |
|
|
78
|
+
|
|
79
|
+
## 对话框与诊断
|
|
80
|
+
|
|
81
|
+
| 工具 | 参数 | 输出 | 守卫 | 说明 |
|
|
82
|
+
| --- | --- | --- | --- | --- |
|
|
83
|
+
| `browser_dialog` | `action`(`inspect` / `accept` / `dismiss`,必填), `promptText?` | `{ dialog?, policy }` | – | 查看/引导 JS 对话框(`alert`/`confirm`/`prompt`)。**对话框会冻住渲染器**,所以宿主默认立刻接受并记下内容 —— `inspect` 报告上一次(排空后再报,所以紧跟着触发它的那次调用也能看到)与当前策略;要驱动「确认删除」这类页面,先用 `dismiss`(或 `accept`,配 `promptText` 填 `prompt()`)设好**下一个**怎么答,再触发它 |
|
|
84
|
+
| `browser_console` | `level?`, `limit?`, `clear?` | `{ messages[] }` | – | 读控制台消息与未捕获异常(有界环形缓冲,各 200 条,最新在后)。**读不清空**,`clear: true` 才清;`level` 过滤 `log`/`info`/`warning`/`error`/`debug` |
|
|
85
|
+
| `browser_network` | `urlContains?`, `failedOnly?`, `limit?`, `clear?` | `{ requests[] }` | – | 读网络请求:`method`/`url`/`status`/`mime`/`kind`/`ms`/`failed`。同样**读不清空**;`failedOnly` 只看没跑完的 |
|
|
86
|
+
|
|
87
|
+
## 设备模拟
|
|
88
|
+
|
|
89
|
+
| 工具 | 参数 | 输出 | 守卫 | 说明 |
|
|
90
|
+
| --- | --- | --- | --- | --- |
|
|
91
|
+
| `browser_emulate` | `width?`, `height?`, `deviceScaleFactor?`, `mobile?`, `userAgent?`, `colorScheme?`, `clear?` | `{ applied[] }` | ✅ | 在活动标签上模拟设备:视口尺寸(可带移动端行为与 DPR)、自定义 UA、`prefers-color-scheme`。这是**渲染层覆盖**,窗口本身不变大;`clear: true` 一次撤销三样 |
|
|
92
|
+
|
|
93
|
+
**页面没反应时怎么查**(这三件套比截图有用)
|
|
94
|
+
```
|
|
95
|
+
browser_console → 看有没有报错 / 未捕获异常
|
|
96
|
+
browser_network urlContains="/api" → 请求到底发出去没有、状态码是多少
|
|
97
|
+
browser_execute → 页面状态探针(比如 elementFromPoint(x,y) 到底命中谁)
|
|
98
|
+
```
|
|
99
|
+
## 常用组合
|
|
100
|
+
|
|
101
|
+
**调研一个网站**
|
|
102
|
+
```
|
|
103
|
+
browser_open https://site → browser_content format=markdown → browser_snapshot → browser_click_ref(snapshotId, ref) → 逐页浏览
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
**登录并下载文件**
|
|
107
|
+
```
|
|
108
|
+
browser_open https://site/login → browser_fill(用户名/密码) submit=true →
|
|
109
|
+
等待跳转 → browser_download(url, savePath)
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
**表单填写(React/Vue 页面)**
|
|
113
|
+
```
|
|
114
|
+
browser_snapshot → browser_fill(fields=[{name:'email',value:'a@b.c'},{label:'密码',value:'***'}], submit=true)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
**误操作恢复**
|
|
118
|
+
```
|
|
119
|
+
browser_reset_session → browser_open(重新开始)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
**遇到验证码**
|
|
123
|
+
```
|
|
124
|
+
browser_challenge → browser_handoff state=waiting-user → 用户在共享窗口完成验证并交还 Agent →
|
|
125
|
+
browser_snapshot 复查
|
|
126
|
+
```
|