返回 DeepSeek-Reasonix
script.go
根目录 / internal / browser / cdp / script.go
1 package cdp
2
3 // bootstrapJS installs the executor's isolated-world helper. It runs once per
4 // document in a world the page cannot reach, so refs, the take-over counter,
5 // and the agent-input window are out of reach of page script. Every later
6 // evaluate is one call into __rx.
7 const bootstrapJS = `
8 (() => {
9 const W = {refs: new Map(), next: 1, userSeq: 0, agentUntil: 0};
10 globalThis.__rx = W;
11 const mark = (e) => { if (e.isTrusted && Date.now() > W.agentUntil) W.userSeq++; };
12 for (const t of ['pointerdown', 'keydown', 'wheel']) {
13 document.addEventListener(t, mark, {capture: true, passive: true});
14 }
15 const clip = (s, n) => s.length > n ? s.slice(0, n) + '…' : s;
16 const flat = (s) => (s || '').replace(/\s+/g, ' ').trim();
17 const named = (s) => s ? ' ' + JSON.stringify(s) : '';
18 const ACTIONABLE = 'a[href],button,input,select,textarea,summary,[role],[tabindex],[contenteditable]';
19
20 W.window = (ms) => { W.agentUntil = Date.now() + ms; return W.userSeq; };
21 W.state = () => ({userSeq: W.userSeq, url: location.href, title: document.title, ready: document.readyState});
22 W.element = (ref) => { const el = W.refs.get(ref); return (el && el.isConnected) ? el : null; };
23
24 W.rect = (ref) => {
25 const el = W.element(ref);
26 if (!el) return null;
27 try { el.scrollIntoView({block: 'center', inline: 'center'}); } catch (e) {}
28 const r = el.getBoundingClientRect();
29 const tag = el.tagName.toLowerCase();
30 const type = flat((el.getAttribute('type') || '')).toLowerCase();
31 if (r.width <= 0 && r.height <= 0) return {hidden: true, tag: tag, type: type};
32 return {x: r.left + r.width / 2, y: r.top + r.height / 2, width: r.width, height: r.height, tag: tag, type: type};
33 };
34
35 W.focus = (ref) => {
36 const el = W.element(ref);
37 if (!el) return {ok: false, reason: 'the ref is not on this page any more'};
38 try { el.focus({preventScroll: false}); } catch (e) {}
39 const active = document.activeElement;
40 return {ok: active === el || el.contains(active), reason: 'the element refused focus'};
41 };
42
43 W.select = (ref, wanted) => {
44 const el = W.element(ref);
45 if (!el) return {ok: false, reason: 'the ref is not on this page any more'};
46 if (el.tagName !== 'SELECT') return {ok: false, reason: 'element ' + el.tagName.toLowerCase() + ' is not a select'};
47 const opts = Array.from(el.options);
48 const chosen = [];
49 for (const want of wanted) {
50 const hit = opts.find((o) => o.value === want) || opts.find((o) => flat(o.label || o.textContent) === want);
51 if (!hit) return {ok: false, reason: 'no option has the value or label ' + JSON.stringify(want)};
52 chosen.push(hit);
53 }
54 if (chosen.length > 1 && !el.multiple) return {ok: false, reason: 'this select accepts one option'};
55 for (const o of opts) o.selected = false;
56 for (const o of chosen) o.selected = true;
57 el.dispatchEvent(new Event('input', {bubbles: true}));
58 el.dispatchEvent(new Event('change', {bubbles: true}));
59 return {ok: true, selected: chosen.map((o) => o.value)};
60 };
61
62 const SKIP = new Set(['script', 'style', 'noscript', 'template', 'head', 'meta', 'link', 'title', 'svg', 'path', 'br']);
63 const ROLES = {
64 a: 'link', button: 'button', select: 'combobox', textarea: 'textbox', summary: 'disclosure',
65 h1: 'heading', h2: 'heading', h3: 'heading', h4: 'heading', h5: 'heading', h6: 'heading',
66 img: 'image', table: 'table', th: 'columnheader', td: 'cell', li: 'listitem', form: 'form',
67 nav: 'navigation', main: 'main', header: 'banner', footer: 'contentinfo', dialog: 'dialog',
68 option: 'option', label: 'label', iframe: 'iframe',
69 };
70 const INPUT_ROLES = {
71 checkbox: 'checkbox', radio: 'radio', submit: 'button', button: 'button', reset: 'button',
72 file: 'file-input', range: 'slider', color: 'color-picker', hidden: '',
73 };
74
75 const typeOf = (el) => flat(el.getAttribute('type') || '').toLowerCase();
76
77 const roleOf = (el) => {
78 const explicit = flat(el.getAttribute('role'));
79 if (explicit) return explicit.split(' ')[0];
80 const tag = el.tagName.toLowerCase();
81 if (tag === 'input') {
82 const t = typeOf(el);
83 return Object.prototype.hasOwnProperty.call(INPUT_ROLES, t) ? INPUT_ROLES[t] : 'textbox';
84 }
85 return ROLES[tag] || '';
86 };
87
88 // nameOf follows the accessible-name order that matters here: an explicit
89 // label beats the placeholder a user only sees while the field is empty, and
90 // an element's own text names it only when it has nothing else to say, so a
91 // container is not named after everything inside it.
92 const nameOf = (el, allowText) => {
93 const aria = flat(el.getAttribute('aria-label'));
94 if (aria) return clip(aria, 160);
95 if (el.labels && el.labels.length) {
96 const label = flat(el.labels[0].textContent);
97 if (label) return clip(label, 160);
98 }
99 const attr = flat(el.getAttribute('alt') || el.getAttribute('title') || el.getAttribute('placeholder'));
100 if (attr) return clip(attr, 160);
101 if (!allowText) return '';
102 return clip(flat(el.innerText || el.textContent), 160);
103 };
104
105 const interactive = (el) => {
106 const tag = el.tagName.toLowerCase();
107 if (tag === 'a') return el.hasAttribute('href');
108 if (tag === 'button' || tag === 'select' || tag === 'textarea' || tag === 'summary') return true;
109 if (tag === 'input') return typeOf(el) !== 'hidden';
110 if (el.isContentEditable) return true;
111 if (el.hasAttribute('tabindex') || el.hasAttribute('onclick')) return true;
112 const role = flat(el.getAttribute('role'));
113 return ['button', 'link', 'checkbox', 'radio', 'tab', 'menuitem', 'switch', 'option', 'textbox', 'searchbox'].includes(role);
114 };
115
116 const visible = (el) => {
117 if (typeof el.checkVisibility === 'function') return el.checkVisibility({checkVisibilityCSS: true});
118 return el.getClientRects().length > 0;
119 };
120
121 const attrsOf = (el) => {
122 const out = [];
123 if (el.disabled) out.push('disabled');
124 if (el.checked) out.push('checked');
125 if (el.required) out.push('required');
126 if (el.getAttribute('aria-expanded') === 'true') out.push('expanded');
127 if (el.tagName === 'A' && el.getAttribute('href')) out.push('href=' + clip(el.getAttribute('href'), 120));
128 if ('value' in el && typeOf(el) !== 'password' && flat(el.value)) out.push('value=' + JSON.stringify(clip(flat(el.value), 80)));
129 if (typeOf(el) === 'password' && el.value) out.push('value=(hidden)');
130 return out.length ? ' [' + out.join(' ') + ']' : '';
131 };
132
133 W.snapshot = (selector, budget) => {
134 W.refs = new Map();
135 W.next = 1;
136 let root = document.body || document.documentElement;
137 if (selector) {
138 root = document.querySelector(selector);
139 if (!root) return {error: 'no element matches the selector'};
140 }
141 const lines = [];
142 let left = budget;
143 const pad = (d) => ' '.repeat(Math.min(d, 20));
144 const walk = (el, depth) => {
145 if (left <= 0 || depth > 40) return;
146 for (const node of el.childNodes) {
147 if (left <= 0) return;
148 if (node.nodeType === 3) {
149 const text = flat(node.textContent);
150 if (text) { left--; lines.push(pad(depth) + '- text: ' + clip(text, 200)); }
151 continue;
152 }
153 if (node.nodeType !== 1 || SKIP.has(node.tagName.toLowerCase()) || !visible(node)) continue;
154 const role = roleOf(node);
155 const leaf = node.children.length === 0;
156 if (interactive(node)) {
157 const ref = 'e' + (W.next++);
158 W.refs.set(ref, node);
159 left--;
160 lines.push(pad(depth) + '- ' + (role || 'control') + named(nameOf(node, true)) + attrsOf(node) + ' [ref=' + ref + ']');
161 // A control's own text is already its name; walk into it only when it
162 // wraps another control the model could act on.
163 if (!leaf && node.tagName !== 'SELECT' && node.tagName !== 'TEXTAREA' && node.querySelector(ACTIONABLE)) walk(node, depth + 1);
164 continue;
165 }
166 if (role) {
167 left--;
168 lines.push(pad(depth) + '- ' + role + named(nameOf(node, leaf)));
169 if (!leaf) walk(node, depth + 1);
170 continue;
171 }
172 walk(node, depth);
173 }
174 };
175 walk(root, 0);
176 return {
177 url: location.href, title: document.title, tree: lines.join('\n'),
178 refs: W.next - 1, userSeq: W.userSeq, truncated: left <= 0,
179 };
180 };
181 })()
182 `
183
183 lines GO