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
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
* <- { id, op: 'ping' } | { id, op: 'createView', viewId, key?, label? } |
|
|
10
10
|
* { id, op: 'destroyView', viewId } | { id, op: 'showView', viewId } |
|
|
11
11
|
* { id, op: 'label', viewId, label } | { id, op: 'listWindows' } |
|
|
12
|
-
* { id, op: 'command', viewId, method, params }
|
|
12
|
+
* { id, op: 'command', viewId, method, params } |
|
|
13
|
+
* { id, op: 'printToPdf', viewId, options }
|
|
13
14
|
* -> { id, ok: true, result? } | { id, ok: false, err }
|
|
14
15
|
*
|
|
15
16
|
* The parent never parses stderr, so diagnostics may go there freely.
|
|
@@ -28,7 +29,7 @@ import { taskSummaryUrl } from './task-summary.js'
|
|
|
28
29
|
import { taskThumbnailDataUrl, type ThumbnailImage } from './task-thumbnail.js'
|
|
29
30
|
import { exportCookiesForAuth, selectCookiesForClear } from './auth-cookies.js'
|
|
30
31
|
import { resolveBrowserIconPath } from './icon.js'
|
|
31
|
-
import { createBootstrap, createPatch, type ChromeBookmark, type ChromePatchOperation, type ChromeTabSummary, type ChromeTaskSummary, type ChromeTrailEntry, type ChromeWorkspaceState } from './chrome-state.js'
|
|
32
|
+
import { createBootstrap, createPatch, type ChromeBookmark, type ChromePatchOperation, type ChromeTabSummary, type ChromeTaskSummary, type ChromeTaskTodo, type ChromeTrailEntry, type ChromeWorkspaceState } from './chrome-state.js'
|
|
32
33
|
|
|
33
34
|
// Isolate this host's profile from the DSH app's default Electron userData:
|
|
34
35
|
// several Electron instances sharing Roaming\Electron fight over the GPU
|
|
@@ -274,6 +275,13 @@ function saveBookmarksToDisk(): void {
|
|
|
274
275
|
*/
|
|
275
276
|
let chromeBookmarkBar = false
|
|
276
277
|
|
|
278
|
+
/**
|
|
279
|
+
* Where the human parked the floating orb. Remembered in the chrome prefs file
|
|
280
|
+
* rather than in the page: a page-side position is per ORIGIN and would jump
|
|
281
|
+
* back to the corner on the next site.
|
|
282
|
+
*/
|
|
283
|
+
let chromeOrbPosition: { x: number; y: number } | undefined
|
|
284
|
+
|
|
277
285
|
let chromePrefsFile: string | undefined
|
|
278
286
|
|
|
279
287
|
function loadPrefsFromDisk(): void {
|
|
@@ -282,12 +290,24 @@ function loadPrefsFromDisk(): void {
|
|
|
282
290
|
const parsed: unknown = JSON.parse(readFileSync(chromePrefsFile, 'utf8'))
|
|
283
291
|
if (typeof parsed !== 'object' || parsed === null) return
|
|
284
292
|
chromeBookmarkBar = (parsed as { bookmarkBar?: unknown }).bookmarkBar === true
|
|
293
|
+
const orb = (parsed as { orbPosition?: unknown }).orbPosition
|
|
294
|
+
if (typeof orb === 'object' && orb !== null) {
|
|
295
|
+
const { x, y } = orb as { x?: unknown; y?: unknown }
|
|
296
|
+
if (typeof x === 'number' && typeof y === 'number' && Number.isFinite(x) && Number.isFinite(y)) {
|
|
297
|
+
chromeOrbPosition = { x: Math.round(x), y: Math.round(y) }
|
|
298
|
+
}
|
|
299
|
+
}
|
|
285
300
|
} catch { /* first run, or an unreadable file */ }
|
|
286
301
|
}
|
|
287
302
|
|
|
288
303
|
function savePrefsToDisk(): void {
|
|
289
304
|
if (chromePrefsFile === undefined) return
|
|
290
|
-
try {
|
|
305
|
+
try {
|
|
306
|
+
writeFileSync(chromePrefsFile, JSON.stringify({
|
|
307
|
+
bookmarkBar: chromeBookmarkBar,
|
|
308
|
+
...chromeOrbPosition !== undefined ? { orbPosition: chromeOrbPosition } : {},
|
|
309
|
+
}, null, 2), 'utf8')
|
|
310
|
+
} catch { /* read-only profile */ }
|
|
291
311
|
}
|
|
292
312
|
|
|
293
313
|
/** Only these raster types are admitted; anything else keeps the letter fallback. */
|
|
@@ -486,6 +506,25 @@ const dialogPolicies = new Map<string, { behavior: 'accept' | 'dismiss'; promptT
|
|
|
486
506
|
* browser_network. A ring (not a stream) on purpose: the agent asks after the fact,
|
|
487
507
|
* and an unbounded log would grow for the life of the tab.
|
|
488
508
|
*/
|
|
509
|
+
/**
|
|
510
|
+
* The slice of Electron's `printToPDF` surface this host uses, named locally.
|
|
511
|
+
*
|
|
512
|
+
* Electron is an optional dependency, so CI (npm ci --omit=optional) has neither
|
|
513
|
+
* its types nor the global `Electron` namespace — a reference to either one fails
|
|
514
|
+
* the build there while passing locally.
|
|
515
|
+
*/
|
|
516
|
+
interface PrintToPdfOptions {
|
|
517
|
+
landscape?: boolean
|
|
518
|
+
printBackground?: boolean
|
|
519
|
+
pageSize?: { width: number; height: number }
|
|
520
|
+
margins?: { top?: number; bottom?: number; left?: number; right?: number }
|
|
521
|
+
scaleFactor?: number
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
interface PrintToPdfHost {
|
|
525
|
+
printToPDF: (options: PrintToPdfOptions) => Promise<Buffer>
|
|
526
|
+
}
|
|
527
|
+
|
|
489
528
|
interface ConsoleEntry { level: string; text: string; at: string }
|
|
490
529
|
interface NetworkEntry { method: string; url: string; status?: number; mime?: string; kind?: string; failed?: string; ms?: number; at: string }
|
|
491
530
|
const CONSOLE_CAP = 200
|
|
@@ -507,6 +546,13 @@ const taskViewIds = new Map<string, Set<string>>()
|
|
|
507
546
|
const taskThumbnails = new Map<string, string>()
|
|
508
547
|
const taskThumbnailVersions = new Map<string, number>()
|
|
509
548
|
const taskStates = new Map<string, HostTaskState>()
|
|
549
|
+
/**
|
|
550
|
+
* The Agent's todo list per task, mirrored from the DSH session projection by the
|
|
551
|
+
* parent (see `setTaskTodos`). Kept here so the visible surface can be handed the
|
|
552
|
+
* current plan the moment it becomes visible — the list itself never rides in a
|
|
553
|
+
* task summary, because summaries reach every page's main world.
|
|
554
|
+
*/
|
|
555
|
+
const taskTodos = new Map<string, readonly ChromeTaskTodo[]>()
|
|
510
556
|
const thumbnailTimers = new Map<string, ReturnType<typeof setTimeout>>()
|
|
511
557
|
const thumbnailDirty = new Set<string>()
|
|
512
558
|
let thumbnailCaptureInFlight = false
|
|
@@ -599,6 +645,17 @@ function makeWindow(): BrowserWindow {
|
|
|
599
645
|
* the page and swallow clicks in the top 84px (the chrome is drawn there today).
|
|
600
646
|
*/
|
|
601
647
|
const CHROME_FRAME_HEIGHT = 84
|
|
648
|
+
/** Height of the bookmark bar when it is shown. It becomes part of the chrome frame. */
|
|
649
|
+
const BOOKMARK_BAR_HEIGHT = 34
|
|
650
|
+
/**
|
|
651
|
+
* How tall the chrome frame is right now: tab strip + toolbar, plus the bookmark
|
|
652
|
+
* bar when it is on. The page view starts below ALL of it, so a site's
|
|
653
|
+
* `position: fixed` header can never end up hidden under the bar — the same
|
|
654
|
+
* reason the toolbar itself lives in a view instead of a page padding.
|
|
655
|
+
*/
|
|
656
|
+
function chromeFrameHeight(): number {
|
|
657
|
+
return CHROME_FRAME_HEIGHT + (chromeBookmarkBar ? BOOKMARK_BAR_HEIGHT : 0)
|
|
658
|
+
}
|
|
602
659
|
let chromeFrame: WebContentsView | undefined
|
|
603
660
|
/** Why the frame view could not be created, if it could not. Published to the chrome. */
|
|
604
661
|
let chromeFrameError = ''
|
|
@@ -685,7 +742,7 @@ function handleChromeAction(view: WebContentsView, viewId: string, chromeToken:
|
|
|
685
742
|
const binding = (params ?? {}) as { name?: unknown; payload?: unknown }
|
|
686
743
|
if (binding.name === '__dshBrowserTaskAction' && typeof binding.payload === 'string') {
|
|
687
744
|
try {
|
|
688
|
-
const action = JSON.parse(binding.payload) as { type?: unknown; taskKey?: unknown; tabId?: unknown; tasks?: unknown; trail?: unknown; control?: unknown; factor?: unknown; url?: unknown; title?: unknown; visible?: unknown; tabs?: unknown; action?: unknown; id?: unknown; open?: unknown; left?: unknown; width?: unknown; toIndex?: unknown; cookies?: unknown; pinned?: unknown }
|
|
745
|
+
const action = JSON.parse(binding.payload) as { type?: unknown; taskKey?: unknown; tabId?: unknown; tasks?: unknown; trail?: unknown; control?: unknown; factor?: unknown; url?: unknown; title?: unknown; visible?: unknown; tabs?: unknown; action?: unknown; id?: unknown; open?: unknown; left?: unknown; width?: unknown; toIndex?: unknown; cookies?: unknown; pinned?: unknown; x?: unknown; y?: unknown }
|
|
689
746
|
// Authenticate before acting: only our injected chrome knows this
|
|
690
747
|
// view's token, so a forged payload never reaches the dispatcher.
|
|
691
748
|
if (!authorizeChromeAction(action, chromeToken)) return
|
|
@@ -699,7 +756,17 @@ function handleChromeAction(view: WebContentsView, viewId: string, chromeToken:
|
|
|
699
756
|
&& typeof action.trail === 'boolean') {
|
|
700
757
|
workspacePanels = { tasks: action.tasks, trail: action.trail }
|
|
701
758
|
if (workspacePanels.tasks && visibleTaskKey !== undefined) scheduleVisibleTaskThumbnail(visibleTaskKey)
|
|
759
|
+
// Opening the panel is the moment the other rows become visible, so hand
|
|
760
|
+
// the surface every image we already have for them.
|
|
761
|
+
if (workspacePanels.tasks) { pushCachedTaskThumbnails(); pushCachedTaskTodos() }
|
|
702
762
|
queueChromePatch({ op: 'panels.set', panels: workspacePanels })
|
|
763
|
+
} else if (action.type === 'orb-move'
|
|
764
|
+
&& typeof action.x === 'number'
|
|
765
|
+
&& typeof action.y === 'number'
|
|
766
|
+
&& Number.isFinite(action.x)
|
|
767
|
+
&& Number.isFinite(action.y)) {
|
|
768
|
+
chromeOrbPosition = { x: Math.round(action.x), y: Math.round(action.y) }
|
|
769
|
+
savePrefsToDisk()
|
|
703
770
|
} else if (action.type === 'set-control-owner'
|
|
704
771
|
&& typeof action.taskKey === 'string'
|
|
705
772
|
&& (action.control === 'agent' || action.control === 'human')
|
|
@@ -727,6 +794,9 @@ function handleChromeAction(view: WebContentsView, viewId: string, chromeToken:
|
|
|
727
794
|
// The bar is a profile-wide preference, so the host owns it and the
|
|
728
795
|
// chrome reads it back from the bootstrap / patch stream.
|
|
729
796
|
chromeBookmarkBar = action.visible
|
|
797
|
+
// 书签栏现在占的是帧视图的高度,所以开关一变就得重排:
|
|
798
|
+
// 只发 patch 的话,帧还是 84px 高、页面视图还从 84 开始 —— 书签栏就会压住页面。
|
|
799
|
+
layoutViews()
|
|
730
800
|
savePrefsToDisk()
|
|
731
801
|
queueChromePatch({ op: 'bookmarkbar.set', visible: chromeBookmarkBar })
|
|
732
802
|
} else if (action.type === 'bookmark-remove' && typeof action.url === 'string') {
|
|
@@ -903,7 +973,7 @@ function ensureChromeFrame(): void {
|
|
|
903
973
|
// did nothing (its binding was never installed).
|
|
904
974
|
try { frame.webContents.debugger.attach('1.3') } catch { /* already attached */ }
|
|
905
975
|
win.contentView.addChildView(frame)
|
|
906
|
-
frame.setBounds({ x: 0, y: 0, width: win.getContentSize()[0] ?? 0, height:
|
|
976
|
+
frame.setBounds({ x: 0, y: 0, width: win.getContentSize()[0] ?? 0, height: chromeFrameHeight() })
|
|
907
977
|
// HIDDEN until the frame's chrome can drive the page. A visible frame sits on
|
|
908
978
|
// top of the page view, so a person's clicks land on it — and the frame's
|
|
909
979
|
// chrome can only do the things it can do *in its own document* today: its
|
|
@@ -955,11 +1025,12 @@ function layoutViews(): void {
|
|
|
955
1025
|
try {
|
|
956
1026
|
// Below the chrome frame: the page viewport is genuinely smaller now, so a
|
|
957
1027
|
// sticky/fixed header lands at the top of the page instead of under the chrome.
|
|
1028
|
+
const frameHeight = chromeFrameHeight()
|
|
958
1029
|
entry.webContentsView.setBounds({
|
|
959
1030
|
x: 0,
|
|
960
|
-
y:
|
|
1031
|
+
y: frameHeight,
|
|
961
1032
|
width: width ?? 0,
|
|
962
|
-
height: Math.max(0, (height ?? 0) -
|
|
1033
|
+
height: Math.max(0, (height ?? 0) - frameHeight),
|
|
963
1034
|
})
|
|
964
1035
|
} catch { /* destroyed */ }
|
|
965
1036
|
}
|
|
@@ -975,7 +1046,7 @@ function raiseChromeFrame(): void {
|
|
|
975
1046
|
try { win.contentView.removeChildView(chromeFrame) } catch { /* not attached */ }
|
|
976
1047
|
try {
|
|
977
1048
|
win.contentView.addChildView(chromeFrame)
|
|
978
|
-
chromeFrame.setBounds({ x: 0, y: 0, width: win.getContentSize()[0] ?? 0, height:
|
|
1049
|
+
chromeFrame.setBounds({ x: 0, y: 0, width: win.getContentSize()[0] ?? 0, height: chromeFrameHeight() })
|
|
979
1050
|
} catch { /* destroyed */ }
|
|
980
1051
|
}
|
|
981
1052
|
|
|
@@ -1282,6 +1353,7 @@ function chromeWorkspaceState(selectedTaskKey = visibleTaskKey): ChromeWorkspace
|
|
|
1282
1353
|
trail: activeTraceForTask(selectedTaskKey),
|
|
1283
1354
|
bookmarks: chromeBookmarks,
|
|
1284
1355
|
bookmarkBar: chromeBookmarkBar,
|
|
1356
|
+
...chromeOrbPosition !== undefined ? { orbPosition: chromeOrbPosition } : {},
|
|
1285
1357
|
frameError: chromeFrameError,
|
|
1286
1358
|
windowProbe: (() => {
|
|
1287
1359
|
const win = window
|
|
@@ -1313,6 +1385,9 @@ function chromePatchScript(operations: readonly ChromePatchOperation[]): string
|
|
|
1313
1385
|
const patch = createPatch(chromeEpoch, ++chromeRevision, operations)
|
|
1314
1386
|
return ';window.__dshChromePatch = ' + JSON.stringify(patch)
|
|
1315
1387
|
+ ';try { window.__dshChromeApply?.(window.__dshChromePatch) } catch {}'
|
|
1388
|
+
// 补丁里含 Agent 的计划(task.todos),而默认主世界下页面脚本读得到 window ——
|
|
1389
|
+
// 应用完立刻把这份明文抹掉,别让它一直挂在页面上。
|
|
1390
|
+
+ ';try { delete window.__dshChromePatch } catch { window.__dshChromePatch = undefined }'
|
|
1316
1391
|
}
|
|
1317
1392
|
|
|
1318
1393
|
|
|
@@ -1405,6 +1480,50 @@ function queueFrameAddress(): void {
|
|
|
1405
1480
|
runChromeScript(frame, `window.__dshChromeAddress && window.__dshChromeAddress(${JSON.stringify(url)})`)
|
|
1406
1481
|
}
|
|
1407
1482
|
|
|
1483
|
+
/**
|
|
1484
|
+
* Replay every task's last known image to the visible surface.
|
|
1485
|
+
*
|
|
1486
|
+
* A task's picture never travels in the task summaries: those are injected into
|
|
1487
|
+
* every visited page's main world, and the picture is screen content. So a
|
|
1488
|
+
* chrome surface only ever learns an image from a targeted 'task.thumbnail'
|
|
1489
|
+
* patch, and the capture path queues those for the VISIBLE task alone. Without
|
|
1490
|
+
* this replay, the surface that shows the panel paints its own task's image and
|
|
1491
|
+
* a placeholder for every other one — even for tasks the human already looked
|
|
1492
|
+
* at, which is exactly what the panel looked like before this existed.
|
|
1493
|
+
*
|
|
1494
|
+
* Safe by construction: the patches go to {@link chromeSurfaces}, i.e. the
|
|
1495
|
+
* visible task's page and the chrome frame, which are the only surfaces that
|
|
1496
|
+
* can show the panel in the first place.
|
|
1497
|
+
*/
|
|
1498
|
+
/** Store one task's plan and push it to the surface that shows the panel. */
|
|
1499
|
+
function updateTaskTodos(key: string, todos: unknown): void {
|
|
1500
|
+
if (!Array.isArray(todos)) return
|
|
1501
|
+
const clean: ChromeTaskTodo[] = []
|
|
1502
|
+
for (const item of todos) {
|
|
1503
|
+
const entry = item as { content?: unknown; status?: unknown }
|
|
1504
|
+
if (typeof entry?.content !== 'string' || entry.content === '') continue
|
|
1505
|
+
const status = entry.status === 'completed' || entry.status === 'in_progress' ? entry.status : 'pending'
|
|
1506
|
+
clean.push({ content: entry.content.slice(0, 160), status })
|
|
1507
|
+
if (clean.length >= 40) break
|
|
1508
|
+
}
|
|
1509
|
+
taskTodos.set(key, clean)
|
|
1510
|
+
queueChromePatch({ op: 'task.todos', key, todos: clean })
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
/**
|
|
1514
|
+
* Hand every known plan to the visible surface. The orb is always on screen, so
|
|
1515
|
+
* a surface that has just become visible has never seen any of them.
|
|
1516
|
+
*/
|
|
1517
|
+
function pushCachedTaskTodos(): void {
|
|
1518
|
+
for (const [key, todos] of taskTodos) queueChromePatch({ op: 'task.todos', key, todos })
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
function pushCachedTaskThumbnails(): void {
|
|
1522
|
+
for (const [key, dataUrl] of taskThumbnails) {
|
|
1523
|
+
queueChromePatch({ op: 'task.thumbnail', key, version: taskThumbnailVersions.get(key) ?? 0, dataUrl })
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1408
1527
|
function scheduleVisibleTaskThumbnail(taskKey: string, delayMs = 360): void {
|
|
1409
1528
|
if (taskKey !== visibleTaskKey) return
|
|
1410
1529
|
thumbnailDirty.add(taskKey)
|
|
@@ -1494,6 +1613,10 @@ function switchVisibleTask(taskKey: string): void {
|
|
|
1494
1613
|
// changed with it. Queued after the bootstrap: resetChromeDelivery() inside
|
|
1495
1614
|
// pushVisibleChromeState() drops anything queued before it.
|
|
1496
1615
|
queueTabsSet()
|
|
1616
|
+
// 新露出来的那个表面此前没收到过别的任务的图像(补丁只发给可见表面),所以把
|
|
1617
|
+
// 宿主手里已有的图都补给它 —— 否则切过去之后面板里除了当前任务全是占位符。
|
|
1618
|
+
pushCachedTaskThumbnails()
|
|
1619
|
+
pushCachedTaskTodos()
|
|
1497
1620
|
// 必须在 pushVisibleChromeState() **之后**排队:它里面的 resetChromeDelivery() 会把
|
|
1498
1621
|
// 先排的东西丢掉(切标签就是走这条路)。导航走 applyPageChrome,不会播。
|
|
1499
1622
|
queueChromePatch({ op: 'reveal' })
|
|
@@ -1575,6 +1698,10 @@ function applyPageChrome(view: WebContentsView, viewId: string): void {
|
|
|
1575
1698
|
// (whose own chrome is not on screen but whose title belongs in the strip).
|
|
1576
1699
|
queueTabsSet()
|
|
1577
1700
|
scheduleVisibleTaskThumbnail(taskKey, 550)
|
|
1701
|
+
// 每一次真导航都会重跑整个 chrome 脚本,页面侧那份 todo 缓存跟着归零。
|
|
1702
|
+
// 缩略图有 scheduleVisibleTaskThumbnail 兜住,计划没有对应物 —— 漏了它,
|
|
1703
|
+
// Agent 一导航球上就只剩状态圆点(这是本轮功能的卖点场景,必现)。
|
|
1704
|
+
pushCachedTaskTodos()
|
|
1578
1705
|
}
|
|
1579
1706
|
}
|
|
1580
1707
|
|
|
@@ -1642,7 +1769,7 @@ function authorizeChromeAction(action: unknown, token: string): boolean {
|
|
|
1642
1769
|
}
|
|
1643
1770
|
|
|
1644
1771
|
/** Handle one command. */
|
|
1645
|
-
async function handle(op: string, msg: { id: number; viewId?: string; method?: string; params?: Record<string, unknown>; expression?: string; url?: string; savePath?: string; cookies?: unknown[]; entry?: unknown; key?: string; label?: string; task?: Record<string, unknown>; domain?: string; name?: string; all?: boolean; behavior?: string; promptText?: string; clear?: boolean }): Promise<void> {
|
|
1772
|
+
async function handle(op: string, msg: { id: number; viewId?: string; method?: string; params?: Record<string, unknown>; expression?: string; url?: string; savePath?: string; cookies?: unknown[]; entry?: unknown; key?: string; label?: string; task?: Record<string, unknown>; todos?: unknown; domain?: string; name?: string; all?: boolean; behavior?: string; promptText?: string; clear?: boolean; options?: Record<string, unknown> }): Promise<void> {
|
|
1646
1773
|
try {
|
|
1647
1774
|
switch (op) {
|
|
1648
1775
|
case 'ping':
|
|
@@ -1667,6 +1794,7 @@ async function handle(op: string, msg: { id: number; viewId?: string; method?: s
|
|
|
1667
1794
|
const trail = activeTraceForTask(entryView.taskKey).at(-1)
|
|
1668
1795
|
if (trail !== undefined) operations.push({ op: 'trail.append', taskKey: entryView.taskKey, entry: trail })
|
|
1669
1796
|
scheduleVisibleTaskThumbnail(entryView.taskKey)
|
|
1797
|
+
pushCachedTaskTodos()
|
|
1670
1798
|
}
|
|
1671
1799
|
if (operations.length > 0) queueChromePatch(...operations)
|
|
1672
1800
|
}
|
|
@@ -1918,6 +2046,7 @@ async function handle(op: string, msg: { id: number; viewId?: string; method?: s
|
|
|
1918
2046
|
if (thumbnailTimer !== undefined) clearTimeout(thumbnailTimer)
|
|
1919
2047
|
thumbnailTimers.delete(entry.taskKey)
|
|
1920
2048
|
taskThumbnails.delete(entry.taskKey)
|
|
2049
|
+
taskTodos.delete(entry.taskKey)
|
|
1921
2050
|
taskThumbnailVersions.delete(entry.taskKey)
|
|
1922
2051
|
thumbnailDirty.delete(entry.taskKey)
|
|
1923
2052
|
thumbnailLastCapturedAt.delete(entry.taskKey)
|
|
@@ -1984,8 +2113,16 @@ async function handle(op: string, msg: { id: number; viewId?: string; method?: s
|
|
|
1984
2113
|
const expression = msg.expression
|
|
1985
2114
|
if (typeof expression !== 'string') throw new Error('chromeEval: expression must be a string')
|
|
1986
2115
|
try { frame.webContents.debugger.attach('1.3') } catch { /* already attached */ }
|
|
2116
|
+
// 隔离模式(默认)下,帧里的 chrome 也住在它自己的世界 —— 读它必须带上下文,
|
|
2117
|
+
// 否则读的是帧的主世界(那里什么都没有)。
|
|
2118
|
+
const frameContextId = CHROME_WORLD === 'isolated' ? await ensureChromeContext(frame) : undefined
|
|
1987
2119
|
frame.webContents.debugger
|
|
1988
|
-
.sendCommand('Runtime.evaluate', {
|
|
2120
|
+
.sendCommand('Runtime.evaluate', {
|
|
2121
|
+
expression,
|
|
2122
|
+
returnByValue: true,
|
|
2123
|
+
awaitPromise: true,
|
|
2124
|
+
...frameContextId === undefined ? {} : { contextId: frameContextId },
|
|
2125
|
+
})
|
|
1989
2126
|
.then(
|
|
1990
2127
|
(result: unknown) => {
|
|
1991
2128
|
// The client resolves with `msg.result`, so the value goes in there.
|
|
@@ -2014,6 +2151,19 @@ async function handle(op: string, msg: { id: number; viewId?: string; method?: s
|
|
|
2014
2151
|
reply(msg.id, { ok: true })
|
|
2015
2152
|
return
|
|
2016
2153
|
}
|
|
2154
|
+
case 'focusWindow': {
|
|
2155
|
+
const viewId = msg.viewId
|
|
2156
|
+
if (viewId === undefined) throw new Error('focusWindow missing viewId')
|
|
2157
|
+
if (!views.has(viewId)) throw new Error(`focusWindow: unknown view ${viewId}`)
|
|
2158
|
+
const win = ensureWindow()
|
|
2159
|
+
// The same three steps capture uses: a minimized window has to be
|
|
2160
|
+
// restored, and a window behind another one only comes forward on focus.
|
|
2161
|
+
try { if (!win.isVisible()) win.show() } catch { /* closing */ }
|
|
2162
|
+
try { win.restore() } catch { /* not minimized */ }
|
|
2163
|
+
try { win.focus() } catch { /* closing */ }
|
|
2164
|
+
reply(msg.id, { ok: true })
|
|
2165
|
+
return
|
|
2166
|
+
}
|
|
2017
2167
|
case 'label': {
|
|
2018
2168
|
const viewId = msg.viewId
|
|
2019
2169
|
const label = msg.label
|
|
@@ -2053,6 +2203,13 @@ async function handle(op: string, msg: { id: number; viewId?: string; method?: s
|
|
|
2053
2203
|
reply(msg.id, { ok: true, result: { task: task ?? null } })
|
|
2054
2204
|
return
|
|
2055
2205
|
}
|
|
2206
|
+
case 'setTaskTodos': {
|
|
2207
|
+
const key = msg.key
|
|
2208
|
+
if (typeof key !== 'string' || msg.todos === undefined) throw new Error('setTaskTodos missing key or todos')
|
|
2209
|
+
updateTaskTodos(key, msg.todos)
|
|
2210
|
+
reply(msg.id, { ok: true, result: {} })
|
|
2211
|
+
return
|
|
2212
|
+
}
|
|
2056
2213
|
case 'updateTask': {
|
|
2057
2214
|
const key = msg.key
|
|
2058
2215
|
if (typeof key !== 'string' || msg.task === undefined) throw new Error('updateTask missing key or task')
|
|
@@ -2073,6 +2230,28 @@ async function handle(op: string, msg: { id: number; viewId?: string; method?: s
|
|
|
2073
2230
|
reply(msg.id, { ok: true, result })
|
|
2074
2231
|
return
|
|
2075
2232
|
}
|
|
2233
|
+
case 'chromeWorldEval': {
|
|
2234
|
+
// Run a snippet in the world the *page's* injected chrome lives in.
|
|
2235
|
+
//
|
|
2236
|
+
// In isolated mode the chrome's globals are not reachable from the page
|
|
2237
|
+
// world, and creating a world with the same name from outside yields a
|
|
2238
|
+
// different, empty context — so the host, which owns the real one, has to
|
|
2239
|
+
// resolve it here. In main mode this is a plain page-world evaluate.
|
|
2240
|
+
const viewId = msg.viewId
|
|
2241
|
+
if (viewId === undefined) throw new Error('chromeWorldEval missing viewId')
|
|
2242
|
+
const entry = views.get(viewId)
|
|
2243
|
+
if (entry === undefined) throw new Error(`chromeWorldEval: unknown view ${viewId}`)
|
|
2244
|
+
if (typeof msg.expression !== 'string') throw new Error('chromeWorldEval missing expression')
|
|
2245
|
+
const contextId = CHROME_WORLD === 'isolated' ? await ensureChromeContext(entry.webContentsView) : undefined
|
|
2246
|
+
const result = await entry.webContentsView.webContents.debugger.sendCommand('Runtime.evaluate', {
|
|
2247
|
+
expression: msg.expression,
|
|
2248
|
+
returnByValue: true,
|
|
2249
|
+
awaitPromise: true,
|
|
2250
|
+
...contextId === undefined ? {} : { contextId },
|
|
2251
|
+
})
|
|
2252
|
+
reply(msg.id, { ok: true, result })
|
|
2253
|
+
return
|
|
2254
|
+
}
|
|
2076
2255
|
case 'capture': {
|
|
2077
2256
|
const viewId = msg.viewId
|
|
2078
2257
|
if (viewId === undefined) throw new Error('capture missing viewId')
|
|
@@ -2140,6 +2319,23 @@ async function handle(op: string, msg: { id: number; viewId?: string; method?: s
|
|
|
2140
2319
|
reply(msg.id, { ok: true, result: { base64, width: 0, height: 0 } })
|
|
2141
2320
|
return
|
|
2142
2321
|
}
|
|
2322
|
+
case 'printToPdf': {
|
|
2323
|
+
const viewId = msg.viewId
|
|
2324
|
+
if (viewId === undefined) throw new Error('printToPdf missing viewId')
|
|
2325
|
+
const entry = views.get(viewId)
|
|
2326
|
+
if (entry === undefined) throw new Error(`printToPdf: unknown view ${viewId}`)
|
|
2327
|
+
// Electron's debugger does not implement CDP `Page.printToPDF`; this
|
|
2328
|
+
// native path is the one that actually produces a document.
|
|
2329
|
+
const options = (msg.options ?? {}) as PrintToPdfOptions
|
|
2330
|
+
// `printToPDF` and its option type are Electron's; Electron is an optional
|
|
2331
|
+
// dependency (CI installs with --omit=optional, so neither the type nor the
|
|
2332
|
+
// `Electron` namespace exists there). Name the shape locally instead of
|
|
2333
|
+
// reaching for the namespace, and let this child — which only ever runs
|
|
2334
|
+
// inside Electron — be the one that calls it.
|
|
2335
|
+
const data = await (entry.webContentsView.webContents as unknown as PrintToPdfHost).printToPDF(options)
|
|
2336
|
+
reply(msg.id, { ok: true, result: { base64: data.toString('base64') } })
|
|
2337
|
+
return
|
|
2338
|
+
}
|
|
2143
2339
|
case 'download': {
|
|
2144
2340
|
const viewId = msg.viewId
|
|
2145
2341
|
const url = msg.url
|
|
@@ -2303,7 +2499,7 @@ void app.whenReady().then(() => {
|
|
|
2303
2499
|
rl.on('line', line => {
|
|
2304
2500
|
const text = line.trim()
|
|
2305
2501
|
if (text === '') return
|
|
2306
|
-
let msg: { id: number; op?: string; viewId?: string; method?: string; params?: Record<string, unknown>; expression?: string; url?: string; savePath?: string; cookies?: unknown[]; key?: string; label?: string; task?: Record<string, unknown>; domain?: string; name?: string; all?: boolean }
|
|
2502
|
+
let msg: { id: number; op?: string; viewId?: string; method?: string; params?: Record<string, unknown>; expression?: string; url?: string; savePath?: string; cookies?: unknown[]; key?: string; label?: string; task?: Record<string, unknown>; todos?: unknown; domain?: string; name?: string; all?: boolean; options?: Record<string, unknown> }
|
|
2307
2503
|
try {
|
|
2308
2504
|
msg = JSON.parse(text) as typeof msg
|
|
2309
2505
|
} catch {
|