@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.
Files changed (91) hide show
  1. package/README.md +130 -54
  2. package/dist/anti-detection/behavior.d.ts +0 -8
  3. package/dist/anti-detection/behavior.d.ts.map +1 -1
  4. package/dist/anti-detection/behavior.js +0 -16
  5. package/dist/anti-detection/behavior.js.map +1 -1
  6. package/dist/cdp/client.d.ts +0 -2
  7. package/dist/cdp/client.d.ts.map +1 -1
  8. package/dist/cdp/client.js +30 -45
  9. package/dist/cdp/client.js.map +1 -1
  10. package/dist/cdp/launcher.d.ts +1 -8
  11. package/dist/cdp/launcher.d.ts.map +1 -1
  12. package/dist/cdp/launcher.js +11 -21
  13. package/dist/cdp/launcher.js.map +1 -1
  14. package/dist/core/auto-wait.d.ts +2 -2
  15. package/dist/core/auto-wait.d.ts.map +1 -1
  16. package/dist/core/auto-wait.js +1 -1
  17. package/dist/core/auto-wait.js.map +1 -1
  18. package/dist/core/errors.d.ts +10 -13
  19. package/dist/core/errors.d.ts.map +1 -1
  20. package/dist/core/errors.js +19 -25
  21. package/dist/core/errors.js.map +1 -1
  22. package/dist/core/locator.d.ts +6 -7
  23. package/dist/core/locator.d.ts.map +1 -1
  24. package/dist/core/locator.js +77 -31
  25. package/dist/core/locator.js.map +1 -1
  26. package/dist/core/retry.d.ts.map +1 -1
  27. package/dist/core/retry.js +1 -1
  28. package/dist/core/retry.js.map +1 -1
  29. package/dist/core/session.d.ts +37 -33
  30. package/dist/core/session.d.ts.map +1 -1
  31. package/dist/core/session.js +159 -116
  32. package/dist/core/session.js.map +1 -1
  33. package/dist/core/types.d.ts +25 -1
  34. package/dist/core/types.d.ts.map +1 -1
  35. package/dist/core/types.js +20 -0
  36. package/dist/core/types.js.map +1 -1
  37. package/dist/core/unified-session.d.ts +105 -67
  38. package/dist/core/unified-session.d.ts.map +1 -1
  39. package/dist/core/unified-session.js +347 -186
  40. package/dist/core/unified-session.js.map +1 -1
  41. package/dist/extension/bridge.d.ts +25 -19
  42. package/dist/extension/bridge.d.ts.map +1 -1
  43. package/dist/extension/bridge.js +29 -52
  44. package/dist/extension/bridge.js.map +1 -1
  45. package/dist/extension/http-server.d.ts +13 -11
  46. package/dist/extension/http-server.d.ts.map +1 -1
  47. package/dist/extension/http-server.js +101 -95
  48. package/dist/extension/http-server.js.map +1 -1
  49. package/dist/index.js +18 -64
  50. package/dist/index.js.map +1 -1
  51. package/dist/tools/browse.d.ts +3 -80
  52. package/dist/tools/browse.d.ts.map +1 -1
  53. package/dist/tools/browse.js +135 -291
  54. package/dist/tools/browse.js.map +1 -1
  55. package/dist/tools/cookies.d.ts +3 -71
  56. package/dist/tools/cookies.d.ts.map +1 -1
  57. package/dist/tools/cookies.js +75 -157
  58. package/dist/tools/cookies.js.map +1 -1
  59. package/dist/tools/evaluate.d.ts +3 -52
  60. package/dist/tools/evaluate.d.ts.map +1 -1
  61. package/dist/tools/evaluate.js +35 -86
  62. package/dist/tools/evaluate.js.map +1 -1
  63. package/dist/tools/extract.d.ts +5 -227
  64. package/dist/tools/extract.d.ts.map +1 -1
  65. package/dist/tools/extract.js +586 -184
  66. package/dist/tools/extract.js.map +1 -1
  67. package/dist/tools/index.d.ts +9 -9
  68. package/dist/tools/index.d.ts.map +1 -1
  69. package/dist/tools/index.js +9 -9
  70. package/dist/tools/index.js.map +1 -1
  71. package/dist/tools/input.d.ts +3 -258
  72. package/dist/tools/input.d.ts.map +1 -1
  73. package/dist/tools/input.js +95 -147
  74. package/dist/tools/input.js.map +1 -1
  75. package/dist/tools/logs.d.ts +3 -51
  76. package/dist/tools/logs.d.ts.map +1 -1
  77. package/dist/tools/logs.js +47 -108
  78. package/dist/tools/logs.js.map +1 -1
  79. package/dist/tools/manage.d.ts +3 -64
  80. package/dist/tools/manage.d.ts.map +1 -1
  81. package/dist/tools/manage.js +243 -373
  82. package/dist/tools/manage.js.map +1 -1
  83. package/dist/tools/schema.d.ts +16 -182
  84. package/dist/tools/schema.d.ts.map +1 -1
  85. package/dist/tools/schema.js +70 -159
  86. package/dist/tools/schema.js.map +1 -1
  87. package/dist/tools/wait.d.ts +3 -221
  88. package/dist/tools/wait.d.ts.map +1 -1
  89. package/dist/tools/wait.js +74 -145
  90. package/dist/tools/wait.js.map +1 -1
  91. 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
- constructor() { }
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 必须是函数表达式,会被包装为 IIFE:(script)(arg1, arg2, ...)
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
- // 检测裸 return 语句,自动包裹 IIFE
358
- // 排除已经是函数表达式或 IIFE 的情况
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
- if (args && args.length > 0) {
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(code, args, timeout);
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(code, args, timeout);
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 上下文:通过 evaluateInFrame 使用 contextId 精确定位
385
- const result = await this.extensionBridge.evaluateInFrame(currentFrameId, expression, timeout);
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.text);
330
+ throw new Error(formatCdpException(result.exceptionDetails));
388
331
  }
389
- return result.result?.value;
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.text);
346
+ throw new Error(formatCdpException(result.exceptionDetails));
404
347
  }
405
- return result.result?.value;
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
- if (filter.secure !== undefined && c.secure !== filter.secure)
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, { button: effectiveButton, clickCount: 1 });
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 mouseWheel(deltaX, deltaY) {
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.inputMouse('mouseWheel', x, y, { deltaX, deltaY });
903
+ await this.extensionBridge.stealthClick(x, y, effectiveButton);
863
904
  return;
864
905
  }
865
- await getCdpSession().mouseWheel(deltaX, deltaY);
906
+ await this.mouseDown(button);
907
+ await this.mouseUp(button);
866
908
  }
909
+ // ==================== 键鼠输入 ====================
910
+ // stealth 模式:使用 JS 事件模拟,不触发调试提示,推荐用于反检测场景
911
+ // precise 模式:使用 debugger API,精确但会显示"扩展程序正在调试此浏览器"
867
912
  /**
868
- * 点击(stealth 模式专用)
913
+ * 鼠标滚轮
869
914
  */
870
- async clickAt(x, y, button = 'left') {
915
+ async mouseWheel(deltaX, deltaY) {
871
916
  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
- }
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().mouseMove(x, y);
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
  * 获取统一会话管理器实例