@pyrokine/mcp-chrome 1.7.0 → 2.0.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 (109) hide show
  1. package/README.md +71 -31
  2. package/dist/anti-detection/behavior.d.ts.map +1 -1
  3. package/dist/anti-detection/behavior.js.map +1 -1
  4. package/dist/anti-detection/index.d.ts +1 -1
  5. package/dist/anti-detection/index.d.ts.map +1 -1
  6. package/dist/anti-detection/index.js +1 -1
  7. package/dist/anti-detection/index.js.map +1 -1
  8. package/dist/anti-detection/injection.d.ts +6 -2
  9. package/dist/anti-detection/injection.d.ts.map +1 -1
  10. package/dist/anti-detection/injection.js +32 -79
  11. package/dist/anti-detection/injection.js.map +1 -1
  12. package/dist/cdp/client.d.ts +2 -2
  13. package/dist/cdp/client.d.ts.map +1 -1
  14. package/dist/cdp/client.js +8 -10
  15. package/dist/cdp/client.js.map +1 -1
  16. package/dist/cdp/index.d.ts.map +1 -1
  17. package/dist/cdp/index.js.map +1 -1
  18. package/dist/cdp/launcher.d.ts.map +1 -1
  19. package/dist/cdp/launcher.js +40 -13
  20. package/dist/cdp/launcher.js.map +1 -1
  21. package/dist/core/auto-wait.d.ts +2 -2
  22. package/dist/core/auto-wait.d.ts.map +1 -1
  23. package/dist/core/auto-wait.js +2 -2
  24. package/dist/core/auto-wait.js.map +1 -1
  25. package/dist/core/browser-driver.d.ts +307 -0
  26. package/dist/core/browser-driver.d.ts.map +1 -0
  27. package/dist/core/browser-driver.js +21 -0
  28. package/dist/core/browser-driver.js.map +1 -0
  29. package/dist/core/error-sanitizer.d.ts +25 -0
  30. package/dist/core/error-sanitizer.d.ts.map +1 -0
  31. package/dist/core/error-sanitizer.js +66 -0
  32. package/dist/core/error-sanitizer.js.map +1 -0
  33. package/dist/core/errors.d.ts +10 -1
  34. package/dist/core/errors.d.ts.map +1 -1
  35. package/dist/core/errors.js +17 -4
  36. package/dist/core/errors.js.map +1 -1
  37. package/dist/core/extension-errors.d.ts +20 -0
  38. package/dist/core/extension-errors.d.ts.map +1 -0
  39. package/dist/core/extension-errors.js +40 -0
  40. package/dist/core/extension-errors.js.map +1 -0
  41. package/dist/core/index.d.ts.map +1 -1
  42. package/dist/core/index.js.map +1 -1
  43. package/dist/core/locator.d.ts +2 -2
  44. package/dist/core/locator.d.ts.map +1 -1
  45. package/dist/core/locator.js +25 -65
  46. package/dist/core/locator.js.map +1 -1
  47. package/dist/core/retry.d.ts +2 -2
  48. package/dist/core/retry.d.ts.map +1 -1
  49. package/dist/core/retry.js +2 -2
  50. package/dist/core/retry.js.map +1 -1
  51. package/dist/core/session.d.ts +153 -46
  52. package/dist/core/session.d.ts.map +1 -1
  53. package/dist/core/session.js +672 -177
  54. package/dist/core/session.js.map +1 -1
  55. package/dist/core/types.d.ts +9 -3
  56. package/dist/core/types.d.ts.map +1 -1
  57. package/dist/core/types.js +13 -6
  58. package/dist/core/types.js.map +1 -1
  59. package/dist/core/unified-session.d.ts +46 -81
  60. package/dist/core/unified-session.d.ts.map +1 -1
  61. package/dist/core/unified-session.js +338 -635
  62. package/dist/core/unified-session.js.map +1 -1
  63. package/dist/core/utils.d.ts +7 -0
  64. package/dist/core/utils.d.ts.map +1 -0
  65. package/dist/core/utils.js +33 -0
  66. package/dist/core/utils.js.map +1 -0
  67. package/dist/extension/bridge.d.ts +69 -50
  68. package/dist/extension/bridge.d.ts.map +1 -1
  69. package/dist/extension/bridge.js +176 -77
  70. package/dist/extension/bridge.js.map +1 -1
  71. package/dist/extension/http-server.d.ts +6 -4
  72. package/dist/extension/http-server.d.ts.map +1 -1
  73. package/dist/extension/http-server.js +45 -31
  74. package/dist/extension/http-server.js.map +1 -1
  75. package/dist/extension/index.d.ts.map +1 -1
  76. package/dist/extension/index.js.map +1 -1
  77. package/dist/index.js +3 -1
  78. package/dist/index.js.map +1 -1
  79. package/dist/tools/browse.d.ts.map +1 -1
  80. package/dist/tools/browse.js +32 -34
  81. package/dist/tools/browse.js.map +1 -1
  82. package/dist/tools/cookies.d.ts.map +1 -1
  83. package/dist/tools/cookies.js +38 -16
  84. package/dist/tools/cookies.js.map +1 -1
  85. package/dist/tools/evaluate.d.ts.map +1 -1
  86. package/dist/tools/evaluate.js +54 -23
  87. package/dist/tools/evaluate.js.map +1 -1
  88. package/dist/tools/extract.d.ts.map +1 -1
  89. package/dist/tools/extract.js +221 -153
  90. package/dist/tools/extract.js.map +1 -1
  91. package/dist/tools/index.d.ts.map +1 -1
  92. package/dist/tools/index.js.map +1 -1
  93. package/dist/tools/input.d.ts.map +1 -1
  94. package/dist/tools/input.js +271 -90
  95. package/dist/tools/input.js.map +1 -1
  96. package/dist/tools/logs.d.ts.map +1 -1
  97. package/dist/tools/logs.js +31 -17
  98. package/dist/tools/logs.js.map +1 -1
  99. package/dist/tools/manage.d.ts.map +1 -1
  100. package/dist/tools/manage.js +25 -28
  101. package/dist/tools/manage.js.map +1 -1
  102. package/dist/tools/schema.d.ts +1 -1
  103. package/dist/tools/schema.d.ts.map +1 -1
  104. package/dist/tools/schema.js +31 -55
  105. package/dist/tools/schema.js.map +1 -1
  106. package/dist/tools/wait.d.ts.map +1 -1
  107. package/dist/tools/wait.js +19 -16
  108. package/dist/tools/wait.js.map +1 -1
  109. package/package.json +48 -40
@@ -6,7 +6,8 @@
6
6
  * 2. CDP 模式:通过 Chrome DevTools Protocol 操作(Fallback)
7
7
  */
8
8
  import { ExtensionBridge } from '../extension/index.js';
9
- import { getSession as getCdpSession } from './session.js';
9
+ import { isExtensionDisconnected } from './extension-errors.js';
10
+ import { getKeyDefinition, getSession as getCdpSession } from './session.js';
10
11
  import { extractCdpValue, formatCdpException, MODIFIER_KEYS } from './types.js';
