@customize-agent/knowledge 4.0.17 → 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;
@@ -60,6 +60,8 @@ export declare class ContentExtractor {
60
60
  private cleanOcrText;
61
61
  /** 加载图片像素数据(依赖 sharp) */
62
62
  private loadImagePixels;
63
+ private readImageDimensions;
64
+ private isTooSmallForOcr;
63
65
  private extractPdfText;
64
66
  private normalizedTextLength;
65
67
  private toPdfTextItem;
@@ -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,6 +927,12 @@ 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;
932
+ if (this.isTooSmallForOcr(imageData.width, imageData.height)) {
933
+ metadata.contentCoverage = 'image_too_small_for_ocr';
934
+ return { text: this.metadataOnlyText(file), metadata, warnings: [`图片尺寸过小(${imageData.width}x${imageData.height}),已跳过 OCR 并仅索引元数据`] };
935
+ }
921
936
  }
922
937
  catch (e) {
923
938
  metadata.contentCoverage = 'image_decode_failed';
@@ -944,7 +959,7 @@ export class ContentExtractor {
944
959
  };
945
960
  }
946
961
  metadata.contentCoverage = 'ocr_no_text';
947
- warnings.push(`${metadata.ocrProvider} 未识别到文字,未入库`);
962
+ warnings.push(`${metadata.ocrProvider} 未识别到文字,未生成可检索文本切片`);
948
963
  return { text: '', metadata, warnings };
949
964
  }
950
965
  catch (e) {
@@ -1094,10 +1109,21 @@ export class ContentExtractor {
1094
1109
  for (let i = 0; i < pageImages.length; i++) {
1095
1110
  const imgPath = pageImages[i];
1096
1111
  try {
1112
+ const dimensions = await this.readImageDimensions(imgPath);
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)) {
1119
+ failedPages.push({ page: i + 1, reason: `image_too_small_for_ocr_${dimensions.width}x${dimensions.height}` });
1120
+ warnings.push(`PDF 第 ${i + 1} 页渲染图片尺寸过小(${dimensions.width}x${dimensions.height}),已跳过 OCR`);
1121
+ continue;
1122
+ }
1097
1123
  const provider = await getOcrProvider();
1098
1124
  const ocrResult = await provider.recognize({
1099
1125
  data: new Uint8Array(0),
1100
- width: 0, height: 0, channels: 0,
1126
+ width: dimensions.width, height: dimensions.height, channels: 0,
1101
1127
  filePath: imgPath,
1102
1128
  });
1103
1129
  const ocrText = this.cleanOcrText(ocrResult.text);
@@ -1219,6 +1245,22 @@ export class ContentExtractor {
1219
1245
  const { data, info } = await sharpFn(filePath).raw().toBuffer({ resolveWithObject: true });
1220
1246
  return { data: new Uint8Array(data), width: info.width, height: info.height };
1221
1247
  }
1248
+ async readImageDimensions(filePath) {
1249
+ try {
1250
+ const sharpMod = await resolveAndImport('sharp');
1251
+ const sharpFn = sharpMod.default ?? sharpMod;
1252
+ const metadata = await sharpFn(filePath).metadata();
1253
+ const width = Number(metadata.width ?? 0);
1254
+ const height = Number(metadata.height ?? 0);
1255
+ return width > 0 && height > 0 ? { width, height } : undefined;
1256
+ }
1257
+ catch {
1258
+ return undefined;
1259
+ }
1260
+ }
1261
+ isTooSmallForOcr(width, height) {
1262
+ return width < 8 || height < 8 || width * height < 128;
1263
+ }
1222
1264
  async extractPdfText(buffer) {
1223
1265
  let pdfjsText = '';
1224
1266
  // 第一层:pdfjs-dist 文本提取(处理压缩内容流、CJK 字体、现代 PDF)
@@ -48,6 +48,8 @@ export declare class TesseractJsProvider implements OcrProvider {
48
48
  filePath?: string;
49
49
  }): Promise<OcrResult>;
50
50
  getWarnings(): string[];
51
+ private readImageDimensions;
52
+ private isTooSmallForOcr;
51
53
  private getWorker;
52
54
  private createReusableWorker;
53
55
  dispose(): Promise<void>;
@@ -44,9 +44,14 @@ export class TesseractJsProvider {
44
44
  async recognize(input) {
45
45
  let pngPath;
46
46
  let tmpDir = null;
47
+ let width = input.width;
48
+ let height = input.height;
47
49
  // 如果传了 filePath,直接使用;否则 raw pixels → PNG
48
50
  if (input.filePath && fs.existsSync(input.filePath)) {
49
51
  pngPath = input.filePath;
52
+ const dimensions = await this.readImageDimensions(pngPath);
53
+ width = dimensions?.width ?? width;
54
+ height = dimensions?.height ?? height;
50
55
  }
51
56
  else {
52
57
  const sharpMod = await resolveAndImport('sharp');
@@ -61,6 +66,9 @@ export class TesseractJsProvider {
61
66
  .withMetadata({ density: 288 })
62
67
  .png().toFile(pngPath);
63
68
  }
69
+ if (this.isTooSmallForOcr(width, height)) {
70
+ return { text: '', confidence: 0, regions: [], warnings: [`image too small for OCR: ${width}x${height}`] };
71
+ }
64
72
  try {
65
73
  const worker = await this.getWorker();
66
74
  const result = await worker.recognize(pngPath);
@@ -88,6 +96,22 @@ export class TesseractJsProvider {
88
96
  getWarnings() {
89
97
  return [...new Set(this.warnings)].slice(-20);
90
98
  }
99
+ async readImageDimensions(filePath) {
100
+ try {
101
+ const sharpMod = await resolveAndImport('sharp');
102
+ const sharpFn = sharpMod.default ?? sharpMod;
103
+ const metadata = await sharpFn(filePath).metadata();
104
+ const width = Number(metadata.width ?? 0);
105
+ const height = Number(metadata.height ?? 0);
106
+ return width > 0 && height > 0 ? { width, height } : undefined;
107
+ }
108
+ catch {
109
+ return undefined;
110
+ }
111
+ }
112
+ isTooSmallForOcr(width, height) {
113
+ return width < 8 || height < 8 || width * height < 128;
114
+ }
91
115
  async getWorker() {
92
116
  if (this.worker)
93
117
  return this.worker;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@customize-agent/knowledge",
3
- "version": "4.0.17",
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",