@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.
@@ -37,6 +37,7 @@ export declare class TextChunker {
37
37
  private collectBraceBalancedBlocks;
38
38
  private splitCodeByStructuralFallback;
39
39
  private splitIntoSections;
40
+ private mergeSmallSections;
40
41
  private mergeLeadingHeader;
41
42
  private recursiveSplit;
42
43
  private mergeParts;
@@ -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*/u,
19
- /(?<=[,、])\s*/u,
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 candidates = this.enforceCandidateLimit(this.createCandidates(normalized, file, config), config);
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
  }
@@ -58,6 +58,7 @@ export declare class ContentExtractor {
58
58
  private formatBoundingBox;
59
59
  private validateRasterImage;
60
60
  private extractPdf;
61
+ private shouldAugmentPdfWithOcr;
61
62
  private extractScannedPdfOcr;
62
63
  private extractPdfText;
63
64
  private normalizedTextLength;
@@ -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
- return { text: [this.metadataOnlyText(file), this.toMarkdownDocument(text)].join('\n\n'), metadata, warnings };
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.default;
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
- if (pdfjsText)
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] ?? '')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@customize-agent/knowledge",
3
- "version": "4.0.10",
3
+ "version": "4.0.11",
4
4
  "description": "Local knowledge base infrastructure for customize-agent",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",