@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.
Files changed (91) hide show
  1. package/README.md +103 -53
  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 +4 -20
  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 +32 -33
  30. package/dist/core/session.d.ts.map +1 -1
  31. package/dist/core/session.js +154 -114
  32. package/dist/core/session.js.map +1 -1
  33. package/dist/core/types.d.ts +4 -0
  34. package/dist/core/types.d.ts.map +1 -1
  35. package/dist/core/types.js +6 -0
  36. package/dist/core/types.js.map +1 -1
  37. package/dist/core/unified-session.d.ts +54 -67
  38. package/dist/core/unified-session.d.ts.map +1 -1
  39. package/dist/core/unified-session.js +215 -181
  40. package/dist/core/unified-session.js.map +1 -1
  41. package/dist/extension/bridge.d.ts +0 -19
  42. package/dist/extension/bridge.d.ts.map +1 -1
  43. package/dist/extension/bridge.js +6 -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 +11 -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 +3 -226
  64. package/dist/tools/extract.d.ts.map +1 -1
  65. package/dist/tools/extract.js +98 -170
  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 +56 -143
  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,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 必须是函数表达式,会被包装为 IIFE:(script)(arg1, arg2, ...)
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
- // 检测裸 return 语句,自动包裹 IIFE
358
- // 排除已经是函数表达式或 IIFE 的情况
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
- if (args && args.length > 0) {
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 上下文:通过 evaluateInFrame 使用 contextId 精确定位
385
- const result = await this.extensionBridge.evaluateInFrame(currentFrameId, expression, timeout);
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
- if (filter.secure !== undefined && c.secure !== filter.secure)
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, { button: effectiveButton, clickCount: 1 });
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
  * 获取统一会话管理器实例