dsh-inner-browser-use 0.35.2 → 0.35.3
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/package.json +1 -1
- package/read.js +61 -1
- package/tools.js +68 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-inner-browser-use",
|
|
3
|
-
"version": "0.35.
|
|
3
|
+
"version": "0.35.3",
|
|
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",
|
package/read.js
CHANGED
|
@@ -432,6 +432,66 @@ export function resolve(ref) {
|
|
|
432
432
|
})()`
|
|
433
433
|
}
|
|
434
434
|
|
|
435
|
+
/**
|
|
436
|
+
* The first of several controls that still says these words, found in one
|
|
437
|
+
* evaluate. A caller hunts through a dozen numbers after a redraw: one page
|
|
438
|
+
* round trip answers "which of them is still there", instead of a dozen.
|
|
439
|
+
* @param {number[]} refs - numbers to try, in the order a person would.
|
|
440
|
+
* @param {string} wanted - the words, compared with the spaces taken out.
|
|
441
|
+
* @returns {string} an expression that answers with the first that is live.
|
|
442
|
+
*/
|
|
443
|
+
export function resolveFirst(refs, wanted) {
|
|
444
|
+
const order = refs.map((ref) => Number(ref)).filter((ref) => Number.isFinite(ref))
|
|
445
|
+
return `(() => {
|
|
446
|
+
const reg = window.${REGISTRY}
|
|
447
|
+
if (!reg) return JSON.stringify({ gone: true })
|
|
448
|
+
const squeeze = (text) => String(text).toLowerCase().replace(/\\s+/g, '')
|
|
449
|
+
const want = ${JSON.stringify(String(wanted))}
|
|
450
|
+
const spotOf = (el) => {
|
|
451
|
+
const b = el.getBoundingClientRect()
|
|
452
|
+
if (b.width < 2 || b.height < 2) return null
|
|
453
|
+
const clipped = () => {
|
|
454
|
+
const me = el.getBoundingClientRect()
|
|
455
|
+
for (let up = el.parentElement; up; up = up.parentElement) {
|
|
456
|
+
const css = getComputedStyle(up)
|
|
457
|
+
if (!/auto|scroll|hidden|clip/.test(css.overflowX + ' ' + css.overflowY)) continue
|
|
458
|
+
const box = up.getBoundingClientRect()
|
|
459
|
+
if (me.top < box.top - 1 || me.bottom > box.bottom + 1 || me.left < box.left - 1 || me.right > box.right + 1) return true
|
|
460
|
+
}
|
|
461
|
+
return false
|
|
462
|
+
}
|
|
463
|
+
if (b.top < 0 || b.left < 0 || b.bottom > innerHeight || b.right > innerWidth || clipped()) el.scrollIntoView({ block: 'center', inline: 'center' })
|
|
464
|
+
const after = el.getBoundingClientRect()
|
|
465
|
+
return { x: Math.round(after.left + after.width / 2), y: Math.round(after.top + after.height / 2) }
|
|
466
|
+
}
|
|
467
|
+
for (const ref of ${JSON.stringify(order)}) {
|
|
468
|
+
const hit = reg.find(ref)
|
|
469
|
+
if (!hit) continue
|
|
470
|
+
const el = hit.el
|
|
471
|
+
const name = String(el.innerText ?? el.value ?? el.getAttribute?.('aria-label') ?? '').trim().slice(0, 80)
|
|
472
|
+
if (want !== '' && !squeeze(name).includes(want)) continue
|
|
473
|
+
const spot = spotOf(el)
|
|
474
|
+
if (spot === null) continue
|
|
475
|
+
return JSON.stringify({ gone: false, ref, name, x: spot.x, y: spot.y })
|
|
476
|
+
}
|
|
477
|
+
return JSON.stringify({ gone: true })
|
|
478
|
+
})()`
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Ask the page which of these numbers still carries the words.
|
|
483
|
+
* @param {any} page - sidecar page.
|
|
484
|
+
* @param {number[]} refs - numbers to try, in order.
|
|
485
|
+
* @param {string} wanted - the words said by the caller.
|
|
486
|
+
* @returns {Promise<{gone: boolean, ref?: number, name?: string, x?: number, y?: number}>} the first live one.
|
|
487
|
+
*/
|
|
488
|
+
export async function locateFirst(page, refs, wanted) {
|
|
489
|
+
const said = await evaluateIn(page, resolveFirst(refs, wanted)).catch(() => undefined)
|
|
490
|
+
let spot = { gone: true }
|
|
491
|
+
try { spot = JSON.parse(String(said ?? '{"gone":true}')) } catch (error) { spot = { gone: true } }
|
|
492
|
+
return spot
|
|
493
|
+
}
|
|
494
|
+
|
|
435
495
|
/**
|
|
436
496
|
* Click the control the page itself registered, instead of moving the mouse to
|
|
437
497
|
* it. A page whose own handlers want a click, or a control an overlay covers,
|
|
@@ -685,7 +745,7 @@ export function covering(ref) {
|
|
|
685
745
|
}
|
|
686
746
|
|
|
687
747
|
/** Where refs of controls inside iframes start. Main-frame refs stay small. */
|
|
688
|
-
const FRAME_BASE = 900_000
|
|
748
|
+
export const FRAME_BASE = 900_000
|
|
689
749
|
|
|
690
750
|
/** Room for the refs of one frame between FRAME_BASE steps. */
|
|
691
751
|
const FRAME_SLOT = 100_000
|
package/tools.js
CHANGED
|
@@ -15,7 +15,7 @@ import { homedir } from 'node:os'
|
|
|
15
15
|
import { basename, join } from 'node:path'
|
|
16
16
|
import { mkdir, readdir, stat, unlink, writeFile } from 'node:fs/promises'
|
|
17
17
|
import { VIEW } from './sidecar.js?v=1'
|
|
18
|
-
import { readPage, renderPage, readArticle, readRelevant, findOnPage, locate, covering, contextFor, identityOf, sameThing, differs, labelSpot, selectSpot, bounds, pictures, clickInDom, writeInDom, REF_GONE } from './read.js?v=1'
|
|
18
|
+
import { readPage, renderPage, readArticle, readRelevant, findOnPage, locate, locateFirst, FRAME_BASE, covering, contextFor, identityOf, sameThing, differs, labelSpot, selectSpot, bounds, pictures, clickInDom, writeInDom, REF_GONE } from './read.js?v=1'
|
|
19
19
|
import { brief as askBrief, recordRound, lastRoundNote, lastRound, markRound, MARK_STATUS } from './review.js?v=1'
|
|
20
20
|
|
|
21
21
|
/** Which chat the running call belongs to, so each gets its own tab. */
|
|
@@ -1050,6 +1050,49 @@ async function until(page, expression, timeout) {
|
|
|
1050
1050
|
* @returns {Promise<string>} a compact report.
|
|
1051
1051
|
*/
|
|
1052
1052
|
/** The first control whose words match, scrolled into view. Text can beat a number: it costs no read. */
|
|
1053
|
+
/** Words a label can lose without changing what it asks for. */
|
|
1054
|
+
const SMALL_WORDS = new Set(['a', 'an', 'the', 'to', 'of', 'and', 'or', 'in', 'on', 'at', 'for', 'with', 'my', 'your', 'please', 'click', 'tap', 'button', 'btn', 'link', 'now'])
|
|
1055
|
+
|
|
1056
|
+
/**
|
|
1057
|
+
* The significant words of a label, lowercased.
|
|
1058
|
+
* @param {unknown} text - a label.
|
|
1059
|
+
* @returns {string[]} words worth comparing.
|
|
1060
|
+
*/
|
|
1061
|
+
export function tokensOf(text) {
|
|
1062
|
+
return String(text ?? '').toLowerCase().split(/[^a-z0-9\u4e00-\u9fff]+/).filter((word) => word !== '' && !SMALL_WORDS.has(word))
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
/**
|
|
1066
|
+
* The one label that means what the caller said when no label repeats it.
|
|
1067
|
+
* Only an unambiguous winner is offered, so a paraphrase cannot silently click
|
|
1068
|
+
* a different control than the one asked for.
|
|
1069
|
+
* @param {Array<{ref: number, name?: string, view?: boolean}>} controls - controls from a read.
|
|
1070
|
+
* @param {string} words - what the caller asked for.
|
|
1071
|
+
* @returns {any} the best control, or null when nothing stands out.
|
|
1072
|
+
*/
|
|
1073
|
+
export function nearestByWords(controls, words) {
|
|
1074
|
+
const want = tokensOf(words)
|
|
1075
|
+
if (want.length === 0 || !Array.isArray(controls)) return null
|
|
1076
|
+
const scored = []
|
|
1077
|
+
for (const control of controls) {
|
|
1078
|
+
if (control?.view === false) continue
|
|
1079
|
+
const have = new Set(tokensOf(control?.name))
|
|
1080
|
+
if (have.size === 0) continue
|
|
1081
|
+
let shared = 0
|
|
1082
|
+
for (const word of want) if (have.has(word)) shared += 1
|
|
1083
|
+
const cover = shared / want.length
|
|
1084
|
+
if (shared === 0 || cover < 0.5) continue
|
|
1085
|
+
scored.push({ control, score: cover * 0.7 + (shared / have.size) * 0.3 })
|
|
1086
|
+
}
|
|
1087
|
+
if (scored.length === 0) return null
|
|
1088
|
+
scored.sort((a, b) => b.score - a.score)
|
|
1089
|
+
const best = scored[0]
|
|
1090
|
+
const second = scored[1]
|
|
1091
|
+
if (second !== undefined && second.score > best.score - 0.05) return null
|
|
1092
|
+
if (String(best.control.name ?? '').length > String(words).length * 3 + 12) return null
|
|
1093
|
+
return { ...best.control, ref: Number(best.control.ref) }
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1053
1096
|
export async function findWords(page, words, known) {
|
|
1054
1097
|
/* The words a caller used usually came from a read that already happened, so
|
|
1055
1098
|
look among those numbers first: one position check is a single evaluate,
|
|
@@ -1066,7 +1109,19 @@ export async function findWords(page, words, known) {
|
|
|
1066
1109
|
end is only the fallback. */
|
|
1067
1110
|
const firstInOrder = remembered.filter((control) => control.view !== false).sort((a, b) => Number(a.ref) - Number(b.ref))
|
|
1068
1111
|
const nearEnd = remembered.slice(-8).reverse()
|
|
1069
|
-
|
|
1112
|
+
const order = [...firstInOrder.slice(0, 4), ...nearEnd]
|
|
1113
|
+
/* One round trip answers which of these the page still has: a dozen numbers
|
|
1114
|
+
left over from a read that a redraw invalidated used to cost a dozen
|
|
1115
|
+
checks. Numbers inside a frame keep the per-number route below. */
|
|
1116
|
+
const plain = order.map((control) => Number(control.ref)).filter((ref) => Number.isFinite(ref) && ref < FRAME_BASE)
|
|
1117
|
+
if (plain.length > 0) {
|
|
1118
|
+
const any = await locateFirst(page, plain, wantedFirst)
|
|
1119
|
+
if (any?.gone !== true && Number.isFinite(Number(any?.x))) {
|
|
1120
|
+
const byRef = order.find((control) => Number(control.ref) === Number(any.ref)) ?? { ref: Number(any.ref) }
|
|
1121
|
+
return { ...byRef, ref: Number(any.ref), name: String(any.name ?? byRef.name ?? ''), x: Number(any.x), y: Number(any.y) }
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
for (const control of order.filter((control) => Number(control.ref) >= FRAME_BASE)) {
|
|
1070
1125
|
const spot = await locate(page, Number(control.ref))
|
|
1071
1126
|
if (spot.gone === true) continue
|
|
1072
1127
|
const name = String(spot.name ?? control.name ?? '')
|
|
@@ -1080,7 +1135,17 @@ export async function findWords(page, words, known) {
|
|
|
1080
1135
|
const squeeze = (text) => String(text).toLowerCase().replace(/\s+/g, '')
|
|
1081
1136
|
const wanted = squeeze(words)
|
|
1082
1137
|
const found = shown.find((control) => squeeze(control.name) === wanted) ?? shown.find((control) => squeeze(control.name).includes(wanted))
|
|
1083
|
-
if (found === undefined)
|
|
1138
|
+
if (found === undefined) {
|
|
1139
|
+
/* A button the page redrew often only swaps one word, and a person says
|
|
1140
|
+
"submit order" for "Place order". Accept only an unambiguous winner. */
|
|
1141
|
+
const near = nearestByWords(shown, words)
|
|
1142
|
+
if (near !== null) {
|
|
1143
|
+
const spot = await locate(page, near.ref)
|
|
1144
|
+
if (spot.gone === true) return null
|
|
1145
|
+
return { ...near, x: Number(spot.x), y: Number(spot.y) }
|
|
1146
|
+
}
|
|
1147
|
+
return null
|
|
1148
|
+
}
|
|
1084
1149
|
/* When only the squeezed form matched, say the words the caller used: the
|
|
1085
1150
|
page spread them over nodes and "Signin" is not what anyone wrote. */
|
|
1086
1151
|
const hit = squeeze(found.name) === wanted && found.name.toLowerCase() !== words.toLowerCase() ? { ...found, name: words } : found
|