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