@lmcc-dev/mult-fetch-mcp-server 1.3.0 → 1.3.2
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 +68 -15
- package/README.zh.md +31 -0
- package/dist/src/client.js +118 -91
- package/dist/src/lib/fetchers/browser/BrowserFetcher.js +25 -10
- package/dist/src/lib/fetchers/browser/BrowserInstance.js +67 -64
- package/dist/src/lib/fetchers/common/BaseFetcher.js +106 -10
- package/dist/src/lib/fetchers/common/types.js +10 -1
- package/dist/src/lib/fetchers/common/utils.js +1 -1
- package/dist/src/lib/fetchers/node/HttpClient.js +41 -21
- package/dist/src/lib/fetchers/node/NodeFetcher.js +24 -8
- package/dist/src/lib/i18n/index.js +2 -2
- package/dist/src/lib/i18n/keys/client.js +1 -0
- package/dist/src/lib/i18n/keys/extractor.js +26 -0
- package/dist/src/lib/i18n/keys/fetcher.js +12 -1
- package/dist/src/lib/i18n/keys/index.js +1 -0
- package/dist/src/lib/i18n/keys/node.js +2 -0
- package/dist/src/lib/i18n/locales/en/client.js +1 -0
- package/dist/src/lib/i18n/locales/en/extractor.js +24 -0
- package/dist/src/lib/i18n/locales/en/fetcher.js +3 -1
- package/dist/src/lib/i18n/locales/en/index.js +2 -0
- package/dist/src/lib/i18n/locales/en/node.js +2 -0
- package/dist/src/lib/i18n/locales/zh/client.js +1 -0
- package/dist/src/lib/i18n/locales/zh/extractor.js +22 -0
- package/dist/src/lib/i18n/locales/zh/fetcher.js +13 -3
- package/dist/src/lib/i18n/locales/zh/index.js +2 -0
- package/dist/src/lib/i18n/locales/zh/node.js +2 -0
- package/dist/src/lib/i18n/logger.js +2 -2
- package/dist/src/lib/logger.js +38 -17
- package/dist/src/lib/server/browser.js +2 -2
- package/dist/src/lib/server/fetcher.js +0 -3
- package/dist/src/lib/server/index.js +2 -2
- package/dist/src/lib/server/prompts.js +4 -4
- package/dist/src/lib/server/tools.js +127 -354
- package/dist/src/lib/utils/ChunkManager.js +2 -2
- package/dist/src/lib/utils/ContentExtractor.js +141 -0
- package/dist/src/lib/utils/ContentProcessor.js +5 -11
- package/dist/src/lib/utils/ContentSizeManager.js +2 -2
- package/dist/src/lib/utils/ErrorHandler.js +1 -0
- package/dist/src/lib/utils/TemplateUtils.js +6 -2
- package/dist/tests/client.test.js +1 -1
- package/dist/tests/test-extract-single.js +389 -0
- package/dist/tests/utils/ContentExtractor.test.js +173 -0
- package/dist/tests/utils/ContentProcessor.test.js +136 -0
- package/dist/tests/utils/TemplateUtils.test.js +118 -0
- package/package.json +14 -3
- package/dist/i18n-test-report.json +0 -8
- package/dist/i18n-unused-keys-report.json +0 -8
- package/dist/src/lib/BrowserFetcher.js +0 -787
- package/dist/src/lib/NodeFetcher.js +0 -492
- package/dist/src/lib/i18n/keys.js +0 -529
- package/dist/src/test-i18n.js +0 -139
- package/dist/tests/BrowserFetcher.test.js +0 -951
- package/dist/tests/NodeFetcher.test.js +0 -263
- package/dist/tests/i18n-remove-unused-keys.js +0 -236
- package/dist/tests/i18n-test-report.json +0 -2004
- package/dist/tests/src/lib/i18n/index.js +0 -108
- package/dist/tests/src/lib/i18n/keys/base.js +0 -47
- package/dist/tests/src/lib/i18n/keys/browser.js +0 -93
- package/dist/tests/src/lib/i18n/keys/client.js +0 -70
- package/dist/tests/src/lib/i18n/keys/errors.js +0 -34
- package/dist/tests/src/lib/i18n/keys/fetcher.js +0 -84
- package/dist/tests/src/lib/i18n/keys/index.js +0 -31
- package/dist/tests/src/lib/i18n/keys/node.js +0 -56
- package/dist/tests/src/lib/i18n/keys/prompts.js +0 -82
- package/dist/tests/src/lib/i18n/keys/resources.js +0 -50
- package/dist/tests/src/lib/i18n/keys/server.js +0 -64
- package/dist/tests/src/lib/i18n/keys/tools.js +0 -34
- package/dist/tests/src/lib/i18n/locales/en/browser.js +0 -88
- package/dist/tests/src/lib/i18n/locales/en/client.js +0 -66
- package/dist/tests/src/lib/i18n/locales/en/errors.js +0 -28
- package/dist/tests/src/lib/i18n/locales/en/fetcher.js +0 -71
- package/dist/tests/src/lib/i18n/locales/en/index.js +0 -29
- package/dist/tests/src/lib/i18n/locales/en/node.js +0 -51
- package/dist/tests/src/lib/i18n/locales/en/prompts.js +0 -52
- package/dist/tests/src/lib/i18n/locales/en/resources.js +0 -50
- package/dist/tests/src/lib/i18n/locales/en/server.js +0 -56
- package/dist/tests/src/lib/i18n/locales/en/tools.js +0 -28
- package/dist/tests/src/lib/i18n/locales/zh/browser.js +0 -87
- package/dist/tests/src/lib/i18n/locales/zh/client.js +0 -66
- package/dist/tests/src/lib/i18n/locales/zh/errors.js +0 -28
- package/dist/tests/src/lib/i18n/locales/zh/fetcher.js +0 -71
- package/dist/tests/src/lib/i18n/locales/zh/index.js +0 -29
- package/dist/tests/src/lib/i18n/locales/zh/node.js +0 -51
- package/dist/tests/src/lib/i18n/locales/zh/prompts.js +0 -53
- package/dist/tests/src/lib/i18n/locales/zh/resources.js +0 -50
- package/dist/tests/src/lib/i18n/locales/zh/server.js +0 -57
- package/dist/tests/src/lib/i18n/locales/zh/tools.js +0 -28
- package/dist/tests/src/lib/i18n/logger.js +0 -114
- package/dist/tests/src/lib/logger.js +0 -181
- package/dist/tests/tests/test-i18n.js +0 -588
- package/dist/vitest.config.js +0 -29
|
@@ -144,7 +144,7 @@ export class BrowserFetcher extends BaseFetcher {
|
|
|
144
144
|
* @returns 获取结果 (Fetch result)
|
|
145
145
|
*/
|
|
146
146
|
async html(requestPayload) {
|
|
147
|
-
const { url, debug = false, closeBrowser = false, contentSizeLimit = ContentSizeManager.getDefaultSizeLimit(), enableContentSplitting = true, chunkId, startCursor = 0 } = requestPayload;
|
|
147
|
+
const { url, debug = false, closeBrowser: _closeBrowser = false, contentSizeLimit = ContentSizeManager.getDefaultSizeLimit(), enableContentSplitting = true, chunkId, startCursor = 0, extractContent = false, includeMetadata = true, fallbackToOriginal = true } = requestPayload;
|
|
148
148
|
// 如果提供了分段ID和起始游标,则从缓存中获取分段内容 (If chunk ID and startCursor are provided, get chunk content from cache)
|
|
149
149
|
if (chunkId && startCursor !== undefined) {
|
|
150
150
|
return this.getChunkContent(chunkId, startCursor, contentSizeLimit, debug, COMPONENTS.BROWSER_FETCH);
|
|
@@ -159,13 +159,28 @@ export class BrowserFetcher extends BaseFetcher {
|
|
|
159
159
|
}
|
|
160
160
|
// 获取HTML内容 (Get HTML content)
|
|
161
161
|
const html = result.content[0].text;
|
|
162
|
+
// 处理内容提取 (Process content extraction)
|
|
163
|
+
const { content: processedContent, metadata } = this.processWithContentExtraction(html, url, {
|
|
164
|
+
extractContent,
|
|
165
|
+
includeMetadata,
|
|
166
|
+
fallbackToOriginal
|
|
167
|
+
}, debug, COMPONENTS.BROWSER_FETCH);
|
|
162
168
|
// 检查内容大小并处理 (Check content size and process)
|
|
163
|
-
const chunkingResult = this.handleContentChunking(
|
|
169
|
+
const chunkingResult = this.handleContentChunking(processedContent, contentSizeLimit, enableContentSplitting, debug, COMPONENTS.BROWSER_FETCH);
|
|
164
170
|
if (chunkingResult) {
|
|
171
|
+
// 如果有元数据,添加到分块结果中 (If there is metadata, add it to the chunk result)
|
|
172
|
+
if (metadata) {
|
|
173
|
+
chunkingResult.metadata = metadata;
|
|
174
|
+
}
|
|
165
175
|
return chunkingResult;
|
|
166
176
|
}
|
|
167
|
-
//
|
|
168
|
-
|
|
177
|
+
// 创建新的结果(使用处理后的内容) (Create new result with processed content)
|
|
178
|
+
const processedResult = BaseFetcher.createSuccessResponse(processedContent);
|
|
179
|
+
// 如果有元数据,添加到结果中 (If there is metadata, add it to the result)
|
|
180
|
+
if (metadata) {
|
|
181
|
+
processedResult.metadata = metadata;
|
|
182
|
+
}
|
|
183
|
+
return processedResult;
|
|
169
184
|
}
|
|
170
185
|
catch (error) {
|
|
171
186
|
// 处理错误 (Handle error)
|
|
@@ -180,7 +195,7 @@ export class BrowserFetcher extends BaseFetcher {
|
|
|
180
195
|
* @returns 获取结果 (Fetch result)
|
|
181
196
|
*/
|
|
182
197
|
async json(requestPayload) {
|
|
183
|
-
const { url, debug = false, closeBrowser = false, contentSizeLimit = ContentSizeManager.getDefaultSizeLimit(), enableContentSplitting = true, chunkId, startCursor = 0 } = requestPayload;
|
|
198
|
+
const { url, debug = false, closeBrowser: _closeBrowser = false, contentSizeLimit = ContentSizeManager.getDefaultSizeLimit(), enableContentSplitting = true, chunkId, startCursor = 0 } = requestPayload;
|
|
184
199
|
// 如果提供了分段ID和起始游标,则从缓存中获取分段内容 (If chunk ID and startCursor are provided, get chunk content from cache)
|
|
185
200
|
if (chunkId && startCursor !== undefined) {
|
|
186
201
|
return this.getChunkContent(chunkId, startCursor, contentSizeLimit, debug, COMPONENTS.BROWSER_FETCH);
|
|
@@ -226,7 +241,7 @@ export class BrowserFetcher extends BaseFetcher {
|
|
|
226
241
|
* @returns 获取结果 (Fetch result)
|
|
227
242
|
*/
|
|
228
243
|
async txt(requestPayload) {
|
|
229
|
-
const { url, debug = false, closeBrowser = false, contentSizeLimit = ContentSizeManager.getDefaultSizeLimit(), enableContentSplitting = true, chunkId, startCursor = 0 } = requestPayload;
|
|
244
|
+
const { url, debug = false, closeBrowser: _closeBrowser = false, contentSizeLimit = ContentSizeManager.getDefaultSizeLimit(), enableContentSplitting = true, chunkId, startCursor = 0 } = requestPayload;
|
|
230
245
|
// 如果提供了分段ID和起始游标,则从缓存中获取分段内容 (If chunk ID and startCursor are provided, get chunk content from cache)
|
|
231
246
|
if (chunkId && startCursor !== undefined) {
|
|
232
247
|
return this.getChunkContent(chunkId, startCursor, contentSizeLimit, debug, COMPONENTS.BROWSER_FETCH);
|
|
@@ -266,7 +281,7 @@ export class BrowserFetcher extends BaseFetcher {
|
|
|
266
281
|
* @returns 纯文本响应 (Plain text response)
|
|
267
282
|
*/
|
|
268
283
|
async plainText(requestPayload) {
|
|
269
|
-
const { url, debug = false, closeBrowser = false, contentSizeLimit = ContentSizeManager.getDefaultSizeLimit(), enableContentSplitting = true, chunkId, startCursor = 0 } = requestPayload;
|
|
284
|
+
const { url: _url, debug = false, closeBrowser: _closeBrowser = false, contentSizeLimit = ContentSizeManager.getDefaultSizeLimit(), enableContentSplitting = true, chunkId, startCursor = 0 } = requestPayload;
|
|
270
285
|
// 如果提供了分段ID和起始游标,则从缓存中获取分段内容 (If chunk ID and startCursor are provided, get chunk content from cache)
|
|
271
286
|
if (chunkId && startCursor !== undefined) {
|
|
272
287
|
return this.getChunkContent(chunkId, startCursor, contentSizeLimit, debug, COMPONENTS.BROWSER_FETCH);
|
|
@@ -280,7 +295,7 @@ export class BrowserFetcher extends BaseFetcher {
|
|
|
280
295
|
// 获取HTML内容 (Get HTML content)
|
|
281
296
|
const html = result.content[0].text;
|
|
282
297
|
// 使用ContentProcessor将HTML转换为纯文本 (Use ContentProcessor to convert HTML to plain text)
|
|
283
|
-
const plainText = ContentProcessor.htmlToText(html, debug
|
|
298
|
+
const plainText = ContentProcessor.htmlToText(html, debug);
|
|
284
299
|
// 检查内容大小并处理 (Check content size and process)
|
|
285
300
|
const chunkingResult = this.handleContentChunking(plainText, contentSizeLimit, enableContentSplitting, debug, COMPONENTS.BROWSER_FETCH);
|
|
286
301
|
if (chunkingResult) {
|
|
@@ -295,7 +310,7 @@ export class BrowserFetcher extends BaseFetcher {
|
|
|
295
310
|
* @returns Markdown内容 (Markdown content)
|
|
296
311
|
*/
|
|
297
312
|
async markdown(requestPayload) {
|
|
298
|
-
const { url, debug = false, closeBrowser = false, contentSizeLimit = ContentSizeManager.getDefaultSizeLimit(), enableContentSplitting = true, chunkId, startCursor = 0 } = requestPayload;
|
|
313
|
+
const { url: _url, debug = false, closeBrowser: _closeBrowser = false, contentSizeLimit = ContentSizeManager.getDefaultSizeLimit(), enableContentSplitting = true, chunkId, startCursor = 0 } = requestPayload;
|
|
299
314
|
// 如果提供了分段ID和起始游标,则从缓存中获取分段内容 (If chunk ID and startCursor are provided, get chunk content from cache)
|
|
300
315
|
if (chunkId && startCursor !== undefined) {
|
|
301
316
|
return this.getChunkContent(chunkId, startCursor, contentSizeLimit, debug, COMPONENTS.BROWSER_FETCH);
|
|
@@ -309,7 +324,7 @@ export class BrowserFetcher extends BaseFetcher {
|
|
|
309
324
|
// 获取HTML内容 (Get HTML content)
|
|
310
325
|
const html = result.content[0].text;
|
|
311
326
|
// 使用ContentProcessor将HTML转换为Markdown (Use ContentProcessor to convert HTML to Markdown)
|
|
312
|
-
const markdown = ContentProcessor.htmlToMarkdown(html, debug
|
|
327
|
+
const markdown = ContentProcessor.htmlToMarkdown(html, debug);
|
|
313
328
|
// 检查内容大小并处理 (Check content size and process)
|
|
314
329
|
const chunkingResult = this.handleContentChunking(markdown, contentSizeLimit, enableContentSplitting, debug, COMPONENTS.BROWSER_FETCH);
|
|
315
330
|
if (chunkingResult) {
|
|
@@ -88,75 +88,78 @@ export class BrowserInstance {
|
|
|
88
88
|
// 标记浏览器正在启动 (Mark browser as starting)
|
|
89
89
|
this.browserStarting = true;
|
|
90
90
|
// 创建启动Promise (Create startup Promise)
|
|
91
|
-
this.browserStartPromise = new Promise(
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
'
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
91
|
+
this.browserStartPromise = new Promise((resolve, reject) => {
|
|
92
|
+
// 内部定义并立即调用异步函数
|
|
93
|
+
(async () => {
|
|
94
|
+
try {
|
|
95
|
+
log('browser.startingBrowser', debug, {}, COMPONENTS.BROWSER_FETCH);
|
|
96
|
+
// 准备浏览器启动参数 (Prepare browser startup parameters)
|
|
97
|
+
const launchOptions = {
|
|
98
|
+
headless: 'new',
|
|
99
|
+
args: [
|
|
100
|
+
'--no-sandbox',
|
|
101
|
+
'--disable-setuid-sandbox',
|
|
102
|
+
'--disable-dev-shm-usage',
|
|
103
|
+
'--disable-accelerated-2d-canvas',
|
|
104
|
+
'--no-first-run',
|
|
105
|
+
'--no-zygote',
|
|
106
|
+
'--disable-gpu',
|
|
107
|
+
'--disable-infobars',
|
|
108
|
+
'--window-position=0,0',
|
|
109
|
+
'--ignore-certificate-errors',
|
|
110
|
+
'--ignore-certificate-errors-spki-list',
|
|
111
|
+
'--disable-extensions',
|
|
112
|
+
'--disable-default-apps',
|
|
113
|
+
'--enable-features=NetworkService',
|
|
114
|
+
'--disable-features=IsolateOrigins,site-per-process',
|
|
115
|
+
'--disable-web-security',
|
|
116
|
+
'--disable-site-isolation-trials',
|
|
117
|
+
'--disable-features=BlockInsecurePrivateNetworkRequests',
|
|
118
|
+
'--disable-features=IsolateOrigins',
|
|
119
|
+
'--disable-features=site-per-process',
|
|
120
|
+
'--disable-blink-features=AutomationControlled',
|
|
121
|
+
'--user-agent=' + getRandomUserAgent()
|
|
122
|
+
],
|
|
123
|
+
ignoreHTTPSErrors: true,
|
|
124
|
+
defaultViewport: {
|
|
125
|
+
width: 1920,
|
|
126
|
+
height: 1080
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
// 检查是否有环境变量指定的Chrome路径 (Check if there is a Chrome path specified by environment variables)
|
|
130
|
+
const executablePath = process.env.PUPPETEER_EXECUTABLE_PATH;
|
|
131
|
+
if (executablePath) {
|
|
132
|
+
log('browser.usingCustomChromePath', debug, { path: executablePath }, COMPONENTS.BROWSER_FETCH);
|
|
133
|
+
launchOptions.executablePath = executablePath;
|
|
134
|
+
launchOptions.channel = undefined;
|
|
125
135
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
136
|
+
// 启动浏览器 (Launch browser)
|
|
137
|
+
// @ts-ignore - 忽略puppeteer-extra的类型错误
|
|
138
|
+
this.browser = await puppeteerExtra.launch(launchOptions);
|
|
139
|
+
log('browser.browserStarted', debug, {}, COMPONENTS.BROWSER_FETCH);
|
|
140
|
+
// 设置浏览器关闭事件处理 (Set browser close event handling)
|
|
141
|
+
this.browser.on('disconnected', () => {
|
|
142
|
+
log('browser.browserDisconnected', debug, {}, COMPONENTS.BROWSER_FETCH);
|
|
143
|
+
this.browser = null;
|
|
144
|
+
this.browserStarting = false;
|
|
145
|
+
this.browserStartPromise = null;
|
|
146
|
+
});
|
|
147
|
+
// 重置标志 (Reset flags)
|
|
148
|
+
this.browserStarting = false;
|
|
149
|
+
// 解析Promise (Resolve Promise)
|
|
150
|
+
resolve(this.browser);
|
|
133
151
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
// 设置浏览器关闭事件处理 (Set browser close event handling)
|
|
139
|
-
this.browser.on('disconnected', () => {
|
|
140
|
-
log('browser.browserDisconnected', debug, {}, COMPONENTS.BROWSER_FETCH);
|
|
152
|
+
catch (error) {
|
|
153
|
+
// 记录错误 (Log error)
|
|
154
|
+
log('browser.browserStartError', true, { error: String(error) }, COMPONENTS.BROWSER_FETCH);
|
|
155
|
+
// 重置标志 (Reset flags)
|
|
141
156
|
this.browser = null;
|
|
142
157
|
this.browserStarting = false;
|
|
143
158
|
this.browserStartPromise = null;
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
resolve(this.browser);
|
|
149
|
-
}
|
|
150
|
-
catch (error) {
|
|
151
|
-
// 记录错误 (Log error)
|
|
152
|
-
log('browser.browserStartError', true, { error: String(error) }, COMPONENTS.BROWSER_FETCH);
|
|
153
|
-
// 重置标志 (Reset flags)
|
|
154
|
-
this.browser = null;
|
|
155
|
-
this.browserStarting = false;
|
|
156
|
-
this.browserStartPromise = null;
|
|
157
|
-
// 拒绝Promise (Reject Promise)
|
|
158
|
-
reject(error);
|
|
159
|
-
}
|
|
159
|
+
// 拒绝Promise (Reject Promise)
|
|
160
|
+
reject(error);
|
|
161
|
+
}
|
|
162
|
+
})();
|
|
160
163
|
});
|
|
161
164
|
return this.browserStartPromise;
|
|
162
165
|
}
|
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
* Co-Author: AI Assistant (Claude)
|
|
4
4
|
* Description: This code was collaboratively developed by Martin and AI Assistant.
|
|
5
5
|
*/
|
|
6
|
-
import { log } from
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import { TemplateUtils } from
|
|
6
|
+
import { log } from '../../logger.js';
|
|
7
|
+
import { ChunkManager } from '../../utils/ChunkManager.js';
|
|
8
|
+
import { ContentSizeManager } from '../../utils/ContentSizeManager.js';
|
|
9
|
+
import { TemplateUtils } from '../../utils/TemplateUtils.js';
|
|
10
|
+
import { ContentExtractor } from '../../utils/ContentExtractor.js';
|
|
10
11
|
/**
|
|
11
12
|
* 基础获取器类 (Base fetcher class)
|
|
12
13
|
* 提供所有获取器共用的基础功能 (Provides basic functionality used by all fetchers)
|
|
@@ -80,7 +81,14 @@ export class BaseFetcher {
|
|
|
80
81
|
const estimatedRequests = Math.ceil(remainingBytes / contentSizeLimit);
|
|
81
82
|
// 添加分段提示 (Add chunk prompt)
|
|
82
83
|
const isFirstRequest = true;
|
|
83
|
-
|
|
84
|
+
let chunkWithPrompt;
|
|
85
|
+
// 检查是否已经检索了全部内容
|
|
86
|
+
if (remainingBytes <= 0) {
|
|
87
|
+
chunkWithPrompt = firstChunk + TemplateUtils.generateSizeBasedLastChunkPrompt(fetchedBytes, totalSize, isFirstRequest);
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
chunkWithPrompt = firstChunk + TemplateUtils.generateSizeBasedChunkPrompt(fetchedBytes, totalSize, chunkId, remainingBytes, estimatedRequests, contentSizeLimit, isFirstRequest);
|
|
91
|
+
}
|
|
84
92
|
// 创建响应 (Create response)
|
|
85
93
|
return {
|
|
86
94
|
isError: false,
|
|
@@ -136,12 +144,26 @@ export class BaseFetcher {
|
|
|
136
144
|
}
|
|
137
145
|
// 获取相关信息 (Get related information)
|
|
138
146
|
const { content, fetchedBytes, remainingBytes, isLastChunk, totalBytes } = chunkResult;
|
|
147
|
+
// 记录是否为最后一个分块的详细信息
|
|
148
|
+
log('fetcher.chunkInfo', debug, {
|
|
149
|
+
chunkId,
|
|
150
|
+
fetchedBytes,
|
|
151
|
+
totalBytes,
|
|
152
|
+
remainingBytes,
|
|
153
|
+
isLastChunk,
|
|
154
|
+
percentage: `${Math.round((fetchedBytes / totalBytes) * 100)}%`
|
|
155
|
+
}, component);
|
|
139
156
|
// 计算预计还需要的请求次数 (Calculate estimated number of requests needed)
|
|
140
157
|
const estimatedRequests = Math.ceil(remainingBytes / sizeLimit);
|
|
141
158
|
// 添加分段提示 (Add chunk prompt)
|
|
142
159
|
let contentWithPrompt;
|
|
143
|
-
|
|
144
|
-
|
|
160
|
+
// 如果是最后一个分段或没有剩余内容 (If it's the last chunk or there's no remaining content)
|
|
161
|
+
if (isLastChunk || remainingBytes <= 0) {
|
|
162
|
+
log('fetcher.lastChunkDetected', debug, {
|
|
163
|
+
fetchedBytes,
|
|
164
|
+
totalBytes,
|
|
165
|
+
remainingBytes
|
|
166
|
+
}, component);
|
|
145
167
|
contentWithPrompt = content + TemplateUtils.generateSizeBasedLastChunkPrompt(fetchedBytes, totalBytes, startCursor === 0 // 如果startCursor为0,则为首次请求 (If startCursor is 0, it's the first request)
|
|
146
168
|
);
|
|
147
169
|
}
|
|
@@ -164,7 +186,8 @@ export class BaseFetcher {
|
|
|
164
186
|
totalBytes,
|
|
165
187
|
fetchedBytes,
|
|
166
188
|
remainingBytes,
|
|
167
|
-
hasMoreChunks:
|
|
189
|
+
hasMoreChunks: remainingBytes > 0,
|
|
190
|
+
isLastChunk // 明确添加isLastChunk属性以便客户端使用
|
|
168
191
|
};
|
|
169
192
|
}
|
|
170
193
|
/**
|
|
@@ -184,8 +207,18 @@ export class BaseFetcher {
|
|
|
184
207
|
return response;
|
|
185
208
|
}
|
|
186
209
|
// 使用基于字节的提示 (Use byte-based prompt)
|
|
187
|
-
|
|
188
|
-
)
|
|
210
|
+
let promptText;
|
|
211
|
+
// 检查是否还有剩余字节 (Check if there are remaining bytes)
|
|
212
|
+
if (response.remainingBytes <= 0) {
|
|
213
|
+
// 没有剩余字节,使用最后一块的提示 (No remaining bytes, use last chunk prompt)
|
|
214
|
+
promptText = TemplateUtils.generateSizeBasedLastChunkPrompt(response.fetchedBytes, response.totalBytes, false // 设置为false表示这不是首次请求 (Set to false indicating this is not the first request)
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
// 仍有剩余字节,使用普通分块提示 (Still has remaining bytes, use regular chunk prompt)
|
|
219
|
+
promptText = TemplateUtils.generateSizeBasedChunkPrompt(response.fetchedBytes, response.totalBytes, chunkId, response.remainingBytes, estimatedRequests, currentSizeLimit, false // 设置为false表示这不是首次请求 (Set to false indicating this is not the first request)
|
|
220
|
+
);
|
|
221
|
+
}
|
|
189
222
|
// 创建新的响应对象,避免修改原始对象 (Create new response object to avoid modifying the original)
|
|
190
223
|
return {
|
|
191
224
|
...response,
|
|
@@ -200,4 +233,67 @@ export class BaseFetcher {
|
|
|
200
233
|
// 如果不需要添加提示,返回原始响应 (If no prompt needed, return original response)
|
|
201
234
|
return response;
|
|
202
235
|
}
|
|
236
|
+
/**
|
|
237
|
+
* 处理内容提取 (Process content extraction)
|
|
238
|
+
* 根据参数决定是否提取内容,返回处理后的内容和元数据 (Decide whether to extract content based on parameters, return processed content and metadata)
|
|
239
|
+
* @param html 原始HTML内容 (Original HTML content)
|
|
240
|
+
* @param url 页面URL (Page URL)
|
|
241
|
+
* @param options 内容提取选项 (Content extraction options)
|
|
242
|
+
* @param debug 是否启用调试 (Whether to enable debugging)
|
|
243
|
+
* @param component 组件名称,用于日志 (Component name for logging)
|
|
244
|
+
* @returns 处理后的内容和元数据 (Processed content and metadata)
|
|
245
|
+
*/
|
|
246
|
+
processWithContentExtraction(html, url, options, debug = false, component) {
|
|
247
|
+
const { extractContent = false, includeMetadata = true, fallbackToOriginal = true } = options;
|
|
248
|
+
// 如果不需要提取内容,直接返回原始内容 (If content extraction is not needed, return original content)
|
|
249
|
+
if (!extractContent) {
|
|
250
|
+
return { content: html, metadata: null };
|
|
251
|
+
}
|
|
252
|
+
log('fetcher.extractingContent', debug, { url }, component);
|
|
253
|
+
try {
|
|
254
|
+
// 提取内容 (Extract content)
|
|
255
|
+
const extractResult = ContentExtractor.extractContent(html, url, debug);
|
|
256
|
+
// 如果提取成功且有内容 (If extraction is successful and has content)
|
|
257
|
+
if (extractResult.content) {
|
|
258
|
+
// 准备元数据 (Prepare metadata)
|
|
259
|
+
const metadata = includeMetadata ? {
|
|
260
|
+
title: extractResult.title,
|
|
261
|
+
byline: extractResult.byline,
|
|
262
|
+
siteName: extractResult.siteName,
|
|
263
|
+
excerpt: extractResult.excerpt,
|
|
264
|
+
isReaderable: extractResult.isReaderable,
|
|
265
|
+
length: extractResult.length
|
|
266
|
+
} : null;
|
|
267
|
+
log('fetcher.extractionSuccess', debug, {
|
|
268
|
+
contentLength: extractResult.content.length,
|
|
269
|
+
title: extractResult.title
|
|
270
|
+
}, component);
|
|
271
|
+
// 直接返回处理后的内容 (Directly return the processed content)
|
|
272
|
+
return { content: extractResult.content, metadata };
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
log('fetcher.extractionFailed', debug, { url }, component);
|
|
276
|
+
// 如果提取失败且不允许回退到原始内容 (If extraction failed and fallback is not allowed)
|
|
277
|
+
if (!fallbackToOriginal) {
|
|
278
|
+
log('fetcher.noFallback', debug, {}, component);
|
|
279
|
+
throw new Error("Content extraction failed, and fallback is disabled");
|
|
280
|
+
}
|
|
281
|
+
log('fetcher.usingOriginalContent', debug, {}, component);
|
|
282
|
+
return { content: html, metadata: null };
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
catch (error) {
|
|
286
|
+
log('fetcher.extractionError', debug, {
|
|
287
|
+
error: error instanceof Error ? error.message : String(error),
|
|
288
|
+
url
|
|
289
|
+
}, component);
|
|
290
|
+
// 如果不允许回退到原始内容,则抛出错误 (If fallback is not allowed, throw error)
|
|
291
|
+
if (!fallbackToOriginal) {
|
|
292
|
+
throw error;
|
|
293
|
+
}
|
|
294
|
+
// 回退到原始内容 (Fallback to original content)
|
|
295
|
+
log('fetcher.fallbackToOriginal', debug, {}, component);
|
|
296
|
+
return { content: html, metadata: null };
|
|
297
|
+
}
|
|
298
|
+
}
|
|
203
299
|
}
|
|
@@ -17,6 +17,15 @@ export const BrowserParamsSchema = z.object({
|
|
|
17
17
|
autoDetectMode: z.boolean().optional(),
|
|
18
18
|
closeBrowser: z.boolean().optional(),
|
|
19
19
|
});
|
|
20
|
+
/**
|
|
21
|
+
* 通用提取选项 (Common extraction options)
|
|
22
|
+
* 用于配置内容提取行为 (Used to configure content extraction behavior)
|
|
23
|
+
*/
|
|
24
|
+
export const ContentExtractionOptionsSchema = z.object({
|
|
25
|
+
extractContent: z.boolean().optional(),
|
|
26
|
+
includeMetadata: z.boolean().optional(),
|
|
27
|
+
fallbackToOriginal: z.boolean().optional()
|
|
28
|
+
});
|
|
20
29
|
/**
|
|
21
30
|
* 请求参数模式 (Request parameters schema)
|
|
22
31
|
*/
|
|
@@ -42,4 +51,4 @@ export const RequestPayloadSchema = z.object({
|
|
|
42
51
|
enableContentSplitting: z.boolean().optional(),
|
|
43
52
|
chunkId: z.string().optional(),
|
|
44
53
|
startCursor: z.number().optional(),
|
|
45
|
-
}).merge(BrowserParamsSchema);
|
|
54
|
+
}).merge(BrowserParamsSchema).merge(ContentExtractionOptionsSchema);
|
|
@@ -58,6 +58,7 @@ export class HttpClient {
|
|
|
58
58
|
*/
|
|
59
59
|
static async handleDelay(noDelay, isRedirect, debug) {
|
|
60
60
|
if (!noDelay && isRedirect) {
|
|
61
|
+
log('node.addingRandomDelay', debug, {}, COMPONENTS.NODE_FETCH);
|
|
61
62
|
await randomDelay();
|
|
62
63
|
}
|
|
63
64
|
}
|
|
@@ -202,38 +203,57 @@ export class HttpClient {
|
|
|
202
203
|
log('node.responseStatus', debug, {
|
|
203
204
|
status: response.status,
|
|
204
205
|
statusText: response.statusText,
|
|
205
|
-
|
|
206
|
+
duration: Date.now() - fetchStart,
|
|
206
207
|
}, COMPONENTS.NODE_FETCH);
|
|
207
|
-
//
|
|
208
|
+
// 处理错误响应 (Handle error response)
|
|
209
|
+
if (response.status >= 400) {
|
|
210
|
+
await this.handleErrorResponse(response, debug);
|
|
211
|
+
}
|
|
212
|
+
// 检查是否需要重定向 (Check if redirect is needed)
|
|
208
213
|
if (response.status >= 300 && response.status < 400 && response.headers.has('location')) {
|
|
209
|
-
redirectCount++;
|
|
210
214
|
// 获取重定向URL (Get redirect URL)
|
|
211
215
|
const location = response.headers.get('location');
|
|
212
|
-
log('node.redirectingTo', debug, { location }, COMPONENTS.NODE_FETCH);
|
|
213
|
-
// 构建完整的重定向URL (Build complete redirect URL)
|
|
214
216
|
currentUrl = this.buildRedirectUrl(location, currentUrl, debug);
|
|
217
|
+
// 增加重定向计数 (Increase redirect count)
|
|
218
|
+
redirectCount++;
|
|
219
|
+
// 记录重定向 (Log redirect)
|
|
220
|
+
log('node.redirecting', debug, {
|
|
221
|
+
to: currentUrl,
|
|
222
|
+
redirectCount,
|
|
223
|
+
}, COMPONENTS.NODE_FETCH);
|
|
224
|
+
// 关闭body读取器 (Close body reader)
|
|
225
|
+
await response.text();
|
|
215
226
|
continue;
|
|
216
227
|
}
|
|
217
|
-
//
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
return response;
|
|
221
|
-
}
|
|
222
|
-
else {
|
|
223
|
-
// 处理错误响应 (Handle error response)
|
|
224
|
-
return await this.handleErrorResponse(response, debug);
|
|
225
|
-
}
|
|
228
|
+
// 返回成功响应 (Return successful response)
|
|
229
|
+
clearTimeout(timeoutId);
|
|
230
|
+
return response;
|
|
226
231
|
}
|
|
227
|
-
//
|
|
228
|
-
|
|
232
|
+
// 重定向次数超过限制 (Redirect count exceeds limit)
|
|
233
|
+
const error = new Error(`Too many redirects (${maxRedirects})`);
|
|
234
|
+
error.code = 'EMAXREDIRECTS';
|
|
235
|
+
throw error;
|
|
229
236
|
}
|
|
230
237
|
catch (error) {
|
|
231
|
-
// 处理请求错误 (Handle request error)
|
|
232
|
-
this.handleRequestError(error, fetchStart, timeout, redirectCount, maxRedirects, debug);
|
|
233
|
-
}
|
|
234
|
-
finally {
|
|
235
|
-
// 清除超时定时器 (Clear timeout timer)
|
|
236
238
|
clearTimeout(timeoutId);
|
|
239
|
+
return this.handleRequestError(error, fetchStart, timeout, redirectCount, maxRedirects, debug);
|
|
237
240
|
}
|
|
238
241
|
}
|
|
242
|
+
/**
|
|
243
|
+
* 计算代理通道等待时间 (Calculate proxy channel wait time)
|
|
244
|
+
* @param proxyType 代理类型 (Proxy type)
|
|
245
|
+
* @param proxy 代理服务器 (Proxy server)
|
|
246
|
+
* @param _debug 调试模式 (Debug mode)
|
|
247
|
+
* @returns 等待时间(毫秒) (Wait time in milliseconds)
|
|
248
|
+
*/
|
|
249
|
+
static calculateProxyDelay(proxyType, proxy, _debug) {
|
|
250
|
+
// 根据代理类型和代理服务器地址计算合适的延迟时间
|
|
251
|
+
if (!proxy)
|
|
252
|
+
return 0;
|
|
253
|
+
// 基础延迟根据代理类型不同
|
|
254
|
+
const baseDelay = proxyType === 'http' ? 2000 : 3000;
|
|
255
|
+
// 计算延迟变量 (0-1000ms)
|
|
256
|
+
const variableDelay = Math.floor(Math.random() * 1000);
|
|
257
|
+
return baseDelay + variableDelay;
|
|
258
|
+
}
|
|
239
259
|
}
|
|
@@ -9,6 +9,7 @@ import { ErrorHandler } from '../../utils/ErrorHandler.js';
|
|
|
9
9
|
import { ContentSizeManager } from '../../utils/ContentSizeManager.js';
|
|
10
10
|
import { BaseFetcher } from "../common/BaseFetcher.js";
|
|
11
11
|
import { ContentProcessor } from "../../utils/ContentProcessor.js";
|
|
12
|
+
// import { ContentExtractor } from '../../utils/ContentExtractor.js';
|
|
12
13
|
/**
|
|
13
14
|
* Node模式获取器类 (Node mode fetcher class)
|
|
14
15
|
* 使用node-fetch实现标准模式的网页获取 (Implements webpage fetching in standard mode using node-fetch)
|
|
@@ -20,7 +21,7 @@ export class NodeFetcher extends BaseFetcher {
|
|
|
20
21
|
* @returns HTML内容 (HTML content)
|
|
21
22
|
*/
|
|
22
23
|
async html(requestPayload) {
|
|
23
|
-
const { debug = false, contentSizeLimit = ContentSizeManager.getDefaultSizeLimit(), enableContentSplitting = true, chunkId, startCursor = 0 } = requestPayload;
|
|
24
|
+
const { debug = false, contentSizeLimit = ContentSizeManager.getDefaultSizeLimit(), enableContentSplitting = true, chunkId, startCursor = 0, extractContent = false, includeMetadata = true, fallbackToOriginal = true } = requestPayload;
|
|
24
25
|
// 如果提供了分段ID和起始游标,则从缓存中获取分段内容 (If chunk ID and startCursor are provided, get chunk content from cache)
|
|
25
26
|
if (chunkId && startCursor !== undefined) {
|
|
26
27
|
return this.getChunkContent(chunkId, startCursor, contentSizeLimit, debug, COMPONENTS.NODE_FETCH);
|
|
@@ -33,13 +34,28 @@ export class NodeFetcher extends BaseFetcher {
|
|
|
33
34
|
log('node.readingText', debug, {}, COMPONENTS.NODE_FETCH);
|
|
34
35
|
const html = await response.text();
|
|
35
36
|
log('node.htmlContentLength', debug, { length: html.length }, COMPONENTS.NODE_FETCH);
|
|
37
|
+
// 处理内容提取 (Process content extraction)
|
|
38
|
+
const { content: processedContent, metadata } = this.processWithContentExtraction(html, requestPayload.url, {
|
|
39
|
+
extractContent,
|
|
40
|
+
includeMetadata,
|
|
41
|
+
fallbackToOriginal
|
|
42
|
+
}, debug, COMPONENTS.NODE_FETCH);
|
|
36
43
|
// 检查内容大小并处理 (Check content size and process)
|
|
37
|
-
const chunkingResult = this.handleContentChunking(
|
|
44
|
+
const chunkingResult = this.handleContentChunking(processedContent, contentSizeLimit, enableContentSplitting, debug, COMPONENTS.NODE_FETCH);
|
|
38
45
|
if (chunkingResult) {
|
|
46
|
+
// 如果有元数据,添加到分块结果中 (If there is metadata, add it to the chunk result)
|
|
47
|
+
if (metadata) {
|
|
48
|
+
chunkingResult.metadata = metadata;
|
|
49
|
+
}
|
|
39
50
|
return chunkingResult;
|
|
40
51
|
}
|
|
41
|
-
//
|
|
42
|
-
|
|
52
|
+
// 返回内容 (Return content)
|
|
53
|
+
const result = BaseFetcher.createSuccessResponse(processedContent);
|
|
54
|
+
// 如果有元数据,添加到结果中 (If there is metadata, add it to the result)
|
|
55
|
+
if (metadata) {
|
|
56
|
+
result.metadata = metadata;
|
|
57
|
+
}
|
|
58
|
+
return result;
|
|
43
59
|
}
|
|
44
60
|
catch (error) {
|
|
45
61
|
// 使用ErrorHandler处理错误 (Use ErrorHandler to handle error)
|
|
@@ -151,8 +167,8 @@ export class NodeFetcher extends BaseFetcher {
|
|
|
151
167
|
const response = await HttpClient.fetchWithRedirects(requestPayload);
|
|
152
168
|
// 读取响应文本 (Read response text)
|
|
153
169
|
const html = await response.text();
|
|
154
|
-
//
|
|
155
|
-
const plainText = ContentProcessor.htmlToText(html, debug
|
|
170
|
+
// 将HTML转换为纯文本 (Convert HTML to plain text)
|
|
171
|
+
const plainText = ContentProcessor.htmlToText(html, debug);
|
|
156
172
|
// 检查内容大小并处理 (Check content size and process)
|
|
157
173
|
const chunkingResult = this.handleContentChunking(plainText, contentSizeLimit, enableContentSplitting, debug, COMPONENTS.NODE_FETCH);
|
|
158
174
|
if (chunkingResult) {
|
|
@@ -185,8 +201,8 @@ export class NodeFetcher extends BaseFetcher {
|
|
|
185
201
|
const response = await HttpClient.fetchWithRedirects(requestPayload);
|
|
186
202
|
// 读取响应文本 (Read response text)
|
|
187
203
|
const html = await response.text();
|
|
188
|
-
//
|
|
189
|
-
const markdown = ContentProcessor.htmlToMarkdown(html, debug
|
|
204
|
+
// 将HTML转换为Markdown (Convert HTML to Markdown)
|
|
205
|
+
const markdown = ContentProcessor.htmlToMarkdown(html, debug);
|
|
190
206
|
// 检查内容大小并处理 (Check content size and process)
|
|
191
207
|
const chunkingResult = this.handleContentChunking(markdown, contentSizeLimit, enableContentSplitting, debug, COMPONENTS.NODE_FETCH);
|
|
192
208
|
if (chunkingResult) {
|
|
@@ -63,7 +63,7 @@ const initializeI18n = () => {
|
|
|
63
63
|
saveMissing: false, // 不保存缺失的翻译键 (Don't save missing translation keys)
|
|
64
64
|
keySeparator: false, // 禁用键分隔符,使用完整的键路径 (Disable key separator, use full key path)
|
|
65
65
|
nsSeparator: false, // 禁用命名空间分隔符 (Disable namespace separator)
|
|
66
|
-
missingKeyHandler: (
|
|
66
|
+
missingKeyHandler: (_lng, _ns, _key, _fallbackValue) => {
|
|
67
67
|
// 不输出任何日志,由上层调用决定是否输出 (Don't output any logs, let the upper-level call decide whether to output)
|
|
68
68
|
}
|
|
69
69
|
});
|
|
@@ -89,7 +89,7 @@ export const t = (key, options) => {
|
|
|
89
89
|
const result = i18n.t(key, options);
|
|
90
90
|
return result;
|
|
91
91
|
}
|
|
92
|
-
catch (
|
|
92
|
+
catch (_error) {
|
|
93
93
|
return key; // 出错时返回原始键 (Return the original key when an error occurs)
|
|
94
94
|
}
|
|
95
95
|
};
|
|
@@ -54,6 +54,7 @@ export const CLIENT_KEYS = (() => {
|
|
|
54
54
|
responseStructure: keyGen('responseStructure'),
|
|
55
55
|
parsedByteChunkInfo: keyGen('parsedByteChunkInfo'),
|
|
56
56
|
parsedChunkInfo: keyGen('parsedChunkInfo'),
|
|
57
|
+
chunkInfoParsed: keyGen('chunkInfoParsed'),
|
|
57
58
|
chunkLimitNotice: keyGen('chunkLimitNotice'),
|
|
58
59
|
chunkLimitHint: keyGen('chunkLimitHint'),
|
|
59
60
|
fetchingChunkProgress: keyGen('fetchingChunkProgress'),
|