@sleepinsummer/agent-browser-cli 0.2.9 → 0.3.1-beta.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/AI_INSTALL.md +40 -13
- package/CHANGELOG.md +163 -0
- package/README.md +5 -4
- package/README_EN.md +5 -4
- package/assets/tmwd_cdp_bridge/background.js +418 -8
- package/assets/tmwd_cdp_bridge/content.js +7 -3
- package/assets/tmwd_cdp_bridge/icons/bot-128.png +0 -0
- package/assets/tmwd_cdp_bridge/icons/bot-16.png +0 -0
- package/assets/tmwd_cdp_bridge/icons/bot-32.png +0 -0
- package/assets/tmwd_cdp_bridge/icons/bot-48.png +0 -0
- package/assets/tmwd_cdp_bridge/manifest.json +32 -8
- package/npm/bin/agent-browser-cli.js +2 -1
- package/package.json +7 -6
- package/skills/agent-browser-cli/SKILL.md +119 -199
- package/skills/agent-browser-cli/references/operations.md +145 -0
|
@@ -19,6 +19,25 @@ let lastCommandAt = 0;
|
|
|
19
19
|
const DEFAULT_WS_PORT = 18765;
|
|
20
20
|
const CLI_API_PORT = 18767;
|
|
21
21
|
let wsPort = DEFAULT_WS_PORT;
|
|
22
|
+
const browserId = `browser-${crypto.randomUUID()}`;
|
|
23
|
+
let profileId = null;
|
|
24
|
+
let profileLabel = null;
|
|
25
|
+
|
|
26
|
+
async function loadClientIdentity() {
|
|
27
|
+
const data = await chrome.storage.local.get({ profileId: null, profileLabel: null });
|
|
28
|
+
profileId = data.profileId || `profile-${crypto.randomUUID()}`;
|
|
29
|
+
profileLabel = data.profileLabel || null;
|
|
30
|
+
if (!data.profileId) await chrome.storage.local.set({ profileId });
|
|
31
|
+
return { browserId, profileId, profileLabel };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function withClientIdentity(payload) {
|
|
35
|
+
return Object.assign({
|
|
36
|
+
browser_id: browserId,
|
|
37
|
+
profile_id: profileId || 'profile-pending',
|
|
38
|
+
profile_label: profileLabel || undefined
|
|
39
|
+
}, payload);
|
|
40
|
+
}
|
|
22
41
|
|
|
23
42
|
async function handleExtMessage(msg, sender) {
|
|
24
43
|
if (msg.cmd === 'status') return handleStatus();
|
|
@@ -28,11 +47,23 @@ async function handleExtMessage(msg, sender) {
|
|
|
28
47
|
if (msg.cmd === 'cdp') return await handleCDP(msg, sender);
|
|
29
48
|
if (msg.cmd === 'batch') return await handleBatch(msg, sender);
|
|
30
49
|
if (msg.cmd === 'openTab') return await handleOpenTab(msg);
|
|
50
|
+
if (msg.cmd === 'closeTab') return await handleCloseTab(msg, sender);
|
|
51
|
+
if (msg.cmd === 'networkStart') return await handleNetworkStart(msg, sender);
|
|
52
|
+
if (msg.cmd === 'networkList') return await handleNetworkList(msg, sender);
|
|
53
|
+
if (msg.cmd === 'networkDetail') return await handleNetworkDetail(msg, sender);
|
|
54
|
+
if (msg.cmd === 'networkClear') return await handleNetworkClear(msg, sender);
|
|
55
|
+
if (msg.cmd === 'networkStop') return await handleNetworkStop(msg, sender);
|
|
56
|
+
if (msg.cmd === 'consoleStart') return await handleConsoleStart(msg, sender);
|
|
57
|
+
if (msg.cmd === 'consoleList') return await handleConsoleList(msg, sender);
|
|
58
|
+
if (msg.cmd === 'consoleClear') return await handleConsoleClear(msg, sender);
|
|
59
|
+
if (msg.cmd === 'consoleStop') return await handleConsoleStop(msg, sender);
|
|
60
|
+
if (msg.cmd === 'debugClearAll') return await handleDebugClearAll();
|
|
31
61
|
if (msg.cmd === 'tabs') {
|
|
32
62
|
try {
|
|
33
63
|
if (msg.method === 'switch') {
|
|
34
64
|
const tab = await chrome.tabs.update(msg.tabId, { active: true });
|
|
35
|
-
|
|
65
|
+
// 默认只切换 Chrome 内部 active tab,不抢占系统前台窗口。
|
|
66
|
+
if (msg.allowFocus === true && tab.windowId) await chrome.windows.update(tab.windowId, { focused: true });
|
|
36
67
|
return { ok: true };
|
|
37
68
|
} else {
|
|
38
69
|
const tabs = (await chrome.tabs.query({})).filter(t => isScriptable(t.url));
|
|
@@ -84,7 +115,10 @@ function handleStatus() {
|
|
|
84
115
|
wsConnected: !!ws && ws.readyState === WebSocket.OPEN,
|
|
85
116
|
wsUrl: getWsUrl(),
|
|
86
117
|
wsPort,
|
|
87
|
-
lastCommandAt
|
|
118
|
+
lastCommandAt,
|
|
119
|
+
browserId,
|
|
120
|
+
profileId,
|
|
121
|
+
profileLabel
|
|
88
122
|
}
|
|
89
123
|
};
|
|
90
124
|
}
|
|
@@ -128,18 +162,385 @@ async function handleCookies(msg, sender) {
|
|
|
128
162
|
}
|
|
129
163
|
}
|
|
130
164
|
|
|
165
|
+
|
|
166
|
+
const debugSessions = new Map();
|
|
167
|
+
|
|
168
|
+
function resolveTabId(msg, sender) {
|
|
169
|
+
const tabId = Number(msg.tabId || sender.tab?.id);
|
|
170
|
+
if (!Number.isInteger(tabId) || tabId <= 0) throw new Error('no tabId');
|
|
171
|
+
return tabId;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function getDebugSession(tabId) {
|
|
175
|
+
let session = debugSessions.get(tabId);
|
|
176
|
+
if (!session) {
|
|
177
|
+
session = {
|
|
178
|
+
tabId,
|
|
179
|
+
attached: false,
|
|
180
|
+
network: false,
|
|
181
|
+
console: false,
|
|
182
|
+
requests: new Map(),
|
|
183
|
+
requestOrder: [],
|
|
184
|
+
logs: []
|
|
185
|
+
};
|
|
186
|
+
debugSessions.set(tabId, session);
|
|
187
|
+
}
|
|
188
|
+
return session;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function ensureDebugAttached(session) {
|
|
192
|
+
if (session.attached) return;
|
|
193
|
+
await chrome.debugger.attach({ tabId: session.tabId }, '1.3');
|
|
194
|
+
session.attached = true;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function detachDebugIfIdle(session) {
|
|
198
|
+
if (!session.attached || session.network || session.console) return;
|
|
199
|
+
try { await chrome.debugger.detach({ tabId: session.tabId }); } catch (_) {}
|
|
200
|
+
session.attached = false;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async function handleNetworkStart(msg, sender) {
|
|
204
|
+
try {
|
|
205
|
+
const tabId = resolveTabId(msg, sender);
|
|
206
|
+
const session = getDebugSession(tabId);
|
|
207
|
+
await ensureDebugAttached(session);
|
|
208
|
+
await chrome.debugger.sendCommand({ tabId }, 'Network.enable', {});
|
|
209
|
+
session.network = true;
|
|
210
|
+
return { ok: true, status: 'started', tabId, count: session.requestOrder.length };
|
|
211
|
+
} catch (e) {
|
|
212
|
+
return { ok: false, error: e.message };
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function handleNetworkList(msg, sender) {
|
|
217
|
+
try {
|
|
218
|
+
const tabId = resolveTabId(msg, sender);
|
|
219
|
+
const session = getDebugSession(tabId);
|
|
220
|
+
const filter = String(msg.filter || '').toLowerCase();
|
|
221
|
+
const limit = Math.max(1, Math.min(Number(msg.limit || 100), 1000));
|
|
222
|
+
let items = session.requestOrder.map(id => session.requests.get(id)).filter(Boolean);
|
|
223
|
+
if (filter) {
|
|
224
|
+
items = items.filter(item => [item.url, item.method, item.status, item.mimeType, item.resourceType].some(v => String(v || '').toLowerCase().includes(filter)));
|
|
225
|
+
}
|
|
226
|
+
items = items.slice(-limit).map(summarizeRequest);
|
|
227
|
+
return { ok: true, status: session.network ? 'started' : 'stopped', tabId, count: items.length, requests: items };
|
|
228
|
+
} catch (e) {
|
|
229
|
+
return { ok: false, error: e.message };
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async function handleNetworkDetail(msg, sender) {
|
|
234
|
+
try {
|
|
235
|
+
const tabId = resolveTabId(msg, sender);
|
|
236
|
+
const requestId = String(msg.requestId || '');
|
|
237
|
+
const session = getDebugSession(tabId);
|
|
238
|
+
const item = session.requests.get(requestId);
|
|
239
|
+
if (!item) return { ok: false, error: 'unknown requestId: ' + requestId };
|
|
240
|
+
const detail = Object.assign({}, item);
|
|
241
|
+
if (item.completed && !item.failed && session.attached) {
|
|
242
|
+
try {
|
|
243
|
+
const body = await chrome.debugger.sendCommand({ tabId }, 'Network.getResponseBody', { requestId });
|
|
244
|
+
detail.body = truncateText(body.body || '', 20000);
|
|
245
|
+
detail.base64Encoded = !!body.base64Encoded;
|
|
246
|
+
detail.bodyTruncated = String(body.body || '').length > 20000;
|
|
247
|
+
} catch (e) {
|
|
248
|
+
detail.bodyError = e.message;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return { ok: true, tabId, request: detail };
|
|
252
|
+
} catch (e) {
|
|
253
|
+
return { ok: false, error: e.message };
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async function handleNetworkClear(msg, sender) {
|
|
258
|
+
try {
|
|
259
|
+
const tabId = resolveTabId(msg, sender);
|
|
260
|
+
const session = getDebugSession(tabId);
|
|
261
|
+
session.requests.clear();
|
|
262
|
+
session.requestOrder = [];
|
|
263
|
+
return { ok: true, status: 'cleared', tabId };
|
|
264
|
+
} catch (e) {
|
|
265
|
+
return { ok: false, error: e.message };
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async function handleNetworkStop(msg, sender) {
|
|
270
|
+
try {
|
|
271
|
+
const tabId = resolveTabId(msg, sender);
|
|
272
|
+
const session = getDebugSession(tabId);
|
|
273
|
+
if (session.attached) {
|
|
274
|
+
try { await chrome.debugger.sendCommand({ tabId }, 'Network.disable', {}); } catch (_) {}
|
|
275
|
+
}
|
|
276
|
+
session.network = false;
|
|
277
|
+
session.requests.clear();
|
|
278
|
+
session.requestOrder = [];
|
|
279
|
+
await detachDebugIfIdle(session);
|
|
280
|
+
return { ok: true, status: 'stopped', cleared: true, tabId };
|
|
281
|
+
} catch (e) {
|
|
282
|
+
return { ok: false, error: e.message };
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async function handleConsoleStart(msg, sender) {
|
|
287
|
+
try {
|
|
288
|
+
const tabId = resolveTabId(msg, sender);
|
|
289
|
+
const session = getDebugSession(tabId);
|
|
290
|
+
await ensureDebugAttached(session);
|
|
291
|
+
await chrome.debugger.sendCommand({ tabId }, 'Runtime.enable', {});
|
|
292
|
+
await chrome.debugger.sendCommand({ tabId }, 'Log.enable', {}).catch(() => null);
|
|
293
|
+
session.console = true;
|
|
294
|
+
return { ok: true, status: 'started', tabId, count: session.logs.length };
|
|
295
|
+
} catch (e) {
|
|
296
|
+
return { ok: false, error: e.message };
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
async function handleConsoleList(msg, sender) {
|
|
301
|
+
try {
|
|
302
|
+
const tabId = resolveTabId(msg, sender);
|
|
303
|
+
const session = getDebugSession(tabId);
|
|
304
|
+
const level = String(msg.level || '').toLowerCase();
|
|
305
|
+
const limit = Math.max(1, Math.min(Number(msg.limit || 100), 1000));
|
|
306
|
+
let logs = session.logs;
|
|
307
|
+
if (level) logs = logs.filter(item => String(item.level || '').toLowerCase() === level);
|
|
308
|
+
return { ok: true, status: session.console ? 'started' : 'stopped', tabId, count: logs.length, logs: logs.slice(-limit) };
|
|
309
|
+
} catch (e) {
|
|
310
|
+
return { ok: false, error: e.message };
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
async function handleConsoleClear(msg, sender) {
|
|
315
|
+
try {
|
|
316
|
+
const tabId = resolveTabId(msg, sender);
|
|
317
|
+
const session = getDebugSession(tabId);
|
|
318
|
+
session.logs = [];
|
|
319
|
+
return { ok: true, status: 'cleared', tabId };
|
|
320
|
+
} catch (e) {
|
|
321
|
+
return { ok: false, error: e.message };
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
async function handleConsoleStop(msg, sender) {
|
|
326
|
+
try {
|
|
327
|
+
const tabId = resolveTabId(msg, sender);
|
|
328
|
+
const session = getDebugSession(tabId);
|
|
329
|
+
if (session.attached) {
|
|
330
|
+
try { await chrome.debugger.sendCommand({ tabId }, 'Runtime.disable', {}); } catch (_) {}
|
|
331
|
+
try { await chrome.debugger.sendCommand({ tabId }, 'Log.disable', {}); } catch (_) {}
|
|
332
|
+
}
|
|
333
|
+
session.console = false;
|
|
334
|
+
session.logs = [];
|
|
335
|
+
await detachDebugIfIdle(session);
|
|
336
|
+
return { ok: true, status: 'stopped', cleared: true, tabId };
|
|
337
|
+
} catch (e) {
|
|
338
|
+
return { ok: false, error: e.message };
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function summarizeRequest(item) {
|
|
343
|
+
return {
|
|
344
|
+
requestId: item.requestId,
|
|
345
|
+
url: item.url,
|
|
346
|
+
method: item.method,
|
|
347
|
+
status: item.status,
|
|
348
|
+
mimeType: item.mimeType,
|
|
349
|
+
resourceType: item.resourceType,
|
|
350
|
+
completed: !!item.completed,
|
|
351
|
+
failed: !!item.failed,
|
|
352
|
+
errorText: item.errorText,
|
|
353
|
+
timestamp: item.timestamp
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function rememberRequest(session, requestId) {
|
|
358
|
+
if (!session.requests.has(requestId)) session.requestOrder.push(requestId);
|
|
359
|
+
while (session.requestOrder.length > 1000) {
|
|
360
|
+
const old = session.requestOrder.shift();
|
|
361
|
+
session.requests.delete(old);
|
|
362
|
+
}
|
|
363
|
+
let item = session.requests.get(requestId);
|
|
364
|
+
if (!item) {
|
|
365
|
+
item = { requestId };
|
|
366
|
+
session.requests.set(requestId, item);
|
|
367
|
+
}
|
|
368
|
+
return item;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function pushLog(session, item) {
|
|
372
|
+
session.logs.push(item);
|
|
373
|
+
if (session.logs.length > 1000) session.logs.splice(0, session.logs.length - 1000);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function remoteObjectText(arg) {
|
|
377
|
+
if (!arg) return '';
|
|
378
|
+
if ('value' in arg) {
|
|
379
|
+
if (typeof arg.value === 'string') return truncateText(arg.value, 2000);
|
|
380
|
+
try { return truncateText(JSON.stringify(arg.value), 2000); } catch (_) { return String(arg.value); }
|
|
381
|
+
}
|
|
382
|
+
return truncateText(arg.description || arg.type || '', 2000);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function truncateText(text, max) {
|
|
386
|
+
const value = String(text || '');
|
|
387
|
+
return value.length > max ? value.slice(0, max) : value;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function onDebuggerEvent(source, method, params) {
|
|
391
|
+
const tabId = source.tabId;
|
|
392
|
+
if (!tabId) return;
|
|
393
|
+
const session = debugSessions.get(tabId);
|
|
394
|
+
if (!session) return;
|
|
395
|
+
if (method === 'Network.requestWillBeSent') {
|
|
396
|
+
const item = rememberRequest(session, params.requestId);
|
|
397
|
+
item.url = params.request?.url || item.url;
|
|
398
|
+
item.method = params.request?.method || item.method;
|
|
399
|
+
item.resourceType = params.type || item.resourceType;
|
|
400
|
+
item.timestamp = params.wallTime || params.timestamp || item.timestamp;
|
|
401
|
+
item.requestHeaders = params.request?.headers;
|
|
402
|
+
} else if (method === 'Network.responseReceived') {
|
|
403
|
+
const item = rememberRequest(session, params.requestId);
|
|
404
|
+
item.status = params.response?.status;
|
|
405
|
+
item.statusText = params.response?.statusText;
|
|
406
|
+
item.mimeType = params.response?.mimeType;
|
|
407
|
+
item.responseHeaders = params.response?.headers;
|
|
408
|
+
item.url = params.response?.url || item.url;
|
|
409
|
+
item.resourceType = params.type || item.resourceType;
|
|
410
|
+
} else if (method === 'Network.loadingFinished') {
|
|
411
|
+
const item = rememberRequest(session, params.requestId);
|
|
412
|
+
item.completed = true;
|
|
413
|
+
item.encodedDataLength = params.encodedDataLength;
|
|
414
|
+
} else if (method === 'Network.loadingFailed') {
|
|
415
|
+
const item = rememberRequest(session, params.requestId);
|
|
416
|
+
item.completed = true;
|
|
417
|
+
item.failed = true;
|
|
418
|
+
item.errorText = params.errorText;
|
|
419
|
+
} else if (method === 'Runtime.consoleAPICalled') {
|
|
420
|
+
pushLog(session, {
|
|
421
|
+
level: params.type || 'log',
|
|
422
|
+
text: (params.args || []).map(remoteObjectText).join(' '),
|
|
423
|
+
timestamp: params.timestamp || Date.now(),
|
|
424
|
+
url: params.stackTrace?.callFrames?.[0]?.url || '',
|
|
425
|
+
line: params.stackTrace?.callFrames?.[0]?.lineNumber,
|
|
426
|
+
column: params.stackTrace?.callFrames?.[0]?.columnNumber
|
|
427
|
+
});
|
|
428
|
+
} else if (method === 'Runtime.exceptionThrown') {
|
|
429
|
+
pushLog(session, {
|
|
430
|
+
level: 'error',
|
|
431
|
+
text: params.exceptionDetails?.text || params.exceptionDetails?.exception?.description || 'exception thrown',
|
|
432
|
+
timestamp: params.timestamp || Date.now(),
|
|
433
|
+
url: params.exceptionDetails?.url || '',
|
|
434
|
+
line: params.exceptionDetails?.lineNumber,
|
|
435
|
+
column: params.exceptionDetails?.columnNumber
|
|
436
|
+
});
|
|
437
|
+
} else if (method === 'Log.entryAdded') {
|
|
438
|
+
const e = params.entry || {};
|
|
439
|
+
pushLog(session, {
|
|
440
|
+
level: e.level || 'log',
|
|
441
|
+
text: e.text || '',
|
|
442
|
+
timestamp: e.timestamp || Date.now(),
|
|
443
|
+
url: e.url || '',
|
|
444
|
+
line: e.lineNumber,
|
|
445
|
+
column: undefined
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function onDebuggerDetach(source) {
|
|
451
|
+
if (!source.tabId) return;
|
|
452
|
+
const session = debugSessions.get(source.tabId);
|
|
453
|
+
if (session) {
|
|
454
|
+
session.attached = false;
|
|
455
|
+
session.network = false;
|
|
456
|
+
session.console = false;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
chrome.debugger.onEvent.addListener(onDebuggerEvent);
|
|
461
|
+
chrome.debugger.onDetach.addListener(onDebuggerDetach);
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
async function handleCloseTab(msg, sender) {
|
|
466
|
+
try {
|
|
467
|
+
const tabId = Number(msg.tabId || sender.tab?.id);
|
|
468
|
+
if (!Number.isInteger(tabId) || tabId <= 0) throw new Error('tabId is required');
|
|
469
|
+
const session = debugSessions.get(tabId);
|
|
470
|
+
if (session?.attached) {
|
|
471
|
+
try { await chrome.debugger.detach({ tabId }); } catch (_) {}
|
|
472
|
+
}
|
|
473
|
+
debugSessions.delete(tabId);
|
|
474
|
+
await chrome.tabs.remove(tabId);
|
|
475
|
+
return { ok: true, data: { status: 'success', tabId } };
|
|
476
|
+
} catch (e) {
|
|
477
|
+
return { ok: false, error: e.message };
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
async function handleDebugClearAll() {
|
|
482
|
+
const tabIds = [...debugSessions.keys()];
|
|
483
|
+
for (const tabId of tabIds) {
|
|
484
|
+
const session = debugSessions.get(tabId);
|
|
485
|
+
if (!session) continue;
|
|
486
|
+
session.requests.clear();
|
|
487
|
+
session.requestOrder = [];
|
|
488
|
+
session.logs = [];
|
|
489
|
+
session.network = false;
|
|
490
|
+
session.console = false;
|
|
491
|
+
if (session.attached) {
|
|
492
|
+
try { await chrome.debugger.detach({ tabId }); } catch (_) {}
|
|
493
|
+
session.attached = false;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
debugSessions.clear();
|
|
497
|
+
return { ok: true, status: 'cleared', tabs: tabIds.length };
|
|
498
|
+
}
|
|
499
|
+
|
|
131
500
|
async function handleOpenTab(msg) {
|
|
132
501
|
try {
|
|
133
502
|
const url = normalizeOpenUrl(msg.url);
|
|
134
503
|
const active = msg.active !== false;
|
|
504
|
+
if (msg.window === true) {
|
|
505
|
+
// window 模式默认不聚焦,避免新窗口打断用户当前工作区。
|
|
506
|
+
const win = await chrome.windows.create({ url, focused: msg.allowFocus === true });
|
|
507
|
+
const tab = Array.isArray(win.tabs) && win.tabs.length ? win.tabs[0] : null;
|
|
508
|
+
const group = tab?.id ? await groupTabIfRequested(tab.id, msg.groupTitle) : { ok: false, skipped: true, reason: 'window created without tab info' };
|
|
509
|
+
return { ok: true, data: { id: tab?.id, url: tab?.url || url, title: tab?.title || '', active: !!tab?.active, windowId: win.id, window: true, group } };
|
|
510
|
+
}
|
|
135
511
|
const tab = await chrome.tabs.create({ url, active });
|
|
136
|
-
|
|
137
|
-
|
|
512
|
+
const group = await groupTabIfRequested(tab.id, msg.groupTitle);
|
|
513
|
+
// 默认创建/激活标签页但不聚焦浏览器窗口,避免打断当前工作区。
|
|
514
|
+
if (active && msg.allowFocus === true && tab.windowId) await chrome.windows.update(tab.windowId, { focused: true });
|
|
515
|
+
return { ok: true, data: { id: tab.id, url: tab.url || url, title: tab.title || '', active: tab.active, windowId: tab.windowId, window: false, group } };
|
|
138
516
|
} catch (e) {
|
|
139
517
|
return { ok: false, error: e.message };
|
|
140
518
|
}
|
|
141
519
|
}
|
|
142
520
|
|
|
521
|
+
async function groupTabIfRequested(tabId, title) {
|
|
522
|
+
const cleanTitle = String(title || '').trim();
|
|
523
|
+
if (!cleanTitle) return null;
|
|
524
|
+
if (!chrome.tabs?.group || !chrome.tabGroups?.update) {
|
|
525
|
+
return { ok: false, skipped: true, reason: 'tabGroups API unavailable' };
|
|
526
|
+
}
|
|
527
|
+
try {
|
|
528
|
+
const existing = await findTabGroupByTitle(cleanTitle);
|
|
529
|
+
const groupId = await chrome.tabs.group(existing ? { tabIds: tabId, groupId: existing.id } : { tabIds: tabId });
|
|
530
|
+
await chrome.tabGroups.update(groupId, { title: cleanTitle });
|
|
531
|
+
return { ok: true, id: groupId, title: cleanTitle };
|
|
532
|
+
} catch (e) {
|
|
533
|
+
// 分组只是整理标签,不影响打开页面的主流程。
|
|
534
|
+
return { ok: false, skipped: true, reason: e.message };
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
async function findTabGroupByTitle(title) {
|
|
539
|
+
if (!chrome.tabGroups?.query) return null;
|
|
540
|
+
const groups = await chrome.tabGroups.query({});
|
|
541
|
+
return groups.find(group => group.title === title) || null;
|
|
542
|
+
}
|
|
543
|
+
|
|
143
544
|
function normalizeOpenUrl(url) {
|
|
144
545
|
const raw = String(url || '').trim();
|
|
145
546
|
if (!raw) throw new Error('url is required');
|
|
@@ -162,6 +563,10 @@ async function handleBatch(msg, sender) {
|
|
|
162
563
|
R.push({ ok: true, data: tabs.map(t => ({ id: t.id, url: t.url, title: t.title, active: t.active, windowId: t.windowId })) });
|
|
163
564
|
} else if (c.cmd === 'cdp') {
|
|
164
565
|
const tabId = c.tabId || msg.tabId || sender.tab?.id;
|
|
566
|
+
if (c.method === 'Page.bringToFront' && c.allowFocus !== true && msg.allowFocus !== true) {
|
|
567
|
+
R.push({ skipped: true, reason: 'Page.bringToFront requires allowFocus=true' });
|
|
568
|
+
continue;
|
|
569
|
+
}
|
|
165
570
|
if (attached !== tabId) {
|
|
166
571
|
if (attached) { await chrome.debugger.detach({ tabId: attached }); attached = null; }
|
|
167
572
|
await chrome.debugger.attach({ tabId }, '1.3');
|
|
@@ -183,6 +588,9 @@ async function handleBatch(msg, sender) {
|
|
|
183
588
|
async function handleCDP(msg, sender) {
|
|
184
589
|
const tabId = msg.tabId || sender.tab?.id;
|
|
185
590
|
if (!tabId) return { ok: false, error: 'no tabId' };
|
|
591
|
+
if (msg.method === 'Page.bringToFront' && msg.allowFocus !== true) {
|
|
592
|
+
return { ok: true, data: { skipped: true, reason: 'Page.bringToFront requires allowFocus=true' } };
|
|
593
|
+
}
|
|
186
594
|
try {
|
|
187
595
|
await chrome.debugger.attach({ tabId }, '1.3');
|
|
188
596
|
const result = await chrome.debugger.sendCommand({ tabId }, msg.method, msg.params || {});
|
|
@@ -439,11 +847,12 @@ async function connectWS() {
|
|
|
439
847
|
ws.onopen = async () => {
|
|
440
848
|
console.log('[TMWD-WS] Connected!');
|
|
441
849
|
scheduleKeepalive(); // Keep SW alive while connected
|
|
850
|
+
await loadClientIdentity();
|
|
442
851
|
const tabs = (await chrome.tabs.query({})).filter(t => isScriptable(t.url));
|
|
443
|
-
ws.send(JSON.stringify({
|
|
852
|
+
ws.send(JSON.stringify(withClientIdentity({
|
|
444
853
|
type: 'ext_ready',
|
|
445
854
|
tabs: tabs.map(t => ({ id: t.id, url: t.url, title: t.title }))
|
|
446
|
-
}));
|
|
855
|
+
})));
|
|
447
856
|
console.log('[TMWD-WS] Sent ext_ready with', tabs.length, 'tabs');
|
|
448
857
|
};
|
|
449
858
|
ws.onmessage = async (event) => {
|
|
@@ -512,10 +921,11 @@ chrome.runtime.onInstalled.addListener(() => {
|
|
|
512
921
|
async function sendTabsUpdate() {
|
|
513
922
|
if (!ws || ws.readyState !== WebSocket.OPEN) return;
|
|
514
923
|
const tabs = (await chrome.tabs.query({})).filter(t => isScriptable(t.url) && !/streamlit/i.test(t.title));
|
|
515
|
-
|
|
924
|
+
await loadClientIdentity();
|
|
925
|
+
ws.send(JSON.stringify(withClientIdentity({
|
|
516
926
|
type: 'tabs_update',
|
|
517
927
|
tabs: tabs.map(t => ({ id: t.id, url: t.url, title: t.title }))
|
|
518
|
-
}));
|
|
928
|
+
})));
|
|
519
929
|
}
|
|
520
930
|
chrome.tabs.onUpdated.addListener((_, changeInfo) => {
|
|
521
931
|
if (changeInfo.status === 'complete') sendTabsUpdate();
|
|
@@ -142,13 +142,17 @@ async function handle(el) {
|
|
|
142
142
|
if (cmd === 'cookies') {
|
|
143
143
|
resp = await chrome.runtime.sendMessage({ cmd: 'cookies', url: req.url || location.href });
|
|
144
144
|
} else if (cmd === 'cdp') {
|
|
145
|
-
resp = await chrome.runtime.sendMessage({ cmd: 'cdp', method: req.method, params: req.params || {}, tabId: req.tabId });
|
|
145
|
+
resp = await chrome.runtime.sendMessage({ cmd: 'cdp', method: req.method, params: req.params || {}, tabId: req.tabId, allowFocus: req.allowFocus });
|
|
146
146
|
} else if (cmd === 'batch') {
|
|
147
147
|
resp = await chrome.runtime.sendMessage({ cmd: 'batch', commands: req.commands, tabId: req.tabId });
|
|
148
148
|
} else if (cmd === 'tabs') {
|
|
149
|
-
resp = await chrome.runtime.sendMessage({ cmd: 'tabs', method: req.method, tabId: req.tabId });
|
|
149
|
+
resp = await chrome.runtime.sendMessage({ cmd: 'tabs', method: req.method, tabId: req.tabId, allowFocus: req.allowFocus });
|
|
150
150
|
} else if (cmd === 'openTab') {
|
|
151
|
-
resp = await chrome.runtime.sendMessage({ cmd: 'openTab', url: req.url, active: req.active });
|
|
151
|
+
resp = await chrome.runtime.sendMessage({ cmd: 'openTab', url: req.url, active: req.active, allowFocus: req.allowFocus, groupTitle: req.groupTitle });
|
|
152
|
+
} else if (cmd === 'closeTab') {
|
|
153
|
+
resp = await chrome.runtime.sendMessage({ cmd: 'closeTab', tabId: req.tabId });
|
|
154
|
+
} else if (cmd === 'networkStart' || cmd === 'networkList' || cmd === 'networkDetail' || cmd === 'networkClear' || cmd === 'networkStop' || cmd === 'consoleStart' || cmd === 'consoleList' || cmd === 'consoleClear' || cmd === 'consoleStop' || cmd === 'debugClearAll') {
|
|
155
|
+
resp = await chrome.runtime.sendMessage(Object.assign({}, req, { cmd }));
|
|
152
156
|
} else {
|
|
153
157
|
resp = { ok: false, error: 'unknown cmd: ' + cmd };
|
|
154
158
|
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"manifest_version": 3,
|
|
3
|
-
"name": "
|
|
3
|
+
"name": "Agent Browser CLI Bridge",
|
|
4
4
|
"version": "2.0",
|
|
5
|
-
"description": "
|
|
5
|
+
"description": "Browser control bridge for agent-browser-cli",
|
|
6
6
|
"permissions": [
|
|
7
7
|
"cookies",
|
|
8
8
|
"tabs",
|
|
9
|
+
"tabGroups",
|
|
9
10
|
"activeTab",
|
|
10
11
|
"debugger",
|
|
11
12
|
"scripting",
|
|
@@ -15,27 +16,50 @@
|
|
|
15
16
|
"management",
|
|
16
17
|
"contentSettings"
|
|
17
18
|
],
|
|
18
|
-
"host_permissions": [
|
|
19
|
+
"host_permissions": [
|
|
20
|
+
"<all_urls>"
|
|
21
|
+
],
|
|
19
22
|
"background": {
|
|
20
23
|
"service_worker": "background.js"
|
|
21
24
|
},
|
|
22
25
|
"content_scripts": [
|
|
23
26
|
{
|
|
24
|
-
"matches": [
|
|
25
|
-
|
|
27
|
+
"matches": [
|
|
28
|
+
"<all_urls>"
|
|
29
|
+
],
|
|
30
|
+
"js": [
|
|
31
|
+
"disable_dialogs.js"
|
|
32
|
+
],
|
|
26
33
|
"run_at": "document_start",
|
|
27
34
|
"all_frames": true,
|
|
28
35
|
"world": "MAIN"
|
|
29
36
|
},
|
|
30
37
|
{
|
|
31
|
-
"matches": [
|
|
32
|
-
|
|
38
|
+
"matches": [
|
|
39
|
+
"<all_urls>"
|
|
40
|
+
],
|
|
41
|
+
"js": [
|
|
42
|
+
"config.js",
|
|
43
|
+
"content.js"
|
|
44
|
+
],
|
|
33
45
|
"run_at": "document_idle",
|
|
34
46
|
"all_frames": true
|
|
35
47
|
}
|
|
36
48
|
],
|
|
37
49
|
"action": {
|
|
38
50
|
"default_popup": "popup.html",
|
|
39
|
-
"default_title": "
|
|
51
|
+
"default_title": "Agent Browser CLI Bridge",
|
|
52
|
+
"default_icon": {
|
|
53
|
+
"16": "icons/bot-16.png",
|
|
54
|
+
"32": "icons/bot-32.png",
|
|
55
|
+
"48": "icons/bot-48.png",
|
|
56
|
+
"128": "icons/bot-128.png"
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
"icons": {
|
|
60
|
+
"16": "icons/bot-16.png",
|
|
61
|
+
"32": "icons/bot-32.png",
|
|
62
|
+
"48": "icons/bot-48.png",
|
|
63
|
+
"128": "icons/bot-128.png"
|
|
40
64
|
}
|
|
41
65
|
}
|
|
@@ -29,7 +29,8 @@ function resolveBinary() {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
const bin = resolveBinary();
|
|
32
|
-
const
|
|
32
|
+
const env = { ...process.env, AGENT_BROWSER_CLI_PACKAGE_DIR: path.resolve(__dirname, "..") };
|
|
33
|
+
const result = spawnSync(bin, process.argv.slice(2), { stdio: "inherit", env });
|
|
33
34
|
if (result.error) {
|
|
34
35
|
console.error(result.error.message);
|
|
35
36
|
process.exit(1);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sleepinsummer/agent-browser-cli",
|
|
3
|
-
"version": "0.2
|
|
3
|
+
"version": "0.3.1-beta.2",
|
|
4
4
|
"description": "Agent-oriented browser sensing and control CLI backed by a native Rust daemon.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bin": {
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"README.md",
|
|
15
15
|
"README_EN.md",
|
|
16
16
|
"AI_INSTALL.md",
|
|
17
|
+
"CHANGELOG.md",
|
|
17
18
|
"LICENSE"
|
|
18
19
|
],
|
|
19
20
|
"scripts": {
|
|
@@ -23,11 +24,11 @@
|
|
|
23
24
|
"postinstall": "node npm/postinstall.js"
|
|
24
25
|
},
|
|
25
26
|
"optionalDependencies": {
|
|
26
|
-
"@sleepinsummer/agent-browser-cli-darwin-arm64": "0.2
|
|
27
|
-
"@sleepinsummer/agent-browser-cli-darwin-x64": "0.2
|
|
28
|
-
"@sleepinsummer/agent-browser-cli-linux-x64": "0.2
|
|
29
|
-
"@sleepinsummer/agent-browser-cli-linux-arm64": "0.2
|
|
30
|
-
"@sleepinsummer/agent-browser-cli-win32-x64": "0.2
|
|
27
|
+
"@sleepinsummer/agent-browser-cli-darwin-arm64": "0.3.1-beta.2",
|
|
28
|
+
"@sleepinsummer/agent-browser-cli-darwin-x64": "0.3.1-beta.2",
|
|
29
|
+
"@sleepinsummer/agent-browser-cli-linux-x64": "0.3.1-beta.2",
|
|
30
|
+
"@sleepinsummer/agent-browser-cli-linux-arm64": "0.3.1-beta.2",
|
|
31
|
+
"@sleepinsummer/agent-browser-cli-win32-x64": "0.3.1-beta.2"
|
|
31
32
|
},
|
|
32
33
|
"engines": {
|
|
33
34
|
"node": ">=18"
|