@pyrokine/mcp-chrome 2.0.2 → 2.2.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 (80) hide show
  1. package/README.md +86 -35
  2. package/README_zh.md +570 -0
  3. package/dist/cdp/launcher.d.ts.map +1 -1
  4. package/dist/cdp/launcher.js +41 -19
  5. package/dist/cdp/launcher.js.map +1 -1
  6. package/dist/core/browser-driver.d.ts +118 -1
  7. package/dist/core/browser-driver.d.ts.map +1 -1
  8. package/dist/core/browser-driver.js +11 -0
  9. package/dist/core/browser-driver.js.map +1 -1
  10. package/dist/core/error-sanitizer.js +2 -2
  11. package/dist/core/error-sanitizer.js.map +1 -1
  12. package/dist/core/errors.d.ts.map +1 -1
  13. package/dist/core/errors.js +17 -4
  14. package/dist/core/errors.js.map +1 -1
  15. package/dist/core/index.d.ts +1 -1
  16. package/dist/core/index.d.ts.map +1 -1
  17. package/dist/core/index.js +1 -1
  18. package/dist/core/index.js.map +1 -1
  19. package/dist/core/locator.d.ts.map +1 -1
  20. package/dist/core/locator.js +4 -3
  21. package/dist/core/locator.js.map +1 -1
  22. package/dist/core/session.d.ts +5 -2
  23. package/dist/core/session.d.ts.map +1 -1
  24. package/dist/core/session.js +107 -4
  25. package/dist/core/session.js.map +1 -1
  26. package/dist/core/types.d.ts +24 -1
  27. package/dist/core/types.d.ts.map +1 -1
  28. package/dist/core/types.js +77 -4
  29. package/dist/core/types.js.map +1 -1
  30. package/dist/core/unified-session.d.ts +38 -9
  31. package/dist/core/unified-session.d.ts.map +1 -1
  32. package/dist/core/unified-session.js +291 -18
  33. package/dist/core/unified-session.js.map +1 -1
  34. package/dist/core/utils.d.ts +1 -0
  35. package/dist/core/utils.d.ts.map +1 -1
  36. package/dist/core/utils.js +35 -7
  37. package/dist/core/utils.js.map +1 -1
  38. package/dist/extension/bridge.d.ts +37 -22
  39. package/dist/extension/bridge.d.ts.map +1 -1
  40. package/dist/extension/bridge.js +223 -63
  41. package/dist/extension/bridge.js.map +1 -1
  42. package/dist/extension/http-server.d.ts +6 -0
  43. package/dist/extension/http-server.d.ts.map +1 -1
  44. package/dist/extension/http-server.js +125 -14
  45. package/dist/extension/http-server.js.map +1 -1
  46. package/dist/tools/browse.d.ts.map +1 -1
  47. package/dist/tools/browse.js +153 -54
  48. package/dist/tools/browse.js.map +1 -1
  49. package/dist/tools/cookies.d.ts.map +1 -1
  50. package/dist/tools/cookies.js +2 -4
  51. package/dist/tools/cookies.js.map +1 -1
  52. package/dist/tools/evaluate.d.ts.map +1 -1
  53. package/dist/tools/evaluate.js +37 -10
  54. package/dist/tools/evaluate.js.map +1 -1
  55. package/dist/tools/extract.d.ts +1 -0
  56. package/dist/tools/extract.d.ts.map +1 -1
  57. package/dist/tools/extract.js +466 -227
  58. package/dist/tools/extract.js.map +1 -1
  59. package/dist/tools/input.d.ts.map +1 -1
  60. package/dist/tools/input.js +781 -444
  61. package/dist/tools/input.js.map +1 -1
  62. package/dist/tools/logs.d.ts.map +1 -1
  63. package/dist/tools/logs.js +75 -23
  64. package/dist/tools/logs.js.map +1 -1
  65. package/dist/tools/manage.d.ts +0 -3
  66. package/dist/tools/manage.d.ts.map +1 -1
  67. package/dist/tools/manage.js +339 -285
  68. package/dist/tools/manage.js.map +1 -1
  69. package/dist/tools/png.d.ts +16 -0
  70. package/dist/tools/png.d.ts.map +1 -0
  71. package/dist/tools/png.js +197 -0
  72. package/dist/tools/png.js.map +1 -0
  73. package/dist/tools/schema.d.ts +14 -112
  74. package/dist/tools/schema.d.ts.map +1 -1
  75. package/dist/tools/schema.js +21 -12
  76. package/dist/tools/schema.js.map +1 -1
  77. package/dist/tools/wait.d.ts.map +1 -1
  78. package/dist/tools/wait.js +73 -13
  79. package/dist/tools/wait.js.map +1 -1
  80. package/package.json +16 -13
