@lmcc-dev/mult-fetch-mcp-server 1.2.0 → 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 (82) 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 +528 -119
  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/browser.js +40 -8
  49. package/dist/src/lib/server/fetcher.js +96 -15
  50. package/dist/src/lib/server/tools.js +202 -52
  51. package/dist/src/lib/types.js +5 -0
  52. package/dist/src/lib/utils/ChunkManager.js +177 -0
  53. package/dist/src/lib/utils/ContentProcessor.js +134 -0
  54. package/dist/src/lib/utils/ContentSizeManager.js +123 -0
  55. package/dist/src/lib/utils/TemplateUtils.js +71 -0
  56. package/dist/src/lib/utils/errors.js +2 -8
  57. package/dist/src/mcp-server.js +0 -0
  58. package/dist/src/test-i18n.js +0 -0
  59. package/dist/tests/BrowserFetcher.test.js +0 -0
  60. package/dist/tests/NodeFetcher.test.js +0 -0
  61. package/dist/tests/client.test.js +0 -0
  62. package/dist/tests/fetch.test.js +0 -0
  63. package/dist/tests/fetchers/plain-text.test.js +146 -0
  64. package/dist/tests/i18n-missing-keys.js +0 -0
  65. package/dist/tests/i18n-remove-unused-keys.js +0 -0
  66. package/dist/tests/i18n-unused-keys.js +0 -0
  67. package/dist/tests/i18n.test.js +0 -0
  68. package/dist/tests/logger.test.js +0 -0
  69. package/dist/tests/mcp-server.test.js +0 -0
  70. package/dist/tests/server/fetcher.test.js +69 -15
  71. package/dist/tests/server/tools.test.js +165 -16
  72. package/dist/tests/setup.js +0 -0
  73. package/dist/tests/test-direct-client.js +113 -5
  74. package/dist/tests/test-i18n.js +0 -0
  75. package/dist/tests/test-mcp-methods.js +67 -1
  76. package/dist/tests/test-mcp.js +83 -3
  77. package/dist/tests/test-mini4k.js +90 -3
  78. package/dist/tests/types.test.js +0 -0
  79. package/dist/tests/utils/ChunkManager.test.js +163 -0
  80. package/dist/tests/utils/ContentSizeManager.test.js +170 -0
  81. package/dist/vitest.config.js +0 -0
  82. package/package.json +27 -23