11
12
  class UnifiedSessionManager {
12
13
  static instance;
@@ -16,12 +17,13 @@ class UnifiedSessionManager {
16
17
  currentMousePosition = { x: 0, y: 0 }; // 跟踪鼠标位置
17
18
  /** 当前按下的修饰键位掩码 */
18
19
  modifiers = 0;
20
+ /** 当前按下的所有键(用于 Puppeteer 风格的 rawKeyDown/autoRepeat 长按重复) */
21
+ pressedKeys = new Set();
19
22
  lastConnectionFailure = 0;
20
23
  tabSwitchLock = Promise.resolve(); // 串行化 tab 切换,防止并发竞态
21
24
  requireExtension = false; // 指定 tabId 或 frame 时为 true,禁止 CDP 回退
22
25
  currentFrameOffset = null; // iframe 在主页面的偏移量(withFrame 期间有效)
23
- constructor() {
24
- }
26
+ constructor() { }
25
27
  static getInstance() {
26
28
  if (!UnifiedSessionManager.instance) {
27
29
  UnifiedSessionManager.instance = new UnifiedSessionManager();
@@ -103,15 +105,15 @@ class UnifiedSessionManager {
103
105
  async launch(options = {}) {
104
106
  // 优先检查 Extension 是否已连接,如果断开则等待重连(受 timeout 约束)
105
107
  if (await this.ensureExtensionConnected(options.timeout)) {
106
- // createTab 会设置 currentTabId,需要加锁
107
- const tab = await this.withTabLock(async () => {
108
- return this.extensionBridge.createTab(undefined, options.timeout);
108
+ // newPage 会设置 currentTabId,需要加锁
109
+ const result = await this.withTabLock(async () => {
110
+ return this.extensionBridge.newPage(undefined, options.timeout);
109
111
  });
110
112
  return {
111
- targetId: String(tab.id),
112
- type: 'page',
113
- url: tab.url,
114
- title: tab.title,
113
+ targetId: result.targetId,
114
+ type: result.type ?? 'page',
115
+ url: result.url,
116
+ title: result.title,
115
117
  mode: 'extension',
116
118
  };
117
119
  }
@@ -126,131 +128,68 @@ class UnifiedSessionManager {
126
128
  * 列出所有页面
127
129
  */
128
130
  async listTargets() {
129
- // 优先使用 Extension,如果断开则等待重连
130
- if (await this.ensureExtensionConnected()) {
131
- const tabs = await this.extensionBridge.listTabs();
132
- const currentTabId = this.extensionBridge.getCurrentTabId();
133
- return tabs.map(tab => ({
134
- targetId: String(tab.id),
135
- type: 'page',
136
- url: tab.url,
137
- title: tab.title,
138
- mode: 'extension',
139
- managed: tab.managed,
140
- isActive: tab.id === currentTabId,
141
- windowId: tab.windowId,
142
- index: tab.index,
143
- pinned: tab.pinned,
144
- incognito: tab.incognito,
145
- status: tab.status,
146
- }));
147
- }
148
- if (getCdpSession().isConnected()) {
149
- const targets = await getCdpSession().listTargets();
150
- const currentTargetId = getCdpSession().getState()?.targetId;
151
- return targets.map(t => ({
152
- ...t,
153
- mode: 'cdp',
154
- isActive: t.targetId === currentTargetId,
155
- }));
156
- }
157
- return [];
131
+ const mode = this.getMode();
132
+ if (mode === 'none') {
133
+ return [];
134
+ }
135
+ const driver = await this.getDriver();
136
+ const targets = await driver.listTargets();
137
+ return targets.map((t) => ({
138
+ targetId: t.targetId ?? String(t.id),
139
+ type: t.type ?? 'page',
140
+ url: t.url,
141
+ title: t.title,
142
+ mode,
143
+ isActive: t.active ?? false,
144
+ managed: t.managed,
145
+ windowId: t.windowId,
146
+ index: t.index,
147
+ pinned: t.pinned,
148
+ incognito: t.incognito,
149
+ status: t.status,
150
+ }));
158
151
  }
159
152
  /**
160
153
  * 导航到 URL
161
154
  */
162
155
  async navigate(url, options = {}) {
163
- if (await this.ensureExtensionConnected(options.timeout)) {
164
- // 加锁保护 currentTabId,防止并发 withTabId 将命令路由到临时切换的 tab
165
- await this.withTabLock(async () => {
166
- await this.extensionBridge.navigate(url, {
167
- waitUntil: options.wait,
168
- timeout: options.timeout,
169
- });
170
- });
171
- return;
172
- }
173
- await getCdpSession().navigate(url, options);
156
+ const driver = await this.getDriver(options.timeout);
157
+ await this.withTabLock(() => driver.navigate(url, { wait: options.wait, timeout: options.timeout }));
174
158
  }
175
- /**
176
- * 后退
177
- */
178
159
  async goBack(timeout) {
179
- if (await this.ensureExtensionConnected(timeout)) {
180
- return this.withTabLock(async () => {
181
- const result = await this.extensionBridge.goBack(timeout);
182
- return { navigated: result.navigated };
183
- });
184
- }
185
- return getCdpSession().goBack(timeout);
160
+ const driver = await this.getDriver(timeout);
161
+ const result = await this.withTabLock(() => driver.goBack(timeout));
162
+ return { navigated: result.navigated };
186
163
  }
187
- /**
188
- * 前进
189
- */
190
164
  async goForward(timeout) {
191
- if (await this.ensureExtensionConnected(timeout)) {
192
- return this.withTabLock(async () => {
193
- const result = await this.extensionBridge.goForward(timeout);
194
- return { navigated: result.navigated };
195
- });
196
- }
197
- return getCdpSession().goForward(timeout);
165
+ const driver = await this.getDriver(timeout);
166
+ const result = await this.withTabLock(() => driver.goForward(timeout));
167
+ return { navigated: result.navigated };
198
168
  }
199
- /**
200
- * 刷新
201
- */
202
169
  async reload(options = {}) {
203
- if (await this.ensureExtensionConnected(options.timeout)) {
204
- await this.withTabLock(async () => {
205
- await this.extensionBridge.reload(options.ignoreCache, options.waitUntil, options.timeout);
206
- });
207
- return;
208
- }
209
- await getCdpSession().reload(options);
170
+ const driver = await this.getDriver(options.timeout);
171
+ await this.withTabLock(() => driver.reload(options.ignoreCache, options.waitUntil, options.timeout));
210
172
  }
211
173
  /**
212
174
  * 读取页面(Accessibility Tree)
213
175
  */
214
176
  async readPage(options) {
215
- if (await this.ensureExtensionConnected()) {
216
- return this.extensionBridge.readPage(options);
217
- }
218
- // CDP 模式使用 getPageState
219
- const state = await getCdpSession().getPageState();
220
- const elements = state.elements || [];
221
- // 构建简单的文本表示
222
- const lines = elements.map(e => {
223
- let line = e.role;
224
- if (e.name) {
225
- line += ` "${e.name}"`;
226
- }
227
- return line;
228
- });
229
- return {
230
- pageContent: lines.join('\n'),
231
- viewport: state.viewport,
232
- };
177
+ const driver = await this.getDriver();
178
+ return driver.readPage(options);
233
179
  }
234
180
  /**
235
181
  * 截图
236
182
  */
237
183
  async screenshot(options) {
238
- if (await this.ensureExtensionConnected()) {
239
- const result = await this.extensionBridge.screenshot(options);
240
- return result.data;
241
- }
242
- return getCdpSession().screenshot(options?.fullPage, options?.scale, options?.format, options?.quality, options?.clip);
184
+ const result = await (await this.getDriver()).screenshot(options);
185
+ return result.data;
243
186
  }
244
187
  /**
245
188
  * 点击元素
246
189
  */
247
190
  async click(refId) {
248
- if (await this.ensureExtensionConnected()) {
249
- await this.extensionBridge.click(refId);
250
- return;
251
- }
252
- // CDP 模式需要通过 Locator
253
- throw new Error('CDP 模式下请使用 input 工具的 click action');
191
+ const driver = await this.getDriver();
192
+ await driver.click(refId);
254
193
  }
255
194
  /**
256
195
  * 带 actionability 检查的点击(Extension 模式)
@@ -258,57 +197,43 @@ class UnifiedSessionManager {
258
197
  * 返回结构化结果,让调用方知道操作是否真正生效
259
198
  */
260
199
  async actionableClick(refId, force) {
261
- if (await this.ensureExtensionConnected()) {
262
- return this.extensionBridge.actionableClick(refId, force);
263
- }
264
- throw new Error('actionableClick 仅支持 Extension 模式');
200
+ const driver = await this.getDriver();
201
+ return driver.actionableClick(refId, force);
265
202
  }
266
203
  /**
267
- * 检查元素可操作性(Extension 模式)
204
+ * dispatch 模式输入(ISOLATED 世界,兼容 React/Vue 受控组件)
268
205
  */
269
- async checkActionability(refId) {
270
- if (await this.ensureExtensionConnected()) {
271
- return this.extensionBridge.checkActionability(refId);
272
- }
273
- throw new Error('checkActionability 仅支持 Extension 模式');
206
+ async dispatchInput(refId, text) {
207
+ const driver = await this.getDriver();
208
+ return driver.dispatchInput(refId, text);
274
209
  }
275
210
  /**
276
- * dispatch 模式输入(ISOLATED 世界,兼容 React/Vue 受控组件)
211
+ * HTML5 drag/drop(ISOLATED 世界,通过 refId 访问 __mcpElementMap 中的元素引用)
277
212
  */
278
- async dispatchInput(refId, text) {
279
- if (await this.ensureExtensionConnected()) {
280
- return this.extensionBridge.dispatchInput(refId, text);
281
- }
282
- throw new Error('dispatchInput 仅支持 Extension 模式');
213
+ async dragAndDrop(srcRefId, dstRefId) {
214
+ const driver = await this.getDriver();
215
+ return driver.dragAndDrop(srcRefId, dstRefId);
283
216
  }
284
217
  /**
285
218
  * 获取元素 computed style(ISOLATED 世界)
286
219
  */
287
220
  async getComputedStyle(refId, prop) {
288
- if (await this.ensureExtensionConnected()) {
289
- return this.extensionBridge.getComputedStyle(refId, prop);
290
- }
291
- throw new Error('getComputedStyle 仅支持 Extension 模式');
221
+ const driver = await this.getDriver();
222
+ return driver.getComputedStyle(refId, prop);
292
223
  }
293
224
  /**
294
225
  * 输入文本
295
226
  */
296
227
  async type(refId, text, clear = false) {
297
- if (await this.ensureExtensionConnected()) {
298
- await this.extensionBridge.type(refId, text, clear);
299
- return;
300
- }
301
- throw new Error('CDP 模式下请使用 input 工具');
228
+ const driver = await this.getDriver();
229
+ await driver.typeRef(refId, text, clear);
302
230
  }
303
231
  /**
304
232
  * 滚动
305
233
  */
306
234
  async scroll(x, y, refId) {
307
- if (await this.ensureExtensionConnected()) {
308
- await this.extensionBridge.scroll(x, y, refId);
309
- return;
310
- }
311
- await getCdpSession().mouseWheel(x, y);
235
+ const driver = await this.getDriver();
236
+ await driver.scrollAt(x, y, refId);
312
237
  }
313
238
  /**
314
239
  * 执行 JavaScript
@@ -320,8 +245,8 @@ class UnifiedSessionManager {
320
245
  * stealth 模式:使用 chrome.scripting.executeScript(受 CSP 限制)
321
246
  * precise 模式:使用 debugger API Runtime.evaluate(可绕过 CSP)
322
247
  *
323
- * 使用 args 时 script 必须是函数表达式,如 "(x) => x + 1"
324
- * precise 模式通过 callFunctionOn 传递参数(支持大 payload),stealth 模式仍用字符串拼接。
248
+ * 使用 args 时 script 必须是函数表达式,如 "(x) => x + 1"
249
+ * precise 模式通过 callFunctionOn 传递参数(支持大 payload),stealth 模式仍用字符串拼接
325
250
  * @param code JavaScript 代码
326
251
  * @param mode 执行模式(stealth/precise)
327
252
  * @param timeout 端到端预算(毫秒),同时作为脚本执行超时和 sendCommand 的端到端预算
@@ -329,12 +254,13 @@ class UnifiedSessionManager {
329
254
  * @param _retried 内部重试标记,外部不应传入
330
255
  */
331
256
  async evaluate(code, mode, timeout, args, _retried) {
257
+ const entryStart = Date.now();
332
258
  const effectiveMode = mode ?? this.inputMode;
333
259
  const hasArgs = args && args.length > 0;
334
260
  // stealth 模式:args 只能通过字符串拼接(chrome.scripting 不支持协议级参数传递)
335
261
  let expression = code;
336
262
  if (hasArgs && effectiveMode === 'stealth') {
337
- const argsStr = args.map(a => JSON.stringify(a)).join(', ');
263
+ const argsStr = args.map((a) => JSON.stringify(a)).join(', ');
338
264
  expression = `(${code})(${argsStr})`;
339
265
  }
340
266
  const cdpScript = hasArgs ? code : expression;
@@ -343,61 +269,37 @@ class UnifiedSessionManager {
343
269
  // 轮询上下文:快速失败,端到端预算受控
344
270
  if (!this.isExtensionConnected()) {
345
271
  this.assertCdpFallbackAllowed();
346
- return getCdpSession().evaluate(cdpScript, args, timeout);
272
+ return this.evaluateViaCdp(cdpScript, args, timeout);
347
273
  }
348
274
  }
349
275
  else {
350
276
  // 非轮询上下文:允许等待重连;连接失败时回退 CDP
351
277
  if (!(await this.ensureExtensionConnected())) {
352
- return getCdpSession().evaluate(cdpScript, args, timeout);
278
+ return this.evaluateViaCdp(cdpScript, args, timeout);
353
279
  }
354
280
  }
355
281
  // Extension 路径
356
- const currentFrameId = this.extensionBridge.getCurrentFrameId();
357
282
  if (effectiveMode === 'precise') {
358
- // precise + args + 主 frame:使用 callFunctionOn 避免大 payload 字符串拼接
359
- if (hasArgs && currentFrameId === 0) {
360
- return this.callFunctionOn(code, args, timeout);
361
- }
362
- if (currentFrameId !== 0) {
363
- // iframe:args 仍用字符串拼接(evaluateInFrame 使用 expression 字符串)
364
- let iframeExpression = expression;
365
- if (hasArgs) {
366
- const argsStr = args.map(a => JSON.stringify(a)).join(', ');
367
- iframeExpression = `(${code})(${argsStr})`;
368
- }
369
- const result = await this.extensionBridge.evaluateInFrame(currentFrameId, iframeExpression, timeout);
370
- return this.checkCdpResult(result);
371
- }
372
- // 主 frame,无 args:直接 Runtime.evaluate
373
- const params = {
374
- expression,
375
- returnByValue: true,
376
- awaitPromise: true,
377
- };
378
- if (timeout !== undefined) {
379
- params.timeout = timeout;
380
- }
381
- // timeout 即端到端预算,直接作为 RPC 超时(不额外加 margin)
382
- const result = await this.extensionBridge.debuggerSend('Runtime.evaluate', params, undefined, timeout);
383
- return this.checkCdpResult(result);
283
+ return this.evaluateViaExtensionPrecise(code, expression, args, timeout);
384
284
  }
385
- return await this.extensionBridge.evaluate(expression, timeout, timeout);
285
+ return this.evaluateViaExtensionStealth(expression, timeout);
386
286
  }
387
287
  catch (err) {
388
288
  const msg = err instanceof Error ? err.message : String(err);
289
+ // 计算重试时剩余预算(避免重试用满预算导致总耗时超额)
290
+ const remainingTimeout = timeout !== undefined ? Math.max(0, timeout - (Date.now() - entryStart)) : undefined;
389
291
  // Extension 断连时等待重连后重试一次
390
- if (!_retried &&
391
- (msg.includes('Extension disconnected') ||
392
- msg.includes('Connection replaced') ||
393
- msg.includes('not connected'))) {
394
- await new Promise(r => setTimeout(r, 2000));
292
+ if (!_retried && (isExtensionDisconnected(err) || msg.includes('not connected'))) {
293
+ if (remainingTimeout !== undefined && remainingTimeout <= 0) {
294
+ throw err;
295
+ }
296
+ await new Promise((r) => setTimeout(r, 2000));
395
297
  if (this.isExtensionConnected()) {
396
- return this.evaluate(code, mode, timeout, args, true);
298
+ return this.evaluate(code, mode, remainingTimeout, args, true);
397
299
  }
398
300
  // Extension 重连失败,尝试 CDP fallback
399
301
  if (!this.requireExtension) {
400
- return getCdpSession().evaluate(cdpScript, args, timeout);
302
+ return this.evaluateViaCdp(cdpScript, args, remainingTimeout);
401
303
  }
402
304
  }
403
305
  // 裸 return 语句导致语法错误时,自动包裹 IIFE 重试(仅一次)
@@ -414,96 +316,30 @@ class UnifiedSessionManager {
414
316
  * 获取页面文本
415
317
  */
416
318
  async getText(selector) {
417
- if (await this.ensureExtensionConnected()) {
418
- return this.extensionBridge.getText(selector);
419
- }
420
- if (selector) {
421
- return getCdpSession().evaluate(`(s => document.querySelector(s)?.textContent || '')`, [selector]);
422
- }
423
- return getCdpSession().evaluate('document.body.innerText');
319
+ return (await this.getDriver()).getPageText(selector);
424
320
  }
425
- /**
426
- * 获取页面 HTML
427
- */
428
321
  async getHtml(selector, outer = true) {
429
- if (await this.ensureExtensionConnected()) {
430
- return this.extensionBridge.getHtml(selector, outer);
431
- }
432
- if (selector) {
433
- const prop = outer ? 'outerHTML' : 'innerHTML';
434
- return getCdpSession().evaluate(`((s, p) => { const el = document.querySelector(s); return el ? el[p] : ''; })`, [selector, prop]);
435
- }
436
- return getCdpSession().evaluate('document.documentElement.outerHTML');
322
+ return (await this.getDriver()).getPageHtml(selector, outer);
437
323
  }
438
324
  /**
439
325
  * 获取页面 HTML + 图片元信息
440
326
  */
441
327
  async getHtmlWithImages(selector, outer = true) {
442
- if (await this.ensureExtensionConnected()) {
443
- return this.extensionBridge.getHtmlWithImages(selector, outer);
444
- }
445
- // CDP 模式:evaluate 注入函数
446
- const selectorArg = JSON.stringify(selector ?? null);
447
- return getCdpSession().evaluate(`(function() {
448
- var root = ${selectorArg} ? document.querySelector(${selectorArg}) : document.documentElement;
449
- if (!root) return {html: '', images: []};
450
- var html = ${selectorArg}
451
- ? (${outer} ? root.outerHTML : root.innerHTML)
452
- : document.documentElement.outerHTML;
453
- var imgList = [];
454
- if (root.tagName === 'IMG') imgList.push(root);
455
- root.querySelectorAll('img').forEach(function(img) { imgList.push(img); });
456
- var images = [];
457
- for (var i = 0; i < imgList.length; i++) {
458
- var img = imgList[i];
459
- 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});
460
- }
461
- return {html: html, images: images};
462
- })()`);
328
+ const driver = await this.getDriver();
329
+ return driver.getHtmlWithImages(selector, outer);
463
330
  }
464
331
  /**
465
332
  * 获取页面元信息
466
333
  */
467
334
  async getMetadata() {
468
- if (await this.ensureExtensionConnected()) {
469
- return this.extensionBridge.getMetadata();
470
- }
471
- // CDP 模式:evaluate 注入函数
472
- return getCdpSession().evaluate(`(function() {
473
- function meta(name) {
474
- var el = document.querySelector('meta[name="'+name+'"],meta[property="'+name+'"]');
475
- return el ? el.content || undefined : undefined;
476
- }
477
- var og = {}, tw = {};
478
- document.querySelectorAll('meta[property^="og:"]').forEach(function(m) { og[m.getAttribute('property')] = m.content || ''; });
479
- document.querySelectorAll('meta[name^="twitter:"]').forEach(function(m) { tw[m.getAttribute('name')] = m.content || ''; });
480
- var jsonLd = [];
481
- document.querySelectorAll('script[type="application/ld+json"]').forEach(function(s) {
482
- try { jsonLd.push(JSON.parse(s.textContent || '')); } catch(e) {}
483
- });
484
- var alternates = [];
485
- document.querySelectorAll('link[rel="alternate"]').forEach(function(l) {
486
- alternates.push({href: l.href, type: l.getAttribute('type') || undefined, hreflang: l.getAttribute('hreflang') || undefined});
487
- });
488
- var feeds = [];
489
- document.querySelectorAll('link[type="application/rss+xml"],link[type="application/atom+xml"]').forEach(function(l) {
490
- feeds.push({href: l.href, type: l.getAttribute('type'), title: l.getAttribute('title') || undefined});
491
- });
492
- return {
493
- title: document.title,
494
- description: meta('description'),
495
- canonical: (document.querySelector('link[rel="canonical"]') || {}).href || undefined,
496
- charset: document.characterSet,
497
- viewport: meta('viewport'),
498
- og: og, twitter: tw, jsonLd: jsonLd, alternates: alternates, feeds: feeds
499
- };
500
- })()`);
335
+ const driver = await this.getDriver();
336
+ return driver.getMetadata();
501
337
  }
502
338
  /**
503
339
  * 批量从浏览器缓存获取资源内容
504
340
  *
505
- * 只调用一次 Page.enable + Page.getFrameTree,然后逐个获取资源。
506
- * 并发限制避免 CDP 连接拥塞。
341
+ * 只调用一次 Page.enable + Page.getFrameTree,然后逐个获取资源,
342
+ * 并发限制避免 CDP 连接拥塞
507
343
  */
508
344
  async getResourceContentBatch(urls, concurrency = 6) {
509
345
  const results = new Map();
@@ -512,7 +348,7 @@ class UnifiedSessionManager {
512
348
  }
513
349
  try {
514
350
  await this.sendCdpCommand('Page.enable');
515
- const frameTree = await this.sendCdpCommand('Page.getFrameTree');
351
+ const frameTree = (await this.sendCdpCommand('Page.getFrameTree'));
516
352
  const frameId = frameTree.frameTree.frame.id;
517
353
  // 并发控制
518
354
  let idx = 0;
@@ -520,18 +356,18 @@ class UnifiedSessionManager {
520
356
  while (idx < urls.length) {
521
357
  const url = urls[idx++];
522
358
  try {
523
- const result = await this.sendCdpCommand('Page.getResourceContent', { frameId, url });
359
+ const result = (await this.sendCdpCommand('Page.getResourceContent', { frameId, url }));
524
360
  results.set(url, result);
525
361
  }
526
- catch {
527
- // 单个资源获取失败不影响其他
362
+ catch (err) {
363
+ console.warn('[MCP] 资源获取失败:', url, err);
528
364
  }
529
365
  }
530
366
  };
531
367
  await Promise.all(Array.from({ length: Math.min(concurrency, urls.length) }, () => next()));
532
368
  }
533
- catch {
534
- // Page.enable getFrameTree 失败,返回空结果
369
+ catch (err) {
370
+ console.warn('[MCP] Page 域不可用,返回空资源列表:', err);
535
371
  }
536
372
  return results;
537
373
  }
@@ -554,218 +390,103 @@ class UnifiedSessionManager {
554
390
  return this.extensionBridge.find(selector, text, xpath, timeout);
555
391
  }
556
392
  this.assertCdpFallbackAllowed();
557
- return this.findViaCdp(selector, text, xpath, timeout);
393
+ return getCdpSession().find(selector, text, xpath, timeout);
558
394
  }
559
- // 非轮询上下文:允许等待重连
560
- if (await this.ensureExtensionConnected()) {
561
- return this.withExtensionRetry(() => this.extensionBridge.find(selector, text, xpath), () => this.findViaCdp(selector, text, xpath));
562
- }
563
- return this.findViaCdp(selector, text, xpath);
395
+ // 非轮询上下文:允许等待重连,断连时 fallback CDP
396
+ const driver = await this.getDriver();
397
+ return this.withExtensionRetry(() => driver.find(selector, text, xpath), () => getCdpSession().find(selector, text, xpath));
564
398
  }
565
399
  /**
566
400
  * 获取元素属性
567
401
  */
568
402
  async getAttribute(selector, refId, attribute) {
569
- if (await this.ensureExtensionConnected()) {
570
- return this.extensionBridge.getAttribute(selector, refId, attribute);
571
- }
572
- throw new Error('CDP 模式下请使用 extract 工具');
403
+ const driver = await this.getDriver();
404
+ return driver.getAttribute(selector, refId, attribute);
573
405
  }
574
406
  /**
575
407
  * 获取 Cookies
576
408
  */
577
409
  async getCookies(filter) {
578
- if (await this.ensureExtensionConnected()) {
579
- return this.extensionBridge.getCookies(filter);
580
- }
581
- // CDP 模式:支持按字段过滤
582
- const urls = filter?.url ? [filter.url] : undefined;
583
- const cookies = await getCdpSession().getCookies(urls);
584
- if (!filter) {
585
- return cookies;
586
- }
587
- return cookies.filter((c) => {
588
- if (filter.name && c.name !== filter.name) {
589
- return false;
590
- }
591
- if (filter.domain) {
592
- // 域名匹配:精确匹配或子域匹配(.example.com 匹配 sub.example.com)
593
- const filterDomain = filter.domain.replace(/^\./, '');
594
- const cookieDomain = (c.domain ?? '').replace(/^\./, '');
595
- if (cookieDomain !== filterDomain && !cookieDomain.endsWith('.' + filterDomain)) {
596
- return false;
597
- }
598
- }
599
- if (filter.path && c.path !== filter.path) {
600
- return false;
601
- }
602
- if (filter.secure !== undefined && c.secure !== filter.secure) {
603
- return false;
604
- }
605
- if (filter.session !== undefined) {
606
- // session cookie: expires 为 -1 或 0(CDP 返回 session cookie 的 expires 为 -1)
607
- const isSession = (c.expires ?? -1) <= 0;
608
- if (filter.session !== isSession) {
609
- return false;
610
- }
611
- }
612
- return true;
613
- });
410
+ return (await this.getDriver()).getCookies(filter);
614
411
  }
615
- /**
616
- * 设置 Cookie
617
- */
618
- async setCookie(name, value, options = {}) {
619
- if (await this.ensureExtensionConnected()) {
620
- const state = this.extensionBridge.getState();
621
- const url = options.url || state?.url || 'http://localhost';
622
- // 转换 sameSite 值到 Chrome cookies API 格式
623
- let chromeSameSite;
624
- if (options.sameSite) {
625
- const sameSiteMap = {
626
- None: 'no_restriction',
627
- Lax: 'lax',
628
- Strict: 'strict',
629
- };
630
- chromeSameSite = sameSiteMap[options.sameSite];
631
- }
632
- await this.extensionBridge.setCookie({
633
- url,
634
- name,
635
- value,
636
- domain: options.domain,
637
- path: options.path,
638
- secure: options.secure,
639
- httpOnly: options.httpOnly,
640
- sameSite: chromeSameSite,
641
- expirationDate: options.expirationDate,
642
- });
643
- return;
644
- }
645
- await getCdpSession().setCookie(name, value, options);
412
+ async setCookie(params) {
413
+ const driver = await this.getDriver();
414
+ await driver.setCookie(params);
646
415
  }
647
- /**
648
- * 删除 Cookie
649
- */
650
416
  async deleteCookie(url, name) {
651
- if (await this.ensureExtensionConnected()) {
652
- await this.extensionBridge.deleteCookie(url, name);
653
- return;
654
- }
655
- await getCdpSession().deleteCookie(name, url);
417
+ return (await this.getDriver()).deleteCookie(url, name);
656
418
  }
657
- /**
658
- * 清空 Cookies
659
- */
660
419
  async clearCookies(filter) {
661
- if (await this.ensureExtensionConnected()) {
662
- return await this.extensionBridge.clearCookies(filter);
663
- }
664
- // CDP 模式:有 filter 时先获取匹配的 cookies 再逐条删除,无 filter 时清除全部
665
- if (filter && (filter.url || filter.domain)) {
666
- // 优先使用 url 过滤缩小范围,减少不必要的遍历
667
- const urls = filter.url ? [filter.url] : undefined;
668
- const cookies = await getCdpSession().getCookies(urls);
669
- let count = 0;
670
- for (const cookie of cookies) {
671
- // domain 进一步过滤(url 过滤后可能仍包含不匹配 domain 的 cookie)
672
- if (filter.domain) {
673
- const filterDomain = filter.domain.replace(/^\./, '');
674
- const cookieDomain = cookie.domain.replace(/^\./, '');
675
- if (cookieDomain !== filterDomain && !cookieDomain.endsWith('.' + filterDomain)) {
676
- continue;
677
- }
678
- }
679
- // 构造删除 URL:必须匹配 cookie 自身的 domain/path/secure
680
- const protocol = cookie.secure ? 'https:' : 'http:';
681
- const domain = cookie.domain.startsWith('.') ? cookie.domain.slice(1) : cookie.domain;
682
- const deleteUrl = `${protocol}//${domain}${cookie.path}`;
683
- await getCdpSession().deleteCookie(cookie.name, deleteUrl);
684
- count++;
685
- }
686
- return { count };
687
- }
688
- await getCdpSession().clearCookies();
689
- return { count: -1 };
420
+ return (await this.getDriver()).clearCookies(filter);
690
421
  }
691
422
  /**
692
423
  * 创建新页面
693
424
  */
694
425
  async newPage(url) {
695
- if (await this.ensureExtensionConnected()) {
696
- // createTab 会设置 currentTabId,需要加锁
697
- const tab = await this.withTabLock(async () => {
698
- return this.extensionBridge.createTab(url);
699
- });
426
+ const driver = await this.getDriver();
427
+ const isExt = this.isExtensionConnected();
428
+ const op = async () => {
429
+ const result = await driver.newPage(url);
700
430
  return {
701
- targetId: String(tab.id),
702
- type: 'page',
703
- url: tab.url,
704
- title: tab.title,
431
+ targetId: result.targetId,
432
+ type: result.type ?? 'page',
433
+ url: result.url,
434
+ title: result.title,
705
435
  };
706
- }
707
- const target = await getCdpSession().newPage();
708
- if (url) {
709
- await getCdpSession().navigate(url);
710
- }
711
- return target;
436
+ };
437
+ // Extension 模式 createTab 设置 currentTabId,需要加锁防止并发竞态
438
+ return isExt ? this.withTabLock(op) : op();
712
439
  }
713
440
  /**
714
441
  * 关闭页面
715
442
  */
716
443
  async closePage(targetId) {
717
- if (await this.ensureExtensionConnected()) {
718
- // closeTab 可能触发 currentTabId 重置,需要加锁
719
- await this.withTabLock(async () => {
720
- const tabId = targetId
721
- ? this.parseTabId(targetId)
722
- : this.extensionBridge.getCurrentTabId();
723
- if (tabId === null) {
724
- throw new Error('没有可关闭的页面,请指定 targetId');
725
- }
726
- await this.extensionBridge.closeTab(tabId);
727
- });
728
- return;
444
+ const driver = await this.getDriver();
445
+ const isExt = this.isExtensionConnected();
446
+ const op = () => driver.closePage(targetId);
447
+ if (isExt) {
448
+ await this.withTabLock(op);
449
+ }
450
+ else {
451
+ await op();
729
452
  }
730
- await getCdpSession().closePage(targetId);
731
453
  }
732
454
  /**
733
455
  * 激活页面(切到前台)
734
456
  */
735
457
  async activatePage(targetId) {
736
- if (await this.ensureExtensionConnected()) {
737
- const tabId = this.parseTabId(targetId);
738
- await this.withTabLock(async () => {
739
- await this.extensionBridge.activateTab(tabId);
740
- });
741
- return;
458
+ const driver = await this.getDriver();
459
+ const isExt = this.isExtensionConnected();
460
+ const op = () => driver.activatePage(targetId);
461
+ if (isExt) {
462
+ await this.withTabLock(op);
463
+ }
464
+ else {
465
+ await op();
742
466
  }
743
- // CDP 模式:attach 到目标并切到前台
744
- await getCdpSession().attachToTarget(targetId);
745
- await getCdpSession().activateTarget(targetId);
746
467
  }
747
468
  /**
748
469
  * 选择要操作的页面(不切到前台,只设置当前操作目标)
749
470
  */
750
471
  async selectPage(targetId) {
751
- if (this.isExtensionConnected()) {
752
- const tabId = this.parseTabId(targetId);
753
- await this.withTabLock(async () => {
754
- this.extensionBridge.setCurrentTabId(tabId);
755
- });
756
- return;
472
+ const driver = await this.getDriver();
473
+ const isExt = this.isExtensionConnected();
474
+ const op = () => driver.selectPage(targetId);
475
+ if (isExt) {
476
+ await this.withTabLock(op);
477
+ }
478
+ else {
479
+ await op();
757
480
  }
758
- // CDP 模式下需要 attach 到目标 target
759
- await getCdpSession().attachToTarget(targetId);
760
481
  }
761
482
  /**
762
483
  * 临时切换操作目标 tab,执行完后恢复
763
484
  *
764
485
  * 用于多 tab 并行场景:指定 tabId 时临时切换到该 tab 执行操作,
765
- * 不影响 browse attach 设置的默认 tab
486
+ * 不影响 browse attach 设置的默认 tab
766
487
  *
767
488
  * 即使不传 tabId,也需要加锁:fn() 内调用 bridge 方法会读取 currentTabId,
768
- * 不加锁则并发的 withTabId(someId, ...) 可能在 fn() 执行中途修改 currentTabId
489
+ * 不加锁则并发的 withTabId(someId, ...) 可能在 fn() 执行中途修改 currentTabId
769
490
  */
770
491
  async withTabId(tabId, fn) {
771
492
  // Extension 未连接时不需要锁和 tab 切换(CDP 模式无 currentTabId 竞态)
@@ -786,10 +507,10 @@ class UnifiedSessionManager {
786
507
  // 不切换 tab,但需要加锁保护 currentTabId 不被并发修改
787
508
  return this.withTabLock(fn);
788
509
  }
789
- const numericTabId = this.parseTabId(tabId);
510
+ const driver = this.extensionBridge;
790
511
  return this.withTabLock(async () => {
791
- const previousTabId = this.extensionBridge.getCurrentTabId();
792
- this.extensionBridge.setCurrentTabId(numericTabId);
512
+ const previousTargetId = driver.getCurrentTargetId();
513
+ driver.setCurrentTargetId(tabId);
793
514
  // tabId 明确指定时,禁止 CDP 回退(CDP 不感知 Extension tab)
794
515
  const previousRequireExtension = this.requireExtension;
795
516
  this.requireExtension = true;
@@ -798,15 +519,15 @@ class UnifiedSessionManager {
798
519
  }
799
520
  finally {
800
521
  this.requireExtension = previousRequireExtension;
801
- this.extensionBridge.setCurrentTabId(previousTabId);
522
+ driver.setCurrentTargetId(previousTargetId);
802
523
  }
803
524
  });
804
525
  }
805
526
  /**
806
527
  * 临时切换操作目标 iframe,执行完后恢复
807
528
  *
808
- * frame 支持 CSS 选择器(如 "iframe#main")或索引(如 0)。
809
- * 内部通过 Extension 的 resolveFrame 将选择器解析为 Chrome frameId
529
+ * frame 支持 CSS 选择器(如 "iframe#main")或索引(如 0),
530
+ * 内部通过 Extension 的 resolveFrame 将选择器解析为 Chrome frameId
810
531
  *
811
532
  * 与 withTabId 配合使用时,应嵌套在 withTabId 内部:
812
533
  * withTabId(tabId, () => withFrame(frame, () => { ... }))
@@ -818,31 +539,33 @@ class UnifiedSessionManager {
818
539
  if (!this.extensionBridge?.isConnected()) {
819
540
  throw new Error('iframe 穿透需要 Extension 模式');
820
541
  }
821
- const { frameId, offset } = await this.extensionBridge.resolveFrame(frame);
822
- const previousFrameId = this.extensionBridge.getCurrentFrameId();
542
+ const driver = this.extensionBridge;
543
+ const { frameId, offset } = await driver.resolveFrame(frame);
544
+ const previousFrameId = driver.getCurrentFrameId();
823
545
  const previousFrameOffset = this.currentFrameOffset;
824
546
  const previousRequireExtension = this.requireExtension;
825
- this.extensionBridge.setCurrentFrameId(frameId);
547
+ driver.setCurrentFrameId(frameId);
826
548
  this.currentFrameOffset = offset;
827
549
  this.requireExtension = true;
828
550
  try {
829
551
  return await fn();
830
552
  }
831
553
  finally {
554
+ // 字段赋值不会 throw,driver.setCurrentFrameId 是同步函数也不会 throw,无需嵌套 try
832
555
  this.requireExtension = previousRequireExtension;
833
556
  this.currentFrameOffset = previousFrameOffset;
834
- this.extensionBridge.setCurrentFrameId(previousFrameId);
557
+ driver.setCurrentFrameId(previousFrameId);
835
558
  }
836
559
  }
837
560
  /**
838
561
  * 获取当前状态
839
562
  */
840
563
  getState() {
564
+ // driver.getState() 两侧都返回 DriverState | null({url, title}),SessionState 含 targetId 是其超集,结构兼容
841
565
  if (this.extensionBridge?.isConnected()) {
842
566
  return this.extensionBridge.getState();
843
567
  }
844
- const cdpState = getCdpSession().getState();
845
- return cdpState ? { url: cdpState.url, title: cdpState.title } : null;
568
+ return getCdpSession().getState();
846
569
  }
847
570
  /**
848
571
  * 关闭所有连接
@@ -857,110 +580,119 @@ class UnifiedSessionManager {
857
580
  /**
858
581
  * 按下键盘按键
859
582
  */
860
- async keyDown(key) {
583
+ async keyDown(key, commands) {
584
+ const driver = await this.getDriver();
585
+ const isExt = this.isExtensionConnected();
586
+ // 预检查:stealth + commands 必然抛错,要在任何状态变更前抛出,避免 modifiers 污染
587
+ if (isExt && this.inputMode === 'stealth' && commands && commands.length > 0) {
588
+ throw new Error('commands 参数不支持 stealth 输入模式(stealth 通过 JS 合成事件,无法触发 Chrome 原生编辑命令),请先调用 manage action=inputMode inputMode=precise 切换后重试');
589
+ }
590
+ // Puppeteer 风格:连续 keyDown 同 key → rawKeyDown + autoRepeat(长按重复)
591
+ const isRepeat = this.pressedKeys.has(key);
592
+ this.pressedKeys.add(key);
861
593
  if (MODIFIER_KEYS[key]) {
862
594
  this.modifiers |= MODIFIER_KEYS[key];
863
595
  }
864
- if (await this.ensureExtensionConnected()) {
865
- if (this.inputMode === 'stealth') {
866
- await this.extensionBridge.stealthKey(key, 'down', this.getModifierNames());
867
- }
868
- else {
869
- await this.extensionBridge.inputKey('keyDown', { key, code: key, modifiers: this.modifiers });
870
- }
596
+ if (isExt && this.inputMode === 'stealth') {
597
+ await driver.stealthKey(key, 'down', this.getModifierNames());
871
598
  return;
872
599
  }
873
- await getCdpSession().keyDown(key);
600
+ const def = getKeyDefinition(key);
601
+ await driver.inputKey(isRepeat ? 'rawKeyDown' : 'keyDown', {
602
+ key: def.key,
603
+ code: def.code,
604
+ text: def.text,
605
+ windowsVirtualKeyCode: def.keyCode,
606
+ modifiers: this.modifiers,
607
+ commands,
608
+ autoRepeat: isRepeat || undefined,
609
+ });
874
610
  }
875
611
  /**
876
612
  * 释放键盘按键
877
613
  */
878
614
  async keyUp(key) {
879
- if (await this.ensureExtensionConnected()) {
880
- if (this.inputMode === 'stealth') {
881
- await this.extensionBridge.stealthKey(key, 'up', this.getModifierNames());
882
- }
883
- else {
884
- await this.extensionBridge.inputKey('keyUp', { key, code: key, modifiers: this.modifiers });
885
- }
886
- if (MODIFIER_KEYS[key]) {
887
- this.modifiers &= ~MODIFIER_KEYS[key];
888
- }
889
- return;
615
+ const driver = await this.getDriver();
616
+ const isExt = this.isExtensionConnected();
617
+ if (isExt && this.inputMode === 'stealth') {
618
+ await driver.stealthKey(key, 'up', this.getModifierNames());
619
+ }
620
+ else {
621
+ const def = getKeyDefinition(key);
622
+ await driver.inputKey('keyUp', {
623
+ key: def.key,
624
+ code: def.code,
625
+ windowsVirtualKeyCode: def.keyCode,
626
+ modifiers: this.modifiers,
627
+ });
890
628
  }
891
629
  if (MODIFIER_KEYS[key]) {
892
630
  this.modifiers &= ~MODIFIER_KEYS[key];
893
631
  }
894
- await getCdpSession().keyUp(key);
632
+ this.pressedKeys.delete(key);
895
633
  }
896
634
  /**
897
635
  * 输入文本
898
636
  */
899
637
  async typeText(text, delay = 0) {
900
- if (await this.ensureExtensionConnected()) {
901
- if (this.inputMode === 'stealth') {
902
- await this.extensionBridge.stealthType(text, delay);
903
- }
904
- else {
905
- await this.extensionBridge.inputType(text, delay);
906
- }
907
- return;
638
+ const driver = await this.getDriver();
639
+ const isExt = this.isExtensionConnected();
640
+ if (isExt && this.inputMode === 'stealth') {
641
+ await driver.stealthType(text, delay);
642
+ }
643
+ else {
644
+ await driver.inputType(text, delay);
908
645
  }
909
- await getCdpSession().type(text, delay);
910
646
  }
911
647
  /**
912
648
  * 鼠标移动
913
649
  */
914
650
  async mouseMove(x, y) {
915
651
  this.currentMousePosition = { x, y }; // 更新位置
916
- if (await this.ensureExtensionConnected()) {
917
- if (this.inputMode === 'stealth') {
918
- await this.extensionBridge.stealthMouse('mousemove', x, y);
919
- }
920
- else {
921
- await this.extensionBridge.inputMouse('mouseMoved', x, y, { modifiers: this.modifiers });
922
- }
923
- return;
652
+ const driver = await this.getDriver();
653
+ const isExt = this.isExtensionConnected();
654
+ if (isExt && this.inputMode === 'stealth') {
655
+ await driver.stealthMouse('mousemove', x, y);
656
+ }
657
+ else {
658
+ await driver.inputMouse('mouseMoved', x, y, { modifiers: this.modifiers });
924
659
  }
925
- await getCdpSession().mouseMove(x, y);
926
660
  }
927
661
  /**
928
662
  * 鼠标按下
929
663
  */
930
- async mouseDown(button = 'left') {
931
- const effectiveButton = (button === 'back' || button === 'forward') ? 'left' : button;
932
- const { x, y } = this.currentMousePosition; // 使用当前位置
933
- if (await this.ensureExtensionConnected()) {
934
- if (this.inputMode === 'stealth') {
935
- await this.extensionBridge.stealthMouse('mousedown', x, y, effectiveButton);
936
- }
937
- else {
938
- await this.extensionBridge.inputMouse('mousePressed', x, y, {
939
- button: effectiveButton,
940
- clickCount: 1,
941
- modifiers: this.modifiers,
942
- });
943
- }
944
- return;
664
+ async mouseDown(button = 'left', clickCount = 1) {
665
+ const { x, y } = this.currentMousePosition;
666
+ const driver = await this.getDriver();
667
+ const isExt = this.isExtensionConnected();
668
+ if (isExt && this.inputMode === 'stealth') {
669
+ await driver.stealthMouse('mousedown', x, y, button);
670
+ }
671
+ else {
672
+ await driver.inputMouse('mousePressed', x, y, {
673
+ button,
674
+ clickCount,
675
+ modifiers: this.modifiers,
676
+ });
945
677
  }
946
- await getCdpSession().mouseDown(effectiveButton);
947
678
  }
948
679
  /**
949
680
  * 鼠标释放
950
681
  */
951
- async mouseUp(button = 'left') {
952
- const effectiveButton = (button === 'back' || button === 'forward') ? 'left' : button;
953
- const { x, y } = this.currentMousePosition; // 使用当前位置
954
- if (await this.ensureExtensionConnected()) {
955
- if (this.inputMode === 'stealth') {
956
- await this.extensionBridge.stealthMouse('mouseup', x, y, effectiveButton);
957
- }
958
- else {
959
- await this.extensionBridge.inputMouse('mouseReleased', x, y, { button: effectiveButton, clickCount: 1, modifiers: this.modifiers });
960
- }
961
- return;
682
+ async mouseUp(button = 'left', clickCount = 1) {
683
+ const { x, y } = this.currentMousePosition;
684
+ const driver = await this.getDriver();
685
+ const isExt = this.isExtensionConnected();
686
+ if (isExt && this.inputMode === 'stealth') {
687
+ await driver.stealthMouse('mouseup', x, y, button);
688
+ }
689
+ else {
690
+ await driver.inputMouse('mouseReleased', x, y, {
691
+ button,
692
+ clickCount,
693
+ modifiers: this.modifiers,
694
+ });
962
695
  }
963
- await getCdpSession().mouseUp(effectiveButton);
964
696
  }
965
697
  /**
966
698
  * 鼠标点击(mousedown + mouseup + click 三合一)
@@ -968,132 +700,132 @@ class UnifiedSessionManager {
968
700
  * stealth 模式:原子操作(单次脚本注入完成 mouseover → mousedown → focus → mouseup → click)
969
701
  * precise / CDP 模式:mouseDown + mouseUp,浏览器自动合成原生 click 事件
970
702
  */
971
- async mouseClick(button = 'left') {
972
- if (this.inputMode === 'stealth' && await this.ensureExtensionConnected()) {
973
- const effectiveButton = (button === 'back' || button === 'forward') ? 'left' : button;
703
+ async mouseClick(button = 'left', clickCount = 1, refId) {
704
+ const driver = await this.getDriver();
705
+ const isExt = this.isExtensionConnected();
706
+ if (this.inputMode === 'stealth' && isExt) {
974
707
  const { x, y } = this.currentMousePosition;
975
- await this.extensionBridge.stealthClick(x, y, effectiveButton);
708
+ // stealth 通过单次脚本注入完成所有 mouse/click/dblclick/contextmenu 事件
709
+ // refId 透传:嵌套 iframe overlay 场景下绕过 elementFromPoint 命中外层 IFRAME 的问题
710
+ await driver.stealthClick(x, y, button, clickCount, refId);
976
711
  return;
977
712
  }
978
- await this.mouseDown(button);
979
- await this.mouseUp(button);
713
+ // CDP 每次 mousePressed/mouseReleased 递增 clickCount,让浏览器合成 dblclick/tripleclick
714
+ for (let i = 1; i <= clickCount; i++) {
715
+ await this.mouseDown(button, i);
716
+ await this.mouseUp(button, i);
717
+ }
980
718
  }
981
719
  /**
982
720
  * 鼠标滚轮
983
721
  */
984
722
  async mouseWheel(deltaX, deltaY) {
985
- if (await this.ensureExtensionConnected()) {
986
- const { x, y } = this.currentMousePosition;
987
- await this.extensionBridge.inputMouse('mouseWheel', x, y, { deltaX, deltaY, modifiers: this.modifiers });
988
- return;
989
- }
990
- await getCdpSession().mouseWheel(deltaX, deltaY);
723
+ const { x, y } = this.currentMousePosition;
724
+ const driver = await this.getDriver();
725
+ await driver.inputMouse('mouseWheel', x, y, { deltaX, deltaY, modifiers: this.modifiers });
991
726
  }
992
727
  /**
993
728
  * 注入反检测脚本
994
729
  */
995
730
  async injectStealth() {
996
- if (await this.ensureExtensionConnected()) {
997
- await this.extensionBridge.stealthInject();
998
- return;
999
- }
1000
- throw new Error('CDP 模式下 stealth 脚本在 connect/launch 时通过 stealth 参数自动注入,不支持后续手动注入');
731
+ const driver = await this.getDriver();
732
+ await driver.stealthInject();
1001
733
  }
1002
- // ==================== 键鼠输入 ====================
1003
- // stealth 模式:使用 JS 事件模拟,不触发调试提示,推荐用于反检测场景
1004
- // precise 模式:使用 debugger API,精确但会显示"扩展程序正在调试此浏览器"
1005
734
  /**
1006
735
  * 触摸开始
1007
736
  */
1008
737
  async touchStart(x, y) {
1009
- if (await this.ensureExtensionConnected()) {
1010
- await this.extensionBridge.inputTouch('touchStart', [{ x, y, id: 0 }]);
1011
- return;
1012
- }
1013
- await getCdpSession().touchStart(x, y);
738
+ const driver = await this.getDriver();
739
+ await driver.inputTouch('touchStart', [{ x, y, id: 0 }]);
1014
740
  }
1015
741
  /**
1016
742
  * 触摸移动
1017
743
  */
1018
744
  async touchMove(x, y) {
1019
- if (await this.ensureExtensionConnected()) {
1020
- await this.extensionBridge.inputTouch('touchMove', [{ x, y, id: 0 }]);
1021
- return;
1022
- }
1023
- await getCdpSession().touchMove(x, y);
745
+ const driver = await this.getDriver();
746
+ await driver.inputTouch('touchMove', [{ x, y, id: 0 }]);
1024
747
  }
1025
748
  /**
1026
749
  * 触摸结束
1027
750
  */
1028
751
  async touchEnd() {
1029
- if (await this.ensureExtensionConnected()) {
1030
- await this.extensionBridge.inputTouch('touchEnd', []);
1031
- return;
1032
- }
1033
- await getCdpSession().touchEnd();
752
+ const driver = await this.getDriver();
753
+ await driver.inputTouch('touchEnd', []);
1034
754
  }
755
+ // ==================== 键鼠输入 ====================
756
+ // stealth 模式:使用 JS 事件模拟,不触发调试提示,推荐用于反检测场景
757
+ // precise 模式:使用 debugger API,精确但会显示"扩展程序正在调试此浏览器"
1035
758
  /**
1036
759
  * 启用控制台日志捕获
1037
760
  */
1038
761
  async enableConsole() {
1039
- if (await this.ensureExtensionConnected()) {
1040
- await this.extensionBridge.consoleEnable();
1041
- return;
1042
- }
1043
- // CDP 模式已经在 logs 工具中实现
762
+ const driver = await this.getDriver();
763
+ await driver.consoleEnable();
1044
764
  }
1045
765
  /**
1046
766
  * 获取控制台日志
1047
767
  */
1048
768
  async getConsoleLogs(options = {}) {
1049
- if (await this.ensureExtensionConnected()) {
1050
- return this.extensionBridge.consoleGet(options);
1051
- }
1052
- // CDP 模式需要单独实现
1053
- return [];
769
+ return (await this.getDriver()).getConsoleLogs(options);
1054
770
  }
1055
- /**
1056
- * 启用网络日志捕获
1057
- */
1058
- async enableNetwork() {
1059
- if (await this.ensureExtensionConnected()) {
1060
- await this.extensionBridge.networkEnable();
1061
- return;
1062
- }
1063
- // CDP 模式已在 session 中启用 Network.enable
1064
- }
1065
- /**
1066
- * 获取网络请求日志
1067
- */
1068
771
  async getNetworkRequests(options = {}) {
1069
- if (await this.ensureExtensionConnected()) {
1070
- return this.extensionBridge.networkGet(options);
1071
- }
1072
- return [];
772
+ return (await this.getDriver()).getNetworkRequests(options);
773
+ }
774
+ async enableNetwork() {
775
+ const driver = await this.getDriver();
776
+ await driver.networkEnable();
1073
777
  }
1074
778
  /**
1075
779
  * 发送 CDP 命令(高级用法)
1076
780
  *
1077
- * 自动识别 browser-level 域(Target、Browser、SystemInfo、DeviceAccess、IO)不携带 sessionId,
1078
- * 其他域默认携带 sessionId(page-level 命令)。
781
+ * Extension 模式:经 chrome.debugger.sendCommand 转发;
782
+ * CDP 模式:driver 自动识别 browser-level 域(Target/Browser/SystemInfo/DeviceAccess/IO)vs page-level
1079
783
  */
1080
784
  async sendCdpCommand(method, params) {
1081
- if (await this.ensureExtensionConnected()) {
1082
- return this.extensionBridge.debuggerSend(method, params);
785
+ const driver = await this.getDriver();
786
+ return driver.debuggerSend(method, params);
787
+ }
788
+ evaluateViaCdp(script, args, timeout) {
789
+ return getCdpSession().evaluate(script, args, timeout);
790
+ }
791
+ async evaluateViaExtensionPrecise(code, expression, args, timeout) {
792
+ const hasArgs = args !== undefined && args.length > 0;
793
+ const currentFrameId = this.extensionBridge.getCurrentFrameId();
794
+ // precise + args + 主 frame:使用 callFunctionOn 避免大 payload 字符串拼接
795
+ if (hasArgs && currentFrameId === 0) {
796
+ return this.callFunctionOn(code, args, timeout);
797
+ }
798
+ if (currentFrameId !== 0) {
799
+ // iframe:args 仍用字符串拼接(evaluateInFrame 使用 expression 字符串)
800
+ let iframeExpression = expression;
801
+ if (hasArgs) {
802
+ const argsStr = args.map((a) => JSON.stringify(a)).join(', ');
803
+ iframeExpression = `(${code})(${argsStr})`;
804
+ }
805
+ const result = (await this.extensionBridge.evaluateInFrame(currentFrameId, iframeExpression, timeout));
806
+ return this.checkCdpResult(result);
1083
807
  }
1084
- // CDP 模式:browser-level 域不携带 sessionId
1085
- const domain = method.split('.')[0];
1086
- const browserLevelDomains = ['Target', 'Browser', 'SystemInfo', 'DeviceAccess', 'IO'];
1087
- if (browserLevelDomains.includes(domain)) {
1088
- return getCdpSession().sendBrowserCommand(method, params);
808
+ // frame,无 args:直接 Runtime.evaluate
809
+ const params = {
810
+ expression,
811
+ returnByValue: true,
812
+ awaitPromise: true,
813
+ };
814
+ if (timeout !== undefined) {
815
+ params.timeout = timeout;
1089
816
  }
1090
- return getCdpSession().send(method, params);
817
+ // timeout 即端到端预算,直接作为 RPC 超时(不额外加 margin)
818
+ const result = (await this.extensionBridge.debuggerSend('Runtime.evaluate', params, undefined, timeout));
819
+ return this.checkCdpResult(result);
820
+ }
821
+ async evaluateViaExtensionStealth(expression, timeout) {
822
+ return (await this.extensionBridge.evaluate(expression, undefined, timeout));
1091
823
  }
1092
824
  /**
1093
825
  * Extension 操作断连自动重试
1094
826
  *
1095
- * 操作失败且错误为断连类型时,等待 2 秒让 Extension 重连后重试一次。
1096
- * 若重连失败且提供了 cdpFallback,则降级到 CDP 模式。
827
+ * 操作失败且错误为断连类型时,等待 2 秒让 Extension 重连后重试一次,
828
+ * 若重连失败且提供了 cdpFallback,则降级到 CDP 模式
1097
829
  */
1098
830
  async withExtensionRetry(operation, cdpFallback) {
1099
831
  try {
@@ -1101,11 +833,9 @@ class UnifiedSessionManager {
1101
833
  }
1102
834
  catch (err) {
1103
835
  const msg = err instanceof Error ? err.message : String(err);
1104
- if (msg.includes('Extension disconnected') ||
1105
- msg.includes('Connection replaced') ||
1106
- msg.includes('not connected')) {
836
+ if (isExtensionDisconnected(err) || msg.includes('not connected')) {
1107
837
  // 等待 Extension 重连
1108
- await new Promise(r => setTimeout(r, 2000));
838
+ await new Promise((r) => setTimeout(r, 2000));
1109
839
  if (this.isExtensionConnected()) {
1110
840
  return operation();
1111
841
  }
@@ -1117,51 +847,19 @@ class UnifiedSessionManager {
1117
847
  throw err;
1118
848
  }
1119
849
  }
1120
- /** CDP fallback:通过 Runtime.evaluate 注入 DOM 查询逻辑 */
1121
- async findViaCdp(selector, text, xpath, timeout) {
1122
- return getCdpSession().evaluate(`function(selector, text, xpath) {
1123
- var elements;
1124
- if (xpath) {
1125
- elements = [];
1126
- var xr = document.evaluate(xpath, document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
1127
- for (var i = 0; i < xr.snapshotLength; i++) {
1128
- var node = xr.snapshotItem(i);
1129
- if (node instanceof Element) elements.push(node);
1130
- }
1131
- } else if (selector) {
1132
- elements = Array.from(document.querySelectorAll(selector));
1133
- } else {
1134
- elements = Array.from(document.querySelectorAll('*'));
1135
- }
1136
- var results = [];
1137
- for (var j = 0; j < elements.length; j++) {
1138
- var el = elements[j];
1139
- if (text && !(el.textContent || '').includes(text)) continue;
1140
- var rect = el.getBoundingClientRect();
1141
- results.push({
1142
- refId: '',
1143
- tag: el.tagName.toLowerCase(),
1144
- text: (el.textContent || '').trim().substring(0, 100),
1145
- rect: {x: rect.x, y: rect.y, width: rect.width, height: rect.height}
1146
- });
1147
- if (results.length >= 50) break;
1148
- }
1149
- return results;
1150
- }`, [selector ?? null, text ?? null, xpath ?? null], timeout);
1151
- }
1152
850
  /** 获取当前修饰键名称数组(stealth 模式用) */
1153
851
  getModifierNames() {
1154
852
  const names = [];
1155
- if (this.modifiers & 1) {
853
+ if (this.modifiers & MODIFIER_KEYS.Alt) {
1156
854
  names.push('alt');
1157
855
  }
1158
- if (this.modifiers & 2) {
856
+ if (this.modifiers & MODIFIER_KEYS.Control) {
1159
857
  names.push('ctrl');
1160
858
  }
1161
- if (this.modifiers & 4) {
859
+ if (this.modifiers & MODIFIER_KEYS.Meta) {
1162
860
  names.push('meta');
1163
861
  }
1164
- if (this.modifiers & 8) {
862
+ if (this.modifiers & MODIFIER_KEYS.Shift) {
1165
863
  names.push('shift');
1166
864
  }
1167
865
  return names;
@@ -1179,8 +877,8 @@ class UnifiedSessionManager {
1179
877
  /**
1180
878
  * 检查 CDP 回退是否允许
1181
879
  *
1182
- * 当 requireExtension 为 true(tabId 或 frame 已指定)时,CDP 回退会操作错误目标,必须阻止。
1183
- * 允许时返回 false(供 ensureExtensionConnected 直接返回),不允许时抛出。
880
+ * 当 requireExtension 为 true(tabId 或 frame 已指定)时,CDP 回退会操作错误目标,必须阻止,
881
+ * 允许时返回 false(供 ensureExtensionConnected 直接返回),不允许时抛出
1184
882
  */
1185
883
  assertCdpFallbackAllowed() {
1186
884
  if (this.requireExtension) {
@@ -1192,16 +890,22 @@ class UnifiedSessionManager {
1192
890
  * 确保 Extension 已连接,如果断开则等待重连
1193
891
  * 返回 true 表示 Extension 可用,false 表示应 fallback 到 CDP
1194
892
  *
1195
- * 设计理念:Server 和 Extension 的启动时机完全独立,无任何要求。
893
+ * 设计理念:Server 和 Extension 的启动时机完全独立,无任何要求
1196
894
  * - 先装 Extension,一个月/一年后启动 Server → 能连上
1197
895
  * - 先启动 Server,再打开 Chrome → 能连上
1198
896
  * - 关闭再打开任何一方 → 能自动重连
1199
897
  *
1200
- * 超时设为 30 秒:足够等待 Extension 启动,但不会永远卡住。
898
+ * 超时设为 30 秒:足够等待 Extension 启动,但不会永远卡住
1201
899
  *
1202
- * @param maxWait 调用方的端到端预算(毫秒)。传入时取 min(maxWait, 30000) 作为连接等待上限,
1203
- * 避免工具 timeout 被连接等待吞掉。不传则使用默认 30s
900
+ * @param timeout 调用方的端到端预算(毫秒),传入时取 min(timeout, 30000) 作为连接等待上限,
901
+ * 避免工具 timeout 被连接等待吞掉,不传则使用默认 30s
1204
902
  */
903
+ async getDriver(timeout) {
904
+ if (await this.ensureExtensionConnected(timeout)) {
905
+ return this.extensionBridge;
906
+ }
907
+ return getCdpSession();
908
+ }
1205
909
  async ensureExtensionConnected(maxWait) {
1206
910
  if (!this.extensionBridge) {
1207
911
  return this.assertCdpFallbackAllowed();
@@ -1238,46 +942,45 @@ class UnifiedSessionManager {
1238
942
  /**
1239
943
  * 通过 callFunctionOn 执行函数调用
1240
944
  *
1241
- * 参数通过 CDP 协议结构化传递,避免大 payload 字符串拼接导致的长度限制和转义问题。
1242
- * 要求 code 必须是函数表达式(如 "(x) => x + 1")。
945
+ * 参数通过 CDP 协议结构化传递,避免大 payload 字符串拼接导致的长度限制和转义问题,
946
+ * 要求 code 必须是函数表达式(如 "(x) => x + 1"
1243
947
  */
1244
948
  async callFunctionOn(code, args, timeout) {
1245
- const globalResult = await this.extensionBridge.debuggerSend('Runtime.evaluate', {
949
+ const globalResult = (await this.extensionBridge.debuggerSend('Runtime.evaluate', {
1246
950
  expression: 'globalThis',
1247
951
  returnByValue: false,
1248
- }, undefined, timeout);
952
+ }, undefined, timeout));
1249
953
  try {
1250
954
  const params = {
1251
955
  functionDeclaration: code,
1252
956
  objectId: globalResult.result.objectId,
1253
- arguments: args.map(a => ({ value: a })),
957
+ arguments: args.map((a) => ({ value: a })),
1254
958
  returnByValue: true,
1255
959
  awaitPromise: true,
1256
960
  };
1257
961
  if (timeout !== undefined) {
1258
962
  params.timeout = timeout;
1259
963
  }
1260
- const result = await this.extensionBridge.debuggerSend('Runtime.callFunctionOn', params, undefined, timeout);
964
+ const result = (await this.extensionBridge.debuggerSend('Runtime.callFunctionOn', params, undefined, timeout));
1261
965
  return this.checkCdpResult(result);
1262
966
  }
1263
967
  finally {
1264
968
  this.extensionBridge.debuggerSend('Runtime.releaseObject', {
1265
969
  objectId: globalResult.result.objectId,
1266
- }).catch(() => {
1267
- });
970
+ }).catch(() => { });
1268
971
  }
1269
972
  }
1270
973
  /**
1271
- * 串行化所有 tab 切换操作,防止并发请求互相覆盖 currentTabId
1272
- * 调用者:selectPage/activatePage/newPage/closePage/navigate/reload/launch/withTabId
974
+ * 串行化所有 tab 切换操作,防止并发请求互相覆盖 currentTabId
975
+ * 调用者:selectPage/activatePage/newPage/closePage/navigate/reload/launch/withTabId
1273
976
  *
1274
- * 注意:此锁不可重入。fn() 内禁止调用任何使用 withTabLock 的方法,否则会死锁。
1275
- * 当前所有 fn() 只调用 bridge 的原子操作(createTab/navigate/evaluate 等),不存在此问题。
977
+ * 注意:此锁不可重入,fn() 内禁止调用任何使用 withTabLock 的方法,否则会死锁,
978
+ * 当前所有 fn() 只调用 bridge 的原子操作(createTab/navigate/evaluate 等),不存在此问题
1276
979
  */
1277
980
  async withTabLock(fn) {
1278
981
  const previousLock = this.tabSwitchLock;
1279
982
  let releaseLock;
1280
- this.tabSwitchLock = new Promise(resolve => {
983
+ this.tabSwitchLock = new Promise((resolve) => {
1281
984
  releaseLock = resolve;
1282
985
  });
1283
986
  try {