@customize-agent/knowledge 4.0.31 → 4.0.33
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/core/knowledge-base-manager.d.ts +1 -0
- package/dist/core/knowledge-base-manager.js +66 -17
- package/dist/embedding/embedding-provider.js +1 -1
- package/dist/extraction/content-extractor.d.ts +5 -1
- package/dist/extraction/content-extractor.js +88 -16
- package/dist/vector/hnsw-vector-store.d.ts +4 -0
- package/dist/vector/hnsw-vector-store.js +65 -12
- package/dist/vector/types.d.ts +1 -0
- package/package.json +4 -2
- package/scripts/install-hnsw.cjs +16 -5
|
@@ -175,6 +175,7 @@ export declare class KnowledgeBaseManager {
|
|
|
175
175
|
private updateJobsForFile;
|
|
176
176
|
private ensureVectorStore;
|
|
177
177
|
private deleteVectorFile;
|
|
178
|
+
private consumePendingVectorRelativePaths;
|
|
178
179
|
private ensureVectorIndexFresh;
|
|
179
180
|
uploadSessionIsOpen(operationId: string): boolean;
|
|
180
181
|
private emptyDiff;
|
|
@@ -91,7 +91,17 @@ export class KnowledgeBaseManager {
|
|
|
91
91
|
const jobs = this.store.listPendingIndexJobs(options.limit ?? 500);
|
|
92
92
|
if (jobs.length === 0)
|
|
93
93
|
return this.emptyDiff();
|
|
94
|
-
|
|
94
|
+
const lightweightJobs = [];
|
|
95
|
+
const heavyJobs = [];
|
|
96
|
+
for (const job of jobs) {
|
|
97
|
+
const ext = path.extname(job.relativePath).toLowerCase();
|
|
98
|
+
if (/\.(pdf|png|jpe?g|webp|gif|bmp|tiff?|xlsx?|xlsm|docx?|pptx?)$/iu.test(ext))
|
|
99
|
+
heavyJobs.push(job);
|
|
100
|
+
else
|
|
101
|
+
lightweightJobs.push(job);
|
|
102
|
+
}
|
|
103
|
+
const selectedJobs = [...lightweightJobs, ...heavyJobs].slice(0, options.limit ?? 500);
|
|
104
|
+
return this.incrementalIndex({ ...options, onlyRelativePaths: selectedJobs.map(job => job.relativePath) });
|
|
95
105
|
}
|
|
96
106
|
async incrementalIndex(options = {}) {
|
|
97
107
|
this.initialize();
|
|
@@ -111,12 +121,25 @@ export class KnowledgeBaseManager {
|
|
|
111
121
|
diff.newFiles = diff.newFiles.filter(file => onlyRelativePaths.has(file.relativePath));
|
|
112
122
|
diff.modifiedFiles = diff.modifiedFiles.filter(file => onlyRelativePaths.has(file.relativePath));
|
|
113
123
|
diff.deletedFiles = diff.deletedFiles.filter(file => onlyRelativePaths.has(file.relativePath));
|
|
114
|
-
diff.hasChanges = diff.newFiles.length + diff.modifiedFiles.length + diff.deletedFiles.length > 0;
|
|
115
124
|
for (const relativePath of onlyRelativePaths) {
|
|
116
125
|
const exists = diff.newFiles.some(file => file.relativePath === relativePath) || diff.modifiedFiles.some(file => file.relativePath === relativePath) || diff.deletedFiles.some(file => file.relativePath === relativePath);
|
|
117
|
-
if (
|
|
118
|
-
|
|
126
|
+
if (exists)
|
|
127
|
+
continue;
|
|
128
|
+
const diskStat = diskFiles.get(relativePath);
|
|
129
|
+
if (!diskStat) {
|
|
130
|
+
this.updateJobsForFile(relativePath, 'ERROR', 100, '待索引文件不存在', '待索引文件不存在');
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
const absolutePath = this.resolveKbRelativePath(relativePath);
|
|
134
|
+
const stat = fs.statSync(absolutePath);
|
|
135
|
+
const classified = this.classifier.classify(absolutePath, relativePath, stat);
|
|
136
|
+
const skipReason = this.classifier.shouldSkip(classified);
|
|
137
|
+
if (skipReason)
|
|
138
|
+
this.updateJobsForFile(relativePath, 'ERROR', 100, skipReason, skipReason);
|
|
139
|
+
else
|
|
140
|
+
diff.modifiedFiles.push(classified);
|
|
119
141
|
}
|
|
142
|
+
diff.hasChanges = diff.newFiles.length + diff.modifiedFiles.length + diff.deletedFiles.length > 0;
|
|
120
143
|
}
|
|
121
144
|
for (const deleted of diff.deletedFiles) {
|
|
122
145
|
await this.deleteVectorFile(deleted.collectionName, deleted.relativePath);
|
|
@@ -263,8 +286,13 @@ export class KnowledgeBaseManager {
|
|
|
263
286
|
this.store.setMetadata('total_files_indexed', String(stats.fileCount));
|
|
264
287
|
const hasIndexChanges = diff.newFiles.length + diff.modifiedFiles.length + diff.deletedFiles.length > 0;
|
|
265
288
|
if (options.vectorMode === 'defer') {
|
|
266
|
-
if (hasIndexChanges)
|
|
289
|
+
if (hasIndexChanges) {
|
|
267
290
|
this.store.setMetadata('vector_index_status', 'pending');
|
|
291
|
+
const pending = new Set(this.consumePendingVectorRelativePaths());
|
|
292
|
+
for (const relativePath of vectorRelativePaths)
|
|
293
|
+
pending.add(relativePath);
|
|
294
|
+
this.store.setMetadata('vector_pending_relative_paths', JSON.stringify([...pending]));
|
|
295
|
+
}
|
|
268
296
|
this.reportProgress({ stage: 'vectorizing', percent: 85, message: '解析和切片已完成,向量入库转入后台/稍后执行', chunkCount: stats.chunkCount, vectorStatus: this.getVectorStatus() });
|
|
269
297
|
}
|
|
270
298
|
else {
|
|
@@ -474,10 +502,6 @@ export class KnowledgeBaseManager {
|
|
|
474
502
|
const targetPath = this.resolveKbRelativePath(relativePath);
|
|
475
503
|
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
476
504
|
fs.writeFileSync(targetPath, file.content);
|
|
477
|
-
const record = this.store.listRecords().find(item => item.relativePath === relativePath);
|
|
478
|
-
if (record)
|
|
479
|
-
await this.deleteVectorFile(record.collectionName, relativePath);
|
|
480
|
-
this.store.deleteRecord(relativePath);
|
|
481
505
|
jobs.push(this.store.enqueueIndexJob({ id: `${operationId}-${index}`, relativePath, message: '文件已落盘,等待后台解析' }));
|
|
482
506
|
}
|
|
483
507
|
return jobs;
|
|
@@ -492,10 +516,6 @@ export class KnowledgeBaseManager {
|
|
|
492
516
|
const targetPath = this.resolveKbRelativePath(relativePath);
|
|
493
517
|
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
494
518
|
this.moveUploadedFile(file.sourcePath, targetPath);
|
|
495
|
-
const record = this.store.listRecords().find(item => item.relativePath === relativePath);
|
|
496
|
-
if (record)
|
|
497
|
-
await this.deleteVectorFile(record.collectionName, relativePath);
|
|
498
|
-
this.store.deleteRecord(relativePath);
|
|
499
519
|
jobs.push(this.store.enqueueIndexJob({ id: `${operationId}-${offset + index}`, relativePath, message: '文件已落盘,等待后台解析' }));
|
|
500
520
|
}
|
|
501
521
|
return jobs;
|
|
@@ -543,6 +563,11 @@ export class KnowledgeBaseManager {
|
|
|
543
563
|
return this.store.listIgnoreRules();
|
|
544
564
|
}
|
|
545
565
|
async indexVectors(options = {}) {
|
|
566
|
+
const pendingRelativePaths = !options.rebuild && !options.relativePath && !options.relativePaths?.length
|
|
567
|
+
? this.consumePendingVectorRelativePaths()
|
|
568
|
+
: [];
|
|
569
|
+
if (pendingRelativePaths.length > 0)
|
|
570
|
+
options = { ...options, relativePaths: pendingRelativePaths };
|
|
546
571
|
const chunks = options.relativePaths?.length
|
|
547
572
|
? options.relativePaths.flatMap(relativePath => this.store.listChunks({ collectionName: options.collectionName, relativePath }))
|
|
548
573
|
: this.store.listChunks(options);
|
|
@@ -555,9 +580,15 @@ export class KnowledgeBaseManager {
|
|
|
555
580
|
await this.vectorStores.get(collectionName)?.clearCollection?.();
|
|
556
581
|
}
|
|
557
582
|
else {
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
583
|
+
const cleanupRelativePaths = options.relativePaths ?? [options.relativePath].filter(Boolean);
|
|
584
|
+
for (const collectionName of cleanupCollectionNames) {
|
|
585
|
+
const vectorStore = this.vectorStores.get(collectionName);
|
|
586
|
+
if (vectorStore?.deleteByFilePaths)
|
|
587
|
+
await vectorStore.deleteByFilePaths(cleanupRelativePaths, { persist: false });
|
|
588
|
+
else {
|
|
589
|
+
for (const relativePath of cleanupRelativePaths)
|
|
590
|
+
await vectorStore?.deleteByFilePath(relativePath, { persist: false });
|
|
591
|
+
}
|
|
561
592
|
}
|
|
562
593
|
if (chunks.length === 0) {
|
|
563
594
|
for (const collectionName of cleanupCollectionNames)
|
|
@@ -906,6 +937,22 @@ export class KnowledgeBaseManager {
|
|
|
906
937
|
this.store.setMetadata('vector_index_error', error instanceof Error ? error.message : String(error));
|
|
907
938
|
}
|
|
908
939
|
}
|
|
940
|
+
consumePendingVectorRelativePaths() {
|
|
941
|
+
const raw = this.store.getMetadata('vector_pending_relative_paths');
|
|
942
|
+
if (!raw)
|
|
943
|
+
return [];
|
|
944
|
+
try {
|
|
945
|
+
const parsed = JSON.parse(raw);
|
|
946
|
+
if (!Array.isArray(parsed))
|
|
947
|
+
return [];
|
|
948
|
+
this.store.setMetadata('vector_pending_relative_paths', '');
|
|
949
|
+
return [...new Set(parsed.filter((item) => typeof item === 'string' && item.trim().length > 0))];
|
|
950
|
+
}
|
|
951
|
+
catch {
|
|
952
|
+
this.store.setMetadata('vector_pending_relative_paths', '');
|
|
953
|
+
return [];
|
|
954
|
+
}
|
|
955
|
+
}
|
|
909
956
|
async ensureVectorIndexFresh(chunkCount, options = {}) {
|
|
910
957
|
if (chunkCount === 0)
|
|
911
958
|
return;
|
|
@@ -925,7 +972,7 @@ export class KnowledgeBaseManager {
|
|
|
925
972
|
this.store.setMetadata('last_vector_index_at', String(Date.now()));
|
|
926
973
|
return;
|
|
927
974
|
}
|
|
928
|
-
if (changedRelativePaths.length > 0
|
|
975
|
+
if (changedRelativePaths.length > 0) {
|
|
929
976
|
for (const collectionName of options.changedCollectionNames ?? [])
|
|
930
977
|
this.ensureVectorStore(collectionName);
|
|
931
978
|
await this.indexVectors({ relativePaths: changedRelativePaths, cleanupCollectionNames: options.changedCollectionNames });
|
|
@@ -933,6 +980,8 @@ export class KnowledgeBaseManager {
|
|
|
933
980
|
}
|
|
934
981
|
if (indexedChunks === chunkCount && status === 'ready')
|
|
935
982
|
return;
|
|
983
|
+
if (status === 'pending' || status === 'partial')
|
|
984
|
+
return;
|
|
936
985
|
await this.indexVectors({ rebuild: true });
|
|
937
986
|
}
|
|
938
987
|
uploadSessionIsOpen(operationId) {
|
|
@@ -84,7 +84,7 @@ export class OpenAICompatibleEmbeddingProvider {
|
|
|
84
84
|
}
|
|
85
85
|
function resolveLocalEmbeddingBatchSize(configured) {
|
|
86
86
|
const raw = configured ?? Number(process.env.CUSTOMIZE_EMBEDDING_BATCH_SIZE ?? process.env.KB_EMBEDDING_BATCH_SIZE);
|
|
87
|
-
const fallback = process.platform === 'win32' ? 8 :
|
|
87
|
+
const fallback = process.platform === 'win32' ? 8 : 32;
|
|
88
88
|
if (!Number.isFinite(raw) || raw <= 0)
|
|
89
89
|
return fallback;
|
|
90
90
|
return Math.max(1, Math.min(128, Math.floor(raw)));
|
|
@@ -52,11 +52,15 @@ export declare class ContentExtractor {
|
|
|
52
52
|
private formatBoundingBox;
|
|
53
53
|
private validateRasterImage;
|
|
54
54
|
private extractPdf;
|
|
55
|
+
private hasUsablePdfText;
|
|
55
56
|
private extractPdfHybridPages;
|
|
56
|
-
/** PyMuPDF
|
|
57
|
+
/** PyMuPDF 渲染(默认 200 DPI,低质量页可自适应提高) */
|
|
57
58
|
private tryRenderWithPyMuPDF;
|
|
58
59
|
/** pdfjs-dist + canvas 渲染(降级方案) */
|
|
59
60
|
private tryRenderWithPdfJs;
|
|
61
|
+
private getPdfOcrDpi;
|
|
62
|
+
private getPdfOcrRetryDpi;
|
|
63
|
+
private shouldRetryPdfOcrAtHigherDpi;
|
|
60
64
|
private scoreOcrText;
|
|
61
65
|
private cleanOcrText;
|
|
62
66
|
/** 加载图片像素数据(依赖 sharp) */
|
|
@@ -1029,24 +1029,27 @@ export class ContentExtractor {
|
|
|
1029
1029
|
return undefined;
|
|
1030
1030
|
}
|
|
1031
1031
|
async extractPdf(file) {
|
|
1032
|
-
const
|
|
1033
|
-
|
|
1034
|
-
return hybrid;
|
|
1035
|
-
const metadata = { extractionMode: 'pdf_text', vectorizable: true };
|
|
1036
|
-
const warnings = [...hybrid.warnings];
|
|
1032
|
+
const metadata = { extractionMode: 'pdf_text_first', vectorizable: true };
|
|
1033
|
+
const warnings = [];
|
|
1037
1034
|
try {
|
|
1038
1035
|
const raw = fs.readFileSync(file.absolutePath);
|
|
1039
1036
|
const text = await this.extractPdfText(raw);
|
|
1040
|
-
if (
|
|
1037
|
+
if (this.hasUsablePdfText(text)) {
|
|
1041
1038
|
metadata.contentCoverage = 'pdf_text_streams_layout_markdown';
|
|
1042
1039
|
metadata.pdfExtractor = 'pdfjs-dist';
|
|
1040
|
+
metadata.ocrSkippedReason = 'pdf_text_stream_quality_sufficient';
|
|
1043
1041
|
return { text: [this.metadataOnlyText(file), this.toMarkdownDocument(text)].join('\n\n'), metadata, warnings };
|
|
1044
1042
|
}
|
|
1043
|
+
if (text.trim())
|
|
1044
|
+
warnings.push('PDF 文本层质量不足,已尝试选择性 OCR 增强');
|
|
1045
1045
|
}
|
|
1046
1046
|
catch (error) {
|
|
1047
1047
|
warnings.push(`PDF 文本提取失败: ${error instanceof Error ? error.message : String(error)}`);
|
|
1048
1048
|
metadata.parseError = error instanceof Error ? error.message : String(error);
|
|
1049
1049
|
}
|
|
1050
|
+
const hybrid = await this.extractPdfHybridPages(file);
|
|
1051
|
+
if (hybrid.text.trim())
|
|
1052
|
+
return { text: hybrid.text, metadata: { ...metadata, ...hybrid.metadata }, warnings: [...warnings, ...hybrid.warnings] };
|
|
1050
1053
|
metadata.extractionMode = 'pdf_metadata_only';
|
|
1051
1054
|
metadata.contentCoverage = 'metadata_filename';
|
|
1052
1055
|
metadata.ocrRecommended = true;
|
|
@@ -1055,9 +1058,17 @@ export class ContentExtractor {
|
|
|
1055
1058
|
return {
|
|
1056
1059
|
text: this.metadataOnlyText(file),
|
|
1057
1060
|
metadata,
|
|
1058
|
-
warnings: [...warnings, 'PDF 正文暂未提取到文本,已索引文件名、路径和类型元数据'],
|
|
1061
|
+
warnings: [...warnings, ...hybrid.warnings, 'PDF 正文暂未提取到文本,已索引文件名、路径和类型元数据'],
|
|
1059
1062
|
};
|
|
1060
1063
|
}
|
|
1064
|
+
hasUsablePdfText(text) {
|
|
1065
|
+
const normalized = text.replace(/\s+/gu, ' ').trim();
|
|
1066
|
+
if (normalized.length < Number(process.env.CUSTOMIZE_KB_PDF_TEXT_MIN_CHARS || 80))
|
|
1067
|
+
return false;
|
|
1068
|
+
const replacementRatio = (normalized.match(/[\uFFFD�]/gu)?.length ?? 0) / normalized.length;
|
|
1069
|
+
const visibleRatio = (normalized.match(/[\p{L}\p{N}\p{Script=Han}]/gu)?.length ?? 0) / normalized.length;
|
|
1070
|
+
return replacementRatio < 0.02 && visibleRatio > 0.35;
|
|
1071
|
+
}
|
|
1061
1072
|
async extractPdfHybridPages(file) {
|
|
1062
1073
|
const metadata = {
|
|
1063
1074
|
extractionMode: 'pdf_hybrid_pages',
|
|
@@ -1074,17 +1085,37 @@ export class ContentExtractor {
|
|
|
1074
1085
|
};
|
|
1075
1086
|
const failedPages = [];
|
|
1076
1087
|
const ocrPages = [];
|
|
1088
|
+
const ocrRetryPages = [];
|
|
1077
1089
|
const ocrStrategies = [];
|
|
1078
|
-
// 尝试 PyMuPDF
|
|
1090
|
+
// 尝试 PyMuPDF 渲染(默认 200 DPI),低质量页自动升到 300 DPI 重试
|
|
1079
1091
|
let pageImages;
|
|
1092
|
+
let highDpiImages = null;
|
|
1080
1093
|
let pageCount = 0;
|
|
1081
1094
|
let renderer = 'unknown';
|
|
1095
|
+
const initialDpi = this.getPdfOcrDpi();
|
|
1096
|
+
const retryDpi = this.getPdfOcrRetryDpi(initialDpi);
|
|
1082
1097
|
const tmpDir = fs.mkdtempSync(path.join(this.getTempRoot(), 'kb-pdf-'));
|
|
1098
|
+
metadata.pdfOcrInitialDpi = initialDpi;
|
|
1099
|
+
if (retryDpi > initialDpi)
|
|
1100
|
+
metadata.pdfOcrRetryDpi = retryDpi;
|
|
1101
|
+
const getHighDpiImage = (pageIndex) => {
|
|
1102
|
+
if (retryDpi <= initialDpi)
|
|
1103
|
+
return undefined;
|
|
1104
|
+
if (!highDpiImages) {
|
|
1105
|
+
const retryDir = path.join(tmpDir, `retry-${retryDpi}dpi`);
|
|
1106
|
+
fs.mkdirSync(retryDir, { recursive: true });
|
|
1107
|
+
highDpiImages = this.tryRenderWithPyMuPDF(file.absolutePath, retryDir, retryDpi);
|
|
1108
|
+
if (!highDpiImages?.length)
|
|
1109
|
+
warnings.push(`PDF 高质量 OCR 重试渲染失败(${retryDpi} DPI)`);
|
|
1110
|
+
}
|
|
1111
|
+
const imagePath = highDpiImages?.[pageIndex];
|
|
1112
|
+
return imagePath ? { imagePath, strategy: `PyMuPDF-${retryDpi}dpi` } : undefined;
|
|
1113
|
+
};
|
|
1083
1114
|
try {
|
|
1084
|
-
// ── 方法1: PyMuPDF
|
|
1085
|
-
pageImages = this.tryRenderWithPyMuPDF(file.absolutePath, tmpDir);
|
|
1115
|
+
// ── 方法1: PyMuPDF(默认 200 DPI,低质量页再自适应升到 300 DPI) ──
|
|
1116
|
+
pageImages = this.tryRenderWithPyMuPDF(file.absolutePath, tmpDir, initialDpi);
|
|
1086
1117
|
if (pageImages && pageImages.length > 0) {
|
|
1087
|
-
renderer =
|
|
1118
|
+
renderer = `PyMuPDF-${initialDpi}dpi`;
|
|
1088
1119
|
pageCount = pageImages.length;
|
|
1089
1120
|
}
|
|
1090
1121
|
else {
|
|
@@ -1131,12 +1162,37 @@ export class ContentExtractor {
|
|
|
1131
1162
|
width: dimensions.width, height: dimensions.height, channels: 0,
|
|
1132
1163
|
filePath: imgPath,
|
|
1133
1164
|
});
|
|
1134
|
-
|
|
1165
|
+
let ocrText = this.cleanOcrText(ocrResult.text);
|
|
1166
|
+
let ocrScore = this.scoreOcrText(ocrText);
|
|
1167
|
+
let strategy = renderer;
|
|
1135
1168
|
if (ocrResult.warnings?.length)
|
|
1136
1169
|
warnings.push(...ocrResult.warnings.map(item => `OCR 警告: ${item}`));
|
|
1170
|
+
if (this.shouldRetryPdfOcrAtHigherDpi(ocrText, ocrScore)) {
|
|
1171
|
+
const retry = getHighDpiImage(i);
|
|
1172
|
+
if (retry) {
|
|
1173
|
+
const retryDimensions = await this.readImageDimensions(retry.imagePath);
|
|
1174
|
+
if (retryDimensions && !this.isTooSmallForOcr(retryDimensions.width, retryDimensions.height)) {
|
|
1175
|
+
const retryResult = await provider.recognize({
|
|
1176
|
+
data: new Uint8Array(0),
|
|
1177
|
+
width: retryDimensions.width, height: retryDimensions.height, channels: 0,
|
|
1178
|
+
filePath: retry.imagePath,
|
|
1179
|
+
});
|
|
1180
|
+
const retryText = this.cleanOcrText(retryResult.text);
|
|
1181
|
+
const retryScore = this.scoreOcrText(retryText);
|
|
1182
|
+
if (retryResult.warnings?.length)
|
|
1183
|
+
warnings.push(...retryResult.warnings.map(item => `OCR 重试警告: ${item}`));
|
|
1184
|
+
if (retryScore > ocrScore || (!ocrText && retryText)) {
|
|
1185
|
+
ocrText = retryText;
|
|
1186
|
+
ocrScore = retryScore;
|
|
1187
|
+
strategy = retry.strategy;
|
|
1188
|
+
ocrRetryPages.push(i + 1);
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1137
1193
|
if (ocrText) {
|
|
1138
1194
|
ocrPages.push(i + 1);
|
|
1139
|
-
ocrStrategies.push({ page: i + 1, strategy
|
|
1195
|
+
ocrStrategies.push({ page: i + 1, strategy, score: ocrScore });
|
|
1140
1196
|
pageTexts.push(`## PDF 第 ${i + 1} 页(OCR)\n\n${ocrText}`);
|
|
1141
1197
|
}
|
|
1142
1198
|
else {
|
|
@@ -1158,6 +1214,7 @@ export class ContentExtractor {
|
|
|
1158
1214
|
}
|
|
1159
1215
|
metadata.ocrAugmented = ocrPages.length > 0;
|
|
1160
1216
|
metadata.ocrPages = ocrPages;
|
|
1217
|
+
metadata.ocrRetryPages = ocrRetryPages;
|
|
1161
1218
|
metadata.ocrStrategies = ocrStrategies;
|
|
1162
1219
|
metadata.failedPages = failedPages;
|
|
1163
1220
|
metadata.ocrProvider = ocrProvider?.id ?? 'unknown';
|
|
@@ -1171,11 +1228,11 @@ export class ContentExtractor {
|
|
|
1171
1228
|
warnings,
|
|
1172
1229
|
};
|
|
1173
1230
|
}
|
|
1174
|
-
/** PyMuPDF
|
|
1175
|
-
tryRenderWithPyMuPDF(pdfPath, outputDir) {
|
|
1231
|
+
/** PyMuPDF 渲染(默认 200 DPI,低质量页可自适应提高) */
|
|
1232
|
+
tryRenderWithPyMuPDF(pdfPath, outputDir, dpi = this.getPdfOcrDpi()) {
|
|
1176
1233
|
try {
|
|
1177
1234
|
const workerScript = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', '..', 'scripts', 'render_pdf_pages.py');
|
|
1178
|
-
spawnSync('python3', [workerScript, pdfPath, outputDir,
|
|
1235
|
+
spawnSync('python3', [workerScript, pdfPath, outputDir, String(dpi)], {
|
|
1179
1236
|
encoding: 'utf-8', timeout: 60_000, maxBuffer: 1024 * 1024,
|
|
1180
1237
|
});
|
|
1181
1238
|
// 检查输出文件(即使 Python 非零退出码也可能已渲染部分页面)
|
|
@@ -1226,6 +1283,21 @@ export class ContentExtractor {
|
|
|
1226
1283
|
return null;
|
|
1227
1284
|
}
|
|
1228
1285
|
}
|
|
1286
|
+
getPdfOcrDpi() {
|
|
1287
|
+
return Math.max(120, Math.min(300, Number(process.env.CUSTOMIZE_KB_PDF_OCR_DPI || 200)));
|
|
1288
|
+
}
|
|
1289
|
+
getPdfOcrRetryDpi(initialDpi) {
|
|
1290
|
+
const configured = Number(process.env.CUSTOMIZE_KB_PDF_OCR_RETRY_DPI || 300);
|
|
1291
|
+
return Math.max(initialDpi, Math.min(300, Math.max(120, configured)));
|
|
1292
|
+
}
|
|
1293
|
+
shouldRetryPdfOcrAtHigherDpi(text, score) {
|
|
1294
|
+
const normalizedLength = this.normalizedTextLength(text);
|
|
1295
|
+
if (normalizedLength === 0)
|
|
1296
|
+
return true;
|
|
1297
|
+
const threshold = Number(process.env.CUSTOMIZE_KB_PDF_OCR_RETRY_MIN_SCORE || 120);
|
|
1298
|
+
const replacementRatio = (text.match(/[�□]/gu)?.length ?? 0) / Math.max(1, text.length);
|
|
1299
|
+
return score < threshold || replacementRatio > 0.02;
|
|
1300
|
+
}
|
|
1229
1301
|
scoreOcrText(value) {
|
|
1230
1302
|
const text = String(value ?? '').trim();
|
|
1231
1303
|
const normalizedLength = this.normalizedTextLength(text);
|
|
@@ -9,11 +9,13 @@ export declare class HNSWVectorStore implements VectorStoreInterface {
|
|
|
9
9
|
private deletedSinceRebuild;
|
|
10
10
|
private dirty;
|
|
11
11
|
private readonly documents;
|
|
12
|
+
private readonly rowidsByFilePath;
|
|
12
13
|
constructor(collectionName: string, indexPath: string, dimensions?: number, maxElements?: number);
|
|
13
14
|
ensureCollection(): Promise<void>;
|
|
14
15
|
upsert(documents: VectorDocument[], options?: VectorWriteOptions): Promise<void>;
|
|
15
16
|
clearCollection(): Promise<void>;
|
|
16
17
|
deleteByFilePath(filePath: string, options?: VectorWriteOptions): Promise<void>;
|
|
18
|
+
deleteByFilePaths(filePaths: string[], options?: VectorWriteOptions): Promise<void>;
|
|
17
19
|
flush(): Promise<void>;
|
|
18
20
|
needsRebuild(): boolean;
|
|
19
21
|
search(query: VectorSearchQuery): Promise<VectorSearchResult[]>;
|
|
@@ -21,5 +23,7 @@ export declare class HNSWVectorStore implements VectorStoreInterface {
|
|
|
21
23
|
private persist;
|
|
22
24
|
private toStoredDocument;
|
|
23
25
|
private loadDocuments;
|
|
26
|
+
private trackDocumentFilePath;
|
|
27
|
+
private untrackDocumentFilePath;
|
|
24
28
|
private metadataPath;
|
|
25
29
|
}
|
|
@@ -12,6 +12,7 @@ export class HNSWVectorStore {
|
|
|
12
12
|
deletedSinceRebuild = 0;
|
|
13
13
|
dirty = false;
|
|
14
14
|
documents = new Map();
|
|
15
|
+
rowidsByFilePath = new Map();
|
|
15
16
|
constructor(collectionName, indexPath, dimensions = 512, maxElements = 500_000) {
|
|
16
17
|
this.collectionName = collectionName;
|
|
17
18
|
this.indexPath = indexPath;
|
|
@@ -36,8 +37,13 @@ export class HNSWVectorStore {
|
|
|
36
37
|
const rowid = Number(document.metadata.sqlite_rowid);
|
|
37
38
|
if (!Number.isFinite(rowid) || rowid <= 0)
|
|
38
39
|
throw new Error(`HNSW 向量写入缺少有效 sqlite_rowid: ${document.id}`);
|
|
40
|
+
const existing = this.documents.get(rowid);
|
|
41
|
+
if (existing)
|
|
42
|
+
this.untrackDocumentFilePath(rowid, existing);
|
|
39
43
|
this.index.addPoint(document.embedding, rowid, true);
|
|
40
|
-
|
|
44
|
+
const stored = this.toStoredDocument(document);
|
|
45
|
+
this.documents.set(rowid, stored);
|
|
46
|
+
this.trackDocumentFilePath(rowid, stored);
|
|
41
47
|
this.dirty = true;
|
|
42
48
|
}
|
|
43
49
|
if (options.persist !== false)
|
|
@@ -49,24 +55,43 @@ export class HNSWVectorStore {
|
|
|
49
55
|
if (fs.existsSync(this.metadataPath()))
|
|
50
56
|
fs.rmSync(this.metadataPath(), { force: true });
|
|
51
57
|
this.documents.clear();
|
|
58
|
+
this.rowidsByFilePath.clear();
|
|
52
59
|
this.deletedSinceRebuild = 0;
|
|
53
60
|
this.dirty = false;
|
|
54
61
|
this.index = undefined;
|
|
55
62
|
await this.ensureCollection();
|
|
56
63
|
}
|
|
57
64
|
async deleteByFilePath(filePath, options = {}) {
|
|
65
|
+
await this.deleteByFilePaths([filePath], options);
|
|
66
|
+
}
|
|
67
|
+
async deleteByFilePaths(filePaths, options = {}) {
|
|
58
68
|
await this.ensureCollection();
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
catch { /* 忽略缺失的标签 */ }
|
|
66
|
-
this.documents.delete(rowid);
|
|
67
|
-
this.dirty = true;
|
|
69
|
+
const rowids = new Set();
|
|
70
|
+
for (const filePath of filePaths) {
|
|
71
|
+
const tracked = this.rowidsByFilePath.get(filePath);
|
|
72
|
+
if (tracked) {
|
|
73
|
+
for (const rowid of tracked)
|
|
74
|
+
rowids.add(rowid);
|
|
68
75
|
}
|
|
69
76
|
}
|
|
77
|
+
if (rowids.size === 0) {
|
|
78
|
+
if (options.persist !== false && this.dirty)
|
|
79
|
+
this.persist();
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
for (const rowid of rowids) {
|
|
83
|
+
const document = this.documents.get(rowid);
|
|
84
|
+
if (!document)
|
|
85
|
+
continue;
|
|
86
|
+
try {
|
|
87
|
+
this.index.markDelete(rowid);
|
|
88
|
+
this.deletedSinceRebuild += 1;
|
|
89
|
+
}
|
|
90
|
+
catch { /* 忽略缺失的标签 */ }
|
|
91
|
+
this.documents.delete(rowid);
|
|
92
|
+
this.untrackDocumentFilePath(rowid, document);
|
|
93
|
+
this.dirty = true;
|
|
94
|
+
}
|
|
70
95
|
if (options.persist !== false)
|
|
71
96
|
this.persist();
|
|
72
97
|
}
|
|
@@ -122,12 +147,40 @@ export class HNSWVectorStore {
|
|
|
122
147
|
const entries = Array.isArray(parsed) ? parsed : parsed.documents ?? [];
|
|
123
148
|
this.deletedSinceRebuild = Array.isArray(parsed) ? 0 : Number(parsed.deletedSinceRebuild ?? 0);
|
|
124
149
|
this.documents.clear();
|
|
125
|
-
|
|
126
|
-
|
|
150
|
+
this.rowidsByFilePath.clear();
|
|
151
|
+
for (const [rowid, document] of entries) {
|
|
152
|
+
const numericRowid = Number(rowid);
|
|
153
|
+
const stored = { id: document.id, content: document.content ?? '', metadata: document.metadata };
|
|
154
|
+
this.documents.set(numericRowid, stored);
|
|
155
|
+
this.trackDocumentFilePath(numericRowid, stored);
|
|
156
|
+
}
|
|
127
157
|
}
|
|
128
158
|
catch {
|
|
129
159
|
this.documents.clear();
|
|
160
|
+
this.rowidsByFilePath.clear();
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
trackDocumentFilePath(rowid, document) {
|
|
164
|
+
const filePath = document.metadata.file_path;
|
|
165
|
+
if (typeof filePath !== 'string' || !filePath)
|
|
166
|
+
return;
|
|
167
|
+
let rowids = this.rowidsByFilePath.get(filePath);
|
|
168
|
+
if (!rowids) {
|
|
169
|
+
rowids = new Set();
|
|
170
|
+
this.rowidsByFilePath.set(filePath, rowids);
|
|
130
171
|
}
|
|
172
|
+
rowids.add(rowid);
|
|
173
|
+
}
|
|
174
|
+
untrackDocumentFilePath(rowid, document) {
|
|
175
|
+
const filePath = document.metadata.file_path;
|
|
176
|
+
if (typeof filePath !== 'string' || !filePath)
|
|
177
|
+
return;
|
|
178
|
+
const rowids = this.rowidsByFilePath.get(filePath);
|
|
179
|
+
if (!rowids)
|
|
180
|
+
return;
|
|
181
|
+
rowids.delete(rowid);
|
|
182
|
+
if (rowids.size === 0)
|
|
183
|
+
this.rowidsByFilePath.delete(filePath);
|
|
131
184
|
}
|
|
132
185
|
metadataPath() {
|
|
133
186
|
return `${this.indexPath}.documents.json`;
|
package/dist/vector/types.d.ts
CHANGED
|
@@ -33,6 +33,7 @@ export interface VectorStoreInterface {
|
|
|
33
33
|
ensureCollection(metadata?: Record<string, unknown>): Promise<void>;
|
|
34
34
|
upsert(documents: VectorDocument[], options?: VectorWriteOptions): Promise<void>;
|
|
35
35
|
deleteByFilePath(filePath: string, options?: VectorWriteOptions): Promise<void>;
|
|
36
|
+
deleteByFilePaths?(filePaths: string[], options?: VectorWriteOptions): Promise<void>;
|
|
36
37
|
flush?(): Promise<void>;
|
|
37
38
|
clearCollection?(): Promise<void>;
|
|
38
39
|
needsRebuild?(): boolean;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@customize-agent/knowledge",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.33",
|
|
4
4
|
"description": "Local knowledge base infrastructure for customize-agent",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -36,6 +36,9 @@
|
|
|
36
36
|
"customize-agent",
|
|
37
37
|
"knowledge-base"
|
|
38
38
|
],
|
|
39
|
+
"optionalDependencies": {
|
|
40
|
+
"hnswlib-node": "^3.0.0"
|
|
41
|
+
},
|
|
39
42
|
"dependencies": {
|
|
40
43
|
"@huggingface/transformers": "^3.8.0",
|
|
41
44
|
"@napi-rs/canvas": "^0.1.82",
|
|
@@ -44,7 +47,6 @@
|
|
|
44
47
|
"dwgdxf": "^2.0.1",
|
|
45
48
|
"dxf-parser": "^1.1.2",
|
|
46
49
|
"fast-glob": "^3.3.3",
|
|
47
|
-
"hnswlib-node": "^3.0.0",
|
|
48
50
|
"jszip": "^3.10.1",
|
|
49
51
|
"mammoth": "^1.12.0",
|
|
50
52
|
"node-gyp": "^12.1.0",
|
package/scripts/install-hnsw.cjs
CHANGED
|
@@ -10,7 +10,7 @@ function packageDir(name) {
|
|
|
10
10
|
|
|
11
11
|
function run(command, args, cwd) {
|
|
12
12
|
const result = spawnSync(command, args, { cwd, stdio: 'inherit', shell: false });
|
|
13
|
-
if (result.status !== 0)
|
|
13
|
+
if (result.status !== 0) throw new Error(`${command} ${args.join(' ')} exited with ${result.status || 1}`);
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
function markerPath(hnswDir) {
|
|
@@ -61,7 +61,14 @@ function verify(hnswDir) {
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
try {
|
|
64
|
-
|
|
64
|
+
let hnswDir;
|
|
65
|
+
try {
|
|
66
|
+
hnswDir = packageDir('hnswlib-node');
|
|
67
|
+
} catch {
|
|
68
|
+
console.log('[hnsw] 未安装可选依赖 hnswlib-node,跳过 native 初始化。');
|
|
69
|
+
process.exit(0);
|
|
70
|
+
}
|
|
71
|
+
|
|
65
72
|
if (isMarkerFresh(hnswDir)) {
|
|
66
73
|
try {
|
|
67
74
|
verify(hnswDir);
|
|
@@ -78,7 +85,11 @@ try {
|
|
|
78
85
|
writeMarker(hnswDir);
|
|
79
86
|
console.log('[hnsw] hnswlib-node 安装和运行验证通过');
|
|
80
87
|
} catch (error) {
|
|
81
|
-
console.
|
|
82
|
-
console.
|
|
83
|
-
process.
|
|
88
|
+
console.warn('[hnsw] hnswlib-node 当前不可用,已跳过可选向量索引 native 初始化,不影响主包安装。');
|
|
89
|
+
console.warn('[hnsw] 如需启用本地向量索引,请安装 native 编译工具链后运行 npm rebuild hnswlib-node 或在源码仓库执行 pnpm doctor:hnsw。');
|
|
90
|
+
if (process.env.CUSTOMIZE_AGENT_HNSW_STRICT === '1') {
|
|
91
|
+
console.warn(error && error.stack ? error.stack : error);
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
process.exit(0);
|
|
84
95
|
}
|