@pyrokine/mcp-chrome 1.1.0 → 1.4.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/README.md +130 -54
- package/dist/anti-detection/behavior.d.ts +0 -8
- package/dist/anti-detection/behavior.d.ts.map +1 -1
- package/dist/anti-detection/behavior.js +0 -16
- package/dist/anti-detection/behavior.js.map +1 -1
- package/dist/cdp/client.d.ts +0 -2
- package/dist/cdp/client.d.ts.map +1 -1
- package/dist/cdp/client.js +30 -45
- package/dist/cdp/client.js.map +1 -1
- package/dist/cdp/launcher.d.ts +1 -8
- package/dist/cdp/launcher.d.ts.map +1 -1
- package/dist/cdp/launcher.js +11 -21
- package/dist/cdp/launcher.js.map +1 -1
- package/dist/core/auto-wait.d.ts +2 -2
- package/dist/core/auto-wait.d.ts.map +1 -1
- package/dist/core/auto-wait.js +1 -1
- package/dist/core/auto-wait.js.map +1 -1
- package/dist/core/errors.d.ts +10 -13
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js +19 -25
- package/dist/core/errors.js.map +1 -1
- package/dist/core/locator.d.ts +6 -7
- package/dist/core/locator.d.ts.map +1 -1
- package/dist/core/locator.js +77 -31
- package/dist/core/locator.js.map +1 -1
- package/dist/core/retry.d.ts.map +1 -1
- package/dist/core/retry.js +1 -1
- package/dist/core/retry.js.map +1 -1
- package/dist/core/session.d.ts +37 -33
- package/dist/core/session.d.ts.map +1 -1
- package/dist/core/session.js +159 -116
- package/dist/core/session.js.map +1 -1
- package/dist/core/types.d.ts +25 -1
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js +20 -0
- package/dist/core/types.js.map +1 -1
- package/dist/core/unified-session.d.ts +105 -67
- package/dist/core/unified-session.d.ts.map +1 -1
- package/dist/core/unified-session.js +347 -186
- package/dist/core/unified-session.js.map +1 -1
- package/dist/extension/bridge.d.ts +25 -19
- package/dist/extension/bridge.d.ts.map +1 -1
- package/dist/extension/bridge.js +29 -52
- package/dist/extension/bridge.js.map +1 -1
- package/dist/extension/http-server.d.ts +13 -11
- package/dist/extension/http-server.d.ts.map +1 -1
- package/dist/extension/http-server.js +101 -95
- package/dist/extension/http-server.js.map +1 -1
- package/dist/index.js +18 -64
- package/dist/index.js.map +1 -1
- package/dist/tools/browse.d.ts +3 -80
- package/dist/tools/browse.d.ts.map +1 -1
- package/dist/tools/browse.js +135 -291
- package/dist/tools/browse.js.map +1 -1
- package/dist/tools/cookies.d.ts +3 -71
- package/dist/tools/cookies.d.ts.map +1 -1
- package/dist/tools/cookies.js +75 -157
- package/dist/tools/cookies.js.map +1 -1
- package/dist/tools/evaluate.d.ts +3 -52
- package/dist/tools/evaluate.d.ts.map +1 -1
- package/dist/tools/evaluate.js +35 -86
- package/dist/tools/evaluate.js.map +1 -1
- package/dist/tools/extract.d.ts +5 -227
- package/dist/tools/extract.d.ts.map +1 -1
- package/dist/tools/extract.js +586 -184
- package/dist/tools/extract.js.map +1 -1
- package/dist/tools/index.d.ts +9 -9
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +9 -9
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/input.d.ts +3 -258
- package/dist/tools/input.d.ts.map +1 -1
- package/dist/tools/input.js +95 -147
- package/dist/tools/input.js.map +1 -1
- package/dist/tools/logs.d.ts +3 -51
- package/dist/tools/logs.d.ts.map +1 -1
- package/dist/tools/logs.js +47 -108
- package/dist/tools/logs.js.map +1 -1
- package/dist/tools/manage.d.ts +3 -64
- package/dist/tools/manage.d.ts.map +1 -1
- package/dist/tools/manage.js +243 -373
- package/dist/tools/manage.js.map +1 -1
- package/dist/tools/schema.d.ts +16 -182
- package/dist/tools/schema.d.ts.map +1 -1
- package/dist/tools/schema.js +70 -159
- package/dist/tools/schema.js.map +1 -1
- package/dist/tools/wait.d.ts +3 -221
- package/dist/tools/wait.d.ts.map +1 -1
- package/dist/tools/wait.js +74 -145
- package/dist/tools/wait.js.map +1 -1
- package/package.json +1 -1
|
@@ -7,16 +7,21 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { ExtensionBridge } from '../extension/index.js';
|
|
9
9
|
import { getSession as getCdpSession } from './session.js';
|
|
10
|
+
import { extractCdpValue, formatCdpException, MODIFIER_KEYS } from './types.js';
|
|
10
11
|
class UnifiedSessionManager {
|
|
11
12
|
static instance;
|
|
12
13
|
static CONNECTION_COOLDOWN = 30000; // 连接失败后 30 秒内不重试
|
|
13
14
|
extensionBridge = null;
|
|
14
15
|
inputMode = 'precise'; // 默认使用 precise 模式,可绕过 CSP 限制
|
|
15
16
|
currentMousePosition = { x: 0, y: 0 }; // 跟踪鼠标位置
|
|
17
|
+
/** 当前按下的修饰键位掩码 */
|
|
18
|
+
modifiers = 0;
|
|
16
19
|
lastConnectionFailure = 0;
|
|
17
20
|
tabSwitchLock = Promise.resolve(); // 串行化 tab 切换,防止并发竞态
|
|
18
21
|
requireExtension = false; // 指定 tabId 或 frame 时为 true,禁止 CDP 回退
|
|
19
|
-
|
|
22
|
+
currentFrameOffset = null; // iframe 在主页面的偏移量(withFrame 期间有效)
|
|
23
|
+
constructor() {
|
|
24
|
+
}
|
|
20
25
|
static getInstance() {
|
|
21
26
|
if (!UnifiedSessionManager.instance) {
|
|
22
27
|
UnifiedSessionManager.instance = new UnifiedSessionManager();
|
|
@@ -58,6 +63,12 @@ class UnifiedSessionManager {
|
|
|
58
63
|
getInputMode() {
|
|
59
64
|
return this.inputMode;
|
|
60
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* 获取当前 iframe 在主页面的偏移量(仅 withFrame 期间有效)
|
|
68
|
+
*/
|
|
69
|
+
getFrameOffset() {
|
|
70
|
+
return this.currentFrameOffset;
|
|
71
|
+
}
|
|
61
72
|
/**
|
|
62
73
|
* 获取当前鼠标位置
|
|
63
74
|
*/
|
|
@@ -73,30 +84,12 @@ class UnifiedSessionManager {
|
|
|
73
84
|
this.inputMode = mode;
|
|
74
85
|
console.error(`[MCP] Input mode set to: ${mode}`);
|
|
75
86
|
}
|
|
76
|
-
/**
|
|
77
|
-
* 是否已连接(任一模式)
|
|
78
|
-
*/
|
|
79
|
-
isConnected() {
|
|
80
|
-
return this.getMode() !== 'none';
|
|
81
|
-
}
|
|
82
87
|
/**
|
|
83
88
|
* 是否 Extension 已连接
|
|
84
89
|
*/
|
|
85
90
|
isExtensionConnected() {
|
|
86
91
|
return this.extensionBridge?.isConnected() ?? false;
|
|
87
92
|
}
|
|
88
|
-
/**
|
|
89
|
-
* 检查 CDP 回退是否允许
|
|
90
|
-
*
|
|
91
|
-
* 当 requireExtension 为 true(tabId 或 frame 已指定)时,CDP 回退会操作错误目标,必须阻止。
|
|
92
|
-
* 允许时返回 false(供 ensureExtensionConnected 直接返回),不允许时抛出。
|
|
93
|
-
*/
|
|
94
|
-
assertCdpFallbackAllowed() {
|
|
95
|
-
if (this.requireExtension) {
|
|
96
|
-
throw new Error('Extension 已断开,当前操作需要 Extension(指定 tabId 或 frame)时不可回退 CDP(操作目标不一致)');
|
|
97
|
-
}
|
|
98
|
-
return false;
|
|
99
|
-
}
|
|
100
93
|
/**
|
|
101
94
|
* 是否启用了 Extension 模式(不管当前是否连接)
|
|
102
95
|
* 用于判断应该使用哪种模式
|
|
@@ -104,62 +97,6 @@ class UnifiedSessionManager {
|
|
|
104
97
|
isExtensionModeEnabled() {
|
|
105
98
|
return this.extensionBridge !== null;
|
|
106
99
|
}
|
|
107
|
-
/**
|
|
108
|
-
* 等待 Extension 连接
|
|
109
|
-
* @param timeout 超时时间,0 表示无限等待
|
|
110
|
-
*/
|
|
111
|
-
async waitForExtensionConnection(timeout = 0) {
|
|
112
|
-
if (!this.extensionBridge) {
|
|
113
|
-
return false;
|
|
114
|
-
}
|
|
115
|
-
return this.extensionBridge.waitForConnection(timeout);
|
|
116
|
-
}
|
|
117
|
-
/**
|
|
118
|
-
* 确保 Extension 已连接,如果断开则等待重连
|
|
119
|
-
* 返回 true 表示 Extension 可用,false 表示应 fallback 到 CDP
|
|
120
|
-
*
|
|
121
|
-
* 设计理念:Server 和 Extension 的启动时机完全独立,无任何要求。
|
|
122
|
-
* - 先装 Extension,一个月/一年后启动 Server → 能连上
|
|
123
|
-
* - 先启动 Server,再打开 Chrome → 能连上
|
|
124
|
-
* - 关闭再打开任何一方 → 能自动重连
|
|
125
|
-
*
|
|
126
|
-
* 超时设为 30 秒:足够等待 Extension 启动,但不会永远卡住。
|
|
127
|
-
*
|
|
128
|
-
* @param maxWait 调用方的端到端预算(毫秒)。传入时取 min(maxWait, 30000) 作为连接等待上限,
|
|
129
|
-
* 避免工具 timeout 被连接等待吞掉。不传则使用默认 30s。
|
|
130
|
-
*/
|
|
131
|
-
async ensureExtensionConnected(maxWait) {
|
|
132
|
-
if (!this.extensionBridge) {
|
|
133
|
-
return this.assertCdpFallbackAllowed();
|
|
134
|
-
}
|
|
135
|
-
if (this.extensionBridge.isConnected()) {
|
|
136
|
-
return true;
|
|
137
|
-
}
|
|
138
|
-
// CDP 已连接时跳过 Extension 等待,直接使用 CDP 回退
|
|
139
|
-
if (getCdpSession().isConnected()) {
|
|
140
|
-
return this.assertCdpFallbackAllowed();
|
|
141
|
-
}
|
|
142
|
-
// 冷却期内不重复等待,避免每次操作都阻塞 30 秒
|
|
143
|
-
if (Date.now() - this.lastConnectionFailure < UnifiedSessionManager.CONNECTION_COOLDOWN) {
|
|
144
|
-
return this.assertCdpFallbackAllowed();
|
|
145
|
-
}
|
|
146
|
-
// Extension 服务器已启动但断开连接,等待重连
|
|
147
|
-
const waitTimeout = maxWait !== undefined ? Math.min(maxWait, 30000) : 30000;
|
|
148
|
-
if (waitTimeout <= 0) {
|
|
149
|
-
return this.assertCdpFallbackAllowed();
|
|
150
|
-
}
|
|
151
|
-
console.error(`[MCP] Waiting for Chrome Extension connection (${waitTimeout}ms timeout)...`);
|
|
152
|
-
console.error('[MCP] Please ensure Chrome is running with MCP Chrome extension installed.');
|
|
153
|
-
const connected = await this.extensionBridge.waitForConnection(waitTimeout);
|
|
154
|
-
if (connected) {
|
|
155
|
-
console.error('[MCP] Chrome Extension connected successfully');
|
|
156
|
-
this.lastConnectionFailure = 0;
|
|
157
|
-
return true;
|
|
158
|
-
}
|
|
159
|
-
console.error('[MCP] Chrome Extension connection timeout');
|
|
160
|
-
this.lastConnectionFailure = Date.now();
|
|
161
|
-
return this.assertCdpFallbackAllowed();
|
|
162
|
-
}
|
|
163
100
|
/**
|
|
164
101
|
* 启动浏览器(CDP 模式)或等待 Extension 连接
|
|
165
102
|
*/
|
|
@@ -185,16 +122,6 @@ class UnifiedSessionManager {
|
|
|
185
122
|
mode: 'cdp',
|
|
186
123
|
};
|
|
187
124
|
}
|
|
188
|
-
/**
|
|
189
|
-
* 连接到已运行的浏览器(CDP 模式)
|
|
190
|
-
*/
|
|
191
|
-
async connect(options) {
|
|
192
|
-
const target = await getCdpSession().connect(options);
|
|
193
|
-
return {
|
|
194
|
-
...target,
|
|
195
|
-
mode: 'cdp',
|
|
196
|
-
};
|
|
197
|
-
}
|
|
198
125
|
/**
|
|
199
126
|
* 列出所有页面
|
|
200
127
|
*/
|
|
@@ -289,8 +216,9 @@ class UnifiedSessionManager {
|
|
|
289
216
|
// 构建简单的文本表示
|
|
290
217
|
const lines = elements.map(e => {
|
|
291
218
|
let line = e.role;
|
|
292
|
-
if (e.name)
|
|
219
|
+
if (e.name) {
|
|
293
220
|
line += ` "${e.name}"`;
|
|
221
|
+
}
|
|
294
222
|
return line;
|
|
295
223
|
});
|
|
296
224
|
return {
|
|
@@ -306,7 +234,7 @@ class UnifiedSessionManager {
|
|
|
306
234
|
const result = await this.extensionBridge.screenshot(options);
|
|
307
235
|
return result.data;
|
|
308
236
|
}
|
|
309
|
-
return getCdpSession().screenshot(options?.fullPage);
|
|
237
|
+
return getCdpSession().screenshot(options?.fullPage, options?.scale, options?.format, options?.quality, options?.clip);
|
|
310
238
|
}
|
|
311
239
|
/**
|
|
312
240
|
* 点击元素
|
|
@@ -349,46 +277,61 @@ class UnifiedSessionManager {
|
|
|
349
277
|
* stealth 模式:使用 chrome.scripting.executeScript(受 CSP 限制)
|
|
350
278
|
* precise 模式:使用 debugger API Runtime.evaluate(可绕过 CSP)
|
|
351
279
|
*
|
|
352
|
-
* 使用 args 时 script
|
|
280
|
+
* 使用 args 时 script 必须是函数表达式,如 "(x) => x + 1"。
|
|
281
|
+
* precise 模式通过 callFunctionOn 传递参数(支持大 payload),stealth 模式仍用字符串拼接。
|
|
282
|
+
* @param code JavaScript 代码
|
|
283
|
+
* @param mode 执行模式(stealth/precise)
|
|
353
284
|
* @param timeout 端到端预算(毫秒),同时作为脚本执行超时和 sendCommand 的端到端预算
|
|
285
|
+
* @param args 传递给函数的参数
|
|
354
286
|
*/
|
|
355
287
|
async evaluate(code, mode, timeout, args) {
|
|
356
288
|
const effectiveMode = mode ?? this.inputMode;
|
|
357
|
-
|
|
358
|
-
//
|
|
289
|
+
const hasArgs = args && args.length > 0;
|
|
290
|
+
// 检测裸 return 语句,自动包裹 IIFE(仅无 args 时)
|
|
359
291
|
let expression = code;
|
|
360
|
-
if (/\breturn\b/.test(code) && !/^\s*([(\[]|function\b|async\b|class\b)/.test(code)) {
|
|
292
|
+
if (!hasArgs && /\breturn\b/.test(code) && !/^\s*([(\[]|function\b|async\b|class\b)/.test(code)) {
|
|
361
293
|
expression = `(() => { ${code} })()`;
|
|
362
294
|
}
|
|
363
|
-
|
|
295
|
+
// stealth 模式:args 只能通过字符串拼接(chrome.scripting 不支持协议级参数传递)
|
|
296
|
+
if (hasArgs && effectiveMode === 'stealth') {
|
|
364
297
|
const argsStr = args.map(a => JSON.stringify(a)).join(', ');
|
|
365
298
|
expression = `(${code})(${argsStr})`;
|
|
366
299
|
}
|
|
300
|
+
const cdpScript = hasArgs ? code : expression;
|
|
367
301
|
if (timeout !== undefined) {
|
|
368
302
|
// 轮询上下文:快速失败,端到端预算受控
|
|
369
303
|
if (!this.isExtensionConnected()) {
|
|
370
304
|
this.assertCdpFallbackAllowed();
|
|
371
|
-
return getCdpSession().evaluate(
|
|
305
|
+
return getCdpSession().evaluate(cdpScript, args, timeout);
|
|
372
306
|
}
|
|
373
307
|
}
|
|
374
308
|
else {
|
|
375
309
|
// 非轮询上下文:允许等待重连;连接失败时回退 CDP
|
|
376
310
|
if (!(await this.ensureExtensionConnected())) {
|
|
377
|
-
return getCdpSession().evaluate(
|
|
311
|
+
return getCdpSession().evaluate(cdpScript, args, timeout);
|
|
378
312
|
}
|
|
379
313
|
}
|
|
380
314
|
// Extension 路径
|
|
381
315
|
const currentFrameId = this.extensionBridge.getCurrentFrameId();
|
|
382
316
|
if (effectiveMode === 'precise') {
|
|
317
|
+
// precise + args + 主 frame:使用 callFunctionOn 避免大 payload 字符串拼接
|
|
318
|
+
if (hasArgs && currentFrameId === 0) {
|
|
319
|
+
return this.callFunctionOn(code, args, timeout);
|
|
320
|
+
}
|
|
383
321
|
if (currentFrameId !== 0) {
|
|
384
|
-
// iframe
|
|
385
|
-
|
|
322
|
+
// iframe:args 仍用字符串拼接(evaluateInFrame 使用 expression 字符串)
|
|
323
|
+
let iframeExpression = expression;
|
|
324
|
+
if (hasArgs) {
|
|
325
|
+
const argsStr = args.map(a => JSON.stringify(a)).join(', ');
|
|
326
|
+
iframeExpression = `(${code})(${argsStr})`;
|
|
327
|
+
}
|
|
328
|
+
const result = await this.extensionBridge.evaluateInFrame(currentFrameId, iframeExpression, timeout);
|
|
386
329
|
if (result.exceptionDetails) {
|
|
387
|
-
throw new Error(result.exceptionDetails
|
|
330
|
+
throw new Error(formatCdpException(result.exceptionDetails));
|
|
388
331
|
}
|
|
389
|
-
return result.result
|
|
332
|
+
return extractCdpValue(result.result);
|
|
390
333
|
}
|
|
391
|
-
// 主 frame:直接 Runtime.evaluate
|
|
334
|
+
// 主 frame,无 args:直接 Runtime.evaluate
|
|
392
335
|
const params = {
|
|
393
336
|
expression,
|
|
394
337
|
returnByValue: true,
|
|
@@ -400,9 +343,9 @@ class UnifiedSessionManager {
|
|
|
400
343
|
// timeout 即端到端预算,直接作为 RPC 超时(不额外加 margin)
|
|
401
344
|
const result = await this.extensionBridge.debuggerSend('Runtime.evaluate', params, undefined, timeout);
|
|
402
345
|
if (result.exceptionDetails) {
|
|
403
|
-
throw new Error(result.exceptionDetails
|
|
346
|
+
throw new Error(formatCdpException(result.exceptionDetails));
|
|
404
347
|
}
|
|
405
|
-
return result.result
|
|
348
|
+
return extractCdpValue(result.result);
|
|
406
349
|
}
|
|
407
350
|
return await this.extensionBridge.evaluate(expression, timeout, timeout);
|
|
408
351
|
}
|
|
@@ -431,6 +374,106 @@ class UnifiedSessionManager {
|
|
|
431
374
|
}
|
|
432
375
|
return getCdpSession().evaluate('document.documentElement.outerHTML');
|
|
433
376
|
}
|
|
377
|
+
/**
|
|
378
|
+
* 获取页面 HTML + 图片元信息
|
|
379
|
+
*/
|
|
380
|
+
async getHtmlWithImages(selector, outer = true) {
|
|
381
|
+
if (await this.ensureExtensionConnected()) {
|
|
382
|
+
return this.extensionBridge.getHtmlWithImages(selector, outer);
|
|
383
|
+
}
|
|
384
|
+
// CDP 模式:evaluate 注入函数
|
|
385
|
+
const selectorArg = JSON.stringify(selector ?? null);
|
|
386
|
+
return getCdpSession().evaluate(`(function() {
|
|
387
|
+
var root = ${selectorArg} ? document.querySelector(${selectorArg}) : document.documentElement;
|
|
388
|
+
if (!root) return {html: '', images: []};
|
|
389
|
+
var html = ${selectorArg}
|
|
390
|
+
? (${outer} ? root.outerHTML : root.innerHTML)
|
|
391
|
+
: document.documentElement.outerHTML;
|
|
392
|
+
var imgList = [];
|
|
393
|
+
if (root.tagName === 'IMG') imgList.push(root);
|
|
394
|
+
root.querySelectorAll('img').forEach(function(img) { imgList.push(img); });
|
|
395
|
+
var images = [];
|
|
396
|
+
for (var i = 0; i < imgList.length; i++) {
|
|
397
|
+
var img = imgList[i];
|
|
398
|
+
images.push({index: i, src: img.src, dataSrc: (function() { var raw = img.dataset.src || img.dataset.lazySrc || img.dataset.original || ''; if (!raw) return ''; try { return new URL(raw, location.href).href } catch(e) { return raw } })(), alt: img.alt, width: img.width, height: img.height, naturalWidth: img.naturalWidth, naturalHeight: img.naturalHeight});
|
|
399
|
+
}
|
|
400
|
+
return {html: html, images: images};
|
|
401
|
+
})()`);
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* 获取页面元信息
|
|
405
|
+
*/
|
|
406
|
+
async getMetadata() {
|
|
407
|
+
if (await this.ensureExtensionConnected()) {
|
|
408
|
+
return this.extensionBridge.getMetadata();
|
|
409
|
+
}
|
|
410
|
+
// CDP 模式:evaluate 注入函数
|
|
411
|
+
return getCdpSession().evaluate(`(function() {
|
|
412
|
+
function meta(name) {
|
|
413
|
+
var el = document.querySelector('meta[name="'+name+'"],meta[property="'+name+'"]');
|
|
414
|
+
return el ? el.content || undefined : undefined;
|
|
415
|
+
}
|
|
416
|
+
var og = {}, tw = {};
|
|
417
|
+
document.querySelectorAll('meta[property^="og:"]').forEach(function(m) { og[m.getAttribute('property')] = m.content || ''; });
|
|
418
|
+
document.querySelectorAll('meta[name^="twitter:"]').forEach(function(m) { tw[m.getAttribute('name')] = m.content || ''; });
|
|
419
|
+
var jsonLd = [];
|
|
420
|
+
document.querySelectorAll('script[type="application/ld+json"]').forEach(function(s) {
|
|
421
|
+
try { jsonLd.push(JSON.parse(s.textContent || '')); } catch(e) {}
|
|
422
|
+
});
|
|
423
|
+
var alternates = [];
|
|
424
|
+
document.querySelectorAll('link[rel="alternate"]').forEach(function(l) {
|
|
425
|
+
alternates.push({href: l.href, type: l.getAttribute('type') || undefined, hreflang: l.getAttribute('hreflang') || undefined});
|
|
426
|
+
});
|
|
427
|
+
var feeds = [];
|
|
428
|
+
document.querySelectorAll('link[type="application/rss+xml"],link[type="application/atom+xml"]').forEach(function(l) {
|
|
429
|
+
feeds.push({href: l.href, type: l.getAttribute('type'), title: l.getAttribute('title') || undefined});
|
|
430
|
+
});
|
|
431
|
+
return {
|
|
432
|
+
title: document.title,
|
|
433
|
+
description: meta('description'),
|
|
434
|
+
canonical: (document.querySelector('link[rel="canonical"]') || {}).href || undefined,
|
|
435
|
+
charset: document.characterSet,
|
|
436
|
+
viewport: meta('viewport'),
|
|
437
|
+
og: og, twitter: tw, jsonLd: jsonLd, alternates: alternates, feeds: feeds
|
|
438
|
+
};
|
|
439
|
+
})()`);
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* 批量从浏览器缓存获取资源内容
|
|
443
|
+
*
|
|
444
|
+
* 只调用一次 Page.enable + Page.getFrameTree,然后逐个获取资源。
|
|
445
|
+
* 并发限制避免 CDP 连接拥塞。
|
|
446
|
+
*/
|
|
447
|
+
async getResourceContentBatch(urls, concurrency = 6) {
|
|
448
|
+
const results = new Map();
|
|
449
|
+
if (urls.length === 0) {
|
|
450
|
+
return results;
|
|
451
|
+
}
|
|
452
|
+
try {
|
|
453
|
+
await this.sendCdpCommand('Page.enable');
|
|
454
|
+
const frameTree = await this.sendCdpCommand('Page.getFrameTree');
|
|
455
|
+
const frameId = frameTree.frameTree.frame.id;
|
|
456
|
+
// 并发控制
|
|
457
|
+
let idx = 0;
|
|
458
|
+
const next = async () => {
|
|
459
|
+
while (idx < urls.length) {
|
|
460
|
+
const url = urls[idx++];
|
|
461
|
+
try {
|
|
462
|
+
const result = await this.sendCdpCommand('Page.getResourceContent', { frameId, url });
|
|
463
|
+
results.set(url, result);
|
|
464
|
+
}
|
|
465
|
+
catch {
|
|
466
|
+
// 单个资源获取失败不影响其他
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
};
|
|
470
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, urls.length) }, () => next()));
|
|
471
|
+
}
|
|
472
|
+
catch {
|
|
473
|
+
// Page.enable 或 getFrameTree 失败,返回空结果
|
|
474
|
+
}
|
|
475
|
+
return results;
|
|
476
|
+
}
|
|
434
477
|
/**
|
|
435
478
|
* 查找元素
|
|
436
479
|
*
|
|
@@ -438,6 +481,9 @@ class UnifiedSessionManager {
|
|
|
438
481
|
* - 传入 timeout(轮询上下文):isExtensionConnected() 快速失败,不会主动等待重连;
|
|
439
482
|
* 仅在"预检通过但竞态断连落入 sendCommand"时才发生预算内的连接等待
|
|
440
483
|
* - 不传 timeout(一次性调用):ensureExtensionConnected() 允许等待重连(最多 30s)
|
|
484
|
+
* @param selector CSS 选择器
|
|
485
|
+
* @param text 文本内容
|
|
486
|
+
* @param xpath XPath 表达式
|
|
441
487
|
* @param timeout 端到端预算(毫秒),包含连接等待和请求超时,传给 bridge.find → sendCommand
|
|
442
488
|
*/
|
|
443
489
|
async find(selector, text, xpath, timeout) {
|
|
@@ -473,11 +519,13 @@ class UnifiedSessionManager {
|
|
|
473
519
|
// CDP 模式:支持按字段过滤
|
|
474
520
|
const urls = filter?.url ? [filter.url] : undefined;
|
|
475
521
|
const cookies = await getCdpSession().getCookies(urls);
|
|
476
|
-
if (!filter)
|
|
522
|
+
if (!filter) {
|
|
477
523
|
return cookies;
|
|
524
|
+
}
|
|
478
525
|
return cookies.filter((c) => {
|
|
479
|
-
if (filter.name && c.name !== filter.name)
|
|
526
|
+
if (filter.name && c.name !== filter.name) {
|
|
480
527
|
return false;
|
|
528
|
+
}
|
|
481
529
|
if (filter.domain) {
|
|
482
530
|
// 域名匹配:精确匹配或子域匹配(.example.com 匹配 sub.example.com)
|
|
483
531
|
const filterDomain = filter.domain.replace(/^\./, '');
|
|
@@ -486,15 +534,18 @@ class UnifiedSessionManager {
|
|
|
486
534
|
return false;
|
|
487
535
|
}
|
|
488
536
|
}
|
|
489
|
-
if (filter.path && c.path !== filter.path)
|
|
537
|
+
if (filter.path && c.path !== filter.path) {
|
|
490
538
|
return false;
|
|
491
|
-
|
|
539
|
+
}
|
|
540
|
+
if (filter.secure !== undefined && c.secure !== filter.secure) {
|
|
492
541
|
return false;
|
|
542
|
+
}
|
|
493
543
|
if (filter.session !== undefined) {
|
|
494
544
|
// session cookie: expires 为 -1 或 0(CDP 返回 session cookie 的 expires 为 -1)
|
|
495
545
|
const isSession = (c.expires ?? -1) <= 0;
|
|
496
|
-
if (filter.session !== isSession)
|
|
546
|
+
if (filter.session !== isSession) {
|
|
497
547
|
return false;
|
|
548
|
+
}
|
|
498
549
|
}
|
|
499
550
|
return true;
|
|
500
551
|
});
|
|
@@ -645,25 +696,6 @@ class UnifiedSessionManager {
|
|
|
645
696
|
// CDP 模式下需要 attach 到目标 target
|
|
646
697
|
await getCdpSession().attachToTarget(targetId);
|
|
647
698
|
}
|
|
648
|
-
/**
|
|
649
|
-
* 串行化所有 tab 切换操作,防止并发请求互相覆盖 currentTabId。
|
|
650
|
-
* 调用者:selectPage/activatePage/newPage/closePage/navigate/reload/launch/withTabId。
|
|
651
|
-
*
|
|
652
|
-
* 注意:此锁不可重入。fn() 内禁止调用任何使用 withTabLock 的方法,否则会死锁。
|
|
653
|
-
* 当前所有 fn() 只调用 bridge 的原子操作(createTab/navigate/evaluate 等),不存在此问题。
|
|
654
|
-
*/
|
|
655
|
-
async withTabLock(fn) {
|
|
656
|
-
const previousLock = this.tabSwitchLock;
|
|
657
|
-
let releaseLock;
|
|
658
|
-
this.tabSwitchLock = new Promise(resolve => { releaseLock = resolve; });
|
|
659
|
-
try {
|
|
660
|
-
await previousLock;
|
|
661
|
-
return await fn();
|
|
662
|
-
}
|
|
663
|
-
finally {
|
|
664
|
-
releaseLock();
|
|
665
|
-
}
|
|
666
|
-
}
|
|
667
699
|
/**
|
|
668
700
|
* 临时切换操作目标 tab,执行完后恢复
|
|
669
701
|
*
|
|
@@ -675,8 +707,9 @@ class UnifiedSessionManager {
|
|
|
675
707
|
*/
|
|
676
708
|
async withTabId(tabId, fn) {
|
|
677
709
|
// Extension 未连接时不需要锁和 tab 切换(CDP 模式无 currentTabId 竞态)
|
|
678
|
-
if (!this.extensionBridge?.isConnected())
|
|
710
|
+
if (!this.extensionBridge?.isConnected()) {
|
|
679
711
|
return fn();
|
|
712
|
+
}
|
|
680
713
|
if (!tabId) {
|
|
681
714
|
// 不切换 tab,但需要加锁保护 currentTabId 不被并发修改
|
|
682
715
|
return this.withTabLock(fn);
|
|
@@ -707,21 +740,25 @@ class UnifiedSessionManager {
|
|
|
707
740
|
* withTabId(tabId, () => withFrame(frame, () => { ... }))
|
|
708
741
|
*/
|
|
709
742
|
async withFrame(frame, fn) {
|
|
710
|
-
if (frame === undefined)
|
|
743
|
+
if (frame === undefined) {
|
|
711
744
|
return fn();
|
|
745
|
+
}
|
|
712
746
|
if (!this.extensionBridge?.isConnected()) {
|
|
713
747
|
throw new Error('iframe 穿透需要 Extension 模式');
|
|
714
748
|
}
|
|
715
|
-
const { frameId } = await this.extensionBridge.resolveFrame(frame);
|
|
749
|
+
const { frameId, offset } = await this.extensionBridge.resolveFrame(frame);
|
|
716
750
|
const previousFrameId = this.extensionBridge.getCurrentFrameId();
|
|
751
|
+
const previousFrameOffset = this.currentFrameOffset;
|
|
717
752
|
const previousRequireExtension = this.requireExtension;
|
|
718
753
|
this.extensionBridge.setCurrentFrameId(frameId);
|
|
754
|
+
this.currentFrameOffset = offset;
|
|
719
755
|
this.requireExtension = true;
|
|
720
756
|
try {
|
|
721
757
|
return await fn();
|
|
722
758
|
}
|
|
723
759
|
finally {
|
|
724
760
|
this.requireExtension = previousRequireExtension;
|
|
761
|
+
this.currentFrameOffset = previousFrameOffset;
|
|
725
762
|
this.extensionBridge.setCurrentFrameId(previousFrameId);
|
|
726
763
|
}
|
|
727
764
|
}
|
|
@@ -745,29 +782,19 @@ class UnifiedSessionManager {
|
|
|
745
782
|
}
|
|
746
783
|
await getCdpSession().close();
|
|
747
784
|
}
|
|
748
|
-
/**
|
|
749
|
-
* 解析 tab ID 字符串为数字,校验 NaN
|
|
750
|
-
*/
|
|
751
|
-
parseTabId(id) {
|
|
752
|
-
const tabId = parseInt(id, 10);
|
|
753
|
-
if (isNaN(tabId)) {
|
|
754
|
-
throw new Error(`无效的 Tab ID: ${id}`);
|
|
755
|
-
}
|
|
756
|
-
return tabId;
|
|
757
|
-
}
|
|
758
|
-
// ==================== 键鼠输入 ====================
|
|
759
|
-
// stealth 模式:使用 JS 事件模拟,不触发调试提示,推荐用于反检测场景
|
|
760
|
-
// precise 模式:使用 debugger API,精确但会显示"扩展程序正在调试此浏览器"
|
|
761
785
|
/**
|
|
762
786
|
* 按下键盘按键
|
|
763
787
|
*/
|
|
764
788
|
async keyDown(key) {
|
|
789
|
+
if (MODIFIER_KEYS[key]) {
|
|
790
|
+
this.modifiers |= MODIFIER_KEYS[key];
|
|
791
|
+
}
|
|
765
792
|
if (await this.ensureExtensionConnected()) {
|
|
766
793
|
if (this.inputMode === 'stealth') {
|
|
767
|
-
await this.extensionBridge.stealthKey(key, 'down');
|
|
794
|
+
await this.extensionBridge.stealthKey(key, 'down', this.getModifierNames());
|
|
768
795
|
}
|
|
769
796
|
else {
|
|
770
|
-
await this.extensionBridge.inputKey('keyDown', { key, code: key });
|
|
797
|
+
await this.extensionBridge.inputKey('keyDown', { key, code: key, modifiers: this.modifiers });
|
|
771
798
|
}
|
|
772
799
|
return;
|
|
773
800
|
}
|
|
@@ -779,13 +806,19 @@ class UnifiedSessionManager {
|
|
|
779
806
|
async keyUp(key) {
|
|
780
807
|
if (await this.ensureExtensionConnected()) {
|
|
781
808
|
if (this.inputMode === 'stealth') {
|
|
782
|
-
await this.extensionBridge.stealthKey(key, 'up');
|
|
809
|
+
await this.extensionBridge.stealthKey(key, 'up', this.getModifierNames());
|
|
783
810
|
}
|
|
784
811
|
else {
|
|
785
|
-
await this.extensionBridge.inputKey('keyUp', { key, code: key });
|
|
812
|
+
await this.extensionBridge.inputKey('keyUp', { key, code: key, modifiers: this.modifiers });
|
|
813
|
+
}
|
|
814
|
+
if (MODIFIER_KEYS[key]) {
|
|
815
|
+
this.modifiers &= ~MODIFIER_KEYS[key];
|
|
786
816
|
}
|
|
787
817
|
return;
|
|
788
818
|
}
|
|
819
|
+
if (MODIFIER_KEYS[key]) {
|
|
820
|
+
this.modifiers &= ~MODIFIER_KEYS[key];
|
|
821
|
+
}
|
|
789
822
|
await getCdpSession().keyUp(key);
|
|
790
823
|
}
|
|
791
824
|
/**
|
|
@@ -813,7 +846,7 @@ class UnifiedSessionManager {
|
|
|
813
846
|
await this.extensionBridge.stealthMouse('mousemove', x, y);
|
|
814
847
|
}
|
|
815
848
|
else {
|
|
816
|
-
await this.extensionBridge.inputMouse('mouseMoved', x, y);
|
|
849
|
+
await this.extensionBridge.inputMouse('mouseMoved', x, y, { modifiers: this.modifiers });
|
|
817
850
|
}
|
|
818
851
|
return;
|
|
819
852
|
}
|
|
@@ -830,7 +863,11 @@ class UnifiedSessionManager {
|
|
|
830
863
|
await this.extensionBridge.stealthMouse('mousedown', x, y, effectiveButton);
|
|
831
864
|
}
|
|
832
865
|
else {
|
|
833
|
-
await this.extensionBridge.inputMouse('mousePressed', x, y, {
|
|
866
|
+
await this.extensionBridge.inputMouse('mousePressed', x, y, {
|
|
867
|
+
button: effectiveButton,
|
|
868
|
+
clickCount: 1,
|
|
869
|
+
modifiers: this.modifiers,
|
|
870
|
+
});
|
|
834
871
|
}
|
|
835
872
|
return;
|
|
836
873
|
}
|
|
@@ -847,40 +884,41 @@ class UnifiedSessionManager {
|
|
|
847
884
|
await this.extensionBridge.stealthMouse('mouseup', x, y, effectiveButton);
|
|
848
885
|
}
|
|
849
886
|
else {
|
|
850
|
-
await this.extensionBridge.inputMouse('mouseReleased', x, y, { button: effectiveButton });
|
|
887
|
+
await this.extensionBridge.inputMouse('mouseReleased', x, y, { button: effectiveButton, clickCount: 1, modifiers: this.modifiers });
|
|
851
888
|
}
|
|
852
889
|
return;
|
|
853
890
|
}
|
|
854
891
|
await getCdpSession().mouseUp(effectiveButton);
|
|
855
892
|
}
|
|
856
893
|
/**
|
|
857
|
-
*
|
|
894
|
+
* 鼠标点击(mousedown + mouseup + click 三合一)
|
|
895
|
+
*
|
|
896
|
+
* stealth 模式:原子操作(单次脚本注入完成 mouseover → mousedown → focus → mouseup → click)
|
|
897
|
+
* precise / CDP 模式:mouseDown + mouseUp,浏览器自动合成原生 click 事件
|
|
858
898
|
*/
|
|
859
|
-
async
|
|
860
|
-
if (await this.ensureExtensionConnected()) {
|
|
899
|
+
async mouseClick(button = 'left') {
|
|
900
|
+
if (this.inputMode === 'stealth' && await this.ensureExtensionConnected()) {
|
|
901
|
+
const effectiveButton = (button === 'back' || button === 'forward') ? 'left' : button;
|
|
861
902
|
const { x, y } = this.currentMousePosition;
|
|
862
|
-
await this.extensionBridge.
|
|
903
|
+
await this.extensionBridge.stealthClick(x, y, effectiveButton);
|
|
863
904
|
return;
|
|
864
905
|
}
|
|
865
|
-
await
|
|
906
|
+
await this.mouseDown(button);
|
|
907
|
+
await this.mouseUp(button);
|
|
866
908
|
}
|
|
909
|
+
// ==================== 键鼠输入 ====================
|
|
910
|
+
// stealth 模式:使用 JS 事件模拟,不触发调试提示,推荐用于反检测场景
|
|
911
|
+
// precise 模式:使用 debugger API,精确但会显示"扩展程序正在调试此浏览器"
|
|
867
912
|
/**
|
|
868
|
-
*
|
|
913
|
+
* 鼠标滚轮
|
|
869
914
|
*/
|
|
870
|
-
async
|
|
915
|
+
async mouseWheel(deltaX, deltaY) {
|
|
871
916
|
if (await this.ensureExtensionConnected()) {
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
}
|
|
875
|
-
else {
|
|
876
|
-
await this.extensionBridge.inputMouse('mousePressed', x, y, { button: button, clickCount: 1 });
|
|
877
|
-
await this.extensionBridge.inputMouse('mouseReleased', x, y, { button: button });
|
|
878
|
-
}
|
|
917
|
+
const { x, y } = this.currentMousePosition;
|
|
918
|
+
await this.extensionBridge.inputMouse('mouseWheel', x, y, { deltaX, deltaY, modifiers: this.modifiers });
|
|
879
919
|
return;
|
|
880
920
|
}
|
|
881
|
-
await getCdpSession().
|
|
882
|
-
await getCdpSession().mouseDown(button);
|
|
883
|
-
await getCdpSession().mouseUp(button);
|
|
921
|
+
await getCdpSession().mouseWheel(deltaX, deltaY);
|
|
884
922
|
}
|
|
885
923
|
/**
|
|
886
924
|
* 注入反检测脚本
|
|
@@ -922,7 +960,6 @@ class UnifiedSessionManager {
|
|
|
922
960
|
}
|
|
923
961
|
await getCdpSession().touchEnd();
|
|
924
962
|
}
|
|
925
|
-
// ==================== 控制台日志 ====================
|
|
926
963
|
/**
|
|
927
964
|
* 启用控制台日志捕获
|
|
928
965
|
*/
|
|
@@ -943,16 +980,6 @@ class UnifiedSessionManager {
|
|
|
943
980
|
// CDP 模式需要单独实现
|
|
944
981
|
return [];
|
|
945
982
|
}
|
|
946
|
-
/**
|
|
947
|
-
* 清除控制台日志
|
|
948
|
-
*/
|
|
949
|
-
async clearConsoleLogs() {
|
|
950
|
-
if (await this.ensureExtensionConnected()) {
|
|
951
|
-
await this.extensionBridge.consoleClear();
|
|
952
|
-
return;
|
|
953
|
-
}
|
|
954
|
-
}
|
|
955
|
-
// ==================== 网络日志 ====================
|
|
956
983
|
/**
|
|
957
984
|
* 启用网络日志捕获
|
|
958
985
|
*/
|
|
@@ -972,16 +999,6 @@ class UnifiedSessionManager {
|
|
|
972
999
|
}
|
|
973
1000
|
return [];
|
|
974
1001
|
}
|
|
975
|
-
/**
|
|
976
|
-
* 清除网络日志
|
|
977
|
-
*/
|
|
978
|
-
async clearNetworkLogs() {
|
|
979
|
-
if (await this.ensureExtensionConnected()) {
|
|
980
|
-
await this.extensionBridge.networkClear();
|
|
981
|
-
return;
|
|
982
|
-
}
|
|
983
|
-
}
|
|
984
|
-
// ==================== Debugger 直接访问 ====================
|
|
985
1002
|
/**
|
|
986
1003
|
* 发送 CDP 命令(高级用法)
|
|
987
1004
|
*
|
|
@@ -1000,6 +1017,150 @@ class UnifiedSessionManager {
|
|
|
1000
1017
|
}
|
|
1001
1018
|
return getCdpSession().send(method, params);
|
|
1002
1019
|
}
|
|
1020
|
+
/** 获取当前修饰键名称数组(stealth 模式用) */
|
|
1021
|
+
getModifierNames() {
|
|
1022
|
+
const names = [];
|
|
1023
|
+
if (this.modifiers & 1) {
|
|
1024
|
+
names.push('alt');
|
|
1025
|
+
}
|
|
1026
|
+
if (this.modifiers & 2) {
|
|
1027
|
+
names.push('ctrl');
|
|
1028
|
+
}
|
|
1029
|
+
if (this.modifiers & 4) {
|
|
1030
|
+
names.push('meta');
|
|
1031
|
+
}
|
|
1032
|
+
if (this.modifiers & 8) {
|
|
1033
|
+
names.push('shift');
|
|
1034
|
+
}
|
|
1035
|
+
return names;
|
|
1036
|
+
}
|
|
1037
|
+
// ==================== 控制台日志 ====================
|
|
1038
|
+
/**
|
|
1039
|
+
* 检查 CDP 回退是否允许
|
|
1040
|
+
*
|
|
1041
|
+
* 当 requireExtension 为 true(tabId 或 frame 已指定)时,CDP 回退会操作错误目标,必须阻止。
|
|
1042
|
+
* 允许时返回 false(供 ensureExtensionConnected 直接返回),不允许时抛出。
|
|
1043
|
+
*/
|
|
1044
|
+
assertCdpFallbackAllowed() {
|
|
1045
|
+
if (this.requireExtension) {
|
|
1046
|
+
throw new Error('Extension 已断开,当前操作需要 Extension(指定 tabId 或 frame)时不可回退 CDP(操作目标不一致)');
|
|
1047
|
+
}
|
|
1048
|
+
return false;
|
|
1049
|
+
}
|
|
1050
|
+
/**
|
|
1051
|
+
* 确保 Extension 已连接,如果断开则等待重连
|
|
1052
|
+
* 返回 true 表示 Extension 可用,false 表示应 fallback 到 CDP
|
|
1053
|
+
*
|
|
1054
|
+
* 设计理念:Server 和 Extension 的启动时机完全独立,无任何要求。
|
|
1055
|
+
* - 先装 Extension,一个月/一年后启动 Server → 能连上
|
|
1056
|
+
* - 先启动 Server,再打开 Chrome → 能连上
|
|
1057
|
+
* - 关闭再打开任何一方 → 能自动重连
|
|
1058
|
+
*
|
|
1059
|
+
* 超时设为 30 秒:足够等待 Extension 启动,但不会永远卡住。
|
|
1060
|
+
*
|
|
1061
|
+
* @param maxWait 调用方的端到端预算(毫秒)。传入时取 min(maxWait, 30000) 作为连接等待上限,
|
|
1062
|
+
* 避免工具 timeout 被连接等待吞掉。不传则使用默认 30s。
|
|
1063
|
+
*/
|
|
1064
|
+
async ensureExtensionConnected(maxWait) {
|
|
1065
|
+
if (!this.extensionBridge) {
|
|
1066
|
+
return this.assertCdpFallbackAllowed();
|
|
1067
|
+
}
|
|
1068
|
+
if (this.extensionBridge.isConnected()) {
|
|
1069
|
+
return true;
|
|
1070
|
+
}
|
|
1071
|
+
// CDP 已连接时跳过 Extension 等待,直接使用 CDP 回退
|
|
1072
|
+
if (getCdpSession().isConnected()) {
|
|
1073
|
+
return this.assertCdpFallbackAllowed();
|
|
1074
|
+
}
|
|
1075
|
+
// 冷却期内不重复等待,避免每次操作都阻塞 30 秒
|
|
1076
|
+
if (Date.now() - this.lastConnectionFailure < UnifiedSessionManager.CONNECTION_COOLDOWN) {
|
|
1077
|
+
return this.assertCdpFallbackAllowed();
|
|
1078
|
+
}
|
|
1079
|
+
// Extension 服务器已启动但断开连接,等待重连
|
|
1080
|
+
const waitTimeout = maxWait !== undefined ? Math.min(maxWait, 30000) : 30000;
|
|
1081
|
+
if (waitTimeout <= 0) {
|
|
1082
|
+
return this.assertCdpFallbackAllowed();
|
|
1083
|
+
}
|
|
1084
|
+
console.error(`[MCP] Waiting for Chrome Extension connection (${waitTimeout}ms timeout)...`);
|
|
1085
|
+
console.error('[MCP] Please ensure Chrome is running with MCP Chrome extension installed.');
|
|
1086
|
+
const connected = await this.extensionBridge.waitForConnection(waitTimeout);
|
|
1087
|
+
if (connected) {
|
|
1088
|
+
console.error('[MCP] Chrome Extension connected successfully');
|
|
1089
|
+
this.lastConnectionFailure = 0;
|
|
1090
|
+
return true;
|
|
1091
|
+
}
|
|
1092
|
+
console.error('[MCP] Chrome Extension connection timeout');
|
|
1093
|
+
this.lastConnectionFailure = Date.now();
|
|
1094
|
+
return this.assertCdpFallbackAllowed();
|
|
1095
|
+
}
|
|
1096
|
+
// ==================== 网络日志 ====================
|
|
1097
|
+
/**
|
|
1098
|
+
* 通过 callFunctionOn 执行函数调用
|
|
1099
|
+
*
|
|
1100
|
+
* 参数通过 CDP 协议结构化传递,避免大 payload 字符串拼接导致的长度限制和转义问题。
|
|
1101
|
+
* 要求 code 必须是函数表达式(如 "(x) => x + 1")。
|
|
1102
|
+
*/
|
|
1103
|
+
async callFunctionOn(code, args, timeout) {
|
|
1104
|
+
const globalResult = await this.extensionBridge.debuggerSend('Runtime.evaluate', {
|
|
1105
|
+
expression: 'globalThis',
|
|
1106
|
+
returnByValue: false,
|
|
1107
|
+
}, undefined, timeout);
|
|
1108
|
+
try {
|
|
1109
|
+
const params = {
|
|
1110
|
+
functionDeclaration: code,
|
|
1111
|
+
objectId: globalResult.result.objectId,
|
|
1112
|
+
arguments: args.map(a => ({ value: a })),
|
|
1113
|
+
returnByValue: true,
|
|
1114
|
+
awaitPromise: true,
|
|
1115
|
+
};
|
|
1116
|
+
if (timeout !== undefined) {
|
|
1117
|
+
params.timeout = timeout;
|
|
1118
|
+
}
|
|
1119
|
+
const result = await this.extensionBridge.debuggerSend('Runtime.callFunctionOn', params, undefined, timeout);
|
|
1120
|
+
if (result.exceptionDetails) {
|
|
1121
|
+
throw new Error(formatCdpException(result.exceptionDetails));
|
|
1122
|
+
}
|
|
1123
|
+
return extractCdpValue(result.result);
|
|
1124
|
+
}
|
|
1125
|
+
finally {
|
|
1126
|
+
this.extensionBridge.debuggerSend('Runtime.releaseObject', {
|
|
1127
|
+
objectId: globalResult.result.objectId,
|
|
1128
|
+
}).catch(() => {
|
|
1129
|
+
});
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
/**
|
|
1133
|
+
* 串行化所有 tab 切换操作,防止并发请求互相覆盖 currentTabId。
|
|
1134
|
+
* 调用者:selectPage/activatePage/newPage/closePage/navigate/reload/launch/withTabId。
|
|
1135
|
+
*
|
|
1136
|
+
* 注意:此锁不可重入。fn() 内禁止调用任何使用 withTabLock 的方法,否则会死锁。
|
|
1137
|
+
* 当前所有 fn() 只调用 bridge 的原子操作(createTab/navigate/evaluate 等),不存在此问题。
|
|
1138
|
+
*/
|
|
1139
|
+
async withTabLock(fn) {
|
|
1140
|
+
const previousLock = this.tabSwitchLock;
|
|
1141
|
+
let releaseLock;
|
|
1142
|
+
this.tabSwitchLock = new Promise(resolve => {
|
|
1143
|
+
releaseLock = resolve;
|
|
1144
|
+
});
|
|
1145
|
+
try {
|
|
1146
|
+
await previousLock;
|
|
1147
|
+
return await fn();
|
|
1148
|
+
}
|
|
1149
|
+
finally {
|
|
1150
|
+
releaseLock();
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
// ==================== Debugger 直接访问 ====================
|
|
1154
|
+
/**
|
|
1155
|
+
* 解析 tab ID 字符串为数字,校验 NaN
|
|
1156
|
+
*/
|
|
1157
|
+
parseTabId(id) {
|
|
1158
|
+
const tabId = parseInt(id, 10);
|
|
1159
|
+
if (isNaN(tabId)) {
|
|
1160
|
+
throw new Error(`无效的 Tab ID: ${id}`);
|
|
1161
|
+
}
|
|
1162
|
+
return tabId;
|
|
1163
|
+
}
|
|
1003
1164
|
}
|
|
1004
1165
|
/**
|
|
1005
1166
|
* 获取统一会话管理器实例
|