@lmcc-dev/mult-fetch-mcp-server 1.0.3 → 1.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.
Files changed (136) hide show
  1. package/README.md +11 -9
  2. package/README.zh.md +19 -10
  3. package/dist/i18n-test-report.json +8 -0
  4. package/dist/i18n-unused-keys-report.json +8 -0
  5. package/dist/src/client.js +75 -62
  6. package/dist/src/index.js +0 -0
  7. package/dist/src/lib/BrowserFetcher.js +12 -12
  8. package/dist/src/lib/NodeFetcher.js +165 -106
  9. package/dist/src/lib/fetch.js +40 -0
  10. package/dist/src/lib/fetchers/FetcherFactory.js +43 -0
  11. package/dist/src/lib/fetchers/browser/BrowserFetcher.js +381 -0
  12. package/dist/src/lib/fetchers/browser/BrowserInstance.js +180 -0
  13. package/dist/src/lib/fetchers/browser/PageOperations.js +168 -0
  14. package/dist/src/lib/fetchers/common/types.js +41 -0
  15. package/dist/src/lib/fetchers/common/utils.js +141 -0
  16. package/dist/src/lib/fetchers/index.js +62 -0
  17. package/dist/src/lib/fetchers/node/HttpClient.js +239 -0
  18. package/dist/src/lib/fetchers/node/NodeFetcher.js +180 -0
  19. package/dist/src/lib/i18n/index.js +105 -17
  20. package/dist/src/lib/i18n/keys/base.js +41 -0
  21. package/dist/src/lib/i18n/keys/browser.js +91 -0
  22. package/dist/src/lib/i18n/keys/client.js +68 -0
  23. package/dist/src/lib/i18n/keys/errors.js +49 -0
  24. package/dist/src/lib/i18n/keys/factory.js +46 -0
  25. package/dist/src/lib/i18n/keys/fetcher.js +87 -0
  26. package/dist/src/lib/i18n/keys/index.js +16 -0
  27. package/dist/src/lib/i18n/keys/node.js +69 -0
  28. package/dist/src/lib/i18n/keys/prompts.js +79 -0
  29. package/dist/src/lib/i18n/keys/resources.js +53 -0
  30. package/dist/src/lib/i18n/keys/server.js +67 -0
  31. package/dist/src/lib/i18n/keys/tools.js +39 -0
  32. package/dist/src/lib/i18n/keys.js +504 -0
  33. package/dist/src/lib/i18n/locales/en/browser.js +79 -55
  34. package/dist/src/lib/i18n/locales/en/client.js +57 -33
  35. package/dist/src/lib/i18n/locales/en/errors.js +36 -12
  36. package/dist/src/lib/i18n/locales/en/factory.js +38 -0
  37. package/dist/src/lib/i18n/locales/en/fetcher.js +67 -60
  38. package/dist/src/lib/i18n/locales/en/index.js +9 -7
  39. package/dist/src/lib/i18n/locales/en/node.js +57 -35
  40. package/dist/src/lib/i18n/locales/en/prompts.js +42 -65
  41. package/dist/src/lib/i18n/locales/en/resources.js +52 -40
  42. package/dist/src/lib/i18n/locales/en/server.js +49 -24
  43. package/dist/src/lib/i18n/locales/en/tools.js +27 -19
  44. package/dist/src/lib/i18n/locales/zh/browser.js +79 -56
  45. package/dist/src/lib/i18n/locales/zh/client.js +57 -32
  46. package/dist/src/lib/i18n/locales/zh/errors.js +36 -12
  47. package/dist/src/lib/i18n/locales/zh/factory.js +38 -0
  48. package/dist/src/lib/i18n/locales/zh/fetcher.js +67 -60
  49. package/dist/src/lib/i18n/locales/zh/index.js +9 -7
  50. package/dist/src/lib/i18n/locales/zh/node.js +61 -36
  51. package/dist/src/lib/i18n/locales/zh/prompts.js +35 -57
  52. package/dist/src/lib/i18n/locales/zh/resources.js +51 -39
  53. package/dist/src/lib/i18n/locales/zh/server.js +51 -25
  54. package/dist/src/lib/i18n/locales/zh/tools.js +27 -19
  55. package/dist/src/lib/i18n/logger.js +90 -19
  56. package/dist/src/lib/logger.js +135 -14
  57. package/dist/src/lib/server/browser.js +12 -18
  58. package/dist/src/lib/server/fetcher.js +13 -34
  59. package/dist/src/lib/server/index.js +2 -2
  60. package/dist/src/lib/server/prompts.js +1 -1
  61. package/dist/src/lib/server/resources.js +1 -1
  62. package/dist/src/lib/server/tools.js +404 -308
  63. package/dist/src/lib/utils/ErrorHandler.js +125 -0
  64. package/dist/src/lib/utils/errorDetection.js +41 -0
  65. package/dist/src/lib/utils/errors.js +279 -0
  66. package/dist/src/mcp-server.js +0 -0
  67. package/dist/src/test-i18n.js +139 -0
  68. package/dist/tests/BrowserFetcher.test.js +951 -0
  69. package/dist/tests/NodeFetcher.test.js +263 -0
  70. package/dist/tests/client.test.js +211 -0
  71. package/dist/tests/fetch.test.js +70 -0
  72. package/dist/tests/fetchers/Fetcher.test.js +108 -0
  73. package/dist/tests/fetchers/browser/BrowserFetcher.test.js +195 -0
  74. package/dist/tests/fetchers/browser/BrowserInstance.test.js +163 -0
  75. package/dist/tests/fetchers/node/NodeFetcher.test.js +211 -0
  76. package/dist/tests/i18n-missing-keys.js +364 -0
  77. package/dist/tests/i18n-remove-unused-keys.js +236 -0
  78. package/dist/tests/i18n-test-report.json +2004 -0
  79. package/dist/tests/i18n-unused-keys.js +425 -0
  80. package/dist/tests/i18n.test.js +178 -0
  81. package/dist/tests/logger.test.js +100 -0
  82. package/dist/tests/mcp-server.test.js +113 -0
  83. package/dist/tests/server/browser.test.js +201 -0
  84. package/dist/tests/server/fetcher.test.js +173 -0
  85. package/dist/tests/server/index.test.js +122 -0
  86. package/dist/tests/server/prompts.test.js +246 -0
  87. package/dist/tests/server/resources.test.js +395 -0
  88. package/dist/tests/server/tools.test.js +223 -0
  89. package/dist/tests/setup.js +36 -0
  90. package/dist/tests/src/lib/i18n/index.js +108 -0
  91. package/dist/tests/src/lib/i18n/keys/base.js +47 -0
  92. package/dist/tests/src/lib/i18n/keys/browser.js +93 -0
  93. package/dist/tests/src/lib/i18n/keys/client.js +70 -0
  94. package/dist/tests/src/lib/i18n/keys/errors.js +34 -0
  95. package/dist/tests/src/lib/i18n/keys/fetcher.js +84 -0
  96. package/dist/tests/src/lib/i18n/keys/index.js +31 -0
  97. package/dist/tests/src/lib/i18n/keys/node.js +56 -0
  98. package/dist/tests/src/lib/i18n/keys/prompts.js +82 -0
  99. package/dist/tests/src/lib/i18n/keys/resources.js +50 -0
  100. package/dist/tests/src/lib/i18n/keys/server.js +64 -0
  101. package/dist/tests/src/lib/i18n/keys/tools.js +34 -0
  102. package/dist/tests/src/lib/i18n/locales/en/browser.js +88 -0
  103. package/dist/tests/src/lib/i18n/locales/en/client.js +66 -0
  104. package/dist/tests/src/lib/i18n/locales/en/errors.js +28 -0
  105. package/dist/tests/src/lib/i18n/locales/en/fetcher.js +71 -0
  106. package/dist/tests/src/lib/i18n/locales/en/index.js +29 -0
  107. package/dist/tests/src/lib/i18n/locales/en/node.js +51 -0
  108. package/dist/tests/src/lib/i18n/locales/en/prompts.js +52 -0
  109. package/dist/tests/src/lib/i18n/locales/en/resources.js +50 -0
  110. package/dist/tests/src/lib/i18n/locales/en/server.js +56 -0
  111. package/dist/tests/src/lib/i18n/locales/en/tools.js +28 -0
  112. package/dist/tests/src/lib/i18n/locales/zh/browser.js +87 -0
  113. package/dist/tests/src/lib/i18n/locales/zh/client.js +66 -0
  114. package/dist/tests/src/lib/i18n/locales/zh/errors.js +28 -0
  115. package/dist/tests/src/lib/i18n/locales/zh/fetcher.js +71 -0
  116. package/dist/tests/src/lib/i18n/locales/zh/index.js +29 -0
  117. package/dist/tests/src/lib/i18n/locales/zh/node.js +51 -0
  118. package/dist/tests/src/lib/i18n/locales/zh/prompts.js +53 -0
  119. package/dist/tests/src/lib/i18n/locales/zh/resources.js +50 -0
  120. package/dist/tests/src/lib/i18n/locales/zh/server.js +57 -0
  121. package/dist/tests/src/lib/i18n/locales/zh/tools.js +28 -0
  122. package/dist/tests/src/lib/i18n/logger.js +114 -0
  123. package/dist/tests/src/lib/logger.js +181 -0
  124. package/dist/tests/test-direct-client.js +0 -0
  125. package/dist/tests/test-i18n.js +530 -0
  126. package/dist/tests/test-mcp-methods.js +1 -1
  127. package/dist/tests/test-mcp.js +0 -0
  128. package/dist/tests/test-mini4k.js +0 -0
  129. package/dist/tests/tests/test-i18n.js +588 -0
  130. package/dist/tests/types.test.js +57 -0
  131. package/dist/tests/utils/ErrorHandler.test.js +144 -0
  132. package/dist/vitest.config.js +29 -0
  133. package/package.json +32 -19
  134. package/dist/src/cli-language.js +0 -78
  135. package/dist/src/lib/example.js +0 -93
  136. package/dist/src/lib/server/logger.js +0 -23
