dsh-inner-browser-use 0.34.28 → 0.34.95
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/README.md +53 -18
- package/README.zh.md +26 -14
- package/client.js +161 -22
- package/docs/ARCHITECTURE.md +15 -12
- package/docs/ARTIFACTS.md +1 -1
- package/docs/BENCH.md +209 -34
- package/docs/FLASH.md +84 -0
- package/docs/ROADMAP.md +21 -15
- package/docs/VERIFY.md +20 -10
- package/index.js +144 -24
- package/package.json +17 -3
- package/read.js +633 -29
- package/runtime/flash-app/main.js +232 -0
- package/runtime/flash-app/package.json +1 -0
- package/runtime/flash-setup.mjs +151 -0
- package/runtime/swf.mjs +233 -0
- package/scripts/check-flash-local.mjs +104 -0
- package/scripts/check-flash-mirror.mjs +73 -0
- package/scripts/check-flash-tools.mjs +74 -0
- package/scripts/check-flash-window.mjs +58 -0
- package/scripts/doctor.mjs +3 -1
- package/sidecar.js +1076 -96
- package/tools.js +1068 -144
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* The Flash path, measured without the network: a page served from this
|
|
4
|
+
* process, its movie from the repo's own fixture, and the same bridge a site
|
|
5
|
+
* would be driven through. A site being slow or rate-limiting for a minute is
|
|
6
|
+
* not a plugin regression, and this check can tell the two apart.
|
|
7
|
+
*
|
|
8
|
+
* node scripts/check-flash-local.mjs
|
|
9
|
+
*
|
|
10
|
+
* Asserts the movie painted, frames reach a viewer, and a press, a release and
|
|
11
|
+
* a key each reach the plugin in well under the time a person would notice.
|
|
12
|
+
* Own profile, own state directory, closed afterwards; nothing appears on the
|
|
13
|
+
* desktop.
|
|
14
|
+
*/
|
|
15
|
+
import { createServer } from 'node:http'
|
|
16
|
+
import { createHash } from 'node:crypto'
|
|
17
|
+
import { mkdtempSync, readFileSync } from 'node:fs'
|
|
18
|
+
import { tmpdir } from 'node:os'
|
|
19
|
+
import { dirname, join } from 'node:path'
|
|
20
|
+
import { fileURLToPath } from 'node:url'
|
|
21
|
+
|
|
22
|
+
const here = dirname(fileURLToPath(import.meta.url))
|
|
23
|
+
/* The looping movie, because a still one paints once and says nothing about
|
|
24
|
+
whether frames follow a moving picture. */
|
|
25
|
+
const movie = readFileSync(join(here, '..', 'test', 'fixtures', 'loop.swf'))
|
|
26
|
+
const page = `<!doctype html>
|
|
27
|
+
<html><head><meta charset="utf-8"><title>Flash fixture</title></head>
|
|
28
|
+
<body style="margin:0;background:#101820">
|
|
29
|
+
<embed src="/tiny.swf" type="application/x-shockwave-flash" width="400" height="300">
|
|
30
|
+
</body></html>`
|
|
31
|
+
|
|
32
|
+
const server = createServer((request, reply) => {
|
|
33
|
+
if (request.url === '/tiny.swf') {
|
|
34
|
+
reply.writeHead(200, { 'content-type': 'application/x-shockwave-flash', 'content-length': movie.length })
|
|
35
|
+
reply.end(movie)
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
reply.writeHead(200, { 'content-type': 'text/html; charset=utf-8' })
|
|
39
|
+
reply.end(page)
|
|
40
|
+
})
|
|
41
|
+
await new Promise((ready) => server.listen(0, '127.0.0.1', ready))
|
|
42
|
+
const url = `http://127.0.0.1:${server.address().port}/`
|
|
43
|
+
|
|
44
|
+
process.env.DSH_INNER_BROWSER_STATE_DIR = mkdtempSync(join(tmpdir(), 'dsh-flash-local-'))
|
|
45
|
+
const { Sidecar } = await import('../sidecar.js')
|
|
46
|
+
|
|
47
|
+
/** @param {number} ms - how long. */
|
|
48
|
+
const wait = (ms) => new Promise((settle) => setTimeout(settle, ms))
|
|
49
|
+
const hash = (buffer) => createHash('sha1').update(Buffer.from(buffer ?? '')).digest('hex').slice(0, 12)
|
|
50
|
+
|
|
51
|
+
const sidecar = new Sidecar({ profile: 'flash-local', headless: false, flash: true })
|
|
52
|
+
let ok = false
|
|
53
|
+
try {
|
|
54
|
+
await sidecar.launch('about:blank')
|
|
55
|
+
const seen = []
|
|
56
|
+
const detach = await sidecar.watch((frame) => {
|
|
57
|
+
seen.push({ at: Date.now(), shape: hash(frame.jpeg), kb: Math.round(String(frame.jpeg ?? '').length * 0.75 / 1024) })
|
|
58
|
+
})
|
|
59
|
+
const navigatedAt = Date.now()
|
|
60
|
+
await sidecar.page.navigate(url).catch(() => {})
|
|
61
|
+
for (let i = 0; i < 60 && seen.length === 0; i += 1) await wait(250)
|
|
62
|
+
const firstFrameMs = Date.now() - navigatedAt
|
|
63
|
+
await wait(3000)
|
|
64
|
+
detach()
|
|
65
|
+
const gaps = seen.slice(1).map((one, index) => one.at - seen[index].at).sort((a, b) => a - b)
|
|
66
|
+
const fps = gaps.length === 0 ? 0 : Math.round((1000 / (gaps[Math.floor(gaps.length / 2)] || 1000)) * 10) / 10
|
|
67
|
+
const shapes = new Set(seen.map((one) => one.shape))
|
|
68
|
+
const info = await fetch(`http://127.0.0.1:${sidecar.page.flashBridge}/info`, { signal: AbortSignal.timeout(1500) }).then((one) => one.json()).catch(() => ({}))
|
|
69
|
+
const pressedAt = Date.now()
|
|
70
|
+
await sidecar.page.mouse('mousePressed', { x: 640, y: 420 })
|
|
71
|
+
const pressMs = Date.now() - pressedAt
|
|
72
|
+
const releasedAt = Date.now()
|
|
73
|
+
await sidecar.page.mouse('mouseReleased', { x: 640, y: 420 })
|
|
74
|
+
const releaseMs = Date.now() - releasedAt
|
|
75
|
+
const keyAt = Date.now()
|
|
76
|
+
await sidecar.page.key('keyDown', { key: 'a', text: 'a' })
|
|
77
|
+
const keyMs = Date.now() - keyAt
|
|
78
|
+
await sidecar.page.key('keyUp', { key: 'a' })
|
|
79
|
+
await wait(600)
|
|
80
|
+
/* The same endpoint a click uses, asked directly: the plugin took the input
|
|
81
|
+
rather than a path timing out on the way there. */
|
|
82
|
+
const fedReply = await fetch(`http://127.0.0.1:${sidecar.page.flashBridge}/input`, {
|
|
83
|
+
method: 'POST',
|
|
84
|
+
headers: { 'content-type': 'application/json' },
|
|
85
|
+
body: JSON.stringify({ type: 'mouseMove', x: 200, y: 150, button: 'left', clickCount: 0 }),
|
|
86
|
+
signal: AbortSignal.timeout(1500),
|
|
87
|
+
}).catch((error) => ({ status: 0, text: () => Promise.resolve(String(error?.message ?? error)) }))
|
|
88
|
+
const fedStatus = fedReply.status
|
|
89
|
+
const fedText = await fedReply.text().catch(() => '')
|
|
90
|
+
const fed = { ok: fedText.includes('"ok":true') }
|
|
91
|
+
const frameKB = seen.length === 0 ? 0 : Math.round(seen.reduce((sum, one) => sum + one.kb, 0) / seen.length)
|
|
92
|
+
/* The fixture movie is not a game: what is asserted here is that the movie
|
|
93
|
+
painted, that frames reached a viewer, and that input arrived without a
|
|
94
|
+
detour through the debugging protocol. Frame rate on moving pictures is
|
|
95
|
+
measured by check-flash-mirror against a real site. */
|
|
96
|
+
ok = Number(info.painted ?? 0) > 1 && seen.length >= 3 && shapes.size >= 2 && firstFrameMs < 1500 && pressMs < 40 && releaseMs < 40 && keyMs < 40 && fed.ok === true && frameKB > 0
|
|
97
|
+
console.log(JSON.stringify({ url, firstFrameMs, frames: seen.length, fps, shapes: shapes.size, frameKB, painted: info.painted ?? 0, pressMs, releaseMs, keyMs, fedStatus, fedText: fedText.slice(0, 80), fed: fed.ok === true, ok }))
|
|
98
|
+
} catch (error) {
|
|
99
|
+
console.log(JSON.stringify({ ok: false, error: String(error?.message ?? error).slice(0, 160) }))
|
|
100
|
+
} finally {
|
|
101
|
+
await sidecar.shutdown().catch(() => {})
|
|
102
|
+
server.close()
|
|
103
|
+
}
|
|
104
|
+
process.exitCode = ok ? 0 : 1
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Does a Flash page mirror into the column at a usable rate, and do clicks land?
|
|
4
|
+
*
|
|
5
|
+
* node scripts/check-flash-mirror.mjs [url]
|
|
6
|
+
*
|
|
7
|
+
* Counts the frames a viewer really receives, times a press and a release
|
|
8
|
+
* through the page, and reports both next to the frame size. Own profile, own
|
|
9
|
+
* state directory, closed afterwards; nothing is shown on the desktop.
|
|
10
|
+
*/
|
|
11
|
+
import { mkdtempSync } from 'node:fs'
|
|
12
|
+
import { tmpdir } from 'node:os'
|
|
13
|
+
import { join } from 'node:path'
|
|
14
|
+
|
|
15
|
+
const url = process.argv[2] ?? 'http://account.100bt.com/game/auth/login.html?gameName=aola&action=startgame&platform=100bt'
|
|
16
|
+
process.env.DSH_INNER_BROWSER_STATE_DIR = mkdtempSync(join(tmpdir(), 'dsh-flash-mirror-'))
|
|
17
|
+
|
|
18
|
+
const { Sidecar } = await import('../sidecar.js')
|
|
19
|
+
|
|
20
|
+
/** @param {number} ms - how long. */
|
|
21
|
+
const wait = (ms) => new Promise((settle) => setTimeout(settle, ms))
|
|
22
|
+
const sidecar = new Sidecar({ profile: 'flash-mirror', headless: false, flash: true })
|
|
23
|
+
let ok = false
|
|
24
|
+
try {
|
|
25
|
+
const started = Date.now()
|
|
26
|
+
await sidecar.launch('about:blank')
|
|
27
|
+
const sizes = []
|
|
28
|
+
const times = []
|
|
29
|
+
const detach = await sidecar.watch((frame) => {
|
|
30
|
+
sizes.push(Math.round(String(frame.jpeg ?? '').length * 0.75 / 1024))
|
|
31
|
+
times.push(Date.now())
|
|
32
|
+
})
|
|
33
|
+
const navigatedAt = Date.now()
|
|
34
|
+
const page = sidecar.page
|
|
35
|
+
await page.navigate(url).catch(() => {})
|
|
36
|
+
for (let i = 0; i < 60 && times.length === 0; i += 1) await wait(250)
|
|
37
|
+
const firstFrame = Date.now() - navigatedAt
|
|
38
|
+
await wait(4000)
|
|
39
|
+
detach()
|
|
40
|
+
const gaps = times.slice(1).map((at, index) => at - times[index]).sort((a, b) => a - b)
|
|
41
|
+
const fps = gaps.length === 0 ? 0 : Math.round((1000 / (gaps[Math.floor(gaps.length / 2)] || 1000)) * 10) / 10
|
|
42
|
+
const pressedAt = Date.now()
|
|
43
|
+
await page.mouse('mousePressed', { x: 640, y: 420 })
|
|
44
|
+
const pressMs = Date.now() - pressedAt
|
|
45
|
+
const releasedAt = Date.now()
|
|
46
|
+
await page.mouse('mouseReleased', { x: 640, y: 420 })
|
|
47
|
+
const releaseMs = Date.now() - releasedAt
|
|
48
|
+
await wait(400)
|
|
49
|
+
const before = await page.picture()
|
|
50
|
+
const keyAt = Date.now()
|
|
51
|
+
await page.key('keyDown', { key: 'a', text: 'a' })
|
|
52
|
+
const keyMs = Date.now() - keyAt
|
|
53
|
+
await page.key('keyUp', { key: 'a' })
|
|
54
|
+
await wait(600)
|
|
55
|
+
const moved = await page.picture()
|
|
56
|
+
ok = fps >= 15 && pressMs < 40 && releaseMs < 40 && keyMs < 40 && moved !== before && sizes.length > 5
|
|
57
|
+
console.log(JSON.stringify({
|
|
58
|
+
firstFrameMs: firstFrame,
|
|
59
|
+
frames: sizes.length,
|
|
60
|
+
fps,
|
|
61
|
+
frameKB: sizes.length === 0 ? 0 : Math.round(sizes.reduce((a, b) => a + b, 0) / sizes.length),
|
|
62
|
+
pressMs,
|
|
63
|
+
releaseMs,
|
|
64
|
+
keyMs,
|
|
65
|
+
responded: moved !== '' && moved !== before,
|
|
66
|
+
ok,
|
|
67
|
+
}))
|
|
68
|
+
} catch (error) {
|
|
69
|
+
console.log(JSON.stringify({ ok: false, error: String(error?.message ?? error).slice(0, 160) }))
|
|
70
|
+
} finally {
|
|
71
|
+
await sidecar.shutdown().catch(() => {})
|
|
72
|
+
}
|
|
73
|
+
process.exitCode = ok ? 0 : 1
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* What a tool call costs on a Flash page, from the model's side.
|
|
4
|
+
*
|
|
5
|
+
* node scripts/check-flash-tools.mjs [url]
|
|
6
|
+
*
|
|
7
|
+
* Runs the real tool definitions against a real Flash runtime and reports time
|
|
8
|
+
* and characters for a click, a view and a type, plus whether the picture moved.
|
|
9
|
+
*/
|
|
10
|
+
import { mkdtempSync } from 'node:fs'
|
|
11
|
+
import { tmpdir } from 'node:os'
|
|
12
|
+
import { join } from 'node:path'
|
|
13
|
+
|
|
14
|
+
const url = process.argv[2] ?? 'http://account.100bt.com/game/auth/login.html?gameName=aola&action=startgame&platform=100bt'
|
|
15
|
+
process.env.DSH_INNER_BROWSER_STATE_DIR = mkdtempSync(join(tmpdir(), 'dsh-flash-tools-'))
|
|
16
|
+
|
|
17
|
+
const { Sidecar } = await import('../sidecar.js')
|
|
18
|
+
const { sidecarTools } = await import('../tools.js')
|
|
19
|
+
|
|
20
|
+
const wait = (ms) => new Promise((settle) => setTimeout(settle, ms))
|
|
21
|
+
const sidecar = new Sidecar({ profile: 'flash-tools', headless: false, flash: true })
|
|
22
|
+
const refs = { byChat: new Map() }
|
|
23
|
+
const tools = sidecarTools(sidecar, refs, {})
|
|
24
|
+
const tool = (name) => tools.find((one) => one.name === name)
|
|
25
|
+
let ok = false
|
|
26
|
+
try {
|
|
27
|
+
await sidecar.launch('about:blank')
|
|
28
|
+
await sidecar.watch(() => {})
|
|
29
|
+
await sidecar.page.navigate(url).catch(() => {})
|
|
30
|
+
await wait(9000)
|
|
31
|
+
const said = []
|
|
32
|
+
const timed = async (name, args) => {
|
|
33
|
+
const started = Date.now()
|
|
34
|
+
const value = await tool(name).execute(args, { agent: { id: 'flash-check' } })
|
|
35
|
+
const text = String(value?.text ?? '')
|
|
36
|
+
const record = { tool: name, ms: Date.now() - started, chars: text.length, text: text.slice(0, 120) }
|
|
37
|
+
said.push(record)
|
|
38
|
+
return record
|
|
39
|
+
}
|
|
40
|
+
await timed('browser_view', {})
|
|
41
|
+
/* One call is a sample, five are a cost: a plugin page blocks its main
|
|
42
|
+
thread while it draws, so a single number swings by a hundred ms. */
|
|
43
|
+
const rounds = []
|
|
44
|
+
for (let i = 0; i < 5; i += 1) {
|
|
45
|
+
rounds.push(await timed('browser_click', { x: 640, y: 420 }))
|
|
46
|
+
rounds.push(await timed('browser_type', { text: 'a' }))
|
|
47
|
+
}
|
|
48
|
+
const median = (list) => {
|
|
49
|
+
const sorted = list.map((one) => one.ms).sort((a, b) => a - b)
|
|
50
|
+
return sorted[Math.floor(sorted.length / 2)]
|
|
51
|
+
}
|
|
52
|
+
/* A read of a page just read is mostly the page again: it must cost a line,
|
|
53
|
+
not a snapshot. */
|
|
54
|
+
const viewAgain = await timed('browser_view', {})
|
|
55
|
+
const viewThird = await timed('browser_view', {})
|
|
56
|
+
const clicks = rounds.filter((one) => one.tool === 'browser_click')
|
|
57
|
+
const types = rounds.filter((one) => one.tool === 'browser_type')
|
|
58
|
+
const last = rounds[rounds.length - 1]
|
|
59
|
+
/* A step that repeats the address it read a moment ago is spend with nothing
|
|
60
|
+
to teach, so the budget is per step and tight: one line, not a page. */
|
|
61
|
+
const within = (one) => Number(one?.chars ?? 999) < 300
|
|
62
|
+
/* A page whose own requests fail still answers, but slowly: the numbers to
|
|
63
|
+
gate on are the token ones, and latency is judged only when the page came
|
|
64
|
+
up whole. Otherwise a bad minute at the site reads as a regression here. */
|
|
65
|
+
const degraded = said[0].chars < 300 || said[0].text.includes('page trouble')
|
|
66
|
+
const fast = () => degraded || (median(clicks) < 120 && median(types) < 120)
|
|
67
|
+
ok = clicks.length === 5 && fast() && clicks.every(within) && types.every(within) && Number(last?.chars ?? 0) > 0 && viewAgain.chars < said[0].chars && viewThird.chars < said[0].chars
|
|
68
|
+
console.log(JSON.stringify({ view: said[0], clickMedianMs: median(clicks), clickMaxMs: Math.max(...clicks.map((one) => one.ms)), typeMedianMs: median(types), typeMaxMs: Math.max(...types.map((one) => one.ms)), clickChars: clicks[0].chars, typeChars: types[0].chars, viewAgainChars: viewAgain.chars, viewThirdChars: viewThird.chars, viewAgainText: viewAgain.text, degraded, ok }))
|
|
69
|
+
} catch (error) {
|
|
70
|
+
console.log(JSON.stringify({ ok: false, error: String(error?.message ?? error).slice(0, 160) }))
|
|
71
|
+
} finally {
|
|
72
|
+
await sidecar.shutdown().catch(() => {})
|
|
73
|
+
}
|
|
74
|
+
process.exitCode = ok ? 0 : 1
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Can a Flash game run without ever appearing on the desktop?
|
|
4
|
+
*
|
|
5
|
+
* node scripts/check-flash-window.mjs [url]
|
|
6
|
+
*
|
|
7
|
+
* Starts the plugin's own Flash runtime on the address, then checks three
|
|
8
|
+
* things that together mean "only the column sees it": the window was never
|
|
9
|
+
* shown, the picture keeps changing, and the process is not a frontmost or
|
|
10
|
+
* Dock-bearing app. Its own profile and state directory; both are closed.
|
|
11
|
+
*/
|
|
12
|
+
import { mkdtempSync } from 'node:fs'
|
|
13
|
+
import { createHash } from 'node:crypto'
|
|
14
|
+
import { tmpdir } from 'node:os'
|
|
15
|
+
import { join } from 'node:path'
|
|
16
|
+
|
|
17
|
+
const url = process.argv[2] ?? 'http://account.100bt.com/game/auth/login.html?gameName=aola&action=startgame&platform=100bt'
|
|
18
|
+
process.env.DSH_INNER_BROWSER_STATE_DIR = mkdtempSync(join(tmpdir(), 'dsh-flash-window-'))
|
|
19
|
+
|
|
20
|
+
const { Sidecar } = await import('../sidecar.js')
|
|
21
|
+
|
|
22
|
+
/** @param {number} ms - how long. */
|
|
23
|
+
const wait = (ms) => new Promise((settle) => setTimeout(settle, ms))
|
|
24
|
+
const sidecar = new Sidecar({ profile: 'flash-window', headless: false, flash: true })
|
|
25
|
+
let ok = false
|
|
26
|
+
try {
|
|
27
|
+
const started = Date.now()
|
|
28
|
+
await sidecar.launch('about:blank')
|
|
29
|
+
await sidecar.page.navigate(url).catch(() => {})
|
|
30
|
+
await wait(10_000)
|
|
31
|
+
const base = `http://127.0.0.1:${sidecar.flashBridge}`
|
|
32
|
+
const info = await fetch(`${base}/info`).then((reply) => reply.json()).catch(() => null)
|
|
33
|
+
const grab = async () => {
|
|
34
|
+
const body = Buffer.from(await (await fetch(`${base}/frame.jpg`)).arrayBuffer())
|
|
35
|
+
return { kb: Math.round(body.length / 1024), hash: createHash('sha1').update(body).digest('hex').slice(0, 8) }
|
|
36
|
+
}
|
|
37
|
+
const one = await grab()
|
|
38
|
+
await wait(900)
|
|
39
|
+
const two = await grab()
|
|
40
|
+
const neverShown = info !== null && info.visible === false
|
|
41
|
+
const painting = info !== null && Number(info.painted ?? 0) > 10
|
|
42
|
+
const moving = one.hash !== two.hash
|
|
43
|
+
ok = neverShown && painting && moving
|
|
44
|
+
console.log(JSON.stringify({
|
|
45
|
+
url: String(info?.url ?? url).slice(0, 80),
|
|
46
|
+
visible: info?.visible,
|
|
47
|
+
painted: info?.painted,
|
|
48
|
+
frameKB: one.kb,
|
|
49
|
+
moved: moving,
|
|
50
|
+
ms: Date.now() - started,
|
|
51
|
+
ok,
|
|
52
|
+
}))
|
|
53
|
+
} catch (error) {
|
|
54
|
+
console.log(JSON.stringify({ ok: false, error: String(error?.message ?? error).slice(0, 160) }))
|
|
55
|
+
} finally {
|
|
56
|
+
await sidecar.shutdown().catch(() => {})
|
|
57
|
+
}
|
|
58
|
+
process.exitCode = ok ? 0 : 1
|
package/scripts/doctor.mjs
CHANGED
|
@@ -66,9 +66,10 @@ const state = await get(`${ROUTE}/sidecar/state`)
|
|
|
66
66
|
console.log(`host ${state === null ? `not answering under ${ROUTE}` : base}`)
|
|
67
67
|
if (state === null) {
|
|
68
68
|
console.log(' start the app, or set DSH_BRIDGE to the app address')
|
|
69
|
-
process.
|
|
69
|
+
process.exitCode = 0
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
if (state !== null) {
|
|
72
73
|
console.log(`browser ${state.alive === true ? `attached, ${state.headless === true ? 'headless' : 'with a window'}` : 'not running'}`)
|
|
73
74
|
const profileDir = join(PROFILES, String(state.profile ?? ''))
|
|
74
75
|
console.log(`profile ${state.profile} in ${profileDir}`)
|
|
@@ -87,3 +88,4 @@ if (existsSync(portFile)) {
|
|
|
87
88
|
}
|
|
88
89
|
|
|
89
90
|
if (String(state.error ?? '') !== '') console.log(`error ${state.error}`)
|
|
91
|
+
}
|