@sleepinsummer/agent-browser-cli 0.2.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/AI_INSTALL.md +126 -0
- package/LICENSE +21 -0
- package/README.md +194 -0
- package/README_EN.md +169 -0
- package/assets/tmwd_cdp_bridge/background.js +436 -0
- package/assets/tmwd_cdp_bridge/config.js +1 -0
- package/assets/tmwd_cdp_bridge/content.js +79 -0
- package/assets/tmwd_cdp_bridge/disable_dialogs.js +24 -0
- package/assets/tmwd_cdp_bridge/manifest.json +40 -0
- package/assets/tmwd_cdp_bridge/popup.html +19 -0
- package/assets/tmwd_cdp_bridge/popup.js +24 -0
- package/npm/bin/agent-browser-cli.js +35 -0
- package/npm/platform/darwin-arm64/bin/agent-browser-cli +0 -0
- package/npm/platform/darwin-arm64/package.json +14 -0
- package/npm/postinstall.js +9 -0
- package/package.json +30 -0
- package/skills/agent-browser-cli/SKILL.md +315 -0
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
// background.js - Cookie + CDP Bridge
|
|
2
|
+
chrome.runtime.onInstalled.addListener(() => {
|
|
3
|
+
console.log('CDP Bridge installed');
|
|
4
|
+
// Strip CSP headers to allow eval/inline scripts
|
|
5
|
+
chrome.declarativeNetRequest.updateDynamicRules({
|
|
6
|
+
removeRuleIds: [9999],
|
|
7
|
+
addRules: [{
|
|
8
|
+
id: 9999, priority: 1,
|
|
9
|
+
action: { type: 'modifyHeaders', responseHeaders: [
|
|
10
|
+
{ header: 'content-security-policy', operation: 'remove' },
|
|
11
|
+
{ header: 'content-security-policy-report-only', operation: 'remove' }
|
|
12
|
+
]},
|
|
13
|
+
condition: { urlFilter: '*', resourceTypes: ['main_frame', 'sub_frame'] }
|
|
14
|
+
}]
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
async function handleExtMessage(msg, sender) {
|
|
19
|
+
if (msg.cmd === 'status') return handleStatus();
|
|
20
|
+
if (msg.cmd === 'cookies') return await handleCookies(msg, sender);
|
|
21
|
+
if (msg.cmd === 'cdp') return await handleCDP(msg, sender);
|
|
22
|
+
if (msg.cmd === 'batch') return await handleBatch(msg, sender);
|
|
23
|
+
if (msg.cmd === 'openTab') return await handleOpenTab(msg);
|
|
24
|
+
if (msg.cmd === 'tabs') {
|
|
25
|
+
try {
|
|
26
|
+
if (msg.method === 'switch') {
|
|
27
|
+
const tab = await chrome.tabs.update(msg.tabId, { active: true });
|
|
28
|
+
await chrome.windows.update(tab.windowId, { focused: true });
|
|
29
|
+
return { ok: true };
|
|
30
|
+
} else {
|
|
31
|
+
const tabs = (await chrome.tabs.query({})).filter(t => isScriptable(t.url));
|
|
32
|
+
const data = tabs.map(t => ({ id: t.id, url: t.url, title: t.title, active: t.active, windowId: t.windowId }));
|
|
33
|
+
return { ok: true, data };
|
|
34
|
+
}
|
|
35
|
+
} catch (e) { return { ok: false, error: e.message }; }
|
|
36
|
+
}
|
|
37
|
+
if (msg.cmd === 'management') {
|
|
38
|
+
try {
|
|
39
|
+
if (msg.method === 'list') {
|
|
40
|
+
const all = await chrome.management.getAll();
|
|
41
|
+
return { ok: true, data: all.map(e => ({ id: e.id, name: e.name, enabled: e.enabled, type: e.type, version: e.version })) };
|
|
42
|
+
}
|
|
43
|
+
if (msg.method === 'reload') {
|
|
44
|
+
chrome.alarms.create('tmwd-self-reload', { when: Date.now() + 200 });
|
|
45
|
+
return { ok: true };
|
|
46
|
+
}
|
|
47
|
+
if (msg.method === 'disable') {
|
|
48
|
+
await chrome.management.setEnabled(msg.extId, false);
|
|
49
|
+
return { ok: true };
|
|
50
|
+
}
|
|
51
|
+
if (msg.method === 'enable') {
|
|
52
|
+
await chrome.management.setEnabled(msg.extId, true);
|
|
53
|
+
return { ok: true };
|
|
54
|
+
}
|
|
55
|
+
return { ok: false, error: 'Unknown method: ' + msg.method };
|
|
56
|
+
} catch (e) { return { ok: false, error: e.message }; }
|
|
57
|
+
}
|
|
58
|
+
if (msg.cmd === 'contentSettings') {
|
|
59
|
+
try {
|
|
60
|
+
const type = msg.type || 'automaticDownloads';
|
|
61
|
+
const setting = msg.setting || 'allow';
|
|
62
|
+
const pattern = msg.pattern || '<all_urls>';
|
|
63
|
+
await chrome.contentSettings[type].set({
|
|
64
|
+
primaryPattern: pattern,
|
|
65
|
+
setting: setting
|
|
66
|
+
});
|
|
67
|
+
return { ok: true };
|
|
68
|
+
} catch (e) { return { ok: false, error: e.message }; }
|
|
69
|
+
}
|
|
70
|
+
return { ok: false, error: 'Unknown cmd: ' + msg.cmd };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function handleStatus() {
|
|
74
|
+
return {
|
|
75
|
+
ok: true,
|
|
76
|
+
data: {
|
|
77
|
+
wsConnected: !!ws && ws.readyState === WebSocket.OPEN,
|
|
78
|
+
wsUrl: WS_URL
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
|
84
|
+
handleExtMessage(msg, sender).then(sendResponse);
|
|
85
|
+
return true;
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
async function handleCookies(msg, sender) {
|
|
89
|
+
try {
|
|
90
|
+
let url = msg.url || sender.tab?.url;
|
|
91
|
+
if (!url && msg.tabId) {
|
|
92
|
+
const tab = await chrome.tabs.get(msg.tabId);
|
|
93
|
+
url = tab.url;
|
|
94
|
+
}
|
|
95
|
+
const origin = url.match(/^https?:\/\/[^\/]+/)[0];
|
|
96
|
+
const all = await chrome.cookies.getAll({ url });
|
|
97
|
+
const part = await chrome.cookies.getAll({ url, partitionKey: { topLevelSite: origin } }).catch(() => []);
|
|
98
|
+
const merged = [...all];
|
|
99
|
+
for (const c of part) {
|
|
100
|
+
if (!merged.some(x => x.name === c.name && x.domain === c.domain)) merged.push(c);
|
|
101
|
+
}
|
|
102
|
+
return { ok: true, data: merged };
|
|
103
|
+
} catch (e) {
|
|
104
|
+
return { ok: false, error: e.message };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function handleOpenTab(msg) {
|
|
109
|
+
try {
|
|
110
|
+
const url = normalizeOpenUrl(msg.url);
|
|
111
|
+
const active = msg.active !== false;
|
|
112
|
+
const tab = await chrome.tabs.create({ url, active });
|
|
113
|
+
if (active && tab.windowId) await chrome.windows.update(tab.windowId, { focused: true });
|
|
114
|
+
return { ok: true, data: { id: tab.id, url: tab.url || url, title: tab.title || '', active: tab.active, windowId: tab.windowId } };
|
|
115
|
+
} catch (e) {
|
|
116
|
+
return { ok: false, error: e.message };
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function normalizeOpenUrl(url) {
|
|
121
|
+
const raw = String(url || '').trim();
|
|
122
|
+
if (!raw) throw new Error('url is required');
|
|
123
|
+
if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(raw)) return raw;
|
|
124
|
+
return 'https://' + raw;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function handleBatch(msg, sender) {
|
|
128
|
+
const R = [];
|
|
129
|
+
let attached = null;
|
|
130
|
+
const resolve$N = (params) => JSON.parse(JSON.stringify(params || {}).replace(/"\$(\d+)\.([^"]+)"/g,
|
|
131
|
+
(_, i, path) => { let v = R[+i]; for (const k of path.split('.')) v = v[k]; return JSON.stringify(v); }));
|
|
132
|
+
try {
|
|
133
|
+
for (const c of msg.commands) {
|
|
134
|
+
if (c.tabId === undefined && msg.tabId !== undefined) c.tabId = msg.tabId;
|
|
135
|
+
if (c.cmd === 'cookies') {
|
|
136
|
+
R.push(await handleCookies(c, sender));
|
|
137
|
+
} else if (c.cmd === 'tabs') {
|
|
138
|
+
const tabs = (await chrome.tabs.query({})).filter(t => isScriptable(t.url));
|
|
139
|
+
R.push({ ok: true, data: tabs.map(t => ({ id: t.id, url: t.url, title: t.title, active: t.active, windowId: t.windowId })) });
|
|
140
|
+
} else if (c.cmd === 'cdp') {
|
|
141
|
+
const tabId = c.tabId || msg.tabId || sender.tab?.id;
|
|
142
|
+
if (attached !== tabId) {
|
|
143
|
+
if (attached) { await chrome.debugger.detach({ tabId: attached }); attached = null; }
|
|
144
|
+
await chrome.debugger.attach({ tabId }, '1.3');
|
|
145
|
+
attached = tabId;
|
|
146
|
+
}
|
|
147
|
+
R.push(await chrome.debugger.sendCommand({ tabId }, c.method, resolve$N(c.params)));
|
|
148
|
+
} else {
|
|
149
|
+
R.push({ ok: false, error: 'unknown cmd: ' + c.cmd });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (attached) await chrome.debugger.detach({ tabId: attached });
|
|
153
|
+
return { ok: true, results: R };
|
|
154
|
+
} catch (e) {
|
|
155
|
+
if (attached) try { await chrome.debugger.detach({ tabId: attached }); } catch (_) {}
|
|
156
|
+
return { ok: false, error: e.message, results: R };
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function handleCDP(msg, sender) {
|
|
161
|
+
const tabId = msg.tabId || sender.tab?.id;
|
|
162
|
+
if (!tabId) return { ok: false, error: 'no tabId' };
|
|
163
|
+
try {
|
|
164
|
+
await chrome.debugger.attach({ tabId }, '1.3');
|
|
165
|
+
const result = await chrome.debugger.sendCommand({ tabId }, msg.method, msg.params || {});
|
|
166
|
+
await chrome.debugger.detach({ tabId });
|
|
167
|
+
return { ok: true, data: result };
|
|
168
|
+
} catch (e) {
|
|
169
|
+
try { await chrome.debugger.detach({ tabId }); } catch (_) {}
|
|
170
|
+
return { ok: false, error: e.message };
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
// Filter out chrome:// and other internal tabs that can't be scripted
|
|
174
|
+
const isScriptable = url => url && /^https?:/.test(url);
|
|
175
|
+
|
|
176
|
+
// --- Shared page/CDP script builder core ---
|
|
177
|
+
function buildExecScript(code, errorHandler) {
|
|
178
|
+
return `(async () => {
|
|
179
|
+
function smartProcessResult(result) {
|
|
180
|
+
if (result === null || result === undefined || typeof result !== 'object') return result;
|
|
181
|
+
try { if (result.window === result && result.document) return '[Window: ' + (result.location?.href || 'about:blank') + ']'; } catch(_){}
|
|
182
|
+
if (typeof jQuery !== 'undefined' && result instanceof jQuery) {
|
|
183
|
+
const elements = []; for (let i = 0; i < result.length; i++) { if (result[i] && result[i].nodeType === 1) elements.push(result[i].outerHTML); } return elements;
|
|
184
|
+
}
|
|
185
|
+
if (result instanceof NodeList || result instanceof HTMLCollection) {
|
|
186
|
+
const elements = []; for (let i = 0; i < result.length; i++) { if (result[i] && result[i].nodeType === 1) elements.push(result[i].outerHTML); } return elements;
|
|
187
|
+
}
|
|
188
|
+
if (result.nodeType === 1) return result.outerHTML;
|
|
189
|
+
if (!Array.isArray(result) && typeof result === 'object' && 'length' in result && typeof result.length === 'number') {
|
|
190
|
+
const firstElement = result[0];
|
|
191
|
+
if (firstElement && firstElement.nodeType === 1) {
|
|
192
|
+
const elements = []; const length = Math.min(result.length, 100);
|
|
193
|
+
for (let i = 0; i < length; i++) { const elem = result[i]; if (elem && elem.nodeType === 1) elements.push(elem.outerHTML); } return elements;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
try { return JSON.parse(JSON.stringify(result, function(key, value) { if (typeof value === 'object' && value !== null) { if (value.nodeType === 1) return value.outerHTML; if (value === window || value === document) return '[Object]'; try { if (value.window === value && value.document) return '[Window]'; } catch(_){} } return value; })); } catch (e) { return '[无法序列化: ' + e.message + ']'; }
|
|
197
|
+
}
|
|
198
|
+
try {
|
|
199
|
+
const jsCode = ${JSON.stringify(code)}.trim();
|
|
200
|
+
const lines = jsCode.split(/\\r?\\n/).filter(l => l.trim());
|
|
201
|
+
const lastLine = lines.length > 0 ? lines[lines.length - 1].trim() : '';
|
|
202
|
+
const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor;
|
|
203
|
+
let r;
|
|
204
|
+
function _air(c) { const ls = c.split(/\\r?\\n/); let i = ls.length - 1; while (i >= 0 && !ls[i].trim()) i--; if (i < 0) return c; const t = ls[i].trim(); if (/^(return |return;|return$|let |const |var |if |if\\(|for |for\\(|while |while\\(|switch|try |throw |class |function |async |import |export |\\/\\/|})/.test(t)) return c; ls[i] = ls[i].match(/^(\\s*)/)[1] + 'return ' + t; return ls.join('\\n'); }
|
|
205
|
+
if (lastLine.startsWith('return')) {
|
|
206
|
+
r = await (new AsyncFunction(jsCode))();
|
|
207
|
+
} else {
|
|
208
|
+
try { r = eval(jsCode); if (r instanceof Promise) r = await r; } catch (e) {
|
|
209
|
+
if (e instanceof SyntaxError && (/return/i.test(e.message) || /await/i.test(e.message))) { r = await (new AsyncFunction(_air(jsCode)))(); } else throw e;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return { ok: true, data: smartProcessResult(r) };
|
|
213
|
+
} catch (e) {
|
|
214
|
+
${errorHandler}
|
|
215
|
+
}
|
|
216
|
+
})()`;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function buildPageScript(code) {
|
|
220
|
+
return buildExecScript(code, `
|
|
221
|
+
const errMsg = e.message || String(e);
|
|
222
|
+
return { ok: false, error: { name: e.name || 'Error', message: errMsg, stack: e.stack || '' },
|
|
223
|
+
csp: errMsg.includes('Refused to evaluate') || errMsg.includes('unsafe-eval') || errMsg.includes('Content Security Policy') };
|
|
224
|
+
`);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function buildCdpScript(code) {
|
|
228
|
+
return buildExecScript(code, `
|
|
229
|
+
return { ok: false, error: { name: e.name || 'Error', message: e.message || String(e), stack: e.stack || '' } };
|
|
230
|
+
`);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// --- WebSocket Client for TMWebDriver ---
|
|
234
|
+
let ws = null;
|
|
235
|
+
const WS_URL = 'ws://127.0.0.1:18765';
|
|
236
|
+
|
|
237
|
+
function scheduleProbe() {
|
|
238
|
+
// Use chrome.alarms to survive MV3 service worker suspension
|
|
239
|
+
chrome.alarms.create('tmwd-ws-probe', { delayInMinutes: 0.083 }); // ~5s
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function scheduleKeepalive() {
|
|
243
|
+
// Keep SW alive while WS is connected (~25s, under 30s SW timeout)
|
|
244
|
+
chrome.alarms.create('tmwd-ws-keepalive', { delayInMinutes: 0.4 }); // ~24s
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async function isServerAlive() {
|
|
248
|
+
try {
|
|
249
|
+
const ctrl = new AbortController();
|
|
250
|
+
setTimeout(() => ctrl.abort(), 2000);
|
|
251
|
+
await fetch('http://127.0.0.1:18765', { signal: ctrl.signal });
|
|
252
|
+
return true; // Got HTTP response → port is listening
|
|
253
|
+
} catch (e) {
|
|
254
|
+
return false; // Network error (connection refused) or timeout → server not alive
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
chrome.alarms.onAlarm.addListener(async (alarm) => {
|
|
259
|
+
if (alarm.name === 'tmwd-self-reload') {
|
|
260
|
+
chrome.runtime.reload();
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
if (alarm.name === 'tmwd-ws-keepalive') {
|
|
264
|
+
// Keepalive: ping to keep SW alive + detect dead connections
|
|
265
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
266
|
+
try { ws.send('{"type":"ping"}'); } catch (_) {}
|
|
267
|
+
scheduleKeepalive();
|
|
268
|
+
} else {
|
|
269
|
+
// Connection lost, switch to probe mode
|
|
270
|
+
ws = null;
|
|
271
|
+
scheduleProbe();
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
if (alarm.name === 'tmwd-ws-probe') {
|
|
275
|
+
if (ws && ws.readyState <= 1) return; // Already connected/connecting
|
|
276
|
+
if (await isServerAlive()) {
|
|
277
|
+
console.log('[TMWD-WS] Server detected, connecting...');
|
|
278
|
+
connectWS();
|
|
279
|
+
} else {
|
|
280
|
+
scheduleProbe(); // Server not up, keep probing
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
async function handleWsExec(data) {
|
|
286
|
+
const tabId = data.tabId;
|
|
287
|
+
console.log('[TMWD-WS] Exec request', data.id, 'on tab', tabId);
|
|
288
|
+
ws.send(JSON.stringify({ type: 'ack', id: data.id }));
|
|
289
|
+
if (!tabId) {
|
|
290
|
+
ws.send(JSON.stringify({ type: 'error', id: data.id, error: 'No tabId provided' }));
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
// Use onCreated listener to reliably capture new tabs (avoids race condition with query-diff)
|
|
294
|
+
const newTabIds = new Set();
|
|
295
|
+
const onCreated = (tab) => { newTabIds.add(tab.id); };
|
|
296
|
+
chrome.tabs.onCreated.addListener(onCreated);
|
|
297
|
+
try {
|
|
298
|
+
let res;
|
|
299
|
+
try {
|
|
300
|
+
const result = await chrome.scripting.executeScript({
|
|
301
|
+
target: { tabId },
|
|
302
|
+
world: 'MAIN',
|
|
303
|
+
func: async (s) => await eval(s),
|
|
304
|
+
args: [buildPageScript(data.code)]
|
|
305
|
+
});
|
|
306
|
+
res = result[0]?.result;
|
|
307
|
+
if (res === null || res === undefined) {
|
|
308
|
+
console.log('[TMWD-WS] executeScript returned null/undefined, treating as CSP issue');
|
|
309
|
+
res = { ok: false, error: { name: 'Error', message: 'executeScript returned null (possible CSP or context issue)', stack: '' }, csp: true };
|
|
310
|
+
}
|
|
311
|
+
} catch (e) {
|
|
312
|
+
console.log('[TMWD-WS] scripting.executeScript failed:', e.message);
|
|
313
|
+
res = { ok: false, error: { name: e.name || 'Error', message: e.message || String(e), stack: e.stack || '' }, csp: true };
|
|
314
|
+
}
|
|
315
|
+
// CDP fallback for CSP-restricted pages
|
|
316
|
+
if (res && !res.ok && res.csp) {
|
|
317
|
+
console.log('[TMWD-WS] CDP fallback for tab', tabId);
|
|
318
|
+
const wrappedCode = buildCdpScript(data.code);
|
|
319
|
+
try {
|
|
320
|
+
await chrome.debugger.attach({ tabId }, '1.3');
|
|
321
|
+
const cdpRes = await chrome.debugger.sendCommand({ tabId }, 'Runtime.evaluate', {
|
|
322
|
+
expression: wrappedCode, awaitPromise: true, returnByValue: true
|
|
323
|
+
});
|
|
324
|
+
await chrome.debugger.detach({ tabId });
|
|
325
|
+
if (cdpRes.exceptionDetails) {
|
|
326
|
+
const desc = cdpRes.exceptionDetails.exception?.description || 'CDP Error';
|
|
327
|
+
res = { ok: false, error: { name: 'Error', message: desc, stack: desc } };
|
|
328
|
+
} else {
|
|
329
|
+
res = cdpRes.result.value;
|
|
330
|
+
}
|
|
331
|
+
} catch (cdpErr) {
|
|
332
|
+
try { await chrome.debugger.detach({ tabId }); } catch (_) {}
|
|
333
|
+
res = { ok: false, error: { name: 'Error', message: 'CDP fallback failed: ' + cdpErr.message, stack: '' } };
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
// Grace period for async tab creation (e.g. link click with target=_blank)
|
|
337
|
+
if (newTabIds.size === 0) await new Promise(r => setTimeout(r, 200));
|
|
338
|
+
chrome.tabs.onCreated.removeListener(onCreated);
|
|
339
|
+
// Get full info for captured new tabs
|
|
340
|
+
const newTabs = [];
|
|
341
|
+
for (const id of newTabIds) {
|
|
342
|
+
try { const t = await chrome.tabs.get(id); newTabs.push({id: t.id, url: t.url, title: t.title}); } catch (_) {}
|
|
343
|
+
}
|
|
344
|
+
if (res?.ok) {
|
|
345
|
+
ws.send(JSON.stringify({ type: 'result', id: data.id, result: res.data, newTabs }));
|
|
346
|
+
} else {
|
|
347
|
+
console.log(res);
|
|
348
|
+
ws.send(JSON.stringify({ type: 'error', id: data.id, error: res?.error || 'Unknown error', newTabs }));
|
|
349
|
+
}
|
|
350
|
+
} catch (e) {
|
|
351
|
+
ws.send(JSON.stringify({ type: 'error', id: data.id, error: { name: e.name || 'Error', message: e.message || String(e), stack: e.stack || '' } }));
|
|
352
|
+
} finally {
|
|
353
|
+
chrome.tabs.onCreated.removeListener(onCreated);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function connectWS() {
|
|
358
|
+
if (ws && ws.readyState <= 1) return; // CONNECTING or OPEN
|
|
359
|
+
ws = null;
|
|
360
|
+
console.log('[TMWD-WS] Connecting to', WS_URL);
|
|
361
|
+
try {
|
|
362
|
+
ws = new WebSocket(WS_URL);
|
|
363
|
+
} catch (e) {
|
|
364
|
+
console.error('[TMWD-WS] Constructor error:', e);
|
|
365
|
+
ws = null;
|
|
366
|
+
scheduleProbe();
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
ws.onopen = async () => {
|
|
370
|
+
console.log('[TMWD-WS] Connected!');
|
|
371
|
+
scheduleKeepalive(); // Keep SW alive while connected
|
|
372
|
+
const tabs = (await chrome.tabs.query({})).filter(t => isScriptable(t.url));
|
|
373
|
+
ws.send(JSON.stringify({
|
|
374
|
+
type: 'ext_ready',
|
|
375
|
+
tabs: tabs.map(t => ({ id: t.id, url: t.url, title: t.title }))
|
|
376
|
+
}));
|
|
377
|
+
console.log('[TMWD-WS] Sent ext_ready with', tabs.length, 'tabs');
|
|
378
|
+
};
|
|
379
|
+
ws.onmessage = async (event) => {
|
|
380
|
+
try {
|
|
381
|
+
const data = JSON.parse(event.data);
|
|
382
|
+
if (data.id && data.code) {
|
|
383
|
+
let code = data.code;
|
|
384
|
+
// If code is a JSON string representing an object, parse it
|
|
385
|
+
if (typeof code === 'string') {
|
|
386
|
+
try { const p = JSON.parse(code); if (p && typeof p === 'object') code = p; } catch (_) {}
|
|
387
|
+
}
|
|
388
|
+
if (typeof code === 'object' && code !== null && code.cmd) {
|
|
389
|
+
// Custom protocol message → route to handleExtMessage
|
|
390
|
+
if (code.tabId === undefined && data.tabId !== undefined) code.tabId = data.tabId;
|
|
391
|
+
const res = await handleExtMessage(code, {});
|
|
392
|
+
ws.send(JSON.stringify({ type: res.ok ? 'result' : 'error', id: data.id, result: res.data ?? res.results ?? res, error: res.error }));
|
|
393
|
+
} else if (typeof code === 'string') {
|
|
394
|
+
// Plain JS code
|
|
395
|
+
await handleWsExec(data);
|
|
396
|
+
} else if (typeof code === 'object' && code !== null) {
|
|
397
|
+
// Object without cmd → legacy extension message
|
|
398
|
+
const msg = code.tabId === undefined && data.tabId !== undefined ? { ...code, tabId: data.tabId } : code;
|
|
399
|
+
const res = await handleExtMessage(msg, {});
|
|
400
|
+
ws.send(JSON.stringify({ type: res.ok ? 'result' : 'error', id: data.id, result: res.data ?? res.results ?? res, error: res.error }));
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
} catch (e) {
|
|
404
|
+
console.error('[TMWD-WS] message parse error', e);
|
|
405
|
+
}
|
|
406
|
+
};
|
|
407
|
+
ws.onclose = () => {
|
|
408
|
+
console.log('[TMWD-WS] Disconnected');
|
|
409
|
+
ws = null;
|
|
410
|
+
scheduleProbe();
|
|
411
|
+
};
|
|
412
|
+
ws.onerror = (e) => {
|
|
413
|
+
console.error('[TMWD-WS] Error:', e);
|
|
414
|
+
// onclose will fire after this, which triggers reconnect
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Initial connect + wake-up hooks
|
|
419
|
+
connectWS();
|
|
420
|
+
chrome.runtime.onStartup.addListener(() => connectWS());
|
|
421
|
+
chrome.runtime.onInstalled.addListener(() => connectWS());
|
|
422
|
+
|
|
423
|
+
// Sync tab list on changes
|
|
424
|
+
async function sendTabsUpdate() {
|
|
425
|
+
if (!ws || ws.readyState !== WebSocket.OPEN) return;
|
|
426
|
+
const tabs = (await chrome.tabs.query({})).filter(t => isScriptable(t.url) && !/streamlit/i.test(t.title));
|
|
427
|
+
ws.send(JSON.stringify({
|
|
428
|
+
type: 'tabs_update',
|
|
429
|
+
tabs: tabs.map(t => ({ id: t.id, url: t.url, title: t.title }))
|
|
430
|
+
}));
|
|
431
|
+
}
|
|
432
|
+
chrome.tabs.onUpdated.addListener((_, changeInfo) => {
|
|
433
|
+
if (changeInfo.status === 'complete') sendTabsUpdate();
|
|
434
|
+
});
|
|
435
|
+
chrome.tabs.onRemoved.addListener(() => sendTabsUpdate());
|
|
436
|
+
chrome.tabs.onCreated.addListener(() => sendTabsUpdate());
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const TID = '__agent_browser_cli_bridge_26c9f1';
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
;(function(){ if (/streamlit/i.test(document.title)) return;
|
|
2
|
+
|
|
3
|
+
// Remove meta CSP tags
|
|
4
|
+
document.querySelectorAll('meta[http-equiv="Content-Security-Policy"]').forEach(e => e.remove());
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Render a bottom-right badge that reflects the real bridge connection status.
|
|
8
|
+
*/
|
|
9
|
+
(function(){
|
|
10
|
+
if(window.self!==window.top)return;
|
|
11
|
+
const d=document.createElement('div');
|
|
12
|
+
d.id='agent-browser-cli-ind';
|
|
13
|
+
d.style.cssText='position:fixed;bottom:8px;right:8px;color:white;padding:4px 7px;border-radius:4px;font-size:11px;font-weight:bold;z-index:99999;cursor:pointer;box-shadow:0 2px 4px rgba(0,0,0,0.2);opacity:0.5;';
|
|
14
|
+
/**
|
|
15
|
+
* Show the badge only when the bridge is really connected.
|
|
16
|
+
*/
|
|
17
|
+
function setBadgeState(connected, detail) {
|
|
18
|
+
// Hide the indicator completely when offline to avoid persistent page noise.
|
|
19
|
+
d.style.display = connected ? 'block' : 'none';
|
|
20
|
+
d.innerText = 'agent_browser_cli: 已连接';
|
|
21
|
+
d.style.background = '#4CAF50';
|
|
22
|
+
d.dataset.connected = connected ? '1' : '0';
|
|
23
|
+
d.dataset.detail = detail || '';
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Poll background status and update the badge visibility.
|
|
27
|
+
*/
|
|
28
|
+
async function refreshBadgeState() {
|
|
29
|
+
try {
|
|
30
|
+
const resp = await chrome.runtime.sendMessage({ cmd: 'status' });
|
|
31
|
+
const connected = !!resp?.ok && !!resp?.data?.wsConnected;
|
|
32
|
+
setBadgeState(connected, resp?.data?.wsUrl || '');
|
|
33
|
+
} catch (e) {
|
|
34
|
+
setBadgeState(false, e.message || '');
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
d.addEventListener('click',()=>alert((d.dataset.connected === '1' ? '会话活跃' : '会话未连接') + '\nURL: ' + location.href + (d.dataset.detail ? '\nBridge: ' + d.dataset.detail : '')));
|
|
38
|
+
(document.body||document.documentElement).appendChild(d);
|
|
39
|
+
setBadgeState(false, '');
|
|
40
|
+
refreshBadgeState();
|
|
41
|
+
setInterval(refreshBadgeState, 3000);
|
|
42
|
+
})();
|
|
43
|
+
|
|
44
|
+
new MutationObserver(muts => {
|
|
45
|
+
for (const m of muts) for (const n of m.addedNodes) {
|
|
46
|
+
if (n.id === TID || (n.querySelector && n.querySelector('#' + TID))) {
|
|
47
|
+
const el = n.id === TID ? n : n.querySelector('#' + TID);
|
|
48
|
+
handle(el);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}).observe(document.documentElement, { childList: true, subtree: true });
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Consume page-side bridge requests and forward them to the extension background worker.
|
|
55
|
+
*/
|
|
56
|
+
async function handle(el) {
|
|
57
|
+
try {
|
|
58
|
+
const req = el.textContent.trim() ? JSON.parse(el.textContent) : { cmd: 'cookies' };
|
|
59
|
+
const cmd = req.cmd || 'cookies';
|
|
60
|
+
let resp;
|
|
61
|
+
if (cmd === 'cookies') {
|
|
62
|
+
resp = await chrome.runtime.sendMessage({ cmd: 'cookies', url: req.url || location.href });
|
|
63
|
+
} else if (cmd === 'cdp') {
|
|
64
|
+
resp = await chrome.runtime.sendMessage({ cmd: 'cdp', method: req.method, params: req.params || {}, tabId: req.tabId });
|
|
65
|
+
} else if (cmd === 'batch') {
|
|
66
|
+
resp = await chrome.runtime.sendMessage({ cmd: 'batch', commands: req.commands, tabId: req.tabId });
|
|
67
|
+
} else if (cmd === 'tabs') {
|
|
68
|
+
resp = await chrome.runtime.sendMessage({ cmd: 'tabs', method: req.method, tabId: req.tabId });
|
|
69
|
+
} else if (cmd === 'openTab') {
|
|
70
|
+
resp = await chrome.runtime.sendMessage({ cmd: 'openTab', url: req.url, active: req.active });
|
|
71
|
+
} else {
|
|
72
|
+
resp = { ok: false, error: 'unknown cmd: ' + cmd };
|
|
73
|
+
}
|
|
74
|
+
el.textContent = JSON.stringify(resp);
|
|
75
|
+
} catch (e) {
|
|
76
|
+
el.textContent = JSON.stringify({ ok: false, error: e.message });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
})();
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Disable alert/confirm/prompt to prevent page JS from blocking extension
|
|
2
|
+
(function() {
|
|
3
|
+
const _log = console.log.bind(console);
|
|
4
|
+
function toast(type, msg) {
|
|
5
|
+
_log('[TMWD] ' + type + ' suppressed:', msg);
|
|
6
|
+
try {
|
|
7
|
+
const d = document.createElement('div');
|
|
8
|
+
d.textContent = '[' + type + '] ' + msg;
|
|
9
|
+
Object.assign(d.style, {
|
|
10
|
+
position:'fixed', top:'12px', right:'12px', zIndex:'2147483647',
|
|
11
|
+
background:'#222', color:'#fff', padding:'10px 18px', borderRadius:'8px',
|
|
12
|
+
fontSize:'14px', maxWidth:'420px', wordBreak:'break-all',
|
|
13
|
+
boxShadow:'0 4px 16px rgba(0,0,0,.3)', opacity:'1',
|
|
14
|
+
transition:'opacity .5s', pointerEvents:'none'
|
|
15
|
+
});
|
|
16
|
+
(document.body || document.documentElement).appendChild(d);
|
|
17
|
+
setTimeout(() => { d.style.opacity = '0'; }, 3000);
|
|
18
|
+
setTimeout(() => { d.remove(); }, 3600);
|
|
19
|
+
} catch(e) {}
|
|
20
|
+
}
|
|
21
|
+
window.alert = function(msg) { toast('alert', msg); };
|
|
22
|
+
window.confirm = function(msg) { toast('confirm', msg); return true; };
|
|
23
|
+
window.prompt = function(msg, def) { toast('prompt', msg); return def || null; };
|
|
24
|
+
})();
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"manifest_version": 3,
|
|
3
|
+
"name": "TMWD CDP Bridge",
|
|
4
|
+
"version": "2.0",
|
|
5
|
+
"description": "Cookie viewer + CDP bridge",
|
|
6
|
+
"permissions": [
|
|
7
|
+
"cookies",
|
|
8
|
+
"tabs",
|
|
9
|
+
"activeTab",
|
|
10
|
+
"debugger",
|
|
11
|
+
"scripting",
|
|
12
|
+
"alarms",
|
|
13
|
+
"declarativeNetRequest",
|
|
14
|
+
"management",
|
|
15
|
+
"contentSettings"
|
|
16
|
+
],
|
|
17
|
+
"host_permissions": ["<all_urls>"],
|
|
18
|
+
"background": {
|
|
19
|
+
"service_worker": "background.js"
|
|
20
|
+
},
|
|
21
|
+
"content_scripts": [
|
|
22
|
+
{
|
|
23
|
+
"matches": ["<all_urls>"],
|
|
24
|
+
"js": ["disable_dialogs.js"],
|
|
25
|
+
"run_at": "document_start",
|
|
26
|
+
"all_frames": true,
|
|
27
|
+
"world": "MAIN"
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"matches": ["<all_urls>"],
|
|
31
|
+
"js": ["config.js", "content.js"],
|
|
32
|
+
"run_at": "document_idle",
|
|
33
|
+
"all_frames": true
|
|
34
|
+
}
|
|
35
|
+
],
|
|
36
|
+
"action": {
|
|
37
|
+
"default_popup": "popup.html",
|
|
38
|
+
"default_title": "TMWD CDP Bridge"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<style>
|
|
6
|
+
body{width:420px;max-height:500px;margin:0;padding:8px;font:12px monospace;background:#1e1e1e;color:#d4d4d4;overflow-y:auto}
|
|
7
|
+
h3{margin:4px 0;color:#569cd6}
|
|
8
|
+
button{background:#264f78;color:#fff;border:none;padding:4px 12px;cursor:pointer;border-radius:3px;margin-bottom:6px}
|
|
9
|
+
button:hover{background:#37699e}
|
|
10
|
+
pre{white-space:pre-wrap;word-break:break-all;margin:0;padding:6px;background:#252526;border-radius:3px;max-height:420px;overflow-y:auto}
|
|
11
|
+
</style>
|
|
12
|
+
</head>
|
|
13
|
+
<body>
|
|
14
|
+
<h3>🍪 Cookies</h3>
|
|
15
|
+
<button id="refresh">刷新</button>
|
|
16
|
+
<pre id="out">点击刷新获取 cookies...</pre>
|
|
17
|
+
<script src="popup.js"></script>
|
|
18
|
+
</body>
|
|
19
|
+
</html>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
2
|
+
const out = document.getElementById('out');
|
|
3
|
+
const btn = document.getElementById('refresh');
|
|
4
|
+
btn.addEventListener('click', fetchCookies);
|
|
5
|
+
fetchCookies();
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
async function fetchCookies() {
|
|
9
|
+
const out = document.getElementById('out');
|
|
10
|
+
try {
|
|
11
|
+
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
|
12
|
+
if (!tab?.url) { out.textContent = 'No active tab'; return; }
|
|
13
|
+
const resp = await chrome.runtime.sendMessage({ cmd: 'cookies', url: tab.url });
|
|
14
|
+
if (!resp?.ok) { out.textContent = 'Error: ' + (resp?.error || 'unknown'); return; }
|
|
15
|
+
if (!resp.data.length) { out.textContent = '(no cookies)'; return; }
|
|
16
|
+
// 展示带标记
|
|
17
|
+
out.textContent = resp.data.map(c =>
|
|
18
|
+
`${c.name}=${c.value}` + (c.httpOnly ? ' [H]' : '') + (c.secure ? ' [S]' : '') + (c.partitionKey ? ' [P]' : '')
|
|
19
|
+
).join('\n');
|
|
20
|
+
// 自动复制 name=value; 格式到剪贴板
|
|
21
|
+
const str = resp.data.map(c => `${c.name}=${c.value}`).join('; ');
|
|
22
|
+
await navigator.clipboard.writeText(str);
|
|
23
|
+
} catch (e) { out.textContent = 'Error: ' + e.message; }
|
|
24
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { spawnSync } = require("node:child_process");
|
|
4
|
+
const fs = require("node:fs");
|
|
5
|
+
const path = require("node:path");
|
|
6
|
+
|
|
7
|
+
function platformPackageName() {
|
|
8
|
+
const platform = process.platform;
|
|
9
|
+
const arch = process.arch;
|
|
10
|
+
if (platform === "darwin" && arch === "arm64") return "@sleepinsummer/agent-browser-cli-darwin-arm64";
|
|
11
|
+
if (platform === "darwin" && arch === "x64") return "@sleepinsummer/agent-browser-cli-darwin-x64";
|
|
12
|
+
if (platform === "win32" && arch === "x64") return "@sleepinsummer/agent-browser-cli-win32-x64";
|
|
13
|
+
throw new Error(`Unsupported platform: ${platform}-${arch}`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function resolveBinary() {
|
|
17
|
+
if (process.env.AGENT_BROWSER_CLI_BIN) return process.env.AGENT_BROWSER_CLI_BIN;
|
|
18
|
+
try {
|
|
19
|
+
const pkg = platformPackageName();
|
|
20
|
+
return require.resolve(`${pkg}/bin/agent-browser-cli${process.platform === "win32" ? ".exe" : ""}`);
|
|
21
|
+
} catch (_) {
|
|
22
|
+
const ext = process.platform === "win32" ? ".exe" : "";
|
|
23
|
+
const local = path.resolve(__dirname, "..", "..", "target", "release", `agent-browser-cli${ext}`);
|
|
24
|
+
if (fs.existsSync(local)) return local;
|
|
25
|
+
throw new Error("agent-browser-cli native binary not found. Run `npm run build` for local development.");
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const bin = resolveBinary();
|
|
30
|
+
const result = spawnSync(bin, process.argv.slice(2), { stdio: "inherit" });
|
|
31
|
+
if (result.error) {
|
|
32
|
+
console.error(result.error.message);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
process.exit(result.status ?? 0);
|
|
Binary file
|