@@ -0,0 +1,263 @@
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 { NodeFetcher } from '../src/lib/NodeFetcher.js';
7
+ // 模拟node-fetch
8
+ jest.mock('node-fetch', () => {
9
+ const mockFetch = jest.fn();
10
+ // 创建模拟Response
11
+ const createMockResponse = (url) => {
12
+ // 根据URL返回不同的内容
13
+ let textContent = '<html><body><h1>测试页面</h1></body></html>';
14
+ let jsonContent = { test: 'data' };
15
+ let contentType = 'text/html; charset=utf-8';
16
+ if (url.includes('/api')) {
17
+ textContent = JSON.stringify(jsonContent);
18
+ contentType = 'application/json; charset=utf-8';
19
+ }
20
+ else if (url.includes('/text')) {
21
+ textContent = '这是纯文本内容';
22
+ contentType = 'text/plain; charset=utf-8';
23
+ }
24
+ else if (url.includes('/md')) {
25
+ textContent = '# 标题\n\n这是Markdown内容';
26
+ contentType = 'text/markdown; charset=utf-8';
27
+ }
28
+ return {
29
+ ok: true,
30
+ status: 200,
31
+ statusText: 'OK',
32
+ url: url,
33
+ headers: {
34
+ get: jest.fn((name) => {
35
+ if (name.toLowerCase() === 'content-type') {
36
+ return contentType;
37
+ }
38
+ return null;
39
+ }),
40
+ raw: () => ({}),
41
+ entries: () => [['content-type', contentType]]
42
+ },
43
+ text: jest.fn().mockResolvedValue(textContent),
44
+ json: jest.fn().mockResolvedValue(jsonContent),
45
+ buffer: jest.fn().mockResolvedValue(Buffer.from(textContent)),
46
+ clone: jest.fn(function () { return this; })
47
+ };
48
+ };
49
+ // 设置默认返回值
50
+ mockFetch.mockImplementation((url) => {
51
+ return Promise.resolve(createMockResponse(url));
52
+ });
53
+ return mockFetch;
54
+ });
55
+ // 模拟http-proxy-agent和https-proxy-agent
56
+ jest.mock('http-proxy-agent', () => ({
57
+ HttpProxyAgent: jest.fn().mockImplementation(() => ({}))
58
+ }));
59
+ jest.mock('https-proxy-agent', () => ({
60
+ HttpsProxyAgent: jest.fn().mockImplementation(() => ({}))
61
+ }));
62
+ // 模拟child_process
63
+ jest.mock('child_process', () => ({
64
+ execSync: jest.fn().mockReturnValue(Buffer.from(''))
65
+ }));
66
+ // 模拟i18n
67
+ jest.mock('../src/lib/i18n/index.js', () => ({
68
+ t: (key, options) => {
69
+ return options ? `${key} ${JSON.stringify(options)}` : key;
70
+ }
71
+ }));
72
+ // 模拟logger
73
+ jest.mock('../src/lib/logger.js', () => ({
74
+ log: jest.fn(),
75
+ COMPONENTS: {
76
+ NODE_FETCH: 'NODE-FETCH'
77
+ }
78
+ }));
79
+ describe('NodeFetcher类测试 (NodeFetcher Class Tests)', () => {
80
+ let fetch;
81
+ beforeEach(() => {
82
+ // 重置所有模拟
83
+ jest.clearAllMocks();
84
+ // 获取模拟的fetch函数
85
+ fetch = require('node-fetch');
86
+ });
87
+ describe('基本功能测试 (Basic Functionality Tests)', () => {
88
+ test('应该有html方法 (Should have html method)', () => {
89
+ expect(typeof NodeFetcher.html).toBe('function');
90
+ });
91
+ test('应该有json方法 (Should have json method)', () => {
92
+ expect(typeof NodeFetcher.json).toBe('function');
93
+ });
94
+ test('应该有txt方法 (Should have txt method)', () => {
95
+ expect(typeof NodeFetcher.txt).toBe('function');
96
+ });
97
+ test('应该有markdown方法 (Should have markdown method)', () => {
98
+ expect(typeof NodeFetcher.markdown).toBe('function');
99
+ });
100
+ });
101
+ describe('html方法测试 (html Method Tests)', () => {
102
+ test('应该能够获取HTML内容 (Should fetch HTML content)', async () => {
103
+ // 准备测试数据
104
+ const payload = {
105
+ url: 'https://example.com',
106
+ debug: true
107
+ };
108
+ // 调用方法
109
+ const result = await NodeFetcher.html(payload);
110
+ // 验证结果
111
+ expect(result).toBeDefined();
112
+ expect(result.html).toBeDefined();
113
+ expect(result.url).toBe('https://example.com');
114
+ expect(result.status).toBe(200);
115
+ expect(result.headers).toBeDefined();
116
+ expect(result.html).toContain('<html>');
117
+ // 验证fetch被调用
118
+ expect(fetch).toHaveBeenCalledWith(payload.url, expect.objectContaining({
119
+ headers: expect.any(Object),
120
+ timeout: expect.any(Number)
121
+ }));
122
+ });
123
+ test('应该处理获取HTML时的错误 (Should handle errors when fetching HTML)', async () => {
124
+ // 模拟fetch失败
125
+ fetch.mockRejectedValueOnce(new Error('Network error'));
126
+ // 准备测试数据
127
+ const payload = {
128
+ url: 'https://example.com',
129
+ debug: true
130
+ };
131
+ // 调用方法并验证它抛出错误
132
+ await expect(NodeFetcher.html(payload)).rejects.toThrow('Network error');
133
+ });
134
+ });
135
+ describe('json方法测试 (json Method Tests)', () => {
136
+ test('应该能够获取JSON内容 (Should fetch JSON content)', async () => {
137
+ // 准备测试数据
138
+ const payload = {
139
+ url: 'https://example.com/api',
140
+ debug: true
141
+ };
142
+ // 调用方法
143
+ const result = await NodeFetcher.json(payload);
144
+ // 验证结果
145
+ expect(result).toBeDefined();
146
+ expect(result.json).toBeDefined();
147
+ expect(result.json).toEqual({ test: 'data' });
148
+ expect(result.url).toBe('https://example.com/api');
149
+ expect(result.status).toBe(200);
150
+ expect(result.headers).toBeDefined();
151
+ // 验证fetch被调用
152
+ expect(fetch).toHaveBeenCalledWith(payload.url, expect.objectContaining({
153
+ headers: expect.any(Object),
154
+ timeout: expect.any(Number)
155
+ }));
156
+ });
157
+ test('应该处理获取JSON时的错误 (Should handle errors when fetching JSON)', async () => {
158
+ // 模拟fetch失败
159
+ fetch.mockRejectedValueOnce(new Error('Network error'));
160
+ // 准备测试数据
161
+ const payload = {
162
+ url: 'https://example.com/api',
163
+ debug: true
164
+ };
165
+ // 调用方法并验证它抛出错误
166
+ await expect(NodeFetcher.json(payload)).rejects.toThrow('Network error');
167
+ });
168
+ });
169
+ describe('txt方法测试 (txt Method Tests)', () => {
170
+ test('应该能够获取文本内容 (Should fetch text content)', async () => {
171
+ // 准备测试数据
172
+ const payload = {
173
+ url: 'https://example.com/text',
174
+ debug: true
175
+ };
176
+ // 调用方法
177
+ const result = await NodeFetcher.txt(payload);
178
+ // 验证结果
179
+ expect(result).toBeDefined();
180
+ expect(result.text).toBeDefined();
181
+ expect(result.url).toBe('https://example.com/text');
182
+ expect(result.status).toBe(200);
183
+ expect(result.headers).toBeDefined();
184
+ // 验证fetch被调用
185
+ expect(fetch).toHaveBeenCalledWith(payload.url, expect.objectContaining({
186
+ headers: expect.any(Object),
187
+ timeout: expect.any(Number)
188
+ }));
189
+ });
190
+ test('应该处理获取文本时的错误 (Should handle errors when fetching text)', async () => {
191
+ // 模拟fetch失败
192
+ fetch.mockRejectedValueOnce(new Error('Network error'));
193
+ // 准备测试数据
194
+ const payload = {
195
+ url: 'https://example.com/text',
196
+ debug: true
197
+ };
198
+ // 调用方法并验证它抛出错误
199
+ await expect(NodeFetcher.txt(payload)).rejects.toThrow('Network error');
200
+ });
201
+ });
202
+ describe('markdown方法测试 (markdown Method Tests)', () => {
203
+ test('应该能够获取Markdown内容 (Should fetch Markdown content)', async () => {
204
+ // 准备测试数据
205
+ const payload = {
206
+ url: 'https://example.com/md',
207
+ debug: true
208
+ };
209
+ // 调用方法
210
+ const result = await NodeFetcher.markdown(payload);
211
+ // 验证结果
212
+ expect(result).toBeDefined();
213
+ expect(result.markdown).toBeDefined();
214
+ expect(result.url).toBe('https://example.com/md');
215
+ expect(result.status).toBe(200);
216
+ expect(result.headers).toBeDefined();
217
+ // 验证fetch被调用
218
+ expect(fetch).toHaveBeenCalledWith(payload.url, expect.objectContaining({
219
+ headers: expect.any(Object),
220
+ timeout: expect.any(Number)
221
+ }));
222
+ });
223
+ test('应该处理获取Markdown时的错误 (Should handle errors when fetching Markdown)', async () => {
224
+ // 模拟fetch失败
225
+ fetch.mockRejectedValueOnce(new Error('Network error'));
226
+ // 准备测试数据
227
+ const payload = {
228
+ url: 'https://example.com/md',
229
+ debug: true
230
+ };
231
+ // 调用方法并验证它抛出错误
232
+ await expect(NodeFetcher.markdown(payload)).rejects.toThrow('Network error');
233
+ });
234
+ });
235
+ describe('代理功能测试 (Proxy Functionality Tests)', () => {
236
+ test('应该正确处理HTTP代理 (Should handle HTTP proxy correctly)', async () => {
237
+ // 准备测试数据
238
+ const payload = {
239
+ url: 'http://example.com',
240
+ proxy: 'http://proxy.example.com:8080',
241
+ debug: true
242
+ };
243
+ // 调用方法
244
+ await NodeFetcher.html(payload);
245
+ // 验证HttpProxyAgent被调用
246
+ const { HttpProxyAgent } = require('http-proxy-agent');
247
+ expect(HttpProxyAgent).toHaveBeenCalledWith(payload.proxy);
248
+ });
249
+ test('应该正确处理HTTPS代理 (Should handle HTTPS proxy correctly)', async () => {
250
+ // 准备测试数据
251
+ const payload = {
252
+ url: 'https://example.com',
253
+ proxy: 'http://proxy.example.com:8080',
254
+ debug: true
255
+ };
256
+ // 调用方法
257
+ await NodeFetcher.html(payload);
258
+ // 验证HttpsProxyAgent被调用
259
+ const { HttpsProxyAgent } = require('https-proxy-agent');
260
+ expect(HttpsProxyAgent).toHaveBeenCalledWith(payload.proxy);
261
+ });
262
+ });
263
+ });
@@ -0,0 +1,211 @@
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 { Client } from '@modelcontextprotocol/sdk/client/index.js';
7
+ import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
8
+ import { vi, describe, test, expect, beforeEach, afterAll } from 'vitest';
9
+ // 保存原始值以便在测试后恢复
10
+ const originalArgv = process.argv;
11
+ const originalExit = process.exit;
12
+ const originalStdoutWrite = process.stdout.write;
13
+ // 模拟 logger 模块
14
+ const mockLog = vi.fn();
15
+ vi.mock('../src/lib/logger.js', () => ({
16
+ log: mockLog,
17
+ COMPONENTS: {
18
+ CLIENT: 'CLIENT',
19
+ SERVER: 'SERVER',
20
+ NODE_FETCH: 'NODE-FETCH',
21
+ BROWSER_FETCH: 'BROWSER-FETCH'
22
+ }
23
+ }));
24
+ // 模拟依赖模块
25
+ vi.mock('@modelcontextprotocol/sdk/client/index.js', () => ({
26
+ Client: vi.fn().mockImplementation(() => ({
27
+ connect: vi.fn().mockResolvedValue(undefined),
28
+ callTool: vi.fn(),
29
+ close: vi.fn().mockResolvedValue(undefined)
30
+ }))
31
+ }));
32
+ vi.mock('@modelcontextprotocol/sdk/client/stdio.js', () => ({
33
+ StdioClientTransport: vi.fn().mockImplementation(() => ({}))
34
+ }));
35
+ vi.mock('child_process', () => ({
36
+ execSync: vi.fn()
37
+ }));
38
+ // 模拟 process.exit
39
+ vi.spyOn(process, 'exit').mockImplementation((code) => {
40
+ console.log(`[模拟] process.exit(${code})`);
41
+ return undefined;
42
+ });
43
+ // 创建模拟的客户端模块
44
+ const mockClientModule = {
45
+ responseRequiresBrowser: vi.fn((response, debug = false) => {
46
+ if (response.isError) {
47
+ const errorText = response.content[0].text.toLowerCase();
48
+ if (debug) {
49
+ mockLog('fetcher.fetchError', debug, { error: response.content[0].text }, 'CLIENT');
50
+ if (errorText.includes('403') || errorText.includes('forbidden')) {
51
+ mockLog('errors.forbidden', debug, {}, 'CLIENT');
52
+ }
53
+ }
54
+ return errorText.includes('403') ||
55
+ errorText.includes('forbidden') ||
56
+ errorText.includes('cloudflare') ||
57
+ errorText.includes('timeout') ||
58
+ errorText.includes('econnrefused');
59
+ }
60
+ return false;
61
+ }),
62
+ smartFetch: vi.fn(async (params) => {
63
+ const mockClient = new Client({
64
+ name: "fetch-mcp-client",
65
+ version: "1.0.0"
66
+ });
67
+ await mockClient.connect(new StdioClientTransport({
68
+ command: 'node',
69
+ args: ['index.js'],
70
+ stderr: 'inherit'
71
+ }));
72
+ try {
73
+ // 使用 any 类型来避免类型错误
74
+ const result = await mockClient.callTool({
75
+ name: params.method || 'fetch_html',
76
+ arguments: params
77
+ });
78
+ // 确保返回值符合 ResponseType 接口
79
+ return {
80
+ isError: typeof result.isError === 'boolean' ? result.isError : false,
81
+ content: Array.isArray(result.content) ? result.content : [{ text: '' }]
82
+ };
83
+ }
84
+ finally {
85
+ await mockClient.close();
86
+ }
87
+ }),
88
+ main: vi.fn(() => {
89
+ // 检查命令行参数
90
+ if (process.argv.length < 4) {
91
+ mockLog('client.usageInfo', true, {}, 'CLIENT');
92
+ mockLog('client.exampleUsage', true, {}, 'CLIENT');
93
+ process.exit(1);
94
+ return;
95
+ }
96
+ const method = process.argv[2];
97
+ let paramsJson = process.argv[3];
98
+ try {
99
+ JSON.parse(paramsJson);
100
+ }
101
+ catch (error) {
102
+ mockLog('client.invalidJson', true, {}, 'CLIENT');
103
+ process.exit(1);
104
+ return;
105
+ }
106
+ // 模拟成功执行
107
+ return;
108
+ })
109
+ };
110
+ // 模拟 client.ts 模块
111
+ vi.mock('../src/client.ts', () => mockClientModule);
112
+ describe('client.ts 测试', () => {
113
+ // 在每个测试前重置模拟
114
+ beforeEach(() => {
115
+ vi.resetModules();
116
+ vi.clearAllMocks();
117
+ });
118
+ // 在所有测试后恢复原始值
119
+ afterAll(() => {
120
+ process.exit = originalExit;
121
+ process.argv = originalArgv;
122
+ });
123
+ describe('responseRequiresBrowser 函数测试', () => {
124
+ test('应该在响应包含 403 错误时返回 true', () => {
125
+ const response = {
126
+ isError: true,
127
+ content: [{ text: '403 Forbidden' }]
128
+ };
129
+ const result = mockClientModule.responseRequiresBrowser(response, true);
130
+ expect(result).toBe(true);
131
+ });
132
+ test('应该在响应不是错误时返回 false', () => {
133
+ const response = {
134
+ isError: false,
135
+ content: [{ text: 'Success' }]
136
+ };
137
+ const result = mockClientModule.responseRequiresBrowser(response);
138
+ expect(result).toBe(false);
139
+ });
140
+ });
141
+ describe('smartFetch 函数测试', () => {
142
+ test('应该使用标准模式发送请求并返回结果', async () => {
143
+ // 模拟成功响应
144
+ const mockResult = {
145
+ isError: false,
146
+ content: [{ text: '<html><body>Success</body></html>' }]
147
+ };
148
+ // 设置 callTool 的返回值
149
+ const mockCallTool = vi.fn().mockResolvedValue(mockResult);
150
+ Client.mockImplementation(() => ({
151
+ connect: vi.fn().mockResolvedValue(undefined),
152
+ callTool: mockCallTool,
153
+ close: vi.fn().mockResolvedValue(undefined)
154
+ }));
155
+ // 调用函数
156
+ const result = await mockClientModule.smartFetch({
157
+ url: 'https://example.com',
158
+ debug: true
159
+ });
160
+ // 验证结果
161
+ expect(result).toEqual(mockResult);
162
+ // 验证调用
163
+ expect(Client).toHaveBeenCalled();
164
+ expect(StdioClientTransport).toHaveBeenCalled();
165
+ expect(mockCallTool).toHaveBeenCalledWith({
166
+ name: 'fetch_html',
167
+ arguments: expect.objectContaining({
168
+ url: 'https://example.com',
169
+ debug: true
170
+ })
171
+ });
172
+ });
173
+ test('应该在任何情况下都关闭客户端连接', async () => {
174
+ // 模拟 callTool 抛出错误
175
+ const mockClose = vi.fn().mockResolvedValue(undefined);
176
+ Client.mockImplementation(() => ({
177
+ connect: vi.fn().mockResolvedValue(undefined),
178
+ callTool: vi.fn().mockRejectedValue(new Error('Test error')),
179
+ close: mockClose
180
+ }));
181
+ // 调用函数并捕获错误
182
+ await expect(mockClientModule.smartFetch({
183
+ url: 'https://example.com',
184
+ debug: true
185
+ })).rejects.toThrow('Test error');
186
+ // 验证 close 被调用
187
+ expect(mockClose).toHaveBeenCalled();
188
+ });
189
+ });
190
+ describe('main 函数测试', () => {
191
+ test('应该在参数不足时显示使用信息并退出', () => {
192
+ // 设置命令行参数
193
+ process.argv = ['node', 'client.js'];
194
+ // 调用函数
195
+ mockClientModule.main();
196
+ // 验证日志和退出
197
+ expect(mockLog).toHaveBeenCalledWith('client.usageInfo', true, {}, 'CLIENT');
198
+ expect(mockLog).toHaveBeenCalledWith('client.exampleUsage', true, {}, 'CLIENT');
199
+ expect(process.exit).toHaveBeenCalledWith(1);
200
+ });
201
+ test('应该在 JSON 解析失败时显示错误并退出', () => {
202
+ // 设置命令行参数
203
+ process.argv = ['node', 'client.js', 'fetch_html', 'invalid-json'];
204
+ // 调用函数
205
+ mockClientModule.main();
206
+ // 验证日志和退出
207
+ expect(mockLog).toHaveBeenCalledWith('client.invalidJson', true, {}, 'CLIENT');
208
+ expect(process.exit).toHaveBeenCalledWith(1);
209
+ });
210
+ });
211
+ });
@@ -0,0 +1,70 @@
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 { describe, expect, test, beforeEach, vi } from 'vitest';
7
+ import { fetchHtml, fetchJson, fetchTxt, fetchMarkdown } from '../src/lib/fetch.js';
8
+ import { Fetcher } from '../src/lib/fetchers/index.js';
9
+ // Mock Fetcher
10
+ vi.mock('../src/lib/fetchers/index.js', () => ({
11
+ Fetcher: {
12
+ html: vi.fn(),
13
+ json: vi.fn(),
14
+ txt: vi.fn(),
15
+ markdown: vi.fn()
16
+ }
17
+ }));
18
+ describe('fetch 函数测试 (fetch functions tests)', () => {
19
+ // 模拟响应
20
+ const mockResponse = {
21
+ content: [{ type: 'text', text: '测试内容' }],
22
+ isError: false
23
+ };
24
+ // 测试请求参数
25
+ const testPayload = {
26
+ url: 'https://example.com',
27
+ debug: true
28
+ };
29
+ beforeEach(() => {
30
+ // 重置所有 mock
31
+ vi.clearAllMocks();
32
+ // 设置 Fetcher 方法的返回值
33
+ Fetcher.html.mockResolvedValue(mockResponse);
34
+ Fetcher.json.mockResolvedValue(mockResponse);
35
+ Fetcher.txt.mockResolvedValue(mockResponse);
36
+ Fetcher.markdown.mockResolvedValue(mockResponse);
37
+ });
38
+ test('fetchHtml 应该调用 Fetcher.html (fetchHtml should call Fetcher.html)', async () => {
39
+ // 调用方法
40
+ const result = await fetchHtml(testPayload);
41
+ // 验证 Fetcher.html 被调用
42
+ expect(Fetcher.html).toHaveBeenCalledWith(testPayload);
43
+ // 验证返回结果
44
+ expect(result).toBe(mockResponse);
45
+ });
46
+ test('fetchJson 应该调用 Fetcher.json (fetchJson should call Fetcher.json)', async () => {
47
+ // 调用方法
48
+ const result = await fetchJson(testPayload);
49
+ // 验证 Fetcher.json 被调用
50
+ expect(Fetcher.json).toHaveBeenCalledWith(testPayload);
51
+ // 验证返回结果
52
+ expect(result).toBe(mockResponse);
53
+ });
54
+ test('fetchTxt 应该调用 Fetcher.txt (fetchTxt should call Fetcher.txt)', async () => {
55
+ // 调用方法
56
+ const result = await fetchTxt(testPayload);
57
+ // 验证 Fetcher.txt 被调用
58
+ expect(Fetcher.txt).toHaveBeenCalledWith(testPayload);
59
+ // 验证返回结果
60
+ expect(result).toBe(mockResponse);
61
+ });
62
+ test('fetchMarkdown 应该调用 Fetcher.markdown (fetchMarkdown should call Fetcher.markdown)', async () => {
63
+ // 调用方法
64
+ const result = await fetchMarkdown(testPayload);
65
+ // 验证 Fetcher.markdown 被调用
66
+ expect(Fetcher.markdown).toHaveBeenCalledWith(testPayload);
67
+ // 验证返回结果
68
+ expect(result).toBe(mockResponse);
69
+ });
70
+ });
@@ -0,0 +1,108 @@
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 { describe, expect, test, beforeEach, vi } from 'vitest';
7
+ import { Fetcher } from '../../src/lib/fetchers/index.js';
8
+ import { FetcherFactory } from '../../src/lib/fetchers/FetcherFactory.js';
9
+ import { log, COMPONENTS } from '../../src/lib/logger.js';
10
+ // Mock FetcherFactory
11
+ vi.mock('../../src/lib/fetchers/FetcherFactory.js', () => ({
12
+ FetcherFactory: {
13
+ createFetcher: vi.fn()
14
+ }
15
+ }));
16
+ // Mock logger
17
+ vi.mock('../../src/lib/logger.js', () => ({
18
+ log: vi.fn(),
19
+ COMPONENTS: {
20
+ SERVER: 'SERVER'
21
+ }
22
+ }));
23
+ describe('Fetcher 类测试 (Fetcher Class Tests)', () => {
24
+ // 模拟 fetcher 实例
25
+ const mockFetcher = {
26
+ html: vi.fn(),
27
+ json: vi.fn(),
28
+ txt: vi.fn(),
29
+ markdown: vi.fn()
30
+ };
31
+ // 模拟响应
32
+ const mockResponse = {
33
+ content: [{ type: 'text', text: '测试内容' }],
34
+ isError: false
35
+ };
36
+ // 测试请求参数
37
+ const testPayload = {
38
+ url: 'https://example.com',
39
+ debug: true
40
+ };
41
+ beforeEach(() => {
42
+ // 重置所有 mock
43
+ vi.clearAllMocks();
44
+ // 设置 FetcherFactory.createFetcher 的返回值
45
+ FetcherFactory.createFetcher.mockReturnValue(mockFetcher);
46
+ // 设置 mockFetcher 方法的返回值
47
+ mockFetcher.html.mockResolvedValue(mockResponse);
48
+ mockFetcher.json.mockResolvedValue(mockResponse);
49
+ mockFetcher.txt.mockResolvedValue(mockResponse);
50
+ mockFetcher.markdown.mockResolvedValue(mockResponse);
51
+ });
52
+ test('应该正确获取HTML内容 (Should fetch HTML content correctly)', async () => {
53
+ // 调用方法
54
+ const result = await Fetcher.html(testPayload);
55
+ // 验证 FetcherFactory.createFetcher 被调用
56
+ expect(FetcherFactory.createFetcher).toHaveBeenCalledWith(testPayload);
57
+ // 验证 mockFetcher.html 被调用
58
+ expect(mockFetcher.html).toHaveBeenCalledWith(testPayload);
59
+ // 验证返回结果
60
+ expect(result).toBe(mockResponse);
61
+ // 验证日志被记录
62
+ expect(log).toHaveBeenCalledWith('fetcher.fetchingHtml', true, expect.objectContaining({
63
+ url: testPayload.url
64
+ }), COMPONENTS.SERVER);
65
+ });
66
+ test('应该正确获取JSON内容 (Should fetch JSON content correctly)', async () => {
67
+ // 调用方法
68
+ const result = await Fetcher.json(testPayload);
69
+ // 验证 FetcherFactory.createFetcher 被调用
70
+ expect(FetcherFactory.createFetcher).toHaveBeenCalledWith(testPayload);
71
+ // 验证 mockFetcher.json 被调用
72
+ expect(mockFetcher.json).toHaveBeenCalledWith(testPayload);
73
+ // 验证返回结果
74
+ expect(result).toBe(mockResponse);
75
+ // 验证日志被记录
76
+ expect(log).toHaveBeenCalledWith('fetcher.fetchingJson', true, expect.objectContaining({
77
+ url: testPayload.url
78
+ }), COMPONENTS.SERVER);
79
+ });
80
+ test('应该正确获取纯文本内容 (Should fetch plain text content correctly)', async () => {
81
+ // 调用方法
82
+ const result = await Fetcher.txt(testPayload);
83
+ // 验证 FetcherFactory.createFetcher 被调用
84
+ expect(FetcherFactory.createFetcher).toHaveBeenCalledWith(testPayload);
85
+ // 验证 mockFetcher.txt 被调用
86
+ expect(mockFetcher.txt).toHaveBeenCalledWith(testPayload);
87
+ // 验证返回结果
88
+ expect(result).toBe(mockResponse);
89
+ // 验证日志被记录
90
+ expect(log).toHaveBeenCalledWith('fetcher.fetchingTxt', true, expect.objectContaining({
91
+ url: testPayload.url
92
+ }), COMPONENTS.SERVER);
93
+ });
94
+ test('应该正确获取Markdown内容 (Should fetch Markdown content correctly)', async () => {
95
+ // 调用方法
96
+ const result = await Fetcher.markdown(testPayload);
97
+ // 验证 FetcherFactory.createFetcher 被调用
98
+ expect(FetcherFactory.createFetcher).toHaveBeenCalledWith(testPayload);
99
+ // 验证 mockFetcher.markdown 被调用
100
+ expect(mockFetcher.markdown).toHaveBeenCalledWith(testPayload);
101
+ // 验证返回结果
102
+ expect(result).toBe(mockResponse);
103
+ // 验证日志被记录
104
+ expect(log).toHaveBeenCalledWith('fetcher.fetchingMarkdown', true, expect.objectContaining({
105
+ url: testPayload.url
106
+ }), COMPONENTS.SERVER);
107
+ });
108
+ });