@customize-agent/knowledge 4.0.7 → 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;
|
|
@@ -112,7 +113,7 @@ export declare class KnowledgeBaseManager {
|
|
|
112
113
|
fileName: string;
|
|
113
114
|
sourcePath: string;
|
|
114
115
|
targetRelativePath?: string;
|
|
115
|
-
}>, operationId?: string, offset?: number): Promise<import("./index-state-store.js").KnowledgeIndexJob[]>;
|
|
116
|
+
}>, operationId?: string, offset?: number, uploadComplete?: boolean): Promise<import("./index-state-store.js").KnowledgeIndexJob[]>;
|
|
116
117
|
uploadFiles(files: Array<{
|
|
117
118
|
fileName: string;
|
|
118
119
|
content: Buffer;
|
|
@@ -176,6 +177,10 @@ export declare class KnowledgeBaseManager {
|
|
|
176
177
|
private ensureVectorStore;
|
|
177
178
|
private deleteVectorFile;
|
|
178
179
|
private ensureVectorIndexFresh;
|
|
180
|
+
uploadSessionIsOpen(operationId: string): boolean;
|
|
181
|
+
private emptyDiff;
|
|
182
|
+
private statRelativePaths;
|
|
183
|
+
private moveUploadedFile;
|
|
179
184
|
private hasUsableContent;
|
|
180
185
|
private defaultUploadRelativePath;
|
|
181
186
|
private validateUploadRelativePath;
|
|
@@ -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)
|
|
@@ -448,15 +451,16 @@ export class KnowledgeBaseManager {
|
|
|
448
451
|
}
|
|
449
452
|
return jobs;
|
|
450
453
|
}
|
|
451
|
-
async stageUploadedFilePaths(files, operationId = `upload-${Date.now()}`, offset = 0) {
|
|
454
|
+
async stageUploadedFilePaths(files, operationId = `upload-${Date.now()}`, offset = 0, uploadComplete = true) {
|
|
452
455
|
this.initialize();
|
|
456
|
+
this.store.setMetadata(`upload_session:${operationId}`, uploadComplete ? 'complete' : 'open');
|
|
453
457
|
const jobs = [];
|
|
454
458
|
for (let index = 0; index < files.length; index++) {
|
|
455
459
|
const file = files[index];
|
|
456
460
|
const relativePath = this.getUploadRelativePath(file.fileName, file.targetRelativePath);
|
|
457
461
|
const targetPath = this.resolveKbRelativePath(relativePath);
|
|
458
462
|
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
459
|
-
|
|
463
|
+
this.moveUploadedFile(file.sourcePath, targetPath);
|
|
460
464
|
const record = this.store.listRecords().find(item => item.relativePath === relativePath);
|
|
461
465
|
if (record)
|
|
462
466
|
await this.deleteVectorFile(record.collectionName, relativePath);
|
|
@@ -927,6 +931,39 @@ ${resultsText}
|
|
|
927
931
|
return;
|
|
928
932
|
await this.indexVectors({ rebuild: true });
|
|
929
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
|
+
}
|
|
930
967
|
hasUsableContent(text, metadata) {
|
|
931
968
|
const coverage = String(metadata.contentCoverage ?? '');
|
|
932
969
|
if (['metadata', 'metadata_filename', 'pdf_metadata_only', 'office_zip_empty_text', 'office_zip_failed'].includes(coverage))
|
|
@@ -964,6 +1001,6 @@ ${resultsText}
|
|
|
964
1001
|
return targetPath;
|
|
965
1002
|
}
|
|
966
1003
|
normalizeRelativePath(relativePath) {
|
|
967
|
-
return relativePath.replace(/\\/gu, '/').split(path.sep).join('/').replace(/^\/+/, '');
|
|
1004
|
+
return relativePath.replace(/\\/gu, '/').split(path.sep).join('/').replace(/^\/+/, '').replace(/\/+/gu, '/');
|
|
968
1005
|
}
|
|
969
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 {
|