@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.
- package/README.md +71 -31
- package/dist/anti-detection/behavior.d.ts.map +1 -1
- package/dist/anti-detection/behavior.js.map +1 -1
- package/dist/anti-detection/index.d.ts +1 -1
- package/dist/anti-detection/index.d.ts.map +1 -1
- package/dist/anti-detection/index.js +1 -1
- package/dist/anti-detection/index.js.map +1 -1
- package/dist/anti-detection/injection.d.ts +6 -2
- package/dist/anti-detection/injection.d.ts.map +1 -1
- package/dist/anti-detection/injection.js +32 -79
- package/dist/anti-detection/injection.js.map +1 -1
- package/dist/cdp/client.d.ts +2 -2
- package/dist/cdp/client.d.ts.map +1 -1
- package/dist/cdp/client.js +8 -10
- package/dist/cdp/client.js.map +1 -1
- package/dist/cdp/index.d.ts.map +1 -1
- package/dist/cdp/index.js.map +1 -1
- package/dist/cdp/launcher.d.ts.map +1 -1
- package/dist/cdp/launcher.js +40 -13
- package/dist/cdp/launcher.js.map +1 -1
- package/dist/core/auto-wait.d.ts +2 -2
- package/dist/core/auto-wait.d.ts.map +1 -1
- package/dist/core/auto-wait.js +2 -2
- package/dist/core/auto-wait.js.map +1 -1
- package/dist/core/browser-driver.d.ts +307 -0
- package/dist/core/browser-driver.d.ts.map +1 -0
- package/dist/core/browser-driver.js +21 -0
- package/dist/core/browser-driver.js.map +1 -0
- package/dist/core/error-sanitizer.d.ts +25 -0
- package/dist/core/error-sanitizer.d.ts.map +1 -0
- package/dist/core/error-sanitizer.js +66 -0
- package/dist/core/error-sanitizer.js.map +1 -0
- package/dist/core/errors.d.ts +10 -1
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js +17 -4
- package/dist/core/errors.js.map +1 -1
- package/dist/core/extension-errors.d.ts +20 -0
- package/dist/core/extension-errors.d.ts.map +1 -0
- package/dist/core/extension-errors.js +40 -0
- package/dist/core/extension-errors.js.map +1 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/locator.d.ts +2 -2
- package/dist/core/locator.d.ts.map +1 -1
- package/dist/core/locator.js +25 -65
- package/dist/core/locator.js.map +1 -1
- package/dist/core/retry.d.ts +2 -2
- package/dist/core/retry.d.ts.map +1 -1
- package/dist/core/retry.js +2 -2
- package/dist/core/retry.js.map +1 -1
- package/dist/core/session.d.ts +153 -46
- package/dist/core/session.d.ts.map +1 -1
- package/dist/core/session.js +672 -177
- package/dist/core/session.js.map +1 -1
- package/dist/core/types.d.ts +9 -3
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js +13 -6
- package/dist/core/types.js.map +1 -1
- package/dist/core/unified-session.d.ts +46 -81
- package/dist/core/unified-session.d.ts.map +1 -1
- package/dist/core/unified-session.js +338 -635
- package/dist/core/unified-session.js.map +1 -1
- package/dist/core/utils.d.ts +7 -0
- package/dist/core/utils.d.ts.map +1 -0
- package/dist/core/utils.js +33 -0
- package/dist/core/utils.js.map +1 -0
- package/dist/extension/bridge.d.ts +69 -50
- package/dist/extension/bridge.d.ts.map +1 -1
- package/dist/extension/bridge.js +176 -77
- package/dist/extension/bridge.js.map +1 -1
- package/dist/extension/http-server.d.ts +6 -4
- package/dist/extension/http-server.d.ts.map +1 -1
- package/dist/extension/http-server.js +45 -31
- package/dist/extension/http-server.js.map +1 -1
- package/dist/extension/index.d.ts.map +1 -1
- package/dist/extension/index.js.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/tools/browse.d.ts.map +1 -1
- package/dist/tools/browse.js +32 -34
- package/dist/tools/browse.js.map +1 -1
- package/dist/tools/cookies.d.ts.map +1 -1
- package/dist/tools/cookies.js +38 -16
- package/dist/tools/cookies.js.map +1 -1
- package/dist/tools/evaluate.d.ts.map +1 -1
- package/dist/tools/evaluate.js +54 -23
- package/dist/tools/evaluate.js.map +1 -1
- package/dist/tools/extract.d.ts.map +1 -1
- package/dist/tools/extract.js +221 -153
- package/dist/tools/extract.js.map +1 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/input.d.ts.map +1 -1
- package/dist/tools/input.js +271 -90
- package/dist/tools/input.js.map +1 -1
- package/dist/tools/logs.d.ts.map +1 -1
- package/dist/tools/logs.js +31 -17
- package/dist/tools/logs.js.map +1 -1
- package/dist/tools/manage.d.ts.map +1 -1
- package/dist/tools/manage.js +25 -28
- package/dist/tools/manage.js.map +1 -1
- package/dist/tools/schema.d.ts +1 -1
- package/dist/tools/schema.d.ts.map +1 -1
- package/dist/tools/schema.js +31 -55
- package/dist/tools/schema.js.map +1 -1
- package/dist/tools/wait.d.ts.map +1 -1
- package/dist/tools/wait.js +19 -16
- package/dist/tools/wait.js.map +1 -1
- 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 {
|
|
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
|
-
//
|
|
107
|
-
const
|
|
108
|
-
return this.extensionBridge.
|
|
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:
|
|
112
|
-
type: 'page',
|
|
113
|
-
url:
|
|
114
|
-
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
|
-
|
|
130
|
-
if (
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
164
|
-
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
|
|
204
|
-
|
|
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
|
-
|
|
216
|
-
|
|
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
|
-
|
|
239
|
-
|
|
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
|
-
|
|
249
|
-
|
|
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
|
-
|
|
262
|
-
|
|
263
|
-
}
|
|
264
|
-
throw new Error('actionableClick 仅支持 Extension 模式');
|
|
200
|
+
const driver = await this.getDriver();
|
|
201
|
+
return driver.actionableClick(refId, force);
|
|
265
202
|
}
|
|
266
203
|
/**
|
|
267
|
-
*
|
|
204
|
+
* dispatch 模式输入(ISOLATED 世界,兼容 React/Vue 受控组件)
|
|
268
205
|
*/
|
|
269
|
-
async
|
|
270
|
-
|
|
271
|
-
|
|
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
|
-
*
|
|
211
|
+
* HTML5 drag/drop(ISOLATED 世界,通过 refId 访问 __mcpElementMap 中的元素引用)
|
|
277
212
|
*/
|
|
278
|
-
async
|
|
279
|
-
|
|
280
|
-
|
|
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
|
-
|
|
289
|
-
|
|
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
|
-
|
|
298
|
-
|
|
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
|
-
|
|
308
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
(
|
|
392
|
-
|
|
393
|
-
|
|
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,
|
|
298
|
+
return this.evaluate(code, mode, remainingTimeout, args, true);
|
|
397
299
|
}
|
|
398
300
|
// Extension 重连失败,尝试 CDP fallback
|
|
399
301
|
if (!this.requireExtension) {
|
|
400
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
443
|
-
|
|
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
|
-
|
|
469
|
-
|
|
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
|
-
|
|
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
|
|
393
|
+
return getCdpSession().find(selector, text, xpath, timeout);
|
|
558
394
|
}
|
|
559
|
-
//
|
|
560
|
-
|
|
561
|
-
|
|
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
|
-
|
|
570
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
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:
|
|
702
|
-
type: 'page',
|
|
703
|
-
url:
|
|
704
|
-
title:
|
|
431
|
+
targetId: result.targetId,
|
|
432
|
+
type: result.type ?? 'page',
|
|
433
|
+
url: result.url,
|
|
434
|
+
title: result.title,
|
|
705
435
|
};
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
|
|
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
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
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
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
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
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
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
|
|
510
|
+
const driver = this.extensionBridge;
|
|
790
511
|
return this.withTabLock(async () => {
|
|
791
|
-
const
|
|
792
|
-
|
|
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
|
-
|
|
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
|
|
822
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
865
|
-
|
|
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
|
-
|
|
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
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
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
|
-
|
|
632
|
+
this.pressedKeys.delete(key);
|
|
895
633
|
}
|
|
896
634
|
/**
|
|
897
635
|
* 输入文本
|
|
898
636
|
*/
|
|
899
637
|
async typeText(text, delay = 0) {
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
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
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
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
|
|
932
|
-
const
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
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
|
|
953
|
-
const
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
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
|
-
|
|
973
|
-
|
|
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
|
-
|
|
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
|
-
|
|
979
|
-
|
|
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
|
-
|
|
986
|
-
|
|
987
|
-
|
|
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
|
-
|
|
997
|
-
|
|
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
|
-
|
|
1010
|
-
|
|
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
|
-
|
|
1020
|
-
|
|
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
|
-
|
|
1030
|
-
|
|
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
|
-
|
|
1040
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
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
|
-
*
|
|
1078
|
-
*
|
|
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
|
-
|
|
1082
|
-
|
|
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
|
-
//
|
|
1085
|
-
const
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
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
|
-
|
|
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('
|
|
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 &
|
|
853
|
+
if (this.modifiers & MODIFIER_KEYS.Alt) {
|
|
1156
854
|
names.push('alt');
|
|
1157
855
|
}
|
|
1158
|
-
if (this.modifiers &
|
|
856
|
+
if (this.modifiers & MODIFIER_KEYS.Control) {
|
|
1159
857
|
names.push('ctrl');
|
|
1160
858
|
}
|
|
1161
|
-
if (this.modifiers &
|
|
859
|
+
if (this.modifiers & MODIFIER_KEYS.Meta) {
|
|
1162
860
|
names.push('meta');
|
|
1163
861
|
}
|
|
1164
|
-
if (this.modifiers &
|
|
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
|
|
1203
|
-
* 避免工具 timeout
|
|
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
|
-
*
|
|
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 {
|