@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.
- package/dist/chunking/text-chunker.js +14 -0
- package/dist/core/knowledge-base-manager.d.ts +1 -0
- package/dist/core/knowledge-base-manager.js +9 -4
- package/dist/extraction/content-extractor.d.ts +2 -0
- package/dist/extraction/content-extractor.js +44 -2
- package/dist/extraction/ocr-providers.d.ts +2 -0
- package/dist/extraction/ocr-providers.js +24 -0
- package/package.json +1 -1
|
@@ -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:
|
|
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;
|