@@ -0,0 +1,177 @@
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 { v4 as uuidv4 } from 'uuid';
8
+ /**
9
+ * 分段内容管理器 (Chunk content manager)
10
+ * 用于存储和管理分段内容 (Used to store and manage chunked content)
11
+ */
12
+ export class ChunkManager {
13
+ /**
14
+ * 存储分段内容的Map (Map to store chunked content)
15
+ * 键为分段ID,值为分段内容数组 (Key is chunk ID, value is array of chunk content)
16
+ */
17
+ static chunks = new Map();
18
+ /**
19
+ * 存储分段内容大小信息的Map (Map to store chunk size information)
20
+ * 键为分段ID,值为{totalBytes, fetchedBytes}对象 (Key is chunk ID, value is {totalBytes, fetchedBytes} object)
21
+ */
22
+ static sizeInfo = new Map();
23
+ /**
24
+ * 分段内容的过期时间(毫秒) (Expiration time for chunked content in milliseconds)
25
+ * 默认为10分钟 (Default is 10 minutes)
26
+ */
27
+ static EXPIRATION_TIME = 10 * 60 * 1000; // 10分钟
28
+ /**
29
+ * 分段内容的过期时间Map (Map to store expiration time for chunked content)
30
+ * 键为分段ID,值为过期时间戳 (Key is chunk ID, value is expiration timestamp)
31
+ */
32
+ static expirations = new Map();
33
+ /**
34
+ * 存储分段内容 (Store chunked content)
35
+ * @param chunks 分段内容数组 (Array of chunk content)
36
+ * @param totalBytes 原始内容总字节数 (Total bytes of original content)
37
+ * @param debug 是否启用调试模式 (Whether debug mode is enabled)
38
+ * @returns 分段ID (Chunk ID)
39
+ */
40
+ static storeChunks(chunks, totalBytes, debug = false) {
41
+ // 清理过期的分段内容 (Clean up expired chunks)
42
+ this.cleanupExpiredChunks(debug);
43
+ // 生成唯一的分段ID (Generate unique chunk ID)
44
+ const chunkId = uuidv4();
45
+ // 计算每个分块的字节大小 (Calculate byte size of each chunk)
46
+ const chunkSizes = chunks.map(chunk => Buffer.byteLength(chunk, 'utf8'));
47
+ // 存储分段内容 (Store chunked content)
48
+ this.chunks.set(chunkId, chunks);
49
+ // 存储分段大小信息 (Store chunk size information)
50
+ this.sizeInfo.set(chunkId, {
51
+ totalBytes,
52
+ fetchedBytes: chunkSizes
53
+ });
54
+ // 设置过期时间 (Set expiration time)
55
+ const expirationTime = Date.now() + this.EXPIRATION_TIME;
56
+ this.expirations.set(chunkId, expirationTime);
57
+ log('chunkManager.storedChunks', debug, {
58
+ chunkId,
59
+ count: chunks.length,
60
+ totalChunks: chunks.length,
61
+ totalBytes,
62
+ expiresAt: new Date(expirationTime).toISOString()
63
+ }, COMPONENTS.CHUNK_MANAGER);
64
+ return chunkId;
65
+ }
66
+ /**
67
+ * 获取分段内容 (Get chunked content)
68
+ * @param chunkId 分段ID (Chunk ID)
69
+ * @param startCursor 开始游标位置,指示从哪个字节开始获取 (Start cursor position, indicating from which byte to start fetching)
70
+ * @param sizeLimit 本次获取的最大字节数 (Maximum bytes to fetch in this request)
71
+ * @param debug 是否启用调试模式 (Whether debug mode is enabled)
72
+ * @returns 分段内容,包含获取信息 (Chunk content with fetch information)
73
+ */
74
+ static getChunkBySize(chunkId, startCursor = 0, sizeLimit = 50 * 1024, debug = false) {
75
+ // 检查分段ID是否存在 (Check if chunk ID exists)
76
+ if (!this.chunks.has(chunkId) || !this.sizeInfo.has(chunkId)) {
77
+ log('chunkManager.chunkIdNotFound', debug, { chunkId }, COMPONENTS.CHUNK_MANAGER);
78
+ return null;
79
+ }
80
+ // 获取分段内容数组和大小信息 (Get chunked content array and size information)
81
+ const chunks = this.chunks.get(chunkId);
82
+ const { totalBytes, fetchedBytes } = this.sizeInfo.get(chunkId);
83
+ // 验证startCursor是否有效 (Validate if startCursor is valid)
84
+ if (startCursor < 0 || startCursor >= totalBytes) {
85
+ log('chunkManager.invalidStartCursor', debug, {
86
+ chunkId,
87
+ startCursor,
88
+ totalBytes
89
+ }, COMPONENTS.CHUNK_MANAGER);
90
+ return null;
91
+ }
92
+ // 计算当前位置和每个分块的起始位置 (Calculate current position and start position of each chunk)
93
+ let currentPosition = 0;
94
+ let chunkIndex = 0;
95
+ let fetchedSoFar = 0;
96
+ let resultContent = '';
97
+ // 找到开始位置对应的分块 (Find the chunk corresponding to the start position)
98
+ for (let i = 0; i < fetchedBytes.length; i++) {
99
+ if (currentPosition + fetchedBytes[i] > startCursor) {
100
+ chunkIndex = i;
101
+ break;
102
+ }
103
+ fetchedSoFar += fetchedBytes[i];
104
+ currentPosition += fetchedBytes[i];
105
+ }
106
+ // 从找到的分块开始,读取指定大小的内容 (Start reading from the found chunk, up to the specified size)
107
+ let bytesToFetch = sizeLimit;
108
+ let bytesActuallyFetched = 0;
109
+ while (chunkIndex < chunks.length && bytesToFetch > 0) {
110
+ resultContent += chunks[chunkIndex];
111
+ bytesActuallyFetched += fetchedBytes[chunkIndex];
112
+ bytesToFetch -= fetchedBytes[chunkIndex];
113
+ chunkIndex++;
114
+ }
115
+ // 计算总获取字节数和剩余字节数 (Calculate total fetched bytes and remaining bytes)
116
+ const totalFetchedBytes = startCursor + bytesActuallyFetched;
117
+ const remainingBytes = totalBytes - totalFetchedBytes;
118
+ const isLastChunk = (remainingBytes <= 0);
119
+ log('chunkManager.retrievedChunkBySize', debug, {
120
+ chunkId,
121
+ startCursor,
122
+ size: sizeLimit,
123
+ bytesRequested: sizeLimit,
124
+ bytesRetrieved: bytesActuallyFetched,
125
+ totalFetchedBytes,
126
+ remainingBytes,
127
+ isLastChunk
128
+ }, COMPONENTS.CHUNK_MANAGER);
129
+ // 返回内容和获取信息 (Return content and fetch information)
130
+ return {
131
+ content: resultContent,
132
+ fetchedBytes: totalFetchedBytes,
133
+ remainingBytes,
134
+ isLastChunk,
135
+ totalBytes
136
+ };
137
+ }
138
+ /**
139
+ * 获取分段大小信息 (Get chunk size information)
140
+ * @param chunkId 分段ID (Chunk ID)
141
+ * @param debug 是否启用调试模式 (Whether debug mode is enabled)
142
+ * @returns 分段大小信息 (Chunk size information)
143
+ */
144
+ static getSizeInfo(chunkId, debug = false) {
145
+ // 检查分段ID是否存在 (Check if chunk ID exists)
146
+ if (!this.sizeInfo.has(chunkId)) {
147
+ log('chunkManager.sizeInfoNotFound', debug, { chunkId }, COMPONENTS.CHUNK_MANAGER);
148
+ return null;
149
+ }
150
+ // 返回分段大小信息 (Return chunk size information)
151
+ return this.sizeInfo.get(chunkId);
152
+ }
153
+ /**
154
+ * 清理过期的分段内容 (Clean up expired chunks)
155
+ * @param debug 是否启用调试模式 (Whether debug mode is enabled)
156
+ */
157
+ static cleanupExpiredChunks(debug = false) {
158
+ const now = Date.now();
159
+ let expiredCount = 0;
160
+ // 遍历所有分段ID (Iterate through all chunk IDs)
161
+ for (const [chunkId, expirationTime] of this.expirations.entries()) {
162
+ // 如果分段内容已过期,则删除 (If chunk content has expired, delete it)
163
+ if (expirationTime < now) {
164
+ this.chunks.delete(chunkId);
165
+ this.sizeInfo.delete(chunkId);
166
+ this.expirations.delete(chunkId);
167
+ expiredCount++;
168
+ }
169
+ }
170
+ if (expiredCount > 0) {
171
+ log('chunkManager.cleanedUpExpiredChunks', debug, {
172
+ expiredCount,
173
+ remainingChunks: this.chunks.size
174
+ }, COMPONENTS.CHUNK_MANAGER);
175
+ }
176
+ }
177
+ }
@@ -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
  /**
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes