dsh-inner-browser-use 0.34.28 → 0.34.96
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 +223 -23
- 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 +186 -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 +1098 -96
- package/tools.js +1068 -144
package/docs/ROADMAP.md
CHANGED
|
@@ -50,7 +50,7 @@ stealing the column from other chats.
|
|
|
50
50
|
|
|
51
51
|
## Phase 2 — one call, many steps (target: a login flow in 1 tool call)
|
|
52
52
|
|
|
53
|
-
- `
|
|
53
|
+
- `browser_do`: ordered steps with per-step waits (`wait_text`, `wait_gone`,
|
|
54
54
|
`wait_idle`, `wait_url`), one result: what changed, the final URL, tokens spent
|
|
55
55
|
on one read instead of five.
|
|
56
56
|
- Auto-wait after every action: navigation start/finish, DOM quiet, and
|
|
@@ -65,7 +65,7 @@ stealing the column from other chats.
|
|
|
65
65
|
- File upload (`DOM.setFileInputFiles`), download capture with a returned path,
|
|
66
66
|
print-to-PDF, full-page and element screenshots.
|
|
67
67
|
- Tabs: new / switch / close / list; iframes addressed by frame id; dialog
|
|
68
|
-
accept/dismiss; `
|
|
68
|
+
accept/dismiss; `browser_eval` for the last 1% of cases; cookies read/write for
|
|
69
69
|
debugging; console + failed requests surfaced in the state read.
|
|
70
70
|
- Keyboard: modifiers, shortcuts, clipboard paste, IME composition unchanged.
|
|
71
71
|
|
|
@@ -86,14 +86,14 @@ stealing the column from other chats.
|
|
|
86
86
|
|
|
87
87
|
- Human takeover (0.17.1): real input in the pane (click / text / wheel / key)
|
|
88
88
|
holds the agent's next action for 6 s and says so; hover and move do not count;
|
|
89
|
-
reads stay open; `
|
|
89
|
+
reads stay open; `browser_resume` takes the page back at once.
|
|
90
90
|
- Guarded actions (upload, download, close tab, buy-ish submits) can require a
|
|
91
91
|
confirm flag. Shipped in 0.14.0 for the narrow case that matters most: an
|
|
92
92
|
overlay in the way is cleared only when its own way out is a safe word
|
|
93
93
|
(accept / agree / close / 同意 / 关闭 …) and the overlay says nothing about
|
|
94
94
|
deleting, paying or signing out; otherwise it is reported, never taken.
|
|
95
95
|
Since 0.15.0 the same clearing runs before every control-naming step of a
|
|
96
|
-
`
|
|
96
|
+
`browser_do` batch, so a multi-step flow through a cookie wall fails zero times.
|
|
97
97
|
- Crash recovery: attach to a still-running Chrome, rebuild the page, keep the
|
|
98
98
|
profile; a killed host never leaves a zombie Chrome.
|
|
99
99
|
- Every failure names what happened, what state the page is in, and the next
|
|
@@ -124,10 +124,10 @@ finding the control again by identity (kind + exact words + link destination),
|
|
|
124
124
|
refusing when the words are not unique. Measured at 462 ms against 445 ms for a
|
|
125
125
|
fresh control — 17 ms when needed, nothing when not.
|
|
126
126
|
|
|
127
|
-
Since 0.16.1 the count is visible: `
|
|
127
|
+
Since 0.16.1 the count is visible: `browser_state` reports `refs followed N`,
|
|
128
128
|
and breadth asserts it moved after a redraw, so the next rounds can tell from
|
|
129
129
|
real sites whether redrawing is frequent enough to justify more work here.
|
|
130
|
-
Next in this direction: the same identity recovery for `
|
|
130
|
+
Next in this direction: the same identity recovery for `browser_do` steps that
|
|
131
131
|
name a field by its label when the label element is rebuilt (today the step
|
|
132
132
|
falls back to a fresh read).
|
|
133
133
|
|
|
@@ -142,7 +142,7 @@ then repeat the loop with whatever the numbers say is worst.
|
|
|
142
142
|
Two things were wrong at the edge of the tool surface, and both were only
|
|
143
143
|
reachable on real sites.
|
|
144
144
|
|
|
145
|
-
The first was that the shape of the browser could not change. `
|
|
145
|
+
The first was that the shape of the browser could not change. `browser_open`
|
|
146
146
|
accepted `headless: false` and answered success; when a browser was already
|
|
147
147
|
running nothing happened, because the flag was only read at launch. A site that
|
|
148
148
|
refuses a windowless browser (chatgpt.com on this machine: a "just a moment"
|
|
@@ -150,7 +150,7 @@ page forever, versus a login page in a windowed one) therefore had no way out
|
|
|
150
150
|
except killing a process by hand. Since 0.17.0 a request for the other shape
|
|
151
151
|
restarts the browser on the same profile, cookie jar and address — 21.6 s
|
|
152
152
|
windowless → windowed, 22.1 s back, 0 ms when the shape already matches — and
|
|
153
|
-
both `
|
|
153
|
+
both `browser_open` and `browser_state` say which shape is in front of the
|
|
154
154
|
person.
|
|
155
155
|
|
|
156
156
|
The second was silence on a check page. A site asking for proof of a person is
|
|
@@ -181,7 +181,7 @@ The tool used to treat "the control that says Email" as "the field called
|
|
|
181
181
|
Email". Real forms separate the two, so the writing step now resolves the words
|
|
182
182
|
to the element that takes text, and says which one it used.
|
|
183
183
|
|
|
184
|
-
Next in this direction: the same resolution for `
|
|
184
|
+
Next in this direction: the same resolution for `browser_type` when the caller
|
|
185
185
|
names a field by its words instead of a ref, and for date/time pickers where the
|
|
186
186
|
visible box is a div and the real input is hidden beside it.
|
|
187
187
|
|
|
@@ -189,15 +189,15 @@ visible box is a div and the real input is hidden beside it.
|
|
|
189
189
|
|
|
190
190
|
Typing by name is in. The same resolution is what other steps should be able to
|
|
191
191
|
use when the caller knows the destination but not the number — the next place
|
|
192
|
-
worth doing this is `
|
|
192
|
+
worth doing this is `browser_select` (choose an option by the field it belongs
|
|
193
193
|
to), and after that the date/time case where the visible box is a div and the
|
|
194
194
|
real input sits hidden beside it.
|
|
195
195
|
|
|
196
196
|
## Phase 12 — the same words, the same place, 0.21.0
|
|
197
197
|
|
|
198
|
-
`
|
|
198
|
+
`browser_select` joined `browser_type` and `browser_do` on the shared resolver.
|
|
199
199
|
Left in this direction: the date/time case where the box the user sees is a div
|
|
200
|
-
and the real input is hidden beside it, and `
|
|
200
|
+
and the real input is hidden beside it, and `browser_click` on a control whose
|
|
201
201
|
words are split across sibling nodes (`<span>Sign</span><span>in</span>`).
|
|
202
202
|
|
|
203
203
|
## Phase 13 — the page's spelling is not the caller's, 0.22.0
|
|
@@ -216,7 +216,7 @@ name, so the fallback has to be a picture plus a click, not a name lookup.
|
|
|
216
216
|
|
|
217
217
|
## Phase 16 — a file, not a path, 0.25.0
|
|
218
218
|
|
|
219
|
-
`
|
|
219
|
+
`browser_grab` returns a path. Next: say one line about the picture (size, and
|
|
220
220
|
whether it is mostly one colour) so a model that cannot open files still learns
|
|
221
221
|
"the chart is blank" without a full read.
|
|
222
222
|
|
|
@@ -248,7 +248,7 @@ at -3000,-3000, and the column shows the only picture.
|
|
|
248
248
|
|
|
249
249
|
## Phase 21 — a review round that closes the loop, 0.30.0
|
|
250
250
|
|
|
251
|
-
`
|
|
251
|
+
`browser_ask` sends and returns an answer. Next: a round trip that carries the
|
|
252
252
|
repository address there and the review back, and records in the round log which
|
|
253
253
|
commit the review was about, so a later round can tell whether the advice was
|
|
254
254
|
already taken.
|
|
@@ -298,6 +298,12 @@ The phases above were written one at a time as the work happened; the run from
|
|
|
298
298
|
|
|
299
299
|
## 页游这一档(2026-10-28 起)
|
|
300
300
|
|
|
301
|
+
- 操作方式已分叉(0.34.29):`browser_mode` 每个聊天记一种,`human` / `dom` 单次可覆盖。页游默认 human,登录/表单默认 dom 更省事。
|
|
302
|
+
- Flash 判死:headless Chrome 里 Ruffle 交得上 swf 但起不了画面(26 秒,canvas 0、metadata 0),不承诺。真页游只能走站方 H5 / 微端 / 老 360 浏览器,页面上线前先用 `scripts/probe-flash.mjs` 问一句它要不要 Flash。
|
|
303
|
+
- 开玩前先体检:`browser_prepare` 一次调用给出输入面、帧率、按键是否真的到得了框架、以及 Flash 结论(0.34.30)。4399《怪物小汽车》实测页面 58 fps、框架 59 fps —— 不少「Flash 小游戏」其实已经是 H5,先体检再下结论;`npm run ready <url>` 可复现。
|
|
304
|
+
- 读 SWF 标签流(`npm run swf`):尺寸、帧率、AVM 版本、资源构成,FWS/CWS 都读,ZWS 明确报不支持(0.34.32)。
|
|
305
|
+
- 空白画面的两种原因分开报(`viewers` / `listening chats` / `asks.log`),并在「浏览器在跑、没人画」时自己向所属会话补发一次开页签请求(0.34.31)。
|
|
306
|
+
|
|
301
307
|
输入这一层已经到位:按住键、组合键、按住鼠标,一项 300 ms 的按住整次调用 305 ms,事件由页面自己证实。实时画面沿用同一条 CDP 帧路(60 fps、镜像延迟中位数 8 ms)。
|
|
302
308
|
|
|
303
309
|
还差的一层是「看」:4399 / 7k7k / 奥拉星这类页游把画面画在 canvas 上,文本树里没有可读的东西,判断走位、Boss 血条、按钮位置都要靠像素。本会话的模型没有视觉能力,所以这里的正确做法不是假装能玩,而是把可判读的信号做出来:canvas 局部像素采样、颜色/区域变化率、画面是否在动(`blind` 行已经在报 canvas 数量)。有视觉能力的模型接着用同一套帧路就能玩。
|
|
@@ -311,7 +317,7 @@ Ranked by what a real task still loses:
|
|
|
311
317
|
HTTP and by pid, not by a person clicking. A browser-level test that opens the
|
|
312
318
|
column, kills the driver, and clicks restore is the missing evidence.
|
|
313
319
|
2. **One-click "open here" when this chat has no seat.** `state.chats` can lack this
|
|
314
|
-
chat, and the column then only tells the model to call `
|
|
320
|
+
chat, and the column then only tells the model to call `browser_open`. The button
|
|
315
321
|
belongs in the column.
|
|
316
322
|
3. **Competitor behaviours still unmatched.** Playwright MCP's accessibility refs are
|
|
317
323
|
ours, and the blind line covers the vision-less read; Stagehand v4's
|
package/docs/VERIFY.md
CHANGED
|
@@ -33,7 +33,7 @@ Expected before any tool call: `"alive":false`, `"profile":"default"`,
|
|
|
33
33
|
|
|
34
34
|
## 2. A tool opens its own page
|
|
35
35
|
|
|
36
|
-
Ask the model `用
|
|
36
|
+
Ask the model `用 browser_open 打开 https://www.bing.com`, or call it yourself.
|
|
37
37
|
Expected: the result names the address and title, and this chat's right column
|
|
38
38
|
shows the picture in a tab called **后台浏览器** or **Background browser**.
|
|
39
39
|
`doctor.mjs` now reads `attached, headless`, `viewers 1`, frames climbing.
|
|
@@ -43,8 +43,8 @@ shows the picture in a tab called **后台浏览器** or **Background browser**.
|
|
|
43
43
|
- `"alive":false` with a non-empty `"error"`: Chrome did not start. Check the
|
|
44
44
|
binary at a listed path, and on macOS that the launch carried
|
|
45
45
|
`--use-mock-keychain`.
|
|
46
|
-
- Frames flat while viewers is 1: the screencast stalled. `
|
|
47
|
-
`
|
|
46
|
+
- Frames flat while viewers is 1: the screencast stalled. `browser_stop`, then
|
|
47
|
+
`browser_open` again.
|
|
48
48
|
- `"pointer":null`: no tool input yet; it fills on the first click or move.
|
|
49
49
|
|
|
50
50
|
## 3. Only the chat that asked
|
|
@@ -60,18 +60,28 @@ A mirror that opens in every chat means the page half is not comparing
|
|
|
60
60
|
- **Hover** the search field: your cursor turns into an I-beam, the black arrow
|
|
61
61
|
does not move. An arrow that follows the mouse means the pointer paths are
|
|
62
62
|
crossed.
|
|
63
|
-
- **Type** Chinese with `
|
|
63
|
+
- **Type** Chinese with `browser_type { "text": "…", "submit": true }`: the
|
|
64
64
|
results page appears and the field holds characters, not boxes.
|
|
65
|
-
- **Backspace** with `
|
|
65
|
+
- **Backspace** with `browser_key { "key": "Backspace" }` in a filled field:
|
|
66
66
|
one character goes. Nothing happening means the key reached the document.
|
|
67
|
-
- **Screenshot** with `
|
|
67
|
+
- **Screenshot** with `browser_shot {}`: an absolute path under
|
|
68
68
|
`~/.dsh-inner-browser-use/screenshots`. A black picture means the screencast
|
|
69
69
|
was not running when the frame was taken.
|
|
70
|
+
- **The whole keyboard, on this machine**: `npm run keys` starts a real
|
|
71
|
+
background Chrome, presses all 150 keys into a field, and checks what the page
|
|
72
|
+
reported for each one (`key`, `code`, `keyCode`, shift) plus what the field
|
|
73
|
+
typed. It then runs copy / cut / paste / select-all / undo / redo, caret keys,
|
|
74
|
+
Tab focus, Enter, and a 180 ms hold. One JSON line, exit code 0 when every key
|
|
75
|
+
matched. CI runs it on macOS and Windows in the `keys` job.
|
|
76
|
+
- **A shortcut by hand**: type `Hi`, press `browser_key { "key": "Mod+a" }`,
|
|
77
|
+
type `X`: the field holds `X` and nothing before it. `Mod` is Command on macOS
|
|
78
|
+
and Control elsewhere; `Ctrl+C/X/V/A/Z/Y` and `Ctrl+Shift+Z` run the same
|
|
79
|
+
editing commands a person would.
|
|
70
80
|
|
|
71
81
|
## 5. Stop, then open again
|
|
72
82
|
|
|
73
|
-
`
|
|
74
|
-
`
|
|
83
|
+
`browser_stop` reads `"alive":false` and leaves the profile alone.
|
|
84
|
+
`browser_open https://www.bing.com` brings the same profile back, still signed
|
|
75
85
|
in from the first pass.
|
|
76
86
|
|
|
77
87
|
Signed out after the restart: Chrome came up on another profile, or was killed
|
|
@@ -140,13 +150,13 @@ broke. Two of them cover a cross-site frame, which Chrome runs in its own
|
|
|
140
150
|
process. Three cover a control under an overlay: the click clears a cookie
|
|
141
151
|
banner and lands (`under clicked`), typing into a field under the same banner
|
|
142
152
|
works, and a modal that offers to delete the account is reported without
|
|
143
|
-
anything being pressed. Two more drive the same overlay through `
|
|
153
|
+
anything being pressed. Two more drive the same overlay through `browser_do`:
|
|
144
154
|
a batch step clears the banner before it clicks, and a batch step fills a field
|
|
145
155
|
that was under it. One more counts the recoveries: after the redraw check, the
|
|
146
156
|
state line says at least one ref was followed to its new number. Three cover a
|
|
147
157
|
hand on the mirror: while the person is using it a read still works
|
|
148
158
|
(`hold-allows-read`), an action is refused and names both ways out
|
|
149
|
-
(`hold-refuses-action`), and `
|
|
159
|
+
(`hold-refuses-action`), and `browser_resume` hands the page back so the same
|
|
150
160
|
click goes through (`resume-lets-go`).
|
|
151
161
|
|
|
152
162
|
## Who the mirror serves
|
package/index.js
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* learns no port and no secret. // ui-copy-ignore
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
-
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
23
|
+
import { appendFileSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
24
24
|
import { homedir } from 'node:os'
|
|
25
25
|
import { join } from 'node:path'
|
|
26
26
|
import { Sidecar } from './sidecar.js?v=1'
|
|
@@ -106,6 +106,17 @@ export class Bridge {
|
|
|
106
106
|
this.wanted = { session: kept.session, at: kept.at }
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
+
/**
|
|
110
|
+
* A chat is the chat that asked, and nothing else. Guessing at an alias once
|
|
111
|
+
* put the mirror in a stranger's column: two conversations are never the same
|
|
112
|
+
* conversation, however few pages happen to be on screen.
|
|
113
|
+
* @param {string} session - whatever id the caller has.
|
|
114
|
+
* @returns {string} the same id.
|
|
115
|
+
*/
|
|
116
|
+
canon(session) {
|
|
117
|
+
return String(session ?? '')
|
|
118
|
+
}
|
|
119
|
+
|
|
109
120
|
/**
|
|
110
121
|
* Give the mirror to one chat and take it away from every earlier one.
|
|
111
122
|
* @param {string} sessionId - chat that asked for it.
|
|
@@ -123,11 +134,12 @@ export class Bridge {
|
|
|
123
134
|
* @returns {boolean} whether a live tab took the frame.
|
|
124
135
|
*/
|
|
125
136
|
want(sessionId, at = Date.now()) {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
this.
|
|
129
|
-
|
|
130
|
-
|
|
137
|
+
const id = this.canon(sessionId)
|
|
138
|
+
if (id === '') return false
|
|
139
|
+
this.claim(id)
|
|
140
|
+
this.wanted = { session: id, at }
|
|
141
|
+
if (this.persist === true) writeMirror({ owner: this.owner, session: id, at })
|
|
142
|
+
return this.push(id, { type: 'sidecar-wanted', session: id, at })
|
|
131
143
|
}
|
|
132
144
|
|
|
133
145
|
/**
|
|
@@ -195,7 +207,7 @@ export class Bridge {
|
|
|
195
207
|
/** Page read: address, title, controls, then text, as JSON the Host parses. */ // ui-copy-ignore
|
|
196
208
|
/* The read model and the tool surface live in their own files: this module is
|
|
197
209
|
the Host half (routes, bridge, lifetimes), not the model's vocabulary. */ // ui-copy-ignore
|
|
198
|
-
import {
|
|
210
|
+
import { keyEvent, sidecarTools, sessionScope, workScope } from './tools.js?v=1'
|
|
199
211
|
|
|
200
212
|
/**
|
|
201
213
|
* A state poll may pick up a browser that outlived a reload, and only for the
|
|
@@ -252,6 +264,36 @@ export function shouldAdoptOnState(said, chat, owner) {
|
|
|
252
264
|
return said?.alive === false && chat !== '' && owner === chat
|
|
253
265
|
}
|
|
254
266
|
|
|
267
|
+
/**
|
|
268
|
+
* Whether a running browser still needs its column told to draw it.
|
|
269
|
+
* @param {{alive: boolean, owner: string, wanted: string, listeners: number}} what - current facts.
|
|
270
|
+
* @returns {boolean} true when the owner is known, the browser runs, and nothing listens.
|
|
271
|
+
*/
|
|
272
|
+
export function wantsMirrorAgain(what) {
|
|
273
|
+
if (what?.alive !== true) return false
|
|
274
|
+
const owner = String(what?.owner ?? '')
|
|
275
|
+
if (owner === '') return false
|
|
276
|
+
if (String(what?.wanted ?? '') !== owner) return false
|
|
277
|
+
return Number(what?.listeners ?? 0) <= 0
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Whether asking again can achieve anything. A pane that is polling this very
|
|
282
|
+
* endpoint is awake and already knows the frames are missing, so a second ask
|
|
283
|
+
* every few seconds only writes log lines; a pane that went quiet may have lost
|
|
284
|
+
* the stream and does need one. One ask per gap at most.
|
|
285
|
+
* @param {any} what - { askedAgo, sinceLast, quiet, gap } in milliseconds.
|
|
286
|
+
* @returns {boolean} true when the ask should be sent now.
|
|
287
|
+
*/
|
|
288
|
+
export function nudgeDue(what) {
|
|
289
|
+
const askedAgo = Number(what?.askedAgo ?? -1)
|
|
290
|
+
const sinceLast = Number(what?.sinceLast ?? Infinity)
|
|
291
|
+
const quiet = Number(what?.quiet ?? 15_000)
|
|
292
|
+
const gap = Number(what?.gap ?? 10_000)
|
|
293
|
+
if (!Number.isFinite(askedAgo) || askedAgo < quiet) return false
|
|
294
|
+
return sinceLast >= gap
|
|
295
|
+
}
|
|
296
|
+
|
|
255
297
|
export const name = 'dsh-inner-browser-use'
|
|
256
298
|
export const inject = ['webServer']
|
|
257
299
|
|
|
@@ -268,6 +310,31 @@ export function apply(ctx) {
|
|
|
268
310
|
/** @type {{refs: any[], at: number, last?: any}} */
|
|
269
311
|
const sidecarRefs = { refs: [], at: 0 }
|
|
270
312
|
ctx.effect(() => () => bridge.dispose(), 'dsh-inner-browser-use: bridge lifetime')
|
|
313
|
+
/* A live picture that nobody is drawing is the one failure people notice
|
|
314
|
+
first: the browser is fine, the column just lost its pane. The chat that
|
|
315
|
+
owns the browser is asked again, on a slow beat, until the frames stream
|
|
316
|
+
has a listener — the client keeps the rule that only the mounted chat's
|
|
317
|
+
column may open. */
|
|
318
|
+
/** @type {Map<string, number>} when each chat last polled for state. */
|
|
319
|
+
const lastAsk = new Map()
|
|
320
|
+
ctx.effect(() => {
|
|
321
|
+
let last = 0
|
|
322
|
+
const timer = setInterval(() => {
|
|
323
|
+
try {
|
|
324
|
+
const owner = bridge.owner
|
|
325
|
+
const listeners = sidecar.page?.frames?.size ?? 0
|
|
326
|
+
const again = wantsMirrorAgain({ alive: sidecar.page !== null && sidecar.page !== undefined, owner, wanted: bridge.wanted.session, listeners }) // ui-copy-ignore
|
|
327
|
+
if (again !== true) return
|
|
328
|
+
const due = nudgeDue({ askedAgo: Date.now() - (lastAsk.get(owner) ?? 0), sinceLast: Date.now() - last }) // ui-copy-ignore
|
|
329
|
+
if (due !== true) return
|
|
330
|
+
last = Date.now()
|
|
331
|
+
bridge.want(owner)
|
|
332
|
+
appendFileSync(join(STATE_DIR, 'asks.log'), `${new Date().toISOString()} asked ${owner.slice(-8)} to draw the mirror again\n`)
|
|
333
|
+
} catch {}
|
|
334
|
+
}, 4000)
|
|
335
|
+
timer.unref?.()
|
|
336
|
+
return () => clearInterval(timer)
|
|
337
|
+
}, 'dsh-inner-browser-use: mirror keepalive')
|
|
271
338
|
/* Closing the browser cleanly on quit is what keeps a sign-in alive: a
|
|
272
339
|
killed Chrome never writes the cookies a page set this minute. */
|
|
273
340
|
ctx.effect(() => {
|
|
@@ -285,6 +352,10 @@ export function apply(ctx) {
|
|
|
285
352
|
/** @param {import('node:http').IncomingMessage} req - request. @param {import('node:http').ServerResponse} res - response. */
|
|
286
353
|
/** @type {{at: number, path: string, session: string}[]} the last few calls the column made. */
|
|
287
354
|
const recent = []
|
|
355
|
+
/* Whether the pane is asking at all is the difference between "the browser
|
|
356
|
+
stopped" and "nothing is drawing it": the poll is every 2.5 s, so a chat
|
|
357
|
+
that asks is a chat with a mounted pane. Kept on disk because the answer
|
|
358
|
+
has to survive a plugin reload, which is when the question is asked. */
|
|
288
359
|
const handler = async (req, res) => {
|
|
289
360
|
const url = new URL(req.url ?? ROUTE, 'http://dsh.invalid')
|
|
290
361
|
const path = url.pathname.slice(ROUTE.length) || '/'
|
|
@@ -334,6 +405,8 @@ export function apply(ctx) {
|
|
|
334
405
|
const asked = String(url.searchParams.get('session') ?? '')
|
|
335
406
|
const watching = asked !== '' ? asked : sessionId
|
|
336
407
|
if (held(watching) && refuse()) return
|
|
408
|
+
/* Only the owner's own page may stream the picture. */
|
|
409
|
+
const streaming = watching
|
|
337
410
|
res.writeHead(200, {
|
|
338
411
|
'content-type': 'text/event-stream; charset=utf-8',
|
|
339
412
|
'cache-control': 'no-cache, no-transform',
|
|
@@ -353,7 +426,7 @@ export function apply(ctx) {
|
|
|
353
426
|
page wait: the newest picture is the only one worth sending. */
|
|
354
427
|
if (res.writableLength > 3 * 1024 * 1024) return
|
|
355
428
|
res.write(`data: ${JSON.stringify(frame)}\n\n`)
|
|
356
|
-
},
|
|
429
|
+
}, streaming)
|
|
357
430
|
const info = await sidecar.state()
|
|
358
431
|
/* The pane needs one address to offer back when this chat's own page
|
|
359
432
|
went away; the state route answers the same question for the tab
|
|
@@ -426,6 +499,15 @@ export function apply(ctx) {
|
|
|
426
499
|
said = await sidecar.state()
|
|
427
500
|
} catch {}
|
|
428
501
|
}
|
|
502
|
+
if (chat !== '') {
|
|
503
|
+
const before = lastAsk.get(chat) ?? 0
|
|
504
|
+
if (Date.now() - before > 2000) {
|
|
505
|
+
lastAsk.set(chat, Date.now())
|
|
506
|
+
try {
|
|
507
|
+
appendFileSync(join(STATE_DIR, 'asks.log'), `${new Date().toISOString()} ${chat.slice(-8)} alive=${String(said?.alive === true)} viewers=${String(said?.viewers ?? 0)}\n`)
|
|
508
|
+
} catch {}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
429
511
|
json(200, { ...said, ...(chat === '' ? {} : { chat: mineChat ?? (await sidecar.chatState(chat)) }), owner: bridge.owner, chats: [...bridge.seats.keys()], recent: mine })
|
|
430
512
|
return
|
|
431
513
|
}
|
|
@@ -463,6 +545,38 @@ export function apply(ctx) {
|
|
|
463
545
|
return
|
|
464
546
|
}
|
|
465
547
|
|
|
548
|
+
if (path === '/sidecar/viewport' && req.method === 'POST') {
|
|
549
|
+
/* The pane tells the page how big it is, so the picture fills the column
|
|
550
|
+
instead of being letterboxed inside it. Only the owner may resize what
|
|
551
|
+
its own mirror shows. */
|
|
552
|
+
let body
|
|
553
|
+
try {
|
|
554
|
+
body = JSON.parse(await readBody(req))
|
|
555
|
+
} catch {
|
|
556
|
+
json(400, { ok: false, text: 'bad frame' }) // ui-copy-ignore
|
|
557
|
+
return
|
|
558
|
+
}
|
|
559
|
+
const actor = acting(body)
|
|
560
|
+
if (actor === '') {
|
|
561
|
+
json(403, { ok: false, text: 'this chat does not own the browser' }) // ui-copy-ignore
|
|
562
|
+
return
|
|
563
|
+
}
|
|
564
|
+
try {
|
|
565
|
+
/* The shape belongs to the page this chat is actually looking at: a
|
|
566
|
+
chat driving a tab of its own must not resize the browser's base
|
|
567
|
+
page and keep a letterboxed picture. */
|
|
568
|
+
const page = sidecar.pageFor(actor) ?? sidecar.page
|
|
569
|
+
if (page === null || page === undefined) {
|
|
570
|
+
json(409, { ok: false, text: 'the browser is not open' }) // ui-copy-ignore
|
|
571
|
+
return
|
|
572
|
+
}
|
|
573
|
+
json(200, { ok: true, view: await page.setViewport(body.w, body.h) })
|
|
574
|
+
} catch (error) {
|
|
575
|
+
json(500, { ok: false, text: String(error?.message ?? error) })
|
|
576
|
+
}
|
|
577
|
+
return
|
|
578
|
+
}
|
|
579
|
+
|
|
466
580
|
if (path === '/sidecar/input' && req.method === 'POST') {
|
|
467
581
|
let body
|
|
468
582
|
try {
|
|
@@ -495,21 +609,26 @@ export function apply(ctx) {
|
|
|
495
609
|
} else if (kind === 'wheel') await page.wheel({ x, y, dx: Number(body.dx ?? 0), dy: Number(body.dy ?? 0) })
|
|
496
610
|
else if (kind === 'text') await page.insertText(String(body.text ?? ''))
|
|
497
611
|
else if (kind === 'key') {
|
|
498
|
-
const
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
modifiers: Number(body.modifiers ?? 0),
|
|
506
|
-
}
|
|
507
|
-
await page.key('keyDown', spec)
|
|
508
|
-
await page.key('keyUp', { ...spec, text: undefined })
|
|
612
|
+
const key = keyEvent(body)
|
|
613
|
+
if (key.down !== true) await page.key('keyUp', { ...key.spec, text: undefined })
|
|
614
|
+
else if (key.repeat === true) {
|
|
615
|
+
/* Typing repeat: the page draws a held key as a fresh keydown. */
|
|
616
|
+
await page.key('keyDown', { ...key.spec, autoRepeat: true })
|
|
617
|
+
} else if (key.command === null) await page.key('keyDown', key.spec)
|
|
618
|
+
else await page.key('keyDown', { ...key.spec, text: undefined, commands: [key.command] })
|
|
509
619
|
}
|
|
510
620
|
/* Hovering and moving are looking, not acting: only real input counts as
|
|
511
621
|
the person taking the page. */
|
|
512
|
-
if (kind !== 'hover' && kind !== 'move')
|
|
622
|
+
if (kind !== 'hover' && kind !== 'move') {
|
|
623
|
+
sidecar.humanUse({ kind, x, y })
|
|
624
|
+
/* The arrow goes now, not on the next repaint: a still page may not
|
|
625
|
+
paint again for a second and a half. */
|
|
626
|
+
try {
|
|
627
|
+
page.announceHold()
|
|
628
|
+
} catch {
|
|
629
|
+
/* the stream is gone; the next frame carries the same answer */
|
|
630
|
+
}
|
|
631
|
+
}
|
|
513
632
|
json(200, cursor === undefined ? { ok: true } : { ok: true, cursor })
|
|
514
633
|
} catch (error) {
|
|
515
634
|
json(500, { ok: false, text: String(error?.message ?? error) })
|
|
@@ -545,6 +664,44 @@ export function apply(ctx) {
|
|
|
545
664
|
return
|
|
546
665
|
}
|
|
547
666
|
|
|
667
|
+
if (path === '/sidecar/tabs' && req.method === 'GET') {
|
|
668
|
+
/* This chat's windows only: another chat's pages never enter this list. */
|
|
669
|
+
const who = acting({ session: url.searchParams.get('session') })
|
|
670
|
+
if (who === '') return
|
|
671
|
+
try {
|
|
672
|
+
const tabs = await sidecar.windowsFor(who)
|
|
673
|
+
json(200, { ok: true, tabs, ...(url.searchParams.get('session') === '' ? { session: who } : {}) })
|
|
674
|
+
} catch (error) {
|
|
675
|
+
json(500, { ok: false, text: String(error?.message ?? error) })
|
|
676
|
+
}
|
|
677
|
+
return
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
if (path === '/sidecar/tabs' && req.method === 'POST') {
|
|
681
|
+
let body
|
|
682
|
+
try {
|
|
683
|
+
body = JSON.parse(await readBody(req))
|
|
684
|
+
} catch {
|
|
685
|
+
json(400, { ok: false, text: 'bad frame' })
|
|
686
|
+
return
|
|
687
|
+
}
|
|
688
|
+
const actor = acting(body)
|
|
689
|
+
if (actor === '') return
|
|
690
|
+
try {
|
|
691
|
+
const at = Number(body.index ?? -1)
|
|
692
|
+
if (body.op === 'close') {
|
|
693
|
+
await sidecar.closeTab(at, actor)
|
|
694
|
+
} else {
|
|
695
|
+
await sidecar.useTab(at, actor)
|
|
696
|
+
}
|
|
697
|
+
const tabs = await sidecar.windowsFor(actor)
|
|
698
|
+
json(200, { ok: true, tabs })
|
|
699
|
+
} catch (error) {
|
|
700
|
+
json(500, { ok: false, text: String(error?.message ?? error) })
|
|
701
|
+
}
|
|
702
|
+
return
|
|
703
|
+
}
|
|
704
|
+
|
|
548
705
|
if (path === '/sidecar/forget' && req.method === 'POST') {
|
|
549
706
|
/* Closing the tab is an answer: coming back should not reopen it. Only the
|
|
550
707
|
chat that holds the mirror may withdraw its own ask. */
|
|
@@ -668,11 +825,16 @@ export function apply(ctx) {
|
|
|
668
825
|
name: 'browser-in-this-chat',
|
|
669
826
|
order: 858,
|
|
670
827
|
text: [
|
|
671
|
-
'##
|
|
828
|
+
'## 网页任务:人明确点名才开右栏浏览器',
|
|
829
|
+
'右栏「后台浏览器」是一台独立的后台 Chrome,模型能点、能读、能执行脚本;画面只出现在本会话右栏,不占别的会话。它只做「必须动手」的活,默认不开。',
|
|
672
830
|
'',
|
|
673
|
-
'
|
|
674
|
-
'
|
|
675
|
-
'
|
|
831
|
+
'只有人明确说要看网页/用浏览器时才开始:「用网页端 / 在浏览器里 / 打开某个网站 / 上 ChatGPT 生成 / 帮我在这个页面点…」。人只给了链接、没说要用浏览器时,先用 `web_fetch` 或 envoy 读;读不到(登录墙、空壳、要交互)就说明原因再问一句,不要自己开。',
|
|
832
|
+
'不要用它:读公开文章、单个链接正文、文档、搜索结果摘要、GitHub 仓库或 PR 页面——这些能用 `web_fetch` / envoy 读就不用浏览器;一打开就是一个后台 Chrome 进程加一个页签,会话多了很占内存。',
|
|
833
|
+
'也不要用它做连通性、可达性、服务是否起来的检查:那是一个 HTTP 请求的事,`web_fetch` 或 `curl` 就够,开浏览器只会多一个进程和一栏画面。只有「这一步必须在真实浏览器里点/填/登录」才轮到它。',
|
|
834
|
+
'人点名之后不问、不另起外部 Chrome;画面留在本会话右栏,做完保留,不主动关。',
|
|
835
|
+
'先接着用已有的页:`browser_view`(拿编号控件与可见文字)→ `browser_click` / `browser_type` → 再 `browser_view` 直到出现要的结果;页面要等就重复读,不 sleep。`browser_open` 只用于没有页、要换新地址、或要明确刷新;同一个地址不要反复重开。',
|
|
836
|
+
'页面很长、只想确认某句话/按钮在哪时用 `browser_find`,别整页读。要读长文(文章、文档、issue、论坛帖)用 `browser_read`:它回 Markdown,只花几十分之一的 token,链接可点;要动手点、填表才用 `browser_view` 拿编号。',
|
|
837
|
+
'要保存页面样子用 `browser_shot`,它会给出 PNG 绝对路径;要交给人看的文件,把绝对路径写进回复。',
|
|
676
838
|
].join('\n'),
|
|
677
839
|
}), 'dsh-inner-browser-use: system prompt section')
|
|
678
840
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-inner-browser-use",
|
|
3
|
-
"version": "0.34.
|
|
3
|
+
"version": "0.34.96",
|
|
4
4
|
"description": "An agent-owned Chromium sidecar mirrored into the DeepSeek Harness right column: the agent drives it, the person watches and can take the mouse, and logins survive restarts.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -22,6 +22,11 @@
|
|
|
22
22
|
"cordis.patch.yml",
|
|
23
23
|
"icon.svg",
|
|
24
24
|
"scripts/doctor.mjs",
|
|
25
|
+
"scripts/check-flash-window.mjs",
|
|
26
|
+
"scripts/check-flash-mirror.mjs",
|
|
27
|
+
"scripts/check-flash-local.mjs",
|
|
28
|
+
"scripts/check-flash-tools.mjs",
|
|
29
|
+
"runtime/",
|
|
25
30
|
"docs/",
|
|
26
31
|
"README.md",
|
|
27
32
|
"README.zh.md",
|
|
@@ -43,12 +48,21 @@
|
|
|
43
48
|
"dsh": ">=0.2.0"
|
|
44
49
|
},
|
|
45
50
|
"scripts": {
|
|
46
|
-
"test": "node --test test
|
|
51
|
+
"test": "node --test test/artifacts.test.mjs test/client.test.mjs test/cookies.test.mjs test/diff.test.mjs test/fresh.test.mjs test/host.test.mjs test/input.test.mjs test/isolation.test.mjs test/keyboard.test.mjs test/leak.test.mjs test/login.test.mjs test/no-duplicate.test.mjs test/package.test.mjs test/readfault.test.mjs test/refs.test.mjs test/review.test.mjs test/session.test.mjs test/settle.test.mjs test/sidecar.test.mjs test/swf.test.mjs test/tabs.test.mjs test/trouble.test.mjs",
|
|
47
52
|
"doctor": "node scripts/doctor.mjs",
|
|
48
53
|
"breadth": "node scripts/breadth.mjs",
|
|
49
54
|
"live": "node scripts/live.mjs",
|
|
50
55
|
"adopt": "node scripts/check-adopt.mjs",
|
|
51
|
-
"game": "node scripts/check-game.mjs"
|
|
56
|
+
"game": "node scripts/check-game.mjs",
|
|
57
|
+
"modes": "node scripts/check-modes.mjs",
|
|
58
|
+
"ready": "node scripts/check-prepare.mjs",
|
|
59
|
+
"pdf": "node scripts/check-pdf.mjs",
|
|
60
|
+
"swf": "node scripts/swf-inspect.mjs",
|
|
61
|
+
"flash-hidden": "node scripts/check-flash-window.mjs",
|
|
62
|
+
"flash-mirror": "node scripts/check-flash-mirror.mjs",
|
|
63
|
+
"flash-tools": "node scripts/check-flash-tools.mjs",
|
|
64
|
+
"flash-local": "node scripts/check-flash-local.mjs",
|
|
65
|
+
"keys": "node scripts/keyboard.mjs"
|
|
52
66
|
},
|
|
53
67
|
"license": "MIT",
|
|
54
68
|
"author": {
|