dsh-inner-browser-use 0.43.0 → 0.45.0
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 +3 -0
- package/README.zh.md +3 -0
- package/docs/ARCHITECTURE.md +36 -4
- package/docs/ARTIFACTS.md +19 -14
- package/docs/BENCH.md +30 -0
- package/docs/FLASH.md +10 -0
- package/docs/VERIFY.md +41 -0
- package/index.js +1 -1
- package/package.json +10 -3
- package/pixels.js +217 -0
- package/read.js +87 -17
- package/scripts/check-flash-tools.mjs +6 -2
- package/scripts/check-look.mjs +86 -0
- package/scripts/check-sites.mjs +94 -0
- package/scripts/soak.mjs +110 -0
- package/sidecar.js +45 -0
- package/tools.js +302 -23
package/README.md
CHANGED
|
@@ -20,10 +20,13 @@ live picture of that browser, and your own cursor keeps working on the picture.
|
|
|
20
20
|
| `browser_key` | Presses one key: Enter, Tab, Escape, Backspace, Delete, Insert, arrows, Home, End, PageUp, PageDown, F1-F12, the numeric keypad, letters, digits, punctuation and every shifted symbol, plus ContextMenu, CapsLock, PrintScreen and media keys. `Ctrl`/`Alt`/`Shift`/`Meta`/`Mod` modifiers, chords with `+`, and `Ctrl+C/X/V/A/Z/Y` run the real editing commands. |
|
|
21
21
|
| `browser_scroll` | Scrolls the wheel at the centre of the page, or at `x`/`y`. `dy: 600` is about a screen down. |
|
|
22
22
|
| `browser_shot` | Saves a full-size PNG into `~/.dsh-inner-browser-use/screenshots` and returns the path. |
|
|
23
|
+
| `browser_look` | Looks at a page that draws itself — a game, a canvas, a Flash movie — and returns it as a grid of letters, one per block of screen, the same letter for the same colour: 12×8 costs about 200 characters, so a picture costs no image tokens. A second look reports only the cells that changed. `browser_shot` is still the tool for colour, layout and anything a person should see. |
|
|
23
24
|
| `browser_media` | Lists the video and sound the page already fetched, with sizes, the one its own player shows first and marked `main`; `save: <row>`, `save: main` or part of the address keeps one. Ads and beacons stay out unless `all: true`; `watch: false` stops remembering the page. A stream is followed to its pieces and joined, so nothing has to be installed. |
|
|
24
25
|
| `browser_login` | Keeps a sign-in by name: `save` stores this window's cookies and each site's local storage, `list` shows what is kept, `use` signs that account back in and reopens the page, `import` takes a sign-in that is already in Chrome, Brave, Edge or Chromium on this Mac and keeps it under a name, `drop` forgets it. Names survive a restart and belong to the browser rather than to one chat, so two accounts on one site are two names. What sits on disk is encrypted, with the key in the system keychain. |
|
|
25
26
|
| `browser_state` | Reports whether the browser runs, which profile it uses, its address and title, how many columns are showing it, how much memory it holds right now and when it would close on its own if left alone. |
|
|
26
27
|
|
|
28
|
+
A site you read twice is remembered by origin in `~/.dsh/inner-browser-use/sites.json` (override with `DSH_INNER_BROWSER_STATE_DIR`). After two reads, a site whose page text or control count is big opens in `browser_view` as a `map:` of what is there, and a site whose articles are long has `browser_read` cut at 3,500 characters with a line saying so. Both come with one sentence naming the measurement, and both yield to an explicit `mode`, `query` or `max_chars` — learning changes the default, not the answer to a direct question. `scripts/check-sites.mjs` is the live proof: three views of a 300-button page and three reads of a 12,000-character document, saying where the cut fell.
|
|
29
|
+
|
|
27
30
|
`browser_read` on the same page is the words only, in Markdown (measured: 4,520
|
|
28
31
|
characters of an about page against 2,987 characters of snapshot — and the
|
|
29
32
|
snapshot spends most of its budget on controls; on a long article the gap grows
|
package/README.zh.md
CHANGED
|
@@ -19,10 +19,13 @@ profile,不是产品自带的那个浏览页签:右栏放的是它的画面
|
|
|
19
19
|
| `browser_key` | 按一个键:Enter、Tab、Escape、Backspace、Delete、上下左右、Home、End、PageUp、PageDown。 |
|
|
20
20
|
| `browser_scroll` | 在页面中心滚轮,也可以指定 `x`/`y`。`dy: 600` 约一屏。 |
|
|
21
21
|
| `browser_shot` | 存一张 PNG 到 `~/.dsh/inner-browser-use/screenshots`,返回路径;`region: [x, y, w, h]` 只截一块(游戏、图表、看局部)。 |
|
|
22
|
+
| `browser_look` | 看自己画自己的页面(游戏、canvas、Flash):把画面压成字母网格,一块屏幕一个字母、同色同字母,12×8 约 200 字符,不花图片 token;再看一次只报变了的格子。要颜色、布局或给人看图仍用 `browser_shot`。 |
|
|
22
23
|
| `browser_media` | 列出页面已经请求过的视频和声音(带大小),页面自己播放器里那个排第一、标 `main`;`save: <行号>`、`save: main` 或地址片段存下来。广告和统计默认不出现(`all: true` 才看),`watch: false` 关掉这个页面的记录。流会顺着播放列表把片段取回拼好,不用装任何东西。 |
|
|
23
24
|
| `browser_login` | 按名字留住登录:`save` 存下当前窗口的 cookie 和各站的 localStorage,`list` 看有哪些,`use` 切回那个账号并重开页面,`import` 把本机 Chrome/Brave/Edge 里已经登着的站直接留成一个名字,`drop` 忘掉。名字跟着浏览器走、不跟着一个会话走,重启也在;同一站点两个账号就存两个名字。磁盘上那一份是加密的,钥匙在系统钥匙串里。 |
|
|
24
25
|
| `browser_state` | 报浏览器在不在跑、用的哪个 profile、地址、标题、有几个右栏在看它,以及它此刻占多少内存、没人用的话还有多久关闭。 |
|
|
25
26
|
|
|
27
|
+
同一个站读两次就会被按 origin 记住(`~/.dsh/inner-browser-use/sites.json`,可用 `DSH_INNER_BROWSER_STATE_DIR` 改位置)。第二次之后:正文或控件多的站,`browser_view` 默认先给 `map:` 地标;文章长的站,`browser_read` 默认截到 3500 字符并说明截在哪。两者都带一句实测数字,也都会被显式 `mode`、`query`、`max_chars` 覆盖——学到的是默认值,不是对直接提问的答案。`scripts/check-sites.mjs` 是现场证据:300 个按钮的页看三次、12000 字的文档读三次,报出截断位置。
|
|
28
|
+
|
|
26
29
|
点、输入、按键之后的那一行会多一句结果:`· navigated to …`(换页了)、`· content changed`(页内有变化)、`· no visible change`(点了没动)、Flash 页则是 `· screen updated` / `· no frame change`;`read: true` 时不再多说。
|
|
27
30
|
|
|
28
31
|
`browser_view` 一次给一页文本:
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -134,7 +134,7 @@ browser without pulling the column away from the person reading it.
|
|
|
134
134
|
|
|
135
135
|
## Tool surface
|
|
136
136
|
|
|
137
|
-
`sidecarTools()` in `tools.js` builds the
|
|
137
|
+
`sidecarTools()` in `tools.js` builds the 39 definitions and `index.js` registers them on the
|
|
138
138
|
plugin's tool scope. All of them return one text block. A page read numbers the controls and
|
|
139
139
|
keeps those numbers across later reads, so `browser_click` by ref means the element the model
|
|
140
140
|
last saw; navigation drops them and the model gets `REF_GONE` rather than a wrong click.
|
|
@@ -168,8 +168,12 @@ window) costs one round trip on an animation-heavy page instead of one per frame
|
|
|
168
168
|
## Reading for an answer
|
|
169
169
|
|
|
170
170
|
`browser_read` with a `query` does not return the page; it returns the sentences that answer the
|
|
171
|
-
question. The page-side block reader already returns paragraphs with their heading
|
|
172
|
-
|
|
171
|
+
question. The page-side block reader already returns paragraphs with their heading trail
|
|
172
|
+
(`Plans \u203a Trial`), their link density, a 0-1 quality score and table rows as one
|
|
173
|
+
`column: value | column: value` line, so the work stays in `read.js` and costs no extra round trip.
|
|
174
|
+
When the page's own menu would eat the budget, the blocks past the ceiling are chosen by quality and
|
|
175
|
+
then put back in page order, so prose survives where document order would have spent everything
|
|
176
|
+
above it.
|
|
173
177
|
|
|
174
178
|
Paragraphs are ranked first (BM25 over the question's terms, link-heavy menus down-weighted), then
|
|
175
179
|
split into sentences. Each sentence is scored on its own with the same saturation and length
|
|
@@ -177,7 +181,9 @@ normalisation, and that score is multiplied by three priors: the words of its he
|
|
|
177
181
|
the paragraph it sits in, and inverse link density. Words the page says everywhere are dropped
|
|
178
182
|
before scoring — a term in most sentences cannot point at anything — and a sentence that matches
|
|
179
183
|
only part of the question is scaled by the share of the question's idf weight it actually covers,
|
|
180
|
-
which is what stops a paragraph containing "to" from competing with one containing "refund".
|
|
184
|
+
which is what stops a paragraph containing "to" from competing with one containing "refund". The
|
|
185
|
+
block's quality score and its being a table row are priors as well, so a menu that repeats the
|
|
186
|
+
question's words cannot outrank the sentence that explains them.
|
|
181
187
|
|
|
182
188
|
Selection is not "the top N scores": each pick subtracts its Jaccard overlap with what is already
|
|
183
189
|
chosen (MMR), selection stops when the best remaining sentence is worth nothing, and picked
|
|
@@ -192,6 +198,32 @@ characters) and no text, and the tool turns that into a line saying nothing dire
|
|
|
192
198
|
question plus those passages. The whole page is still one `browser_view` away, so the expensive
|
|
193
199
|
fallback — a page pasted into the context for a question it never addressed — is gone.
|
|
194
200
|
|
|
201
|
+
## Looking at a page that draws itself
|
|
202
|
+
|
|
203
|
+
A canvas, a game and a Flash movie have no text tree and no controls: a snapshot reads
|
|
204
|
+
an empty page and a screenshot costs image tokens on every look. `browser_look` takes
|
|
205
|
+
the captured frame, decodes the PNG inside Node (so no page policy and no browser
|
|
206
|
+
decode step is involved) and prints a grid of palette letters — one letter per block of
|
|
207
|
+
screen, the same letter for the same colour. A 12×8 look costs about 200 characters; a
|
|
208
|
+
second look diffs against the first grid and reports only the cells that moved, plus the
|
|
209
|
+
box they moved in. `browser_prepare` is the way in: it reports the surface, the frame
|
|
210
|
+
rate and whether keys arrive, and says when the page needs the Flash runtime.
|
|
211
|
+
|
|
212
|
+
## Sites that teach the next read
|
|
213
|
+
|
|
214
|
+
Two calls into the same origin, the browser knows what it is looking at. A record per
|
|
215
|
+
origin in `sites.json` holds an exponentially drifted average of the page's character
|
|
216
|
+
count, its control count and its article length, updated by every `browser_view` and
|
|
217
|
+
`browser_read` — measured before the budget cuts the output, so a cut does not shrink
|
|
218
|
+
the site on the next visit. Past two reads, a site whose text or control count is big
|
|
219
|
+
opens `browser_view` as the `map:` view instead of a page of numbered controls, and a
|
|
220
|
+
site whose articles are long has `browser_read` land at 3,500 characters and say where
|
|
221
|
+
it cut. Every learned default carries the measurement that produced it, and every one
|
|
222
|
+
yields to what the model asked for: an explicit `mode`, `query` or `max_chars` is used
|
|
223
|
+
as given. The record lives in `~/.dsh/inner-browser-use/sites.json` (override with
|
|
224
|
+
`DSH_INNER_BROWSER_STATE_DIR`), is written at most once every two seconds, and never
|
|
225
|
+
holds page text — only counts.
|
|
226
|
+
|
|
195
227
|
## Answering across tabs
|
|
196
228
|
|
|
197
229
|
A question can be answered by a tab that is not the one on screen. Moving the mirror there and
|
package/docs/ARTIFACTS.md
CHANGED
|
@@ -1,35 +1,40 @@
|
|
|
1
1
|
# 文件落在哪里
|
|
2
2
|
|
|
3
|
-
浏览器产生的东西——截图、抓下来的图、打印的 PDF
|
|
3
|
+
浏览器产生的东西——截图、抓下来的图、打印的 PDF、存下的视频——落在**当前项目里**,跟会话工作目录走;不在项目根目录,也不散在 home 下。
|
|
4
4
|
|
|
5
5
|
```
|
|
6
|
-
|
|
6
|
+
<项目>/.dsh/browser/<会话>/<种类>/
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
-
-
|
|
10
|
-
- **会话**:会话 id 去掉 `session-` 后的前 8 位,例如 `72557d6e
|
|
9
|
+
- **项目**:会话的工作目录。换项目就换目录,产出跟着项目走,方便直接拷进仓库或用项目里的工具看。
|
|
10
|
+
- **会话**:会话 id 去掉 `session-` 后的前 8 位,例如 `72557d6e`。同一项目里两个会话各存各的,不会互相覆盖。
|
|
11
11
|
- **种类**:`shots/` 是 `browser_shot` 的截图,`pictures/` 是 `browser_grab` 抓的图,`pdf/` 是 `browser_pdf` 打印的文档,`media/` 是 `browser_media` 存下的视频、音频和流。
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
`browser_look` 不落文件:它把屏幕读成字母网格直接返回,用来反复看画面变化。
|
|
14
|
+
|
|
15
|
+
文件名默认带时间戳(`shot-20261028-124500.png`),给了 `name` 就用它;名字里的分隔符、连续点、开头点都会被去掉,跳不出这个目录。`browser_state` 的 `files` 一行给出当前实际落点。
|
|
14
16
|
|
|
15
17
|
每个种类保留最新 40 个文件,旧的自动删,免得跑一天的会话留下一个没人翻得动的文件夹。开着的文件不会被删掉(尽力删除,失败就留着)。
|
|
16
18
|
|
|
17
|
-
|
|
19
|
+
`<项目>/.dsh/browser/.gitignore` 写一行 `*`,所以截图不会进 `git status`;这个文件只在项目里写。调用环境里没有项目信息时,落到 `~/.dsh-inner-browser-use/artifacts/no-project/`,不猜,也不写 `.gitignore`。
|
|
20
|
+
|
|
21
|
+
`DSH_INNER_BROWSER_ARTIFACTS` 能把整个落点换到一个固定目录(测试用,也方便把产出收到别处),此时不写 `.gitignore`。
|
|
18
22
|
|
|
19
23
|
## 整个目录
|
|
20
24
|
|
|
21
25
|
```
|
|
26
|
+
<项目>/.dsh/browser/ 截图、抓图、PDF、媒体(见上)
|
|
22
27
|
~/.dsh-inner-browser-use/
|
|
23
|
-
artifacts
|
|
24
|
-
chrome/
|
|
25
|
-
logins/
|
|
26
|
-
vault.key
|
|
27
|
-
flash/
|
|
28
|
-
browser.log
|
|
29
|
-
asks.log
|
|
28
|
+
artifacts/no-project/ 没有项目时的产出
|
|
29
|
+
chrome/ 浏览器自己的数据:登录态、Cookie、缓存
|
|
30
|
+
logins/ 按名字留住的登录,一个名字一个文件,加密
|
|
31
|
+
vault.key 钥匙串拿不到钥匙时的替代文件,只有本人可读
|
|
32
|
+
flash/ Flash 播放器运行时,第一次用 flash:true 才装
|
|
33
|
+
browser.log 最近一次浏览器启动和页面记录
|
|
34
|
+
asks.log 人问过什么、答了什么
|
|
30
35
|
tabs.json / mirror.json / flash.json / watcher.json 上次开着哪些页
|
|
31
36
|
```
|
|
32
37
|
|
|
33
|
-
`chrome/session-cookies.json` 是 Chrome 退出时会丢的会话 cookie,加密后存在 profile 旁边;`logins/` 与 `vault.key` 由 `browser_login` 和启动时自动保存的登录备份共用,不要手动改或删,要清就用 `browser_login({action:"drop"})`。`chrome/` 其余部分是浏览器进程自己在读写,不要手动改;要清登录态,用 `browser_open`
|
|
38
|
+
`chrome/session-cookies.json` 是 Chrome 退出时会丢的会话 cookie,加密后存在 profile 旁边;`logins/` 与 `vault.key` 由 `browser_login` 和启动时自动保存的登录备份共用,不要手动改或删,要清就用 `browser_login({action:"drop"})`。`chrome/` 其余部分是浏览器进程自己在读写,不要手动改;要清登录态,用 `browser_open` 的档位或插件自己的入口,别删目录里的某个文件。
|
|
34
39
|
|
|
35
40
|
测试里用 `DSH_INNER_BROWSER_ARTIFACTS` 指向临时目录,不写真实 home。
|
package/docs/BENCH.md
CHANGED
|
@@ -1,3 +1,33 @@
|
|
|
1
|
+
## 读过的站第二次更便宜(0.45.0,本地页面,`scripts/check-sites.mjs`)
|
|
2
|
+
|
|
3
|
+
同一台机器、无头 Chrome、两个本地 origin。第一次走老路(整页读),第二次之后用学到的默认值。
|
|
4
|
+
所有数字来自一次真实运行,不是估算。
|
|
5
|
+
|
|
6
|
+
| 页面 | 第一次 | 学到之后 | 省下 | 说明 |
|
|
7
|
+
| --- | --- | --- | --- | --- |
|
|
8
|
+
| 300 个按钮的页面,`browser_view` | 1801 字符 | 760 字符(`map: 90 controls`) | 58% | 两次之后自动改成地标视图,并说明「this site reads big」 |
|
|
9
|
+
| 12000 字文档,`browser_read` | 6096 字符(满预算) | 3726 字符(截到 3500) | 39% | 截断处自带 `cut at 3500 characters`,用 `max_chars` 可要全文 |
|
|
10
|
+
|
|
11
|
+
- 学的尺子量的是页面本身,不是这次输出:`read.js` 的 `chars` 在截断前取值,所以第一次就把整篇长度记下来,不会被 3500 的预算越读越小
|
|
12
|
+
- 显式 `mode` / `query` / `max_chars` 一律优先;学到的是默认值,附一句带数字的理由
|
|
13
|
+
- 记录写在 `sites.json`,每进程最多每 2 秒一次;两次运行之间只留计数,不留正文
|
|
14
|
+
- `scripts/check-look.mjs`:12×8 的字母网格,三次看共 639 字符,全程没有图片 token
|
|
15
|
+
- `scripts/soak.mjs`:无头浏览器 24 轮开页/读页/截图/重读,34.9 秒,进程树内存 1440 → 1287 MB(−153 MB),页数始终 1,8 次截图、24 次读;没有页签或内存随轮数上涨
|
|
16
|
+
|
|
17
|
+
## 读页先读正文(0.44.0,本地页面,`scripts/bench.mjs`)
|
|
18
|
+
|
|
19
|
+
同一台机器、同一档预算。旧法是按页面顺序把预算灌满的整页读。
|
|
20
|
+
|
|
21
|
+
| 页面 | 旧法字符 | 新法字符 | 省下 | 答案 |
|
|
22
|
+
| --- | --- | --- | --- | --- |
|
|
23
|
+
| 九成是菜单、答案在正文(900 字预算) | 1500 | 88 | 94% | 旧法读不到,新法读到 |
|
|
24
|
+
| 定价表(900 字预算) | 679 | 109 | 84% | 价格与列名同一行 |
|
|
25
|
+
|
|
26
|
+
- `read-noise` 2 ms、`read-table` 2 ms,两项预算 300 ms 全过(26 项全过)
|
|
27
|
+
- 表格行形如 `Plan: Pro | Price per month: $84`,列名来自表头,一行就是一条记录
|
|
28
|
+
- 标题路径 `Plans \u203a Trial` 印在证据上方;与最近标题相同、或超过 90 字时退回最近标题
|
|
29
|
+
- 既有数字没有回退:`read-answer` 90 字 / 92%、`read-miss` 438 字 / 79%、`read-tabs` 90 字 / 90%(与 0.43.0 相同)
|
|
30
|
+
|
|
1
31
|
## 一次读穿过登录墙(0.43.0,本地页面,`scripts/bench.mjs`)
|
|
2
32
|
|
|
3
33
|
同一页:带 cookie 时是账户页,不带时是一个只有密码框的登录页。留一个登录(vault),清掉页面的 cookie 与 localStorage 后读它。
|
package/docs/FLASH.md
CHANGED
|
@@ -56,6 +56,16 @@ Electron 11 的两个真实限制,代码里已按它们写:
|
|
|
56
56
|
- 它不会用当前 Chrome 的 profile 目录(会降级并上锁),Flash 分支固定用 `<profile>-flash` 独立目录;两边登录态各自独立。
|
|
57
57
|
- `/json/new` 在 Chromium 87 上是 404,单窗口应用也没有第二个窗口,所以 Flash 分支不建新页签:会话要页签时把唯一那个窗口导航过去。
|
|
58
58
|
|
|
59
|
+
### 起来之后还要证明它真在
|
|
60
|
+
|
|
61
|
+
端口能连上不等于插件装上了:`/json/version` 由浏览器进程回答,插件加载失败时它照样 200,页面只是画出一块黑。所以启动流程在端口就绪之后还问页面一句 `navigator.plugins`,只认名字里带 `Shockwave Flash` 的那条,结果存成浏览器自己的状态:
|
|
62
|
+
|
|
63
|
+
- `browser_open` 在正文里给一行 `flash runtime: plugin loaded (Shockwave Flash)`,或者 `open, but no Flash plugin is loaded — movies will not play; run scripts/flash-setup.mjs`;还没问到时写 `plugin not checked yet`,不猜。
|
|
64
|
+
- `browser_state` 的运行时行把「模式」和「插件」分开:`Flash-era runtime — plugin loaded (…)` / `— no Flash plugin is loaded`。
|
|
65
|
+
- `browser_prepare` 在真的找到 movie、但插件缺失时补一句「它们跑不起来」,免得把黑帧当成游戏还在加载。
|
|
66
|
+
|
|
67
|
+
问不到(页面崩溃、CDP 早断)时记 `null`——没有证据就不下结论,这跟「没装运行时」是两回事:后者在启动阶段就抛 `sidecar: no Flash runtime on this machine`。
|
|
68
|
+
|
|
59
69
|
实测数字(Apple 芯片 + Rosetta,1440×900):
|
|
60
70
|
|
|
61
71
|
| 指标 | 值 |
|
package/docs/VERIFY.md
CHANGED
|
@@ -97,6 +97,47 @@ still hold the cookies.
|
|
|
97
97
|
内置浏览器 means the page half is stale: reload the GUI page.
|
|
98
98
|
- Guest-era logins are not migrated; sign in once in the new profile.
|
|
99
99
|
|
|
100
|
+
## 7. A page that draws itself
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
node scripts/check-look.mjs
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Headless Chrome paints a page, `browser_look` returns a 12×8 letter grid, the page
|
|
107
|
+
paints a red square, and the second look names only the cells that moved. Expected:
|
|
108
|
+
`three looks cost 639 characters and no picture`. A look that returns words instead
|
|
109
|
+
of a grid, or a diff of the whole screen, means the PNG was not decoded locally.
|
|
110
|
+
|
|
111
|
+
## 8. The next visit is cheaper
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
node scripts/check-sites.mjs
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Two local origins: a page of 300 buttons and a 12,000-character document. Expected,
|
|
118
|
+
in order: the first view is the page, the second is still the page (one read is not a
|
|
119
|
+
lesson), the third opens as `map: 90 controls` and says `this site reads big (2 read(s),
|
|
120
|
+
~2946 chars of page, ~90 controls)`; the first document read is the whole thing (6,096
|
|
121
|
+
characters), a forced second read is the whole thing again, and the third comes back
|
|
122
|
+
`cut at 3500 characters` with `this site reads long (2 read(s), ~6000 chars of text)`.
|
|
123
|
+
The run ends with `sites: ok` and leaves `sites.json` in a temporary directory.
|
|
124
|
+
|
|
125
|
+
A third view that is still the page, or a third read that is not cut, means the record
|
|
126
|
+
is written after the default is chosen, or the size recorded is the output's rather
|
|
127
|
+
than the page's: a cut must not shrink the site for the next visit.
|
|
128
|
+
|
|
129
|
+
## 9. It does not grow
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
npm run soak
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Twenty-four cycles of open, read, snapshot, re-read through one headless browser.
|
|
136
|
+
Expected: `soak: ok · 24 cycles` with `pages 1` and memory lower at the end than
|
|
137
|
+
at the start. Memory climbing with the cycle count, or a second page appearing,
|
|
138
|
+
means something is kept per read rather than per page. The run is bounded on
|
|
139
|
+
purpose — under a minute, no internet.
|
|
140
|
+
|
|
100
141
|
## Live
|
|
101
142
|
|
|
102
143
|
`node scripts/live.mjs` does the same work on the open internet, with no local
|
package/index.js
CHANGED
|
@@ -947,7 +947,7 @@ export function apply(ctx) {
|
|
|
947
947
|
'页面很长、只想确认某句话/按钮在哪时用 `browser_find`,别整页读。要读长文(文章、文档、issue、论坛帖)用 `browser_read`:它回 Markdown,只花几十分之一的 token,链接可点;要动手点、填表才用 `browser_view` 拿编号。',
|
|
948
948
|
'要保存页面样子用 `browser_shot`,它会给出 PNG 绝对路径;要交给人看的文件,把绝对路径写进回复。',
|
|
949
949
|
'页面放视频或音乐、人想留下它时用 `browser_media`:它列的是页面自己已经请求过的媒体(带大小),页面自己播放器里那个标 `main` 排第一,广告和统计请求默认不出现(`all:true` 才看),`save: main` 存第一个;视频是流的话会顺着播放列表把片段取回拼成整段,不用装 yt-dlp 或 ffmpeg。先让页面播一下再列,播放请求才是文件。不想要这个页面的记录就 `watch:false`。',
|
|
950
|
-
'游戏、canvas、Flash:画面自己画的地方,`browser_view` 和 `browser_read` 读不出里面是什么。先 `browser_prepare`
|
|
950
|
+
'游戏、canvas、Flash:画面自己画的地方,`browser_view` 和 `browser_read` 读不出里面是什么。先 `browser_prepare` 拿画面位置、帧率、按键是否到达和下一步;之后用 `browser_look` 读屏:它把画面压成字母网格、点完再看一次只报变了哪块,不花图片 token;要定位置或看颜色再 `browser_shot`。按坐标点、按住、按组合键后先 `browser_look` 确认这一步生效,别每步截图。当年用 Flash 做的游戏用 `browser_open` 带 `flash:true` 在栏内跑,不装播放器、不开窗口;Flash 默认关,`flash:true` 开、`flash:false` 回到普通 Chrome,两边都只在本会话右栏。',
|
|
951
951
|
].join('\n'),
|
|
952
952
|
}), 'dsh-inner-browser-use: system prompt section')
|
|
953
953
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-inner-browser-use",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.45.0",
|
|
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",
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"read.js",
|
|
19
19
|
"review.js",
|
|
20
20
|
"media.js",
|
|
21
|
+
"pixels.js",
|
|
21
22
|
"vault.js",
|
|
22
23
|
"chromecookies.js",
|
|
23
24
|
"client.js",
|
|
@@ -29,6 +30,9 @@
|
|
|
29
30
|
"scripts/check-flash-mirror.mjs",
|
|
30
31
|
"scripts/check-flash-local.mjs",
|
|
31
32
|
"scripts/check-flash-tools.mjs",
|
|
33
|
+
"scripts/check-look.mjs",
|
|
34
|
+
"scripts/check-sites.mjs",
|
|
35
|
+
"scripts/soak.mjs",
|
|
32
36
|
"runtime/",
|
|
33
37
|
"docs/",
|
|
34
38
|
"README.md",
|
|
@@ -51,7 +55,7 @@
|
|
|
51
55
|
"dsh": ">=0.2.0"
|
|
52
56
|
},
|
|
53
57
|
"scripts": {
|
|
54
|
-
"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/media.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 test/vault.test.mjs",
|
|
58
|
+
"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/media.test.mjs test/no-duplicate.test.mjs test/package.test.mjs test/blocks.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 test/vault.test.mjs",
|
|
55
59
|
"doctor": "node scripts/doctor.mjs",
|
|
56
60
|
"breadth": "node scripts/breadth.mjs",
|
|
57
61
|
"live": "node scripts/live.mjs",
|
|
@@ -65,7 +69,10 @@
|
|
|
65
69
|
"flash-mirror": "node scripts/check-flash-mirror.mjs",
|
|
66
70
|
"flash-tools": "node scripts/check-flash-tools.mjs",
|
|
67
71
|
"flash-local": "node scripts/check-flash-local.mjs",
|
|
68
|
-
"keys": "node scripts/keyboard.mjs"
|
|
72
|
+
"keys": "node scripts/keyboard.mjs",
|
|
73
|
+
"look": "node scripts/check-look.mjs",
|
|
74
|
+
"sites": "node scripts/check-sites.mjs",
|
|
75
|
+
"soak": "node scripts/soak.mjs"
|
|
69
76
|
},
|
|
70
77
|
"license": "MIT",
|
|
71
78
|
"author": {
|
package/pixels.js
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The screen without the picture: a page that draws itself — a game, a canvas
|
|
3
|
+
* app, a Flash movie — has nothing a text tree can read, and a screenshot costs
|
|
4
|
+
* image tokens on every step. This turns the screen into a small grid of
|
|
5
|
+
* characters instead: one letter per block of pixels, the same letters for the
|
|
6
|
+
* same colours, so a caller can see what changed by reading text.
|
|
7
|
+
*
|
|
8
|
+
* Everything here is pure: decode, average, quantise, diff. No browser, no
|
|
9
|
+
* files, so it can be tested without one.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { inflateSync } from 'node:zlib'
|
|
13
|
+
|
|
14
|
+
/** Colours a screen block can be called, most common things first. */
|
|
15
|
+
export const PALETTE = [
|
|
16
|
+
{ char: '.', name: 'black', rgb: [16, 16, 16] },
|
|
17
|
+
{ char: ':', name: 'dark', rgb: [64, 64, 64] },
|
|
18
|
+
{ char: '-', name: 'gray', rgb: [112, 112, 112] },
|
|
19
|
+
{ char: '+', name: 'light', rgb: [168, 168, 168] },
|
|
20
|
+
{ char: '*', name: 'bright', rgb: [216, 216, 216] },
|
|
21
|
+
{ char: '#', name: 'white', rgb: [248, 248, 248] },
|
|
22
|
+
{ char: 'r', name: 'red', rgb: [208, 48, 48] },
|
|
23
|
+
{ char: 'g', name: 'green', rgb: [64, 176, 72] },
|
|
24
|
+
{ char: 'b', name: 'blue', rgb: [56, 104, 216] },
|
|
25
|
+
{ char: 'y', name: 'yellow', rgb: [232, 208, 72] },
|
|
26
|
+
{ char: 'c', name: 'cyan', rgb: [64, 200, 208] },
|
|
27
|
+
{ char: 'm', name: 'magenta', rgb: [200, 72, 200] },
|
|
28
|
+
{ char: 'o', name: 'orange', rgb: [224, 136, 48] },
|
|
29
|
+
{ char: 'p', name: 'purple', rgb: [112, 56, 168] },
|
|
30
|
+
{ char: 'n', name: 'brown', rgb: [136, 88, 48] },
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The letter closest to a colour. Grays and bright colours are judged by eye
|
|
35
|
+
* first so a dark sky does not read as blue and a red button does not read as
|
|
36
|
+
* brown.
|
|
37
|
+
* @param {number} r - red 0..255.
|
|
38
|
+
* @param {number} g - green 0..255.
|
|
39
|
+
* @param {number} b - blue 0..255.
|
|
40
|
+
* @returns {string} one character.
|
|
41
|
+
*/
|
|
42
|
+
export function quantize(r, g, b) {
|
|
43
|
+
/* Red, green and blue are counted more heavily than a plain euclidean
|
|
44
|
+
distance would: two colours a person calls different should not collapse
|
|
45
|
+
into the same letter. */
|
|
46
|
+
const red = r * 1.30
|
|
47
|
+
const green = g * 1.15
|
|
48
|
+
const blue = b * 0.90
|
|
49
|
+
let best = PALETTE[0].char
|
|
50
|
+
let near = Infinity
|
|
51
|
+
for (const entry of PALETTE) {
|
|
52
|
+
const dr = red - entry.rgb[0] * 1.30
|
|
53
|
+
const dg = green - entry.rgb[1] * 1.15
|
|
54
|
+
const db = blue - entry.rgb[2] * 0.90
|
|
55
|
+
const d = dr * dr + dg * dg + db * db
|
|
56
|
+
if (d < near) {
|
|
57
|
+
near = d
|
|
58
|
+
best = entry.char
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return best
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Read a PNG made by Chrome into plain pixels. Chrome writes 8-bit
|
|
66
|
+
* non-interlaced images, RGB or RGBA; anything else is refused with a reason
|
|
67
|
+
* rather than a guess.
|
|
68
|
+
* @param {Buffer} file - the PNG bytes.
|
|
69
|
+
* @returns {{width: number, height: number, data: Uint8Array, channels: number}} pixels, row-major.
|
|
70
|
+
*/
|
|
71
|
+
export function decodePng(file) {
|
|
72
|
+
const bytes = new Uint8Array(file.buffer !== undefined && file.byteOffset === 0 && file.byteLength === file.buffer.byteLength ? file.buffer : file)
|
|
73
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength)
|
|
74
|
+
const signature = [137, 80, 78, 71, 13, 10, 26, 10]
|
|
75
|
+
for (let i = 0; i < signature.length; i += 1) {
|
|
76
|
+
if (bytes[i] !== signature[i]) throw new Error('pixels: not a PNG')
|
|
77
|
+
}
|
|
78
|
+
let width = 0
|
|
79
|
+
let height = 0
|
|
80
|
+
let depth = 0
|
|
81
|
+
let type = 0
|
|
82
|
+
let interlace = 0
|
|
83
|
+
const chunks = []
|
|
84
|
+
let at = 8
|
|
85
|
+
while (at + 8 <= bytes.length) {
|
|
86
|
+
const length = view.getUint32(at)
|
|
87
|
+
const name = String.fromCharCode(bytes[at + 4], bytes[at + 5], bytes[at + 6], bytes[at + 7])
|
|
88
|
+
const body = bytes.subarray(at + 8, at + 8 + length)
|
|
89
|
+
if (name === 'IHDR') {
|
|
90
|
+
width = view.getUint32(at + 8)
|
|
91
|
+
height = view.getUint32(at + 12)
|
|
92
|
+
depth = body[8]
|
|
93
|
+
type = body[9]
|
|
94
|
+
interlace = body[12]
|
|
95
|
+
} else if (name === 'IDAT') chunks.push(body)
|
|
96
|
+
else if (name === 'IEND') break
|
|
97
|
+
at += 12 + length
|
|
98
|
+
}
|
|
99
|
+
if (depth !== 8) throw new Error(`pixels: PNG depth ${depth} is not 8`)
|
|
100
|
+
const channels = type === 6 ? 4 : type === 2 ? 3 : type === 0 ? 1 : 0
|
|
101
|
+
if (channels === 0) throw new Error(`pixels: PNG colour type ${type} is not RGB`)
|
|
102
|
+
if (interlace !== 0) throw new Error('pixels: interlaced PNG is not supported')
|
|
103
|
+
const packed = inflateSync(Buffer.concat(chunks.map((chunk) => Buffer.from(chunk))))
|
|
104
|
+
const stride = width * channels
|
|
105
|
+
const out = new Uint8Array(width * height * channels)
|
|
106
|
+
let raw = 0
|
|
107
|
+
for (let y = 0; y < height; y += 1) {
|
|
108
|
+
const filter = packed[raw]
|
|
109
|
+
raw += 1
|
|
110
|
+
const row = out.subarray(y * stride, (y + 1) * stride)
|
|
111
|
+
const up = y === 0 ? null : out.subarray((y - 1) * stride, y * stride)
|
|
112
|
+
for (let x = 0; x < stride; x += 1) {
|
|
113
|
+
const value = packed[raw + x]
|
|
114
|
+
const left = x >= channels ? row[x - channels] : 0
|
|
115
|
+
const above = up === null ? 0 : up[x]
|
|
116
|
+
const corner = up !== null && x >= channels ? up[x - channels] : 0
|
|
117
|
+
let said = value
|
|
118
|
+
if (filter === 1) said = value + left
|
|
119
|
+
else if (filter === 2) said = value + above
|
|
120
|
+
else if (filter === 3) said = value + ((left + above) >> 1)
|
|
121
|
+
else if (filter === 4) {
|
|
122
|
+
const guess = left + above - corner
|
|
123
|
+
const dl = Math.abs(guess - left)
|
|
124
|
+
const du = Math.abs(guess - above)
|
|
125
|
+
const dc = Math.abs(guess - corner)
|
|
126
|
+
said = value + (dl <= du && dl <= dc ? left : du <= dc ? above : corner)
|
|
127
|
+
} else if (filter !== 0) throw new Error(`pixels: PNG filter ${filter} is not known`)
|
|
128
|
+
row[x] = said & 0xff
|
|
129
|
+
}
|
|
130
|
+
raw += stride
|
|
131
|
+
}
|
|
132
|
+
return { width, height, data: out, channels }
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Average each block of the picture into one character.
|
|
137
|
+
* @param {{width: number, height: number, data: Uint8Array, channels: number}} png - decoded picture.
|
|
138
|
+
* @param {number} cols - cells across.
|
|
139
|
+
* @returns {{rows: string[], cols: number, rowCount: number, blockW: number, blockH: number}} the grid.
|
|
140
|
+
*/
|
|
141
|
+
export function gridOf(png, cols) {
|
|
142
|
+
const across = Math.min(64, Math.max(4, Math.round(cols)))
|
|
143
|
+
const rows = Math.min(64, Math.max(2, Math.round((across * png.height) / Math.max(1, png.width))))
|
|
144
|
+
const blockW = png.width / across
|
|
145
|
+
const blockH = png.height / rows
|
|
146
|
+
const lines = []
|
|
147
|
+
let seen = ''
|
|
148
|
+
for (let ry = 0; ry < rows; ry += 1) {
|
|
149
|
+
let line = ''
|
|
150
|
+
for (let rx = 0; rx < across; rx += 1) {
|
|
151
|
+
const x0 = Math.floor(rx * blockW)
|
|
152
|
+
const x1 = Math.max(x0 + 1, Math.min(png.width, Math.floor((rx + 1) * blockW)))
|
|
153
|
+
const y0 = Math.floor(ry * blockH)
|
|
154
|
+
const y1 = Math.max(y0 + 1, Math.min(png.height, Math.floor((ry + 1) * blockH)))
|
|
155
|
+
let r = 0
|
|
156
|
+
let g = 0
|
|
157
|
+
let b = 0
|
|
158
|
+
let n = 0
|
|
159
|
+
for (let y = y0; y < y1; y += 1) {
|
|
160
|
+
for (let x = x0; x < x1; x += 1) {
|
|
161
|
+
const at = (y * png.width + x) * png.channels
|
|
162
|
+
r += png.data[at]
|
|
163
|
+
g += png.data[at + 1] ?? png.data[at]
|
|
164
|
+
b += png.data[at + 2] ?? png.data[at]
|
|
165
|
+
n += 1
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
line += quantize(r / n, g / n, b / n)
|
|
169
|
+
}
|
|
170
|
+
lines.push(line)
|
|
171
|
+
seen += line
|
|
172
|
+
}
|
|
173
|
+
return { rows: lines, cols: across, rowCount: rows, blockW, blockH }
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* What changed between two grids of the same screen.
|
|
178
|
+
* @param {string[]} before - earlier rows.
|
|
179
|
+
* @param {string[]} after - later rows.
|
|
180
|
+
* @returns {{cells: number, box: {x: number, y: number, w: number, h: number} | null}} the change.
|
|
181
|
+
*/
|
|
182
|
+
export function diffGrid(before, after) {
|
|
183
|
+
if (before.length !== after.length || before.some((row, i) => row.length !== after[i].length)) {
|
|
184
|
+
return { cells: Number.POSITIVE_INFINITY, box: null }
|
|
185
|
+
}
|
|
186
|
+
let cells = 0
|
|
187
|
+
let x0 = Infinity
|
|
188
|
+
let y0 = Infinity
|
|
189
|
+
let x1 = -1
|
|
190
|
+
let y1 = -1
|
|
191
|
+
for (let y = 0; y < after.length; y += 1) {
|
|
192
|
+
for (let x = 0; x < after[y].length; x += 1) {
|
|
193
|
+
if (before[y][x] === after[y][x]) continue
|
|
194
|
+
cells += 1
|
|
195
|
+
if (x < x0) x0 = x
|
|
196
|
+
if (y < y0) y0 = y
|
|
197
|
+
if (x > x1) x1 = x
|
|
198
|
+
if (y > y1) y1 = y
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (cells === 0) return { cells: 0, box: null }
|
|
202
|
+
return { cells, box: { x: x0, y: y0, w: x1 - x0 + 1, h: y1 - y0 + 1 } }
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Cut one box out of a grid, with a cell of margin around it.
|
|
207
|
+
* @param {string[]} rows - the whole grid.
|
|
208
|
+
* @param {{x: number, y: number, w: number, h: number}} box - changed cells.
|
|
209
|
+
* @returns {{rows: string[], x: number, y: number}} the cut grid and where it starts.
|
|
210
|
+
*/
|
|
211
|
+
export function around(rows, box) {
|
|
212
|
+
const x = Math.max(0, box.x - 1)
|
|
213
|
+
const y = Math.max(0, box.y - 1)
|
|
214
|
+
const to = Math.min(rows[0].length, box.x + box.w + 1)
|
|
215
|
+
const down = Math.min(rows.length, box.y + box.h + 1)
|
|
216
|
+
return { rows: rows.slice(y, down).map((row) => row.slice(x, to)), x, y }
|
|
217
|
+
}
|