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
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 真正驱动指针事件的悬浮球拖动测试(审查要的那条)。
|
|
3
|
+
*
|
|
4
|
+
* 单元测试只能断言「生成的脚本文本里有某个公式」,抓不到 H2 那类错误 ——
|
|
5
|
+
* 慢速拖动曾经因为阈值拿「上一帧已应用的位置」当基准,永远不算拖动:
|
|
6
|
+
* 位置不落盘、松手还会误触 click。这里用离屏 Electron 跑真脚本、派发真指针事件。
|
|
7
|
+
*
|
|
8
|
+
* node scripts/test-orb-drag.mjs
|
|
9
|
+
*/
|
|
10
|
+
import { spawnSync } from 'node:child_process'
|
|
11
|
+
import { mkdtempSync, readFileSync, writeFileSync } from 'node:fs'
|
|
12
|
+
import { tmpdir } from 'node:os'
|
|
13
|
+
import { join } from 'node:path'
|
|
14
|
+
import { createRequire } from 'node:module'
|
|
15
|
+
|
|
16
|
+
const require = createRequire(import.meta.url)
|
|
17
|
+
const chrome = require('../lib/browser-electron/page-chrome.js')
|
|
18
|
+
const dir = mkdtempSync(join(tmpdir(), 'orb-drag-'))
|
|
19
|
+
const W = 620
|
|
20
|
+
const H = 480
|
|
21
|
+
const CENTER = { x: W - 18 - 22, y: H - 18 - 22 }
|
|
22
|
+
|
|
23
|
+
const probe = [
|
|
24
|
+
'window.__moves = [];',
|
|
25
|
+
'window.__seen = [];',
|
|
26
|
+
'window.addEventListener("pointerdown", e => window.__seen.push(["down", e.buttons, Math.round(e.clientX)]), true);',
|
|
27
|
+
'window.addEventListener("pointermove", e => window.__seen.push(["move", e.buttons, Math.round(e.clientX)]), true);',
|
|
28
|
+
'window.__dshChromeToken = "t";',
|
|
29
|
+
'window.__dshChromeTaskKey = "default";',
|
|
30
|
+
'window.__dshBrowserTaskAction = function (payload) { try { const a = JSON.parse(payload); if (a && a.type === "orb-move") window.__moves.push(a) } catch {} };',
|
|
31
|
+
'window.__dshChromeBootstrap = ' + JSON.stringify({
|
|
32
|
+
kind: 'bootstrap', epoch: 1, revision: 1, selectedTaskKey: 'default',
|
|
33
|
+
panels: { tasks: false, trail: false },
|
|
34
|
+
tasks: [{ key: 'default', label: '', active: true, tabs: 1, status: 'running', updatedAt: Date.now() }],
|
|
35
|
+
tabs: [], trail: [], bookmarks: [], bookmarkBar: false,
|
|
36
|
+
}) + ';',
|
|
37
|
+
].join('')
|
|
38
|
+
|
|
39
|
+
const html = '<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;background:#202124}</style></head><body>'
|
|
40
|
+
+ '<scr' + 'ipt>' + probe + '</scr' + 'ipt>'
|
|
41
|
+
+ '<scr' + 'ipt>' + chrome.buildPageChromeScript('t', 'page') + '</scr' + 'ipt>'
|
|
42
|
+
+ '</body></html>'
|
|
43
|
+
|
|
44
|
+
const driver = `
|
|
45
|
+
const { app, BrowserWindow } = require('electron')
|
|
46
|
+
const fs = require('fs')
|
|
47
|
+
const file = process.argv[2]
|
|
48
|
+
const out = process.argv[3]
|
|
49
|
+
const center = JSON.parse(process.argv[4])
|
|
50
|
+
app.whenReady().then(async () => {
|
|
51
|
+
const win = new BrowserWindow({ width: ${W}, height: ${H}, show: false, webPreferences: { offscreen: true } })
|
|
52
|
+
await win.loadFile(file)
|
|
53
|
+
await new Promise(r => setTimeout(r, 700))
|
|
54
|
+
const send = (type, x, y, buttons) => win.webContents.sendInputEvent({ type, x: Math.round(x), y: Math.round(y), button: 'left', buttons, clickCount: 1 })
|
|
55
|
+
send('mouseDown', center.x, center.y, 1)
|
|
56
|
+
await new Promise(r => setTimeout(r, 80))
|
|
57
|
+
// 每步之间等一帧多一点:Chromium 会把挨得太近的 pointermove 合并掉,
|
|
58
|
+
// 合并之后「每帧位移」就超过阈值了,测不出 H2 那种慢速拖动。
|
|
59
|
+
for (let i = 1; i <= 60; i++) { send('mouseMove', center.x - i * 2, center.y - i, 1); await new Promise(r => setTimeout(r, 25)) }
|
|
60
|
+
send('mouseUp', center.x - 120, center.y - 60, 0)
|
|
61
|
+
await new Promise(r => setTimeout(r, 250))
|
|
62
|
+
fs.writeFileSync(out, JSON.stringify({
|
|
63
|
+
moves: await win.webContents.executeJavaScript('window.__moves'),
|
|
64
|
+
seen: await win.webContents.executeJavaScript('window.__seen.slice(0, 3)'),
|
|
65
|
+
total: await win.webContents.executeJavaScript('window.__seen.length'),
|
|
66
|
+
}))
|
|
67
|
+
console.log('DRIVER_DONE')
|
|
68
|
+
app.quit()
|
|
69
|
+
})
|
|
70
|
+
`
|
|
71
|
+
writeFileSync(join(dir, 'page.html'), html)
|
|
72
|
+
writeFileSync(join(dir, 'drive.cjs'), driver)
|
|
73
|
+
const out = join(dir, 'result.json')
|
|
74
|
+
const run = spawnSync(process.execPath, [require.resolve('electron/cli.js'), join(dir, 'drive.cjs'), join(dir, 'page.html'), out, JSON.stringify(CENTER)], { encoding: 'utf8' })
|
|
75
|
+
const text = String(run.stdout ?? '') + String(run.stderr ?? '')
|
|
76
|
+
if (!text.includes('DRIVER_DONE')) { console.error('driver did not finish:\n' + text.slice(-900)); process.exit(1) }
|
|
77
|
+
const result = JSON.parse(readFileSync(out, 'utf8'))
|
|
78
|
+
console.log('page saw ' + result.total + ' pointer events; first 3: ' + JSON.stringify(result.seen))
|
|
79
|
+
const moves = result.moves
|
|
80
|
+
if (moves.length === 0) { console.error('FAIL: a slow drag produced no orb-move — the position would not be persisted'); process.exit(1) }
|
|
81
|
+
const last = moves[moves.length - 1]
|
|
82
|
+
if (!(last.x < CENTER.x) || !(last.y < CENTER.y)) { console.error('FAIL: the orb did not move up-left: ' + JSON.stringify(last)); process.exit(1) }
|
|
83
|
+
console.log('OK: slow drag persisted ' + moves.length + ' move(s), last ' + last.x + ',' + last.y)
|
package/src/browser/runtime.ts
CHANGED
|
@@ -47,9 +47,14 @@ import type {
|
|
|
47
47
|
BrowserTypeRequest,
|
|
48
48
|
BrowserUploadFileRequest,
|
|
49
49
|
BrowserUploadFileResult,
|
|
50
|
+
BrowserPdfRequest,
|
|
51
|
+
BrowserPdfResult,
|
|
52
|
+
BrowserHighlightRequest,
|
|
53
|
+
BrowserHighlightResult,
|
|
50
54
|
BrowserWaitForRequest,
|
|
51
55
|
BrowserWaitForResult,
|
|
52
56
|
BrowserChallenge,
|
|
57
|
+
BrowserTaskTodo,
|
|
53
58
|
ExportedCookie,
|
|
54
59
|
} from './types.ts'
|
|
55
60
|
import { BrowserError } from './types.ts'
|
|
@@ -334,6 +339,14 @@ export class BrowserRuntime extends Service {
|
|
|
334
339
|
}
|
|
335
340
|
|
|
336
341
|
/** Wait for an element through the selected provider (bounded polling). */
|
|
342
|
+
async highlight(session: BrowserSessionId, request: BrowserHighlightRequest, signal?: AbortSignal): Promise<BrowserHighlightResult> {
|
|
343
|
+
return this.resolveProvider().highlight(session, request, signal)
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async pdf(session: BrowserSessionId, request: BrowserPdfRequest, signal?: AbortSignal): Promise<BrowserPdfResult> {
|
|
347
|
+
return this.resolveProvider().pdf(session, request, signal)
|
|
348
|
+
}
|
|
349
|
+
|
|
337
350
|
async waitForElement(session: BrowserSessionId, request: BrowserWaitForRequest, signal?: AbortSignal): Promise<BrowserWaitForResult> {
|
|
338
351
|
return this.resolveProvider().waitForElement(session, request, signal)
|
|
339
352
|
}
|
|
@@ -448,6 +461,29 @@ export class BrowserRuntime extends Service {
|
|
|
448
461
|
return this.resolveProvider().setHandoff(session, state)
|
|
449
462
|
}
|
|
450
463
|
|
|
464
|
+
/**
|
|
465
|
+
* Bring the shared browser window to the front through the selected
|
|
466
|
+
* provider, opening it when nothing is open yet.
|
|
467
|
+
*/
|
|
468
|
+
async ensureWindowVisible(): Promise<void> {
|
|
469
|
+
const provider = this.resolveProvider()
|
|
470
|
+
if (provider.ensureWindowVisible === undefined) {
|
|
471
|
+
throw new BrowserError('browser: this provider has no window to open', 'BROWSER_WINDOW_UNSUPPORTED')
|
|
472
|
+
}
|
|
473
|
+
return provider.ensureWindowVisible()
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Mirror one task's Agent todo list into the shared window (the floating orb
|
|
478
|
+
* reads it there). A provider without a window has nowhere to put it, so this
|
|
479
|
+
* is a no-op rather than an error.
|
|
480
|
+
*/
|
|
481
|
+
async pushTaskTodos(taskKey: string, todos: readonly BrowserTaskTodo[]): Promise<void> {
|
|
482
|
+
const provider = this.resolveProvider()
|
|
483
|
+
if (provider.pushTaskTodos === undefined) return
|
|
484
|
+
return provider.pushTaskTodos(taskKey, todos)
|
|
485
|
+
}
|
|
486
|
+
|
|
451
487
|
/** Close the session through the selected provider. Idempotent; a missing
|
|
452
488
|
* provider is treated as already-closed so teardown paths stay no-ops. */
|
|
453
489
|
async close(session: BrowserSessionId): Promise<void> {
|
package/src/browser/types.ts
CHANGED
|
@@ -147,26 +147,81 @@ export interface BrowserUploadFileResult {
|
|
|
147
147
|
readonly path: string
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
-
/**
|
|
150
|
+
/** Which state a wait is watching for. */
|
|
151
|
+
export type BrowserWaitForState = 'visible' | 'attached' | 'hidden' | 'detached'
|
|
152
|
+
|
|
153
|
+
/** Wait for a selector, some text, or both to reach a state. */
|
|
151
154
|
export interface BrowserWaitForRequest {
|
|
152
|
-
/** CSS selector to wait for. */
|
|
153
|
-
readonly selector
|
|
155
|
+
/** CSS selector to wait for. Omit to watch the document's own text (give `text`). */
|
|
156
|
+
readonly selector?: string
|
|
157
|
+
/** Text that must be present: inside the matched element when a selector is given, otherwise anywhere in the document. */
|
|
158
|
+
readonly text?: string
|
|
159
|
+
/**
|
|
160
|
+
* The state to wait for. `visible` (the default) needs a matching element at least 4x4 px and not
|
|
161
|
+
* visibility:hidden / display:none; `attached` only needs it to exist; `hidden` and `detached`
|
|
162
|
+
* wait for the opposite, so they are how you wait for something to go away.
|
|
163
|
+
*/
|
|
164
|
+
readonly state?: BrowserWaitForState
|
|
154
165
|
/** Total budget in ms. Default 15000. */
|
|
155
166
|
readonly timeoutMs?: number
|
|
156
|
-
/**
|
|
167
|
+
/** Back-compat for `state: 'attached'` (`visible: false`). Prefer `state`. */
|
|
157
168
|
readonly visible?: boolean
|
|
158
169
|
}
|
|
159
170
|
|
|
160
171
|
/** Outcome of a successful wait. */
|
|
161
172
|
export interface BrowserWaitForResult {
|
|
173
|
+
/** The awaited condition is now true. */
|
|
162
174
|
readonly found: true
|
|
175
|
+
/** Which state was awaited. */
|
|
176
|
+
readonly state: BrowserWaitForState
|
|
177
|
+
/** The selector waited on, or `''` for a text-only wait. */
|
|
163
178
|
readonly selector: string
|
|
164
|
-
/**
|
|
179
|
+
/** Matched element's tag name, or `''` when nothing matched (a `detached` wait). */
|
|
165
180
|
readonly tag: string
|
|
166
|
-
/**
|
|
181
|
+
/** Matched element's visible text (first 200 chars), or the document text that satisfied a text wait. */
|
|
167
182
|
readonly text: string
|
|
168
183
|
}
|
|
169
184
|
|
|
185
|
+
/** Print the active tab to a PDF file. */
|
|
186
|
+
export interface BrowserPdfRequest {
|
|
187
|
+
/** Absolute path of the .pdf to write. Must be inside the write roots. */
|
|
188
|
+
readonly savePath: string
|
|
189
|
+
/** Landscape orientation. Default portrait. */
|
|
190
|
+
readonly landscape?: boolean
|
|
191
|
+
/** Include background colours and images. Default true. */
|
|
192
|
+
readonly printBackground?: boolean
|
|
193
|
+
/** Paper width in inches. CDP default is 8.5. */
|
|
194
|
+
readonly paperWidth?: number
|
|
195
|
+
/** Paper height in inches. CDP default is 11. */
|
|
196
|
+
readonly paperHeight?: number
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Where the PDF landed. */
|
|
200
|
+
export interface BrowserPdfResult {
|
|
201
|
+
readonly path: string
|
|
202
|
+
readonly bytes: number
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Draw or clear the DevTools-style highlight box over a selector's first match. */
|
|
206
|
+
export interface BrowserHighlightRequest {
|
|
207
|
+
/** CSS selector whose first match to highlight. Ignored when clear is true. */
|
|
208
|
+
readonly selector?: string
|
|
209
|
+
/** Remove any existing highlight instead of drawing one. */
|
|
210
|
+
readonly clear?: boolean
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** What the highlight call found. */
|
|
214
|
+
export interface BrowserHighlightResult {
|
|
215
|
+
/** A match was found and highlighted. */
|
|
216
|
+
readonly matched: boolean
|
|
217
|
+
/** The highlight was cleared. */
|
|
218
|
+
readonly cleared: boolean
|
|
219
|
+
/** CDP node id of the match, when there was one. */
|
|
220
|
+
readonly nodeId?: number
|
|
221
|
+
/** Content box in CSS pixels, when the element has one. */
|
|
222
|
+
readonly box?: { readonly x: number; readonly y: number; readonly width: number; readonly height: number }
|
|
223
|
+
}
|
|
224
|
+
|
|
170
225
|
/** One field of a batch form fill. Match by selector, or by name/label/placeholder. */
|
|
171
226
|
export interface BrowserFillField {
|
|
172
227
|
/** CSS selector; when present, candidates are scoped to it. */
|
|
@@ -354,6 +409,18 @@ export interface BrowserSnapshotElement {
|
|
|
354
409
|
/** Viewport-relative center, for coordinate fallbacks. */
|
|
355
410
|
readonly x: number
|
|
356
411
|
readonly y: number
|
|
412
|
+
/** Index into `frames` when the element lives inside an iframe; absent at the top level. */
|
|
413
|
+
readonly frame?: number
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/** One iframe found on the page. */
|
|
417
|
+
export interface BrowserSnapshotFrame {
|
|
418
|
+
/** Index into the page's iframe list; matches `BrowserSnapshotElement.frame`. */
|
|
419
|
+
readonly index: number
|
|
420
|
+
/** The frame's src, or its document URL when it has one. */
|
|
421
|
+
readonly url: string
|
|
422
|
+
/** False when the frame is cross-origin, so its contents cannot be read. */
|
|
423
|
+
readonly readable: boolean
|
|
357
424
|
}
|
|
358
425
|
|
|
359
426
|
/**
|
|
@@ -371,6 +438,8 @@ export interface BrowserSnapshotResult {
|
|
|
371
438
|
readonly elements: readonly BrowserSnapshotElement[]
|
|
372
439
|
/** True when the snapshot was truncated (element cap reached). */
|
|
373
440
|
readonly truncated: boolean
|
|
441
|
+
/** Every iframe on the page, including the cross-origin ones that could not be read. */
|
|
442
|
+
readonly frames?: readonly BrowserSnapshotFrame[]
|
|
374
443
|
/** Human-verification challenge blocking the page, when one is detected. */
|
|
375
444
|
readonly challenge?: BrowserChallenge
|
|
376
445
|
/** True when a human interacted with the page within the last minute. */
|
|
@@ -571,6 +640,10 @@ export interface BrowserProvider {
|
|
|
571
640
|
uploadFile(session: BrowserSessionId, request: BrowserUploadFileRequest, signal?: AbortSignal): Promise<BrowserUploadFileResult>
|
|
572
641
|
/** Wait until an element matching the selector exists (and optionally is visible). Honor `signal` for cancellation. */
|
|
573
642
|
waitForElement(session: BrowserSessionId, request: BrowserWaitForRequest, signal?: AbortSignal): Promise<BrowserWaitForResult>
|
|
643
|
+
/** Print the active tab to a PDF file. */
|
|
644
|
+
pdf(session: BrowserSessionId, request: BrowserPdfRequest, signal?: AbortSignal): Promise<BrowserPdfResult>
|
|
645
|
+
/** Draw or clear the DevTools-style highlight box over a selector's first match. */
|
|
646
|
+
highlight(session: BrowserSessionId, request: BrowserHighlightRequest, signal?: AbortSignal): Promise<BrowserHighlightResult>
|
|
574
647
|
/** Fill a form's fields in one batch. Honor `signal` for cancellation. */
|
|
575
648
|
fillForm(session: BrowserSessionId, request: BrowserFillRequest, signal?: AbortSignal): Promise<BrowserFillResult>
|
|
576
649
|
/** Capture the current page. Honor `signal` for cancellation. */
|
|
@@ -615,6 +688,25 @@ export interface BrowserProvider {
|
|
|
615
688
|
setHandoff(session: BrowserSessionId, state: BrowserHandoffState): Promise<BrowserTaskInfo>
|
|
616
689
|
/** Close the session and destroy its backing surface. Idempotent. */
|
|
617
690
|
close(session: BrowserSessionId): Promise<void>
|
|
691
|
+
/**
|
|
692
|
+
* Bring the shared browser window to the front, creating it (and a default
|
|
693
|
+
* session) when nothing is open yet. Optional: a provider whose browser has
|
|
694
|
+
* no window of its own omits it, and callers must report that as unsupported
|
|
695
|
+
* rather than pretending a window was raised.
|
|
696
|
+
*/
|
|
697
|
+
ensureWindowVisible?(): Promise<void>
|
|
698
|
+
/**
|
|
699
|
+
* Mirror one task's Agent todo list into the shared window, where the floating
|
|
700
|
+
* orb renders it. Optional: a provider without a window of its own omits it.
|
|
701
|
+
* `taskKey` is the same task key the tools use (the calling Agent's id).
|
|
702
|
+
*/
|
|
703
|
+
pushTaskTodos?(taskKey: string, todos: readonly BrowserTaskTodo[]): Promise<void>
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/** One entry of an Agent's todo list, as the floating orb shows it. */
|
|
707
|
+
export interface BrowserTaskTodo {
|
|
708
|
+
readonly content: string
|
|
709
|
+
readonly status: 'pending' | 'in_progress' | 'completed'
|
|
618
710
|
}
|
|
619
711
|
|
|
620
712
|
/** One recorded browser operation, in chronological order (seq 1, 2, 3…). */
|
|
@@ -125,17 +125,35 @@ export interface ChromeWorkspaceState {
|
|
|
125
125
|
* rounds before this made it visible.
|
|
126
126
|
*/
|
|
127
127
|
readonly windowProbe?: string
|
|
128
|
+
/** Where the human dragged the floating orb, in page CSS pixels. */
|
|
129
|
+
readonly orbPosition?: { readonly x: number; readonly y: number }
|
|
128
130
|
}
|
|
129
131
|
|
|
130
132
|
export interface ChromeBootstrapMessage extends ChromeWorkspaceState {
|
|
131
133
|
readonly kind: 'bootstrap'
|
|
132
134
|
}
|
|
133
135
|
|
|
136
|
+
/**
|
|
137
|
+
* One entry of the Agent's todo list (`todo_write`) as the floating orb renders
|
|
138
|
+
* it: the same three states the model writes, so the orb spins on
|
|
139
|
+
* `in_progress` and ticks `completed` without translating anything.
|
|
140
|
+
*/
|
|
141
|
+
export interface ChromeTaskTodo {
|
|
142
|
+
readonly content: string
|
|
143
|
+
readonly status: 'pending' | 'in_progress' | 'completed'
|
|
144
|
+
}
|
|
145
|
+
|
|
134
146
|
export type ChromePatchOperation =
|
|
135
147
|
| { readonly op: 'task.upsert'; readonly task: ChromeTaskSummary }
|
|
136
148
|
| { readonly op: 'task.remove'; readonly key: string }
|
|
137
149
|
| { readonly op: 'task.active'; readonly key?: string }
|
|
138
150
|
| { readonly op: 'task.thumbnail'; readonly key: string; readonly version: number; readonly dataUrl?: string }
|
|
151
|
+
/**
|
|
152
|
+
* The Agent's todo list for one task. Like the thumbnail it is NOT part of a
|
|
153
|
+
* summary — summaries are injected into every visited page's main world, and
|
|
154
|
+
* this is the Agent's own plan — so it travels only as a targeted patch.
|
|
155
|
+
*/
|
|
156
|
+
| { readonly op: 'task.todos'; readonly key: string; readonly todos: readonly ChromeTaskTodo[] }
|
|
139
157
|
| { readonly op: 'trail.append'; readonly taskKey: string; readonly entry: ChromeTrailEntry }
|
|
140
158
|
| { readonly op: 'trail.replace'; readonly taskKey?: string; readonly entries: readonly ChromeTrailEntry[] }
|
|
141
159
|
| { readonly op: 'panels.set'; readonly panels: ChromePanels }
|
|
@@ -49,11 +49,18 @@ export interface Config {
|
|
|
49
49
|
*/
|
|
50
50
|
readonly readRoots?: string[]
|
|
51
51
|
/**
|
|
52
|
-
* Which JavaScript world the injected page chrome lives in.
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
52
|
+
* Which JavaScript world the injected page chrome lives in.
|
|
53
|
+
*
|
|
54
|
+
* `isolated` (default) keeps the chrome's task state — including the Agent's
|
|
55
|
+
* plan, which the floating orb renders — and its binding token out of the
|
|
56
|
+
* page's own context: the page's scripts see `undefined` for every `__dsh*`
|
|
57
|
+
* global instead of a readable copy. It costs one extra CDP context per
|
|
58
|
+
* document, and the chrome reads the DOM through that context (the DOM itself
|
|
59
|
+
* is shared, so element lookups and layout still work).
|
|
60
|
+
*
|
|
61
|
+
* `main` puts everything in the page's world: the proven-against-everything
|
|
62
|
+
* path, but a page can read the plan by hooking `Map.prototype.set` or
|
|
63
|
+
* `document.createElement`. Use it only to bisect an isolated-world bug.
|
|
57
64
|
*/
|
|
58
65
|
readonly chromeWorld?: 'main' | 'isolated'
|
|
59
66
|
/**
|
|
@@ -82,7 +89,7 @@ export const Config: z<Config> = z.object({
|
|
|
82
89
|
// meaning "the documented defaults" while an explicit [] still denies all.
|
|
83
90
|
writeRoots: z.array(z.string()).default(defaultWriteRoots()),
|
|
84
91
|
readRoots: z.array(z.string()).default(defaultWriteRoots()),
|
|
85
|
-
chromeWorld: z.union(['main', 'isolated'] as const).default('
|
|
92
|
+
chromeWorld: z.union(['main', 'isolated'] as const).default('isolated'),
|
|
86
93
|
userAgent: z.string(),
|
|
87
94
|
maskAutomation: z.boolean().default(true),
|
|
88
95
|
})
|
|
@@ -93,7 +100,10 @@ export function apply(ctx: Context & { browser: BrowserRuntime }, config: Config
|
|
|
93
100
|
// child is disposed with the fiber, mirroring the shell's lifetime.
|
|
94
101
|
const host: ElectronBrowserViewHost = config.viewHost
|
|
95
102
|
?? new RemoteElectronViewHost(defaultHostMainPath(), {
|
|
96
|
-
|
|
103
|
+
// `?? 'isolated'` 不只是为了类型:入口可能拿到**未经 schema 解析**的配置
|
|
104
|
+
// (测试探针、其它调用方直接调 apply),那时缺省值不会自动出现,
|
|
105
|
+
// 而 remote-host 把 undefined 当成「不加 --chrome-world」→ 子进程退回 main ✗。
|
|
106
|
+
chromeWorld: config.chromeWorld ?? 'isolated',
|
|
97
107
|
maskAutomation: config.maskAutomation,
|
|
98
108
|
...config.userAgent === undefined ? {} : { userAgent: config.userAgent },
|
|
99
109
|
})
|