@pyrokine/mcp-chrome 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/README.md +333 -0
  2. package/dist/anti-detection/behavior.d.ts +58 -0
  3. package/dist/anti-detection/behavior.d.ts.map +1 -0
  4. package/dist/anti-detection/behavior.js +113 -0
  5. package/dist/anti-detection/behavior.js.map +1 -0
  6. package/dist/anti-detection/index.d.ts +6 -0
  7. package/dist/anti-detection/index.d.ts.map +1 -0
  8. package/dist/anti-detection/index.js +6 -0
  9. package/dist/anti-detection/index.js.map +1 -0
  10. package/dist/anti-detection/injection.d.ts +19 -0
  11. package/dist/anti-detection/injection.d.ts.map +1 -0
  12. package/dist/anti-detection/injection.js +270 -0
  13. package/dist/anti-detection/injection.js.map +1 -0
  14. package/dist/cdp/client.d.ts +73 -0
  15. package/dist/cdp/client.d.ts.map +1 -0
  16. package/dist/cdp/client.js +275 -0
  17. package/dist/cdp/client.js.map +1 -0
  18. package/dist/cdp/index.d.ts +6 -0
  19. package/dist/cdp/index.d.ts.map +1 -0
  20. package/dist/cdp/index.js +6 -0
  21. package/dist/cdp/index.js.map +1 -0
  22. package/dist/cdp/launcher.d.ts +42 -0
  23. package/dist/cdp/launcher.d.ts.map +1 -0
  24. package/dist/cdp/launcher.js +181 -0
  25. package/dist/cdp/launcher.js.map +1 -0
  26. package/dist/core/auto-wait.d.ts +71 -0
  27. package/dist/core/auto-wait.d.ts.map +1 -0
  28. package/dist/core/auto-wait.js +165 -0
  29. package/dist/core/auto-wait.js.map +1 -0
  30. package/dist/core/errors.d.ts +123 -0
  31. package/dist/core/errors.d.ts.map +1 -0
  32. package/dist/core/errors.js +226 -0
  33. package/dist/core/errors.js.map +1 -0
  34. package/dist/core/index.d.ts +10 -0
  35. package/dist/core/index.d.ts.map +1 -0
  36. package/dist/core/index.js +10 -0
  37. package/dist/core/index.js.map +1 -0
  38. package/dist/core/locator.d.ts +130 -0
  39. package/dist/core/locator.d.ts.map +1 -0
  40. package/dist/core/locator.js +402 -0
  41. package/dist/core/locator.js.map +1 -0
  42. package/dist/core/retry.d.ts +27 -0
  43. package/dist/core/retry.d.ts.map +1 -0
  44. package/dist/core/retry.js +51 -0
  45. package/dist/core/retry.js.map +1 -0
  46. package/dist/core/session.d.ts +254 -0
  47. package/dist/core/session.d.ts.map +1 -0
  48. package/dist/core/session.js +893 -0
  49. package/dist/core/session.js.map +1 -0
  50. package/dist/core/types.d.ts +263 -0
  51. package/dist/core/types.d.ts.map +1 -0
  52. package/dist/core/types.js +90 -0
  53. package/dist/core/types.js.map +1 -0
  54. package/dist/index.d.ts +14 -0
  55. package/dist/index.d.ts.map +1 -0
  56. package/dist/index.js +121 -0
  57. package/dist/index.js.map +1 -0
  58. package/dist/tools/browse.d.ts +92 -0
  59. package/dist/tools/browse.d.ts.map +1 -0
  60. package/dist/tools/browse.js +368 -0
  61. package/dist/tools/browse.js.map +1 -0
  62. package/dist/tools/cookies.d.ts +75 -0
  63. package/dist/tools/cookies.d.ts.map +1 -0
  64. package/dist/tools/cookies.js +230 -0
  65. package/dist/tools/cookies.js.map +1 -0
  66. package/dist/tools/evaluate.d.ts +45 -0
  67. package/dist/tools/evaluate.d.ts.map +1 -0
  68. package/dist/tools/evaluate.js +85 -0
  69. package/dist/tools/evaluate.js.map +1 -0
  70. package/dist/tools/extract.d.ts +213 -0
  71. package/dist/tools/extract.d.ts.map +1 -0
  72. package/dist/tools/extract.js +304 -0
  73. package/dist/tools/extract.js.map +1 -0
  74. package/dist/tools/index.d.ts +13 -0
  75. package/dist/tools/index.d.ts.map +1 -0
  76. package/dist/tools/index.js +13 -0
  77. package/dist/tools/index.js.map +1 -0
  78. package/dist/tools/input.d.ts +241 -0
  79. package/dist/tools/input.d.ts.map +1 -0
  80. package/dist/tools/input.js +325 -0
  81. package/dist/tools/input.js.map +1 -0
  82. package/dist/tools/logs.d.ts +57 -0
  83. package/dist/tools/logs.d.ts.map +1 -0
  84. package/dist/tools/logs.js +165 -0
  85. package/dist/tools/logs.js.map +1 -0
  86. package/dist/tools/manage.d.ts +65 -0
  87. package/dist/tools/manage.d.ts.map +1 -0
  88. package/dist/tools/manage.js +270 -0
  89. package/dist/tools/manage.js.map +1 -0
  90. package/dist/tools/schema.d.ts +261 -0
  91. package/dist/tools/schema.d.ts.map +1 -0
  92. package/dist/tools/schema.js +151 -0
  93. package/dist/tools/schema.js.map +1 -0
  94. package/dist/tools/wait.d.ts +203 -0
  95. package/dist/tools/wait.d.ts.map +1 -0
  96. package/dist/tools/wait.js +254 -0
  97. package/dist/tools/wait.js.map +1 -0
  98. package/package.json +43 -0
  99. package/scripts/start-chrome-headless.sh +37 -0
  100. package/scripts/start-chrome.sh +41 -0
