@pyrokine/mcp-chrome 1.1.0 → 1.3.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 +103 -53
- 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 +4 -20
- 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 +32 -33
- package/dist/core/session.d.ts.map +1 -1
- package/dist/core/session.js +154 -114
- package/dist/core/session.js.map +1 -1
- package/dist/core/types.d.ts +4 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js +6 -0
- package/dist/core/types.js.map +1 -1
- package/dist/core/unified-session.d.ts +54 -67
- package/dist/core/unified-session.d.ts.map +1 -1
- package/dist/core/unified-session.js +215 -181
- package/dist/core/unified-session.js.map +1 -1
- package/dist/extension/bridge.d.ts +0 -19
- package/dist/extension/bridge.d.ts.map +1 -1
- package/dist/extension/bridge.js +6 -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 +11 -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 +3 -226
- package/dist/tools/extract.d.ts.map +1 -1
- package/dist/tools/extract.js +98 -170
- 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 +56 -143
- 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,20 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { ExtensionBridge } from '../extension/index.js';
|
|
9
9
|
import { getSession as getCdpSession } from './session.js';
|
|
10
|
+
import { 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
|
-
constructor() {
|
|
22
|
+
constructor() {
|
|
23
|
+
}
|
|
20
24
|
static getInstance() {
|
|
21
25
|
if (!UnifiedSessionManager.instance) {
|
|
22
26
|
UnifiedSessionManager.instance = new UnifiedSessionManager();
|
|
@@ -73,30 +77,12 @@ class UnifiedSessionManager {
|
|
|
73
77
|
this.inputMode = mode;
|
|
74
78
|
console.error(`[MCP] Input mode set to: ${mode}`);
|
|
75
79
|
}
|
|
76
|
-
/**
|
|
77
|
-
* 是否已连接(任一模式)
|
|
78
|
-
*/
|
|
79
|
-
isConnected() {
|
|
80
|
-
return this.getMode() !== 'none';
|
|
81
|
-
}
|
|
82
80
|
/**
|
|
83
81
|
* 是否 Extension 已连接
|
|
84
82
|
*/
|
|
85
83
|
isExtensionConnected() {
|
|
86
84
|
return this.extensionBridge?.isConnected() ?? false;
|
|
87
85
|
}
|
|
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
86
|
/**
|
|
101
87
|
* 是否启用了 Extension 模式(不管当前是否连接)
|
|
102
88
|
* 用于判断应该使用哪种模式
|
|
@@ -104,62 +90,6 @@ class UnifiedSessionManager {
|
|
|
104
90
|
isExtensionModeEnabled() {
|
|
105
91
|
return this.extensionBridge !== null;
|
|
106
92
|
}
|
|
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
93
|
/**
|
|
164
94
|
* 启动浏览器(CDP 模式)或等待 Extension 连接
|
|
165
95
|
*/
|
|
@@ -185,16 +115,6 @@ class UnifiedSessionManager {
|
|
|
185
115
|
mode: 'cdp',
|
|
186
116
|
};
|
|
187
117
|
}
|
|
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
118
|
/**
|
|
199
119
|
* 列出所有页面
|
|
200
120
|
*/
|
|
@@ -289,8 +209,9 @@ class UnifiedSessionManager {
|
|
|
289
209
|
// 构建简单的文本表示
|
|
290
210
|
const lines = elements.map(e => {
|
|
291
211
|
let line = e.role;
|
|
292
|
-
if (e.name)
|
|
212
|
+
if (e.name) {
|
|
293
213
|
line += ` "${e.name}"`;
|
|
214
|
+
}
|
|
294
215
|
return line;
|
|
295
216
|
});
|
|
296
217
|
return {
|
|
@@ -306,7 +227,7 @@ class UnifiedSessionManager {
|
|
|
306
227
|
const result = await this.extensionBridge.screenshot(options);
|
|
307
228
|
return result.data;
|
|
308
229
|
}
|
|
309
|
-
return getCdpSession().screenshot(options?.fullPage);
|
|
230
|
+
return getCdpSession().screenshot(options?.fullPage, options?.scale, options?.format, options?.quality);
|
|
310
231
|
}
|
|
311
232
|
/**
|
|
312
233
|
* 点击元素
|
|
@@ -349,18 +270,23 @@ class UnifiedSessionManager {
|
|
|
349
270
|
* stealth 模式:使用 chrome.scripting.executeScript(受 CSP 限制)
|
|
350
271
|
* precise 模式:使用 debugger API Runtime.evaluate(可绕过 CSP)
|
|
351
272
|
*
|
|
352
|
-
* 使用 args 时 script
|
|
273
|
+
* 使用 args 时 script 必须是函数表达式,如 "(x) => x + 1"。
|
|
274
|
+
* precise 模式通过 callFunctionOn 传递参数(支持大 payload),stealth 模式仍用字符串拼接。
|
|
275
|
+
* @param code JavaScript 代码
|
|
276
|
+
* @param mode 执行模式(stealth/precise)
|
|
353
277
|
* @param timeout 端到端预算(毫秒),同时作为脚本执行超时和 sendCommand 的端到端预算
|
|
278
|
+
* @param args 传递给函数的参数
|
|
354
279
|
*/
|
|
355
280
|
async evaluate(code, mode, timeout, args) {
|
|
356
281
|
const effectiveMode = mode ?? this.inputMode;
|
|
357
|
-
|
|
358
|
-
//
|
|
282
|
+
const hasArgs = args && args.length > 0;
|
|
283
|
+
// 检测裸 return 语句,自动包裹 IIFE(仅无 args 时)
|
|
359
284
|
let expression = code;
|
|
360
|
-
if (/\breturn\b/.test(code) && !/^\s*([(\[]|function\b|async\b|class\b)/.test(code)) {
|
|
285
|
+
if (!hasArgs && /\breturn\b/.test(code) && !/^\s*([(\[]|function\b|async\b|class\b)/.test(code)) {
|
|
361
286
|
expression = `(() => { ${code} })()`;
|
|
362
287
|
}
|
|
363
|
-
|
|
288
|
+
// stealth 模式:args 只能通过字符串拼接(chrome.scripting 不支持协议级参数传递)
|
|
289
|
+
if (hasArgs && effectiveMode === 'stealth') {
|
|
364
290
|
const argsStr = args.map(a => JSON.stringify(a)).join(', ');
|
|
365
291
|
expression = `(${code})(${argsStr})`;
|
|
366
292
|
}
|
|
@@ -380,15 +306,24 @@ class UnifiedSessionManager {
|
|
|
380
306
|
// Extension 路径
|
|
381
307
|
const currentFrameId = this.extensionBridge.getCurrentFrameId();
|
|
382
308
|
if (effectiveMode === 'precise') {
|
|
309
|
+
// precise + args + 主 frame:使用 callFunctionOn 避免大 payload 字符串拼接
|
|
310
|
+
if (hasArgs && currentFrameId === 0) {
|
|
311
|
+
return this.callFunctionOn(code, args, timeout);
|
|
312
|
+
}
|
|
383
313
|
if (currentFrameId !== 0) {
|
|
384
|
-
// iframe
|
|
385
|
-
|
|
314
|
+
// iframe:args 仍用字符串拼接(evaluateInFrame 使用 expression 字符串)
|
|
315
|
+
let iframeExpression = expression;
|
|
316
|
+
if (hasArgs) {
|
|
317
|
+
const argsStr = args.map(a => JSON.stringify(a)).join(', ');
|
|
318
|
+
iframeExpression = `(${code})(${argsStr})`;
|
|
319
|
+
}
|
|
320
|
+
const result = await this.extensionBridge.evaluateInFrame(currentFrameId, iframeExpression, timeout);
|
|
386
321
|
if (result.exceptionDetails) {
|
|
387
322
|
throw new Error(result.exceptionDetails.text);
|
|
388
323
|
}
|
|
389
324
|
return result.result?.value;
|
|
390
325
|
}
|
|
391
|
-
// 主 frame:直接 Runtime.evaluate
|
|
326
|
+
// 主 frame,无 args:直接 Runtime.evaluate
|
|
392
327
|
const params = {
|
|
393
328
|
expression,
|
|
394
329
|
returnByValue: true,
|
|
@@ -438,6 +373,9 @@ class UnifiedSessionManager {
|
|
|
438
373
|
* - 传入 timeout(轮询上下文):isExtensionConnected() 快速失败,不会主动等待重连;
|
|
439
374
|
* 仅在"预检通过但竞态断连落入 sendCommand"时才发生预算内的连接等待
|
|
440
375
|
* - 不传 timeout(一次性调用):ensureExtensionConnected() 允许等待重连(最多 30s)
|
|
376
|
+
* @param selector CSS 选择器
|
|
377
|
+
* @param text 文本内容
|
|
378
|
+
* @param xpath XPath 表达式
|
|
441
379
|
* @param timeout 端到端预算(毫秒),包含连接等待和请求超时,传给 bridge.find → sendCommand
|
|
442
380
|
*/
|
|
443
381
|
async find(selector, text, xpath, timeout) {
|
|
@@ -473,11 +411,13 @@ class UnifiedSessionManager {
|
|
|
473
411
|
// CDP 模式:支持按字段过滤
|
|
474
412
|
const urls = filter?.url ? [filter.url] : undefined;
|
|
475
413
|
const cookies = await getCdpSession().getCookies(urls);
|
|
476
|
-
if (!filter)
|
|
414
|
+
if (!filter) {
|
|
477
415
|
return cookies;
|
|
416
|
+
}
|
|
478
417
|
return cookies.filter((c) => {
|
|
479
|
-
if (filter.name && c.name !== filter.name)
|
|
418
|
+
if (filter.name && c.name !== filter.name) {
|
|
480
419
|
return false;
|
|
420
|
+
}
|
|
481
421
|
if (filter.domain) {
|
|
482
422
|
// 域名匹配:精确匹配或子域匹配(.example.com 匹配 sub.example.com)
|
|
483
423
|
const filterDomain = filter.domain.replace(/^\./, '');
|
|
@@ -486,15 +426,18 @@ class UnifiedSessionManager {
|
|
|
486
426
|
return false;
|
|
487
427
|
}
|
|
488
428
|
}
|
|
489
|
-
if (filter.path && c.path !== filter.path)
|
|
429
|
+
if (filter.path && c.path !== filter.path) {
|
|
490
430
|
return false;
|
|
491
|
-
|
|
431
|
+
}
|
|
432
|
+
if (filter.secure !== undefined && c.secure !== filter.secure) {
|
|
492
433
|
return false;
|
|
434
|
+
}
|
|
493
435
|
if (filter.session !== undefined) {
|
|
494
436
|
// session cookie: expires 为 -1 或 0(CDP 返回 session cookie 的 expires 为 -1)
|
|
495
437
|
const isSession = (c.expires ?? -1) <= 0;
|
|
496
|
-
if (filter.session !== isSession)
|
|
438
|
+
if (filter.session !== isSession) {
|
|
497
439
|
return false;
|
|
440
|
+
}
|
|
498
441
|
}
|
|
499
442
|
return true;
|
|
500
443
|
});
|
|
@@ -645,25 +588,6 @@ class UnifiedSessionManager {
|
|
|
645
588
|
// CDP 模式下需要 attach 到目标 target
|
|
646
589
|
await getCdpSession().attachToTarget(targetId);
|
|
647
590
|
}
|
|
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
591
|
/**
|
|
668
592
|
* 临时切换操作目标 tab,执行完后恢复
|
|
669
593
|
*
|
|
@@ -675,8 +599,9 @@ class UnifiedSessionManager {
|
|
|
675
599
|
*/
|
|
676
600
|
async withTabId(tabId, fn) {
|
|
677
601
|
// Extension 未连接时不需要锁和 tab 切换(CDP 模式无 currentTabId 竞态)
|
|
678
|
-
if (!this.extensionBridge?.isConnected())
|
|
602
|
+
if (!this.extensionBridge?.isConnected()) {
|
|
679
603
|
return fn();
|
|
604
|
+
}
|
|
680
605
|
if (!tabId) {
|
|
681
606
|
// 不切换 tab,但需要加锁保护 currentTabId 不被并发修改
|
|
682
607
|
return this.withTabLock(fn);
|
|
@@ -707,8 +632,9 @@ class UnifiedSessionManager {
|
|
|
707
632
|
* withTabId(tabId, () => withFrame(frame, () => { ... }))
|
|
708
633
|
*/
|
|
709
634
|
async withFrame(frame, fn) {
|
|
710
|
-
if (frame === undefined)
|
|
635
|
+
if (frame === undefined) {
|
|
711
636
|
return fn();
|
|
637
|
+
}
|
|
712
638
|
if (!this.extensionBridge?.isConnected()) {
|
|
713
639
|
throw new Error('iframe 穿透需要 Extension 模式');
|
|
714
640
|
}
|
|
@@ -745,29 +671,19 @@ class UnifiedSessionManager {
|
|
|
745
671
|
}
|
|
746
672
|
await getCdpSession().close();
|
|
747
673
|
}
|
|
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
674
|
/**
|
|
762
675
|
* 按下键盘按键
|
|
763
676
|
*/
|
|
764
677
|
async keyDown(key) {
|
|
678
|
+
if (MODIFIER_KEYS[key]) {
|
|
679
|
+
this.modifiers |= MODIFIER_KEYS[key];
|
|
680
|
+
}
|
|
765
681
|
if (await this.ensureExtensionConnected()) {
|
|
766
682
|
if (this.inputMode === 'stealth') {
|
|
767
|
-
await this.extensionBridge.stealthKey(key, 'down');
|
|
683
|
+
await this.extensionBridge.stealthKey(key, 'down', this.getModifierNames());
|
|
768
684
|
}
|
|
769
685
|
else {
|
|
770
|
-
await this.extensionBridge.inputKey('keyDown', { key, code: key });
|
|
686
|
+
await this.extensionBridge.inputKey('keyDown', { key, code: key, modifiers: this.modifiers });
|
|
771
687
|
}
|
|
772
688
|
return;
|
|
773
689
|
}
|
|
@@ -779,13 +695,19 @@ class UnifiedSessionManager {
|
|
|
779
695
|
async keyUp(key) {
|
|
780
696
|
if (await this.ensureExtensionConnected()) {
|
|
781
697
|
if (this.inputMode === 'stealth') {
|
|
782
|
-
await this.extensionBridge.stealthKey(key, 'up');
|
|
698
|
+
await this.extensionBridge.stealthKey(key, 'up', this.getModifierNames());
|
|
783
699
|
}
|
|
784
700
|
else {
|
|
785
|
-
await this.extensionBridge.inputKey('keyUp', { key, code: key });
|
|
701
|
+
await this.extensionBridge.inputKey('keyUp', { key, code: key, modifiers: this.modifiers });
|
|
702
|
+
}
|
|
703
|
+
if (MODIFIER_KEYS[key]) {
|
|
704
|
+
this.modifiers &= ~MODIFIER_KEYS[key];
|
|
786
705
|
}
|
|
787
706
|
return;
|
|
788
707
|
}
|
|
708
|
+
if (MODIFIER_KEYS[key]) {
|
|
709
|
+
this.modifiers &= ~MODIFIER_KEYS[key];
|
|
710
|
+
}
|
|
789
711
|
await getCdpSession().keyUp(key);
|
|
790
712
|
}
|
|
791
713
|
/**
|
|
@@ -813,7 +735,7 @@ class UnifiedSessionManager {
|
|
|
813
735
|
await this.extensionBridge.stealthMouse('mousemove', x, y);
|
|
814
736
|
}
|
|
815
737
|
else {
|
|
816
|
-
await this.extensionBridge.inputMouse('mouseMoved', x, y);
|
|
738
|
+
await this.extensionBridge.inputMouse('mouseMoved', x, y, { modifiers: this.modifiers });
|
|
817
739
|
}
|
|
818
740
|
return;
|
|
819
741
|
}
|
|
@@ -830,7 +752,11 @@ class UnifiedSessionManager {
|
|
|
830
752
|
await this.extensionBridge.stealthMouse('mousedown', x, y, effectiveButton);
|
|
831
753
|
}
|
|
832
754
|
else {
|
|
833
|
-
await this.extensionBridge.inputMouse('mousePressed', x, y, {
|
|
755
|
+
await this.extensionBridge.inputMouse('mousePressed', x, y, {
|
|
756
|
+
button: effectiveButton,
|
|
757
|
+
clickCount: 1,
|
|
758
|
+
modifiers: this.modifiers,
|
|
759
|
+
});
|
|
834
760
|
}
|
|
835
761
|
return;
|
|
836
762
|
}
|
|
@@ -847,41 +773,26 @@ class UnifiedSessionManager {
|
|
|
847
773
|
await this.extensionBridge.stealthMouse('mouseup', x, y, effectiveButton);
|
|
848
774
|
}
|
|
849
775
|
else {
|
|
850
|
-
await this.extensionBridge.inputMouse('mouseReleased', x, y, { button: effectiveButton });
|
|
776
|
+
await this.extensionBridge.inputMouse('mouseReleased', x, y, { button: effectiveButton, modifiers: this.modifiers });
|
|
851
777
|
}
|
|
852
778
|
return;
|
|
853
779
|
}
|
|
854
780
|
await getCdpSession().mouseUp(effectiveButton);
|
|
855
781
|
}
|
|
782
|
+
// ==================== 键鼠输入 ====================
|
|
783
|
+
// stealth 模式:使用 JS 事件模拟,不触发调试提示,推荐用于反检测场景
|
|
784
|
+
// precise 模式:使用 debugger API,精确但会显示"扩展程序正在调试此浏览器"
|
|
856
785
|
/**
|
|
857
786
|
* 鼠标滚轮
|
|
858
787
|
*/
|
|
859
788
|
async mouseWheel(deltaX, deltaY) {
|
|
860
789
|
if (await this.ensureExtensionConnected()) {
|
|
861
790
|
const { x, y } = this.currentMousePosition;
|
|
862
|
-
await this.extensionBridge.inputMouse('mouseWheel', x, y, { deltaX, deltaY });
|
|
791
|
+
await this.extensionBridge.inputMouse('mouseWheel', x, y, { deltaX, deltaY, modifiers: this.modifiers });
|
|
863
792
|
return;
|
|
864
793
|
}
|
|
865
794
|
await getCdpSession().mouseWheel(deltaX, deltaY);
|
|
866
795
|
}
|
|
867
|
-
/**
|
|
868
|
-
* 点击(stealth 模式专用)
|
|
869
|
-
*/
|
|
870
|
-
async clickAt(x, y, button = 'left') {
|
|
871
|
-
if (await this.ensureExtensionConnected()) {
|
|
872
|
-
if (this.inputMode === 'stealth') {
|
|
873
|
-
await this.extensionBridge.stealthClick(x, y, button);
|
|
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
|
-
}
|
|
879
|
-
return;
|
|
880
|
-
}
|
|
881
|
-
await getCdpSession().mouseMove(x, y);
|
|
882
|
-
await getCdpSession().mouseDown(button);
|
|
883
|
-
await getCdpSession().mouseUp(button);
|
|
884
|
-
}
|
|
885
796
|
/**
|
|
886
797
|
* 注入反检测脚本
|
|
887
798
|
*/
|
|
@@ -922,7 +833,6 @@ class UnifiedSessionManager {
|
|
|
922
833
|
}
|
|
923
834
|
await getCdpSession().touchEnd();
|
|
924
835
|
}
|
|
925
|
-
// ==================== 控制台日志 ====================
|
|
926
836
|
/**
|
|
927
837
|
* 启用控制台日志捕获
|
|
928
838
|
*/
|
|
@@ -943,16 +853,6 @@ class UnifiedSessionManager {
|
|
|
943
853
|
// CDP 模式需要单独实现
|
|
944
854
|
return [];
|
|
945
855
|
}
|
|
946
|
-
/**
|
|
947
|
-
* 清除控制台日志
|
|
948
|
-
*/
|
|
949
|
-
async clearConsoleLogs() {
|
|
950
|
-
if (await this.ensureExtensionConnected()) {
|
|
951
|
-
await this.extensionBridge.consoleClear();
|
|
952
|
-
return;
|
|
953
|
-
}
|
|
954
|
-
}
|
|
955
|
-
// ==================== 网络日志 ====================
|
|
956
856
|
/**
|
|
957
857
|
* 启用网络日志捕获
|
|
958
858
|
*/
|
|
@@ -972,16 +872,6 @@ class UnifiedSessionManager {
|
|
|
972
872
|
}
|
|
973
873
|
return [];
|
|
974
874
|
}
|
|
975
|
-
/**
|
|
976
|
-
* 清除网络日志
|
|
977
|
-
*/
|
|
978
|
-
async clearNetworkLogs() {
|
|
979
|
-
if (await this.ensureExtensionConnected()) {
|
|
980
|
-
await this.extensionBridge.networkClear();
|
|
981
|
-
return;
|
|
982
|
-
}
|
|
983
|
-
}
|
|
984
|
-
// ==================== Debugger 直接访问 ====================
|
|
985
875
|
/**
|
|
986
876
|
* 发送 CDP 命令(高级用法)
|
|
987
877
|
*
|
|
@@ -1000,6 +890,150 @@ class UnifiedSessionManager {
|
|
|
1000
890
|
}
|
|
1001
891
|
return getCdpSession().send(method, params);
|
|
1002
892
|
}
|
|
893
|
+
/** 获取当前修饰键名称数组(stealth 模式用) */
|
|
894
|
+
getModifierNames() {
|
|
895
|
+
const names = [];
|
|
896
|
+
if (this.modifiers & 1) {
|
|
897
|
+
names.push('alt');
|
|
898
|
+
}
|
|
899
|
+
if (this.modifiers & 2) {
|
|
900
|
+
names.push('ctrl');
|
|
901
|
+
}
|
|
902
|
+
if (this.modifiers & 4) {
|
|
903
|
+
names.push('meta');
|
|
904
|
+
}
|
|
905
|
+
if (this.modifiers & 8) {
|
|
906
|
+
names.push('shift');
|
|
907
|
+
}
|
|
908
|
+
return names;
|
|
909
|
+
}
|
|
910
|
+
// ==================== 控制台日志 ====================
|
|
911
|
+
/**
|
|
912
|
+
* 检查 CDP 回退是否允许
|
|
913
|
+
*
|
|
914
|
+
* 当 requireExtension 为 true(tabId 或 frame 已指定)时,CDP 回退会操作错误目标,必须阻止。
|
|
915
|
+
* 允许时返回 false(供 ensureExtensionConnected 直接返回),不允许时抛出。
|
|
916
|
+
*/
|
|
917
|
+
assertCdpFallbackAllowed() {
|
|
918
|
+
if (this.requireExtension) {
|
|
919
|
+
throw new Error('Extension 已断开,当前操作需要 Extension(指定 tabId 或 frame)时不可回退 CDP(操作目标不一致)');
|
|
920
|
+
}
|
|
921
|
+
return false;
|
|
922
|
+
}
|
|
923
|
+
/**
|
|
924
|
+
* 确保 Extension 已连接,如果断开则等待重连
|
|
925
|
+
* 返回 true 表示 Extension 可用,false 表示应 fallback 到 CDP
|
|
926
|
+
*
|
|
927
|
+
* 设计理念:Server 和 Extension 的启动时机完全独立,无任何要求。
|
|
928
|
+
* - 先装 Extension,一个月/一年后启动 Server → 能连上
|
|
929
|
+
* - 先启动 Server,再打开 Chrome → 能连上
|
|
930
|
+
* - 关闭再打开任何一方 → 能自动重连
|
|
931
|
+
*
|
|
932
|
+
* 超时设为 30 秒:足够等待 Extension 启动,但不会永远卡住。
|
|
933
|
+
*
|
|
934
|
+
* @param maxWait 调用方的端到端预算(毫秒)。传入时取 min(maxWait, 30000) 作为连接等待上限,
|
|
935
|
+
* 避免工具 timeout 被连接等待吞掉。不传则使用默认 30s。
|
|
936
|
+
*/
|
|
937
|
+
async ensureExtensionConnected(maxWait) {
|
|
938
|
+
if (!this.extensionBridge) {
|
|
939
|
+
return this.assertCdpFallbackAllowed();
|
|
940
|
+
}
|
|
941
|
+
if (this.extensionBridge.isConnected()) {
|
|
942
|
+
return true;
|
|
943
|
+
}
|
|
944
|
+
// CDP 已连接时跳过 Extension 等待,直接使用 CDP 回退
|
|
945
|
+
if (getCdpSession().isConnected()) {
|
|
946
|
+
return this.assertCdpFallbackAllowed();
|
|
947
|
+
}
|
|
948
|
+
// 冷却期内不重复等待,避免每次操作都阻塞 30 秒
|
|
949
|
+
if (Date.now() - this.lastConnectionFailure < UnifiedSessionManager.CONNECTION_COOLDOWN) {
|
|
950
|
+
return this.assertCdpFallbackAllowed();
|
|
951
|
+
}
|
|
952
|
+
// Extension 服务器已启动但断开连接,等待重连
|
|
953
|
+
const waitTimeout = maxWait !== undefined ? Math.min(maxWait, 30000) : 30000;
|
|
954
|
+
if (waitTimeout <= 0) {
|
|
955
|
+
return this.assertCdpFallbackAllowed();
|
|
956
|
+
}
|
|
957
|
+
console.error(`[MCP] Waiting for Chrome Extension connection (${waitTimeout}ms timeout)...`);
|
|
958
|
+
console.error('[MCP] Please ensure Chrome is running with MCP Chrome extension installed.');
|
|
959
|
+
const connected = await this.extensionBridge.waitForConnection(waitTimeout);
|
|
960
|
+
if (connected) {
|
|
961
|
+
console.error('[MCP] Chrome Extension connected successfully');
|
|
962
|
+
this.lastConnectionFailure = 0;
|
|
963
|
+
return true;
|
|
964
|
+
}
|
|
965
|
+
console.error('[MCP] Chrome Extension connection timeout');
|
|
966
|
+
this.lastConnectionFailure = Date.now();
|
|
967
|
+
return this.assertCdpFallbackAllowed();
|
|
968
|
+
}
|
|
969
|
+
// ==================== 网络日志 ====================
|
|
970
|
+
/**
|
|
971
|
+
* 通过 callFunctionOn 执行函数调用
|
|
972
|
+
*
|
|
973
|
+
* 参数通过 CDP 协议结构化传递,避免大 payload 字符串拼接导致的长度限制和转义问题。
|
|
974
|
+
* 要求 code 必须是函数表达式(如 "(x) => x + 1")。
|
|
975
|
+
*/
|
|
976
|
+
async callFunctionOn(code, args, timeout) {
|
|
977
|
+
const globalResult = await this.extensionBridge.debuggerSend('Runtime.evaluate', {
|
|
978
|
+
expression: 'globalThis',
|
|
979
|
+
returnByValue: false,
|
|
980
|
+
}, undefined, timeout);
|
|
981
|
+
try {
|
|
982
|
+
const params = {
|
|
983
|
+
functionDeclaration: code,
|
|
984
|
+
objectId: globalResult.result.objectId,
|
|
985
|
+
arguments: args.map(a => ({ value: a })),
|
|
986
|
+
returnByValue: true,
|
|
987
|
+
awaitPromise: true,
|
|
988
|
+
};
|
|
989
|
+
if (timeout !== undefined) {
|
|
990
|
+
params.timeout = timeout;
|
|
991
|
+
}
|
|
992
|
+
const result = await this.extensionBridge.debuggerSend('Runtime.callFunctionOn', params, undefined, timeout);
|
|
993
|
+
if (result.exceptionDetails) {
|
|
994
|
+
throw new Error(result.exceptionDetails.text);
|
|
995
|
+
}
|
|
996
|
+
return result.result?.value;
|
|
997
|
+
}
|
|
998
|
+
finally {
|
|
999
|
+
this.extensionBridge.debuggerSend('Runtime.releaseObject', {
|
|
1000
|
+
objectId: globalResult.result.objectId,
|
|
1001
|
+
}).catch(() => {
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
/**
|
|
1006
|
+
* 串行化所有 tab 切换操作,防止并发请求互相覆盖 currentTabId。
|
|
1007
|
+
* 调用者:selectPage/activatePage/newPage/closePage/navigate/reload/launch/withTabId。
|
|
1008
|
+
*
|
|
1009
|
+
* 注意:此锁不可重入。fn() 内禁止调用任何使用 withTabLock 的方法,否则会死锁。
|
|
1010
|
+
* 当前所有 fn() 只调用 bridge 的原子操作(createTab/navigate/evaluate 等),不存在此问题。
|
|
1011
|
+
*/
|
|
1012
|
+
async withTabLock(fn) {
|
|
1013
|
+
const previousLock = this.tabSwitchLock;
|
|
1014
|
+
let releaseLock;
|
|
1015
|
+
this.tabSwitchLock = new Promise(resolve => {
|
|
1016
|
+
releaseLock = resolve;
|
|
1017
|
+
});
|
|
1018
|
+
try {
|
|
1019
|
+
await previousLock;
|
|
1020
|
+
return await fn();
|
|
1021
|
+
}
|
|
1022
|
+
finally {
|
|
1023
|
+
releaseLock();
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
// ==================== Debugger 直接访问 ====================
|
|
1027
|
+
/**
|
|
1028
|
+
* 解析 tab ID 字符串为数字,校验 NaN
|
|
1029
|
+
*/
|
|
1030
|
+
parseTabId(id) {
|
|
1031
|
+
const tabId = parseInt(id, 10);
|
|
1032
|
+
if (isNaN(tabId)) {
|
|
1033
|
+
throw new Error(`无效的 Tab ID: ${id}`);
|
|
1034
|
+
}
|
|
1035
|
+
return tabId;
|
|
1036
|
+
}
|
|
1003
1037
|
}
|
|
1004
1038
|
/**
|
|
1005
1039
|
* 获取统一会话管理器实例
|