@evan_zhong/browser-attach 0.17.0 → 0.17.2
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 +7 -5
- package/docs/TEST-REPORT.md +12 -4
- package/docs/USAGE.md +430 -431
- package/extension/connection.js +1 -1
- package/extension/controller.js +1 -1
- package/extension/manifest.json +1 -1
- package/extension/popup.js +7 -1
- package/extension/snapshot.js +39 -19
- package/extension/wait.js +1 -1
- package/package.json +1 -1
- package/src/config.mjs +1 -1
- package/src/mcp.mjs +1 -1
package/extension/connection.js
CHANGED
|
@@ -126,7 +126,7 @@ export function createConnection(chrome, CONNECTION, controller) {
|
|
|
126
126
|
respond({ ok: true });
|
|
127
127
|
}
|
|
128
128
|
if (message.type === 'release_all') {
|
|
129
|
-
|
|
129
|
+
release().then(() => respond({ ok: true }), error => respond({ ok: false, error: error.message }));
|
|
130
130
|
return true;
|
|
131
131
|
}
|
|
132
132
|
});
|
package/extension/controller.js
CHANGED
|
@@ -343,7 +343,7 @@ export function createController(api, notify = () => {}) {
|
|
|
343
343
|
}
|
|
344
344
|
const rendered = renderAX(nodes, token, refs, frame.id, target.sessionId, options);
|
|
345
345
|
if (rendered.length || options.query == null) entries.push({ node: false, frameId: frame.id, line: `Frame ${frame.id} ${frame.url}` });
|
|
346
|
-
entries.push(
|
|
346
|
+
for (const entry of rendered) entries.push(entry);
|
|
347
347
|
}
|
|
348
348
|
const metadata = value(await send(tabId, 'Runtime.evaluate', {
|
|
349
349
|
expression: '({title: document.title, url: location.href})', returnByValue: true,
|
package/extension/manifest.json
CHANGED
package/extension/popup.js
CHANGED
|
@@ -4,7 +4,13 @@ async function refresh() {
|
|
|
4
4
|
document.querySelector('#profile').textContent = `${result.browser.family} · ${result.browser.profile_name}`;
|
|
5
5
|
document.querySelector('#claims').textContent = `正在控制 ${result.claims} 个标签页`;
|
|
6
6
|
}
|
|
7
|
-
document.querySelector('#release').onclick = async () => {
|
|
7
|
+
document.querySelector('#release').onclick = async () => {
|
|
8
|
+
try {
|
|
9
|
+
const result = await chrome.runtime.sendMessage({ type: 'release_all' });
|
|
10
|
+
await refresh();
|
|
11
|
+
if (!result.ok) document.querySelector('#status').textContent = `释放失败:${result.error}`;
|
|
12
|
+
} catch (error) { document.querySelector('#status').textContent = `释放失败:${error.message}`; }
|
|
13
|
+
};
|
|
8
14
|
document.querySelector('#reconnect').onclick = async () => { await chrome.runtime.sendMessage({ type: 'reconnect' }); await refresh(); };
|
|
9
15
|
document.querySelector('#settings').onclick = () => chrome.runtime.openOptionsPage();
|
|
10
16
|
refresh().catch(error => { document.querySelector('#status').textContent = error.message; });
|
package/extension/snapshot.js
CHANGED
|
@@ -21,11 +21,15 @@ export function renderAX(nodes, token, refs, frameId, sessionId, options = snaps
|
|
|
21
21
|
const parents = new Map();
|
|
22
22
|
for (const node of nodes) for (const child of node.childIds || []) parents.set(child, node.nodeId);
|
|
23
23
|
function keepText(node) {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
24
|
+
const pending = [node];
|
|
25
|
+
while (pending.length) {
|
|
26
|
+
const current = pending.pop();
|
|
27
|
+
if (!current || kept.has(current.nodeId)) continue;
|
|
28
|
+
const role = current.role?.value;
|
|
29
|
+
if (!textRoles.has(role) && !current.ignored && !['none', 'generic'].includes(role)) continue;
|
|
30
|
+
kept.add(current.nodeId);
|
|
31
|
+
for (const child of current.childIds || []) pending.push(byId.get(child));
|
|
32
|
+
}
|
|
29
33
|
}
|
|
30
34
|
for (const node of nodes) {
|
|
31
35
|
if (node.ignored || !node.backendDOMNodeId || !(controls.has(node.role?.value) ||
|
|
@@ -37,19 +41,33 @@ export function renderAX(nodes, token, refs, frameId, sessionId, options = snaps
|
|
|
37
41
|
}
|
|
38
42
|
}
|
|
39
43
|
function plainText(node) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
const pending = [node], parts = [];
|
|
45
|
+
while (pending.length) {
|
|
46
|
+
const current = pending.pop();
|
|
47
|
+
if (current === ' ') { parts.push(' '); continue; }
|
|
48
|
+
if (!current) return null;
|
|
49
|
+
const role = current.role?.value;
|
|
50
|
+
if (textRoles.has(role)) { parts.push(String(current.name?.value || '')); continue; }
|
|
51
|
+
if (!current.ignored && !['none', 'generic'].includes(role)) return null;
|
|
52
|
+
const children = current.childIds || [];
|
|
53
|
+
for (let index = children.length - 1; index >= 0; index--) {
|
|
54
|
+
pending.push(byId.get(children[index]));
|
|
55
|
+
if (index > 0) pending.push(' ');
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return parts.join('');
|
|
46
59
|
}
|
|
47
|
-
|
|
48
|
-
|
|
60
|
+
// Preserve preorder and ref numbering without using the JavaScript call stack.
|
|
61
|
+
const children = new Set();
|
|
62
|
+
for (const node of nodes) for (const child of node.childIds || []) children.add(child);
|
|
63
|
+
const pending = nodes.filter(node => !children.has(node.nodeId)).reverse().map(node => ({ node, depth: 0, parentName: '' }));
|
|
64
|
+
while (pending.length) {
|
|
65
|
+
const { node, depth, parentName } = pending.pop();
|
|
66
|
+
if (!node || visited.has(node.nodeId)) continue;
|
|
49
67
|
visited.add(node.nodeId);
|
|
50
|
-
if (kept && !kept.has(node.nodeId))
|
|
68
|
+
if (kept && !kept.has(node.nodeId)) continue;
|
|
51
69
|
const role = node.role?.value || 'node';
|
|
52
|
-
if (role === 'InlineTextBox')
|
|
70
|
+
if (role === 'InlineTextBox') continue;
|
|
53
71
|
let name = String(node.name?.value || '');
|
|
54
72
|
let merged = false;
|
|
55
73
|
if (['listitem', 'paragraph'].includes(role) && !name) {
|
|
@@ -77,11 +95,13 @@ export function renderAX(nodes, token, refs, frameId, sessionId, options = snaps
|
|
|
77
95
|
}
|
|
78
96
|
nextDepth++;
|
|
79
97
|
}
|
|
80
|
-
if (!merged)
|
|
98
|
+
if (!merged) {
|
|
99
|
+
const childIds = node.childIds || [];
|
|
100
|
+
for (let index = childIds.length - 1; index >= 0; index--) {
|
|
101
|
+
pending.push({ node: byId.get(childIds[index]), depth: nextDepth, parentName: skip ? parentName : identityName });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
81
104
|
}
|
|
82
|
-
// AX trees normally have a single root, but ignored roots can be disconnected.
|
|
83
|
-
const children = new Set(nodes.flatMap(node => node.childIds || []));
|
|
84
|
-
for (const node of nodes) if (!children.has(node.nodeId)) render(node, 0);
|
|
85
105
|
return entries;
|
|
86
106
|
}
|
|
87
107
|
|
package/extension/wait.js
CHANGED
|
@@ -70,7 +70,7 @@ export function inspectWait(query, inspect, args) {
|
|
|
70
70
|
if (!element) return { satisfied: false, state: 'absent' };
|
|
71
71
|
if (args.condition === 'attached') return { satisfied: true, state: 'attached' };
|
|
72
72
|
if (args.condition === 'text') {
|
|
73
|
-
const text = element.
|
|
73
|
+
const text = element.innerText ?? '';
|
|
74
74
|
return { satisfied: args.previous != null ? text !== args.previous : text.includes(args.expected), text: text.slice(0, 500) };
|
|
75
75
|
}
|
|
76
76
|
const bounds = inspect.call(element);
|
package/package.json
CHANGED
package/src/config.mjs
CHANGED
|
@@ -15,7 +15,7 @@ export const DATA_DIR = configuredHome
|
|
|
15
15
|
: process.platform === 'win32' ? join(homedir(), '.browser-attach')
|
|
16
16
|
: join(process.env.XDG_DATA_HOME || join(homedir(), '.local', 'share'), 'browser-attach');
|
|
17
17
|
export const DEFAULT_CONFIG = join(DATA_DIR, 'config.json');
|
|
18
|
-
export const VERSION = '0.17.
|
|
18
|
+
export const VERSION = '0.17.2';
|
|
19
19
|
|
|
20
20
|
export async function loadConfig(path = DEFAULT_CONFIG) {
|
|
21
21
|
let config;
|
package/src/mcp.mjs
CHANGED
|
@@ -147,7 +147,7 @@ export function createMcp(client) {
|
|
|
147
147
|
}, null, args => { dragOptions(args); return args; });
|
|
148
148
|
tool('wait', 'Wait without scrolling or input. actionable requires a stable, visible, enabled, unobscured target in the viewport; network_idle uses observed page requests. Timeout includes the last observed state.', {
|
|
149
149
|
...handle, ...dialogPolicy, timeout_ms: integerRange('timeout_ms', 100, 30000).default(5000), ...selector, ...frameScope, nth: target.nth,
|
|
150
|
-
condition: z.enum(['attached', 'visible', 'actionable', 'text', 'url', 'network_idle']), expected: z.string().max(2000).optional().describe('For text:
|
|
150
|
+
condition: z.enum(['attached', 'visible', 'actionable', 'text', 'url', 'network_idle']), expected: z.string().max(2000).optional().describe('For text: rendered innerText substring; for url: main-frame URL substring. Text may use previous instead.'), previous: z.string().max(100000).optional().describe('Text only: wait for full rendered innerText to differ; alternative to expected.'), idle_ms: integerRange('idle_ms', 100, 10000).default(500),
|
|
151
151
|
}, null, args => { waitOptions(args); return args; });
|
|
152
152
|
tool('select_option', 'Set one HTML select option without native UI, using synthetic input/change (events_trusted=false). Verifies retained selection only.', {
|
|
153
153
|
...handle, ...inputTimeout, ...target, value: z.string().optional().describe('Choose exactly one of value, label or zero-based index; value/label match exactly.'), label: z.string().optional(), index: integerRange('index', 0, Number.MAX_SAFE_INTEGER).optional(),
|