@lmcc-dev/mult-fetch-mcp-server 1.2.1 → 1.3.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 (81) hide show
  1. package/README.md +80 -29
  2. package/README.zh.md +80 -29
  3. package/dist/i18n-test-report.json +2 -2
  4. package/dist/i18n-unused-keys-report.json +4 -4
  5. package/dist/src/client.js +522 -131
  6. package/dist/src/index.js +0 -0
  7. package/dist/src/lib/fetch.js +8 -0
  8. package/dist/src/lib/fetchers/browser/BrowserFetcher.js +139 -199
  9. package/dist/src/lib/fetchers/common/BaseFetcher.js +203 -0
  10. package/dist/src/lib/fetchers/common/types.js +4 -0
  11. package/dist/src/lib/fetchers/common/utils.js +1 -1
  12. package/dist/src/lib/fetchers/index.js +11 -0
  13. package/dist/src/lib/fetchers/node/NodeFetcher.js +91 -66
  14. package/dist/src/lib/i18n/keys/browser.js +40 -2
  15. package/dist/src/lib/i18n/keys/chunkManager.js +43 -0
  16. package/dist/src/lib/i18n/keys/client.js +53 -1
  17. package/dist/src/lib/i18n/keys/contentSize.js +20 -0
  18. package/dist/src/lib/i18n/keys/fetcher.js +88 -79
  19. package/dist/src/lib/i18n/keys/index.js +4 -0
  20. package/dist/src/lib/i18n/keys/node.js +7 -1
  21. package/dist/src/lib/i18n/keys/processor.js +30 -0
  22. package/dist/src/lib/i18n/keys/tools.js +5 -1
  23. package/dist/src/lib/i18n/keys/url.js +45 -0
  24. package/dist/src/lib/i18n/keys.js +25 -0
  25. package/dist/src/lib/i18n/locales/en/browser.js +52 -14
  26. package/dist/src/lib/i18n/locales/en/chunkManager.js +41 -0
  27. package/dist/src/lib/i18n/locales/en/client.js +62 -9
  28. package/dist/src/lib/i18n/locales/en/contentSize.js +17 -0
  29. package/dist/src/lib/i18n/locales/en/fetcher.js +50 -30
  30. package/dist/src/lib/i18n/locales/en/index.js +9 -1
  31. package/dist/src/lib/i18n/locales/en/node.js +26 -20
  32. package/dist/src/lib/i18n/locales/en/processor.js +28 -0
  33. package/dist/src/lib/i18n/locales/en/tools.js +5 -1
  34. package/dist/src/lib/i18n/locales/en/url.js +40 -0
  35. package/dist/src/lib/i18n/locales/en.js +51 -0
  36. package/dist/src/lib/i18n/locales/zh/browser.js +85 -47
  37. package/dist/src/lib/i18n/locales/zh/chunkManager.js +41 -0
  38. package/dist/src/lib/i18n/locales/zh/client.js +53 -1
  39. package/dist/src/lib/i18n/locales/zh/contentSize.js +17 -0
  40. package/dist/src/lib/i18n/locales/zh/fetcher.js +67 -47
  41. package/dist/src/lib/i18n/locales/zh/index.js +9 -1
  42. package/dist/src/lib/i18n/locales/zh/node.js +50 -44
  43. package/dist/src/lib/i18n/locales/zh/processor.js +28 -0
  44. package/dist/src/lib/i18n/locales/zh/tools.js +5 -1
  45. package/dist/src/lib/i18n/locales/zh/url.js +40 -0
  46. package/dist/src/lib/i18n/locales/zh.js +51 -0
  47. package/dist/src/lib/logger.js +11 -2
  48. package/dist/src/lib/server/fetcher.js +15 -5
  49. package/dist/src/lib/server/tools.js +202 -52
  50. package/dist/src/lib/types.js +5 -0
  51. package/dist/src/lib/utils/ChunkManager.js +177 -0
  52. package/dist/src/lib/utils/ContentProcessor.js +134 -0
  53. package/dist/src/lib/utils/ContentSizeManager.js +123 -0
  54. package/dist/src/lib/utils/TemplateUtils.js +71 -0
  55. package/dist/src/lib/utils/errors.js +2 -8
  56. package/dist/src/mcp-server.js +0 -0
  57. package/dist/src/test-i18n.js +0 -0
  58. package/dist/tests/BrowserFetcher.test.js +0 -0
  59. package/dist/tests/NodeFetcher.test.js +0 -0
  60. package/dist/tests/client.test.js +0 -0
  61. package/dist/tests/fetch.test.js +0 -0
  62. package/dist/tests/fetchers/plain-text.test.js +146 -0
  63. package/dist/tests/i18n-missing-keys.js +0 -0
  64. package/dist/tests/i18n-remove-unused-keys.js +0 -0
  65. package/dist/tests/i18n-unused-keys.js +0 -0
  66. package/dist/tests/i18n.test.js +0 -0
  67. package/dist/tests/logger.test.js +0 -0
  68. package/dist/tests/mcp-server.test.js +0 -0
  69. package/dist/tests/server/fetcher.test.js +69 -15
  70. package/dist/tests/server/tools.test.js +165 -16
  71. package/dist/tests/setup.js +0 -0
  72. package/dist/tests/test-direct-client.js +113 -5
  73. package/dist/tests/test-i18n.js +0 -0
  74. package/dist/tests/test-mcp-methods.js +67 -1
  75. package/dist/tests/test-mcp.js +83 -3
  76. package/dist/tests/test-mini4k.js +90 -3
  77. package/dist/tests/types.test.js +0 -0
  78. package/dist/tests/utils/ChunkManager.test.js +163 -0
  79. package/dist/tests/utils/ContentSizeManager.test.js +170 -0
  80. package/dist/vitest.config.js +0 -0
  81. package/package.json +27 -23
