@lmcc-dev/mult-fetch-mcp-server 1.3.1 → 1.3.3

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 (109) hide show
  1. package/README.md +70 -15
  2. package/README.zh.md +31 -0
  3. package/dist/src/client.js +118 -91
  4. package/dist/src/index.js +0 -0
  5. package/dist/src/lib/fetchers/browser/BrowserFetcher.js +27 -10
  6. package/dist/src/lib/fetchers/browser/BrowserInstance.js +49 -43
  7. package/dist/src/lib/fetchers/common/BaseFetcher.js +106 -10
  8. package/dist/src/lib/fetchers/common/types.js +10 -1
  9. package/dist/src/lib/fetchers/common/utils.js +1 -1
  10. package/dist/src/lib/fetchers/node/HttpClient.js +47 -21
  11. package/dist/src/lib/fetchers/node/NodeFetcher.js +24 -8
  12. package/dist/src/lib/i18n/index.js +2 -2
  13. package/dist/src/lib/i18n/keys/client.js +1 -0
  14. package/dist/src/lib/i18n/keys/extractor.js +26 -0
  15. package/dist/src/lib/i18n/keys/fetcher.js +12 -1
  16. package/dist/src/lib/i18n/keys/index.js +1 -0
  17. package/dist/src/lib/i18n/keys/node.js +2 -0
  18. package/dist/src/lib/i18n/locales/en/client.js +1 -0
  19. package/dist/src/lib/i18n/locales/en/extractor.js +24 -0
  20. package/dist/src/lib/i18n/locales/en/fetcher.js +3 -1
  21. package/dist/src/lib/i18n/locales/en/index.js +2 -0
  22. package/dist/src/lib/i18n/locales/en/node.js +2 -0
  23. package/dist/src/lib/i18n/locales/zh/client.js +1 -0
  24. package/dist/src/lib/i18n/locales/zh/extractor.js +22 -0
  25. package/dist/src/lib/i18n/locales/zh/fetcher.js +13 -3
  26. package/dist/src/lib/i18n/locales/zh/index.js +2 -0
  27. package/dist/src/lib/i18n/locales/zh/node.js +2 -0
  28. package/dist/src/lib/i18n/logger.js +2 -2
  29. package/dist/src/lib/logger.js +38 -17
  30. package/dist/src/lib/server/browser.js +2 -2
  31. package/dist/src/lib/server/fetcher.js +0 -3
  32. package/dist/src/lib/server/index.js +2 -2
  33. package/dist/src/lib/server/prompts.js +4 -4
  34. package/dist/src/lib/server/tools.js +127 -354
  35. package/dist/src/lib/utils/ChunkManager.js +2 -2
  36. package/dist/src/lib/utils/ContentExtractor.js +141 -0
  37. package/dist/src/lib/utils/ContentProcessor.js +5 -11
  38. package/dist/src/lib/utils/ContentSizeManager.js +2 -2
  39. package/dist/src/lib/utils/ErrorHandler.js +1 -0
  40. package/dist/src/lib/utils/TemplateUtils.js +6 -2
  41. package/dist/src/lib/utils/UrlValidator.js +205 -0
  42. package/dist/src/mcp-server.js +0 -0
  43. package/dist/tests/client.test.js +1 -1
  44. package/dist/tests/fetch.test.js +0 -0
  45. package/dist/tests/fetchers/node/HttpClient.test.js +50 -0
  46. package/dist/tests/i18n-missing-keys.js +0 -0
  47. package/dist/tests/i18n-unused-keys.js +0 -0
  48. package/dist/tests/i18n.test.js +0 -0
  49. package/dist/tests/logger.test.js +0 -0
  50. package/dist/tests/mcp-server.test.js +0 -0
  51. package/dist/tests/setup.js +0 -0
  52. package/dist/tests/test-direct-client.js +0 -0
  53. package/dist/tests/test-extract-single.js +389 -0
  54. package/dist/tests/test-i18n.js +0 -0
  55. package/dist/tests/test-mcp-methods.js +0 -0
  56. package/dist/tests/test-mcp.js +0 -0
  57. package/dist/tests/test-mini4k.js +0 -0
  58. package/dist/tests/types.test.js +0 -0
  59. package/dist/tests/utils/ContentExtractor.test.js +173 -0
  60. package/dist/tests/utils/ContentProcessor.test.js +136 -0
  61. package/dist/tests/utils/TemplateUtils.test.js +118 -0
  62. package/dist/tests/utils/UrlValidator.test.js +58 -0
  63. package/package.json +33 -25
  64. package/dist/i18n-test-report.json +0 -8
  65. package/dist/i18n-unused-keys-report.json +0 -8
  66. package/dist/src/lib/BrowserFetcher.js +0 -787
  67. package/dist/src/lib/NodeFetcher.js +0 -492
  68. package/dist/src/lib/i18n/keys.js +0 -529
  69. package/dist/src/test-i18n.js +0 -139
  70. package/dist/tests/BrowserFetcher.test.js +0 -951
  71. package/dist/tests/NodeFetcher.test.js +0 -263
  72. package/dist/tests/i18n-remove-unused-keys.js +0 -236
  73. package/dist/tests/i18n-test-report.json +0 -2004
  74. package/dist/tests/src/lib/i18n/index.js +0 -108
  75. package/dist/tests/src/lib/i18n/keys/base.js +0 -47
  76. package/dist/tests/src/lib/i18n/keys/browser.js +0 -93
  77. package/dist/tests/src/lib/i18n/keys/client.js +0 -70
  78. package/dist/tests/src/lib/i18n/keys/errors.js +0 -34
  79. package/dist/tests/src/lib/i18n/keys/fetcher.js +0 -84
  80. package/dist/tests/src/lib/i18n/keys/index.js +0 -31
  81. package/dist/tests/src/lib/i18n/keys/node.js +0 -56
  82. package/dist/tests/src/lib/i18n/keys/prompts.js +0 -82
  83. package/dist/tests/src/lib/i18n/keys/resources.js +0 -50
  84. package/dist/tests/src/lib/i18n/keys/server.js +0 -64
  85. package/dist/tests/src/lib/i18n/keys/tools.js +0 -34
  86. package/dist/tests/src/lib/i18n/locales/en/browser.js +0 -88
  87. package/dist/tests/src/lib/i18n/locales/en/client.js +0 -66
  88. package/dist/tests/src/lib/i18n/locales/en/errors.js +0 -28
  89. package/dist/tests/src/lib/i18n/locales/en/fetcher.js +0 -71
  90. package/dist/tests/src/lib/i18n/locales/en/index.js +0 -29
  91. package/dist/tests/src/lib/i18n/locales/en/node.js +0 -51
  92. package/dist/tests/src/lib/i18n/locales/en/prompts.js +0 -52
  93. package/dist/tests/src/lib/i18n/locales/en/resources.js +0 -50
  94. package/dist/tests/src/lib/i18n/locales/en/server.js +0 -56
  95. package/dist/tests/src/lib/i18n/locales/en/tools.js +0 -28
  96. package/dist/tests/src/lib/i18n/locales/zh/browser.js +0 -87
  97. package/dist/tests/src/lib/i18n/locales/zh/client.js +0 -66
  98. package/dist/tests/src/lib/i18n/locales/zh/errors.js +0 -28
  99. package/dist/tests/src/lib/i18n/locales/zh/fetcher.js +0 -71
  100. package/dist/tests/src/lib/i18n/locales/zh/index.js +0 -29
  101. package/dist/tests/src/lib/i18n/locales/zh/node.js +0 -51
  102. package/dist/tests/src/lib/i18n/locales/zh/prompts.js +0 -53
  103. package/dist/tests/src/lib/i18n/locales/zh/resources.js +0 -50
  104. package/dist/tests/src/lib/i18n/locales/zh/server.js +0 -57
  105. package/dist/tests/src/lib/i18n/locales/zh/tools.js +0 -28
  106. package/dist/tests/src/lib/i18n/logger.js +0 -114
  107. package/dist/tests/src/lib/logger.js +0 -181
  108. package/dist/tests/tests/test-i18n.js +0 -588
  109. package/dist/vitest.config.js +0 -29
