@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
package/dist/src/index.js
CHANGED
|
File without changes
|
package/dist/src/lib/fetch.js
CHANGED
|
@@ -28,6 +28,14 @@ export async function fetchJson(requestPayload) {
|
|
|
28
28
|
export async function fetchTxt(requestPayload) {
|
|
29
29
|
return await Fetcher.txt(requestPayload);
|
|
30
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* 获取HTML并转换为纯文本 (Get HTML and convert to plain text)
|
|
33
|
+
* @param requestPayload 请求参数 (Request parameters)
|
|
34
|
+
* @returns 纯文本内容 (Plain text content)
|
|
35
|
+
*/
|
|
36
|
+
export async function fetchPlainText(requestPayload) {
|
|
37
|
+
return await Fetcher.plainText(requestPayload);
|
|
38
|
+
}
|
|
31
39
|
/**
|
|
32
40
|
* 获取Markdown内容 (Get Markdown content)
|
|
33
41
|
* @param requestPayload 请求参数 (Request parameters)
|
|
@@ -3,17 +3,19 @@
|
|
|
3
3
|
* Co-Author: AI Assistant (Claude)
|
|
4
4
|
* Description: This code was collaboratively developed by Martin and AI Assistant.
|
|
5
5
|
*/
|
|
6
|
-
import TurndownService from 'turndown';
|
|
7
6
|
import { JSDOM } from 'jsdom';
|
|
8
7
|
import { log, COMPONENTS } from '../../logger.js';
|
|
9
8
|
import { BrowserInstance } from './BrowserInstance.js';
|
|
10
9
|
import { PageOperations, CookieManager } from './PageOperations.js';
|
|
11
10
|
import { getRandomUserAgent, getSystemProxy } from '../common/utils.js';
|
|
11
|
+
import { ContentSizeManager } from '../../utils/ContentSizeManager.js';
|
|
12
|
+
import { BaseFetcher } from '../common/BaseFetcher.js';
|
|
13
|
+
import { ContentProcessor } from '../../utils/ContentProcessor.js';
|
|
12
14
|
/**
|
|
13
15
|
* 浏览器模式获取器类 (Browser mode fetcher class)
|
|
14
16
|
* 使用Puppeteer实现浏览器模式的网页获取 (Implements webpage fetching in browser mode using Puppeteer)
|
|
15
17
|
*/
|
|
16
|
-
export class BrowserFetcher {
|
|
18
|
+
export class BrowserFetcher extends BaseFetcher {
|
|
17
19
|
/**
|
|
18
20
|
* 使用Puppeteer获取网页内容 (Get webpage content using Puppeteer)
|
|
19
21
|
* 浏览器模式获取的底层实现方法 (Low-level implementation method for browser mode fetching)
|
|
@@ -49,10 +51,7 @@ export class BrowserFetcher {
|
|
|
49
51
|
// 如果只是要关闭浏览器,不需要获取内容
|
|
50
52
|
if (url === 'about:blank' && closeBrowser) {
|
|
51
53
|
await BrowserInstance.closeBrowser(debug);
|
|
52
|
-
return
|
|
53
|
-
content: [{ type: 'text', text: 'Browser closed successfully' }],
|
|
54
|
-
isError: false
|
|
55
|
-
};
|
|
54
|
+
return BaseFetcher.createSuccessResponse('Browser closed successfully');
|
|
56
55
|
}
|
|
57
56
|
// 获取浏览器实例
|
|
58
57
|
const browser = await BrowserInstance.getBrowser(debug);
|
|
@@ -127,10 +126,7 @@ export class BrowserFetcher {
|
|
|
127
126
|
if (closeBrowser) {
|
|
128
127
|
await BrowserInstance.closeBrowser(debug);
|
|
129
128
|
}
|
|
130
|
-
return
|
|
131
|
-
content: [{ type: 'text', text: finalContent }],
|
|
132
|
-
isError: false
|
|
133
|
-
};
|
|
129
|
+
return BaseFetcher.createSuccessResponse(finalContent);
|
|
134
130
|
}
|
|
135
131
|
catch (error) {
|
|
136
132
|
log('browser.fetchError', debug, { error: String(error) }, COMPONENTS.BROWSER_FETCH);
|
|
@@ -138,61 +134,43 @@ export class BrowserFetcher {
|
|
|
138
134
|
if (closeBrowser) {
|
|
139
135
|
await BrowserInstance.closeBrowser(debug);
|
|
140
136
|
}
|
|
141
|
-
return {
|
|
142
|
-
content: [{ type: 'text', text: `Error fetching ${url}: ${error}` }],
|
|
143
|
-
isError: true
|
|
144
|
-
};
|
|
137
|
+
return BaseFetcher.createErrorResponse(`Error fetching ${url}: ${error}`);
|
|
145
138
|
}
|
|
146
139
|
}
|
|
147
140
|
/**
|
|
148
141
|
* 使用Puppeteer获取HTML内容 (Get HTML content using Puppeteer)
|
|
149
|
-
*
|
|
142
|
+
* 通过浏览器模式获取HTML页面 (Fetch HTML page in browser mode)
|
|
150
143
|
* @param requestPayload 请求参数 (Request parameters)
|
|
151
144
|
* @returns 获取结果 (Fetch result)
|
|
152
145
|
*/
|
|
153
146
|
async html(requestPayload) {
|
|
154
|
-
const { url, debug = false, closeBrowser = false } = requestPayload;
|
|
155
|
-
//
|
|
156
|
-
if (
|
|
157
|
-
|
|
158
|
-
await BrowserInstance.closeBrowser(debug);
|
|
159
|
-
return {
|
|
160
|
-
content: [{ type: 'text', text: 'Browser closed successfully' }],
|
|
161
|
-
isError: false
|
|
162
|
-
};
|
|
147
|
+
const { url, debug = false, closeBrowser = false, contentSizeLimit = ContentSizeManager.getDefaultSizeLimit(), enableContentSplitting = true, chunkId, startCursor = 0 } = requestPayload;
|
|
148
|
+
// 如果提供了分段ID和起始游标,则从缓存中获取分段内容 (If chunk ID and startCursor are provided, get chunk content from cache)
|
|
149
|
+
if (chunkId && startCursor !== undefined) {
|
|
150
|
+
return this.getChunkContent(chunkId, startCursor, contentSizeLimit, debug, COMPONENTS.BROWSER_FETCH);
|
|
163
151
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
152
|
+
log('browser.startingHtmlFetch', debug, { url }, COMPONENTS.BROWSER_FETCH);
|
|
153
|
+
try {
|
|
154
|
+
// 使用通用的fetch方法获取内容 (Use common fetch method to get content)
|
|
155
|
+
const result = await BrowserFetcher.fetch(requestPayload);
|
|
156
|
+
// 如果出错,直接返回错误 (If error, return error directly)
|
|
157
|
+
if (result.isError) {
|
|
158
|
+
return result;
|
|
170
159
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
await new Promise(resolve => setTimeout(resolve, delayMs));
|
|
178
|
-
return fetchWithRetry(retryCount + 1);
|
|
179
|
-
}
|
|
180
|
-
// 超过最大重试次数,返回错误
|
|
181
|
-
return {
|
|
182
|
-
content: [{ type: 'text', text: `Error fetching ${url} after ${maxRetries + 1} attempts: ${error}` }],
|
|
183
|
-
isError: true
|
|
184
|
-
};
|
|
160
|
+
// 获取HTML内容 (Get HTML content)
|
|
161
|
+
const html = result.content[0].text;
|
|
162
|
+
// 检查内容大小并处理 (Check content size and process)
|
|
163
|
+
const chunkingResult = this.handleContentChunking(html, contentSizeLimit, enableContentSplitting, debug, COMPONENTS.BROWSER_FETCH);
|
|
164
|
+
if (chunkingResult) {
|
|
165
|
+
return chunkingResult;
|
|
185
166
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
return await fetchWithRetry();
|
|
167
|
+
// 返回HTML内容 (Return HTML content)
|
|
168
|
+
return result;
|
|
189
169
|
}
|
|
190
|
-
|
|
191
|
-
//
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
await BrowserInstance.closeBrowser(debug);
|
|
195
|
-
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
// 处理错误 (Handle error)
|
|
172
|
+
log('browser.htmlFetchError', debug, { error: String(error) }, COMPONENTS.BROWSER_FETCH);
|
|
173
|
+
return BaseFetcher.createErrorResponse(`Error fetching HTML: ${error}`);
|
|
196
174
|
}
|
|
197
175
|
}
|
|
198
176
|
/**
|
|
@@ -202,68 +180,43 @@ export class BrowserFetcher {
|
|
|
202
180
|
* @returns 获取结果 (Fetch result)
|
|
203
181
|
*/
|
|
204
182
|
async json(requestPayload) {
|
|
205
|
-
const { url, debug = false, closeBrowser = false } = requestPayload;
|
|
206
|
-
//
|
|
207
|
-
if (
|
|
208
|
-
|
|
209
|
-
await BrowserInstance.closeBrowser(debug);
|
|
210
|
-
return {
|
|
211
|
-
content: [{ type: 'text', text: 'Browser closed successfully' }],
|
|
212
|
-
isError: false
|
|
213
|
-
};
|
|
183
|
+
const { url, debug = false, closeBrowser = false, contentSizeLimit = ContentSizeManager.getDefaultSizeLimit(), enableContentSplitting = true, chunkId, startCursor = 0 } = requestPayload;
|
|
184
|
+
// 如果提供了分段ID和起始游标,则从缓存中获取分段内容 (If chunk ID and startCursor are provided, get chunk content from cache)
|
|
185
|
+
if (chunkId && startCursor !== undefined) {
|
|
186
|
+
return this.getChunkContent(chunkId, startCursor, contentSizeLimit, debug, COMPONENTS.BROWSER_FETCH);
|
|
214
187
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
188
|
+
log('browser.startingJsonFetch', debug, { url }, COMPONENTS.BROWSER_FETCH);
|
|
189
|
+
try {
|
|
190
|
+
// 使用通用的fetch方法获取内容 (Use common fetch method to get content)
|
|
191
|
+
const result = await BrowserFetcher.fetch(requestPayload);
|
|
192
|
+
// 如果出错,直接返回错误 (If error, return error directly)
|
|
193
|
+
if (result.isError) {
|
|
194
|
+
return result;
|
|
195
|
+
}
|
|
196
|
+
// 获取文本内容 (Get text content)
|
|
197
|
+
const text = result.content[0].text;
|
|
198
|
+
// 尝试解析JSON (Try to parse JSON)
|
|
219
199
|
try {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
JSON.parse(jsonText);
|
|
227
|
-
// 如果解析成功,直接返回结果
|
|
228
|
-
return result;
|
|
229
|
-
}
|
|
230
|
-
catch (error) {
|
|
231
|
-
// JSON解析失败
|
|
232
|
-
log('browser.failedToParseJSON', debug, { error: String(error) }, COMPONENTS.BROWSER_FETCH);
|
|
233
|
-
return {
|
|
234
|
-
content: [{ type: 'text', text: `Error parsing JSON from ${url}: ${error}` }],
|
|
235
|
-
isError: true
|
|
236
|
-
};
|
|
237
|
-
}
|
|
200
|
+
JSON.parse(text);
|
|
201
|
+
log('browser.jsonParsed', debug, {}, COMPONENTS.BROWSER_FETCH);
|
|
202
|
+
// 检查内容大小并处理 (Check content size and process)
|
|
203
|
+
const chunkingResult = this.handleContentChunking(text, contentSizeLimit, enableContentSplitting, debug, COMPONENTS.BROWSER_FETCH);
|
|
204
|
+
if (chunkingResult) {
|
|
205
|
+
return chunkingResult;
|
|
238
206
|
}
|
|
207
|
+
// 返回JSON内容 (Return JSON content)
|
|
239
208
|
return result;
|
|
240
209
|
}
|
|
241
|
-
catch (
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
const delayMs = 1000 * (retryCount + 1);
|
|
246
|
-
log('browser.retryingAfterDelay', debug, { delayMs }, COMPONENTS.BROWSER_FETCH);
|
|
247
|
-
// 等待一段时间后重试
|
|
248
|
-
await new Promise(resolve => setTimeout(resolve, delayMs));
|
|
249
|
-
return fetchWithRetry(retryCount + 1);
|
|
250
|
-
}
|
|
251
|
-
// 超过最大重试次数,返回错误
|
|
252
|
-
return {
|
|
253
|
-
content: [{ type: 'text', text: `Error fetching JSON from ${url} after ${maxRetries + 1} attempts: ${error}` }],
|
|
254
|
-
isError: true
|
|
255
|
-
};
|
|
210
|
+
catch (parseError) {
|
|
211
|
+
// 处理JSON解析错误 (Handle JSON parse error)
|
|
212
|
+
log('browser.jsonParseError', debug, { error: String(parseError) }, COMPONENTS.BROWSER_FETCH);
|
|
213
|
+
return BaseFetcher.createErrorResponse(`Error parsing JSON: ${parseError}`);
|
|
256
214
|
}
|
|
257
|
-
};
|
|
258
|
-
try {
|
|
259
|
-
return await fetchWithRetry();
|
|
260
215
|
}
|
|
261
|
-
|
|
262
|
-
//
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
await BrowserInstance.closeBrowser(debug);
|
|
266
|
-
}
|
|
216
|
+
catch (error) {
|
|
217
|
+
// 处理错误 (Handle error)
|
|
218
|
+
log('browser.jsonFetchError', debug, { error: String(error) }, COMPONENTS.BROWSER_FETCH);
|
|
219
|
+
return BaseFetcher.createErrorResponse(`Error fetching JSON: ${error}`);
|
|
267
220
|
}
|
|
268
221
|
}
|
|
269
222
|
/**
|
|
@@ -273,109 +226,96 @@ export class BrowserFetcher {
|
|
|
273
226
|
* @returns 获取结果 (Fetch result)
|
|
274
227
|
*/
|
|
275
228
|
async txt(requestPayload) {
|
|
276
|
-
const { url, debug = false, closeBrowser = false } = requestPayload;
|
|
277
|
-
//
|
|
278
|
-
if (
|
|
279
|
-
|
|
280
|
-
await BrowserInstance.closeBrowser(debug);
|
|
281
|
-
return {
|
|
282
|
-
content: [{ type: 'text', text: 'Browser closed successfully' }],
|
|
283
|
-
isError: false
|
|
284
|
-
};
|
|
229
|
+
const { url, debug = false, closeBrowser = false, contentSizeLimit = ContentSizeManager.getDefaultSizeLimit(), enableContentSplitting = true, chunkId, startCursor = 0 } = requestPayload;
|
|
230
|
+
// 如果提供了分段ID和起始游标,则从缓存中获取分段内容 (If chunk ID and startCursor are provided, get chunk content from cache)
|
|
231
|
+
if (chunkId && startCursor !== undefined) {
|
|
232
|
+
return this.getChunkContent(chunkId, startCursor, contentSizeLimit, debug, COMPONENTS.BROWSER_FETCH);
|
|
285
233
|
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
// 如果获取成功,提取纯文本
|
|
293
|
-
if (!result.isError) {
|
|
294
|
-
const htmlContent = result.content[0].text;
|
|
295
|
-
// 使用JSDOM提取纯文本
|
|
296
|
-
const dom = new JSDOM(htmlContent);
|
|
297
|
-
const textContent = dom.window.document.body.textContent || '';
|
|
298
|
-
// 清理文本(移除多余空白)
|
|
299
|
-
const cleanedText = textContent
|
|
300
|
-
.replace(/\s+/g, ' ')
|
|
301
|
-
.trim();
|
|
302
|
-
return {
|
|
303
|
-
content: [{ type: 'text', text: cleanedText }],
|
|
304
|
-
isError: false
|
|
305
|
-
};
|
|
306
|
-
}
|
|
234
|
+
log('browser.startingTxtFetch', debug, { url }, COMPONENTS.BROWSER_FETCH);
|
|
235
|
+
try {
|
|
236
|
+
// 使用通用的fetch方法获取内容 (Use common fetch method to get content)
|
|
237
|
+
const result = await BrowserFetcher.fetch(requestPayload);
|
|
238
|
+
// 如果出错,直接返回错误 (If error, return error directly)
|
|
239
|
+
if (result.isError) {
|
|
307
240
|
return result;
|
|
308
241
|
}
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
return {
|
|
321
|
-
content: [{ type: 'text', text: `Error fetching text from ${url} after ${maxRetries + 1} attempts: ${error}` }],
|
|
322
|
-
isError: true
|
|
323
|
-
};
|
|
242
|
+
// 获取HTML内容 (Get HTML content)
|
|
243
|
+
const html = result.content[0].text;
|
|
244
|
+
// 使用JSDOM提取纯文本 (Extract plain text using JSDOM)
|
|
245
|
+
log('browser.extractingText', debug, {}, COMPONENTS.BROWSER_FETCH);
|
|
246
|
+
const dom = new JSDOM(html);
|
|
247
|
+
const text = dom.window.document.body.textContent || '';
|
|
248
|
+
log('browser.textExtracted', debug, { length: text.length }, COMPONENTS.BROWSER_FETCH);
|
|
249
|
+
// 检查内容大小并处理 (Check content size and process)
|
|
250
|
+
const chunkingResult = this.handleContentChunking(text, contentSizeLimit, enableContentSplitting, debug, COMPONENTS.BROWSER_FETCH);
|
|
251
|
+
if (chunkingResult) {
|
|
252
|
+
return chunkingResult;
|
|
324
253
|
}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
return await fetchWithRetry();
|
|
254
|
+
// 返回纯文本内容 (Return plain text content)
|
|
255
|
+
return BaseFetcher.createSuccessResponse(text);
|
|
328
256
|
}
|
|
329
|
-
|
|
330
|
-
//
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
await BrowserInstance.closeBrowser(debug);
|
|
334
|
-
}
|
|
257
|
+
catch (error) {
|
|
258
|
+
// 处理错误 (Handle error)
|
|
259
|
+
log('browser.txtFetchError', debug, { error: String(error) }, COMPONENTS.BROWSER_FETCH);
|
|
260
|
+
return BaseFetcher.createErrorResponse(`Error fetching text: ${error}`);
|
|
335
261
|
}
|
|
336
262
|
}
|
|
337
263
|
/**
|
|
338
|
-
*
|
|
339
|
-
* 通过浏览器模式获取HTML并转换为Markdown (Fetch HTML in browser mode and convert to Markdown)
|
|
264
|
+
* 获取HTML并转换为纯文本 (Get HTML and convert to plain text)
|
|
340
265
|
* @param requestPayload 请求参数 (Request parameters)
|
|
341
|
-
* @returns
|
|
266
|
+
* @returns 纯文本响应 (Plain text response)
|
|
267
|
+
*/
|
|
268
|
+
async plainText(requestPayload) {
|
|
269
|
+
const { url, debug = false, closeBrowser = false, contentSizeLimit = ContentSizeManager.getDefaultSizeLimit(), enableContentSplitting = true, chunkId, startCursor = 0 } = requestPayload;
|
|
270
|
+
// 如果提供了分段ID和起始游标,则从缓存中获取分段内容 (If chunk ID and startCursor are provided, get chunk content from cache)
|
|
271
|
+
if (chunkId && startCursor !== undefined) {
|
|
272
|
+
return this.getChunkContent(chunkId, startCursor, contentSizeLimit, debug, COMPONENTS.BROWSER_FETCH);
|
|
273
|
+
}
|
|
274
|
+
log('browser.startingPlainTextFetch', debug, { url: requestPayload.url }, COMPONENTS.BROWSER_FETCH);
|
|
275
|
+
// 使用HTML方法获取内容 (Use HTML method to get content)
|
|
276
|
+
const result = await this.html(requestPayload);
|
|
277
|
+
if (result.isError) {
|
|
278
|
+
return result;
|
|
279
|
+
}
|
|
280
|
+
// 获取HTML内容 (Get HTML content)
|
|
281
|
+
const html = result.content[0].text;
|
|
282
|
+
// 使用ContentProcessor将HTML转换为纯文本 (Use ContentProcessor to convert HTML to plain text)
|
|
283
|
+
const plainText = ContentProcessor.htmlToText(html, debug, COMPONENTS.BROWSER_FETCH);
|
|
284
|
+
// 检查内容大小并处理 (Check content size and process)
|
|
285
|
+
const chunkingResult = this.handleContentChunking(plainText, contentSizeLimit, enableContentSplitting, debug, COMPONENTS.BROWSER_FETCH);
|
|
286
|
+
if (chunkingResult) {
|
|
287
|
+
return chunkingResult;
|
|
288
|
+
}
|
|
289
|
+
// 返回纯文本内容 (Return plain text content)
|
|
290
|
+
return BaseFetcher.createSuccessResponse(plainText);
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* 获取HTML并转换为Markdown (Get HTML and convert to Markdown)
|
|
294
|
+
* @param requestPayload 请求参数 (Request parameters)
|
|
295
|
+
* @returns Markdown内容 (Markdown content)
|
|
342
296
|
*/
|
|
343
297
|
async markdown(requestPayload) {
|
|
344
|
-
const { url, debug = false } = requestPayload;
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
const htmlResult = await this.html(requestPayload);
|
|
349
|
-
if (htmlResult.isError) {
|
|
350
|
-
return htmlResult;
|
|
351
|
-
}
|
|
352
|
-
// 将HTML转换为Markdown
|
|
353
|
-
const turndownService = new TurndownService({
|
|
354
|
-
headingStyle: 'atx',
|
|
355
|
-
codeBlockStyle: 'fenced',
|
|
356
|
-
bulletListMarker: '-'
|
|
357
|
-
});
|
|
358
|
-
// 添加表格支持
|
|
359
|
-
turndownService.addRule('tables', {
|
|
360
|
-
filter: ['table'],
|
|
361
|
-
replacement: function (content, node) {
|
|
362
|
-
const tableContent = content.trim();
|
|
363
|
-
return '\n\n' + tableContent + '\n\n';
|
|
364
|
-
}
|
|
365
|
-
});
|
|
366
|
-
// 转换HTML为Markdown
|
|
367
|
-
const markdown = turndownService.turndown(htmlResult.content[0].text);
|
|
368
|
-
return {
|
|
369
|
-
content: [{ type: 'text', text: markdown }],
|
|
370
|
-
isError: false
|
|
371
|
-
};
|
|
298
|
+
const { url, debug = false, closeBrowser = false, contentSizeLimit = ContentSizeManager.getDefaultSizeLimit(), enableContentSplitting = true, chunkId, startCursor = 0 } = requestPayload;
|
|
299
|
+
// 如果提供了分段ID和起始游标,则从缓存中获取分段内容 (If chunk ID and startCursor are provided, get chunk content from cache)
|
|
300
|
+
if (chunkId && startCursor !== undefined) {
|
|
301
|
+
return this.getChunkContent(chunkId, startCursor, contentSizeLimit, debug, COMPONENTS.BROWSER_FETCH);
|
|
372
302
|
}
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
303
|
+
log('browser.startingMarkdownFetch', debug, { url: requestPayload.url }, COMPONENTS.BROWSER_FETCH);
|
|
304
|
+
// 使用HTML方法获取内容 (Use HTML method to get content)
|
|
305
|
+
const result = await this.html(requestPayload);
|
|
306
|
+
if (result.isError) {
|
|
307
|
+
return result;
|
|
308
|
+
}
|
|
309
|
+
// 获取HTML内容 (Get HTML content)
|
|
310
|
+
const html = result.content[0].text;
|
|
311
|
+
// 使用ContentProcessor将HTML转换为Markdown (Use ContentProcessor to convert HTML to Markdown)
|
|
312
|
+
const markdown = ContentProcessor.htmlToMarkdown(html, debug, COMPONENTS.BROWSER_FETCH);
|
|
313
|
+
// 检查内容大小并处理 (Check content size and process)
|
|
314
|
+
const chunkingResult = this.handleContentChunking(markdown, contentSizeLimit, enableContentSplitting, debug, COMPONENTS.BROWSER_FETCH);
|
|
315
|
+
if (chunkingResult) {
|
|
316
|
+
return chunkingResult;
|
|
379
317
|
}
|
|
318
|
+
// 返回Markdown内容 (Return Markdown content)
|
|
319
|
+
return BaseFetcher.createSuccessResponse(markdown);
|
|
380
320
|
}
|
|
381
321
|
}
|
|
@@ -0,0 +1,203 @@
|
|
|
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 } from "../../logger.js";
|
|
7
|
+
import { ContentSizeManager } from "../../utils/ContentSizeManager.js";
|
|
8
|
+
import { ChunkManager } from "../../utils/ChunkManager.js";
|
|
9
|
+
import { TemplateUtils } from "../../utils/TemplateUtils.js";
|
|
10
|
+
/**
|
|
11
|
+
* 基础获取器类 (Base fetcher class)
|
|
12
|
+
* 提供所有获取器共用的基础功能 (Provides basic functionality used by all fetchers)
|
|
13
|
+
*/
|
|
14
|
+
export class BaseFetcher {
|
|
15
|
+
/**
|
|
16
|
+
* 创建成功响应 (Create success response)
|
|
17
|
+
* @param content 内容 (Content)
|
|
18
|
+
* @returns 成功响应 (Success response)
|
|
19
|
+
*/
|
|
20
|
+
static createSuccessResponse(content) {
|
|
21
|
+
return {
|
|
22
|
+
isError: false,
|
|
23
|
+
content: [
|
|
24
|
+
{
|
|
25
|
+
type: 'text',
|
|
26
|
+
text: content
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* 创建错误响应 (Create error response)
|
|
33
|
+
* @param errorMessage 错误信息 (Error message)
|
|
34
|
+
* @returns 错误响应 (Error response)
|
|
35
|
+
*/
|
|
36
|
+
static createErrorResponse(errorMessage) {
|
|
37
|
+
return {
|
|
38
|
+
isError: true,
|
|
39
|
+
content: [
|
|
40
|
+
{
|
|
41
|
+
type: 'text',
|
|
42
|
+
text: errorMessage
|
|
43
|
+
}
|
|
44
|
+
]
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* 处理内容分段 (Handle content chunking)
|
|
49
|
+
* 如果内容超过大小限制,则进行分段处理 (If content exceeds size limit, chunk it)
|
|
50
|
+
* @param content 内容 (Content)
|
|
51
|
+
* @param contentSizeLimit 内容大小限制 (Content size limit)
|
|
52
|
+
* @param enableContentSplitting 是否启用内容分段 (Whether to enable content splitting)
|
|
53
|
+
* @param debug 是否启用调试输出 (Whether to enable debug output)
|
|
54
|
+
* @param component 日志组件名称 (Log component name)
|
|
55
|
+
* @returns 分段处理结果,如果不需要分段则返回null (Chunking result, or null if no chunking needed)
|
|
56
|
+
*/
|
|
57
|
+
handleContentChunking(content, contentSizeLimit = ContentSizeManager.getDefaultSizeLimit(), enableContentSplitting = true, debug = false, component) {
|
|
58
|
+
// 检查内容大小 (Check content size)
|
|
59
|
+
const contentLength = content.length;
|
|
60
|
+
const contentBytes = Buffer.byteLength(content, 'utf8');
|
|
61
|
+
log('fetcher.contentLength', debug, { length: contentLength, bytes: contentBytes }, component);
|
|
62
|
+
// 如果内容大小超过限制且启用了内容分段 (If content size exceeds limit and content splitting is enabled)
|
|
63
|
+
if (contentBytes > contentSizeLimit && enableContentSplitting) {
|
|
64
|
+
log('fetcher.contentTooLarge', debug, {
|
|
65
|
+
size: contentBytes,
|
|
66
|
+
limit: contentSizeLimit
|
|
67
|
+
}, component);
|
|
68
|
+
// 分割内容 (Split content)
|
|
69
|
+
const { chunks, totalBytes } = ContentSizeManager.splitContentIntoChunks(content, contentSizeLimit, debug, 0);
|
|
70
|
+
// 存储分割后的内容 (Store chunked content)
|
|
71
|
+
const chunkId = ChunkManager.storeChunks(chunks, totalBytes, debug);
|
|
72
|
+
// 获取第一个分片 (Get first chunk)
|
|
73
|
+
const chunkResult = ChunkManager.getChunkBySize(chunkId, 0, contentSizeLimit, debug);
|
|
74
|
+
if (!chunkResult) {
|
|
75
|
+
log('fetcher.chunkRetrievalFailed', debug, { chunkId }, component);
|
|
76
|
+
return BaseFetcher.createErrorResponse("Failed to retrieve chunk content");
|
|
77
|
+
}
|
|
78
|
+
const { content: firstChunk, fetchedBytes, remainingBytes, totalBytes: totalSize } = chunkResult;
|
|
79
|
+
// 计算预计还需要的请求次数 (Calculate estimated number of requests needed)
|
|
80
|
+
const estimatedRequests = Math.ceil(remainingBytes / contentSizeLimit);
|
|
81
|
+
// 添加分段提示 (Add chunk prompt)
|
|
82
|
+
const isFirstRequest = true;
|
|
83
|
+
const chunkWithPrompt = firstChunk + TemplateUtils.generateSizeBasedChunkPrompt(fetchedBytes, totalSize, chunkId, remainingBytes, estimatedRequests, contentSizeLimit, isFirstRequest);
|
|
84
|
+
// 创建响应 (Create response)
|
|
85
|
+
return {
|
|
86
|
+
isError: false,
|
|
87
|
+
isChunked: true,
|
|
88
|
+
content: [
|
|
89
|
+
{
|
|
90
|
+
type: 'text',
|
|
91
|
+
text: chunkWithPrompt
|
|
92
|
+
}
|
|
93
|
+
],
|
|
94
|
+
chunkId,
|
|
95
|
+
totalBytes: totalSize,
|
|
96
|
+
fetchedBytes,
|
|
97
|
+
remainingBytes,
|
|
98
|
+
hasMoreChunks: remainingBytes > 0
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
// 如果内容大小超过限制但未启用内容分段 (If content size exceeds limit but content splitting is not enabled)
|
|
102
|
+
if (contentBytes > contentSizeLimit && !enableContentSplitting) {
|
|
103
|
+
log('fetcher.contentTruncated', debug, {
|
|
104
|
+
originalBytes: contentBytes,
|
|
105
|
+
truncatedLength: contentSizeLimit
|
|
106
|
+
}, component);
|
|
107
|
+
const truncatedContent = content.substring(0, contentSizeLimit);
|
|
108
|
+
return BaseFetcher.createSuccessResponse(truncatedContent + `\n\n${TemplateUtils.SYSTEM_NOTE.START}\nContent was too large (${contentBytes} bytes) and has been truncated to ${contentSizeLimit} bytes. Enable content splitting to view the full content.\n${TemplateUtils.SYSTEM_NOTE.END}`);
|
|
109
|
+
}
|
|
110
|
+
// 如果不需要分段,返回null (If no chunking needed, return null)
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* 获取分段内容 - 基于字节偏移量 (Get chunked content based on byte offset)
|
|
115
|
+
* @param chunkId 分段ID (Chunk ID)
|
|
116
|
+
* @param startCursor 开始游标位置 (Start cursor position)
|
|
117
|
+
* @param sizeLimit 大小限制 (Size limit)
|
|
118
|
+
* @param debug 是否启用调试模式 (Whether debug mode is enabled)
|
|
119
|
+
* @param component 日志组件名称 (Log component name)
|
|
120
|
+
* @returns 获取结果 (Fetch result)
|
|
121
|
+
*/
|
|
122
|
+
getChunkContent(chunkId, startCursor = 0, sizeLimit = ContentSizeManager.getDefaultSizeLimit(), debug = false, component) {
|
|
123
|
+
log('fetcher.gettingChunkBySize', debug, {
|
|
124
|
+
chunkId,
|
|
125
|
+
startCursor,
|
|
126
|
+
start: startCursor,
|
|
127
|
+
end: startCursor + sizeLimit,
|
|
128
|
+
sizeLimit
|
|
129
|
+
}, component);
|
|
130
|
+
// 获取分段内容 (Get chunked content)
|
|
131
|
+
const chunkResult = ChunkManager.getChunkBySize(chunkId, startCursor, sizeLimit, debug);
|
|
132
|
+
// 如果获取失败,返回错误 (If retrieval failed, return error)
|
|
133
|
+
if (!chunkResult) {
|
|
134
|
+
log('fetcher.chunkNotFound', debug, { chunkId, startCursor }, component);
|
|
135
|
+
return BaseFetcher.createErrorResponse(`Chunk with ID ${chunkId} at cursor position ${startCursor} not found`);
|
|
136
|
+
}
|
|
137
|
+
// 获取相关信息 (Get related information)
|
|
138
|
+
const { content, fetchedBytes, remainingBytes, isLastChunk, totalBytes } = chunkResult;
|
|
139
|
+
// 计算预计还需要的请求次数 (Calculate estimated number of requests needed)
|
|
140
|
+
const estimatedRequests = Math.ceil(remainingBytes / sizeLimit);
|
|
141
|
+
// 添加分段提示 (Add chunk prompt)
|
|
142
|
+
let contentWithPrompt;
|
|
143
|
+
if (isLastChunk) {
|
|
144
|
+
// 如果是最后一个分段 (If it's the last chunk)
|
|
145
|
+
contentWithPrompt = content + TemplateUtils.generateSizeBasedLastChunkPrompt(fetchedBytes, totalBytes, startCursor === 0 // 如果startCursor为0,则为首次请求 (If startCursor is 0, it's the first request)
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
// 如果不是最后一个分段 (If it's not the last chunk)
|
|
150
|
+
contentWithPrompt = content + TemplateUtils.generateSizeBasedChunkPrompt(fetchedBytes, totalBytes, chunkId, remainingBytes, estimatedRequests, sizeLimit, startCursor === 0 // 如果startCursor为0,则为首次请求 (If startCursor is 0, it's the first request)
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
// 创建响应 (Create response)
|
|
154
|
+
return {
|
|
155
|
+
isError: false,
|
|
156
|
+
isChunked: true,
|
|
157
|
+
content: [
|
|
158
|
+
{
|
|
159
|
+
type: 'text',
|
|
160
|
+
text: contentWithPrompt
|
|
161
|
+
}
|
|
162
|
+
],
|
|
163
|
+
chunkId,
|
|
164
|
+
totalBytes,
|
|
165
|
+
fetchedBytes,
|
|
166
|
+
remainingBytes,
|
|
167
|
+
hasMoreChunks: !isLastChunk
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* 添加分段提示词 (Add chunk prompt)
|
|
172
|
+
* @param response 响应对象 (Response object)
|
|
173
|
+
* @returns 添加了提示词的响应对象 (Response object with prompt)
|
|
174
|
+
*/
|
|
175
|
+
static addChunkPrompt(response) {
|
|
176
|
+
// 检查是否存在字节信息并需要添加分块提示 (Check if byte information exists and need to add chunk prompt)
|
|
177
|
+
if (response.isChunked && response.hasMoreChunks && response.totalBytes && response.fetchedBytes && response.remainingBytes) {
|
|
178
|
+
const chunkId = response.chunkId || '';
|
|
179
|
+
const currentSizeLimit = response.fetchedBytes; // 假设当前大小限制与已获取字节数相同 (Assume current size limit is the same as fetched bytes)
|
|
180
|
+
const estimatedRequests = Math.ceil(response.remainingBytes / currentSizeLimit);
|
|
181
|
+
// 检查内容中是否已经包含系统提示 (Check if content already contains system note)
|
|
182
|
+
if (TemplateUtils.hasSystemPrompt(response.content[0].text || '')) {
|
|
183
|
+
// 已存在提示,直接返回原始响应 (Note already exists, return original response)
|
|
184
|
+
return response;
|
|
185
|
+
}
|
|
186
|
+
// 使用基于字节的提示 (Use byte-based prompt)
|
|
187
|
+
const promptText = TemplateUtils.generateSizeBasedChunkPrompt(response.fetchedBytes, response.totalBytes, chunkId, response.remainingBytes, estimatedRequests, currentSizeLimit, false // 设置为false表示这不是首次请求 (Set to false indicating this is not the first request)
|
|
188
|
+
);
|
|
189
|
+
// 创建新的响应对象,避免修改原始对象 (Create new response object to avoid modifying the original)
|
|
190
|
+
return {
|
|
191
|
+
...response,
|
|
192
|
+
content: [
|
|
193
|
+
{
|
|
194
|
+
type: 'text',
|
|
195
|
+
text: (response.content[0].text || '') + promptText
|
|
196
|
+
}
|
|
197
|
+
]
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
// 如果不需要添加提示,返回原始响应 (If no prompt needed, return original response)
|
|
201
|
+
return response;
|
|
202
|
+
}
|
|
203
|
+
}
|