@@ -0,0 +1,893 @@
1
+ /**
2
+ * 会话管理
3
+ *
4
+ * 管理浏览器会话,包括:
5
+ * - CDP 客户端
6
+ * - 反检测注入
7
+ * - 页面状态
8
+ * - 日志收集
9
+ */
10
+ import { BehaviorSimulator, getAntiDetectionScript } from '../anti-detection/index.js';
11
+ import { BrowserLauncher, CDPClient, getBrowserWSEndpoint, getTargets } from '../cdp/index.js';
12
+ import { AutoWait } from './auto-wait.js';
13
+ import { NavigationTimeoutError, SessionNotFoundError, TargetNotFoundError } from './errors.js';
14
+ import { Locator } from './locator.js';
15
+ /**
16
+ * 会话管理器(单例)
17
+ */
18
+ class SessionManager {
19
+ static instance;
20
+ // 日志收集(环形缓冲区,限制最大条数避免内存泄漏)
21
+ static MAX_LOG_ENTRIES = 1000;
22
+ launcher = null;
23
+ cdp = null;
24
+ sessionId = null;
25
+ currentTargetId = null;
26
+ state = null;
27
+ behaviorSimulator = new BehaviorSimulator();
28
+ stealthMode = 'safe';
29
+ // 操作锁(防止并发竞态)
30
+ operationLock = Promise.resolve();
31
+ consoleLogs = [];
32
+ networkRequests = [];
33
+ requestMap = new Map();
34
+ // 监听器安装标志(防止重复安装)
35
+ listenersInstalled = false;
36
+ constructor() {
37
+ }
38
+ static getInstance() {
39
+ if (!SessionManager.instance) {
40
+ SessionManager.instance = new SessionManager();
41
+ }
42
+ return SessionManager.instance;
43
+ }
44
+ /**
45
+ * 启动浏览器
46
+ */
47
+ async launch(options = {}) {
48
+ return this.withLock(async () => {
49
+ // 关闭现有会话
50
+ await this.close();
51
+ // 保存 stealth 模式
52
+ this.stealthMode = options.stealth ?? 'safe';
53
+ // 启动浏览器
54
+ this.launcher = new BrowserLauncher();
55
+ const endpoint = await this.launcher.launch(options);
56
+ // 连接 CDP
57
+ this.cdp = new CDPClient();
58
+ await this.cdp.connect(endpoint, options.timeout);
59
+ // 获取第一个页面
60
+ const targets = await getTargets('127.0.0.1', this.launcher.port);
61
+ const pageTarget = targets.find((t) => t.type === 'page');
62
+ if (!pageTarget) {
63
+ throw new Error('未找到页面');
64
+ }
65
+ // 附加到页面
66
+ await this.attachToTarget(pageTarget.id);
67
+ return {
68
+ targetId: pageTarget.id,
69
+ type: pageTarget.type,
70
+ url: pageTarget.url,
71
+ title: pageTarget.title,
72
+ };
73
+ });
74
+ }
75
+ /**
76
+ * 连接到已运行的浏览器
77
+ */
78
+ async connect(options) {
79
+ return this.withLock(async () => {
80
+ const { host = '127.0.0.1', port, timeout = 30000, stealth = 'safe' } = options;
81
+ // 关闭现有会话
82
+ await this.close();
83
+ // 保存 stealth 模式
84
+ this.stealthMode = stealth;
85
+ // 获取 WebSocket 端点
86
+ const endpoint = await getBrowserWSEndpoint(host, port);
87
+ // 连接 CDP
88
+ this.cdp = new CDPClient();
89
+ await this.cdp.connect(endpoint, timeout);
90
+ // 获取第一个页面
91
+ const targets = await getTargets(host, port);
92
+ const pageTarget = targets.find((t) => t.type === 'page');
93
+ if (!pageTarget) {
94
+ throw new Error('未找到页面');
95
+ }
96
+ // 附加到页面
97
+ await this.attachToTarget(pageTarget.id);
98
+ return {
99
+ targetId: pageTarget.id,
100
+ type: pageTarget.type,
101
+ url: pageTarget.url,
102
+ title: pageTarget.title,
103
+ };
104
+ });
105
+ }
106
+ /**
107
+ * 列出所有可用页面
108
+ */
109
+ async listTargets() {
110
+ this.ensureConnected();
111
+ // 从 CDP 获取 targets
112
+ const { targetInfos } = (await this.cdp.send('Target.getTargets'));
113
+ return targetInfos
114
+ .filter((t) => t.type === 'page')
115
+ .map((t) => ({
116
+ targetId: t.targetId,
117
+ type: t.type,
118
+ url: t.url,
119
+ title: t.title,
120
+ }));
121
+ }
122
+ /**
123
+ * 附加到指定页面
124
+ */
125
+ async attachToTarget(targetId) {
126
+ this.ensureConnected();
127
+ // 如果已经附加到同一个 target,跳过
128
+ if (this.currentTargetId === targetId && this.sessionId) {
129
+ return;
130
+ }
131
+ // 如果有之前的 session,先分离
132
+ if (this.sessionId) {
133
+ try {
134
+ await this.cdp.send('Target.detachFromTarget', {
135
+ sessionId: this.sessionId,
136
+ });
137
+ }
138
+ catch {
139
+ // 忽略分离错误
140
+ }
141
+ }
142
+ // 附加到目标
143
+ const { sessionId } = (await this.cdp.send('Target.attachToTarget', {
144
+ targetId,
145
+ flatten: true,
146
+ }));
147
+ this.sessionId = sessionId;
148
+ this.currentTargetId = targetId;
149
+ // 初始化会话
150
+ await this.initSession();
151
+ }
152
+ /**
153
+ * 导航到 URL
154
+ */
155
+ async navigate(url, options = {}) {
156
+ return this.withLock(async () => {
157
+ this.ensureSession();
158
+ const { wait = 'load', timeout = 30000 } = options;
159
+ // 导航
160
+ const { errorText } = (await this.send('Page.navigate', { url }));
161
+ if (errorText) {
162
+ throw new NavigationTimeoutError(url, timeout);
163
+ }
164
+ // 根据 wait 类型等待
165
+ if (wait === 'networkidle') {
166
+ await this.waitForNetworkIdle(timeout);
167
+ }
168
+ else {
169
+ const eventName = wait === 'domcontentloaded'
170
+ ? 'Page.domContentEventFired'
171
+ : 'Page.loadEventFired';
172
+ await this.cdp.waitForEvent(eventName, undefined, timeout);
173
+ }
174
+ // 更新状态
175
+ await this.updateState();
176
+ });
177
+ }
178
+ /**
179
+ * 等待网络空闲(无进行中的请求且持续指定时间)
180
+ */
181
+ async waitForNetworkIdle(timeout, idleTime = 500) {
182
+ this.ensureSession();
183
+ // 使用局部 Set 追踪本次等待的请求,避免污染成员变量
184
+ const localPendingRequests = new Set();
185
+ return new Promise((resolve, reject) => {
186
+ let idleTimer = null;
187
+ let timeoutTimer = null;
188
+ const checkIdle = () => {
189
+ if (localPendingRequests.size === 0) {
190
+ if (idleTimer === null) {
191
+ idleTimer = setTimeout(() => {
192
+ cleanup();
193
+ resolve();
194
+ }, idleTime);
195
+ }
196
+ }
197
+ else {
198
+ if (idleTimer !== null) {
199
+ clearTimeout(idleTimer);
200
+ idleTimer = null;
201
+ }
202
+ }
203
+ };
204
+ const onRequestStart = (params) => {
205
+ const { requestId } = params;
206
+ localPendingRequests.add(requestId);
207
+ checkIdle();
208
+ };
209
+ const onRequestEnd = (params) => {
210
+ const { requestId } = params;
211
+ localPendingRequests.delete(requestId);
212
+ checkIdle();
213
+ };
214
+ const cleanup = () => {
215
+ if (idleTimer !== null) {
216
+ clearTimeout(idleTimer);
217
+ }
218
+ if (timeoutTimer !== null) {
219
+ clearTimeout(timeoutTimer);
220
+ }
221
+ this.cdp.offEvent('Network.requestWillBeSent', onRequestStart);
222
+ this.cdp.offEvent('Network.loadingFinished', onRequestEnd);
223
+ this.cdp.offEvent('Network.loadingFailed', onRequestEnd);
224
+ };
225
+ // 超时处理
226
+ timeoutTimer = setTimeout(() => {
227
+ cleanup();
228
+ reject(new NavigationTimeoutError('networkidle', timeout));
229
+ }, timeout);
230
+ // 监听网络事件
231
+ this.cdp.onEvent('Network.requestWillBeSent', onRequestStart);
232
+ this.cdp.onEvent('Network.loadingFinished', onRequestEnd);
233
+ this.cdp.onEvent('Network.loadingFailed', onRequestEnd);
234
+ // 初始检查
235
+ checkIdle();
236
+ });
237
+ }
238
+ /**
239
+ * 等待导航完成(Page.loadEventFired 事件)
240
+ */
241
+ async waitForNavigation(timeout = 30000) {
242
+ this.ensureSession();
243
+ await this.cdp.waitForEvent('Page.loadEventFired', undefined, timeout);
244
+ }
245
+ /**
246
+ * 后退
247
+ */
248
+ async goBack() {
249
+ this.ensureSession();
250
+ await this.send('Page.goBack');
251
+ await this.updateState();
252
+ }
253
+ /**
254
+ * 前进
255
+ */
256
+ async goForward() {
257
+ this.ensureSession();
258
+ await this.send('Page.goForward');
259
+ await this.updateState();
260
+ }
261
+ /**
262
+ * 刷新
263
+ */
264
+ async reload(options = {}) {
265
+ this.ensureSession();
266
+ const { ignoreCache = false, timeout = 30000 } = options;
267
+ const waitPromise = this.cdp.waitForEvent('Page.loadEventFired', undefined, timeout);
268
+ await this.send('Page.reload', { ignoreCache });
269
+ await waitPromise;
270
+ await this.updateState();
271
+ }
272
+ /**
273
+ * 创建定位器
274
+ */
275
+ createLocator(target, options) {
276
+ this.ensureSession();
277
+ return new Locator(this.cdp, target, this.sessionId, {
278
+ ...options,
279
+ getUrl: () => this.state?.url,
280
+ });
281
+ }
282
+ /**
283
+ * 创建自动等待器
284
+ */
285
+ createAutoWait(options) {
286
+ this.ensureSession();
287
+ return new AutoWait(this.cdp, this.sessionId, options);
288
+ }
289
+ /**
290
+ * 获取行为模拟器
291
+ */
292
+ getBehaviorSimulator() {
293
+ return this.behaviorSimulator;
294
+ }
295
+ /**
296
+ * 鼠标移动
297
+ */
298
+ async mouseMove(x, y) {
299
+ this.ensureSession();
300
+ await this.send('Input.dispatchMouseEvent', {
301
+ type: 'mouseMoved',
302
+ x,
303
+ y,
304
+ });
305
+ this.behaviorSimulator.setCurrentPosition({ x, y });
306
+ }
307
+ /**
308
+ * 鼠标按下
309
+ */
310
+ async mouseDown(button = 'left') {
311
+ this.ensureSession();
312
+ await this.send('Input.dispatchMouseEvent', {
313
+ type: 'mousePressed',
314
+ button,
315
+ clickCount: 1,
316
+ x: this.behaviorSimulator.getCurrentPosition().x,
317
+ y: this.behaviorSimulator.getCurrentPosition().y,
318
+ });
319
+ }
320
+ /**
321
+ * 鼠标释放
322
+ */
323
+ async mouseUp(button = 'left') {
324
+ this.ensureSession();
325
+ await this.send('Input.dispatchMouseEvent', {
326
+ type: 'mouseReleased',
327
+ button,
328
+ clickCount: 1,
329
+ x: this.behaviorSimulator.getCurrentPosition().x,
330
+ y: this.behaviorSimulator.getCurrentPosition().y,
331
+ });
332
+ }
333
+ /**
334
+ * 滚轮
335
+ */
336
+ async mouseWheel(deltaX, deltaY) {
337
+ this.ensureSession();
338
+ const pos = this.behaviorSimulator.getCurrentPosition();
339
+ await this.send('Input.dispatchMouseEvent', {
340
+ type: 'mouseWheel',
341
+ x: pos.x,
342
+ y: pos.y,
343
+ deltaX,
344
+ deltaY,
345
+ });
346
+ }
347
+ /**
348
+ * 键盘按下
349
+ */
350
+ async keyDown(key) {
351
+ this.ensureSession();
352
+ const keyDefinition = getKeyDefinition(key);
353
+ await this.send('Input.dispatchKeyEvent', {
354
+ type: 'keyDown',
355
+ ...keyDefinition,
356
+ });
357
+ }
358
+ /**
359
+ * 键盘释放
360
+ */
361
+ async keyUp(key) {
362
+ this.ensureSession();
363
+ const keyDefinition = getKeyDefinition(key);
364
+ await this.send('Input.dispatchKeyEvent', {
365
+ type: 'keyUp',
366
+ ...keyDefinition,
367
+ });
368
+ }
369
+ /**
370
+ * 输入文本
371
+ */
372
+ async type(text, delay = 0) {
373
+ this.ensureSession();
374
+ for (const char of text) {
375
+ await this.send('Input.dispatchKeyEvent', {
376
+ type: 'keyDown',
377
+ text: char,
378
+ });
379
+ await this.send('Input.dispatchKeyEvent', {
380
+ type: 'keyUp',
381
+ text: char,
382
+ });
383
+ if (delay > 0) {
384
+ await new Promise((r) => setTimeout(r, delay));
385
+ }
386
+ }
387
+ }
388
+ /**
389
+ * 触屏开始
390
+ */
391
+ async touchStart(x, y) {
392
+ this.ensureSession();
393
+ await this.send('Input.dispatchTouchEvent', {
394
+ type: 'touchStart',
395
+ touchPoints: [{ x, y }],
396
+ });
397
+ }
398
+ /**
399
+ * 触屏移动
400
+ */
401
+ async touchMove(x, y) {
402
+ this.ensureSession();
403
+ await this.send('Input.dispatchTouchEvent', {
404
+ type: 'touchMove',
405
+ touchPoints: [{ x, y }],
406
+ });
407
+ }
408
+ /**
409
+ * 触屏结束
410
+ */
411
+ async touchEnd() {
412
+ this.ensureSession();
413
+ await this.send('Input.dispatchTouchEvent', {
414
+ type: 'touchEnd',
415
+ touchPoints: [],
416
+ });
417
+ }
418
+ /**
419
+ * 截图
420
+ */
421
+ async screenshot(fullPage = false) {
422
+ this.ensureSession();
423
+ if (fullPage) {
424
+ // 获取页面完整高度
425
+ const { result } = (await this.send('Runtime.evaluate', {
426
+ expression: 'JSON.stringify({ width: document.documentElement.scrollWidth, height: document.documentElement.scrollHeight })',
427
+ returnByValue: true,
428
+ }));
429
+ const { width, height } = JSON.parse(result.value);
430
+ // 设置视口
431
+ await this.send('Emulation.setDeviceMetricsOverride', {
432
+ width,
433
+ height,
434
+ deviceScaleFactor: 1,
435
+ mobile: false,
436
+ });
437
+ }
438
+ const { data } = (await this.send('Page.captureScreenshot', {
439
+ format: 'png',
440
+ }));
441
+ if (fullPage) {
442
+ // 恢复视口
443
+ await this.send('Emulation.clearDeviceMetricsOverride');
444
+ }
445
+ return data;
446
+ }
447
+ /**
448
+ * 获取页面状态
449
+ */
450
+ async getPageState() {
451
+ this.ensureSession();
452
+ // 获取基本信息
453
+ const { result } = (await this.send('Runtime.evaluate', {
454
+ expression: `JSON.stringify({
455
+ url: location.href,
456
+ title: document.title,
457
+ viewport: {
458
+ width: window.innerWidth,
459
+ height: window.innerHeight
460
+ }
461
+ })`,
462
+ returnByValue: true,
463
+ }));
464
+ const state = JSON.parse(result.value);
465
+ // 获取可交互元素
466
+ await this.send('Accessibility.enable');
467
+ const { nodes } = (await this.send('Accessibility.getFullAXTree'));
468
+ const interactiveRoles = [
469
+ 'button',
470
+ 'link',
471
+ 'textbox',
472
+ 'checkbox',
473
+ 'radio',
474
+ 'combobox',
475
+ 'listbox',
476
+ 'menuitem',
477
+ 'tab',
478
+ 'slider',
479
+ 'spinbutton',
480
+ 'switch',
481
+ ];
482
+ state.elements = nodes
483
+ .filter((n) => interactiveRoles.includes(n.role?.value?.toLowerCase() ?? ''))
484
+ .map((n) => {
485
+ const props = n.properties ?? [];
486
+ const getProp = (name) => props.find((p) => p.name === name)?.value?.value;
487
+ return {
488
+ role: n.role?.value ?? '',
489
+ name: n.name?.value ?? '',
490
+ description: n.description?.value,
491
+ disabled: getProp('disabled'),
492
+ checked: getProp('checked'),
493
+ value: getProp('value'),
494
+ };
495
+ });
496
+ return state;
497
+ }
498
+ /**
499
+ * 获取 Cookies
500
+ */
501
+ async getCookies() {
502
+ this.ensureSession();
503
+ const { cookies } = (await this.send('Network.getCookies'));
504
+ return cookies;
505
+ }
506
+ /**
507
+ * 设置 Cookie
508
+ */
509
+ async setCookie(name, value, options = {}) {
510
+ this.ensureSession();
511
+ // 获取当前 URL
512
+ const url = this.state?.url ?? 'http://localhost';
513
+ await this.send('Network.setCookie', {
514
+ name,
515
+ value,
516
+ url,
517
+ ...options,
518
+ });
519
+ }
520
+ /**
521
+ * 删除 Cookie
522
+ */
523
+ async deleteCookie(name) {
524
+ this.ensureSession();
525
+ const url = this.state?.url ?? 'http://localhost';
526
+ await this.send('Network.deleteCookies', { name, url });
527
+ }
528
+ /**
529
+ * 清除所有 Cookies
530
+ */
531
+ async clearCookies() {
532
+ this.ensureSession();
533
+ await this.send('Network.clearBrowserCookies');
534
+ }
535
+ /**
536
+ * 获取控制台日志
537
+ */
538
+ getConsoleLogs(level, limit = 100) {
539
+ let logs = this.consoleLogs;
540
+ if (level && level !== 'all') {
541
+ logs = logs.filter((l) => l.level === level);
542
+ }
543
+ return logs.slice(-limit);
544
+ }
545
+ /**
546
+ * 获取网络请求日志
547
+ */
548
+ getNetworkRequests(urlPattern, limit = 100) {
549
+ let requests = this.networkRequests;
550
+ if (urlPattern) {
551
+ const regex = new RegExp(urlPattern.replace(/\*/g, '.*'));
552
+ requests = requests.filter((r) => regex.test(r.url));
553
+ }
554
+ return requests.slice(-limit);
555
+ }
556
+ /**
557
+ * 清除日志
558
+ */
559
+ clearLogs() {
560
+ this.consoleLogs = [];
561
+ this.networkRequests = [];
562
+ this.requestMap.clear();
563
+ }
564
+ /**
565
+ * 执行 JavaScript
566
+ */
567
+ async evaluate(script, args, timeout) {
568
+ this.ensureSession();
569
+ let expression = script;
570
+ if (args && args.length > 0) {
571
+ // 将参数序列化并注入
572
+ const argsStr = args.map((a) => JSON.stringify(a)).join(', ');
573
+ expression = `(${script})(${argsStr})`;
574
+ }
575
+ const { result, exceptionDetails } = (await this.send('Runtime.evaluate', {
576
+ expression,
577
+ returnByValue: true,
578
+ awaitPromise: true,
579
+ }, timeout));
580
+ if (exceptionDetails) {
581
+ throw new Error(exceptionDetails.exception.description);
582
+ }
583
+ return result.value;
584
+ }
585
+ /**
586
+ * 设置视口
587
+ */
588
+ async setViewport(width, height) {
589
+ this.ensureSession();
590
+ await this.send('Emulation.setDeviceMetricsOverride', {
591
+ width,
592
+ height,
593
+ deviceScaleFactor: 1,
594
+ mobile: false,
595
+ });
596
+ }
597
+ /**
598
+ * 设置 User-Agent
599
+ */
600
+ async setUserAgent(userAgent) {
601
+ this.ensureSession();
602
+ await this.send('Emulation.setUserAgentOverride', { userAgent });
603
+ }
604
+ /**
605
+ * 清除缓存
606
+ */
607
+ async clearCache(type = 'all') {
608
+ this.ensureSession();
609
+ if (type === 'all' || type === 'cookies') {
610
+ await this.send('Network.clearBrowserCookies');
611
+ }
612
+ if (type === 'all' || type === 'cache') {
613
+ await this.send('Network.clearBrowserCache');
614
+ }
615
+ if (type === 'all' || type === 'storage') {
616
+ await this.send('Runtime.evaluate', {
617
+ expression: `
618
+ localStorage.clear();
619
+ sessionStorage.clear();
620
+ `,
621
+ });
622
+ }
623
+ }
624
+ /**
625
+ * 新建页面
626
+ */
627
+ async newPage() {
628
+ this.ensureConnected();
629
+ const { targetId } = (await this.cdp.send('Target.createTarget', {
630
+ url: 'about:blank',
631
+ }));
632
+ await this.attachToTarget(targetId);
633
+ return {
634
+ targetId,
635
+ type: 'page',
636
+ url: 'about:blank',
637
+ title: '',
638
+ };
639
+ }
640
+ /**
641
+ * 关闭页面
642
+ */
643
+ async closePage(targetId) {
644
+ this.ensureConnected();
645
+ const id = targetId ?? this.currentTargetId;
646
+ if (!id) {
647
+ throw new TargetNotFoundError('unknown');
648
+ }
649
+ await this.cdp.send('Target.closeTarget', { targetId: id });
650
+ // 如果关闭的是当前页面,清除会话状态
651
+ if (id === this.currentTargetId) {
652
+ this.sessionId = null;
653
+ this.currentTargetId = null;
654
+ this.state = null;
655
+ }
656
+ }
657
+ /**
658
+ * 关闭浏览器
659
+ */
660
+ async close() {
661
+ // 清除日志
662
+ this.clearLogs();
663
+ // 关闭 CDP 连接
664
+ if (this.cdp) {
665
+ this.cdp.close();
666
+ this.cdp = null;
667
+ }
668
+ // 关闭浏览器进程
669
+ if (this.launcher) {
670
+ this.launcher.close();
671
+ this.launcher = null;
672
+ }
673
+ this.sessionId = null;
674
+ this.currentTargetId = null;
675
+ this.state = null;
676
+ this.listenersInstalled = false;
677
+ }
678
+ /**
679
+ * 获取当前状态
680
+ */
681
+ getState() {
682
+ return this.state;
683
+ }
684
+ /**
685
+ * 是否已连接
686
+ */
687
+ isConnected() {
688
+ return this.cdp !== null && this.cdp.isConnected;
689
+ }
690
+ /**
691
+ * 是否有活跃会话
692
+ */
693
+ hasSession() {
694
+ return this.sessionId !== null;
695
+ }
696
+ /**
697
+ * 串行执行操作(防止并发竞态)
698
+ */
699
+ async withLock(fn) {
700
+ const previousLock = this.operationLock;
701
+ let releaseLock;
702
+ this.operationLock = new Promise((resolve) => {
703
+ releaseLock = resolve;
704
+ });
705
+ try {
706
+ await previousLock;
707
+ return await fn();
708
+ }
709
+ finally {
710
+ releaseLock();
711
+ }
712
+ }
713
+ /**
714
+ * 初始化会话
715
+ */
716
+ async initSession() {
717
+ // 启用必要的域
718
+ await Promise.all([
719
+ this.send('Page.enable'),
720
+ this.send('DOM.enable'),
721
+ this.send('Runtime.enable'),
722
+ this.send('Network.enable'),
723
+ this.send('Log.enable'),
724
+ ]);
725
+ // 根据 stealth 模式注入反检测脚本
726
+ if (this.stealthMode !== 'off') {
727
+ const script = getAntiDetectionScript(this.stealthMode);
728
+ await this.send('Page.addScriptToEvaluateOnNewDocument', {
729
+ source: script,
730
+ });
731
+ // 对当前页面立即执行反检测脚本
732
+ await this.send('Runtime.evaluate', {
733
+ expression: script,
734
+ });
735
+ }
736
+ // 监听事件
737
+ this.setupEventListeners();
738
+ // 更新状态
739
+ await this.updateState();
740
+ }
741
+ /**
742
+ * 设置事件监听(幂等,只安装一次)
743
+ */
744
+ setupEventListeners() {
745
+ if (this.listenersInstalled) {
746
+ return;
747
+ }
748
+ this.listenersInstalled = true;
749
+ // 控制台日志
750
+ this.cdp.onEvent('Runtime.consoleAPICalled', (params) => {
751
+ const p = params;
752
+ this.consoleLogs.push({
753
+ level: p.type,
754
+ text: p.args.map((a) => a.value ?? a.description ?? '').join(' '),
755
+ timestamp: p.timestamp,
756
+ url: p.stackTrace?.callFrames[0]?.url,
757
+ lineNumber: p.stackTrace?.callFrames[0]?.lineNumber,
758
+ });
759
+ // 环形缓冲区:超出上限时移除最旧的条目
760
+ if (this.consoleLogs.length > SessionManager.MAX_LOG_ENTRIES) {
761
+ this.consoleLogs.shift();
762
+ }
763
+ });
764
+ // 网络请求
765
+ this.cdp.onEvent('Network.requestWillBeSent', (params) => {
766
+ const p = params;
767
+ this.requestMap.set(p.requestId, {
768
+ url: p.request.url,
769
+ method: p.request.method,
770
+ type: p.type,
771
+ timestamp: p.timestamp,
772
+ });
773
+ });
774
+ this.cdp.onEvent('Network.responseReceived', (params) => {
775
+ const p = params;
776
+ const request = this.requestMap.get(p.requestId);
777
+ if (request) {
778
+ this.networkRequests.push({
779
+ ...request,
780
+ status: p.response.status,
781
+ duration: (p.timestamp - request.timestamp) * 1000,
782
+ });
783
+ // 环形缓冲区:超出上限时移除最旧的条目
784
+ if (this.networkRequests.length > SessionManager.MAX_LOG_ENTRIES) {
785
+ this.networkRequests.shift();
786
+ }
787
+ this.requestMap.delete(p.requestId);
788
+ }
789
+ });
790
+ // 网络请求失败时清理 requestMap,防止泄漏
791
+ this.cdp.onEvent('Network.loadingFailed', (params) => {
792
+ const p = params;
793
+ this.requestMap.delete(p.requestId);
794
+ });
795
+ }
796
+ /**
797
+ * 更新页面状态
798
+ */
799
+ async updateState() {
800
+ const result = (await this.send('Runtime.evaluate', {
801
+ expression: 'JSON.stringify({ url: location.href, title: document.title })',
802
+ returnByValue: true,
803
+ }));
804
+ const { url, title } = JSON.parse(result.result.value);
805
+ this.state = {
806
+ url,
807
+ title,
808
+ targetId: this.currentTargetId,
809
+ };
810
+ }
811
+ /**
812
+ * 发送 CDP 命令
813
+ */
814
+ send(method, params, timeout) {
815
+ return this.cdp.send(method, params, this.sessionId ?? undefined, timeout);
816
+ }
817
+ /**
818
+ * 确保已连接
819
+ */
820
+ ensureConnected() {
821
+ if (!this.cdp || !this.cdp.isConnected) {
822
+ throw new SessionNotFoundError();
823
+ }
824
+ }
825
+ /**
826
+ * 确保有活跃会话
827
+ */
828
+ ensureSession() {
829
+ this.ensureConnected();
830
+ if (!this.sessionId) {
831
+ throw new SessionNotFoundError();
832
+ }
833
+ }
834
+ }
835
+ /**
836
+ * 获取按键定义
837
+ */
838
+ function getKeyDefinition(key) {
839
+ const definitions = {
840
+ // 修饰键
841
+ Control: { key: 'Control', code: 'ControlLeft', keyCode: 17 },
842
+ Shift: { key: 'Shift', code: 'ShiftLeft', keyCode: 16 },
843
+ Alt: { key: 'Alt', code: 'AltLeft', keyCode: 18 },
844
+ Meta: { key: 'Meta', code: 'MetaLeft', keyCode: 91 },
845
+ // 功能键
846
+ Enter: { key: 'Enter', code: 'Enter', keyCode: 13, text: '\r' },
847
+ Tab: { key: 'Tab', code: 'Tab', keyCode: 9 },
848
+ Backspace: { key: 'Backspace', code: 'Backspace', keyCode: 8 },
849
+ Delete: { key: 'Delete', code: 'Delete', keyCode: 46 },
850
+ Escape: { key: 'Escape', code: 'Escape', keyCode: 27 },
851
+ Space: { key: ' ', code: 'Space', keyCode: 32, text: ' ' },
852
+ // 方向键
853
+ ArrowUp: { key: 'ArrowUp', code: 'ArrowUp', keyCode: 38 },
854
+ ArrowDown: { key: 'ArrowDown', code: 'ArrowDown', keyCode: 40 },
855
+ ArrowLeft: { key: 'ArrowLeft', code: 'ArrowLeft', keyCode: 37 },
856
+ ArrowRight: { key: 'ArrowRight', code: 'ArrowRight', keyCode: 39 },
857
+ // 其他常用键
858
+ Home: { key: 'Home', code: 'Home', keyCode: 36 },
859
+ End: { key: 'End', code: 'End', keyCode: 35 },
860
+ PageUp: { key: 'PageUp', code: 'PageUp', keyCode: 33 },
861
+ PageDown: { key: 'PageDown', code: 'PageDown', keyCode: 34 },
862
+ };
863
+ // 如果是已知按键,返回定义
864
+ if (definitions[key]) {
865
+ return definitions[key];
866
+ }
867
+ // 如果是单个字符,生成定义
868
+ if (key.length === 1) {
869
+ const charCode = key.charCodeAt(0);
870
+ const code = key >= 'a' && key <= 'z'
871
+ ? `Key${key.toUpperCase()}`
872
+ : key >= 'A' && key <= 'Z'
873
+ ? `Key${key}`
874
+ : key >= '0' && key <= '9'
875
+ ? `Digit${key}`
876
+ : `Key${key}`;
877
+ return {
878
+ key,
879
+ code,
880
+ keyCode: charCode,
881
+ text: key,
882
+ };
883
+ }
884
+ // 未知按键
885
+ return { key, code: key, keyCode: 0 };
886
+ }
887
+ /**
888
+ * 获取会话管理器实例
889
+ */
890
+ export function getSession() {
891
+ return SessionManager.getInstance();
892
+ }
893
+ //# sourceMappingURL=session.js.map