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