dsh-browser-plus 0.0.0-stage → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +153 -0
- package/LICENSE +22 -0
- package/NOTICE.md +7 -0
- package/README.en.md +119 -0
- package/README.md +118 -2
- package/assets/dsh-browser-plus-256.png +0 -0
- package/assets/dsh-browser-plus-512.png +0 -0
- package/assets/dsh-browser-plus-small.svg +9 -0
- package/assets/dsh-browser-plus.ico +0 -0
- package/assets/dsh-browser-plus.svg +11 -0
- package/assets/readme-workspace.png +0 -0
- package/client/index.js +185 -0
- package/cordis.patch.yml +41 -0
- package/docs/MIGRATION.md +48 -0
- package/docs/README.md +22 -0
- package/docs/SOAK-CHECKLIST.md +98 -0
- package/docs/architecture.md +88 -0
- package/docs/tool-reference.md +126 -0
- package/docs/user-guide.md +137 -0
- package/docs/why-browser.md +45 -0
- package/lib/browser/runtime.d.ts +238 -0
- package/lib/browser/runtime.js +330 -0
- package/lib/browser/types.d.ts +758 -0
- package/lib/browser/types.js +18 -0
- package/lib/browser-electron/auth-cookies.d.ts +54 -0
- package/lib/browser-electron/auth-cookies.js +83 -0
- package/lib/browser-electron/chrome-state.d.ts +211 -0
- package/lib/browser-electron/chrome-state.js +12 -0
- package/lib/browser-electron/entry.d.ts +73 -0
- package/lib/browser-electron/entry.js +65 -0
- package/lib/browser-electron/fingerprint.d.ts +29 -0
- package/lib/browser-electron/fingerprint.js +42 -0
- package/lib/browser-electron/host-main.d.ts +19 -0
- package/lib/browser-electron/host-main.js +2691 -0
- package/lib/browser-electron/icon.d.ts +11 -0
- package/lib/browser-electron/icon.js +23 -0
- package/lib/browser-electron/page-chrome.d.ts +21 -0
- package/lib/browser-electron/page-chrome.js +2269 -0
- package/lib/browser-electron/provider.d.ts +767 -0
- package/lib/browser-electron/provider.js +2825 -0
- package/lib/browser-electron/remote-host.d.ts +145 -0
- package/lib/browser-electron/remote-host.js +993 -0
- package/lib/browser-electron/task-summary.d.ts +2 -0
- package/lib/browser-electron/task-summary.js +12 -0
- package/lib/browser-electron/task-thumbnail.d.ts +11 -0
- package/lib/browser-electron/task-thumbnail.js +9 -0
- package/lib/browser-electron/write-guard.d.ts +41 -0
- package/lib/browser-electron/write-guard.js +123 -0
- package/lib/client.js +185 -0
- package/lib/command-browser/index.d.ts +20 -0
- package/lib/command-browser/index.js +35 -0
- package/lib/http-browser/index.d.ts +28 -0
- package/lib/http-browser/index.js +110 -0
- package/lib/index.d.ts +27 -0
- package/lib/index.js +25 -0
- package/lib/task-todos/index.d.ts +25 -0
- package/lib/task-todos/index.js +100 -0
- package/lib/tool-browser/index.d.ts +31 -0
- package/lib/tool-browser/index.js +2026 -0
- package/package.json +120 -4
- package/screenshots.json +3 -0
- package/scripts/build-client.mjs +20 -0
- package/scripts/build-icons.mjs +80 -0
- package/scripts/capture-window.ps1 +79 -0
- package/scripts/crop-image.ps1 +20 -0
- package/scripts/smoke-browser-tools.mjs +2051 -0
- package/scripts/smoke-chrome-world.mjs +65 -0
- package/scripts/smoke-electron-host.mjs +50 -0
- package/scripts/test-orb-drag.mjs +83 -0
- package/src/browser/runtime.ts +506 -0
- package/src/browser/types.ts +741 -0
- package/src/browser-electron/auth-cookies.ts +125 -0
- package/src/browser-electron/chrome-state.ts +192 -0
- package/src/browser-electron/entry.ts +125 -0
- package/src/browser-electron/fingerprint.ts +45 -0
- package/src/browser-electron/host-main.ts +2526 -0
- package/src/browser-electron/icon.ts +26 -0
- package/src/browser-electron/page-chrome.ts +2281 -0
- package/src/browser-electron/provider.ts +3366 -0
- package/src/browser-electron/remote-host.ts +1051 -0
- package/src/browser-electron/task-summary.ts +10 -0
- package/src/browser-electron/task-thumbnail.ts +17 -0
- package/src/browser-electron/write-guard.ts +134 -0
- package/src/command-browser/index.ts +61 -0
- package/src/http-browser/index.ts +139 -0
- package/src/index.ts +65 -0
- package/src/task-todos/index.ts +114 -0
- package/src/tool-browser/index.ts +2071 -0
- package/src/types/electron-shim.d.ts +143 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verify chromeWorld: 'isolated' in a real window.
|
|
3
|
+
*
|
|
4
|
+
* This is the one declared feature that had never been exercised: the chrome is
|
|
5
|
+
* injected into an isolated world so the page's own scripts cannot read its
|
|
6
|
+
* globals. The claim has two halves -- the chrome still mounts, and the page's
|
|
7
|
+
* main world cannot see it -- and an A/B against the default mode proves the
|
|
8
|
+
* switch actually changes something rather than being inert.
|
|
9
|
+
*
|
|
10
|
+
* Run: npm run smoke:chrome-world
|
|
11
|
+
*/
|
|
12
|
+
import { mkdtempSync, rmSync } from 'node:fs'
|
|
13
|
+
import { tmpdir } from 'node:os'
|
|
14
|
+
import { join } from 'node:path'
|
|
15
|
+
|
|
16
|
+
import { RemoteElectronViewHost, defaultHostMainPath } from '../lib/browser-electron/remote-host.js'
|
|
17
|
+
|
|
18
|
+
const wait = ms => new Promise(resolve => setTimeout(resolve, ms))
|
|
19
|
+
const results = []
|
|
20
|
+
|
|
21
|
+
const probe = async (label, options) => {
|
|
22
|
+
const profile = mkdtempSync(join(tmpdir(), 'dsh-chrome-world-'))
|
|
23
|
+
process.env.DSH_BROWSER_PLUS_USER_DATA = profile
|
|
24
|
+
const host = new RemoteElectronViewHost(defaultHostMainPath(), options)
|
|
25
|
+
try {
|
|
26
|
+
const view = host.createView('chrome-world', 'Chrome World')
|
|
27
|
+
await view.sendCommand('Page.enable', {})
|
|
28
|
+
await view.sendCommand('Page.navigate', { url: 'https://example.com/' })
|
|
29
|
+
await wait(2500)
|
|
30
|
+
const reply = await view.sendCommand('Runtime.evaluate', {
|
|
31
|
+
expression: 'JSON.stringify({ chromeHost: document.getElementById("__dsh_browser_chrome_host__") !== null, painted: (() => { const h = document.getElementById("__dsh_browser_chrome_host__"); if (h === null) return 0; return Math.round(h.getBoundingClientRect().width) })(), binding: typeof window.__dshBrowserTaskAction, tasks: typeof window.__dshTasks, trail: typeof window.__dshTrail })',
|
|
32
|
+
returnByValue: true,
|
|
33
|
+
})
|
|
34
|
+
const value = JSON.parse(reply?.result?.value ?? '{}')
|
|
35
|
+
results.push({ label, ...value })
|
|
36
|
+
console.log(' ' + label.padEnd(22) + JSON.stringify(value))
|
|
37
|
+
return value
|
|
38
|
+
} finally {
|
|
39
|
+
host.dispose()
|
|
40
|
+
await wait(1500)
|
|
41
|
+
try { rmSync(profile, { recursive: true, force: true }) } catch { /* briefly locked */ }
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// 两种模式都**显式**指定:默认值会变(2026-10-04 起默认 isolated),
|
|
46
|
+
// 用 `{}` 表示「main」会让这条检查在默认值一变就静默失效。
|
|
47
|
+
const isolated = await probe('isolated (default)', {})
|
|
48
|
+
const main = await probe('main (opt-in)', { chromeWorld: 'main' })
|
|
49
|
+
|
|
50
|
+
// Assert only what chromeWorld claims. window.__dshBrowserTaskAction is reported
|
|
51
|
+
// but not asserted: it appears asynchronously after load, so a fixed wait
|
|
52
|
+
// measures it inconsistently (observed both ways at 2.5s and 3s).
|
|
53
|
+
const failures = []
|
|
54
|
+
if (main.chromeHost !== true) failures.push('the chrome did not mount in main mode')
|
|
55
|
+
if (main.tasks !== 'object' || main.trail !== 'object') failures.push('main mode should leak the chrome globals, or the A/B proves nothing')
|
|
56
|
+
if (isolated.chromeHost !== true) failures.push('the chrome did not mount in isolated mode')
|
|
57
|
+
for (const key of ['binding', 'tasks', 'trail']) {
|
|
58
|
+
if (isolated[key] !== 'undefined') failures.push('isolated mode still leaks window.' + (key === 'binding' ? '__dshBrowserTaskAction' : '__dsh' + key[0].toUpperCase() + key.slice(1)))
|
|
59
|
+
}
|
|
60
|
+
console.log('')
|
|
61
|
+
if (failures.length > 0) {
|
|
62
|
+
console.log(JSON.stringify({ ok: false, failures }))
|
|
63
|
+
process.exit(1)
|
|
64
|
+
}
|
|
65
|
+
console.log(JSON.stringify({ ok: true, note: 'the chrome mounts in both modes; only main (the opt-in bisect path) leaks its globals to the page' }))
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
|
|
3
|
+
import { RemoteElectronViewHost, defaultHostMainPath } from '../lib/browser-electron/remote-host.js'
|
|
4
|
+
|
|
5
|
+
function withTimeout(promise, timeoutMs, label) {
|
|
6
|
+
return new Promise((resolve, reject) => {
|
|
7
|
+
const timer = setTimeout(() => reject(new Error(label + ' timed out after ' + String(timeoutMs) + 'ms')), timeoutMs)
|
|
8
|
+
promise.then(
|
|
9
|
+
value => { clearTimeout(timer); resolve(value) },
|
|
10
|
+
error => { clearTimeout(timer); reject(error) },
|
|
11
|
+
)
|
|
12
|
+
})
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function delay(ms) {
|
|
16
|
+
return new Promise(resolve => setTimeout(resolve, ms))
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const host = new RemoteElectronViewHost(defaultHostMainPath())
|
|
20
|
+
try {
|
|
21
|
+
const view = host.createView('electron-host-smoke', 'Electron Host Smoke')
|
|
22
|
+
await withTimeout(view.sendCommand('Page.navigate', { url: 'http://127.0.0.1:3080/' }), 15_000, 'Page.navigate')
|
|
23
|
+
const binding = await withTimeout(view.sendCommand('Runtime.evaluate', {
|
|
24
|
+
expression: 'typeof window.__dshBrowserTaskAction',
|
|
25
|
+
returnByValue: true,
|
|
26
|
+
}), 10_000, 'binding probe')
|
|
27
|
+
assert.equal(binding.result?.value, 'function', 'page task binding must be present')
|
|
28
|
+
const chromeReady = await withTimeout(view.sendCommand('Runtime.evaluate', {
|
|
29
|
+
expression: "new Promise(resolve => { const deadline = Date.now() + 3000; const inspect = () => { const host = document.getElementById('__dsh_browser_chrome_host__'); if (host?.getAttribute('data-dsh-browser-ready') === '1' || Date.now() >= deadline) { resolve(host?.getAttribute('data-dsh-browser-ready') || null); return } setTimeout(inspect, 50) }; inspect() })",
|
|
30
|
+
returnByValue: true,
|
|
31
|
+
awaitPromise: true,
|
|
32
|
+
}), 5_000, 'chrome ready probe')
|
|
33
|
+
assert.equal(chromeReady.result?.value, '1', 'page chrome must finish wiring interactive controls')
|
|
34
|
+
|
|
35
|
+
// The renderer's direct DOM interaction path is covered by page-chrome
|
|
36
|
+
// regression tests. CDP Input.dispatchMouseEvent does not surface a DOM
|
|
37
|
+
// pointer event in Electron 42, so it cannot faithfully stand in for a hand.
|
|
38
|
+
await delay(300)
|
|
39
|
+
await withTimeout(view.sendCommand('Runtime.evaluate', {
|
|
40
|
+
expression: "window.__dshBrowserTaskAction(JSON.stringify({ type: 'set-control-owner', taskKey: 'electron-host-smoke', control: 'human' })); 'sent'",
|
|
41
|
+
returnByValue: true,
|
|
42
|
+
}), 10_000, 'binding action')
|
|
43
|
+
await delay(300)
|
|
44
|
+
const task = await withTimeout(host.getTask('electron-host-smoke'), 10_000, 'task state query')
|
|
45
|
+
assert.equal(task?.control, 'human', 'Host must receive the page handoff')
|
|
46
|
+
assert.equal(task?.status, 'waiting-user', 'Host must show the handoff state')
|
|
47
|
+
console.log(JSON.stringify({ ok: true, task }))
|
|
48
|
+
} finally {
|
|
49
|
+
host.dispose()
|
|
50
|
+
}
|
|
@@ -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)
|