@@ -0,0 +1,389 @@
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
+ /**
7
+ * 内容提取功能多格式测试脚本 (Content extraction feature multi-format test script)
8
+ * 测试不同格式和参数组合下的内容提取功能 (Test content extraction functionality under different formats and parameter combinations)
9
+ */
10
+ import { spawn } from 'child_process';
11
+ import path from 'path';
12
+ import { fileURLToPath } from 'url';
13
+ import fs from 'fs';
14
+ // 获取当前文件的目录路径 (Get the directory path of the current file)
15
+ const __filename = fileURLToPath(import.meta.url);
16
+ const __dirname = path.dirname(__filename);
17
+ // 获取项目根目录 (Get project root directory)
18
+ const rootDir = process.cwd();
19
+ // 测试URL - 不修改 (Test URL - Do not modify)
20
+ const TEST_URL = 'https://www.bbc.com/news/articles/ceqjd11l55wo';
21
+ // 测试方法列表 (Test method list)
22
+ const TEST_METHODS = [
23
+ 'fetch_html',
24
+ 'fetch_plaintext',
25
+ 'fetch_markdown',
26
+ 'fetch_txt',
27
+ 'fetch_json'
28
+ ];
29
+ // 测试参数组 (Test parameter groups)
30
+ const TEST_PARAMS = {
31
+ // 基础参数组 - 简化参数 (Base parameter group - Simplified parameters)
32
+ base: {
33
+ url: TEST_URL,
34
+ startCursor: 0,
35
+ extractContent: false
36
+ },
37
+ // 内容提取参数组 - 简化参数 (Content extraction parameter group - Simplified parameters)
38
+ extraction: {
39
+ url: TEST_URL,
40
+ startCursor: 0,
41
+ extractContent: true
42
+ },
43
+ // 无回退参数组 - 增加必要参数 (No fallback parameter group - Add necessary parameters)
44
+ noFallback: {
45
+ url: TEST_URL,
46
+ startCursor: 0,
47
+ extractContent: true,
48
+ fallbackToOriginal: false
49
+ }
50
+ };
51
+ // 创建存放测试结果的目录
52
+ const TEST_RESULTS_DIR = path.join(rootDir, '.test-results');
53
+ console.log(`[信息] 测试结果将保存到: ${TEST_RESULTS_DIR}`);
54
+ if (!fs.existsSync(TEST_RESULTS_DIR)) {
55
+ fs.mkdirSync(TEST_RESULTS_DIR);
56
+ }
57
+ /**
58
+ * 将参数对象转换为命令行参数数组 (Convert parameter object to command line argument array)
59
+ * @param params 参数对象 (Parameter object)
60
+ * @returns 命令行参数数组 (Command line argument array)
61
+ */
62
+ function paramsToCommandArgs(params) {
63
+ return Object.entries(params).map(([key, value]) => {
64
+ // 处理null和undefined值 (Handle null and undefined values)
65
+ if (value === null || value === undefined) {
66
+ return `--${key}=null`;
67
+ }
68
+ // 布尔值直接转换为字符串 (Convert boolean directly to string)
69
+ if (typeof value === 'boolean') {
70
+ return `--${key}=${value}`;
71
+ }
72
+ // 其他类型保持原样 (Keep other types as is)
73
+ return `--${key}=${value}`;
74
+ });
75
+ }
76
+ /**
77
+ * 运行测试 (Run test)
78
+ * @param method 测试方法 (Test method)
79
+ * @param groupName 参数组名称 (Parameter group name)
80
+ * @returns 测试结果对象 (Test result object)
81
+ */
82
+ async function runTest(method, groupName) {
83
+ const params = TEST_PARAMS[groupName];
84
+ console.log(`\n=== 内容提取功能测试 [${method}] [${groupName}] ===`);
85
+ console.log(`[URL] ${TEST_URL}`);
86
+ console.log(`[参数] ${JSON.stringify(params, null, 2)}`);
87
+ console.log();
88
+ return new Promise((resolve, reject) => {
89
+ // 使用shell方式运行命令,避免参数转义问题
90
+ const paramsJson = JSON.stringify(params);
91
+ const command = `node dist/src/client.js ${method} '${paramsJson}' --debug`;
92
+ console.log(`[调试] 执行命令: ${command}`);
93
+ // 使用shell选项启动子进程,传递整个命令字符串
94
+ const child = spawn(command, [], { shell: true });
95
+ let stdout = '';
96
+ let stderr = '';
97
+ // 收集输出 (Collect output)
98
+ child.stdout.on('data', (data) => {
99
+ stdout += data.toString();
100
+ });
101
+ child.stderr.on('data', (data) => {
102
+ stderr += data.toString();
103
+ // 不再将stderr输出视为错误,改为记录日志 (Don't treat stderr output as error, record as log instead)
104
+ console.log(`[日志] ${data.toString()}`);
105
+ });
106
+ // 处理完成 (Handle completion)
107
+ child.on('close', (code) => {
108
+ console.log(`[测试完成] 退出码: ${code}`);
109
+ try {
110
+ // 尝试从stdout中提取JSON对象
111
+ // 只取大括号开始到结束的部分
112
+ const jsonMatch = stdout.match(/\{[\s\S]*\}\s*$/);
113
+ if (!jsonMatch) {
114
+ // 如果没找到合适的JSON,记录输出并返回失败
115
+ console.error('[解析错误] 无法在输出中找到有效的JSON对象');
116
+ console.error('标准输出:');
117
+ console.error(stdout);
118
+ reject(new Error('无法在输出中找到有效的JSON对象'));
119
+ return;
120
+ }
121
+ const jsonString = jsonMatch[0];
122
+ const jsonData = JSON.parse(jsonString);
123
+ // 提取并处理响应内容
124
+ const content = jsonData && jsonData.content && jsonData.content[0] ? jsonData.content[0].text : '';
125
+ const contentLength = content ? content.length : 0;
126
+ // 保存测试结果
127
+ const testResult = {
128
+ method: method,
129
+ responseData: jsonData,
130
+ content: content,
131
+ contentLength: contentLength,
132
+ metadata: jsonData.metadata || null,
133
+ parameters: params,
134
+ groupName: groupName
135
+ };
136
+ // 保存结果到文件
137
+ const resultFile = path.join(TEST_RESULTS_DIR, `${method}-${groupName}-result.json`);
138
+ fs.writeFileSync(resultFile, JSON.stringify(testResult, null, 2));
139
+ // 显示结果摘要
140
+ if (!jsonData.isError) {
141
+ console.log(`[成功] 获取到内容 (总长度: ${contentLength} 字符)`);
142
+ if (jsonData.metadata) {
143
+ console.log(`[元数据] ${JSON.stringify(jsonData.metadata, null, 2)}`);
144
+ }
145
+ // 打印更简洁的内容预览,最多显示200个字符 (Print more concise content preview, up to 200 characters)
146
+ console.log('[内容预览]');
147
+ if (content) {
148
+ // 移除多余空格和换行,简化显示 (Remove excess whitespace and newlines for simplified display)
149
+ const previewText = content.replace(/\s+/g, ' ').trim();
150
+ console.log(previewText.substring(0, Math.min(200, previewText.length)) + '...');
151
+ }
152
+ else {
153
+ console.log('(空内容)');
154
+ }
155
+ resolve(testResult);
156
+ }
157
+ else {
158
+ // 使用响应中的错误消息 (Use error message from response)
159
+ const errorMessage = jsonData.content && jsonData.content[0] ? jsonData.content[0].text : '未知错误';
160
+ console.error(`[错误] ${errorMessage}`);
161
+ // 对于非回退测试的错误,我们也保存结果以便分析
162
+ if (groupName === 'noFallback') {
163
+ console.log('[预期错误] 由于禁用回退,预期会出现错误');
164
+ resolve(testResult);
165
+ }
166
+ else {
167
+ reject(new Error(errorMessage));
168
+ }
169
+ }
170
+ }
171
+ catch (err) {
172
+ console.error(`[解析错误] ${err.message}`);
173
+ console.error('标准输出:');
174
+ console.error(stdout);
175
+ reject(err);
176
+ }
177
+ });
178
+ });
179
+ }
180
+ /**
181
+ * 比较测试结果 (Compare test results)
182
+ * @param method 测试方法 (Test method)
183
+ * @param baseResult 基础参数测试结果 (Base parameter test result)
184
+ * @param extractionResult 内容提取参数测试结果 (Content extraction parameter test result)
185
+ */
186
+ function compareResults(method, baseResult, extractionResult) {
187
+ console.log(`\n====== ${method} 参数效果对比结果 ======`);
188
+ const baseContent = baseResult.content;
189
+ const extractionContent = extractionResult.content;
190
+ // 比较文本长度
191
+ console.log(`[基础参数] 内容长度: ${baseResult.contentLength} 字符`);
192
+ console.log(`[提取参数] 内容长度: ${extractionResult.contentLength} 字符`);
193
+ const lengthDiff = extractionResult.contentLength - baseResult.contentLength;
194
+ const percentDiff = baseResult.contentLength > 0 ?
195
+ ((Math.abs(lengthDiff) / baseResult.contentLength) * 100).toFixed(2) : '0.00';
196
+ if (lengthDiff > 0) {
197
+ console.log(`[对比结果] 内容提取后长度增加了 ${lengthDiff} 字符 (+${percentDiff}%)`);
198
+ }
199
+ else if (lengthDiff < 0) {
200
+ console.log(`[对比结果] 内容提取后长度减少了 ${Math.abs(lengthDiff)} 字符 (-${percentDiff}%)`);
201
+ }
202
+ else {
203
+ console.log(`[对比结果] 内容长度没有变化`);
204
+ }
205
+ // 比较内容特征
206
+ console.log("\n[内容特征对比]");
207
+ const baseHasHtml = /<[^>]*>/.test(baseContent);
208
+ const extractionHasHtml = /<[^>]*>/.test(extractionContent);
209
+ console.log(`- 基础参数内容${baseHasHtml ? '包含' : '不包含'} HTML 标签`);
210
+ console.log(`- 提取参数内容${extractionHasHtml ? '包含' : '不包含'} HTML 标签`);
211
+ // 检查清晰度改进
212
+ const urlCount = (content) => {
213
+ const urlRegex = /(https?:\/\/[^\s]+)/g;
214
+ return (content.match(urlRegex) || []).length;
215
+ };
216
+ const baseUrlCount = urlCount(baseContent);
217
+ const extractionUrlCount = urlCount(extractionContent);
218
+ console.log(`- 基础参数内容包含 ${baseUrlCount} 个URL链接`);
219
+ console.log(`- 提取参数内容包含 ${extractionUrlCount} 个URL链接`);
220
+ if (extractionResult.metadata) {
221
+ console.log("\n[元数据]");
222
+ console.log(JSON.stringify(extractionResult.metadata, null, 2));
223
+ }
224
+ // 内容预览对比
225
+ if (baseResult.contentLength > 0 && extractionResult.contentLength > 0) {
226
+ console.log("\n[内容预览对比]");
227
+ console.log("基础参数内容开头:");
228
+ console.log(baseContent.substring(0, Math.min(200, baseContent.length)) + "...");
229
+ console.log("\n提取参数内容开头:");
230
+ console.log(extractionContent.substring(0, Math.min(200, extractionContent.length)) + "...");
231
+ }
232
+ // 对比后输出总结
233
+ console.log("\n[总体评估]");
234
+ if (extractionHasHtml && !baseHasHtml) {
235
+ console.log("- 内容提取优化了HTML标签处理,保留了更多格式信息");
236
+ }
237
+ else if (!extractionHasHtml && baseHasHtml) {
238
+ console.log("- 内容提取移除了HTML标签,简化了内容");
239
+ }
240
+ if (extractionUrlCount < baseUrlCount) {
241
+ console.log("- 内容提取减少了URL链接,提供了更简洁的阅读体验");
242
+ }
243
+ else if (extractionUrlCount > baseUrlCount) {
244
+ console.log("- 内容提取保留了更多URL链接,信息更完整");
245
+ }
246
+ if (extractionResult.metadata) {
247
+ console.log("- 内容提取成功获取了元数据,提供了额外的页面信息");
248
+ }
249
+ const effectivenessSummary = lengthDiff !== 0 ||
250
+ baseHasHtml !== extractionHasHtml ||
251
+ baseUrlCount !== extractionUrlCount ||
252
+ extractionResult.metadata ?
253
+ "内容提取功能对此网页有明显效果" :
254
+ "内容提取功能对此网页没有明显效果";
255
+ console.log(`- 整体评估: ${effectivenessSummary}`);
256
+ console.log("\n====== 对比结束 ======");
257
+ // 保存比较结果
258
+ const comparisonResult = {
259
+ method: method,
260
+ baseResult: baseResult,
261
+ extractionResult: extractionResult,
262
+ comparison: {
263
+ lengthDiff: lengthDiff,
264
+ percentDiff: percentDiff,
265
+ baseHasHtml: baseHasHtml,
266
+ extractionHasHtml: extractionHasHtml,
267
+ baseUrlCount: baseUrlCount,
268
+ extractionUrlCount: extractionUrlCount,
269
+ effectivenessSummary: effectivenessSummary
270
+ }
271
+ };
272
+ const comparisonFile = path.join(TEST_RESULTS_DIR, `${method}-comparison-result.json`);
273
+ fs.writeFileSync(comparisonFile, JSON.stringify(comparisonResult, null, 2));
274
+ }
275
+ /**
276
+ * 生成测试结果摘要 (Generate test result summary)
277
+ * @param results 所有测试结果 (All test results)
278
+ */
279
+ function generateSummary(results) {
280
+ const summaryFile = path.join(TEST_RESULTS_DIR, 'test-summary.json');
281
+ fs.writeFileSync(summaryFile, JSON.stringify(results, null, 2));
282
+ console.log("\n====== 测试摘要 ======");
283
+ console.log(`测试URL: ${TEST_URL}`);
284
+ console.log(`测试方法: ${TEST_METHODS.join(', ')}`);
285
+ console.log(`参数组: ${Object.keys(TEST_PARAMS).join(', ')}`);
286
+ // 显示各个方法的内容长度对比
287
+ console.log("\n[各格式内容长度对比]");
288
+ const table = {};
289
+ for (const method of TEST_METHODS) {
290
+ table[method] = {};
291
+ for (const group of Object.keys(TEST_PARAMS)) {
292
+ if (results[method] && results[method][group]) {
293
+ table[method][group] = results[method][group].contentLength;
294
+ }
295
+ else {
296
+ table[method][group] = '未测试或失败';
297
+ }
298
+ }
299
+ }
300
+ // 格式化为表格输出
301
+ console.log("方法 | " + Object.keys(TEST_PARAMS).join(' | '));
302
+ console.log("-".repeat(50));
303
+ for (const method of TEST_METHODS) {
304
+ let row = `${method} | `;
305
+ for (const group of Object.keys(TEST_PARAMS)) {
306
+ row += `${table[method][group]} | `;
307
+ }
308
+ console.log(row);
309
+ }
310
+ console.log("\n[元数据支持情况]");
311
+ for (const method of TEST_METHODS) {
312
+ if (results[method] && results[method].extraction && results[method].extraction.metadata) {
313
+ console.log(`- ${method}: 支持元数据`);
314
+ }
315
+ else {
316
+ console.log(`- ${method}: 不支持元数据或测试失败`);
317
+ }
318
+ }
319
+ console.log("\n[测试结论]");
320
+ console.log("1. 各参数对内容提取的影响:");
321
+ console.log(" - extractContent: 决定是否进行内容提取处理");
322
+ console.log(" - includeMetadata: 决定是否在结果中包含元数据");
323
+ console.log(" - fallbackToOriginal: 决定内容提取失败时是否回退到原始内容");
324
+ console.log("\n2. 不同格式工具的特点:");
325
+ console.log(" - fetch_html: 提供完整HTML结构");
326
+ console.log(" - fetch_markdown: 提供Markdown格式内容,适合进一步处理");
327
+ console.log(" - fetch_plaintext: 提供纯文本内容,无HTML标签");
328
+ console.log(" - fetch_txt: 类似plaintext,但可能有不同处理");
329
+ console.log(" - fetch_json: 用于JSON数据,不适用于一般网页内容提取");
330
+ console.log("\n3. 内容提取效果最佳的格式:");
331
+ let bestMethod = TEST_METHODS[0];
332
+ let maxDiff = 0;
333
+ for (const method of TEST_METHODS) {
334
+ if (results[method] &&
335
+ results[method].base &&
336
+ results[method].extraction) {
337
+ const diff = Math.abs(results[method].extraction.contentLength - results[method].base.contentLength);
338
+ if (diff > maxDiff) {
339
+ maxDiff = diff;
340
+ bestMethod = method;
341
+ }
342
+ }
343
+ }
344
+ console.log(` - ${bestMethod}格式表现最佳,内容提取效果最明显`);
345
+ console.log("\n====== 摘要结束 ======");
346
+ console.log(`\n[结果保存] 测试结果已保存至 ${TEST_RESULTS_DIR} 目录`);
347
+ }
348
+ /**
349
+ * 运行所有测试 (Run all tests)
350
+ */
351
+ async function runAllTests() {
352
+ const results = {};
353
+ try {
354
+ // 轮询每个测试方法
355
+ for (const method of TEST_METHODS) {
356
+ console.log(`\n\n======= 开始测试 ${method} =======\n`);
357
+ results[method] = {};
358
+ // 测试各个参数组
359
+ for (const group of Object.keys(TEST_PARAMS)) {
360
+ try {
361
+ console.log(`\n--- 测试 ${method} 使用 ${group} 参数 ---`);
362
+ const result = await runTest(method, group);
363
+ results[method][group] = result;
364
+ }
365
+ catch (err) {
366
+ console.error(`测试 ${method} 使用 ${group} 参数失败: ${err.message}`);
367
+ // 继续测试其他参数组
368
+ }
369
+ }
370
+ // 如果基础和提取参数都测试成功,进行比较
371
+ if (results[method].base && results[method].extraction) {
372
+ compareResults(method, results[method].base, results[method].extraction);
373
+ }
374
+ }
375
+ // 生成测试摘要
376
+ generateSummary(results);
377
+ console.log("\n[测试完成] 所有测试已完成");
378
+ }
379
+ catch (err) {
380
+ console.error(`\n[测试失败] ${err.message}`);
381
+ // 即使有失败,也生成部分摘要
382
+ if (Object.keys(results).length > 0) {
383
+ generateSummary(results);
384
+ }
385
+ process.exit(1);
386
+ }
387
+ }
388
+ // 执行测试 (Execute tests)
389
+ runAllTests();
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -0,0 +1,173 @@
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, test, expect, vi, beforeEach } from 'vitest';
7
+ import { ContentExtractor } from '../../src/lib/utils/ContentExtractor.js';
8
+ import { JSDOM } from 'jsdom';
9
+ import { Readability } from '@mozilla/readability';
10
+ // 模拟logger (Mock logger)
11
+ vi.mock('../../src/lib/logger.js', () => ({
12
+ log: vi.fn(),
13
+ COMPONENTS: {
14
+ EXTRACTOR: 'CONTENT-EXTRACTOR'
15
+ }
16
+ }));
17
+ // 模拟JSDOM (Mock JSDOM)
18
+ vi.mock('jsdom', () => {
19
+ const mockDocument = {
20
+ querySelector: vi.fn(),
21
+ querySelectorAll: vi.fn(),
22
+ location: { href: 'https://example.com/article' },
23
+ title: 'Test Article'
24
+ };
25
+ return {
26
+ JSDOM: vi.fn().mockImplementation(() => ({
27
+ window: {
28
+ document: mockDocument
29
+ }
30
+ }))
31
+ };
32
+ });
33
+ // 模拟Readability (Mock Readability)
34
+ const mockParse = vi.fn();
35
+ vi.mock('@mozilla/readability', () => ({
36
+ Readability: vi.fn().mockImplementation(() => ({
37
+ parse: mockParse
38
+ }))
39
+ }));
40
+ describe('ContentExtractor 测试 (ContentExtractor Tests)', () => {
41
+ beforeEach(() => {
42
+ // 清理mocks (Clear mocks)
43
+ vi.clearAllMocks();
44
+ });
45
+ test('extractContent应正确提取页面内容 (extractContent should correctly extract page content)', () => {
46
+ // 设置模拟返回值 (Set mock return values)
47
+ const mockArticle = {
48
+ title: 'Test Article Title',
49
+ content: '<div><p>This is the article content.</p></div>',
50
+ textContent: 'This is the article content.',
51
+ excerpt: 'Article excerpt',
52
+ byline: 'Author Name',
53
+ siteName: 'Example Site',
54
+ length: 100,
55
+ dir: 'ltr',
56
+ lang: 'en',
57
+ publishedTime: '2023-01-01T00:00:00Z'
58
+ };
59
+ // 设置Readability的parse方法返回模拟文章 (Set Readability's parse method to return mock article)
60
+ mockParse.mockReturnValue(mockArticle);
61
+ // 设置isProbablyReaderable返回true (Set isProbablyReaderable to return true)
62
+ const originalIsProbablyReaderable = ContentExtractor['isProbablyReaderable'];
63
+ ContentExtractor['isProbablyReaderable'] = vi.fn().mockReturnValue(true);
64
+ // 调用测试方法 (Call test method)
65
+ const result = ContentExtractor.extractContent('<html><body><article><p>Test content</p></article></body></html>', 'https://example.com/article', true);
66
+ // 恢复原始方法 (Restore original method)
67
+ ContentExtractor['isProbablyReaderable'] = originalIsProbablyReaderable;
68
+ // 验证结果 (Verify results)
69
+ expect(result).toEqual({
70
+ title: 'Test Article Title',
71
+ content: '<div><p>This is the article content.</p></div>',
72
+ textContent: 'This is the article content.',
73
+ excerpt: 'Article excerpt',
74
+ byline: 'Author Name',
75
+ siteName: 'Example Site',
76
+ length: 100,
77
+ isReaderable: true
78
+ });
79
+ // 验证JSDOM和Readability被正确调用 (Verify JSDOM and Readability were correctly called)
80
+ expect(JSDOM).toHaveBeenCalledWith('<html><body><article><p>Test content</p></article></body></html>', { url: 'https://example.com/article' });
81
+ expect(Readability).toHaveBeenCalled();
82
+ expect(mockParse).toHaveBeenCalled();
83
+ });
84
+ test('extractContent应在无法提取内容时返回空结果 (extractContent should return empty result when content cannot be extracted)', () => {
85
+ // 设置Readability的parse方法返回null (Set Readability's parse method to return null)
86
+ mockParse.mockReturnValue(null);
87
+ // 设置isProbablyReaderable返回false (Set isProbablyReaderable to return false)
88
+ const originalIsProbablyReaderable = ContentExtractor['isProbablyReaderable'];
89
+ ContentExtractor['isProbablyReaderable'] = vi.fn().mockReturnValue(false);
90
+ // 调用测试方法 (Call test method)
91
+ const result = ContentExtractor.extractContent('<html><body><div>Test content</div></body></html>', 'https://example.com/page', true);
92
+ // 恢复原始方法 (Restore original method)
93
+ ContentExtractor['isProbablyReaderable'] = originalIsProbablyReaderable;
94
+ // 验证结果 (Verify results)
95
+ expect(result).toEqual({
96
+ title: null,
97
+ content: null,
98
+ textContent: null,
99
+ excerpt: null,
100
+ byline: null,
101
+ siteName: null,
102
+ length: 0,
103
+ isReaderable: false
104
+ });
105
+ });
106
+ test('extractContent应处理提取过程中的错误 (extractContent should handle errors during extraction)', () => {
107
+ // 设置JSDOM抛出错误 (Set JSDOM to throw an error)
108
+ vi.mocked(JSDOM).mockImplementationOnce(() => {
109
+ throw new Error('JSDOM error');
110
+ });
111
+ // 调用测试方法 (Call test method)
112
+ const result = ContentExtractor.extractContent('<html><body><div>Test content</div></body></html>', 'https://example.com/page', true);
113
+ // 验证结果 (Verify results)
114
+ expect(result).toEqual({
115
+ title: null,
116
+ content: null,
117
+ textContent: null,
118
+ excerpt: null,
119
+ byline: null,
120
+ siteName: null,
121
+ length: 0,
122
+ isReaderable: false
123
+ });
124
+ });
125
+ test('isProbablyReaderable应检测特定类型的页面 (isProbablyReaderable should detect specific types of pages)', () => {
126
+ // 创建测试用的document对象 (Create document object for testing)
127
+ const mockDocument = {
128
+ location: { href: 'https://example.com/article' },
129
+ title: 'Test Article',
130
+ querySelector: vi.fn(),
131
+ querySelectorAll: vi.fn()
132
+ };
133
+ // 测试1: 页面包含不太可能是文章的URL特征 (Test 1: Page contains URL patterns unlikely to be articles)
134
+ const loginPageDoc = {
135
+ ...mockDocument,
136
+ location: { href: 'https://example.com/login' },
137
+ title: 'Login Page'
138
+ };
139
+ expect(ContentExtractor['isProbablyReaderable'](loginPageDoc, true)).toBe(false);
140
+ // 测试2: 页面包含文章元素 (Test 2: Page contains article elements)
141
+ const articlePageDoc = {
142
+ ...mockDocument,
143
+ querySelector: vi.fn().mockImplementation((selector) => {
144
+ if (selector === 'article')
145
+ return { tagName: 'ARTICLE' };
146
+ return null;
147
+ })
148
+ };
149
+ expect(ContentExtractor['isProbablyReaderable'](articlePageDoc, true)).toBe(true);
150
+ // 测试3: 页面包含足够的段落 (Test 3: Page contains enough paragraphs)
151
+ const paragraphsPageDoc = {
152
+ ...mockDocument,
153
+ querySelector: vi.fn().mockReturnValue(null),
154
+ querySelectorAll: vi.fn().mockImplementation((selector) => {
155
+ if (selector === 'p')
156
+ return Array(6).fill({ tagName: 'P' });
157
+ return [];
158
+ })
159
+ };
160
+ expect(ContentExtractor['isProbablyReaderable'](paragraphsPageDoc, true)).toBe(true);
161
+ // 测试4: 页面内容不足 (Test 4: Not enough content on page)
162
+ const insufficientContentDoc = {
163
+ ...mockDocument,
164
+ querySelector: vi.fn().mockReturnValue(null),
165
+ querySelectorAll: vi.fn().mockImplementation((selector) => {
166
+ if (selector === 'p')
167
+ return Array(2).fill({ tagName: 'P' });
168
+ return [];
169
+ })
170
+ };
171
+ expect(ContentExtractor['isProbablyReaderable'](insufficientContentDoc, true)).toBe(false);
172
+ });
173
+ });