dsh-inner-browser-use 0.18.0 → 0.34.28
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/client.js +103 -8
- package/docs/ARTIFACTS.md +19 -0
- package/docs/BENCH.md +485 -0
- package/docs/ROADMAP.md +146 -0
- package/docs/VERIFY.md +14 -0
- package/index.js +320 -29
- package/package.json +9 -3
- package/read.js +403 -39
- package/review.js +213 -0
- package/sidecar.js +717 -40
- package/tools.js +764 -115
package/client.js
CHANGED
|
@@ -32,7 +32,8 @@ window.__ModuleLoader__.load({
|
|
|
32
32
|
const DICT = {
|
|
33
33
|
zh: {
|
|
34
34
|
'side.label': '后台浏览器',
|
|
35
|
-
'side.empty': '
|
|
35
|
+
'side.empty': '还没有打开的页面,在上方地址栏输入网址',
|
|
36
|
+
'side.restore': '恢复上次的页面',
|
|
36
37
|
'side.connecting': '正在连接…',
|
|
37
38
|
'side.failed': '画面没有连上',
|
|
38
39
|
'side.foreign': '这个浏览器属于发起它的会话,回到那个会话继续。',
|
|
@@ -44,7 +45,8 @@ window.__ModuleLoader__.load({
|
|
|
44
45
|
},
|
|
45
46
|
en: {
|
|
46
47
|
'side.label': 'Background browser',
|
|
47
|
-
'side.empty': 'No page open',
|
|
48
|
+
'side.empty': 'No page open; type an address above',
|
|
49
|
+
'side.restore': 'Reopen the last page',
|
|
48
50
|
'side.connecting': 'Connecting…',
|
|
49
51
|
'side.failed': 'The picture did not connect',
|
|
50
52
|
'side.foreign': 'This browser belongs to the chat that opened it.',
|
|
@@ -73,6 +75,8 @@ window.__ModuleLoader__.load({
|
|
|
73
75
|
on screen at once, and moving the mouse never takes the arrow over. */
|
|
74
76
|
'.dshib-side-ptr{position:absolute;width:20px;height:20px;pointer-events:none;z-index:2;transform:translate(-2px,-2px);transition:left 60ms linear,top 60ms linear;filter:drop-shadow(0 1px 2px rgba(0,0,0,.45))}',
|
|
75
77
|
'.dshib-side-note{font-size:12px;color:var(--dsw-alias-label-tertiary);padding:0 16px;text-align:center}',
|
|
78
|
+
'.dshib-side-back{border:0;border-radius:8px;background:transparent;color:var(--dsw-alias-label-primary);font:inherit;font-size:12px;padding:6px 12px;cursor:pointer}',
|
|
79
|
+
'.dshib-side-back:hover{background:var(--dsw-alias-bg-subtle)}',
|
|
76
80
|
].join('')
|
|
77
81
|
|
|
78
82
|
return {
|
|
@@ -84,6 +88,9 @@ window.__ModuleLoader__.load({
|
|
|
84
88
|
/* one locale service or none */
|
|
85
89
|
}
|
|
86
90
|
ctx.effect(() => {
|
|
91
|
+
/* A reload keeps one sheet, not one per lifetime: the same id is
|
|
92
|
+
replaced instead of stacked. */
|
|
93
|
+
document.getElementById?.(STYLE_ID)?.remove()
|
|
87
94
|
const style = document.createElement('style')
|
|
88
95
|
style.id = STYLE_ID
|
|
89
96
|
/* A sheet is keyed by the plugin that brought it. */
|
|
@@ -109,6 +116,12 @@ window.__ModuleLoader__.load({
|
|
|
109
116
|
}
|
|
110
117
|
}
|
|
111
118
|
const closeHere = (sessionId) => {
|
|
119
|
+
/* A tab the person closed is not a tab they want back on return. */
|
|
120
|
+
try {
|
|
121
|
+
void fetch(`${ROUTE}/sidecar/forget?session=${encodeURIComponent(sessionId)}`, { method: 'POST' }) // ui-copy-ignore
|
|
122
|
+
} catch {
|
|
123
|
+
/* a page that cannot hear it keeps its note */
|
|
124
|
+
}
|
|
112
125
|
let records = []
|
|
113
126
|
try {
|
|
114
127
|
records = surface?.tabsIn?.(sessionId) ?? []
|
|
@@ -138,14 +151,23 @@ window.__ModuleLoader__.load({
|
|
|
138
151
|
/* a column that refuses keeps its own tabs */
|
|
139
152
|
}
|
|
140
153
|
}
|
|
141
|
-
|
|
154
|
+
/* The same rule on this side: a second mount of the same tab is a
|
|
155
|
+
no-op, not a thrown error that takes the pane down. */
|
|
156
|
+
const safely = (register) => {
|
|
157
|
+
try {
|
|
158
|
+
return register()
|
|
159
|
+
} catch (error) {
|
|
160
|
+
return () => {}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
disposers.push(safely(() => tabs.register({
|
|
142
164
|
id: SIDE_ID, // ui-copy-ignore
|
|
143
165
|
kind: SIDE_KIND, // ui-copy-ignore
|
|
144
166
|
keepMounted: true,
|
|
145
167
|
single: true,
|
|
146
168
|
priority: 'extension',
|
|
147
169
|
title: () => L('side.label'),
|
|
148
|
-
}))
|
|
170
|
+
})))
|
|
149
171
|
disposers.push(scope.slots.inject(SLOT, () => scope.slots.register({
|
|
150
172
|
name: SLOT, // ui-copy-ignore
|
|
151
173
|
key: SIDE_ID, // ui-copy-ignore
|
|
@@ -177,7 +199,10 @@ window.__ModuleLoader__.load({
|
|
|
177
199
|
}
|
|
178
200
|
const syncAsk = () => {
|
|
179
201
|
const who = mountedOf()
|
|
180
|
-
|
|
202
|
+
/* A stream the browser gave up on keeps its address here, so the
|
|
203
|
+
next tick has to notice it is dead and open a fresh one, or this
|
|
204
|
+
chat never hears an ask again. */
|
|
205
|
+
if (who === askWho && askSource !== null && Number(askSource.readyState) !== 2) return
|
|
181
206
|
closeAsk()
|
|
182
207
|
if (who === '') return
|
|
183
208
|
try {
|
|
@@ -251,6 +276,9 @@ window.__ModuleLoader__.load({
|
|
|
251
276
|
const [ptr, setPtr] = React.useState(null)
|
|
252
277
|
const [phase, setPhase] = React.useState('connecting')
|
|
253
278
|
const [note, setNote] = React.useState('')
|
|
279
|
+
/* The address this chat had before its tab went away: the pane reopens
|
|
280
|
+
that address, so a lost picture costs one click. */
|
|
281
|
+
const [back, setBack] = React.useState('')
|
|
254
282
|
const [url, setUrl] = React.useState('')
|
|
255
283
|
const [draft, setDraft] = React.useState('')
|
|
256
284
|
const [editing, setEditing] = React.useState(false)
|
|
@@ -263,13 +291,48 @@ window.__ModuleLoader__.load({
|
|
|
263
291
|
React.useEffect(() => {
|
|
264
292
|
let alive = true
|
|
265
293
|
let dropped = false
|
|
294
|
+
/* The tab is still in this chat's column, so this chat may say the
|
|
295
|
+
mirror is its own: a reload forgets the owner, and a pane that only
|
|
296
|
+
waited would stay dark until some tool ran. */
|
|
297
|
+
const reclaim = () => {
|
|
298
|
+
const id = String(props.sessionId ?? '')
|
|
299
|
+
if (id === '') return
|
|
300
|
+
void fetch(`${ROUTE}/sidecar/claim`, { // ui-copy-ignore
|
|
301
|
+
method: 'POST',
|
|
302
|
+
headers: { 'content-type': 'application/json' },
|
|
303
|
+
body: JSON.stringify({ session: id }),
|
|
304
|
+
})
|
|
305
|
+
.then((reply) => (reply.ok === true ? reply.json() : null))
|
|
306
|
+
.then((info) => {
|
|
307
|
+
if (alive !== true) return
|
|
308
|
+
if (info?.ok === true) {
|
|
309
|
+
setOwner(id)
|
|
310
|
+
/* The owner was just named, so the next poll may adopt the
|
|
311
|
+
browser: ask now instead of up to a poll later. */
|
|
312
|
+
setTimeout(read, 0)
|
|
313
|
+
}
|
|
314
|
+
})
|
|
315
|
+
.catch(() => {})
|
|
316
|
+
}
|
|
266
317
|
const read = () => {
|
|
267
|
-
|
|
318
|
+
const who = String(props.sessionId ?? '')
|
|
319
|
+
/* Naming the chat lets a browser that outlived a reload be picked
|
|
320
|
+
up for us; an anonymous poll cannot, and the column would sit on
|
|
321
|
+
an empty stage with the browser running behind it. */
|
|
322
|
+
void fetch(`${ROUTE}/sidecar/state?session=${encodeURIComponent(who)}`, { cache: 'no-store' }) // ui-copy-ignore
|
|
268
323
|
.then((reply) => (reply.ok === true ? reply.json() : null))
|
|
269
324
|
.then((info) => {
|
|
270
325
|
if (alive !== true) return
|
|
271
326
|
const next = String(info?.owner ?? '')
|
|
327
|
+
const asked = String(props.sessionId ?? '')
|
|
328
|
+
/* Nobody owns it, or this chat does but the note was lost: ask
|
|
329
|
+
back instead of showing a note for a browser this column shows. */
|
|
330
|
+
if (asked !== '' && next !== asked) reclaim()
|
|
272
331
|
setOwner((prev) => (prev === next ? prev : next))
|
|
332
|
+
/* A browser that is gone still remembers where this chat was:
|
|
333
|
+
keep the address so the pane can offer to reopen it. */
|
|
334
|
+
const was = String(info?.chat?.remembered ?? '')
|
|
335
|
+
if (was !== '') setBack((prev) => (prev === was ? prev : was))
|
|
273
336
|
/* Another chat holds the browser now: this tab goes away instead
|
|
274
337
|
of sitting here as a note. */
|
|
275
338
|
if (dropped !== true && next !== '' && next !== String(props.sessionId ?? '')) {
|
|
@@ -297,9 +360,15 @@ window.__ModuleLoader__.load({
|
|
|
297
360
|
this chat keeps only a note. // ui-copy-ignore */
|
|
298
361
|
const unknown = owner === null
|
|
299
362
|
const mine = unknown !== true && owner === asked
|
|
363
|
+
/* A picture that stops arriving has to come back by itself: the pane
|
|
364
|
+
re-opens its stream instead of sitting on an empty stage until the
|
|
365
|
+
person reloads the page. */
|
|
366
|
+
const [attempt, setAttempt] = React.useState(0)
|
|
367
|
+
const drawnAt = React.useRef(0)
|
|
300
368
|
|
|
301
369
|
React.useEffect(() => {
|
|
302
370
|
if (mine !== true) return undefined
|
|
371
|
+
drawnAt.current = Date.now()
|
|
303
372
|
const who = encodeURIComponent(String(props.sessionId ?? ''))
|
|
304
373
|
const source = new EventSource(`${ROUTE}/sidecar/frames?session=${who}`)
|
|
305
374
|
/* Bursts arrive faster than a picture can decode: only the newest frame
|
|
@@ -316,6 +385,7 @@ window.__ModuleLoader__.load({
|
|
|
316
385
|
setFrame(message)
|
|
317
386
|
setPtr(message.pointer ?? null)
|
|
318
387
|
setPhase('live')
|
|
388
|
+
drawnAt.current = Date.now()
|
|
319
389
|
if (typeof message.url === 'string' && message.url !== '') setUrl(message.url)
|
|
320
390
|
}
|
|
321
391
|
const draw = (message) => {
|
|
@@ -324,6 +394,11 @@ window.__ModuleLoader__.load({
|
|
|
324
394
|
if (typeof requestAnimationFrame === 'function') raf = requestAnimationFrame(paint)
|
|
325
395
|
else paint()
|
|
326
396
|
}
|
|
397
|
+
const stuck = setInterval(() => {
|
|
398
|
+
/* Hello alone is not a picture: six quiet seconds means the stream is
|
|
399
|
+
dead, so ask for a new one. */
|
|
400
|
+
if (Date.now() - drawnAt.current > 6000) setAttempt((n) => n + 1)
|
|
401
|
+
}, 3000)
|
|
327
402
|
source.onmessage = (event) => {
|
|
328
403
|
let message
|
|
329
404
|
try {
|
|
@@ -333,7 +408,9 @@ window.__ModuleLoader__.load({
|
|
|
333
408
|
}
|
|
334
409
|
if (message.type === 'hello') {
|
|
335
410
|
setPhase('live')
|
|
411
|
+
drawnAt.current = Date.now()
|
|
336
412
|
setUrl(String(message.url ?? ''))
|
|
413
|
+
setBack(String(message.chat?.remembered ?? ''))
|
|
337
414
|
} else if (message.type === 'failed') {
|
|
338
415
|
setPhase('failed')
|
|
339
416
|
setNote(String(message.text ?? ''))
|
|
@@ -347,10 +424,11 @@ window.__ModuleLoader__.load({
|
|
|
347
424
|
}
|
|
348
425
|
source.onerror = () => setPhase('lost')
|
|
349
426
|
return () => {
|
|
427
|
+
clearInterval(stuck)
|
|
350
428
|
if (raf !== 0 && typeof cancelAnimationFrame === 'function') cancelAnimationFrame(raf)
|
|
351
429
|
source.close()
|
|
352
430
|
}
|
|
353
|
-
}, [props.sessionId, mine])
|
|
431
|
+
}, [props.sessionId, mine, attempt])
|
|
354
432
|
|
|
355
433
|
/* The address field follows the page except while the person is editing it. */
|
|
356
434
|
React.useEffect(() => {
|
|
@@ -510,7 +588,24 @@ window.__ModuleLoader__.load({
|
|
|
510
588
|
onWheel,
|
|
511
589
|
onKeyDown,
|
|
512
590
|
},
|
|
513
|
-
frame === null
|
|
591
|
+
frame === null && back !== ''
|
|
592
|
+
? h('button', {
|
|
593
|
+
className: 'dshib-side-back',
|
|
594
|
+
type: 'button', // ui-copy-ignore
|
|
595
|
+
onClick: () => { // ui-copy-ignore
|
|
596
|
+
void fetch(`${ROUTE}/sidecar/open`, { // ui-copy-ignore
|
|
597
|
+
method: 'POST',
|
|
598
|
+
headers: { 'content-type': 'application/json' },
|
|
599
|
+
body: JSON.stringify({ session: who, url: back }), // ui-copy-ignore
|
|
600
|
+
})
|
|
601
|
+
.then((reply) => (reply.ok === true ? reply.json() : null))
|
|
602
|
+
.then((info) => {
|
|
603
|
+
if (info?.ok === true) setAttempt((n) => n + 1)
|
|
604
|
+
})
|
|
605
|
+
.catch(() => {})
|
|
606
|
+
},
|
|
607
|
+
}, L('side.restore'))
|
|
608
|
+
: frame === null ? h('div', { className: 'dshib-side-note' }, noteText) : null,
|
|
514
609
|
h('textarea', {
|
|
515
610
|
className: 'dshib-side-ime',
|
|
516
611
|
ref: ime,
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# 文件落在哪里
|
|
2
|
+
|
|
3
|
+
浏览器产生的东西——截图、抓下来的图、打印的 PDF——不落在项目根目录,也不散在 home 下。固定规则:
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
~/.dsh-inner-browser-use/artifacts/<项目>/<会话>/<种类>/
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
- **项目**:会话工作目录的文件夹名,加路径哈希前 6 位,例如 `dsh-inner-browser-use-a1b2c3`。同名文件夹、不同路径不会混。
|
|
10
|
+
- **会话**:会话 id 去掉 `session-` 后的前 8 位,例如 `72557d6e`。同一项目里两个会话各存各的。
|
|
11
|
+
- **种类**:`shots/` 是 `sidecar_shot` 的截图,`pictures/` 是 `sidecar_grab` 抓的图,`pdf/` 是 `sidecar_pdf` 打印的文档。
|
|
12
|
+
|
|
13
|
+
文件名默认带时间戳(`shot-20261028-124500.png`),给了 `name` 就用它;名字里的分隔符、连续点、开头点都会被去掉,跳不出这个目录。
|
|
14
|
+
|
|
15
|
+
每个种类保留最新 40 个文件,旧的自动删,免得跑一天的会话留下一个没人翻得动的文件夹。开着的文件不会被删掉(尽力删除,失败就留着)。
|
|
16
|
+
|
|
17
|
+
调用环境里没有项目或会话信息时,落到 `no-project/no-chat/<种类>/`,不猜。
|
|
18
|
+
|
|
19
|
+
测试里用 `DSH_INNER_BROWSER_ARTIFACTS` 指向临时目录,不写真实 home。
|