dsh-browser-plus 0.5.0 → 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 +131 -144
- package/README.en.md +27 -8
- package/README.md +27 -8
- package/client/index.js +185 -0
- package/cordis.patch.yml +24 -0
- package/docs/README.md +1 -1
- package/docs/SOAK-CHECKLIST.md +1 -1
- package/docs/tool-reference.md +5 -3
- package/docs/user-guide.md +20 -4
- package/lib/browser/runtime.d.ts +14 -1
- package/lib/browser/runtime.js +28 -0
- package/lib/browser/types.d.ts +96 -6
- package/lib/browser-electron/chrome-state.d.ts +24 -0
- package/lib/browser-electron/entry.d.ts +12 -5
- package/lib/browser-electron/entry.js +5 -2
- package/lib/browser-electron/host-main.d.ts +2 -1
- package/lib/browser-electron/host-main.js +205 -8
- package/lib/browser-electron/page-chrome.js +249 -14
- package/lib/browser-electron/provider.d.ts +60 -2
- package/lib/browser-electron/provider.js +300 -50
- package/lib/browser-electron/remote-host.d.ts +3 -1
- package/lib/browser-electron/remote-host.js +42 -1
- 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 +11 -0
- package/lib/index.js +11 -0
- package/lib/task-todos/index.d.ts +25 -0
- package/lib/task-todos/index.js +100 -0
- package/lib/tool-browser/index.js +190 -95
- package/package.json +27 -2
- package/scripts/build-client.mjs +20 -0
- package/scripts/smoke-browser-tools.mjs +84 -1
- package/scripts/smoke-chrome-world.mjs +6 -4
- package/scripts/test-orb-drag.mjs +83 -0
- package/src/browser/runtime.ts +36 -0
- package/src/browser/types.ts +98 -6
- package/src/browser-electron/chrome-state.ts +18 -0
- package/src/browser-electron/entry.ts +17 -7
- package/src/browser-electron/host-main.ts +207 -11
- package/src/browser-electron/page-chrome.ts +249 -14
- package/src/browser-electron/provider.ts +328 -50
- package/src/browser-electron/remote-host.ts +49 -2
- package/src/command-browser/index.ts +61 -0
- package/src/http-browser/index.ts +139 -0
- package/src/index.ts +13 -0
- package/src/task-todos/index.ts +114 -0
- package/src/tool-browser/index.ts +193 -96
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
CHANGED
|
@@ -5,6 +5,13 @@
|
|
|
5
5
|
# and otherwise the plugin self-hosts its own Electron window, so installing
|
|
6
6
|
# the plugin is enough for the browser to work anywhere.
|
|
7
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
|
+
|
|
8
15
|
- id: browser
|
|
9
16
|
name: dsh-browser-plus/browser
|
|
10
17
|
|
|
@@ -15,3 +22,20 @@
|
|
|
15
22
|
|
|
16
23
|
- id: tool-browser
|
|
17
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
|
package/docs/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
| --- | --- |
|
|
7
7
|
| 了解插件为什么存在、与无头方案的区别 | [为什么做共享真实浏览器](why-browser.md) |
|
|
8
8
|
| 安装、配置、日常使用与常见问题 | [用户指南](user-guide.md) |
|
|
9
|
-
| 全部
|
|
9
|
+
| 全部 43 个工具的参数、输出与示例 | [工具参考](tool-reference.md) |
|
|
10
10
|
| 了解 seam / provider / 工具三层与自托管实现 | [架构说明](architecture.md) |
|
|
11
11
|
| 重启 DSH 后的运行时验证清单 | [SOAK-CHECKLIST](SOAK-CHECKLIST.md) |
|
|
12
12
|
| 切换到 dsh-browser-plus 与登录态搬运 | [迁移指南](MIGRATION.md) |
|
package/docs/SOAK-CHECKLIST.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
```bash
|
|
8
8
|
npm run smoke:browser-tools
|
|
9
9
|
```
|
|
10
|
-
用**真实 Electron 宿主 + 真实 Chromium** 驱动**真实 provider**,覆盖 **
|
|
10
|
+
用**真实 Electron 宿主 + 真实 Chromium** 驱动**真实 provider**,覆盖 **87 项**(具体数字以脚本输出的 `passed` / `failed` 为准):
|
|
11
11
|
|
|
12
12
|
- **provider 层(12 项)**:navigate / content / snapshot / screenshot / **click 三种寻址(坐标、选择器、文字)** /
|
|
13
13
|
目标缺失的错误码 / waitForElement / **scrape 并发** / **cookie 导出→文件→清除→导入往返** / listTabs。
|
package/docs/tool-reference.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 工具参考
|
|
2
2
|
|
|
3
|
-
全部
|
|
3
|
+
全部 43 个 `browser_*` 工具。守卫列:✅ 表示该动作受 `browser_restrict` 白名单约束(白名单**按调用任务隔离**,一个任务的规则不影响其它任务);只读工具永不拦截。
|
|
4
4
|
|
|
5
5
|
## 页面与导航
|
|
6
6
|
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
| `browser_reload` | – | `{ reloaded }` | ✅ | 刷新当前页 |
|
|
14
14
|
| `browser_stop` | – | `{ stopped }` | ✅ | 停止当前页加载 |
|
|
15
15
|
| `browser_scroll` | `deltaX?`, `deltaY?` | `{ x,y,maxX,maxY }` | ✅ | 按 CSS 像素滚动;无参数时向下一个视口 |
|
|
16
|
-
| `browser_wait_for` | `selector`
|
|
16
|
+
| `browser_wait_for` | `selector?`, `text?`, `state?`, `timeoutMs?`, `visible?` | `{ found, state, selector, tag, text? }` | ✅ | 等元素或文字出现 / 消失;**选择器与文字至少给一个**;`hidden` 与 `detached` 必须给选择器(要知道等谁消失) |
|
|
17
17
|
| `browser_content` | `format`(html/markdown/txt/json,必填), `selector?`, `maxChars?`, `timeoutMs?` | `{ content, truncated }` | – | 抓取页面内容;`selector` 限定区域 |
|
|
18
18
|
| `browser_challenge` | – | `{ blocked, kind?, reason?, hint? }` | – | 检测人机验证(CAPTCHA/Cloudflare/reCAPTCHA/hCaptcha/Turnstile);阻塞时请用户处理 |
|
|
19
19
|
|
|
@@ -68,11 +68,13 @@
|
|
|
68
68
|
| --- | --- | --- | --- | --- |
|
|
69
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
70
|
|
|
71
|
-
##
|
|
71
|
+
## 截图与打印
|
|
72
72
|
|
|
73
73
|
| 工具 | 参数 | 输出 | 守卫 | 说明 |
|
|
74
74
|
| --- | --- | --- | --- | --- |
|
|
75
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`。 |
|
|
76
78
|
|
|
77
79
|
## 对话框与诊断
|
|
78
80
|
|
package/docs/user-guide.md
CHANGED
|
@@ -8,20 +8,27 @@
|
|
|
8
8
|
## 安装
|
|
9
9
|
|
|
10
10
|
```sh
|
|
11
|
-
# 从 npm 安装
|
|
11
|
+
# 从 npm 安装
|
|
12
|
+
# dsh plugin --profile web add dsh-browser-plus
|
|
13
|
+
|
|
14
|
+
# 或从 GitHub 安装(未发布新版本时用这条)
|
|
12
15
|
dsh plugin --profile web add github:ParticleLight/dsh-browser-plus
|
|
13
16
|
|
|
14
17
|
# 或从源码目录(独立仓库,一插件一仓库)
|
|
15
18
|
dsh plugin --profile web add <本仓库路径>
|
|
16
19
|
```
|
|
17
20
|
|
|
18
|
-
安装会链接插件、把 `dsh-browser-plus` 加入 profile 的 bundle
|
|
21
|
+
安装会链接插件、把 `dsh-browser-plus` 加入 profile 的 bundle 层,并挂载 **7 行**:
|
|
19
22
|
|
|
20
23
|
| 行 | 子路径 | 角色 |
|
|
21
24
|
| --- | --- | --- |
|
|
22
25
|
| `browser` | `dsh-browser-plus/browser` | `ctx.browser` 能力 seam(始终挂载) |
|
|
23
26
|
| `browser-electron` | `dsh-browser-plus/browser-electron` | Electron CDP provider |
|
|
24
27
|
| `tool-browser` | `dsh-browser-plus/tool-browser` | `browser_*` 模型侧工具 |
|
|
28
|
+
| `browser-plus`(根行) | `dsh-browser-plus` | **空行为**的根行:只为让客户端半边(右侧栏面板)被 client-modules 扫到 |
|
|
29
|
+
| `browser-command` | `dsh-browser-plus/command-browser` | `/browser` 斜杠命令(需要 `commands`) |
|
|
30
|
+
| `browser-http` | `dsh-browser-plus/http-browser` | 面板用的 HTTP 路由(需要 `webServer`) |
|
|
31
|
+
| `browser-task-todos` | `dsh-browser-plus/task-todos` | 把 Agent 的 `todo_write` 计划推给悬浮球(依赖可选的 `sessionProjections`) |
|
|
25
32
|
|
|
26
33
|
> 没有桌面外壳时插件**自托管**:自己拉起一个标题为 `dsh-browser-plus` 的 Electron 窗口,`browser_*` 工具照常可用。
|
|
27
34
|
|
|
@@ -51,6 +58,15 @@ dsh plugin --profile web add <本仓库路径>
|
|
|
51
58
|
6. 遇到验证码(browser_challenge 或快照标注 CHALLENGE)时,调用 browser_handoff state=waiting-user,停下等待用户交还任务
|
|
52
59
|
```
|
|
53
60
|
|
|
61
|
+
## 打开浏览器窗口
|
|
62
|
+
|
|
63
|
+
窗口默认只在 Agent 第一次调用浏览器工具时出现。人也可以自己打开它:
|
|
64
|
+
|
|
65
|
+
- **`/browser` 命令** —— 在输入框敲 `/`(或点 `+`)选「浏览器」,回车即打开或前置窗口。它**不产生模型消息**,只是把窗口带到眼前。
|
|
66
|
+
- **右侧栏「DSH-Browser-Plus」** —— 在右侧栏的「添加」列表里选它(名字和图标都是这个插件自己的,避免和 DSH 自带的「浏览器」撞名),面板打开的同时窗口就被带出来了;面板里还有一个按钮可以随时再前置一次,并显示当前有几个浏览器任务。
|
|
67
|
+
|
|
68
|
+
> 右侧栏里 DSH 自带的「浏览器」是另一个东西:DSH 自己的沙箱 iframe 浏览器,与这个自托管窗口无关。
|
|
69
|
+
|
|
54
70
|
## 操作纪律
|
|
55
71
|
|
|
56
72
|
- **优先用快照引用**:先取得 `snapshotId`,再用 `browser_click_ref` 或 `browser_scroll_into_view`;引用过期时重新快照,而不是猜测同名控件。
|
|
@@ -68,7 +84,7 @@ dsh plugin --profile web add <本仓库路径>
|
|
|
68
84
|
|
|
69
85
|
当前版本使用**一个共享可见浏览器窗口**,每个任务仍有隔离的任务视图、标签与历史。页面任务管理器切换可见任务;后台任务操作只更新自己的视图,不会抢走当前页面。`browser_space label="..."` 为本浏览器任务命名,`browser_space`(无参)列出全部浏览器任务。
|
|
70
86
|
|
|
71
|
-
|
|
87
|
+
工具栏是**常驻顶栏**:和标签栏一起占视图的高度(84px,开书签栏时 118px),页面**不需要为它让位**,也不会滑出或收回。任务按钮打开左侧工作区面板,操作轨迹按钮在桌面端打开右侧工作区面板。顶部工具栏最右侧常驻“接管 / 交还 Agent”控件,不必先打开任务面板;任务卡继续显示执行中、等待用户、用户接管、失败和空闲状态。接管期间新的 Agent 页面操作会停止,快照和内容读取仍可用于确认状态。
|
|
72
88
|
|
|
73
89
|
用户直接点击页面、编辑表单或使用非滚动键盘操作时,会自动切换为用户控制;滚轮、触摸拖动、滚动条操作,以及页面非编辑区的上下翻页键不会触发接管。Agent 自己的 CDP 鼠标和键盘输入带有短暂抑制标记,不会误交还控制权。
|
|
74
90
|
|
|
@@ -94,7 +110,7 @@ dsh plugin --profile web add <本仓库路径>
|
|
|
94
110
|
确认运行时是 Electron `42.9.3`,不要用 43.x。自托管截图优先走原生 `capturePage`,共享窗口内存在多个视图且目标未激活时自动兜底到 CDP。
|
|
95
111
|
|
|
96
112
|
**Q:浏览器窗口不见了?**
|
|
97
|
-
|
|
113
|
+
想主动把它叫回来:输入框里敲 `/browser`(或点 `+` 选「浏览器」),或者在右侧栏的「添加」列表里选「DSH-Browser-Plus」—— 两者都会打开或前置窗口,且**不产生模型消息**。窗口标题为 `dsh-browser-plus`(显示当前任务标签时为 `dsh-browser-plus — <名>`);所有任务共享这一可见窗口,通过页面任务管理器切换各自隔离视图。若子进程崩溃会自动重启;重启后旧会话失效,调用 `browser_reset_session` 重建。
|
|
98
114
|
|
|
99
115
|
**Q:下载报 CORS 错误?**
|
|
100
116
|
`browser_download` 在页面上下文内 `fetch`,受同源/CORS 约束;跨域文件请先在同源页面内操作,或直接请求用户提供。
|
package/lib/browser/runtime.d.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { Context, Service } from '@deepseek-ai/cordis';
|
|
10
10
|
import z from '@deepseek-ai/schemastery';
|
|
11
|
-
import type { BrowserContentRequest, BrowserHandoffState, BrowserContentResult, BrowserDownloadRequest, BrowserExecuteRequest, BrowserExecuteResult, BrowserFillRequest, BrowserFillResult, BrowserHistoryEntry, BrowserNavigateRequest, BrowserOpenOptions, BrowserOpenRequest, BrowserDragRequest, BrowserDragResult, BrowserPointerResult, BrowserPointerTarget, BrowserPressKeyRequest, BrowserClearAuthRequest, BrowserScrapeRequest, BrowserScrapeStatus, BrowserClearAuthResult, BrowserProvider, BrowserRefRequest, BrowserScreenshotRequest, BrowserScreenshotResult, BrowserScrollIntoViewRequest, BrowserScrollRequest, BrowserScrollResult, BrowserSessionId, BrowserSnapshotResult, BrowserSpaceInfo, BrowserTaskInfo, BrowserTaskUpdate, BrowserTab, BrowserTypeRequest, BrowserUploadFileRequest, BrowserUploadFileResult, BrowserWaitForRequest, BrowserWaitForResult, BrowserChallenge, ExportedCookie } from './types.ts';
|
|
11
|
+
import type { BrowserContentRequest, BrowserHandoffState, BrowserContentResult, BrowserDownloadRequest, BrowserExecuteRequest, BrowserExecuteResult, BrowserFillRequest, BrowserFillResult, BrowserHistoryEntry, BrowserNavigateRequest, BrowserOpenOptions, BrowserOpenRequest, BrowserDragRequest, BrowserDragResult, BrowserPointerResult, BrowserPointerTarget, BrowserPressKeyRequest, BrowserClearAuthRequest, BrowserScrapeRequest, BrowserScrapeStatus, BrowserClearAuthResult, BrowserProvider, BrowserRefRequest, BrowserScreenshotRequest, BrowserScreenshotResult, BrowserScrollIntoViewRequest, BrowserScrollRequest, BrowserScrollResult, BrowserSessionId, BrowserSnapshotResult, BrowserSpaceInfo, BrowserTaskInfo, BrowserTaskUpdate, BrowserTab, BrowserTypeRequest, BrowserUploadFileRequest, BrowserUploadFileResult, BrowserPdfRequest, BrowserPdfResult, BrowserHighlightRequest, BrowserHighlightResult, BrowserWaitForRequest, BrowserWaitForResult, BrowserChallenge, BrowserTaskTodo, ExportedCookie } from './types.ts';
|
|
12
12
|
export { BrowserError, } from './types.ts';
|
|
13
13
|
export type { BrowserChallenge, BrowserContentFormat, BrowserControlOwner, BrowserContentRequest, BrowserContentResult, BrowserDownloadRequest, BrowserExecuteRequest, BrowserExecuteResult, BrowserFillField, BrowserFillRequest, BrowserFillResult, BrowserHandoffState, BrowserHistoryEntry, BrowserNavigateRequest, BrowserOpenOptions, BrowserOpenRequest, BrowserDragRequest, BrowserDragResult, BrowserPointerResult, BrowserPointerTarget, BrowserPressKeyRequest, BrowserClearAuthRequest, BrowserScrapeRequest, BrowserScrapeStatus, BrowserClearAuthResult, BrowserProvider, BrowserRefRequest, BrowserScreenshotRequest, BrowserScreenshotResult, BrowserScrollIntoViewRequest, BrowserScrollRequest, BrowserScrollResult, BrowserSessionId, BrowserSnapshotElement, BrowserSnapshotResult, BrowserSpaceInfo, BrowserTaskInfo, BrowserTaskStatus, BrowserTaskUpdate, BrowserTab, BrowserTypeRequest, BrowserUploadFileRequest, BrowserUploadFileResult, BrowserWaitForRequest, BrowserWaitForResult, ExportedCookie, } from './types.ts';
|
|
14
14
|
declare module '@deepseek-ai/cordis' {
|
|
@@ -168,6 +168,8 @@ export declare class BrowserRuntime extends Service {
|
|
|
168
168
|
/** Attach a local file to a file input through the selected provider. */
|
|
169
169
|
uploadFile(session: BrowserSessionId, request: BrowserUploadFileRequest, signal?: AbortSignal): Promise<BrowserUploadFileResult>;
|
|
170
170
|
/** Wait for an element through the selected provider (bounded polling). */
|
|
171
|
+
highlight(session: BrowserSessionId, request: BrowserHighlightRequest, signal?: AbortSignal): Promise<BrowserHighlightResult>;
|
|
172
|
+
pdf(session: BrowserSessionId, request: BrowserPdfRequest, signal?: AbortSignal): Promise<BrowserPdfResult>;
|
|
171
173
|
waitForElement(session: BrowserSessionId, request: BrowserWaitForRequest, signal?: AbortSignal): Promise<BrowserWaitForResult>;
|
|
172
174
|
/** Type into the focused element through the selected provider. */
|
|
173
175
|
type(session: BrowserSessionId, request: BrowserTypeRequest, signal?: AbortSignal): Promise<void>;
|
|
@@ -218,6 +220,17 @@ export declare class BrowserRuntime extends Service {
|
|
|
218
220
|
updateTask(session: BrowserSessionId, update: BrowserTaskUpdate): Promise<BrowserTaskInfo>;
|
|
219
221
|
/** Mark one session as waiting for the user or returned to Agent control. */
|
|
220
222
|
setHandoff(session: BrowserSessionId, state: BrowserHandoffState): Promise<BrowserTaskInfo>;
|
|
223
|
+
/**
|
|
224
|
+
* Bring the shared browser window to the front through the selected
|
|
225
|
+
* provider, opening it when nothing is open yet.
|
|
226
|
+
*/
|
|
227
|
+
ensureWindowVisible(): Promise<void>;
|
|
228
|
+
/**
|
|
229
|
+
* Mirror one task's Agent todo list into the shared window (the floating orb
|
|
230
|
+
* reads it there). A provider without a window has nowhere to put it, so this
|
|
231
|
+
* is a no-op rather than an error.
|
|
232
|
+
*/
|
|
233
|
+
pushTaskTodos(taskKey: string, todos: readonly BrowserTaskTodo[]): Promise<void>;
|
|
221
234
|
/** Close the session through the selected provider. Idempotent; a missing
|
|
222
235
|
* provider is treated as already-closed so teardown paths stay no-ops. */
|
|
223
236
|
close(session: BrowserSessionId): Promise<void>;
|
package/lib/browser/runtime.js
CHANGED
|
@@ -190,6 +190,12 @@ export class BrowserRuntime extends Service {
|
|
|
190
190
|
return this.resolveProvider().uploadFile(session, request, signal);
|
|
191
191
|
}
|
|
192
192
|
/** Wait for an element through the selected provider (bounded polling). */
|
|
193
|
+
async highlight(session, request, signal) {
|
|
194
|
+
return this.resolveProvider().highlight(session, request, signal);
|
|
195
|
+
}
|
|
196
|
+
async pdf(session, request, signal) {
|
|
197
|
+
return this.resolveProvider().pdf(session, request, signal);
|
|
198
|
+
}
|
|
193
199
|
async waitForElement(session, request, signal) {
|
|
194
200
|
return this.resolveProvider().waitForElement(session, request, signal);
|
|
195
201
|
}
|
|
@@ -281,6 +287,28 @@ export class BrowserRuntime extends Service {
|
|
|
281
287
|
async setHandoff(session, state) {
|
|
282
288
|
return this.resolveProvider().setHandoff(session, state);
|
|
283
289
|
}
|
|
290
|
+
/**
|
|
291
|
+
* Bring the shared browser window to the front through the selected
|
|
292
|
+
* provider, opening it when nothing is open yet.
|
|
293
|
+
*/
|
|
294
|
+
async ensureWindowVisible() {
|
|
295
|
+
const provider = this.resolveProvider();
|
|
296
|
+
if (provider.ensureWindowVisible === undefined) {
|
|
297
|
+
throw new BrowserError('browser: this provider has no window to open', 'BROWSER_WINDOW_UNSUPPORTED');
|
|
298
|
+
}
|
|
299
|
+
return provider.ensureWindowVisible();
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Mirror one task's Agent todo list into the shared window (the floating orb
|
|
303
|
+
* reads it there). A provider without a window has nowhere to put it, so this
|
|
304
|
+
* is a no-op rather than an error.
|
|
305
|
+
*/
|
|
306
|
+
async pushTaskTodos(taskKey, todos) {
|
|
307
|
+
const provider = this.resolveProvider();
|
|
308
|
+
if (provider.pushTaskTodos === undefined)
|
|
309
|
+
return;
|
|
310
|
+
return provider.pushTaskTodos(taskKey, todos);
|
|
311
|
+
}
|
|
284
312
|
/** Close the session through the selected provider. Idempotent; a missing
|
|
285
313
|
* provider is treated as already-closed so teardown paths stay no-ops. */
|
|
286
314
|
async close(session) {
|
package/lib/browser/types.d.ts
CHANGED
|
@@ -131,24 +131,79 @@ export interface BrowserUploadFileResult {
|
|
|
131
131
|
/** The path uploaded. */
|
|
132
132
|
readonly path: string;
|
|
133
133
|
}
|
|
134
|
-
/**
|
|
134
|
+
/** Which state a wait is watching for. */
|
|
135
|
+
export type BrowserWaitForState = 'visible' | 'attached' | 'hidden' | 'detached';
|
|
136
|
+
/** Wait for a selector, some text, or both to reach a state. */
|
|
135
137
|
export interface BrowserWaitForRequest {
|
|
136
|
-
/** CSS selector to wait for. */
|
|
137
|
-
readonly selector
|
|
138
|
+
/** CSS selector to wait for. Omit to watch the document's own text (give `text`). */
|
|
139
|
+
readonly selector?: string;
|
|
140
|
+
/** Text that must be present: inside the matched element when a selector is given, otherwise anywhere in the document. */
|
|
141
|
+
readonly text?: string;
|
|
142
|
+
/**
|
|
143
|
+
* The state to wait for. `visible` (the default) needs a matching element at least 4x4 px and not
|
|
144
|
+
* visibility:hidden / display:none; `attached` only needs it to exist; `hidden` and `detached`
|
|
145
|
+
* wait for the opposite, so they are how you wait for something to go away.
|
|
146
|
+
*/
|
|
147
|
+
readonly state?: BrowserWaitForState;
|
|
138
148
|
/** Total budget in ms. Default 15000. */
|
|
139
149
|
readonly timeoutMs?: number;
|
|
140
|
-
/**
|
|
150
|
+
/** Back-compat for `state: 'attached'` (`visible: false`). Prefer `state`. */
|
|
141
151
|
readonly visible?: boolean;
|
|
142
152
|
}
|
|
143
153
|
/** Outcome of a successful wait. */
|
|
144
154
|
export interface BrowserWaitForResult {
|
|
155
|
+
/** The awaited condition is now true. */
|
|
145
156
|
readonly found: true;
|
|
157
|
+
/** Which state was awaited. */
|
|
158
|
+
readonly state: BrowserWaitForState;
|
|
159
|
+
/** The selector waited on, or `''` for a text-only wait. */
|
|
146
160
|
readonly selector: string;
|
|
147
|
-
/**
|
|
161
|
+
/** Matched element's tag name, or `''` when nothing matched (a `detached` wait). */
|
|
148
162
|
readonly tag: string;
|
|
149
|
-
/**
|
|
163
|
+
/** Matched element's visible text (first 200 chars), or the document text that satisfied a text wait. */
|
|
150
164
|
readonly text: string;
|
|
151
165
|
}
|
|
166
|
+
/** Print the active tab to a PDF file. */
|
|
167
|
+
export interface BrowserPdfRequest {
|
|
168
|
+
/** Absolute path of the .pdf to write. Must be inside the write roots. */
|
|
169
|
+
readonly savePath: string;
|
|
170
|
+
/** Landscape orientation. Default portrait. */
|
|
171
|
+
readonly landscape?: boolean;
|
|
172
|
+
/** Include background colours and images. Default true. */
|
|
173
|
+
readonly printBackground?: boolean;
|
|
174
|
+
/** Paper width in inches. CDP default is 8.5. */
|
|
175
|
+
readonly paperWidth?: number;
|
|
176
|
+
/** Paper height in inches. CDP default is 11. */
|
|
177
|
+
readonly paperHeight?: number;
|
|
178
|
+
}
|
|
179
|
+
/** Where the PDF landed. */
|
|
180
|
+
export interface BrowserPdfResult {
|
|
181
|
+
readonly path: string;
|
|
182
|
+
readonly bytes: number;
|
|
183
|
+
}
|
|
184
|
+
/** Draw or clear the DevTools-style highlight box over a selector's first match. */
|
|
185
|
+
export interface BrowserHighlightRequest {
|
|
186
|
+
/** CSS selector whose first match to highlight. Ignored when clear is true. */
|
|
187
|
+
readonly selector?: string;
|
|
188
|
+
/** Remove any existing highlight instead of drawing one. */
|
|
189
|
+
readonly clear?: boolean;
|
|
190
|
+
}
|
|
191
|
+
/** What the highlight call found. */
|
|
192
|
+
export interface BrowserHighlightResult {
|
|
193
|
+
/** A match was found and highlighted. */
|
|
194
|
+
readonly matched: boolean;
|
|
195
|
+
/** The highlight was cleared. */
|
|
196
|
+
readonly cleared: boolean;
|
|
197
|
+
/** CDP node id of the match, when there was one. */
|
|
198
|
+
readonly nodeId?: number;
|
|
199
|
+
/** Content box in CSS pixels, when the element has one. */
|
|
200
|
+
readonly box?: {
|
|
201
|
+
readonly x: number;
|
|
202
|
+
readonly y: number;
|
|
203
|
+
readonly width: number;
|
|
204
|
+
readonly height: number;
|
|
205
|
+
};
|
|
206
|
+
}
|
|
152
207
|
/** One field of a batch form fill. Match by selector, or by name/label/placeholder. */
|
|
153
208
|
export interface BrowserFillField {
|
|
154
209
|
/** CSS selector; when present, candidates are scoped to it. */
|
|
@@ -326,6 +381,17 @@ export interface BrowserSnapshotElement {
|
|
|
326
381
|
/** Viewport-relative center, for coordinate fallbacks. */
|
|
327
382
|
readonly x: number;
|
|
328
383
|
readonly y: number;
|
|
384
|
+
/** Index into `frames` when the element lives inside an iframe; absent at the top level. */
|
|
385
|
+
readonly frame?: number;
|
|
386
|
+
}
|
|
387
|
+
/** One iframe found on the page. */
|
|
388
|
+
export interface BrowserSnapshotFrame {
|
|
389
|
+
/** Index into the page's iframe list; matches `BrowserSnapshotElement.frame`. */
|
|
390
|
+
readonly index: number;
|
|
391
|
+
/** The frame's src, or its document URL when it has one. */
|
|
392
|
+
readonly url: string;
|
|
393
|
+
/** False when the frame is cross-origin, so its contents cannot be read. */
|
|
394
|
+
readonly readable: boolean;
|
|
329
395
|
}
|
|
330
396
|
/**
|
|
331
397
|
* AI-friendly page snapshot: a compact, numbered inventory of interactive
|
|
@@ -342,6 +408,8 @@ export interface BrowserSnapshotResult {
|
|
|
342
408
|
readonly elements: readonly BrowserSnapshotElement[];
|
|
343
409
|
/** True when the snapshot was truncated (element cap reached). */
|
|
344
410
|
readonly truncated: boolean;
|
|
411
|
+
/** Every iframe on the page, including the cross-origin ones that could not be read. */
|
|
412
|
+
readonly frames?: readonly BrowserSnapshotFrame[];
|
|
345
413
|
/** Human-verification challenge blocking the page, when one is detected. */
|
|
346
414
|
readonly challenge?: BrowserChallenge;
|
|
347
415
|
/** True when a human interacted with the page within the last minute. */
|
|
@@ -588,6 +656,10 @@ export interface BrowserProvider {
|
|
|
588
656
|
uploadFile(session: BrowserSessionId, request: BrowserUploadFileRequest, signal?: AbortSignal): Promise<BrowserUploadFileResult>;
|
|
589
657
|
/** Wait until an element matching the selector exists (and optionally is visible). Honor `signal` for cancellation. */
|
|
590
658
|
waitForElement(session: BrowserSessionId, request: BrowserWaitForRequest, signal?: AbortSignal): Promise<BrowserWaitForResult>;
|
|
659
|
+
/** Print the active tab to a PDF file. */
|
|
660
|
+
pdf(session: BrowserSessionId, request: BrowserPdfRequest, signal?: AbortSignal): Promise<BrowserPdfResult>;
|
|
661
|
+
/** Draw or clear the DevTools-style highlight box over a selector's first match. */
|
|
662
|
+
highlight(session: BrowserSessionId, request: BrowserHighlightRequest, signal?: AbortSignal): Promise<BrowserHighlightResult>;
|
|
591
663
|
/** Fill a form's fields in one batch. Honor `signal` for cancellation. */
|
|
592
664
|
fillForm(session: BrowserSessionId, request: BrowserFillRequest, signal?: AbortSignal): Promise<BrowserFillResult>;
|
|
593
665
|
/** Capture the current page. Honor `signal` for cancellation. */
|
|
@@ -636,6 +708,24 @@ export interface BrowserProvider {
|
|
|
636
708
|
setHandoff(session: BrowserSessionId, state: BrowserHandoffState): Promise<BrowserTaskInfo>;
|
|
637
709
|
/** Close the session and destroy its backing surface. Idempotent. */
|
|
638
710
|
close(session: BrowserSessionId): Promise<void>;
|
|
711
|
+
/**
|
|
712
|
+
* Bring the shared browser window to the front, creating it (and a default
|
|
713
|
+
* session) when nothing is open yet. Optional: a provider whose browser has
|
|
714
|
+
* no window of its own omits it, and callers must report that as unsupported
|
|
715
|
+
* rather than pretending a window was raised.
|
|
716
|
+
*/
|
|
717
|
+
ensureWindowVisible?(): Promise<void>;
|
|
718
|
+
/**
|
|
719
|
+
* Mirror one task's Agent todo list into the shared window, where the floating
|
|
720
|
+
* orb renders it. Optional: a provider without a window of its own omits it.
|
|
721
|
+
* `taskKey` is the same task key the tools use (the calling Agent's id).
|
|
722
|
+
*/
|
|
723
|
+
pushTaskTodos?(taskKey: string, todos: readonly BrowserTaskTodo[]): Promise<void>;
|
|
724
|
+
}
|
|
725
|
+
/** One entry of an Agent's todo list, as the floating orb shows it. */
|
|
726
|
+
export interface BrowserTaskTodo {
|
|
727
|
+
readonly content: string;
|
|
728
|
+
readonly status: 'pending' | 'in_progress' | 'completed';
|
|
639
729
|
}
|
|
640
730
|
/** One recorded browser operation, in chronological order (seq 1, 2, 3…). */
|
|
641
731
|
export interface BrowserHistoryEntry {
|
|
@@ -118,10 +118,24 @@ export interface ChromeWorkspaceState {
|
|
|
118
118
|
* rounds before this made it visible.
|
|
119
119
|
*/
|
|
120
120
|
readonly windowProbe?: string;
|
|
121
|
+
/** Where the human dragged the floating orb, in page CSS pixels. */
|
|
122
|
+
readonly orbPosition?: {
|
|
123
|
+
readonly x: number;
|
|
124
|
+
readonly y: number;
|
|
125
|
+
};
|
|
121
126
|
}
|
|
122
127
|
export interface ChromeBootstrapMessage extends ChromeWorkspaceState {
|
|
123
128
|
readonly kind: 'bootstrap';
|
|
124
129
|
}
|
|
130
|
+
/**
|
|
131
|
+
* One entry of the Agent's todo list (`todo_write`) as the floating orb renders
|
|
132
|
+
* it: the same three states the model writes, so the orb spins on
|
|
133
|
+
* `in_progress` and ticks `completed` without translating anything.
|
|
134
|
+
*/
|
|
135
|
+
export interface ChromeTaskTodo {
|
|
136
|
+
readonly content: string;
|
|
137
|
+
readonly status: 'pending' | 'in_progress' | 'completed';
|
|
138
|
+
}
|
|
125
139
|
export type ChromePatchOperation = {
|
|
126
140
|
readonly op: 'task.upsert';
|
|
127
141
|
readonly task: ChromeTaskSummary;
|
|
@@ -136,6 +150,16 @@ export type ChromePatchOperation = {
|
|
|
136
150
|
readonly key: string;
|
|
137
151
|
readonly version: number;
|
|
138
152
|
readonly dataUrl?: string;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* The Agent's todo list for one task. Like the thumbnail it is NOT part of a
|
|
156
|
+
* summary — summaries are injected into every visited page's main world, and
|
|
157
|
+
* this is the Agent's own plan — so it travels only as a targeted patch.
|
|
158
|
+
*/
|
|
159
|
+
| {
|
|
160
|
+
readonly op: 'task.todos';
|
|
161
|
+
readonly key: string;
|
|
162
|
+
readonly todos: readonly ChromeTaskTodo[];
|
|
139
163
|
} | {
|
|
140
164
|
readonly op: 'trail.append';
|
|
141
165
|
readonly taskKey: string;
|
|
@@ -38,11 +38,18 @@ export interface Config {
|
|
|
38
38
|
*/
|
|
39
39
|
readonly readRoots?: string[];
|
|
40
40
|
/**
|
|
41
|
-
* Which JavaScript world the injected page chrome lives in.
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
41
|
+
* Which JavaScript world the injected page chrome lives in.
|
|
42
|
+
*
|
|
43
|
+
* `isolated` (default) keeps the chrome's task state — including the Agent's
|
|
44
|
+
* plan, which the floating orb renders — and its binding token out of the
|
|
45
|
+
* page's own context: the page's scripts see `undefined` for every `__dsh*`
|
|
46
|
+
* global instead of a readable copy. It costs one extra CDP context per
|
|
47
|
+
* document, and the chrome reads the DOM through that context (the DOM itself
|
|
48
|
+
* is shared, so element lookups and layout still work).
|
|
49
|
+
*
|
|
50
|
+
* `main` puts everything in the page's world: the proven-against-everything
|
|
51
|
+
* path, but a page can read the plan by hooking `Map.prototype.set` or
|
|
52
|
+
* `document.createElement`. Use it only to bisect an isolated-world bug.
|
|
46
53
|
*/
|
|
47
54
|
readonly chromeWorld?: 'main' | 'isolated';
|
|
48
55
|
/**
|