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.
Files changed (3) hide show
  1. package/package.json +1 -1
  2. package/read.js +61 -1
  3. 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.2",
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
- for (const control of [...firstInOrder.slice(0, 4), ...nearEnd]) {
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) return null
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