@customize-agent/knowledge 4.0.18 → 4.0.19

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.
@@ -295,6 +295,18 @@ export class TextChunker {
295
295
  return chunks.flatMap(chunk => this.estimateTokens(chunk) > maxTokens ? this.splitByWindow(chunk, maxTokens, overlapTokens) : [chunk]);
296
296
  }
297
297
  splitByWindow(text, maxTokens, overlapTokens = 0) {
298
+ if (!/[\s\n\r\t。?!;;.!?,、]/u.test(text) && text.length > maxTokens * 3) {
299
+ const maxChars = Math.max(1, maxTokens * 3);
300
+ const overlapChars = Math.max(0, Math.min(Math.floor(maxChars / 2), overlapTokens * 3));
301
+ const step = Math.max(1, maxChars - overlapChars);
302
+ const chunks = [];
303
+ for (let start = 0; start < text.length; start += step) {
304
+ chunks.push(text.slice(start, start + maxChars).trim());
305
+ if (start + maxChars >= text.length)
306
+ break;
307
+ }
308
+ return chunks.filter(Boolean);
309
+ }
298
310
  const tokens = this.tokenizer.encode(text);
299
311
  if (tokens.length <= maxTokens)
300
312
  return [text.trim()].filter(Boolean);
@@ -470,6 +482,8 @@ export class TextChunker {
470
482
  return text.slice(Math.max(0, text.length - chars));
471
483
  }
472
484
  estimateTokens(text) {
485
+ if (!/[\s\n\r\t。?!;;.!?,、]/u.test(text) && text.length > 2000)
486
+ return Math.max(1, Math.ceil(text.length / 3));
473
487
  return Math.max(1, this.tokenizer.countTokens(text));
474
488
  }
475
489
  }