@@ -6,12 +6,16 @@
6
6
  * 2. CDP 模式:通过 Chrome DevTools Protocol 操作(Fallback)
7
7
  */
8
8
  import { ExtensionBridge } from '../extension/index.js';
9
+ import { DriverCapabilityError, } from './browser-driver.js';
9
10
  import { isExtensionDisconnected } from './extension-errors.js';
10
11
  import { getKeyDefinition, getSession as getCdpSession } from './session.js';
11
- import { extractCdpValue, formatCdpException, MODIFIER_KEYS } from './types.js';
12
+ import { EvaluateResultTooLargeError, extractCdpValue, formatCdpException, isKnownNonSerializableRemoteObject, MODIFIER_KEYS, NonSerializableEvaluateResultError, } from './types.js';
12
13
  class UnifiedSessionManager {
13
14
  static instance;
14
15
  static CONNECTION_COOLDOWN = 30000; // 连接失败后 30 秒内不重试
16
+ static CDP_MATERIALIZE_MAX_DEPTH = 8;
17
+ static CDP_MATERIALIZE_MAX_NODES = 2000;
18
+ static CDP_MATERIALIZE_MAX_CHARS = 1_000_000;
15
19
  extensionBridge = null;
16
20
  inputMode = 'precise'; // 默认使用 precise 模式,可绕过 CSP 限制
17
21
  currentMousePosition = { x: 0, y: 0 }; // 跟踪鼠标位置
@@ -20,6 +24,7 @@ class UnifiedSessionManager {
20
24
  /** 当前按下的所有键(用于 Puppeteer 风格的 rawKeyDown/autoRepeat 长按重复) */
21
25
  pressedKeys = new Set();
22
26
  lastConnectionFailure = 0;
27
+ activeMode = null;
23
28
  tabSwitchLock = Promise.resolve(); // 串行化 tab 切换,防止并发竞态
24
29
  requireExtension = false; // 指定 tabId 或 frame 时为 true,禁止 CDP 回退
25
30
  currentFrameOffset = null; // iframe 在主页面的偏移量(withFrame 期间有效)
@@ -51,14 +56,26 @@ class UnifiedSessionManager {
51
56
  * 获取当前连接模式
52
57
  */
53
58
  getMode() {
59
+ if (this.activeMode === 'extension' && this.extensionBridge?.isConnected()) {
60
+ return 'extension';
61
+ }
62
+ if (this.activeMode === 'cdp' && getCdpSession().isConnected()) {
63
+ return 'cdp';
64
+ }
65
+ this.activeMode = null;
54
66
  if (this.extensionBridge?.isConnected()) {
67
+ this.activeMode = 'extension';
55
68
  return 'extension';
56
69
  }
57
70
  if (getCdpSession().isConnected()) {
71
+ this.activeMode = 'cdp';
58
72
  return 'cdp';
59
73
  }
60
74
  return 'none';
61
75
  }
76
+ setActiveMode(mode) {
77
+ this.activeMode = mode;
78
+ }
62
79
  /**
63
80
  * 获取当前输入模式
64
81
  */
@@ -90,6 +107,9 @@ class UnifiedSessionManager {
90
107
  * 是否 Extension 已连接
91
108
  */
92
109
  isExtensionConnected() {
110
+ return this.activeMode === 'extension' && (this.extensionBridge?.isConnected() ?? false);
111
+ }
112
+ isExtensionAvailable() {
93
113
  return this.extensionBridge?.isConnected() ?? false;
94
114
  }
95
115
  /**
@@ -103,22 +123,28 @@ class UnifiedSessionManager {
103
123
  * 启动浏览器(CDP 模式)或等待 Extension 连接
104
124
  */
105
125
  async launch(options = {}) {
126
+ const extensionReady = this.activeMode === 'cdp'
127
+ ? (this.extensionBridge?.isConnected() ?? false)
128
+ : await this.ensureExtensionConnected(options.timeout);
106
129
  // 优先检查 Extension 是否已连接,如果断开则等待重连(受 timeout 约束)
107
- if (await this.ensureExtensionConnected(options.timeout)) {
130
+ if (extensionReady) {
108
131
  // newPage 会设置 currentTabId,需要加锁
109
132
  const result = await this.withTabLock(async () => {
110
133
  return this.extensionBridge.newPage(undefined, options.timeout);
111
134
  });
135
+ this.activeMode = 'extension';
112
136
  return {
113
137
  targetId: result.targetId,
114
138
  type: result.type ?? 'page',
115
139
  url: result.url,
116
140
  title: result.title,
141
+ managed: result.managed,
117
142
  mode: 'extension',
118
143
  };
119
144
  }
120
145
  // Fallback 到 CDP 模式
121
146
  const target = await getCdpSession().launch(options);
147
+ this.activeMode = 'cdp';
122
148
  return {
123
149
  ...target,
124
150
  mode: 'cdp',
@@ -149,6 +175,88 @@ class UnifiedSessionManager {
149
175
  status: t.status,
150
176
  }));
151
177
  }
178
+ async listTopology() {
179
+ const mode = this.getMode();
180
+ if (mode === 'none') {
181
+ return null;
182
+ }
183
+ const driver = await this.getDriver();
184
+ if (!driver.listWindows) {
185
+ return null;
186
+ }
187
+ return driver.listWindows();
188
+ }
189
+ async adoptPage(targetId) {
190
+ const driver = await this.getDriver();
191
+ if (!driver.adoptPage) {
192
+ throw new DriverCapabilityError('adoptPage 仅支持 Extension 模式');
193
+ }
194
+ return driver.adoptPage(targetId);
195
+ }
196
+ async releasePage(targetId) {
197
+ const driver = await this.getDriver();
198
+ if (!driver.releasePage) {
199
+ throw new DriverCapabilityError('releasePage 仅支持 Extension 模式');
200
+ }
201
+ return driver.releasePage(targetId);
202
+ }
203
+ async movePage(targetId, options) {
204
+ const driver = await this.getDriver();
205
+ if (!driver.movePage) {
206
+ throw new DriverCapabilityError('movePage 仅支持 Extension 模式');
207
+ }
208
+ return driver.movePage(targetId, options);
209
+ }
210
+ async reorderPage(targetId, index) {
211
+ const driver = await this.getDriver();
212
+ if (!driver.reorderPage) {
213
+ throw new DriverCapabilityError('reorderPage 仅支持 Extension 模式');
214
+ }
215
+ return driver.reorderPage(targetId, index);
216
+ }
217
+ async pinPage(targetId, pinned) {
218
+ const driver = await this.getDriver();
219
+ if (!driver.pinPage) {
220
+ throw new DriverCapabilityError('pinPage 仅支持 Extension 模式');
221
+ }
222
+ return driver.pinPage(targetId, pinned);
223
+ }
224
+ async activatePageWithAffected(targetId) {
225
+ const driver = await this.getDriver();
226
+ const bridge = driver;
227
+ if (!bridge.activatePageWithAffected) {
228
+ throw new DriverCapabilityError('activatePage 受控 affected 返回仅支持 Extension 模式');
229
+ }
230
+ return this.withTabLock(() => bridge.activatePageWithAffected(targetId));
231
+ }
232
+ async focusWindow(windowId) {
233
+ const driver = await this.getDriver();
234
+ if (!driver.focusWindow) {
235
+ throw new DriverCapabilityError('focusWindow 仅支持 Extension 模式');
236
+ }
237
+ return driver.focusWindow(windowId);
238
+ }
239
+ async resizeWindow(windowId, options) {
240
+ const driver = await this.getDriver();
241
+ if (!driver.resizeWindow) {
242
+ throw new DriverCapabilityError('resizeWindow 仅支持 Extension 模式');
243
+ }
244
+ return driver.resizeWindow(windowId, options);
245
+ }
246
+ async newWindow(options) {
247
+ const driver = await this.getDriver();
248
+ if (!driver.newWindow) {
249
+ throw new DriverCapabilityError('newWindow 仅支持 Extension 模式');
250
+ }
251
+ return this.withTabLock(() => driver.newWindow(options));
252
+ }
253
+ async closeWindow(windowId) {
254
+ const driver = await this.getDriver();
255
+ if (!driver.closeWindow) {
256
+ throw new DriverCapabilityError('closeWindow 仅支持 Extension 模式');
257
+ }
258
+ return this.withTabLock(() => driver.closeWindow(windowId));
259
+ }
152
260
  /**
153
261
  * 导航到 URL
154
262
  */
@@ -181,8 +289,7 @@ class UnifiedSessionManager {
181
289
  * 截图
182
290
  */
183
291
  async screenshot(options) {
184
- const result = await (await this.getDriver()).screenshot(options);
185
- return result.data;
292
+ return (await this.getDriver()).screenshot(options);
186
293
  }
187
294
  /**
188
295
  * 点击元素
@@ -200,6 +307,10 @@ class UnifiedSessionManager {
200
307
  const driver = await this.getDriver();
201
308
  return driver.actionableClick(refId, force);
202
309
  }
310
+ async checkActionability(refId) {
311
+ const driver = await this.getDriver();
312
+ return driver.checkActionability(refId);
313
+ }
203
314
  /**
204
315
  * dispatch 模式输入(ISOLATED 世界,兼容 React/Vue 受控组件)
205
316
  */
@@ -403,6 +514,13 @@ class UnifiedSessionManager {
403
514
  const driver = await this.getDriver();
404
515
  return driver.getAttribute(selector, refId, attribute);
405
516
  }
517
+ async getFrames() {
518
+ const driver = await this.getDriver();
519
+ if (!driver.getFrames) {
520
+ return { frames: [] };
521
+ }
522
+ return driver.getFrames();
523
+ }
406
524
  /**
407
525
  * 获取 Cookies
408
526
  */
@@ -432,6 +550,7 @@ class UnifiedSessionManager {
432
550
  type: result.type ?? 'page',
433
551
  url: result.url,
434
552
  title: result.title,
553
+ managed: result.managed,
435
554
  };
436
555
  };
437
556
  // Extension 模式 createTab 设置 currentTabId,需要加锁防止并发竞态
@@ -445,11 +564,9 @@ class UnifiedSessionManager {
445
564
  const isExt = this.isExtensionConnected();
446
565
  const op = () => driver.closePage(targetId);
447
566
  if (isExt) {
448
- await this.withTabLock(op);
449
- }
450
- else {
451
- await op();
567
+ return this.withTabLock(op);
452
568
  }
569
+ return op();
453
570
  }
454
571
  /**
455
572
  * 激活页面(切到前台)
@@ -510,15 +627,19 @@ class UnifiedSessionManager {
510
627
  const driver = this.extensionBridge;
511
628
  return this.withTabLock(async () => {
512
629
  const previousTargetId = driver.getCurrentTargetId();
630
+ // 临时切换用 setCurrentTargetId,跳过 managed 校验(tabId 仅用于读取/执行,不改变持久 attach 目标)
513
631
  driver.setCurrentTargetId(tabId);
514
632
  // tabId 明确指定时,禁止 CDP 回退(CDP 不感知 Extension tab)
515
633
  const previousRequireExtension = this.requireExtension;
634
+ const previousActiveMode = this.activeMode;
516
635
  this.requireExtension = true;
636
+ this.activeMode = 'extension';
517
637
  try {
518
638
  return await fn();
519
639
  }
520
640
  finally {
521
641
  this.requireExtension = previousRequireExtension;
642
+ this.activeMode = previousActiveMode;
522
643
  driver.setCurrentTargetId(previousTargetId);
523
644
  }
524
645
  });
@@ -544,15 +665,18 @@ class UnifiedSessionManager {
544
665
  const previousFrameId = driver.getCurrentFrameId();
545
666
  const previousFrameOffset = this.currentFrameOffset;
546
667
  const previousRequireExtension = this.requireExtension;
668
+ const previousActiveMode = this.activeMode;
547
669
  driver.setCurrentFrameId(frameId);
548
670
  this.currentFrameOffset = offset;
549
671
  this.requireExtension = true;
672
+ this.activeMode = 'extension';
550
673
  try {
551
674
  return await fn();
552
675
  }
553
676
  finally {
554
677
  // 字段赋值不会 throw,driver.setCurrentFrameId 是同步函数也不会 throw,无需嵌套 try
555
678
  this.requireExtension = previousRequireExtension;
679
+ this.activeMode = previousActiveMode;
556
680
  this.currentFrameOffset = previousFrameOffset;
557
681
  driver.setCurrentFrameId(previousFrameId);
558
682
  }
@@ -562,7 +686,7 @@ class UnifiedSessionManager {
562
686
  */
563
687
  getState() {
564
688
  // driver.getState() 两侧都返回 DriverState | null({url, title}),SessionState 含 targetId 是其超集,结构兼容
565
- if (this.extensionBridge?.isConnected()) {
689
+ if (this.getMode() === 'extension') {
566
690
  return this.extensionBridge.getState();
567
691
  }
568
692
  return getCdpSession().getState();
@@ -702,7 +826,7 @@ class UnifiedSessionManager {
702
826
  async mouseClick(button = 'left', clickCount = 1, refId) {
703
827
  const driver = await this.getDriver();
704
828
  const isExt = this.isExtensionConnected();
705
- if (this.inputMode === 'stealth' && isExt) {
829
+ if (isExt && (this.inputMode === 'stealth' || button === 'right')) {
706
830
  const { x, y } = this.currentMousePosition;
707
831
  // stealth 通过单次脚本注入完成所有 mouse/click/dblclick/contextmenu 事件
708
832
  // refId 透传:嵌套 iframe overlay 场景下绕过 elementFromPoint 命中外层 IFRAME 的问题
@@ -804,18 +928,19 @@ class UnifiedSessionManager {
804
928
  const result = (await this.extensionBridge.evaluateInFrame(currentFrameId, iframeExpression, timeout));
805
929
  return this.checkCdpResult(result);
806
930
  }
807
- // 主 frame,无 args:直接 Runtime.evaluate
808
931
  const params = {
809
932
  expression,
810
- returnByValue: true,
933
+ returnByValue: false,
811
934
  awaitPromise: true,
812
935
  };
813
936
  if (timeout !== undefined) {
814
937
  params.timeout = timeout;
815
938
  }
816
- // timeout 即端到端预算,直接作为 RPC 超时(不额外加 margin)
817
939
  const result = (await this.extensionBridge.debuggerSend('Runtime.evaluate', params, undefined, timeout));
818
- return this.checkCdpResult(result);
940
+ if (result.exceptionDetails) {
941
+ throw new Error(formatCdpException(result.exceptionDetails));
942
+ }
943
+ return this.materializeCdpResult(result.result, timeout);
819
944
  }
820
945
  async evaluateViaExtensionStealth(expression, timeout) {
821
946
  return (await this.extensionBridge.evaluate(expression, undefined, timeout));
@@ -873,6 +998,126 @@ class UnifiedSessionManager {
873
998
  }
874
999
  return extractCdpValue(result.result);
875
1000
  }
1001
+ async materializeCdpResult(result, timeout) {
1002
+ if (!result?.objectId) {
1003
+ return extractCdpValue(result);
1004
+ }
1005
+ try {
1006
+ return await this.materializeRemoteObject(result, timeout, { seen: new Set(), nodes: 0, chars: 0 }, 0);
1007
+ }
1008
+ finally {
1009
+ this.extensionBridge.debuggerSend('Runtime.releaseObject', { objectId: result.objectId }).catch(() => { });
1010
+ }
1011
+ }
1012
+ async materializeRemoteObject(result, timeout, state, depth) {
1013
+ if (!result.objectId) {
1014
+ return this.trackMaterializedValue(extractCdpValue(result), state, depth);
1015
+ }
1016
+ if (isKnownNonSerializableRemoteObject(result) || state.seen.has(result.objectId)) {
1017
+ throw new NonSerializableEvaluateResultError(result);
1018
+ }
1019
+ this.trackMaterializedNode(result, state, depth);
1020
+ state.seen.add(result.objectId);
1021
+ try {
1022
+ const properties = (await this.extensionBridge.debuggerSend('Runtime.getProperties', {
1023
+ objectId: result.objectId,
1024
+ ownProperties: true,
1025
+ accessorPropertiesOnly: false,
1026
+ generatePreview: false,
1027
+ }, undefined, timeout));
1028
+ if (properties.exceptionDetails) {
1029
+ throw new Error(formatCdpException(properties.exceptionDetails));
1030
+ }
1031
+ const descriptors = properties.result ?? [];
1032
+ state.nodes += descriptors.length;
1033
+ this.assertMaterializeBudget(state, depth, result);
1034
+ if (result.subtype === 'array') {
1035
+ const array = [];
1036
+ for (const descriptor of descriptors) {
1037
+ if (!descriptor.value || !/^\d+$/.test(descriptor.name)) {
1038
+ continue;
1039
+ }
1040
+ const index = Number(descriptor.name);
1041
+ if (index >= UnifiedSessionManager.CDP_MATERIALIZE_MAX_NODES) {
1042
+ this.throwMaterializeLimit(state, depth, result);
1043
+ }
1044
+ this.trackMaterializedPropertyName(descriptor.name, state, depth, result);
1045
+ array[index] = await this.materializePropertyValue(descriptor.value, timeout, state, depth + 1);
1046
+ }
1047
+ return array;
1048
+ }
1049
+ const object = {};
1050
+ for (const descriptor of descriptors) {
1051
+ if (!descriptor.enumerable || descriptor.wasThrown || !descriptor.value) {
1052
+ continue;
1053
+ }
1054
+ this.trackMaterializedPropertyName(descriptor.name, state, depth, result);
1055
+ object[descriptor.name] = await this.materializePropertyValue(descriptor.value, timeout, state, depth + 1);
1056
+ }
1057
+ return object;
1058
+ }
1059
+ finally {
1060
+ state.seen.delete(result.objectId);
1061
+ }
1062
+ }
1063
+ async materializePropertyValue(value, timeout, state, depth) {
1064
+ if (!value.objectId) {
1065
+ return this.trackMaterializedValue(extractCdpValue(value), state, depth);
1066
+ }
1067
+ try {
1068
+ return await this.materializeRemoteObject(value, timeout, state, depth);
1069
+ }
1070
+ finally {
1071
+ this.extensionBridge.debuggerSend('Runtime.releaseObject', { objectId: value.objectId }).catch(() => { });
1072
+ }
1073
+ }
1074
+ trackMaterializedNode(result, state, depth) {
1075
+ state.nodes += 1;
1076
+ state.chars += result.description?.length ?? 0;
1077
+ this.assertMaterializeBudget(state, depth, result);
1078
+ }
1079
+ trackMaterializedPropertyName(name, state, depth, result) {
1080
+ state.chars += name.length;
1081
+ this.assertMaterializeBudget(state, depth, result);
1082
+ }
1083
+ trackMaterializedValue(value, state, depth) {
1084
+ state.nodes += 1;
1085
+ state.chars += this.estimateSerializedLength(value);
1086
+ this.assertMaterializeBudget(state, depth);
1087
+ return value;
1088
+ }
1089
+ estimateSerializedLength(value) {
1090
+ if (value === undefined) {
1091
+ return 'undefined'.length;
1092
+ }
1093
+ try {
1094
+ return JSON.stringify(value)?.length ?? String(value).length;
1095
+ }
1096
+ catch {
1097
+ return String(value).length;
1098
+ }
1099
+ }
1100
+ assertMaterializeBudget(state, depth, result) {
1101
+ if (depth > UnifiedSessionManager.CDP_MATERIALIZE_MAX_DEPTH ||
1102
+ state.nodes > UnifiedSessionManager.CDP_MATERIALIZE_MAX_NODES ||
1103
+ state.chars > UnifiedSessionManager.CDP_MATERIALIZE_MAX_CHARS) {
1104
+ this.throwMaterializeLimit(state, depth, result);
1105
+ }
1106
+ }
1107
+ throwMaterializeLimit(state, depth, result) {
1108
+ throw new EvaluateResultTooLargeError({
1109
+ depth,
1110
+ nodes: state.nodes,
1111
+ chars: state.chars,
1112
+ maxDepth: UnifiedSessionManager.CDP_MATERIALIZE_MAX_DEPTH,
1113
+ maxNodes: UnifiedSessionManager.CDP_MATERIALIZE_MAX_NODES,
1114
+ maxChars: UnifiedSessionManager.CDP_MATERIALIZE_MAX_CHARS,
1115
+ type: result?.type,
1116
+ subtype: result?.subtype,
1117
+ className: result?.className,
1118
+ description: result?.description?.slice(0, 200),
1119
+ });
1120
+ }
876
1121
  /**
877
1122
  * 检查 CDP 回退是否允许
878
1123
  *
@@ -900,20 +1145,31 @@ class UnifiedSessionManager {
900
1145
  * 避免工具 timeout 被连接等待吞掉,不传则使用默认 30s
901
1146
  */
902
1147
  async getDriver(timeout) {
1148
+ if (this.activeMode === 'cdp' && getCdpSession().isConnected()) {
1149
+ this.assertCdpFallbackAllowed();
1150
+ return getCdpSession();
1151
+ }
903
1152
  if (await this.ensureExtensionConnected(timeout)) {
1153
+ this.activeMode = 'extension';
904
1154
  return this.extensionBridge;
905
1155
  }
1156
+ this.activeMode = 'cdp';
906
1157
  return getCdpSession();
907
1158
  }
908
1159
  async ensureExtensionConnected(maxWait) {
909
1160
  if (!this.extensionBridge) {
910
1161
  return this.assertCdpFallbackAllowed();
911
1162
  }
1163
+ if (this.activeMode === 'cdp' && getCdpSession().isConnected()) {
1164
+ return this.assertCdpFallbackAllowed();
1165
+ }
912
1166
  if (this.extensionBridge.isConnected()) {
1167
+ this.activeMode = 'extension';
913
1168
  return true;
914
1169
  }
915
1170
  // CDP 已连接时跳过 Extension 等待,直接使用 CDP 回退
916
1171
  if (getCdpSession().isConnected()) {
1172
+ this.activeMode = 'cdp';
917
1173
  return this.assertCdpFallbackAllowed();
918
1174
  }
919
1175
  // 冷却期内不重复等待,避免每次操作都阻塞 30 秒
@@ -945,27 +1201,44 @@ class UnifiedSessionManager {
945
1201
  * 要求 code 必须是函数表达式(如 "(x) => x + 1")
946
1202
  */
947
1203
  async callFunctionOn(code, args, timeout) {
1204
+ const buildFallbackExpression = () => `(${code})(${args.map((a) => JSON.stringify(a)).join(', ')})`;
948
1205
  const globalResult = (await this.extensionBridge.debuggerSend('Runtime.evaluate', {
949
1206
  expression: 'globalThis',
950
1207
  returnByValue: false,
951
1208
  }, undefined, timeout));
1209
+ const globalObjectId = globalResult.result?.objectId;
1210
+ if (!globalObjectId) {
1211
+ const fallbackExpression = buildFallbackExpression();
1212
+ return this.evaluateViaExtensionPrecise(fallbackExpression, fallbackExpression, undefined, timeout);
1213
+ }
952
1214
  try {
953
1215
  const params = {
954
1216
  functionDeclaration: code,
955
- objectId: globalResult.result.objectId,
1217
+ objectId: globalObjectId,
956
1218
  arguments: args.map((a) => ({ value: a })),
957
- returnByValue: true,
1219
+ returnByValue: false,
958
1220
  awaitPromise: true,
959
1221
  };
960
1222
  if (timeout !== undefined) {
961
1223
  params.timeout = timeout;
962
1224
  }
963
1225
  const result = (await this.extensionBridge.debuggerSend('Runtime.callFunctionOn', params, undefined, timeout));
964
- return this.checkCdpResult(result);
1226
+ if (result.exceptionDetails) {
1227
+ throw new Error(formatCdpException(result.exceptionDetails));
1228
+ }
1229
+ return await this.materializeCdpResult(result.result, timeout);
1230
+ }
1231
+ catch (error) {
1232
+ const message = error instanceof Error ? error.message : String(error);
1233
+ if (message.includes('Either objectId or executionContextId or uniqueContextId must be specified')) {
1234
+ const fallbackExpression = buildFallbackExpression();
1235
+ return this.evaluateViaExtensionPrecise(fallbackExpression, fallbackExpression, undefined, timeout);
1236
+ }
1237
+ throw error;
965
1238
  }
966
1239
  finally {
967
1240
  this.extensionBridge.debuggerSend('Runtime.releaseObject', {
968
- objectId: globalResult.result.objectId,
1241
+ objectId: globalObjectId,
969
1242
  }).catch(() => { });
970
1243
  }
971
1244
  }