@customize-agent/knowledge 4.0.10 → 4.0.11
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.
|
@@ -15,8 +15,8 @@ const RECURSIVE_SEPARATORS = [
|
|
|
15
15
|
/\n(?=#{1,6}\s)/u,
|
|
16
16
|
/\n{2,}/u,
|
|
17
17
|
/\n(?=(?:第[一二三四五六七八九十百千万\d]+[章节条]|[一二三四五六七八九十]+、|\d+[.)、]))/u,
|
|
18
|
-
/(?<=[。!?;])\s
|
|
19
|
-
/(?<=[,、])\s
|
|
18
|
+
/(?<=[。!?;])\s+/u,
|
|
19
|
+
/(?<=[,、])\s+/u,
|
|
20
20
|
/\s+/u,
|
|
21
21
|
];
|
|
22
22
|
const LANGUAGE_ROUTER = {
|
|
@@ -45,7 +45,8 @@ export class TextChunker {
|
|
|
45
45
|
return [];
|
|
46
46
|
const config = DEFAULT_CONFIGS[file.category];
|
|
47
47
|
const normalized = this.withHeader(source, file, config);
|
|
48
|
-
const
|
|
48
|
+
const rawCandidates = this.createCandidates(normalized, file, config);
|
|
49
|
+
const candidates = this.enforceCandidateLimit(rawCandidates, config);
|
|
49
50
|
return candidates.map((candidate, index) => this.createChunk(index, candidate, file, metadata));
|
|
50
51
|
}
|
|
51
52
|
createCandidates(text, file, config) {
|
|
@@ -58,7 +59,7 @@ export class TextChunker {
|
|
|
58
59
|
return this.createTextCandidates(text, file.category, config);
|
|
59
60
|
}
|
|
60
61
|
createTextCandidates(text, category, config) {
|
|
61
|
-
const sections = this.splitIntoSections(text, category);
|
|
62
|
+
const sections = this.mergeSmallSections(this.splitIntoSections(text, category), config);
|
|
62
63
|
const candidates = [];
|
|
63
64
|
const titlePaths = this.buildTitlePaths(sections);
|
|
64
65
|
sections.forEach((section, parentIndex) => {
|
|
@@ -215,6 +216,29 @@ export class TextChunker {
|
|
|
215
216
|
return { text: section, startChar, title: this.extractSectionTitle(section) };
|
|
216
217
|
});
|
|
217
218
|
}
|
|
219
|
+
mergeSmallSections(sections, config) {
|
|
220
|
+
const merged = [];
|
|
221
|
+
let current;
|
|
222
|
+
const targetTokens = Math.max(80, Math.floor(config.maxChunkSize * 0.75));
|
|
223
|
+
for (const section of sections) {
|
|
224
|
+
if (!current) {
|
|
225
|
+
current = { ...section };
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
const candidateText = `${current.text}\n\n${section.text}`;
|
|
229
|
+
const currentTokens = this.estimateTokens(current.text);
|
|
230
|
+
if (currentTokens < targetTokens && this.estimateTokens(candidateText) <= config.maxChunkSize) {
|
|
231
|
+
current = { ...current, text: candidateText, title: current.title ?? section.title };
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
merged.push(current);
|
|
235
|
+
current = { ...section };
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
if (current)
|
|
239
|
+
merged.push(current);
|
|
240
|
+
return merged;
|
|
241
|
+
}
|
|
218
242
|
mergeLeadingHeader(sections) {
|
|
219
243
|
if (sections.length < 2)
|
|
220
244
|
return sections;
|
|
@@ -237,7 +261,7 @@ export class TextChunker {
|
|
|
237
261
|
if (!separator)
|
|
238
262
|
return this.splitBySentenceBoundary(text, maxTokens);
|
|
239
263
|
const parts = text.split(separator).map(part => part.trim()).filter(Boolean);
|
|
240
|
-
if (parts.length <= 1)
|
|
264
|
+
if (parts.length <= 1 || parts.some(part => part === text))
|
|
241
265
|
return this.recursiveSplit(text, maxTokens, separatorIndex + 1);
|
|
242
266
|
return parts.flatMap(part => this.recursiveSplit(part, maxTokens, separatorIndex + 1));
|
|
243
267
|
}
|
|
@@ -1063,7 +1063,16 @@ try {
|
|
|
1063
1063
|
if (text.trim()) {
|
|
1064
1064
|
metadata.contentCoverage = 'pdf_text_streams_layout_markdown';
|
|
1065
1065
|
metadata.pdfExtractor = 'pdfjs-dist';
|
|
1066
|
-
|
|
1066
|
+
const markdownText = this.toMarkdownDocument(text);
|
|
1067
|
+
if (!this.shouldAugmentPdfWithOcr(markdownText))
|
|
1068
|
+
return { text: [this.metadataOnlyText(file), markdownText].join('\n\n'), metadata, warnings };
|
|
1069
|
+
const ocr = await this.extractScannedPdfOcr(file);
|
|
1070
|
+
if (ocr.text.trim()) {
|
|
1071
|
+
metadata.contentCoverage = 'pdf_text_streams_plus_ocr';
|
|
1072
|
+
metadata.ocrAugmented = true;
|
|
1073
|
+
return { text: [this.metadataOnlyText(file), markdownText, '## PDF OCR 备用识别文本', ocr.text].join('\n\n'), metadata: { ...metadata, ocr: ocr.metadata }, warnings: [...warnings, ...ocr.warnings] };
|
|
1074
|
+
}
|
|
1075
|
+
return { text: [this.metadataOnlyText(file), markdownText].join('\n\n'), metadata: { ...metadata, ocrRecommended: true, ocrReason: ocr.metadata.ocrReason ?? 'pdf_text_low_quality' }, warnings: [...warnings, ...ocr.warnings] };
|
|
1067
1076
|
}
|
|
1068
1077
|
}
|
|
1069
1078
|
catch (error) {
|
|
@@ -1086,6 +1095,17 @@ try {
|
|
|
1086
1095
|
warnings: [...warnings, 'PDF 正文暂未提取到文本,已索引文件名、路径和类型元数据', ...ocr.warnings],
|
|
1087
1096
|
};
|
|
1088
1097
|
}
|
|
1098
|
+
shouldAugmentPdfWithOcr(text) {
|
|
1099
|
+
const normalizedLength = this.normalizedTextLength(text);
|
|
1100
|
+
if (normalizedLength < 1200)
|
|
1101
|
+
return true;
|
|
1102
|
+
const lines = text.split(/\r?\n/u).map(line => line.trim()).filter(Boolean);
|
|
1103
|
+
if (lines.length === 0)
|
|
1104
|
+
return true;
|
|
1105
|
+
const shortLineRatio = lines.filter(line => line.length <= 12).length / lines.length;
|
|
1106
|
+
const cjkCount = (text.match(/[\p{Script=Han}]/gu) ?? []).length;
|
|
1107
|
+
return shortLineRatio > 0.65 && cjkCount < 1200;
|
|
1108
|
+
}
|
|
1089
1109
|
async extractScannedPdfOcr(file) {
|
|
1090
1110
|
const metadata = {
|
|
1091
1111
|
extractionMode: 'pdf_page_ocr_embedded',
|
|
@@ -1201,22 +1221,23 @@ process.stdout.write(JSON.stringify({ pageCount: doc.numPages, pageLimit, text:
|
|
|
1201
1221
|
// 第二层:pdf-parse(兼容旧版 PDF),与 pdfjs 结果互补,避免单一解析器漏字
|
|
1202
1222
|
try {
|
|
1203
1223
|
const mod = await resolveAndImport('pdf-parse');
|
|
1204
|
-
const pdfParse = mod
|
|
1224
|
+
const pdfParse = typeof mod === 'function'
|
|
1225
|
+
? mod
|
|
1226
|
+
: mod.default;
|
|
1205
1227
|
if (pdfParse) {
|
|
1206
1228
|
const result = await pdfParse(buffer);
|
|
1207
1229
|
const parseText = result.text.trim();
|
|
1208
1230
|
if (pdfjsText && parseText && this.normalizedTextLength(parseText) > this.normalizedTextLength(pdfjsText) * 1.08)
|
|
1209
1231
|
return [pdfjsText, '## PDF 备用解析文本', parseText].join('\n\n');
|
|
1210
|
-
if (pdfjsText)
|
|
1211
|
-
return pdfjsText;
|
|
1212
|
-
if (parseText)
|
|
1232
|
+
if (parseText && !pdfjsText)
|
|
1213
1233
|
return parseText;
|
|
1214
1234
|
}
|
|
1215
1235
|
}
|
|
1216
1236
|
catch {
|
|
1217
|
-
|
|
1218
|
-
return pdfjsText;
|
|
1237
|
+
// pdfjs 结果已可用时忽略备用解析器失败
|
|
1219
1238
|
}
|
|
1239
|
+
if (pdfjsText)
|
|
1240
|
+
return pdfjsText;
|
|
1220
1241
|
// 第三层:raw regex 回退(未压缩的古老 PDF)
|
|
1221
1242
|
const raw = buffer.toString('latin1');
|
|
1222
1243
|
const matches = Array.from(raw.matchAll(/\(([^()]{2,500})\)\s*T[jJ]/gu), match => match[1] ?? '')
|