@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.
- package/README.md +101 -43
- 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 +11 -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 +69 -68
- package/dist/core/unified-session.d.ts.map +1 -1
- package/dist/core/unified-session.js +356 -615
- 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 +80 -39
- package/dist/extension/bridge.d.ts.map +1 -1
- package/dist/extension/bridge.js +195 -65
- 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 +27 -3
- package/dist/index.js.map +1 -1
- package/dist/tools/browse.d.ts.map +1 -1
- package/dist/tools/browse.js +33 -35
- 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 +59 -13
- package/dist/tools/evaluate.js.map +1 -1
- package/dist/tools/extract.d.ts.map +1 -1
- package/dist/tools/extract.js +263 -155
- 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 +311 -75
- 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 +73 -22
- 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,151 +128,112 @@ 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
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
|
|
260
|
-
|
|
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
|
-
|
|
270
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
(
|
|
354
|
-
|
|
355
|
-
|
|
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,
|
|
298
|
+
return this.evaluate(code, mode, remainingTimeout, args, true);
|
|
359
299
|
}
|
|
360
300
|
// Extension 重连失败,尝试 CDP fallback
|
|
361
301
|
if (!this.requireExtension) {
|
|
362
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
405
|
-
|
|
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
|
-
|
|
431
|
-
|
|
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
|
-
|
|
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
|
|
393
|
+
return getCdpSession().find(selector, text, xpath, timeout);
|
|
520
394
|
}
|
|
521
|
-
//
|
|
522
|
-
|
|
523
|
-
|
|
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
|
-
|
|
532
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
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:
|
|
664
|
-
type: 'page',
|
|
665
|
-
url:
|
|
666
|
-
title:
|
|
431
|
+
targetId: result.targetId,
|
|
432
|
+
type: result.type ?? 'page',
|
|
433
|
+
url: result.url,
|
|
434
|
+
title: result.title,
|
|
667
435
|
};
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
|
|
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
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
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
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
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
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
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
|
|
510
|
+
const driver = this.extensionBridge;
|
|
752
511
|
return this.withTabLock(async () => {
|
|
753
|
-
const
|
|
754
|
-
|
|
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
|
-
|
|
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
|
|
784
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
827
|
-
|
|
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
|
-
|
|
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
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
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
|
-
|
|
632
|
+
this.pressedKeys.delete(key);
|
|
857
633
|
}
|
|
858
634
|
/**
|
|
859
635
|
* 输入文本
|
|
860
636
|
*/
|
|
861
637
|
async typeText(text, delay = 0) {
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
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
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
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
|
|
894
|
-
const
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
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
|
|
915
|
-
const
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
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
|
-
|
|
935
|
-
|
|
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
|
-
|
|
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
|
-
|
|
941
|
-
|
|
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
|
-
|
|
948
|
-
|
|
949
|
-
|
|
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
|
-
|
|
959
|
-
|
|
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
|
-
|
|
972
|
-
|
|
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
|
-
|
|
982
|
-
|
|
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
|
-
|
|
992
|
-
|
|
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
|
-
|
|
1002
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
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
|
-
*
|
|
1040
|
-
*
|
|
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
|
-
|
|
1044
|
-
|
|
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
|
-
//
|
|
1047
|
-
const
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
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
|
-
|
|
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('
|
|
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 &
|
|
853
|
+
if (this.modifiers & MODIFIER_KEYS.Alt) {
|
|
1118
854
|
names.push('alt');
|
|
1119
855
|
}
|
|
1120
|
-
if (this.modifiers &
|
|
856
|
+
if (this.modifiers & MODIFIER_KEYS.Control) {
|
|
1121
857
|
names.push('ctrl');
|
|
1122
858
|
}
|
|
1123
|
-
if (this.modifiers &
|
|
859
|
+
if (this.modifiers & MODIFIER_KEYS.Meta) {
|
|
1124
860
|
names.push('meta');
|
|
1125
861
|
}
|
|
1126
|
-
if (this.modifiers &
|
|
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
|
|
1165
|
-
* 避免工具 timeout
|
|
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
|
-
*
|
|
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 {
|