@lmcc-dev/mult-fetch-mcp-server 1.3.0 → 1.3.2

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 (91) hide show
  1. package/README.md +68 -15
  2. package/README.zh.md +31 -0
  3. package/dist/src/client.js +118 -91
  4. package/dist/src/lib/fetchers/browser/BrowserFetcher.js +25 -10
  5. package/dist/src/lib/fetchers/browser/BrowserInstance.js +67 -64
  6. package/dist/src/lib/fetchers/common/BaseFetcher.js +106 -10
  7. package/dist/src/lib/fetchers/common/types.js +10 -1
  8. package/dist/src/lib/fetchers/common/utils.js +1 -1
  9. package/dist/src/lib/fetchers/node/HttpClient.js +41 -21
  10. package/dist/src/lib/fetchers/node/NodeFetcher.js +24 -8
  11. package/dist/src/lib/i18n/index.js +2 -2
  12. package/dist/src/lib/i18n/keys/client.js +1 -0
  13. package/dist/src/lib/i18n/keys/extractor.js +26 -0
  14. package/dist/src/lib/i18n/keys/fetcher.js +12 -1
  15. package/dist/src/lib/i18n/keys/index.js +1 -0
  16. package/dist/src/lib/i18n/keys/node.js +2 -0
  17. package/dist/src/lib/i18n/locales/en/client.js +1 -0
  18. package/dist/src/lib/i18n/locales/en/extractor.js +24 -0
  19. package/dist/src/lib/i18n/locales/en/fetcher.js +3 -1
  20. package/dist/src/lib/i18n/locales/en/index.js +2 -0
  21. package/dist/src/lib/i18n/locales/en/node.js +2 -0
  22. package/dist/src/lib/i18n/locales/zh/client.js +1 -0
  23. package/dist/src/lib/i18n/locales/zh/extractor.js +22 -0
  24. package/dist/src/lib/i18n/locales/zh/fetcher.js +13 -3
  25. package/dist/src/lib/i18n/locales/zh/index.js +2 -0
  26. package/dist/src/lib/i18n/locales/zh/node.js +2 -0
  27. package/dist/src/lib/i18n/logger.js +2 -2
  28. package/dist/src/lib/logger.js +38 -17
  29. package/dist/src/lib/server/browser.js +2 -2
  30. package/dist/src/lib/server/fetcher.js +0 -3
  31. package/dist/src/lib/server/index.js +2 -2
  32. package/dist/src/lib/server/prompts.js +4 -4
  33. package/dist/src/lib/server/tools.js +127 -354
  34. package/dist/src/lib/utils/ChunkManager.js +2 -2
  35. package/dist/src/lib/utils/ContentExtractor.js +141 -0
  36. package/dist/src/lib/utils/ContentProcessor.js +5 -11
  37. package/dist/src/lib/utils/ContentSizeManager.js +2 -2
  38. package/dist/src/lib/utils/ErrorHandler.js +1 -0
  39. package/dist/src/lib/utils/TemplateUtils.js +6 -2
  40. package/dist/tests/client.test.js +1 -1
  41. package/dist/tests/test-extract-single.js +389 -0
  42. package/dist/tests/utils/ContentExtractor.test.js +173 -0
  43. package/dist/tests/utils/ContentProcessor.test.js +136 -0
  44. package/dist/tests/utils/TemplateUtils.test.js +118 -0
  45. package/package.json +14 -3
  46. package/dist/i18n-test-report.json +0 -8
  47. package/dist/i18n-unused-keys-report.json +0 -8
  48. package/dist/src/lib/BrowserFetcher.js +0 -787
  49. package/dist/src/lib/NodeFetcher.js +0 -492
  50. package/dist/src/lib/i18n/keys.js +0 -529
  51. package/dist/src/test-i18n.js +0 -139
  52. package/dist/tests/BrowserFetcher.test.js +0 -951
  53. package/dist/tests/NodeFetcher.test.js +0 -263
  54. package/dist/tests/i18n-remove-unused-keys.js +0 -236
  55. package/dist/tests/i18n-test-report.json +0 -2004
  56. package/dist/tests/src/lib/i18n/index.js +0 -108
  57. package/dist/tests/src/lib/i18n/keys/base.js +0 -47
  58. package/dist/tests/src/lib/i18n/keys/browser.js +0 -93
  59. package/dist/tests/src/lib/i18n/keys/client.js +0 -70
  60. package/dist/tests/src/lib/i18n/keys/errors.js +0 -34
  61. package/dist/tests/src/lib/i18n/keys/fetcher.js +0 -84
  62. package/dist/tests/src/lib/i18n/keys/index.js +0 -31
  63. package/dist/tests/src/lib/i18n/keys/node.js +0 -56
  64. package/dist/tests/src/lib/i18n/keys/prompts.js +0 -82
  65. package/dist/tests/src/lib/i18n/keys/resources.js +0 -50
  66. package/dist/tests/src/lib/i18n/keys/server.js +0 -64
  67. package/dist/tests/src/lib/i18n/keys/tools.js +0 -34
  68. package/dist/tests/src/lib/i18n/locales/en/browser.js +0 -88
  69. package/dist/tests/src/lib/i18n/locales/en/client.js +0 -66
  70. package/dist/tests/src/lib/i18n/locales/en/errors.js +0 -28
  71. package/dist/tests/src/lib/i18n/locales/en/fetcher.js +0 -71
  72. package/dist/tests/src/lib/i18n/locales/en/index.js +0 -29
  73. package/dist/tests/src/lib/i18n/locales/en/node.js +0 -51
  74. package/dist/tests/src/lib/i18n/locales/en/prompts.js +0 -52
  75. package/dist/tests/src/lib/i18n/locales/en/resources.js +0 -50
  76. package/dist/tests/src/lib/i18n/locales/en/server.js +0 -56
  77. package/dist/tests/src/lib/i18n/locales/en/tools.js +0 -28
  78. package/dist/tests/src/lib/i18n/locales/zh/browser.js +0 -87
  79. package/dist/tests/src/lib/i18n/locales/zh/client.js +0 -66
  80. package/dist/tests/src/lib/i18n/locales/zh/errors.js +0 -28
  81. package/dist/tests/src/lib/i18n/locales/zh/fetcher.js +0 -71
  82. package/dist/tests/src/lib/i18n/locales/zh/index.js +0 -29
  83. package/dist/tests/src/lib/i18n/locales/zh/node.js +0 -51
  84. package/dist/tests/src/lib/i18n/locales/zh/prompts.js +0 -53
  85. package/dist/tests/src/lib/i18n/locales/zh/resources.js +0 -50
  86. package/dist/tests/src/lib/i18n/locales/zh/server.js +0 -57
  87. package/dist/tests/src/lib/i18n/locales/zh/tools.js +0 -28
  88. package/dist/tests/src/lib/i18n/logger.js +0 -114
  89. package/dist/tests/src/lib/logger.js +0 -181
  90. package/dist/tests/tests/test-i18n.js +0 -588
  91. package/dist/vitest.config.js +0 -29
