@customize-agent/knowledge 4.0.6 → 4.0.8
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.
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as crypto from 'node:crypto';
|
|
2
2
|
import * as fs from 'node:fs';
|
|
3
|
+
import { createReadStream } from 'node:fs';
|
|
3
4
|
import * as path from 'node:path';
|
|
4
5
|
/** 文件变更追踪器,用于比对磁盘文件与索引状态之间的差异 */
|
|
5
6
|
export class ChangeTracker {
|
|
@@ -51,7 +52,7 @@ export class ChangeTracker {
|
|
|
51
52
|
continue;
|
|
52
53
|
}
|
|
53
54
|
if (Math.round(diskStat.mtime) !== Math.round(indexed.mtime) || diskStat.size !== indexed.fileSize) {
|
|
54
|
-
const contentHash = this.hashFile(absolutePath);
|
|
55
|
+
const contentHash = await this.hashFile(absolutePath);
|
|
55
56
|
if (contentHash !== indexed.contentHash) {
|
|
56
57
|
modifiedFiles.push(classified);
|
|
57
58
|
}
|
|
@@ -86,8 +87,15 @@ export class ChangeTracker {
|
|
|
86
87
|
* @param filePath 文件路径
|
|
87
88
|
* @returns SHA-256 哈希字符串
|
|
88
89
|
*/
|
|
89
|
-
hashFile(filePath) {
|
|
90
|
-
|
|
90
|
+
async hashFile(filePath) {
|
|
91
|
+
const hash = crypto.createHash('sha256');
|
|
92
|
+
await new Promise((resolve, reject) => {
|
|
93
|
+
const stream = createReadStream(filePath);
|
|
94
|
+
stream.on('data', chunk => hash.update(chunk));
|
|
95
|
+
stream.on('error', reject);
|
|
96
|
+
stream.on('end', resolve);
|
|
97
|
+
});
|
|
98
|
+
return hash.digest('hex');
|
|
91
99
|
}
|
|
92
100
|
parseMetadata(metadataJson) {
|
|
93
101
|
if (!metadataJson)
|
|
@@ -59,6 +59,7 @@ export declare class KnowledgeBaseManager {
|
|
|
59
59
|
onProgress?: (progress: KnowledgeIndexProgress) => void;
|
|
60
60
|
vectorMode?: 'sync' | 'defer';
|
|
61
61
|
limit?: number;
|
|
62
|
+
waitForUploadId?: string;
|
|
62
63
|
}): Promise<DiffResult>;
|
|
63
64
|
incrementalIndex(options?: {
|
|
64
65
|
onProgress?: (progress: KnowledgeIndexProgress) => void;
|
|
@@ -108,6 +109,11 @@ export declare class KnowledgeBaseManager {
|
|
|
108
109
|
content: Buffer;
|
|
109
110
|
targetRelativePath?: string;
|
|
110
111
|
}>, operationId?: string): Promise<import("./index-state-store.js").KnowledgeIndexJob[]>;
|
|
112
|
+
stageUploadedFilePaths(files: Array<{
|
|
113
|
+
fileName: string;
|
|
114
|
+
sourcePath: string;
|
|
115
|
+
targetRelativePath?: string;
|
|
116
|
+
}>, operationId?: string, offset?: number, uploadComplete?: boolean): Promise<import("./index-state-store.js").KnowledgeIndexJob[]>;
|
|
111
117
|
uploadFiles(files: Array<{
|
|
112
118
|
fileName: string;
|
|
113
119
|
content: Buffer;
|
|
@@ -171,8 +177,13 @@ export declare class KnowledgeBaseManager {
|
|
|
171
177
|
private ensureVectorStore;
|
|
172
178
|
private deleteVectorFile;
|
|
173
179
|
private ensureVectorIndexFresh;
|
|
180
|
+
uploadSessionIsOpen(operationId: string): boolean;
|
|
181
|
+
private emptyDiff;
|
|
182
|
+
private statRelativePaths;
|
|
183
|
+
private moveUploadedFile;
|
|
174
184
|
private hasUsableContent;
|
|
175
185
|
private defaultUploadRelativePath;
|
|
186
|
+
private validateUploadRelativePath;
|
|
176
187
|
private resolveKbRelativePath;
|
|
177
188
|
private normalizeRelativePath;
|
|
178
189
|
}
|
|
@@ -87,9 +87,12 @@ export class KnowledgeBaseManager {
|
|
|
87
87
|
}
|
|
88
88
|
async consumePendingIndexJobs(options = {}) {
|
|
89
89
|
this.initialize();
|
|
90
|
-
const jobs = this.store.listPendingIndexJobs(options.limit ??
|
|
91
|
-
if (jobs.length === 0)
|
|
90
|
+
const jobs = this.store.listPendingIndexJobs(options.limit ?? 500);
|
|
91
|
+
if (jobs.length === 0) {
|
|
92
|
+
if (options.waitForUploadId && this.uploadSessionIsOpen(options.waitForUploadId))
|
|
93
|
+
return this.emptyDiff();
|
|
92
94
|
return this.incrementalIndex(options);
|
|
95
|
+
}
|
|
93
96
|
return this.incrementalIndex({ ...options, onlyRelativePaths: jobs.map(job => job.relativePath) });
|
|
94
97
|
}
|
|
95
98
|
async incrementalIndex(options = {}) {
|
|
@@ -101,10 +104,10 @@ export class KnowledgeBaseManager {
|
|
|
101
104
|
this.reportProgress({ stage: 'scanning', percent: 10, message: '正在扫描知识库文件' });
|
|
102
105
|
const kbIgnore = this.scanner.loadKbIgnore(this.kbPath);
|
|
103
106
|
const configIgnore = this.projectConfig?.kbignore ?? [];
|
|
104
|
-
const
|
|
107
|
+
const onlyRelativePaths = options.onlyRelativePaths ? new Set(options.onlyRelativePaths) : undefined;
|
|
108
|
+
const diskFiles = onlyRelativePaths ? this.statRelativePaths([...onlyRelativePaths]) : await this.scanner.scan(this.kbPath, [...kbIgnore, ...configIgnore]);
|
|
105
109
|
const tracker = new ChangeTracker(this.store);
|
|
106
110
|
const diff = await tracker.computeDiff(diskFiles, this.classifier, this.kbPath);
|
|
107
|
-
const onlyRelativePaths = options.onlyRelativePaths ? new Set(options.onlyRelativePaths) : undefined;
|
|
108
111
|
let vectorDeletesApplied = 0;
|
|
109
112
|
if (onlyRelativePaths) {
|
|
110
113
|
diff.newFiles = diff.newFiles.filter(file => onlyRelativePaths.has(file.relativePath));
|
|
@@ -129,7 +132,7 @@ export class KnowledgeBaseManager {
|
|
|
129
132
|
const vectorRelativePaths = [];
|
|
130
133
|
const changedCollectionNames = new Set();
|
|
131
134
|
for (const [index, file] of filesToIndex.entries()) {
|
|
132
|
-
const hash = tracker.hashFile(file.absolutePath);
|
|
135
|
+
const hash = await tracker.hashFile(file.absolutePath);
|
|
133
136
|
const duplicate = this.store.findExactDuplicate(hash, file.relativePath);
|
|
134
137
|
const collectionName = this.scope === 'global'
|
|
135
138
|
? this.collections.getCollectionName('global', file.category)
|
|
@@ -426,7 +429,7 @@ export class KnowledgeBaseManager {
|
|
|
426
429
|
return this.incrementalIndex();
|
|
427
430
|
}
|
|
428
431
|
getUploadRelativePath(fileName, targetRelativePath) {
|
|
429
|
-
return targetRelativePath ? this.normalizeRelativePath(targetRelativePath) : this.defaultUploadRelativePath(fileName);
|
|
432
|
+
return this.validateUploadRelativePath(targetRelativePath ? this.normalizeRelativePath(targetRelativePath) : this.defaultUploadRelativePath(fileName));
|
|
430
433
|
}
|
|
431
434
|
async uploadFile(fileName, content, targetRelativePath, onProgress, options = {}) {
|
|
432
435
|
return this.uploadFiles([{ fileName, content, targetRelativePath }], onProgress, options);
|
|
@@ -448,6 +451,24 @@ export class KnowledgeBaseManager {
|
|
|
448
451
|
}
|
|
449
452
|
return jobs;
|
|
450
453
|
}
|
|
454
|
+
async stageUploadedFilePaths(files, operationId = `upload-${Date.now()}`, offset = 0, uploadComplete = true) {
|
|
455
|
+
this.initialize();
|
|
456
|
+
this.store.setMetadata(`upload_session:${operationId}`, uploadComplete ? 'complete' : 'open');
|
|
457
|
+
const jobs = [];
|
|
458
|
+
for (let index = 0; index < files.length; index++) {
|
|
459
|
+
const file = files[index];
|
|
460
|
+
const relativePath = this.getUploadRelativePath(file.fileName, file.targetRelativePath);
|
|
461
|
+
const targetPath = this.resolveKbRelativePath(relativePath);
|
|
462
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
463
|
+
this.moveUploadedFile(file.sourcePath, targetPath);
|
|
464
|
+
const record = this.store.listRecords().find(item => item.relativePath === relativePath);
|
|
465
|
+
if (record)
|
|
466
|
+
await this.deleteVectorFile(record.collectionName, relativePath);
|
|
467
|
+
this.store.deleteRecord(relativePath);
|
|
468
|
+
jobs.push(this.store.enqueueIndexJob({ id: `${operationId}-${offset + index}`, relativePath, message: '文件已落盘,等待后台解析' }));
|
|
469
|
+
}
|
|
470
|
+
return jobs;
|
|
471
|
+
}
|
|
451
472
|
async uploadFiles(files, onProgress, options = {}) {
|
|
452
473
|
await this.stageUploadedFiles(files);
|
|
453
474
|
return this.incrementalIndex({ onProgress, vectorMode: options.vectorMode });
|
|
@@ -910,6 +931,39 @@ ${resultsText}
|
|
|
910
931
|
return;
|
|
911
932
|
await this.indexVectors({ rebuild: true });
|
|
912
933
|
}
|
|
934
|
+
uploadSessionIsOpen(operationId) {
|
|
935
|
+
return this.store.getMetadata(`upload_session:${operationId}`) === 'open';
|
|
936
|
+
}
|
|
937
|
+
emptyDiff() {
|
|
938
|
+
return { newFiles: [], modifiedFiles: [], deletedFiles: [], unchangedCount: 0, mtimeOnlyCount: 0, skippedFiles: [], hasChanges: false, diffTimeMs: 0 };
|
|
939
|
+
}
|
|
940
|
+
statRelativePaths(relativePaths) {
|
|
941
|
+
const files = new Map();
|
|
942
|
+
for (const relativePath of relativePaths) {
|
|
943
|
+
const normalized = this.normalizeRelativePath(relativePath);
|
|
944
|
+
const absolutePath = this.resolveKbRelativePath(normalized);
|
|
945
|
+
if (!fs.existsSync(absolutePath))
|
|
946
|
+
continue;
|
|
947
|
+
const stat = fs.statSync(absolutePath);
|
|
948
|
+
if (stat.isFile())
|
|
949
|
+
files.set(normalized, { size: stat.size, mtime: stat.mtimeMs });
|
|
950
|
+
}
|
|
951
|
+
return files;
|
|
952
|
+
}
|
|
953
|
+
moveUploadedFile(sourcePath, targetPath) {
|
|
954
|
+
if (fs.existsSync(targetPath))
|
|
955
|
+
fs.rmSync(targetPath, { force: true });
|
|
956
|
+
try {
|
|
957
|
+
fs.renameSync(sourcePath, targetPath);
|
|
958
|
+
}
|
|
959
|
+
catch (error) {
|
|
960
|
+
const code = error && typeof error === 'object' && 'code' in error ? String(error.code) : '';
|
|
961
|
+
if (code !== 'EXDEV')
|
|
962
|
+
throw error;
|
|
963
|
+
fs.copyFileSync(sourcePath, targetPath);
|
|
964
|
+
fs.rmSync(sourcePath, { force: true });
|
|
965
|
+
}
|
|
966
|
+
}
|
|
913
967
|
hasUsableContent(text, metadata) {
|
|
914
968
|
const coverage = String(metadata.contentCoverage ?? '');
|
|
915
969
|
if (['metadata', 'metadata_filename', 'pdf_metadata_only', 'office_zip_empty_text', 'office_zip_failed'].includes(coverage))
|
|
@@ -922,6 +976,21 @@ ${resultsText}
|
|
|
922
976
|
const dir = configDirs[classification.category] ?? DEFAULT_CATEGORY_DIRS[classification.category];
|
|
923
977
|
return `${dir}/${path.basename(fileName)}`;
|
|
924
978
|
}
|
|
979
|
+
validateUploadRelativePath(relativePath) {
|
|
980
|
+
const normalized = this.normalizeRelativePath(relativePath);
|
|
981
|
+
if (!normalized || normalized === '.')
|
|
982
|
+
throw new Error('上传文件路径无效');
|
|
983
|
+
if (normalized.length > 1000)
|
|
984
|
+
throw new Error('上传文件路径过长,请缩短文件夹层级或文件名');
|
|
985
|
+
if (normalized.includes('\0'))
|
|
986
|
+
throw new Error('上传文件路径包含非法字符');
|
|
987
|
+
const parts = normalized.split('/');
|
|
988
|
+
if (parts.some(part => !part || part === '..'))
|
|
989
|
+
throw new Error('上传文件路径无效');
|
|
990
|
+
if (parts.some(part => part.length > 255))
|
|
991
|
+
throw new Error('上传文件名过长,请缩短文件名后重试');
|
|
992
|
+
return normalized;
|
|
993
|
+
}
|
|
925
994
|
resolveKbRelativePath(relativePath) {
|
|
926
995
|
const normalized = this.normalizeRelativePath(relativePath);
|
|
927
996
|
const targetPath = path.resolve(this.kbPath, normalized);
|
|
@@ -932,6 +1001,6 @@ ${resultsText}
|
|
|
932
1001
|
return targetPath;
|
|
933
1002
|
}
|
|
934
1003
|
normalizeRelativePath(relativePath) {
|
|
935
|
-
return relativePath.replace(/\\/gu, '/').split(path.sep).join('/').replace(/^\/+/, '');
|
|
1004
|
+
return relativePath.replace(/\\/gu, '/').split(path.sep).join('/').replace(/^\/+/, '').replace(/\/+/gu, '/');
|
|
936
1005
|
}
|
|
937
1006
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
function resolveVectorIndexBatchSize(configured) {
|
|
2
2
|
const raw = configured ?? Number(process.env.CUSTOMIZE_VECTOR_INDEX_BATCH_SIZE ?? process.env.KB_VECTOR_INDEX_BATCH_SIZE);
|
|
3
3
|
if (!Number.isFinite(raw) || raw <= 0)
|
|
4
|
-
return
|
|
5
|
-
return Math.max(1, Math.min(
|
|
4
|
+
return 256;
|
|
5
|
+
return Math.max(1, Math.min(1024, Math.floor(raw)));
|
|
6
6
|
}
|
|
7
7
|
/** 向量索引器,负责将文本切片生成 Embedding 并写入向量存储 */
|
|
8
8
|
export class VectorIndexer {
|