dsh-inner-browser-use 0.43.0 → 0.44.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/docs/ARCHITECTURE.md +9 -3
- package/docs/BENCH.md +14 -0
- package/package.json +2 -2
- package/read.js +87 -17
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -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
|
package/docs/BENCH.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
## 读页先读正文(0.44.0,本地页面,`scripts/bench.mjs`)
|
|
2
|
+
|
|
3
|
+
同一台机器、同一档预算。旧法是按页面顺序把预算灌满的整页读。
|
|
4
|
+
|
|
5
|
+
| 页面 | 旧法字符 | 新法字符 | 省下 | 答案 |
|
|
6
|
+
| --- | --- | --- | --- | --- |
|
|
7
|
+
| 九成是菜单、答案在正文(900 字预算) | 1500 | 88 | 94% | 旧法读不到,新法读到 |
|
|
8
|
+
| 定价表(900 字预算) | 679 | 109 | 84% | 价格与列名同一行 |
|
|
9
|
+
|
|
10
|
+
- `read-noise` 2 ms、`read-table` 2 ms,两项预算 300 ms 全过(26 项全过)
|
|
11
|
+
- 表格行形如 `Plan: Pro | Price per month: $84`,列名来自表头,一行就是一条记录
|
|
12
|
+
- 标题路径 `Plans \u203a Trial` 印在证据上方;与最近标题相同、或超过 90 字时退回最近标题
|
|
13
|
+
- 既有数字没有回退:`read-answer` 90 字 / 92%、`read-miss` 438 字 / 79%、`read-tabs` 90 字 / 90%(与 0.43.0 相同)
|
|
14
|
+
|
|
1
15
|
## 一次读穿过登录墙(0.43.0,本地页面,`scripts/bench.mjs`)
|
|
2
16
|
|
|
3
17
|
同一页:带 cookie 时是账户页,不带时是一个只有密码框的登录页。留一个登录(vault),清掉页面的 cookie 与 localStorage 后读它。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-inner-browser-use",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.44.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",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"dsh": ">=0.2.0"
|
|
52
52
|
},
|
|
53
53
|
"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",
|
|
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/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
55
|
"doctor": "node scripts/doctor.mjs",
|
|
56
56
|
"breadth": "node scripts/breadth.mjs",
|
|
57
57
|
"live": "node scripts/live.mjs",
|
package/read.js
CHANGED
|
@@ -1124,10 +1124,12 @@ export async function findOnPage(page, query, options = {}) {
|
|
|
1124
1124
|
}
|
|
1125
1125
|
|
|
1126
1126
|
/**
|
|
1127
|
-
* Collect the page as blocks of prose
|
|
1128
|
-
* how much of it is links
|
|
1129
|
-
*
|
|
1130
|
-
*
|
|
1127
|
+
* Collect the page as blocks of prose. Each block carries the trail of headings
|
|
1128
|
+
* it sits under, how much of it is links, and one number for how much of it
|
|
1129
|
+
* reads like written content: navigation, sidebars and footers are mostly links
|
|
1130
|
+
* and mostly short, which is Readability's oldest signal. A table row is one
|
|
1131
|
+
* block, with its column names written into it, so a fact that lives in a table
|
|
1132
|
+
* costs a line instead of a page.
|
|
1131
1133
|
* @param {number} budget - characters of block text to collect.
|
|
1132
1134
|
* @returns {string} an expression to evaluate.
|
|
1133
1135
|
*/
|
|
@@ -1136,32 +1138,66 @@ export function blocks(budget) {
|
|
|
1136
1138
|
const root = document.querySelector('article, main, [role="main"]') || document.body
|
|
1137
1139
|
if (!root) return JSON.stringify({ blocks: [] })
|
|
1138
1140
|
const limit = ${budget}
|
|
1139
|
-
const
|
|
1141
|
+
const guard = limit * 6
|
|
1142
|
+
const BLOCK = new Set(['P', 'LI', 'BLOCKQUOTE', 'PRE', 'FIGCAPTION', 'DD', 'DT', 'SUMMARY'])
|
|
1140
1143
|
const SKIP = new Set(['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEMPLATE', 'SVG', 'IFRAME', 'CANVAS', 'VIDEO', 'HEADER', 'FOOTER', 'NAV', 'ASIDE', 'FORM', 'BUTTON', 'SELECT', 'TEXTAREA'])
|
|
1141
1144
|
const out = []
|
|
1142
1145
|
const seen = new Set()
|
|
1146
|
+
const stack = []
|
|
1143
1147
|
let chars = 0
|
|
1144
|
-
let heading = ''
|
|
1145
1148
|
const flatten = (text) => String(text ?? '').replace(/\\s+/g, ' ').trim()
|
|
1149
|
+
const trail = () => stack.filter((one) => one !== '').join(' \\u203a ')
|
|
1150
|
+
const said = () => stack.filter((one) => one !== '').pop() ?? ''
|
|
1151
|
+
/* How much of a block reads like prose rather than a list of links: ends of
|
|
1152
|
+
sentences per word, plus a length floor. One number, no dictionary. */
|
|
1153
|
+
const written = (text) => {
|
|
1154
|
+
const words = text.toLowerCase().match(/[a-z0-9]+|[\\u3400-\\u9fff]/g) ?? []
|
|
1155
|
+
if (words.length === 0) return 0
|
|
1156
|
+
const ends = text.match(/[.!?\\u3002\\uff01\\uff1f;\\uff1b]/g) ?? []
|
|
1157
|
+
return Math.min(1, (ends.length / words.length) * 12)
|
|
1158
|
+
}
|
|
1159
|
+
const quality = (text, density) => Math.round((0.45 * (1 - density) + 0.35 * written(text) + 0.2 * Math.min(1, text.length / 180)) * 100) / 100
|
|
1146
1160
|
const push = (el) => {
|
|
1147
|
-
if (chars >=
|
|
1161
|
+
if (chars >= guard) return
|
|
1148
1162
|
const text = flatten(el.innerText ?? el.textContent ?? '')
|
|
1149
1163
|
if (text === '' || text.length > 4000) return
|
|
1150
1164
|
let linkChars = 0
|
|
1151
1165
|
for (const link of el.querySelectorAll('a')) linkChars += flatten(link.innerText ?? '').length
|
|
1152
1166
|
if (el.tagName === 'A') linkChars += text.length
|
|
1153
|
-
const density =
|
|
1154
|
-
|
|
1167
|
+
const density = Math.min(1, linkChars / text.length)
|
|
1168
|
+
/* A block that is almost all link and short is a menu item, not a sentence;
|
|
1169
|
+
carrying it only spends the budget. */
|
|
1170
|
+
if (density >= 0.8 && text.length < 80) return
|
|
1171
|
+
const path = trail()
|
|
1172
|
+
out.push({ h: said(), p: path, t: text, d: Math.round(density * 100) / 100, q: quality(text, density) })
|
|
1155
1173
|
chars += text.length
|
|
1156
1174
|
}
|
|
1175
|
+
const row = (tr) => {
|
|
1176
|
+
if (chars >= guard) return
|
|
1177
|
+
const cells = Array.from(tr.children).filter((cell) => cell.tagName === 'TD' || cell.tagName === 'TH')
|
|
1178
|
+
const words = cells.map((cell) => flatten(cell.innerText ?? ''))
|
|
1179
|
+
if (words.filter((one) => one !== '').length < 2) return
|
|
1180
|
+
const table = tr.closest('table')
|
|
1181
|
+
const first = table === null ? null : table.querySelector('tr')
|
|
1182
|
+
const head = first === null || first === tr ? [] : Array.from(first.children).map((cell) => flatten(cell.innerText ?? ''))
|
|
1183
|
+
const line = words.map((one, at) => (head[at] === undefined || head[at] === '' || head[at] === one ? one : head[at] + ': ' + one)).join(' | ')
|
|
1184
|
+
out.push({ h: said(), p: trail(), t: line, d: 0, q: 0.95, r: 1 })
|
|
1185
|
+
chars += line.length
|
|
1186
|
+
}
|
|
1157
1187
|
const walk = (el) => {
|
|
1158
|
-
if (
|
|
1188
|
+
if (el === null || chars >= guard) return
|
|
1159
1189
|
const tag = el.tagName
|
|
1160
1190
|
if (SKIP.has(tag) || el.hidden === true || el.getAttribute?.('aria-hidden') === 'true') return
|
|
1161
1191
|
const style = getComputedStyle(el)
|
|
1162
1192
|
if (style.display === 'none' || style.visibility === 'hidden') return
|
|
1163
1193
|
if (/^H[1-6]$/.test(tag)) {
|
|
1164
|
-
|
|
1194
|
+
const level = Number(tag.slice(1))
|
|
1195
|
+
stack.length = level
|
|
1196
|
+
stack[level - 1] = flatten(el.innerText ?? '')
|
|
1197
|
+
return
|
|
1198
|
+
}
|
|
1199
|
+
if (tag === 'TR') {
|
|
1200
|
+
if (!seen.has(el)) { seen.add(el); row(el) }
|
|
1165
1201
|
return
|
|
1166
1202
|
}
|
|
1167
1203
|
if (BLOCK.has(tag)) {
|
|
@@ -1172,7 +1208,23 @@ export function blocks(budget) {
|
|
|
1172
1208
|
}
|
|
1173
1209
|
walk(root)
|
|
1174
1210
|
const title = document.title
|
|
1175
|
-
|
|
1211
|
+
/* The page arrives in document order, so a menu at the top would spend the
|
|
1212
|
+
whole budget before the article starts. Keep the blocks that read like
|
|
1213
|
+
content, still in page order, until the budget is spent. */
|
|
1214
|
+
let kept = out
|
|
1215
|
+
const total = out.reduce((sum, block) => sum + block.t.length, 0)
|
|
1216
|
+
if (total > limit) {
|
|
1217
|
+
const byQuality = out.map((block, at) => ({ block, at })).sort((a, b) => b.block.q - a.block.q || a.at - b.at)
|
|
1218
|
+
const keep = new Set()
|
|
1219
|
+
let used = 0
|
|
1220
|
+
for (const one of byQuality) {
|
|
1221
|
+
if (used + one.block.t.length > limit) continue
|
|
1222
|
+
keep.add(one.at)
|
|
1223
|
+
used += one.block.t.length
|
|
1224
|
+
}
|
|
1225
|
+
kept = out.filter((block, at) => keep.has(at))
|
|
1226
|
+
}
|
|
1227
|
+
return JSON.stringify({ title, blocks: kept })
|
|
1176
1228
|
})()`
|
|
1177
1229
|
}
|
|
1178
1230
|
|
|
@@ -1217,6 +1269,20 @@ function brief(value) {
|
|
|
1217
1269
|
return body.length > 260 ? `${body.slice(0, 260).replace(/\s+\S*$/, '')}…` : body
|
|
1218
1270
|
}
|
|
1219
1271
|
|
|
1272
|
+
/**
|
|
1273
|
+
* The trail of headings a block sits under, when it says more than the nearest
|
|
1274
|
+
* one: "Pricing › Refunds" tells a caller where a fact lives, which a bare
|
|
1275
|
+
* sentence cannot. Kept short so location never costs more than it explains.
|
|
1276
|
+
* @param {{h?: string, p?: string}} block - the block.
|
|
1277
|
+
* @returns {string} what to print above the passage.
|
|
1278
|
+
*/
|
|
1279
|
+
export function trailOf(block) {
|
|
1280
|
+
const path = String(block?.p ?? '')
|
|
1281
|
+
const near = String(block?.h ?? '')
|
|
1282
|
+
if (path === '' || path === near || path.length > 90) return near
|
|
1283
|
+
return path
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1220
1286
|
/**
|
|
1221
1287
|
* Rank blocks against a query the way a search engine ranks a passage: BM25,
|
|
1222
1288
|
* with the heading counted as a place a term can match. Then read only the
|
|
@@ -1228,7 +1294,7 @@ function brief(value) {
|
|
|
1228
1294
|
export function rank(blocks, query) {
|
|
1229
1295
|
const asked = terms(query)
|
|
1230
1296
|
if (asked.length === 0) return []
|
|
1231
|
-
const docs = blocks.map((block) => ({ block, terms: terms(`${block.h ?? ''} ${block.t}`) }))
|
|
1297
|
+
const docs = blocks.map((block) => ({ block, terms: terms(`${block.p ?? ''} ${block.h ?? ''} ${block.t}`) }))
|
|
1232
1298
|
const average = docs.reduce((sum, one) => sum + one.terms.length, 0) / Math.max(1, docs.length)
|
|
1233
1299
|
const seen = new Map()
|
|
1234
1300
|
for (const one of docs) for (const term of new Set(one.terms)) seen.set(term, (seen.get(term) ?? 0) + 1)
|
|
@@ -1250,6 +1316,9 @@ export function rank(blocks, query) {
|
|
|
1250
1316
|
/* A block that is nearly all link is a menu: it matches words without
|
|
1251
1317
|
saying anything about them. */
|
|
1252
1318
|
score *= 1 - 0.8 * Number(one.block.d ?? 0)
|
|
1319
|
+
/* Prose that answers a question outranks a fragment of navigation that
|
|
1320
|
+
happens to contain the same words; the quality number says which. */
|
|
1321
|
+
score *= 0.85 + 0.15 * Number(one.block.q ?? 0.5)
|
|
1253
1322
|
if (score > 0) scored.push({ block: one.block, score })
|
|
1254
1323
|
}
|
|
1255
1324
|
scored.sort((a, b) => b.score - a.score)
|
|
@@ -1297,7 +1366,7 @@ export function evidence(blocks, query, options = {}) {
|
|
|
1297
1366
|
const near = rank(blocks, query).slice(0, Math.max(1, Math.min(60, Number(options.blocks ?? 40))))
|
|
1298
1367
|
const units = []
|
|
1299
1368
|
for (const row of near) {
|
|
1300
|
-
const heading =
|
|
1369
|
+
const heading = trailOf(row.block)
|
|
1301
1370
|
const body = String(row.block.t ?? '')
|
|
1302
1371
|
const parts = sentences(body)
|
|
1303
1372
|
for (let step = 0; step < parts.length; step += 1) {
|
|
@@ -1339,8 +1408,9 @@ export function evidence(blocks, query, options = {}) {
|
|
|
1339
1408
|
both say "this is the right place", so they lift their sentences. */
|
|
1340
1409
|
const headingHit = asked.filter((term) => terms(unit.heading).includes(term)).length
|
|
1341
1410
|
/* A menu sentence can match the words without saying anything about them;
|
|
1342
|
-
the paragraph's link density
|
|
1343
|
-
|
|
1411
|
+
the paragraph's link density and how much it reads like prose tell the
|
|
1412
|
+
two apart, and a table row is data rather than a sentence to distrust. */
|
|
1413
|
+
const linkish = (1 - 0.8 * Number(unit.block?.d ?? 0)) * (unit.block?.r === 1 ? 1 : 0.9 + 0.1 * Number(unit.block?.q ?? 0.5))
|
|
1344
1414
|
unit.score = score * linkish * (1 + 0.25 * headingHit) * (1 + 0.5 * (unit.prior > 0 ? 1 : 0))
|
|
1345
1415
|
}
|
|
1346
1416
|
/* Below a fifth of the best sentence, a match is coincidence rather than
|
|
@@ -1424,7 +1494,7 @@ export async function readRelevant(page, query, options = {}) {
|
|
|
1424
1494
|
for (const row of pieces.slice(0, limit)) {
|
|
1425
1495
|
const body = String(row.block.t ?? '')
|
|
1426
1496
|
if (used + body.length > budget) continue
|
|
1427
|
-
const heading =
|
|
1497
|
+
const heading = trailOf(row.block)
|
|
1428
1498
|
if (heading !== '' && heading !== lastHeading) { lines.push(`## ${heading}`); lastHeading = heading }
|
|
1429
1499
|
lines.push(body)
|
|
1430
1500
|
used += body.length
|