@@ -1,951 +0,0 @@
1
- /**
2
- * Author: Martin <lmccc.dev@gmail.com>
3
- * Co-Author: AI Assistant (Claude)
4
- * Description: This code was collaboratively developed by Martin and AI Assistant.
5
- */
6
- import { BrowserFetcher } from '../src/lib/BrowserFetcher.js';
7
- import { log, COMPONENTS } from '../src/lib/logger.js';
8
- // Mock Puppeteer
9
- jest.mock('puppeteer', () => ({
10
- __esModule: true,
11
- default: {
12
- launch: jest.fn()
13
- }
14
- }));
15
- // Mock puppeteer-extra
16
- jest.mock('puppeteer-extra', () => {
17
- const mockPuppeteerExtra = {
18
- use: jest.fn(),
19
- launch: jest.fn()
20
- };
21
- return {
22
- __esModule: true,
23
- default: mockPuppeteerExtra
24
- };
25
- });
26
- // Mock puppeteer-extra-plugin-stealth
27
- jest.mock('puppeteer-extra-plugin-stealth', () => {
28
- const mockStealthPlugin = jest.fn();
29
- return {
30
- __esModule: true,
31
- default: mockStealthPlugin
32
- };
33
- });
34
- // Mock child_process
35
- jest.mock('child_process', () => ({
36
- execSync: jest.fn()
37
- }));
38
- // Mock JSDOM
39
- jest.mock('jsdom', () => ({
40
- JSDOM: jest.fn().mockImplementation(() => ({
41
- window: {
42
- document: {
43
- body: {
44
- textContent: '测试文本内容'
45
- }
46
- }
47
- }
48
- }))
49
- }));
50
- // Mock TurndownService
51
- jest.mock('turndown', () => jest.fn().mockImplementation(() => ({
52
- addRule: jest.fn(),
53
- turndown: jest.fn().mockReturnValue('# 转换后的Markdown内容')
54
- })));
55
- // Mock i18n
56
- jest.mock('../src/lib/i18n/index.js', () => ({
57
- t: (key, options) => key
58
- }));
59
- // Mock logger
60
- jest.mock('../src/lib/logger.js', () => ({
61
- log: jest.fn(),
62
- COMPONENTS: {
63
- BROWSER_FETCH: 'BROWSER_FETCH'
64
- }
65
- }));
66
- describe('BrowserFetcher 类测试 (BrowserFetcher Class Tests)', () => {
67
- let mockPuppeteerExtra;
68
- let mockBrowser;
69
- let mockPage;
70
- // 模拟 process.memoryUsage 返回值
71
- const mockMemoryUsage = {
72
- heapUsed: 100 * 1024 * 1024, // 100MB
73
- heapTotal: 200 * 1024 * 1024, // 200MB
74
- rss: 300 * 1024 * 1024, // 300MB
75
- external: 50 * 1024 * 1024, // 50MB
76
- arrayBuffers: 10 * 1024 * 1024 // 10MB
77
- };
78
- beforeAll(() => {
79
- // 启用 Jest 的假计时器
80
- jest.useFakeTimers();
81
- });
82
- afterAll(() => {
83
- // 恢复真实计时器
84
- jest.useRealTimers();
85
- });
86
- beforeEach(() => {
87
- // 重置所有 mock
88
- jest.clearAllMocks();
89
- jest.clearAllTimers();
90
- // 模拟 process.memoryUsage
91
- jest.spyOn(process, 'memoryUsage').mockReturnValue(mockMemoryUsage);
92
- // 创建 mock 页面
93
- mockPage = {
94
- setDefaultTimeout: jest.fn(),
95
- setUserAgent: jest.fn(),
96
- setExtraHTTPHeaders: jest.fn(),
97
- goto: jest.fn(),
98
- waitForSelector: jest.fn(),
99
- content: jest.fn(),
100
- evaluate: jest.fn(),
101
- close: jest.fn(),
102
- cookies: jest.fn(),
103
- setCookie: jest.fn(),
104
- url: jest.fn()
105
- };
106
- // 创建 mock 浏览器
107
- mockBrowser = {
108
- newPage: jest.fn().mockResolvedValue(mockPage),
109
- close: jest.fn(),
110
- on: jest.fn()
111
- };
112
- // 重置 BrowserFetcher 的静态属性
113
- // @ts-ignore - 访问私有属性用于测试
114
- BrowserFetcher.browser = null;
115
- // @ts-ignore
116
- BrowserFetcher.browserStarting = false;
117
- // @ts-ignore
118
- BrowserFetcher.browserStartPromise = null;
119
- // @ts-ignore
120
- BrowserFetcher.lastMemoryCheck = 0;
121
- // @ts-ignore
122
- BrowserFetcher.cookieStore = {};
123
- // 获取 puppeteer-extra 的模拟实例
124
- mockPuppeteerExtra = require('puppeteer-extra').default;
125
- mockPuppeteerExtra.launch.mockReset();
126
- });
127
- describe('内存管理测试 (Memory Management Tests)', () => {
128
- test('应该正确检查内存使用情况 (Should check memory usage correctly)', async () => {
129
- // 准备测试数据
130
- const testUrl = 'https://example.com';
131
- const testPayload = {
132
- url: testUrl,
133
- debug: true
134
- };
135
- // 调用方法
136
- await BrowserFetcher.html(testPayload);
137
- // 验证 process.memoryUsage 被调用
138
- expect(process.memoryUsage).toHaveBeenCalled();
139
- });
140
- test('当内存使用过高时应该记录警告 (Should log warning when memory usage is high)', async () => {
141
- // 模拟高内存使用情况
142
- const highMemoryUsage = {
143
- heapUsed: 900 * 1024 * 1024, // 900MB
144
- heapTotal: 1024 * 1024 * 1024, // 1GB
145
- rss: 1.5 * 1024 * 1024 * 1024, // 1.5GB
146
- external: 200 * 1024 * 1024, // 200MB
147
- arrayBuffers: 100 * 1024 * 1024 // 100MB
148
- };
149
- jest.spyOn(process, 'memoryUsage').mockReturnValue(highMemoryUsage);
150
- // 设置浏览器实例
151
- // @ts-ignore - 访问私有属性用于测试
152
- BrowserFetcher.browser = mockBrowser;
153
- // 准备测试数据
154
- const testUrl = 'https://example.com';
155
- const testPayload = {
156
- url: testUrl,
157
- debug: true
158
- };
159
- // 调用方法
160
- await BrowserFetcher.html(testPayload);
161
- // 验证警告日志被记录
162
- expect(log).toHaveBeenCalledWith('browser.memoryTooHigh', true, expect.objectContaining({
163
- heapUsed: expect.any(Number),
164
- heapTotal: expect.any(Number),
165
- rss: expect.any(Number)
166
- }), COMPONENTS.BROWSER_FETCH);
167
- });
168
- test('应该在检查间隔内只检查一次内存 (Should check memory only once within interval)', async () => {
169
- // 准备测试数据
170
- const testUrl = 'https://example.com';
171
- const testPayload = {
172
- url: testUrl,
173
- debug: true
174
- };
175
- // 设置浏览器实例
176
- // @ts-ignore - 访问私有属性用于测试
177
- BrowserFetcher.browser = mockBrowser;
178
- // 连续调用两次
179
- await BrowserFetcher.html(testPayload);
180
- await BrowserFetcher.html(testPayload);
181
- // 验证 process.memoryUsage 只被调用一次
182
- expect(process.memoryUsage).toHaveBeenCalledTimes(1);
183
- });
184
- test('应该在超过检查间隔后重新检查内存 (Should check memory again after interval)', async () => {
185
- // 准备测试数据
186
- const testUrl = 'https://example.com';
187
- const testPayload = {
188
- url: testUrl,
189
- debug: true
190
- };
191
- // 设置浏览器实例
192
- // @ts-ignore - 访问私有属性用于测试
193
- BrowserFetcher.browser = mockBrowser;
194
- // 第一次调用
195
- await BrowserFetcher.html(testPayload);
196
- // 模拟时间经过
197
- jest.advanceTimersByTime(60000); // 60秒后
198
- // 第二次调用
199
- await BrowserFetcher.html(testPayload);
200
- // 验证 process.memoryUsage 被调用两次
201
- expect(process.memoryUsage).toHaveBeenCalledTimes(2);
202
- });
203
- });
204
- describe('浏览器实例管理测试 (Browser Instance Management Tests)', () => {
205
- test('应该正确创建浏览器实例 (Should create browser instance correctly)', async () => {
206
- // 准备测试数据
207
- const testUrl = 'https://example.com';
208
- const testPayload = {
209
- url: testUrl,
210
- debug: true
211
- };
212
- // 设置 puppeteer-extra 的 launch 方法返回值
213
- const mockLaunch = require('puppeteer-extra').default.launch;
214
- mockLaunch.mockResolvedValue(mockBrowser);
215
- // 调用方法
216
- await BrowserFetcher.html(testPayload);
217
- // 验证浏览器实例被创建
218
- expect(mockLaunch).toHaveBeenCalled();
219
- // 验证事件监听器被设置
220
- expect(mockBrowser.on).toHaveBeenCalledWith('disconnected', expect.any(Function));
221
- });
222
- test('应该复用已存在的浏览器实例 (Should reuse existing browser instance)', async () => {
223
- // 设置已存在的浏览器实例
224
- // @ts-ignore - 访问私有属性用于测试
225
- BrowserFetcher.browser = mockBrowser;
226
- // 准备测试数据
227
- const testUrl = 'https://example.com';
228
- const testPayload = {
229
- url: testUrl,
230
- debug: true
231
- };
232
- // 调用方法
233
- await BrowserFetcher.html(testPayload);
234
- // 验证没有创建新的浏览器实例
235
- expect(require('puppeteer-extra').default.launch).not.toHaveBeenCalled();
236
- });
237
- test('应该正确处理浏览器启动中的状态 (Should handle browser starting state correctly)', async () => {
238
- // 模拟浏览器正在启动
239
- // @ts-ignore - 访问私有属性用于测试
240
- BrowserFetcher.browserStarting = true;
241
- // @ts-ignore
242
- BrowserFetcher.browserStartPromise = Promise.resolve(mockBrowser);
243
- // 准备测试数据
244
- const testUrl = 'https://example.com';
245
- const testPayload = {
246
- url: testUrl,
247
- debug: true
248
- };
249
- // 调用方法
250
- await BrowserFetcher.html(testPayload);
251
- // 验证使用了现有的启动 Promise
252
- expect(require('puppeteer-extra').default.launch).not.toHaveBeenCalled();
253
- });
254
- test('应该在浏览器断开连接时清理实例 (Should clean up instance on browser disconnect)', async () => {
255
- // 设置 puppeteer-extra 的 launch 方法返回值
256
- const mockLaunch = require('puppeteer-extra').default.launch;
257
- mockLaunch.mockResolvedValue(mockBrowser);
258
- // 准备测试数据
259
- const testUrl = 'https://example.com';
260
- const testPayload = {
261
- url: testUrl,
262
- debug: true
263
- };
264
- // 调用方法
265
- await BrowserFetcher.html(testPayload);
266
- // 获取断开连接的处理函数
267
- const disconnectHandler = mockBrowser.on.mock.calls.find(call => call[0] === 'disconnected')?.[1];
268
- // 确保找到了处理函数
269
- expect(disconnectHandler).toBeDefined();
270
- // 调用断开连接的处理函数
271
- if (disconnectHandler) {
272
- disconnectHandler({ targetId: 'test-browser' });
273
- }
274
- // 验证浏览器实例被清理
275
- // @ts-ignore - 访问私有属性用于测试
276
- expect(BrowserFetcher.browser).toBeNull();
277
- // @ts-ignore
278
- expect(BrowserFetcher.browserStarting).toBeFalsy();
279
- // @ts-ignore
280
- expect(BrowserFetcher.browserStartPromise).toBeNull();
281
- });
282
- });
283
- describe('Cookie 管理测试 (Cookie Management Tests)', () => {
284
- beforeEach(() => {
285
- // 重置 BrowserFetcher 的静态属性
286
- BrowserFetcher.cookieStore = {};
287
- BrowserFetcher.browser = null;
288
- BrowserFetcher.browserStarting = false;
289
- BrowserFetcher.browserStartPromise = null;
290
- BrowserFetcher.lastMemoryCheck = 0;
291
- // 重置 mock
292
- mockPage.evaluate.mockClear();
293
- mockPage.setExtraHTTPHeaders.mockClear();
294
- mockPage.goto.mockClear();
295
- mockPage.waitForSelector.mockClear();
296
- mockPage.content.mockClear();
297
- mockPage.close.mockClear();
298
- mockPage.url.mockClear();
299
- // 设置基本的 mock 返回值
300
- mockPage.content.mockResolvedValue('<html><body>Test content</body></html>');
301
- mockPage.waitForSelector.mockResolvedValue(null);
302
- // 模拟 puppeteerExtra.launch 返回浏览器实例
303
- mockPuppeteerExtra.launch.mockResolvedValue(mockBrowser);
304
- });
305
- it('应该正确保存和获取 Cookie (Should save and retrieve cookies correctly)', async () => {
306
- // 设置测试数据
307
- const testUrl = 'https://example.com';
308
- const testDomain = 'example.com';
309
- const testCookie = 'test=value; path=/';
310
- // 模拟 getDomain 方法
311
- jest.spyOn(BrowserFetcher, 'getDomain')
312
- .mockReturnValue(testDomain);
313
- // 直接模拟 saveCookies 方法
314
- const originalSaveCookies = BrowserFetcher['saveCookies'];
315
- BrowserFetcher['saveCookies'] = jest.fn().mockImplementation((url, cookies) => {
316
- const domain = BrowserFetcher['getDomain'](url);
317
- if (domain) {
318
- BrowserFetcher['cookieStore'][domain] = cookies;
319
- }
320
- });
321
- // 模拟 evaluate 方法返回 cookie
322
- mockPage.evaluate.mockImplementation((fn) => {
323
- // 检查是否是获取 document.cookie 的调用
324
- if (fn && fn.toString && fn.toString().includes('document.cookie')) {
325
- return Promise.resolve(testCookie);
326
- }
327
- return Promise.resolve('');
328
- });
329
- // 模拟 fetch 方法
330
- const originalFetch = BrowserFetcher.fetch;
331
- BrowserFetcher.fetch = jest.fn().mockImplementation(async (params) => {
332
- // 模拟 fetch 方法的行为
333
- const { url, saveCookies } = params;
334
- // 如果有存储的 cookies,使用它们
335
- const storedCookies = BrowserFetcher['getCookies'](url);
336
- if (storedCookies && saveCookies) {
337
- await mockPage.setExtraHTTPHeaders({
338
- 'Cookie': storedCookies
339
- });
340
- }
341
- // 模拟获取页面内容
342
- await mockPage.goto(url, { waitUntil: 'networkidle2' });
343
- // 如果需要保存 cookies
344
- if (saveCookies) {
345
- const cookies = await mockPage.evaluate(() => document.cookie);
346
- if (cookies) {
347
- BrowserFetcher['saveCookies'](url, cookies);
348
- }
349
- }
350
- return {
351
- content: [{ type: 'text', text: '<html><body>Test content</body></html>' }],
352
- isError: false
353
- };
354
- });
355
- // 第一次请求
356
- await BrowserFetcher.fetch({
357
- url: testUrl,
358
- useBrowser: true,
359
- saveCookies: true
360
- });
361
- // 验证 saveCookies 是否被调用
362
- expect(BrowserFetcher['saveCookies']).toHaveBeenCalledWith(testUrl, testCookie);
363
- // 验证 Cookie 是否被保存
364
- expect(BrowserFetcher['cookieStore'][testDomain]).toBe(testCookie);
365
- // 重置 mock 以便于验证
366
- mockPage.setExtraHTTPHeaders.mockClear();
367
- // 第二次请求
368
- await BrowserFetcher.fetch({
369
- url: testUrl,
370
- useBrowser: true,
371
- saveCookies: true
372
- });
373
- // 验证是否使用了保存的 Cookie
374
- expect(mockPage.setExtraHTTPHeaders).toHaveBeenCalledWith(expect.objectContaining({
375
- 'Cookie': testCookie
376
- }));
377
- // 恢复原始方法
378
- BrowserFetcher['saveCookies'] = originalSaveCookies;
379
- BrowserFetcher.fetch = originalFetch;
380
- });
381
- it('当 saveCookies 为 false 时不应保存 Cookie (Should not save cookies when saveCookies is false)', async () => {
382
- // 设置测试数据
383
- const testUrl = 'https://example.com';
384
- const testDomain = 'example.com';
385
- const testCookie = 'test=value; path=/';
386
- // 模拟 getDomain 方法
387
- jest.spyOn(BrowserFetcher, 'getDomain')
388
- .mockReturnValue(testDomain);
389
- // 直接模拟 saveCookies 方法
390
- const originalSaveCookies = BrowserFetcher['saveCookies'];
391
- BrowserFetcher['saveCookies'] = jest.fn();
392
- // 模拟 evaluate 方法返回 cookie
393
- mockPage.evaluate.mockImplementation((fn) => {
394
- // 检查是否是获取 document.cookie 的调用
395
- if (fn && fn.toString && fn.toString().includes('document.cookie')) {
396
- return Promise.resolve(testCookie);
397
- }
398
- return Promise.resolve('');
399
- });
400
- // 模拟 fetch 方法
401
- const originalFetch = BrowserFetcher.fetch;
402
- BrowserFetcher.fetch = jest.fn().mockImplementation(async (params) => {
403
- // 模拟 fetch 方法的行为
404
- const { url, saveCookies } = params;
405
- // 模拟获取页面内容
406
- await mockPage.goto(url, { waitUntil: 'networkidle2' });
407
- // 如果需要保存 cookies
408
- if (saveCookies) {
409
- const cookies = await mockPage.evaluate(() => document.cookie);
410
- if (cookies) {
411
- BrowserFetcher['saveCookies'](url, cookies);
412
- }
413
- }
414
- return {
415
- content: [{ type: 'text', text: '<html><body>Test content</body></html>' }],
416
- isError: false
417
- };
418
- });
419
- // 调用 fetch,设置 saveCookies 为 false
420
- await BrowserFetcher.fetch({
421
- url: testUrl,
422
- useBrowser: true,
423
- saveCookies: false
424
- });
425
- // 验证 saveCookies 没有被调用
426
- expect(BrowserFetcher['saveCookies']).not.toHaveBeenCalled();
427
- // 恢复原始方法
428
- BrowserFetcher['saveCookies'] = originalSaveCookies;
429
- BrowserFetcher.fetch = originalFetch;
430
- });
431
- it('应该为不同域名分别保存 Cookie (Should save cookies separately for different domains)', async () => {
432
- // 设置测试数据
433
- const testUrl1 = 'https://example.com/page1';
434
- const testUrl2 = 'https://another-site.com/page2';
435
- const testCookie1 = 'cookie1=value1';
436
- const testCookie2 = 'cookie2=value2';
437
- // 直接设置 cookieStore
438
- BrowserFetcher['cookieStore'] = {
439
- 'example.com': testCookie1,
440
- 'another-site.com': testCookie2
441
- };
442
- // 模拟 getDomain 方法
443
- const originalGetDomain = BrowserFetcher['getDomain'];
444
- BrowserFetcher['getDomain'] = jest.fn().mockImplementation((url) => {
445
- if (url.includes('example.com'))
446
- return 'example.com';
447
- if (url.includes('another-site.com'))
448
- return 'another-site.com';
449
- return '';
450
- });
451
- // 模拟 getCookies 方法
452
- const originalGetCookies = BrowserFetcher['getCookies'];
453
- BrowserFetcher['getCookies'] = jest.fn().mockImplementation((url) => {
454
- const domain = BrowserFetcher['getDomain'](url);
455
- return BrowserFetcher['cookieStore'][domain];
456
- });
457
- // 验证第一个域名的 Cookie
458
- const cookie1 = BrowserFetcher['getCookies'](testUrl1);
459
- expect(cookie1).toBe(testCookie1);
460
- // 验证第二个域名的 Cookie
461
- const cookie2 = BrowserFetcher['getCookies'](testUrl2);
462
- expect(cookie2).toBe(testCookie2);
463
- // 恢复原始方法
464
- BrowserFetcher['getDomain'] = originalGetDomain;
465
- BrowserFetcher['getCookies'] = originalGetCookies;
466
- }, 10000); // 增加超时时间
467
- });
468
- describe('代理设置测试 (Proxy Settings Tests)', () => {
469
- test('应该正确处理用户指定的代理 (Should handle user-specified proxy correctly)', async () => {
470
- // 设置测试数据
471
- const testUrl = 'https://example.com';
472
- const testProxy = 'http://proxy.example.com:8080';
473
- const testPayload = {
474
- url: testUrl,
475
- proxy: testProxy,
476
- debug: true
477
- };
478
- // 设置 puppeteer-extra 的 launch 方法返回值
479
- mockPuppeteerExtra.launch.mockResolvedValue(mockBrowser);
480
- // 模拟 fetch 方法
481
- const originalFetch = BrowserFetcher.fetch;
482
- BrowserFetcher.fetch = jest.fn().mockResolvedValue({
483
- content: [{ type: 'text', text: '<html><body>Test content</body></html>' }],
484
- isError: false
485
- });
486
- // 调用方法
487
- await BrowserFetcher.html(testPayload);
488
- // 验证 fetch 方法被调用,并且传递了正确的代理参数
489
- expect(BrowserFetcher.fetch).toHaveBeenCalledWith(expect.objectContaining({
490
- url: testUrl,
491
- proxy: testProxy
492
- }));
493
- // 恢复原始方法
494
- BrowserFetcher.fetch = originalFetch;
495
- });
496
- test('应该正确处理系统代理设置 (Should handle system proxy settings correctly)', async () => {
497
- // 模拟环境变量
498
- const originalEnv = process.env;
499
- process.env = {
500
- ...originalEnv,
501
- HTTP_PROXY: 'http://system-proxy.example.com:8080'
502
- };
503
- // 模拟 execSync
504
- const { execSync } = require('child_process');
505
- execSync.mockReturnValue('HTTP_PROXY=http://system-proxy.example.com:8080');
506
- // 设置测试数据
507
- const testUrl = 'https://example.com';
508
- const testPayload = {
509
- url: testUrl,
510
- useSystemProxy: true,
511
- debug: true
512
- };
513
- // 设置 puppeteer-extra 的 launch 方法返回值
514
- mockPuppeteerExtra.launch.mockResolvedValue(mockBrowser);
515
- // 模拟 fetch 方法
516
- const originalFetch = BrowserFetcher.fetch;
517
- BrowserFetcher.fetch = jest.fn().mockResolvedValue({
518
- content: [{ type: 'text', text: '<html><body>Test content</body></html>' }],
519
- isError: false
520
- });
521
- // 调用方法
522
- await BrowserFetcher.html(testPayload);
523
- // 验证 fetch 方法被调用,并且传递了正确的系统代理参数
524
- expect(BrowserFetcher.fetch).toHaveBeenCalledWith(expect.objectContaining({
525
- url: testUrl,
526
- useSystemProxy: true
527
- }));
528
- // 恢复原始方法和环境变量
529
- BrowserFetcher.fetch = originalFetch;
530
- process.env = originalEnv;
531
- });
532
- test('当 useSystemProxy 为 false 时不应使用系统代理 (Should not use system proxy when useSystemProxy is false)', async () => {
533
- // 模拟环境变量
534
- const originalEnv = process.env;
535
- process.env = {
536
- ...originalEnv,
537
- HTTP_PROXY: 'http://system-proxy.example.com:8080'
538
- };
539
- // 设置测试数据
540
- const testUrl = 'https://example.com';
541
- const testPayload = {
542
- url: testUrl,
543
- useSystemProxy: false,
544
- debug: true
545
- };
546
- // 设置 puppeteer-extra 的 launch 方法返回值
547
- mockPuppeteerExtra.launch.mockResolvedValue(mockBrowser);
548
- // 模拟 fetch 方法
549
- const originalFetch = BrowserFetcher.fetch;
550
- BrowserFetcher.fetch = jest.fn().mockResolvedValue({
551
- content: [{ type: 'text', text: '<html><body>Test content</body></html>' }],
552
- isError: false
553
- });
554
- // 调用方法
555
- await BrowserFetcher.html(testPayload);
556
- // 验证 fetch 方法被调用,并且传递了正确的 useSystemProxy 参数
557
- expect(BrowserFetcher.fetch).toHaveBeenCalledWith(expect.objectContaining({
558
- url: testUrl,
559
- useSystemProxy: false
560
- }));
561
- // 恢复原始方法和环境变量
562
- BrowserFetcher.fetch = originalFetch;
563
- process.env = originalEnv;
564
- });
565
- test('用户指定的代理应优先于系统代理 (User-specified proxy should take precedence over system proxy)', async () => {
566
- // 模拟环境变量
567
- const originalEnv = process.env;
568
- process.env = {
569
- ...originalEnv,
570
- HTTP_PROXY: 'http://system-proxy.example.com:8080'
571
- };
572
- // 设置测试数据
573
- const testUrl = 'https://example.com';
574
- const testProxy = 'http://user-proxy.example.com:8080';
575
- const testPayload = {
576
- url: testUrl,
577
- proxy: testProxy,
578
- useSystemProxy: true,
579
- debug: true
580
- };
581
- // 设置 puppeteer-extra 的 launch 方法返回值
582
- mockPuppeteerExtra.launch.mockResolvedValue(mockBrowser);
583
- // 模拟 fetch 方法
584
- const originalFetch = BrowserFetcher.fetch;
585
- BrowserFetcher.fetch = jest.fn().mockResolvedValue({
586
- content: [{ type: 'text', text: '<html><body>Test content</body></html>' }],
587
- isError: false
588
- });
589
- // 调用方法
590
- await BrowserFetcher.html(testPayload);
591
- // 验证 fetch 方法被调用,并且传递了正确的代理参数(用户指定的代理)
592
- expect(BrowserFetcher.fetch).toHaveBeenCalledWith(expect.objectContaining({
593
- url: testUrl,
594
- proxy: testProxy,
595
- useSystemProxy: true
596
- }));
597
- // 恢复原始方法和环境变量
598
- BrowserFetcher.fetch = originalFetch;
599
- process.env = originalEnv;
600
- });
601
- });
602
- describe('内容获取测试 (Content Fetching Tests)', () => {
603
- test('应该正确获取 HTML 内容 (Should fetch HTML content correctly)', async () => {
604
- // 设置测试数据
605
- const testUrl = 'https://example.com';
606
- const testHtml = '<html><body><h1>Test Content</h1></body></html>';
607
- const testPayload = {
608
- url: testUrl,
609
- debug: true
610
- };
611
- // 设置 puppeteer-extra 的 launch 方法返回值
612
- mockPuppeteerExtra.launch.mockResolvedValue(mockBrowser);
613
- // 模拟页面内容
614
- mockPage.content.mockResolvedValue(testHtml);
615
- // 模拟 fetch 方法
616
- const originalFetch = BrowserFetcher.fetch;
617
- BrowserFetcher.fetch = jest.fn().mockImplementation(async () => {
618
- return {
619
- content: [{ type: 'text', text: testHtml }],
620
- isError: false
621
- };
622
- });
623
- // 调用方法
624
- const result = await BrowserFetcher.html(testPayload);
625
- // 验证结果
626
- expect(result).toEqual({
627
- content: [{ type: 'text', text: testHtml }],
628
- isError: false
629
- });
630
- // 恢复原始方法
631
- BrowserFetcher.fetch = originalFetch;
632
- });
633
- test('应该正确获取 JSON 内容 (Should fetch JSON content correctly)', async () => {
634
- // 设置测试数据
635
- const testUrl = 'https://api.example.com/data.json';
636
- const testJson = { name: 'Test', value: 123 };
637
- const testPayload = {
638
- url: testUrl,
639
- debug: true
640
- };
641
- // 设置 puppeteer-extra 的 launch 方法返回值
642
- mockPuppeteerExtra.launch.mockResolvedValue(mockBrowser);
643
- // 模拟页面内容
644
- mockPage.content.mockResolvedValue(JSON.stringify(testJson));
645
- // 模拟 fetch 方法
646
- const originalFetch = BrowserFetcher.fetch;
647
- BrowserFetcher.fetch = jest.fn().mockImplementation(async () => {
648
- return {
649
- content: [{ type: 'text', text: JSON.stringify(testJson) }],
650
- isError: false
651
- };
652
- });
653
- // 调用方法
654
- const result = await BrowserFetcher.json(testPayload);
655
- // 验证结果
656
- expect(result).toEqual({
657
- content: [{ type: 'text', text: JSON.stringify(testJson) }],
658
- isError: false
659
- });
660
- // 恢复原始方法
661
- BrowserFetcher.fetch = originalFetch;
662
- });
663
- test('应该正确获取纯文本内容 (Should fetch text content correctly)', async () => {
664
- // 设置测试数据
665
- const testUrl = 'https://example.com/text';
666
- const testHtml = '<html><body><h1>Test Content</h1><p>This is a paragraph.</p></body></html>';
667
- const expectedText = 'Test Content This is a paragraph.';
668
- const testPayload = {
669
- url: testUrl,
670
- debug: true
671
- };
672
- // 设置 puppeteer-extra 的 launch 方法返回值
673
- mockPuppeteerExtra.launch.mockResolvedValue(mockBrowser);
674
- // 模拟页面内容
675
- mockPage.content.mockResolvedValue(testHtml);
676
- // 模拟 JSDOM
677
- const mockJsdom = require('jsdom').JSDOM;
678
- mockJsdom.mockImplementation(() => ({
679
- window: {
680
- document: {
681
- body: {
682
- textContent: expectedText
683
- }
684
- }
685
- }
686
- }));
687
- // 模拟 fetch 方法
688
- const originalFetch = BrowserFetcher.fetch;
689
- BrowserFetcher.fetch = jest.fn().mockImplementation(async () => {
690
- return {
691
- content: [{ type: 'text', text: testHtml }],
692
- isError: false
693
- };
694
- });
695
- // 调用方法
696
- const result = await BrowserFetcher.txt(testPayload);
697
- // 验证结果
698
- expect(result).toEqual({
699
- content: [{ type: 'text', text: expectedText }],
700
- isError: false
701
- });
702
- // 恢复原始方法
703
- BrowserFetcher.fetch = originalFetch;
704
- });
705
- test('应该正确获取 Markdown 内容 (Should fetch Markdown content correctly)', async () => {
706
- // 设置测试数据
707
- const testUrl = 'https://example.com/markdown';
708
- const testHtml = '<html><body><h1>Test Content</h1><p>This is a paragraph.</p></body></html>';
709
- const expectedMarkdown = '# 转换后的Markdown内容';
710
- const testPayload = {
711
- url: testUrl,
712
- debug: true
713
- };
714
- // 设置 puppeteer-extra 的 launch 方法返回值
715
- mockPuppeteerExtra.launch.mockResolvedValue(mockBrowser);
716
- // 模拟页面内容
717
- mockPage.content.mockResolvedValue(testHtml);
718
- // 模拟 TurndownService
719
- const mockTurndownService = require('turndown');
720
- mockTurndownService.mockImplementation(() => ({
721
- addRule: jest.fn(),
722
- turndown: jest.fn().mockReturnValue(expectedMarkdown)
723
- }));
724
- // 模拟 fetch 方法
725
- const originalFetch = BrowserFetcher.fetch;
726
- BrowserFetcher.fetch = jest.fn().mockImplementation(async () => {
727
- return {
728
- content: [{ type: 'text', text: testHtml }],
729
- isError: false
730
- };
731
- });
732
- // 调用方法
733
- const result = await BrowserFetcher.markdown(testPayload);
734
- // 验证结果
735
- expect(result).toEqual({
736
- content: [{ type: 'text', text: expectedMarkdown }],
737
- isError: false
738
- });
739
- // 恢复原始方法
740
- BrowserFetcher.fetch = originalFetch;
741
- });
742
- test('应该正确处理滚动到底部的选项 (Should handle scrollToBottom option correctly)', async () => {
743
- // 设置测试数据
744
- const testUrl = 'https://example.com';
745
- const testHtml = '<html><body><h1>Test Content</h1></body></html>';
746
- const testPayload = {
747
- url: testUrl,
748
- scrollToBottom: true,
749
- debug: true
750
- };
751
- // 设置 puppeteer-extra 的 launch 方法返回值
752
- mockPuppeteerExtra.launch.mockResolvedValue(mockBrowser);
753
- // 模拟页面内容和评估方法
754
- mockPage.content.mockResolvedValue(testHtml);
755
- mockPage.evaluate.mockImplementation((fn) => {
756
- // 检查是否是自动滚动的调用
757
- if (fn && fn.toString && fn.toString().includes('scrollBy')) {
758
- return Promise.resolve();
759
- }
760
- return Promise.resolve('');
761
- });
762
- // 模拟 fetch 方法
763
- const originalFetch = BrowserFetcher.fetch;
764
- BrowserFetcher.fetch = jest.fn().mockImplementation(async (params) => {
765
- // 如果需要滚动到底部
766
- if (params.scrollToBottom) {
767
- // 模拟自动滚动
768
- await mockPage.evaluate(() => { });
769
- }
770
- return {
771
- content: [{ type: 'text', text: testHtml }],
772
- isError: false
773
- };
774
- });
775
- // 调用方法
776
- const result = await BrowserFetcher.html(testPayload);
777
- // 验证结果
778
- expect(result).toEqual({
779
- content: [{ type: 'text', text: testHtml }],
780
- isError: false
781
- });
782
- // 验证 fetch 方法被调用,并且传递了正确的 scrollToBottom 参数
783
- expect(BrowserFetcher.fetch).toHaveBeenCalledWith(expect.objectContaining({
784
- url: testUrl,
785
- scrollToBottom: true
786
- }));
787
- // 恢复原始方法
788
- BrowserFetcher.fetch = originalFetch;
789
- });
790
- });
791
- describe('错误处理和重试测试 (Error Handling and Retry Tests)', () => {
792
- test('应该正确处理网络错误并重试 (Should handle network errors and retry correctly)', async () => {
793
- // 设置测试数据
794
- const testUrl = 'https://example.com';
795
- const testPayload = {
796
- url: testUrl,
797
- debug: true
798
- };
799
- // 设置 puppeteer-extra 的 launch 方法返回值
800
- mockPuppeteerExtra.launch.mockResolvedValue(mockBrowser);
801
- // 模拟 fetch 方法抛出错误,然后成功
802
- const originalFetch = BrowserFetcher.fetch;
803
- let fetchCallCount = 0;
804
- BrowserFetcher.fetch = jest.fn().mockImplementation(async () => {
805
- fetchCallCount++;
806
- if (fetchCallCount === 1) {
807
- throw new Error('Network error');
808
- }
809
- return {
810
- content: [{ type: 'text', text: '<html><body>Test content</body></html>' }],
811
- isError: false
812
- };
813
- });
814
- // 模拟 setTimeout
815
- jest.useFakeTimers();
816
- const originalSetTimeout = global.setTimeout;
817
- // @ts-ignore - 忽略 setTimeout 类型错误
818
- global.setTimeout = jest.fn().mockImplementation((callback, ms) => {
819
- callback();
820
- return null;
821
- });
822
- // 调用方法
823
- const result = await BrowserFetcher.html(testPayload);
824
- // 验证 fetch 被调用了两次(一次失败,一次成功)
825
- expect(fetchCallCount).toBe(2);
826
- // 验证结果
827
- expect(result).toEqual({
828
- content: [{ type: 'text', text: '<html><body>Test content</body></html>' }],
829
- isError: false
830
- });
831
- // 恢复原始方法
832
- BrowserFetcher.fetch = originalFetch;
833
- global.setTimeout = originalSetTimeout;
834
- jest.useRealTimers();
835
- });
836
- test('应该在超过最大重试次数后返回错误 (Should return error after exceeding maximum retry count)', async () => {
837
- // 设置测试数据
838
- const testUrl = 'https://example.com';
839
- const testPayload = {
840
- url: testUrl,
841
- debug: true
842
- };
843
- // 设置 puppeteer-extra 的 launch 方法返回值
844
- mockPuppeteerExtra.launch.mockResolvedValue(mockBrowser);
845
- // 模拟 fetch 方法始终抛出错误
846
- const originalFetch = BrowserFetcher.fetch;
847
- BrowserFetcher.fetch = jest.fn().mockImplementation(async () => {
848
- throw new Error('Network error');
849
- });
850
- // 模拟 setTimeout
851
- jest.useFakeTimers();
852
- const originalSetTimeout = global.setTimeout;
853
- // @ts-ignore - 忽略 setTimeout 类型错误
854
- global.setTimeout = jest.fn().mockImplementation((callback, ms) => {
855
- callback();
856
- return null;
857
- });
858
- // 调用方法
859
- const result = await BrowserFetcher.html(testPayload);
860
- // 验证 fetch 被调用了 3 次(初始 + 2 次重试)
861
- expect(BrowserFetcher.fetch).toHaveBeenCalledTimes(3);
862
- // 验证结果是错误
863
- expect(result.isError).toBe(true);
864
- // 恢复原始方法
865
- BrowserFetcher.fetch = originalFetch;
866
- global.setTimeout = originalSetTimeout;
867
- jest.useRealTimers();
868
- });
869
- test('应该正确处理 Cloudflare 保护 (Should handle Cloudflare protection correctly)', async () => {
870
- // 设置测试数据
871
- const testUrl = 'https://example.com';
872
- const testPayload = {
873
- url: testUrl,
874
- debug: true
875
- };
876
- // 设置 puppeteer-extra 的 launch 方法返回值
877
- mockPuppeteerExtra.launch.mockResolvedValue(mockBrowser);
878
- // 模拟页面内容和评估方法
879
- mockPage.content.mockResolvedValue('<html><body><h1>Test Content</h1></body></html>');
880
- // 模拟 fetch 方法
881
- const originalFetch = BrowserFetcher.fetch;
882
- BrowserFetcher.fetch = jest.fn().mockResolvedValue({
883
- content: [{ type: 'text', text: '<html><body>Test content</body></html>' }],
884
- isError: false
885
- });
886
- // 调用方法
887
- const result = await BrowserFetcher.html(testPayload);
888
- // 验证结果
889
- expect(result.isError).toBe(false);
890
- // 恢复原始方法
891
- BrowserFetcher.fetch = originalFetch;
892
- }, 10000); // 增加超时时间
893
- test('应该正确处理 about:blank 和 closeBrowser 选项 (Should handle about:blank and closeBrowser option correctly)', async () => {
894
- // 设置测试数据
895
- const testPayload = {
896
- url: 'about:blank',
897
- closeBrowser: true,
898
- debug: true
899
- };
900
- // 设置 puppeteer-extra 的 launch 方法返回值
901
- mockPuppeteerExtra.launch.mockResolvedValue(mockBrowser);
902
- // 设置浏览器实例
903
- // @ts-ignore - 访问私有属性用于测试
904
- BrowserFetcher.browser = mockBrowser;
905
- // 调用方法
906
- const result = await BrowserFetcher.html(testPayload);
907
- // 验证浏览器关闭方法被调用
908
- expect(mockBrowser.close).toHaveBeenCalled();
909
- // 验证结果
910
- expect(result).toEqual({
911
- content: [{ type: 'text', text: 'Browser closed successfully' }],
912
- isError: false
913
- });
914
- });
915
- test('应该在内容过长时截断内容 (Should truncate content when it is too long)', async () => {
916
- // 设置测试数据
917
- const testUrl = 'https://example.com';
918
- const testPayload = {
919
- url: testUrl,
920
- debug: true
921
- };
922
- // 生成一个超过 10MB 的内容
923
- const longContent = 'a'.repeat(11 * 1024 * 1024); // 11MB
924
- // 设置 puppeteer-extra 的 launch 方法返回值
925
- mockPuppeteerExtra.launch.mockResolvedValue(mockBrowser);
926
- // 模拟页面内容
927
- mockPage.content.mockResolvedValue(longContent);
928
- // 模拟 fetch 方法
929
- const originalFetch = BrowserFetcher.fetch;
930
- BrowserFetcher.fetch = jest.fn().mockImplementation(async () => {
931
- // 获取页面内容
932
- const content = await mockPage.content();
933
- // 如果内容太长,截断它
934
- let finalContent = content;
935
- if (content.length > 10 * 1024 * 1024) { // 10MB
936
- finalContent = content.substring(0, 10 * 1024 * 1024);
937
- }
938
- return {
939
- content: [{ type: 'text', text: finalContent }],
940
- isError: false
941
- };
942
- });
943
- // 调用方法
944
- const result = await BrowserFetcher.html(testPayload);
945
- // 验证结果内容长度不超过 10MB
946
- expect(result.content[0].text.length).toBe(10 * 1024 * 1024);
947
- // 恢复原始方法
948
- BrowserFetcher.fetch = originalFetch;
949
- });
950
- });
951
- });