@lmcc-dev/mult-fetch-mcp-server 1.2.1 → 1.3.1
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.
- package/README.md +80 -29
- package/README.zh.md +80 -29
- package/dist/i18n-test-report.json +2 -2
- package/dist/i18n-unused-keys-report.json +4 -4
- package/dist/src/client.js +522 -131
- package/dist/src/index.js +0 -0
- package/dist/src/lib/fetch.js +8 -0
- package/dist/src/lib/fetchers/browser/BrowserFetcher.js +139 -199
- package/dist/src/lib/fetchers/common/BaseFetcher.js +203 -0
- package/dist/src/lib/fetchers/common/types.js +4 -0
- package/dist/src/lib/fetchers/common/utils.js +1 -1
- package/dist/src/lib/fetchers/index.js +11 -0
- package/dist/src/lib/fetchers/node/NodeFetcher.js +91 -66
- package/dist/src/lib/i18n/keys/browser.js +40 -2
- package/dist/src/lib/i18n/keys/chunkManager.js +43 -0
- package/dist/src/lib/i18n/keys/client.js +53 -1
- package/dist/src/lib/i18n/keys/contentSize.js +20 -0
- package/dist/src/lib/i18n/keys/fetcher.js +88 -79
- package/dist/src/lib/i18n/keys/index.js +4 -0
- package/dist/src/lib/i18n/keys/node.js +7 -1
- package/dist/src/lib/i18n/keys/processor.js +30 -0
- package/dist/src/lib/i18n/keys/tools.js +5 -1
- package/dist/src/lib/i18n/keys/url.js +45 -0
- package/dist/src/lib/i18n/keys.js +25 -0
- package/dist/src/lib/i18n/locales/en/browser.js +52 -14
- package/dist/src/lib/i18n/locales/en/chunkManager.js +41 -0
- package/dist/src/lib/i18n/locales/en/client.js +62 -9
- package/dist/src/lib/i18n/locales/en/contentSize.js +17 -0
- package/dist/src/lib/i18n/locales/en/fetcher.js +50 -30
- package/dist/src/lib/i18n/locales/en/index.js +9 -1
- package/dist/src/lib/i18n/locales/en/node.js +26 -20
- package/dist/src/lib/i18n/locales/en/processor.js +28 -0
- package/dist/src/lib/i18n/locales/en/tools.js +5 -1
- package/dist/src/lib/i18n/locales/en/url.js +40 -0
- package/dist/src/lib/i18n/locales/en.js +51 -0
- package/dist/src/lib/i18n/locales/zh/browser.js +85 -47
- package/dist/src/lib/i18n/locales/zh/chunkManager.js +41 -0
- package/dist/src/lib/i18n/locales/zh/client.js +53 -1
- package/dist/src/lib/i18n/locales/zh/contentSize.js +17 -0
- package/dist/src/lib/i18n/locales/zh/fetcher.js +67 -47
- package/dist/src/lib/i18n/locales/zh/index.js +9 -1
- package/dist/src/lib/i18n/locales/zh/node.js +50 -44
- package/dist/src/lib/i18n/locales/zh/processor.js +28 -0
- package/dist/src/lib/i18n/locales/zh/tools.js +5 -1
- package/dist/src/lib/i18n/locales/zh/url.js +40 -0
- package/dist/src/lib/i18n/locales/zh.js +51 -0
- package/dist/src/lib/logger.js +11 -2
- package/dist/src/lib/server/fetcher.js +15 -5
- package/dist/src/lib/server/tools.js +202 -52
- package/dist/src/lib/types.js +5 -0
- package/dist/src/lib/utils/ChunkManager.js +177 -0
- package/dist/src/lib/utils/ContentProcessor.js +134 -0
- package/dist/src/lib/utils/ContentSizeManager.js +123 -0
- package/dist/src/lib/utils/TemplateUtils.js +71 -0
- package/dist/src/lib/utils/errors.js +2 -8
- package/dist/src/mcp-server.js +0 -0
- package/dist/src/test-i18n.js +0 -0
- package/dist/tests/BrowserFetcher.test.js +0 -0
- package/dist/tests/NodeFetcher.test.js +0 -0
- package/dist/tests/client.test.js +0 -0
- package/dist/tests/fetch.test.js +0 -0
- package/dist/tests/fetchers/plain-text.test.js +146 -0
- package/dist/tests/i18n-missing-keys.js +0 -0
- package/dist/tests/i18n-remove-unused-keys.js +0 -0
- package/dist/tests/i18n-unused-keys.js +0 -0
- package/dist/tests/i18n.test.js +0 -0
- package/dist/tests/logger.test.js +0 -0
- package/dist/tests/mcp-server.test.js +0 -0
- package/dist/tests/server/fetcher.test.js +69 -15
- package/dist/tests/server/tools.test.js +165 -16
- package/dist/tests/setup.js +0 -0
- package/dist/tests/test-direct-client.js +113 -5
- package/dist/tests/test-i18n.js +0 -0
- package/dist/tests/test-mcp-methods.js +67 -1
- package/dist/tests/test-mcp.js +83 -3
- package/dist/tests/test-mini4k.js +90 -3
- package/dist/tests/types.test.js +0 -0
- package/dist/tests/utils/ChunkManager.test.js +163 -0
- package/dist/tests/utils/ContentSizeManager.test.js +170 -0
- package/dist/vitest.config.js +0 -0
- package/package.json +28 -23
|
@@ -0,0 +1,134 @@
|
|
|
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 TurndownService from 'turndown';
|
|
7
|
+
import { log, COMPONENTS } from '../logger.js';
|
|
8
|
+
import { ToolError, ErrorType } from '../utils/errors.js';
|
|
9
|
+
import { convert as htmlToText } from 'html-to-text';
|
|
10
|
+
/**
|
|
11
|
+
* 内容处理器类 (Content processor class)
|
|
12
|
+
* 处理不同格式的内容转换和处理 (Process and convert content in different formats)
|
|
13
|
+
*/
|
|
14
|
+
export class ContentProcessor {
|
|
15
|
+
/**
|
|
16
|
+
* 将HTML转换为Markdown (Convert HTML to Markdown)
|
|
17
|
+
* @param html HTML内容 (HTML content)
|
|
18
|
+
* @param debug 是否开启调试 (Whether to enable debugging)
|
|
19
|
+
* @param component 组件名称 (Component name for logging) - 仅用于向下兼容,实际会使用PROCESSOR组件
|
|
20
|
+
* @returns Markdown内容 (Markdown content)
|
|
21
|
+
*/
|
|
22
|
+
static htmlToMarkdown(html, debug, component) {
|
|
23
|
+
// 始终使用COMPONENTS.PROCESSOR作为组件标识符 (Always use COMPONENTS.PROCESSOR as component identifier)
|
|
24
|
+
log('processor.creatingTurndown', debug, {}, COMPONENTS.PROCESSOR);
|
|
25
|
+
const turndownService = new TurndownService({
|
|
26
|
+
headingStyle: 'atx',
|
|
27
|
+
codeBlockStyle: 'fenced',
|
|
28
|
+
bulletListMarker: '-'
|
|
29
|
+
});
|
|
30
|
+
// 添加表格支持 (Add table support)
|
|
31
|
+
turndownService.addRule('tables', {
|
|
32
|
+
filter: ['table'],
|
|
33
|
+
replacement: function (content) {
|
|
34
|
+
const tableContent = content.trim();
|
|
35
|
+
return '\n\n' + tableContent + '\n\n';
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
// 将HTML转换为Markdown (Convert HTML to Markdown)
|
|
39
|
+
log('processor.convertingToMarkdown', debug, {}, COMPONENTS.PROCESSOR);
|
|
40
|
+
const markdown = turndownService.turndown(html);
|
|
41
|
+
log('processor.markdownContentLength', debug, { length: markdown.length }, COMPONENTS.PROCESSOR);
|
|
42
|
+
return markdown;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* 将HTML转换为纯文本 (Convert HTML to plain text)
|
|
46
|
+
* @param html HTML内容 (HTML content)
|
|
47
|
+
* @param debug 是否开启调试 (Whether to enable debugging)
|
|
48
|
+
* @param component 组件名称 (Component name for logging) - 仅用于向下兼容,实际会使用PROCESSOR组件
|
|
49
|
+
* @returns 纯文本内容 (Plain text content)
|
|
50
|
+
*/
|
|
51
|
+
static htmlToText(html, debug, component) {
|
|
52
|
+
// 始终使用COMPONENTS.PROCESSOR作为组件标识符 (Always use COMPONENTS.PROCESSOR as component identifier)
|
|
53
|
+
log('processor.creatingHtmlToText', debug, {}, COMPONENTS.PROCESSOR);
|
|
54
|
+
// 配置html-to-text选项 (Configure html-to-text options)
|
|
55
|
+
const options = {
|
|
56
|
+
wordwrap: false,
|
|
57
|
+
selectors: [
|
|
58
|
+
{ selector: 'a', options: { hideLinkHrefIfSameAsText: true } },
|
|
59
|
+
{ selector: 'img', format: 'skip' },
|
|
60
|
+
{ selector: 'table', options: { uppercaseHeaderCells: false } }
|
|
61
|
+
]
|
|
62
|
+
};
|
|
63
|
+
// 将HTML转换为纯文本 (Convert HTML to plain text)
|
|
64
|
+
log('processor.convertingToText', debug, {}, COMPONENTS.PROCESSOR);
|
|
65
|
+
const text = htmlToText(html, options);
|
|
66
|
+
log('processor.textContentLength', debug, { length: text.length }, COMPONENTS.PROCESSOR);
|
|
67
|
+
return text;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* 解析JSON字符串 (Parse JSON string)
|
|
71
|
+
* @param text JSON字符串 (JSON string)
|
|
72
|
+
* @param debug 是否开启调试 (Whether to enable debugging)
|
|
73
|
+
* @param component 组件名称 (Component name for logging) - 仅用于向下兼容,实际会使用PROCESSOR组件
|
|
74
|
+
* @returns 解析结果 (Parse result - success or error with message)
|
|
75
|
+
*/
|
|
76
|
+
static parseJson(text, debug, component) {
|
|
77
|
+
log('processor.parsingJson', debug, {}, COMPONENTS.PROCESSOR);
|
|
78
|
+
try {
|
|
79
|
+
const parsed = JSON.parse(text);
|
|
80
|
+
log('processor.jsonParsed', debug, {}, COMPONENTS.PROCESSOR);
|
|
81
|
+
return { success: true, result: parsed };
|
|
82
|
+
}
|
|
83
|
+
catch (parseError) {
|
|
84
|
+
const textPreview = text.length > 100 ? `${text.substring(0, 100)}...` : text;
|
|
85
|
+
const errorMessage = `Invalid JSON: ${parseError instanceof Error ? parseError.message : String(parseError)}. Text preview: "${textPreview}", length: ${text.length}`;
|
|
86
|
+
log('processor.jsonParseError', debug, {
|
|
87
|
+
error: String(parseError),
|
|
88
|
+
textPreview,
|
|
89
|
+
textLength: text.length
|
|
90
|
+
}, COMPONENTS.PROCESSOR);
|
|
91
|
+
return { success: false, error: errorMessage };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* 处理文本内容,确保是UTF-8编码 (Process text content, ensure it's UTF-8 encoded)
|
|
96
|
+
* @param text 文本内容 (Text content)
|
|
97
|
+
* @param debug 是否开启调试 (Whether to enable debugging)
|
|
98
|
+
* @param component 组件名称 (Component name for logging) - 仅用于向下兼容,实际会使用PROCESSOR组件
|
|
99
|
+
* @returns 处理后的文本 (Processed text)
|
|
100
|
+
*/
|
|
101
|
+
static processTextContent(text, debug, component) {
|
|
102
|
+
log('processor.processingText', debug, { length: text.length }, COMPONENTS.PROCESSOR);
|
|
103
|
+
// 这里可以添加文本处理逻辑,如编码转换、去除特殊字符等
|
|
104
|
+
// (Add text processing logic here, such as encoding conversion, removing special characters, etc.)
|
|
105
|
+
return text;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* 验证内容大小并处理过大的内容 (Validate content size and handle oversized content)
|
|
109
|
+
* @param content 内容 (Content)
|
|
110
|
+
* @param contentSizeLimit 内容大小限制 (Content size limit)
|
|
111
|
+
* @param debug 是否开启调试 (Whether to enable debugging)
|
|
112
|
+
* @param component 组件名称 (Component name for logging) - 仅用于向下兼容,实际会使用PROCESSOR组件
|
|
113
|
+
* @throws {ToolError} 如果内容太大且不允许分割 (If content is too large and splitting is not allowed)
|
|
114
|
+
*/
|
|
115
|
+
static validateContentSize(content, contentSizeLimit, debug, component) {
|
|
116
|
+
const contentSize = content.length;
|
|
117
|
+
if (contentSize > contentSizeLimit) {
|
|
118
|
+
log('processor.contentTooLarge', debug, {
|
|
119
|
+
contentSize,
|
|
120
|
+
contentSizeLimit
|
|
121
|
+
}, COMPONENTS.PROCESSOR);
|
|
122
|
+
throw new ToolError(`Content size (${contentSize}) exceeds the allowed limit (${contentSizeLimit})`, ErrorType.TOOL_EXECUTION_ERROR, COMPONENTS.PROCESSOR, {
|
|
123
|
+
contentSize,
|
|
124
|
+
contentSizeLimit
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
log('processor.contentSizeOk', debug, {
|
|
129
|
+
contentSize,
|
|
130
|
+
contentSizeLimit
|
|
131
|
+
}, COMPONENTS.PROCESSOR);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
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 { log, COMPONENTS } from '../logger.js';
|
|
7
|
+
import { TemplateUtils } from './TemplateUtils.js';
|
|
8
|
+
/**
|
|
9
|
+
* 内容大小管理器 (Content size manager)
|
|
10
|
+
* 用于处理内容大小限制,防止返回过大的内容 (Used to handle content size limits, prevent returning too large content)
|
|
11
|
+
*/
|
|
12
|
+
export class ContentSizeManager {
|
|
13
|
+
/**
|
|
14
|
+
* 默认大小限制,单位为字节 (Default size limit in bytes)
|
|
15
|
+
* 默认为50KB (Default is 50KB)
|
|
16
|
+
*/
|
|
17
|
+
static DEFAULT_SIZE_LIMIT = 50 * 1024; // 50KB
|
|
18
|
+
/**
|
|
19
|
+
* 获取默认大小限制 (Get default size limit)
|
|
20
|
+
* @returns 默认大小限制,单位为字节 (Default size limit in bytes)
|
|
21
|
+
*/
|
|
22
|
+
static getDefaultSizeLimit() {
|
|
23
|
+
return this.DEFAULT_SIZE_LIMIT;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* 检查内容大小是否超过限制 (Check if content size exceeds limit)
|
|
27
|
+
* @param content 内容 (Content)
|
|
28
|
+
* @param sizeLimit 大小限制,单位为字节 (Size limit in bytes)
|
|
29
|
+
* @param debug 是否启用调试模式 (Whether debug mode is enabled)
|
|
30
|
+
* @returns 是否超过限制 (Whether exceeds limit)
|
|
31
|
+
*/
|
|
32
|
+
static exceedsLimit(content, sizeLimit = this.DEFAULT_SIZE_LIMIT, debug = false) {
|
|
33
|
+
const contentSize = Buffer.byteLength(content, 'utf8');
|
|
34
|
+
const exceedsLimit = contentSize > sizeLimit;
|
|
35
|
+
if (debug) {
|
|
36
|
+
log('contentSize.checking', debug, {
|
|
37
|
+
contentSize: `${(contentSize / 1024).toFixed(2)}KB`,
|
|
38
|
+
limit: `${(sizeLimit / 1024).toFixed(2)}KB`,
|
|
39
|
+
exceedsLimit
|
|
40
|
+
}, COMPONENTS.CONTENT_SIZE);
|
|
41
|
+
}
|
|
42
|
+
return exceedsLimit;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* 将内容分割成多个片段,不添加分段信息 (Split content into multiple chunks without adding chunk information)
|
|
46
|
+
* 这个方法用于内部处理,返回原始分段 (This method is for internal processing, returns raw chunks)
|
|
47
|
+
* @param content 原始内容 (Original content)
|
|
48
|
+
* @param sizeLimit 每个片段的大小限制,单位为字节 (Size limit for each chunk in bytes)
|
|
49
|
+
* @param debug 是否启用调试模式 (Whether debug mode is enabled)
|
|
50
|
+
* @returns 内容片段数组 (Array of content chunks)
|
|
51
|
+
*/
|
|
52
|
+
static splitContentIntoRawChunks(content, sizeLimit = this.DEFAULT_SIZE_LIMIT, debug = false) {
|
|
53
|
+
if (!this.exceedsLimit(content, sizeLimit, debug)) {
|
|
54
|
+
return [content];
|
|
55
|
+
}
|
|
56
|
+
const contentSize = Buffer.byteLength(content, 'utf8');
|
|
57
|
+
log('contentSize.splitting', debug, {
|
|
58
|
+
originalSize: `${(contentSize / 1024).toFixed(2)}KB`,
|
|
59
|
+
chunkSize: `${(sizeLimit / 1024).toFixed(2)}KB`
|
|
60
|
+
}, COMPONENTS.CONTENT_SIZE);
|
|
61
|
+
// 分割内容 (Split content)
|
|
62
|
+
const chunks = [];
|
|
63
|
+
let currentChunk = '';
|
|
64
|
+
let currentSize = 0;
|
|
65
|
+
const chars = [...content];
|
|
66
|
+
for (const char of chars) {
|
|
67
|
+
const charSize = Buffer.byteLength(char, 'utf8');
|
|
68
|
+
// 如果添加这个字符会超过限制,创建新的片段 (If adding this character would exceed the limit, create a new chunk)
|
|
69
|
+
if (currentSize + charSize > sizeLimit && currentChunk.length > 0) {
|
|
70
|
+
chunks.push(currentChunk);
|
|
71
|
+
currentChunk = '';
|
|
72
|
+
currentSize = 0;
|
|
73
|
+
}
|
|
74
|
+
currentChunk += char;
|
|
75
|
+
currentSize += charSize;
|
|
76
|
+
}
|
|
77
|
+
// 添加最后一个片段 (Add the last chunk)
|
|
78
|
+
if (currentChunk.length > 0) {
|
|
79
|
+
chunks.push(currentChunk);
|
|
80
|
+
}
|
|
81
|
+
log('contentSize.splitComplete', debug, {
|
|
82
|
+
chunks: chunks.length,
|
|
83
|
+
avgChunkSize: `${(contentSize / chunks.length / 1024).toFixed(2)}KB`
|
|
84
|
+
}, COMPONENTS.CONTENT_SIZE);
|
|
85
|
+
return chunks;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* 将内容分割成多个片段,并添加分段信息 (Split content into multiple chunks and add chunk information)
|
|
89
|
+
* @param content 原始内容 (Original content)
|
|
90
|
+
* @param sizeLimit 每个片段的大小限制,单位为字节 (Size limit for each chunk in bytes)
|
|
91
|
+
* @param debug 是否启用调试模式 (Whether debug mode is enabled)
|
|
92
|
+
* @param offset 当前偏移量,用于确定是首次请求还是后续请求 (Current offset, used to determine if it's initial or subsequent request)
|
|
93
|
+
* @returns 内容片段数组 (Array of content chunks)
|
|
94
|
+
*/
|
|
95
|
+
static splitContentIntoChunks(content, sizeLimit = this.DEFAULT_SIZE_LIMIT, debug = false, offset = 0) {
|
|
96
|
+
// 计算内容总字节大小 (Calculate total size of content in bytes)
|
|
97
|
+
const totalBytes = Buffer.byteLength(content, 'utf8');
|
|
98
|
+
// 使用TemplateUtils中的常量和方法生成示例模板以计算大小
|
|
99
|
+
// (Use constants and methods from TemplateUtils to generate example templates for size calculation)
|
|
100
|
+
const sampleChunkId = 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx';
|
|
101
|
+
const samplePrompt = TemplateUtils.generateSizeBasedChunkPrompt(50000, // fetchedBytes
|
|
102
|
+
totalBytes, sampleChunkId, totalBytes - 50000, // remainingBytes
|
|
103
|
+
Math.ceil((totalBytes - 50000) / sizeLimit), // estimatedRequests
|
|
104
|
+
sizeLimit, true // isFirstRequest
|
|
105
|
+
);
|
|
106
|
+
const maxChunkInfoSize = Buffer.byteLength(samplePrompt, 'utf8');
|
|
107
|
+
// 计算实际可用的分段大小 (Calculate actual available chunk size)
|
|
108
|
+
const effectiveChunkSize = sizeLimit - maxChunkInfoSize;
|
|
109
|
+
// 分割内容 (Split content)
|
|
110
|
+
const rawChunks = this.splitContentIntoRawChunks(content, effectiveChunkSize, debug);
|
|
111
|
+
// 不需要添加分段信息,这将在加载时动态添加
|
|
112
|
+
// (No need to add chunk information, it will be added dynamically when loading)
|
|
113
|
+
log('contentSize.splitIntoChunks', debug, {
|
|
114
|
+
totalChunks: rawChunks.length,
|
|
115
|
+
chunkCount: rawChunks.length,
|
|
116
|
+
chunkSize: effectiveChunkSize,
|
|
117
|
+
totalBytes,
|
|
118
|
+
effectiveChunkSize,
|
|
119
|
+
sizeLimit
|
|
120
|
+
}, COMPONENTS.CONTENT_SIZE);
|
|
121
|
+
return { chunks: rawChunks, totalBytes };
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
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
|
+
* 模板工具类 (Template utilities class)
|
|
8
|
+
* 提供处理模板替换的通用方法 (Provides common methods for template replacement)
|
|
9
|
+
*/
|
|
10
|
+
export class TemplateUtils {
|
|
11
|
+
/**
|
|
12
|
+
* 替换内容中的占位符 (Replace placeholders in content)
|
|
13
|
+
* @param content 原始内容 (Original content)
|
|
14
|
+
* @param replacements 替换项 (Replacements)
|
|
15
|
+
* @returns 替换后的内容 (Content after replacement)
|
|
16
|
+
*/
|
|
17
|
+
static replaceTemplateVariables(content, replacements) {
|
|
18
|
+
let result = content;
|
|
19
|
+
// 遍历所有替换项 (Iterate through all replacements)
|
|
20
|
+
for (const [key, value] of Object.entries(replacements)) {
|
|
21
|
+
// 创建正则表达式 (Create regular expression)
|
|
22
|
+
const regex = new RegExp(`{{${key}}}`, 'g');
|
|
23
|
+
// 替换所有匹配项 (Replace all matches)
|
|
24
|
+
result = result.replace(regex, value);
|
|
25
|
+
}
|
|
26
|
+
return result;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* 系统提示相关的常量 (Constants related to system prompts)
|
|
30
|
+
*/
|
|
31
|
+
static SYSTEM_NOTE = {
|
|
32
|
+
START: "=== SYSTEM NOTE ===",
|
|
33
|
+
END: "==================="
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* 生成基于字节大小的分块内容提示文本 (Generate size-based chunk prompt text)
|
|
37
|
+
* @param fetchedBytes 已获取的字节数 (Bytes already fetched)
|
|
38
|
+
* @param totalBytes 总字节数 (Total bytes)
|
|
39
|
+
* @param chunkId 块ID (Chunk ID)
|
|
40
|
+
* @param remainingBytes 剩余字节数 (Remaining bytes)
|
|
41
|
+
* @param estimatedRequests 预计需要的请求次数 (Estimated number of requests needed)
|
|
42
|
+
* @param currentSizeLimit 当前大小限制 (Current size limit)
|
|
43
|
+
* @param isFirstRequest 是否为首次请求 (Whether it's the first request)
|
|
44
|
+
* @returns 格式化的提示文本 (Formatted prompt text)
|
|
45
|
+
*/
|
|
46
|
+
static generateSizeBasedChunkPrompt(fetchedBytes, totalBytes, chunkId, remainingBytes, estimatedRequests, currentSizeLimit, isFirstRequest = true) {
|
|
47
|
+
const prefix = isFirstRequest ? 'Content is too long and has been split. ' : '';
|
|
48
|
+
const fetchedPercent = Math.round((fetchedBytes / totalBytes) * 100);
|
|
49
|
+
return `\n\n${TemplateUtils.SYSTEM_NOTE.START}\n${prefix}You've retrieved ${fetchedBytes.toLocaleString()} bytes (${fetchedPercent}% of total ${totalBytes.toLocaleString()} bytes). ${remainingBytes.toLocaleString()} bytes remaining. With current contentSizeLimit=${currentSizeLimit.toLocaleString()}, approximately ${estimatedRequests} more requests needed to retrieve all content. To continue, use the same tool function with parameters chunkId="${chunkId}" and startCursor=${fetchedBytes}.\n${TemplateUtils.SYSTEM_NOTE.END}`;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* 生成最后一个分块的基于字节大小的提示信息 (Generate size-based prompt for the last chunk)
|
|
53
|
+
* @param fetchedBytes 已获取的字节数 (Bytes already fetched)
|
|
54
|
+
* @param totalBytes 总字节数 (Total bytes)
|
|
55
|
+
* @param isFirstRequest 是否为首次请求 (Whether it's the first request)
|
|
56
|
+
* @returns 分段提示信息 (Chunk prompt information)
|
|
57
|
+
*/
|
|
58
|
+
static generateSizeBasedLastChunkPrompt(fetchedBytes, totalBytes, isFirstRequest = true) {
|
|
59
|
+
const prefix = isFirstRequest ? 'Content is too long and has been split. ' : '';
|
|
60
|
+
const fetchedPercent = Math.round((fetchedBytes / totalBytes) * 100);
|
|
61
|
+
return `\n\n${TemplateUtils.SYSTEM_NOTE.START}\n${prefix}You've retrieved ${fetchedBytes.toLocaleString()} bytes (100% of total ${totalBytes.toLocaleString()} bytes).\nThis is the last part of the content.\n${TemplateUtils.SYSTEM_NOTE.END}`;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* 检查内容是否已包含系统提示 (Check if content already contains system prompt)
|
|
65
|
+
* @param content 要检查的内容 (Content to check)
|
|
66
|
+
* @returns 是否包含系统提示 (Whether it contains system prompt)
|
|
67
|
+
*/
|
|
68
|
+
static hasSystemPrompt(content) {
|
|
69
|
+
return content.includes(TemplateUtils.SYSTEM_NOTE.START) && content.includes(TemplateUtils.SYSTEM_NOTE.END);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { log, COMPONENTS } from '../logger.js';
|
|
7
7
|
import { isAccessDeniedError, isNetworkError } from './errorDetection.js';
|
|
8
|
+
import { BaseFetcher } from '../fetchers/common/BaseFetcher.js';
|
|
8
9
|
// 扩展组件常量 (Extend component constants)
|
|
9
10
|
const EXTENDED_COMPONENTS = {
|
|
10
11
|
...COMPONENTS,
|
|
@@ -90,14 +91,7 @@ export class FetchError extends Error {
|
|
|
90
91
|
* @returns 标准响应对象 (Standard response object)
|
|
91
92
|
*/
|
|
92
93
|
toResponse() {
|
|
93
|
-
return
|
|
94
|
-
isError: true,
|
|
95
|
-
content: [
|
|
96
|
-
{
|
|
97
|
-
text: this.message
|
|
98
|
-
}
|
|
99
|
-
]
|
|
100
|
-
};
|
|
94
|
+
return BaseFetcher.createErrorResponse(this.message);
|
|
101
95
|
}
|
|
102
96
|
}
|
|
103
97
|
/**
|
package/dist/src/mcp-server.js
CHANGED
|
File without changes
|
package/dist/src/test-i18n.js
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/dist/tests/fetch.test.js
CHANGED
|
File without changes
|
|
@@ -0,0 +1,146 @@
|
|
|
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 } from 'vitest';
|
|
7
|
+
import { fetchPlainText } from '../../src/lib/fetch.js';
|
|
8
|
+
import { Fetcher } from '../../src/lib/fetchers/index.js';
|
|
9
|
+
// Mock the Fetcher.plainText function
|
|
10
|
+
vi.mock('../../src/lib/fetchers/index.js', () => ({
|
|
11
|
+
Fetcher: {
|
|
12
|
+
plainText: vi.fn()
|
|
13
|
+
}
|
|
14
|
+
}));
|
|
15
|
+
describe('plain-text fetcher 测试 (plain-text fetcher tests)', () => {
|
|
16
|
+
test('应该正确获取并转换内容为纯文本 (should correctly fetch and convert content to plain text)', async () => {
|
|
17
|
+
// 模拟 Fetcher.plainText 的返回值
|
|
18
|
+
Fetcher.plainText.mockResolvedValue({
|
|
19
|
+
content: [{ type: 'text', text: '<div>Example content</div>' }],
|
|
20
|
+
isError: false
|
|
21
|
+
});
|
|
22
|
+
// 调用被测试的函数
|
|
23
|
+
const result = await fetchPlainText({ url: 'https://example.com', startCursor: 0 });
|
|
24
|
+
// 验证 Fetcher.plainText 被调用并传递了正确的参数
|
|
25
|
+
expect(Fetcher.plainText).toHaveBeenCalledWith({
|
|
26
|
+
url: 'https://example.com',
|
|
27
|
+
startCursor: 0
|
|
28
|
+
});
|
|
29
|
+
// 验证返回结果正确
|
|
30
|
+
expect(result).toHaveProperty('content');
|
|
31
|
+
expect(result).toHaveProperty('isError', false);
|
|
32
|
+
expect(result.content[0]).toHaveProperty('type', 'text');
|
|
33
|
+
expect(result.content[0]).toHaveProperty('text', '<div>Example content</div>');
|
|
34
|
+
});
|
|
35
|
+
test('应该正确处理大型内容并返回分块信息 (should handle large content and return chunk information)', async () => {
|
|
36
|
+
// 模拟返回值,包含分块信息
|
|
37
|
+
const chunkInfo = {
|
|
38
|
+
isChunked: true,
|
|
39
|
+
chunkId: 'test-chunk-id',
|
|
40
|
+
startCursor: 0,
|
|
41
|
+
totalBytes: 10000,
|
|
42
|
+
fetchedBytes: 1000,
|
|
43
|
+
remainingBytes: 9000,
|
|
44
|
+
isLastChunk: false
|
|
45
|
+
};
|
|
46
|
+
Fetcher.plainText.mockResolvedValue({
|
|
47
|
+
content: [{
|
|
48
|
+
type: 'text',
|
|
49
|
+
text: 'Large content example',
|
|
50
|
+
metadata: { chunkInfo }
|
|
51
|
+
}],
|
|
52
|
+
isError: false
|
|
53
|
+
});
|
|
54
|
+
// 调用被测试的函数
|
|
55
|
+
const result = await fetchPlainText({
|
|
56
|
+
url: 'https://example.com/large',
|
|
57
|
+
enableContentSplitting: true,
|
|
58
|
+
contentSizeLimit: 1000,
|
|
59
|
+
startCursor: 0
|
|
60
|
+
});
|
|
61
|
+
// 验证 Fetcher.plainText 被调用并传递了正确的参数
|
|
62
|
+
expect(Fetcher.plainText).toHaveBeenCalledWith({
|
|
63
|
+
url: 'https://example.com/large',
|
|
64
|
+
enableContentSplitting: true,
|
|
65
|
+
contentSizeLimit: 1000,
|
|
66
|
+
startCursor: 0
|
|
67
|
+
});
|
|
68
|
+
// 验证返回结果包含分块信息
|
|
69
|
+
expect(result).toHaveProperty('content');
|
|
70
|
+
expect(result).toHaveProperty('isError', false);
|
|
71
|
+
// 由于类型问题,使用toEqual代替单独的属性断言
|
|
72
|
+
expect(result.content[0]).toEqual({
|
|
73
|
+
type: 'text',
|
|
74
|
+
text: 'Large content example',
|
|
75
|
+
metadata: { chunkInfo }
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
test('应该正确处理后续分块请求 (should handle subsequent chunk requests)', async () => {
|
|
79
|
+
// 模拟返回值,包含后续分块信息
|
|
80
|
+
const chunkInfo = {
|
|
81
|
+
isChunked: true,
|
|
82
|
+
chunkId: 'test-chunk-id',
|
|
83
|
+
startCursor: 1000,
|
|
84
|
+
totalBytes: 10000,
|
|
85
|
+
fetchedBytes: 1000,
|
|
86
|
+
remainingBytes: 8000,
|
|
87
|
+
isLastChunk: false
|
|
88
|
+
};
|
|
89
|
+
Fetcher.plainText.mockResolvedValue({
|
|
90
|
+
content: [{
|
|
91
|
+
type: 'text',
|
|
92
|
+
text: 'Subsequent chunk content',
|
|
93
|
+
metadata: { chunkInfo }
|
|
94
|
+
}],
|
|
95
|
+
isError: false
|
|
96
|
+
});
|
|
97
|
+
// 调用被测试的函数,包含chunkId和startCursor
|
|
98
|
+
const result = await fetchPlainText({
|
|
99
|
+
url: 'https://example.com/large',
|
|
100
|
+
chunkId: 'test-chunk-id',
|
|
101
|
+
startCursor: 1000
|
|
102
|
+
});
|
|
103
|
+
// 验证 Fetcher.plainText 被调用并传递了正确的参数
|
|
104
|
+
expect(Fetcher.plainText).toHaveBeenCalledWith({
|
|
105
|
+
url: 'https://example.com/large',
|
|
106
|
+
chunkId: 'test-chunk-id',
|
|
107
|
+
startCursor: 1000
|
|
108
|
+
});
|
|
109
|
+
// 验证返回结果包含分块信息
|
|
110
|
+
expect(result).toHaveProperty('content');
|
|
111
|
+
expect(result).toHaveProperty('isError', false);
|
|
112
|
+
// 由于类型问题,使用toEqual代替单独的属性断言
|
|
113
|
+
expect(result.content[0]).toEqual({
|
|
114
|
+
type: 'text',
|
|
115
|
+
text: 'Subsequent chunk content',
|
|
116
|
+
metadata: { chunkInfo }
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
test('当URL无效时应抛出错误 (should throw error when URL is invalid)', async () => {
|
|
120
|
+
// 模拟 Fetcher.plainText 抛出错误
|
|
121
|
+
const errorMessage = 'Invalid URL or unable to fetch';
|
|
122
|
+
Fetcher.plainText.mockRejectedValue(new Error(errorMessage));
|
|
123
|
+
// 将 fetchPlainText 包装在 try/catch 中来获取生成的错误响应
|
|
124
|
+
let result;
|
|
125
|
+
try {
|
|
126
|
+
await fetchPlainText({ url: 'https://example.com/error', startCursor: 0 });
|
|
127
|
+
// 如果没有抛出错误,测试应该失败
|
|
128
|
+
expect(true).toBe(false);
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
// 处理错误,获取生成的错误响应
|
|
132
|
+
result = {
|
|
133
|
+
isError: true,
|
|
134
|
+
content: [{ type: 'text', text: errorMessage }]
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
// 验证 Fetcher.plainText 被调用并传递了正确的参数
|
|
138
|
+
expect(Fetcher.plainText).toHaveBeenCalledWith({
|
|
139
|
+
url: 'https://example.com/error',
|
|
140
|
+
startCursor: 0
|
|
141
|
+
});
|
|
142
|
+
// 验证返回结果包含错误信息
|
|
143
|
+
expect(result).toHaveProperty('isError', true);
|
|
144
|
+
expect(result.content[0]).toHaveProperty('text', errorMessage);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/dist/tests/i18n.test.js
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|