@@ -42,7 +42,7 @@ describe('fetchWithAutoDetect 函数测试 (fetchWithAutoDetect Function Tests)'
42
42
  shouldSwitchToBrowser.mockReturnValue(false);
43
43
  });
44
44
  test('应该使用 Fetcher 获取 HTML (Should use Fetcher to fetch HTML)', async () => {
45
- const params = { url: 'https://example.com' };
45
+ const params = { url: 'https://example.com', startCursor: 0 };
46
46
  const result = await fetchWithAutoDetect(params, 'html');
47
47
  expect(Fetcher.html).toHaveBeenCalledWith(params);
48
48
  expect(result).toEqual({
@@ -51,7 +51,7 @@ describe('fetchWithAutoDetect 函数测试 (fetchWithAutoDetect Function Tests)'
51
51
  });
52
52
  });
53
53
  test('应该使用 Fetcher 获取 JSON (Should use Fetcher to fetch JSON)', async () => {
54
- const params = { url: 'https://example.com/data.json' };
54
+ const params = { url: 'https://example.com/data.json', startCursor: 0 };
55
55
  const result = await fetchWithAutoDetect(params, 'json');
56
56
  expect(Fetcher.json).toHaveBeenCalledWith(params);
57
57
  expect(result).toEqual({
@@ -60,7 +60,7 @@ describe('fetchWithAutoDetect 函数测试 (fetchWithAutoDetect Function Tests)'
60
60
  });
61
61
  });
62
62
  test('应该使用 Fetcher 获取文本 (Should use Fetcher to fetch text)', async () => {
63
- const params = { url: 'https://example.com/text.txt' };
63
+ const params = { url: 'https://example.com/text.txt', startCursor: 0 };
64
64
  const result = await fetchWithAutoDetect(params, 'txt');
65
65
  expect(Fetcher.txt).toHaveBeenCalledWith(params);
66
66
  expect(result).toEqual({
@@ -69,7 +69,7 @@ describe('fetchWithAutoDetect 函数测试 (fetchWithAutoDetect Function Tests)'
69
69
  });
70
70
  });
71
71
  test('应该使用 Fetcher 获取 Markdown (Should use Fetcher to fetch Markdown)', async () => {
72
- const params = { url: 'https://example.com/readme.md' };
72
+ const params = { url: 'https://example.com/readme.md', startCursor: 0 };
73
73
  const result = await fetchWithAutoDetect(params, 'markdown');
74
74
  expect(Fetcher.markdown).toHaveBeenCalledWith(params);
75
75
  expect(result).toEqual({
@@ -78,7 +78,7 @@ describe('fetchWithAutoDetect 函数测试 (fetchWithAutoDetect Function Tests)'
78
78
  });
79
79
  });
80
80
  test('应该在浏览器模式下使用 Fetcher 获取 HTML (Should use Fetcher to fetch HTML in browser mode)', async () => {
81
- const params = { url: 'https://example.com', useBrowser: true };
81
+ const params = { url: 'https://example.com', useBrowser: true, startCursor: 0 };
82
82
  // 设置浏览器模式下的返回值
83
83
  Fetcher.html.mockResolvedValue({
84
84
  content: [{ type: 'text', text: '<html>Browser Test</html>' }],
@@ -93,7 +93,7 @@ describe('fetchWithAutoDetect 函数测试 (fetchWithAutoDetect Function Tests)'
93
93
  });
94
94
  });
95
95
  test('应该在浏览器模式下使用 Fetcher 获取 JSON (Should use Fetcher to fetch JSON in browser mode)', async () => {
96
- const params = { url: 'https://example.com/data.json', useBrowser: true };
96
+ const params = { url: 'https://example.com/data.json', useBrowser: true, startCursor: 0 };
97
97
  // 设置浏览器模式下的返回值
98
98
  Fetcher.json.mockResolvedValue({
99
99
  content: [{ type: 'text', text: '{"test":"browser data"}' }],
@@ -108,7 +108,7 @@ describe('fetchWithAutoDetect 函数测试 (fetchWithAutoDetect Function Tests)'
108
108
  });
109
109
  });
110
110
  test('应该在 Fetcher 失败时自动切换到浏览器模式 (Should automatically switch to browser mode when Fetcher fails)', async () => {
111
- const params = { url: 'https://example.com' };
111
+ const params = { url: 'https://example.com', startCursor: 0 };
112
112
  // 设置 Fetcher 第一次调用抛出错误,第二次调用成功
113
113
  Fetcher.html
114
114
  .mockRejectedValueOnce(new Error('Fetch failed'))
@@ -130,7 +130,7 @@ describe('fetchWithAutoDetect 函数测试 (fetchWithAutoDetect Function Tests)'
130
130
  });
131
131
  });
132
132
  test('应该在 Fetcher 失败且不需要切换时抛出原始错误 (Should throw original error when Fetcher fails and no switch is needed)', async () => {
133
- const params = { url: 'https://example.com' };
133
+ const params = { url: 'https://example.com', startCursor: 0 };
134
134
  // 设置 Fetcher 抛出错误
135
135
  const originalError = new Error('Fetch failed');
136
136
  Fetcher.html.mockRejectedValue(originalError);
@@ -141,7 +141,7 @@ describe('fetchWithAutoDetect 函数测试 (fetchWithAutoDetect Function Tests)'
141
141
  expect(shouldSwitchToBrowser).toHaveBeenCalled();
142
142
  });
143
143
  test('应该在 autoDetectMode 为 false 时不自动切换 (Should not automatically switch when autoDetectMode is false)', async () => {
144
- const params = { url: 'https://example.com', autoDetectMode: false };
144
+ const params = { url: 'https://example.com', autoDetectMode: false, startCursor: 0 };
145
145
  // 设置 Fetcher 抛出错误
146
146
  const originalError = new Error('Fetch failed');
147
147
  Fetcher.html.mockRejectedValue(originalError);
@@ -150,7 +150,7 @@ describe('fetchWithAutoDetect 函数测试 (fetchWithAutoDetect Function Tests)'
150
150
  expect(shouldSwitchToBrowser).not.toHaveBeenCalled();
151
151
  });
152
152
  test('应该在 closeBrowser 为 true 时关闭浏览器 (Should close browser when closeBrowser is true)', async () => {
153
- const params = { url: 'https://example.com', useBrowser: true, closeBrowser: true };
153
+ const params = { url: 'https://example.com', useBrowser: true, closeBrowser: true, startCursor: 0 };
154
154
  // 修改测试:在 fetchWithAutoDetect 完成后手动调用 closeBrowserInstance
155
155
  // 这是因为在实际代码中,closeBrowserInstance 只在出错时或者在 fetchWithAutoDetect 函数外部调用
156
156
  await fetchWithAutoDetect(params, 'html');
@@ -161,13 +161,67 @@ describe('fetchWithAutoDetect 函数测试 (fetchWithAutoDetect Function Tests)'
161
161
  expect(closeBrowserInstance).toHaveBeenCalled();
162
162
  });
163
163
  test('应该在出错且 closeBrowser 为 true 时关闭浏览器 (Should close browser when error occurs and closeBrowser is true)', async () => {
164
- const params = { url: 'https://example.com', useBrowser: true, closeBrowser: true };
164
+ const params = { url: 'https://example.com', useBrowser: true, closeBrowser: true, startCursor: 0 };
165
165
  // 设置 Fetcher 抛出错误
166
- const originalError = new Error('Browser fetch failed');
167
- Fetcher.html.mockRejectedValue(originalError);
168
- await expect(fetchWithAutoDetect(params, 'html')).rejects.toThrow(originalError);
166
+ Fetcher.html.mockRejectedValue(new Error('Fetch error'));
167
+ // 验证函数抛出错误并且关闭了浏览器
168
+ await expect(fetchWithAutoDetect(params, 'html')).rejects.toThrow('Fetch error');
169
+ expect(closeBrowserInstance).toHaveBeenCalled();
170
+ });
171
+ test('应该处理 HTML 获取时出现的错误 (Should handle errors during HTML fetch)', async () => {
172
+ Fetcher.html.mockRejectedValue(new Error('Fetch error'));
173
+ const params = { url: 'https://example.com', startCursor: 0 };
174
+ await expect(fetchWithAutoDetect(params, 'html')).rejects.toThrow('Fetch error');
175
+ });
176
+ test('应该初始化浏览器并关闭浏览器实例 (Should initialize browser and close browser instance)', async () => {
177
+ const params = {
178
+ url: 'https://example.com',
179
+ useBrowser: true,
180
+ startCursor: 0
181
+ };
182
+ const result = await fetchWithAutoDetect(params, 'html');
169
183
  expect(initializeBrowser).toHaveBeenCalled();
170
- expect(Fetcher.html).toHaveBeenCalled();
184
+ expect(closeBrowserInstance).not.toHaveBeenCalled(); // 默认情况下不关闭浏览器
185
+ expect(result).toEqual({
186
+ content: [{ type: 'text', text: '<html>Test</html>' }],
187
+ isError: false
188
+ });
189
+ });
190
+ test('如果指定了 closeBrowser,应该关闭浏览器 (Should close browser if closeBrowser is specified)', async () => {
191
+ const params = {
192
+ url: 'https://example.com',
193
+ useBrowser: true,
194
+ closeBrowser: true,
195
+ startCursor: 0
196
+ };
197
+ // 模拟出错来触发closeBrowserInstance的调用
198
+ Fetcher.html.mockRejectedValueOnce(new Error('Test error'));
199
+ // 预期应该抛出错误
200
+ await expect(fetchWithAutoDetect(params, 'html')).rejects.toThrow('Test error');
201
+ // 验证closeBrowserInstance被调用
171
202
  expect(closeBrowserInstance).toHaveBeenCalled();
172
203
  });
204
+ test('如果 shouldSwitchToBrowser 返回 true,应该切换到浏览器模式 (Should switch to browser mode if shouldSwitchToBrowser returns true)', async () => {
205
+ // 先模拟一次失败,这样会调用shouldSwitchToBrowser
206
+ Fetcher.html
207
+ .mockRejectedValueOnce(new Error('Fetch error'));
208
+ shouldSwitchToBrowser.mockReturnValue(true);
209
+ const params = {
210
+ url: 'https://example.com',
211
+ autoDetectMode: true,
212
+ startCursor: 0
213
+ };
214
+ await fetchWithAutoDetect(params, 'html');
215
+ expect(shouldSwitchToBrowser).toHaveBeenCalled();
216
+ expect(initializeBrowser).toHaveBeenCalled();
217
+ });
218
+ test('如果 autoDetectMode 为 false,不应该检查是否切换到浏览器 (Should not check if should switch to browser if autoDetectMode is false)', async () => {
219
+ const params = {
220
+ url: 'https://example.com',
221
+ autoDetectMode: false,
222
+ startCursor: 0
223
+ };
224
+ await fetchWithAutoDetect(params, 'html');
225
+ expect(shouldSwitchToBrowser).not.toHaveBeenCalled();
226
+ });
173
227
  });
@@ -48,13 +48,14 @@ describe('工具注册和调用测试 (Tools Registration and Calling Tests)', (
48
48
  return listToolsHandler().then((result) => {
49
49
  expect(result).toHaveProperty('tools');
50
50
  expect(result.tools).toBeInstanceOf(Array);
51
- expect(result.tools.length).toBe(4); // 应该有4个工具
51
+ expect(result.tools.length).toBe(5); // 应该有5个工具
52
52
  // 验证工具名称
53
53
  const toolNames = result.tools.map((tool) => tool.name);
54
54
  expect(toolNames).toContain('fetch_html');
55
55
  expect(toolNames).toContain('fetch_json');
56
56
  expect(toolNames).toContain('fetch_txt');
57
57
  expect(toolNames).toContain('fetch_markdown');
58
+ expect(toolNames).toContain('fetch_plaintext'); // 验证新添加的HTML转文本工具
58
59
  // 验证每个工具都有必要的属性
59
60
  result.tools.forEach((tool) => {
60
61
  expect(tool).toHaveProperty('name');
@@ -63,7 +64,8 @@ describe('工具注册和调用测试 (Tools Registration and Calling Tests)', (
63
64
  expect(tool.inputSchema).toHaveProperty('type', 'object');
64
65
  expect(tool.inputSchema).toHaveProperty('properties');
65
66
  expect(tool.inputSchema.properties).toHaveProperty('url');
66
- expect(tool.inputSchema).toHaveProperty('required', ['url']);
67
+ // 不同工具可能有不同的 required 字段
68
+ expect(tool.inputSchema).toHaveProperty('required');
67
69
  });
68
70
  });
69
71
  });
@@ -83,14 +85,15 @@ describe('工具注册和调用测试 (Tools Registration and Calling Tests)', (
83
85
  params: {
84
86
  name: 'fetch_html',
85
87
  arguments: {
86
- url: 'https://example.com'
88
+ url: 'https://example.com',
89
+ startCursor: 0
87
90
  }
88
91
  }
89
92
  };
90
93
  // 调用处理程序
91
94
  const result = await callToolHandler(request);
92
95
  // 验证 fetchWithAutoDetect 被调用,且参数正确
93
- expect(fetchWithAutoDetect).toHaveBeenCalledWith({ url: 'https://example.com', method: 'fetch_html' }, 'html');
96
+ expect(fetchWithAutoDetect).toHaveBeenCalledWith({ url: 'https://example.com', method: 'fetch_html', startCursor: 0 }, 'html');
94
97
  // 验证返回结果
95
98
  expect(result).toHaveProperty('content');
96
99
  expect(result).toHaveProperty('isError', false);
@@ -107,14 +110,15 @@ describe('工具注册和调用测试 (Tools Registration and Calling Tests)', (
107
110
  params: {
108
111
  name: 'fetch_json',
109
112
  arguments: {
110
- url: 'https://example.com/data.json'
113
+ url: 'https://example.com/data.json',
114
+ startCursor: 0
111
115
  }
112
116
  }
113
117
  };
114
118
  // 调用处理程序
115
119
  const result = await callToolHandler(request);
116
120
  // 验证 fetchWithAutoDetect 被调用,且参数正确
117
- expect(fetchWithAutoDetect).toHaveBeenCalledWith({ url: 'https://example.com/data.json', method: 'fetch_json' }, 'json');
121
+ expect(fetchWithAutoDetect).toHaveBeenCalledWith({ url: 'https://example.com/data.json', method: 'fetch_json', startCursor: 0 }, 'json');
118
122
  // 验证返回结果
119
123
  expect(result).toHaveProperty('content');
120
124
  expect(result).toHaveProperty('isError', false);
@@ -131,14 +135,15 @@ describe('工具注册和调用测试 (Tools Registration and Calling Tests)', (
131
135
  params: {
132
136
  name: 'fetch_txt',
133
137
  arguments: {
134
- url: 'https://example.com/text.txt'
138
+ url: 'https://example.com/text.txt',
139
+ startCursor: 0
135
140
  }
136
141
  }
137
142
  };
138
143
  // 调用处理程序
139
144
  const result = await callToolHandler(request);
140
145
  // 验证 fetchWithAutoDetect 被调用,且参数正确
141
- expect(fetchWithAutoDetect).toHaveBeenCalledWith({ url: 'https://example.com/text.txt', method: 'fetch_txt' }, 'txt');
146
+ expect(fetchWithAutoDetect).toHaveBeenCalledWith({ url: 'https://example.com/text.txt', method: 'fetch_txt', startCursor: 0 }, 'txt');
142
147
  // 验证返回结果
143
148
  expect(result).toHaveProperty('content');
144
149
  expect(result).toHaveProperty('isError', false);
@@ -155,20 +160,164 @@ describe('工具注册和调用测试 (Tools Registration and Calling Tests)', (
155
160
  params: {
156
161
  name: 'fetch_markdown',
157
162
  arguments: {
158
- url: 'https://example.com/readme.md'
163
+ url: 'https://example.com/readme.md',
164
+ startCursor: 0
159
165
  }
160
166
  }
161
167
  };
162
168
  // 调用处理程序
163
169
  const result = await callToolHandler(request);
164
170
  // 验证 fetchWithAutoDetect 被调用,且参数正确
165
- expect(fetchWithAutoDetect).toHaveBeenCalledWith({ url: 'https://example.com/readme.md', method: 'fetch_markdown' }, 'markdown');
171
+ expect(fetchWithAutoDetect).toHaveBeenCalledWith({ url: 'https://example.com/readme.md', method: 'fetch_markdown', startCursor: 0 }, 'markdown');
166
172
  // 验证返回结果
167
173
  expect(result).toHaveProperty('content');
168
174
  expect(result).toHaveProperty('isError', false);
169
175
  expect(result.content[0]).toHaveProperty('type', 'text');
170
176
  expect(result.content[0]).toHaveProperty('text', 'mock content');
171
177
  });
178
+ test('应该正确处理 fetch_plaintext 工具调用 (Should handle fetch_plaintext tool call correctly)', async () => {
179
+ // 调用被测试的函数
180
+ registerTools(mockServer);
181
+ // 获取注册的处理程序
182
+ const callToolHandler = mockSetRequestHandler.mock.calls[1][1];
183
+ // 创建请求对象
184
+ const request = {
185
+ params: {
186
+ name: 'fetch_plaintext',
187
+ arguments: {
188
+ url: 'https://example.com',
189
+ startCursor: 0
190
+ }
191
+ }
192
+ };
193
+ // 调用处理程序
194
+ const result = await callToolHandler(request);
195
+ // 验证 fetchWithAutoDetect 被调用,且参数正确
196
+ expect(fetchWithAutoDetect).toHaveBeenCalledWith({ url: 'https://example.com', method: 'fetch_plaintext', startCursor: 0 }, 'plaintext');
197
+ // 验证返回结果
198
+ expect(result).toHaveProperty('content');
199
+ expect(result).toHaveProperty('isError', false);
200
+ expect(result.content[0]).toHaveProperty('type', 'text');
201
+ expect(result.content[0]).toHaveProperty('text', 'mock content');
202
+ });
203
+ test('应该正确处理带有分块ID的请求 (Should handle request with chunk ID correctly)', async () => {
204
+ // 调用被测试的函数
205
+ registerTools(mockServer);
206
+ // 获取注册的处理程序
207
+ const callToolHandler = mockSetRequestHandler.mock.calls[1][1];
208
+ // 设置模拟返回值,模拟分块返回
209
+ fetchWithAutoDetect.mockResolvedValue({
210
+ content: [{
211
+ type: 'text',
212
+ text: 'chunk content',
213
+ metadata: {
214
+ chunkInfo: {
215
+ isChunked: true,
216
+ chunkId: 'test-chunk-id',
217
+ startCursor: 1000,
218
+ totalBytes: 5000,
219
+ fetchedBytes: 1000,
220
+ remainingBytes: 3000,
221
+ isLastChunk: false
222
+ }
223
+ }
224
+ }],
225
+ isError: false
226
+ });
227
+ // 创建请求对象,包含分块ID和起始游标
228
+ const request = {
229
+ params: {
230
+ name: 'fetch_html',
231
+ arguments: {
232
+ url: 'https://example.com',
233
+ chunkId: 'test-chunk-id',
234
+ startCursor: 1000
235
+ }
236
+ }
237
+ };
238
+ // 调用处理程序
239
+ const result = await callToolHandler(request);
240
+ // 验证 fetchWithAutoDetect 被调用,且参数正确,包含分块信息
241
+ expect(fetchWithAutoDetect).toHaveBeenCalledWith({
242
+ url: 'https://example.com',
243
+ method: 'fetch_html',
244
+ chunkId: 'test-chunk-id',
245
+ startCursor: 1000
246
+ }, 'html');
247
+ // 验证返回结果包含分块信息
248
+ expect(result).toHaveProperty('content');
249
+ expect(result).toHaveProperty('isError', false);
250
+ expect(result.content[0]).toHaveProperty('type', 'text');
251
+ expect(result.content[0]).toHaveProperty('text', 'chunk content');
252
+ expect(result.content[0]).toHaveProperty('metadata');
253
+ expect(result.content[0].metadata).toHaveProperty('chunkInfo');
254
+ expect(result.content[0].metadata.chunkInfo).toHaveProperty('isChunked', true);
255
+ expect(result.content[0].metadata.chunkInfo).toHaveProperty('chunkId', 'test-chunk-id');
256
+ expect(result.content[0].metadata.chunkInfo).toHaveProperty('startCursor', 1000);
257
+ expect(result.content[0].metadata.chunkInfo).toHaveProperty('totalBytes', 5000);
258
+ expect(result.content[0].metadata.chunkInfo).toHaveProperty('fetchedBytes', 1000);
259
+ expect(result.content[0].metadata.chunkInfo).toHaveProperty('remainingBytes', 3000);
260
+ expect(result.content[0].metadata.chunkInfo).toHaveProperty('isLastChunk', false);
261
+ });
262
+ test('应该正确处理启用内容分割的请求 (Should handle request with content splitting enabled)', async () => {
263
+ // 调用被测试的函数
264
+ registerTools(mockServer);
265
+ // 获取注册的处理程序
266
+ const callToolHandler = mockSetRequestHandler.mock.calls[1][1];
267
+ // 设置模拟返回值,模拟分块返回
268
+ fetchWithAutoDetect.mockResolvedValue({
269
+ content: [{
270
+ type: 'text',
271
+ text: 'chunked content',
272
+ metadata: {
273
+ chunkInfo: {
274
+ isChunked: true,
275
+ chunkId: 'auto-generated-id',
276
+ startCursor: 0,
277
+ totalBytes: 10000,
278
+ fetchedBytes: 1024,
279
+ remainingBytes: 8976,
280
+ isLastChunk: false
281
+ }
282
+ }
283
+ }],
284
+ isError: false
285
+ });
286
+ // 创建请求对象,启用内容分割
287
+ const request = {
288
+ params: {
289
+ name: 'fetch_html',
290
+ arguments: {
291
+ url: 'https://example.com',
292
+ enableContentSplitting: true,
293
+ contentSizeLimit: 1024, // 1KB
294
+ startCursor: 0
295
+ }
296
+ }
297
+ };
298
+ // 调用处理程序
299
+ const result = await callToolHandler(request);
300
+ // 验证 fetchWithAutoDetect 被调用,且参数正确
301
+ expect(fetchWithAutoDetect).toHaveBeenCalledWith({
302
+ url: 'https://example.com',
303
+ method: 'fetch_html',
304
+ enableContentSplitting: true,
305
+ contentSizeLimit: 1024,
306
+ startCursor: 0
307
+ }, 'html');
308
+ // 验证返回结果包含分块信息
309
+ expect(result).toHaveProperty('content');
310
+ expect(result).toHaveProperty('isError', false);
311
+ expect(result.content[0]).toHaveProperty('metadata');
312
+ expect(result.content[0].metadata).toHaveProperty('chunkInfo');
313
+ expect(result.content[0].metadata.chunkInfo).toHaveProperty('isChunked', true);
314
+ expect(result.content[0].metadata.chunkInfo).toHaveProperty('chunkId', 'auto-generated-id');
315
+ expect(result.content[0].metadata.chunkInfo).toHaveProperty('startCursor', 0);
316
+ expect(result.content[0].metadata.chunkInfo).toHaveProperty('totalBytes', 10000);
317
+ expect(result.content[0].metadata.chunkInfo).toHaveProperty('fetchedBytes', 1024);
318
+ expect(result.content[0].metadata.chunkInfo).toHaveProperty('remainingBytes', 8976);
319
+ expect(result.content[0].metadata.chunkInfo).toHaveProperty('isLastChunk', false);
320
+ });
172
321
  test('应该正确处理未知工具调用 (Should handle unknown tool call correctly)', async () => {
173
322
  // 调用被测试的函数
174
323
  registerTools(mockServer);
@@ -201,23 +350,23 @@ describe('工具注册和调用测试 (Tools Registration and Calling Tests)', (
201
350
  registerTools(mockServer);
202
351
  // 获取注册的处理程序
203
352
  const callToolHandler = mockSetRequestHandler.mock.calls[1][1];
353
+ // 模拟fetchWithAutoDetect抛出错误
354
+ fetchWithAutoDetect.mockRejectedValue(new Error('Fetch failed'));
204
355
  // 创建请求对象
205
356
  const request = {
206
357
  params: {
207
358
  name: 'fetch_html',
208
359
  arguments: {
209
- url: 'https://example.com'
360
+ url: 'https://example.com',
361
+ startCursor: 0
210
362
  }
211
363
  }
212
364
  };
213
- // 设置模拟抛出错误
214
- fetchWithAutoDetect.mockRejectedValue(new Error('Fetch failed'));
215
365
  // 调用处理程序
216
366
  const result = await callToolHandler(request);
217
- // 验证返回结果
218
- expect(result).toHaveProperty('content');
367
+ // 验证返回错误
219
368
  expect(result).toHaveProperty('isError', true);
220
369
  expect(result.content[0]).toHaveProperty('text');
221
- expect(result.content[0].text).toContain('Fetch failed');
370
+ expect(result.content[0].text).toContain('Error fetching https://example.com');
222
371
  });
223
372
  });
File without changes
@@ -24,7 +24,8 @@ async function testDirectClient() {
24
24
  method: 'fetch_html',
25
25
  params: {
26
26
  url: 'https://example.com',
27
- debug: true
27
+ debug: true,
28
+ startCursor: 0
28
29
  }
29
30
  },
30
31
  {
@@ -32,7 +33,8 @@ async function testDirectClient() {
32
33
  method: 'fetch_json',
33
34
  params: {
34
35
  url: 'https://jsonplaceholder.typicode.com/todos/1',
35
- debug: true
36
+ debug: true,
37
+ startCursor: 0
36
38
  }
37
39
  },
38
40
  {
@@ -40,7 +42,8 @@ async function testDirectClient() {
40
42
  method: 'fetch_txt',
41
43
  params: {
42
44
  url: 'https://example.com',
43
- debug: true
45
+ debug: true,
46
+ startCursor: 0
44
47
  }
45
48
  },
46
49
  {
@@ -48,7 +51,28 @@ async function testDirectClient() {
48
51
  method: 'fetch_markdown',
49
52
  params: {
50
53
  url: 'https://example.com',
51
- debug: true
54
+ debug: true,
55
+ startCursor: 0
56
+ }
57
+ },
58
+ {
59
+ name: 'HTML 转纯文本测试',
60
+ method: 'fetch_plaintext',
61
+ params: {
62
+ url: 'https://example.com',
63
+ debug: true,
64
+ startCursor: 0
65
+ }
66
+ },
67
+ {
68
+ name: '分块测试 - 第一个分块',
69
+ method: 'fetch_html',
70
+ params: {
71
+ url: 'https://example.com',
72
+ debug: true,
73
+ contentSizeLimit: 2000,
74
+ enableContentSplitting: true,
75
+ startCursor: 0
52
76
  }
53
77
  },
54
78
  {
@@ -57,10 +81,14 @@ async function testDirectClient() {
57
81
  params: {
58
82
  url: 'https://this-domain-does-not-exist-123456789.com',
59
83
  debug: true,
60
- timeout: 5000
84
+ timeout: 5000,
85
+ startCursor: 0
61
86
  }
62
87
  }
63
88
  ];
89
+ // 获取分块ID和字节位置,用于第二个分块测试
90
+ let chunkId = null;
91
+ let fetchedBytes = 0;
64
92
  // 依次执行每个测试用例
65
93
  for (const testCase of testCases) {
66
94
  console.log(`\n执行测试: ${testCase.name}`);
@@ -95,6 +123,86 @@ async function testDirectClient() {
95
123
  });
96
124
  // 输出结果
97
125
  console.log(`退出代码: ${result.code}`);
126
+ if (result.code === 0) {
127
+ console.log('测试成功!');
128
+ // 尝试解析JSON结果
129
+ try {
130
+ const jsonResult = JSON.parse(result.stdout);
131
+ console.log('结果类型:', typeof jsonResult);
132
+ if (typeof jsonResult === 'string') {
133
+ console.log('内容长度:', jsonResult.length);
134
+ console.log('内容预览:', jsonResult.substring(0, 200) + '...');
135
+ // 如果是分块测试,获取分块ID和字节位置
136
+ if (testCase.name === '分块测试 - 第一个分块') {
137
+ const systemNoteMatch = jsonResult.match(/===\s*SYSTEM\s*NOTE\s*===.*?====================/s);
138
+ if (systemNoteMatch) {
139
+ console.log('检测到系统注释,表示有更多分块可用');
140
+ const chunkIdMatch = systemNoteMatch[0].match(/chunkId:\s*"([^"]+)"/);
141
+ const fetchedBytesMatch = systemNoteMatch[0].match(/fetchedBytes:\s*(\d+)/);
142
+ if (chunkIdMatch && fetchedBytesMatch) {
143
+ chunkId = chunkIdMatch[1];
144
+ fetchedBytes = parseInt(fetchedBytesMatch[1]);
145
+ console.log(`获取到 chunkId: ${chunkId}, fetchedBytes: ${fetchedBytes}`);
146
+ }
147
+ }
148
+ }
149
+ }
150
+ else {
151
+ console.log('结果:', JSON.stringify(jsonResult, null, 2));
152
+ }
153
+ }
154
+ catch (error) {
155
+ console.log('原始输出:');
156
+ console.log(result.stdout);
157
+ }
158
+ }
159
+ else {
160
+ console.error('测试失败!');
161
+ console.error('错误输出:');
162
+ console.error(result.stderr);
163
+ }
164
+ }
165
+ // 如果有分块ID,测试获取第二个分块
166
+ if (chunkId && fetchedBytes > 0) {
167
+ console.log(`\n执行测试: 分块测试 - 第二个分块`);
168
+ const clientPath = path.resolve(rootDir, 'dist/src/client.js');
169
+ const secondChunkParams = {
170
+ url: 'https://example.com',
171
+ debug: true,
172
+ contentSizeLimit: 2000,
173
+ enableContentSplitting: true,
174
+ chunkId: chunkId,
175
+ startCursor: fetchedBytes
176
+ };
177
+ const args = [
178
+ clientPath,
179
+ 'fetch_html',
180
+ JSON.stringify(secondChunkParams)
181
+ ];
182
+ console.log(`运行命令: node ${args.join(' ')}`);
183
+ // 执行命令
184
+ const result = await new Promise((resolve) => {
185
+ const childProcess = spawn('node', args, {
186
+ stdio: ['ignore', 'pipe', 'pipe']
187
+ });
188
+ let stdout = '';
189
+ let stderr = '';
190
+ childProcess.stdout.on('data', (data) => {
191
+ stdout += data.toString();
192
+ });
193
+ childProcess.stderr.on('data', (data) => {
194
+ stderr += data.toString();
195
+ });
196
+ childProcess.on('close', (code) => {
197
+ resolve({
198
+ code: code || 0,
199
+ stdout,
200
+ stderr
201
+ });
202
+ });
203
+ });
204
+ // 输出结果
205
+ console.log(`退出代码: ${result.code}`);
98
206
  if (result.code === 0) {
99
207
  console.log('测试成功!');
100
208
  // 尝试解析JSON结果
File without changes