@@ -180,6 +180,7 @@ export declare class KnowledgeBaseManager {
180
180
  private statRelativePaths;
181
181
  private moveUploadedFile;
182
182
  private hasUsableContent;
183
+ private isMetadataOnlyNonBlocking;
183
184
  private defaultUploadRelativePath;
184
185
  private validateUploadRelativePath;
185
186
  private resolveKbRelativePath;
@@ -148,8 +148,9 @@ export class KnowledgeBaseManager {
148
148
  extraction.metadata.textLength = extraction.text.length;
149
149
  if (!this.hasUsableContent(extraction.text, extraction.metadata)) {
150
150
  const reason = extraction.warnings[0] ?? '未解析出可用于模型的正文内容,已跳过向量化';
151
+ const metadataOnly = this.isMetadataOnlyNonBlocking(file, extraction.metadata);
151
152
  diff.skippedFiles.push({ file, reason });
152
- this.updateJobsForFile(file.relativePath, 'ERROR', 100, reason, reason);
153
+ this.updateJobsForFile(file.relativePath, metadataOnly ? 'SUCCESS' : 'ERROR', 100, reason, metadataOnly ? undefined : reason);
153
154
  this.store.upsertRecord({
154
155
  relativePath: file.relativePath,
155
156
  category: file.category,
@@ -161,9 +162,9 @@ export class KnowledgeBaseManager {
161
162
  collectionName,
162
163
  indexedAt: now,
163
164
  lastVerifiedAt: now,
164
- status: 'error',
165
- errorMessage: reason,
166
- metadataJson: JSON.stringify({ mimeType: file.mimeType, warnings: extraction.warnings }),
165
+ status: metadataOnly ? 'active' : 'error',
166
+ errorMessage: metadataOnly ? undefined : reason,
167
+ metadataJson: JSON.stringify({ mimeType: file.mimeType, ...extraction.metadata, warnings: extraction.warnings, metadataOnly }),
167
168
  });
168
169
  continue;
169
170
  }
@@ -973,6 +974,10 @@ export class KnowledgeBaseManager {
973
974
  return false;
974
975
  return text.trim().length > 0;
975
976
  }
977
+ isMetadataOnlyNonBlocking(file, metadata) {
978
+ const coverage = String(metadata.contentCoverage ?? '');
979
+ return file.category === 'image' && ['image_too_small_for_ocr', 'ocr_no_text'].includes(coverage);
980
+ }
976
981
  defaultUploadRelativePath(fileName) {
977
982
  const classification = this.classifier.classifyVirtual(fileName);
978
983
  const configDirs = this.projectConfig?.categoryDirs ?? DEFAULT_CATEGORY_DIRS;
@@ -906,6 +906,15 @@ export class ContentExtractor {
906
906
  metadata.parseError = validationError;
907
907
  return { text: '', metadata, warnings: [`图片文件无效或不完整:${validationError},未入库`] };
908
908
  }
909
+ const dimensions = await this.readImageDimensions(file.absolutePath);
910
+ if (dimensions) {
911
+ metadata.imageWidth = dimensions.width;
912
+ metadata.imageHeight = dimensions.height;
913
+ if (this.isTooSmallForOcr(dimensions.width, dimensions.height)) {
914
+ metadata.contentCoverage = 'image_too_small_for_ocr';
915
+ return { text: this.metadataOnlyText(file), metadata, warnings: [`图片尺寸过小(${dimensions.width}x${dimensions.height}),已跳过 OCR 并仅索引元数据`] };
916
+ }
917
+ }
909
918
  // 1. 尝试外部 PaddleOCR 命令(CUSTOMIZE_PADDLE_OCR_CMD)
910
919
  const paddleExternal = await this.tryPaddleOcrLayout(file.absolutePath);
911
920
  if (paddleExternal) {
@@ -918,10 +927,10 @@ export class ContentExtractor {
918
927
  let imageData;
919
928
  try {
920
929
  imageData = await this.loadImagePixels(file.absolutePath);
930
+ metadata.imageWidth = imageData.width;
931
+ metadata.imageHeight = imageData.height;
921
932
  if (this.isTooSmallForOcr(imageData.width, imageData.height)) {
922
933
  metadata.contentCoverage = 'image_too_small_for_ocr';
923
- metadata.imageWidth = imageData.width;
924
- metadata.imageHeight = imageData.height;
925
934
  return { text: this.metadataOnlyText(file), metadata, warnings: [`图片尺寸过小(${imageData.width}x${imageData.height}),已跳过 OCR 并仅索引元数据`] };
926
935
  }
927
936
  }
@@ -950,7 +959,7 @@ export class ContentExtractor {
950
959
  };
951
960
  }
952
961
  metadata.contentCoverage = 'ocr_no_text';
953
- warnings.push(`${metadata.ocrProvider} 未识别到文字,未入库`);
962
+ warnings.push(`${metadata.ocrProvider} 未识别到文字,未生成可检索文本切片`);
954
963
  return { text: '', metadata, warnings };
955
964
  }
956
965
  catch (e) {
@@ -1101,7 +1110,12 @@ export class ContentExtractor {
1101
1110
  const imgPath = pageImages[i];
1102
1111
  try {
1103
1112
  const dimensions = await this.readImageDimensions(imgPath);
1104
- if (dimensions && this.isTooSmallForOcr(dimensions.width, dimensions.height)) {
1113
+ if (!dimensions) {
1114
+ failedPages.push({ page: i + 1, reason: 'image_dimensions_unavailable' });
1115
+ warnings.push(`PDF 第 ${i + 1} 页渲染图片无法读取尺寸,已跳过 OCR`);
1116
+ continue;
1117
+ }
1118
+ if (this.isTooSmallForOcr(dimensions.width, dimensions.height)) {
1105
1119
  failedPages.push({ page: i + 1, reason: `image_too_small_for_ocr_${dimensions.width}x${dimensions.height}` });
1106
1120
  warnings.push(`PDF 第 ${i + 1} 页渲染图片尺寸过小(${dimensions.width}x${dimensions.height}),已跳过 OCR`);
1107
1121
  continue;
@@ -1109,7 +1123,7 @@ export class ContentExtractor {
1109
1123
  const provider = await getOcrProvider();
1110
1124
  const ocrResult = await provider.recognize({
1111
1125
  data: new Uint8Array(0),
1112
- width: dimensions?.width ?? 0, height: dimensions?.height ?? 0, channels: 0,
1126
+ width: dimensions.width, height: dimensions.height, channels: 0,
1113
1127
  filePath: imgPath,
1114
1128
  });
1115
1129
  const ocrText = this.cleanOcrText(ocrResult.text);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@customize-agent/knowledge",
3
- "version": "4.0.18",
3
+ "version": "4.0.19",
4
4
  "description": "Local knowledge base infrastructure for customize-agent",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",