@customize-agent/knowledge 4.0.1 → 4.0.3
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/bge-tokenizer.d.ts +12 -0
- package/dist/chunking/bge-tokenizer.js +70 -0
- package/dist/chunking/text-chunker.d.ts +20 -0
- package/dist/chunking/text-chunker.js +166 -53
- package/dist/classification/classifier.d.ts +1 -0
- package/dist/classification/classifier.js +1 -1
- package/dist/constants.d.ts +7 -0
- package/dist/constants.js +7 -0
- package/dist/core/change-tracker.d.ts +13 -0
- package/dist/core/change-tracker.js +13 -0
- package/dist/core/file-scanner.d.ts +13 -0
- package/dist/core/file-scanner.js +12 -0
- package/dist/core/index-state-store.d.ts +67 -0
- package/dist/core/index-state-store.js +204 -50
- package/dist/core/knowledge-base-manager.d.ts +24 -2
- package/dist/core/knowledge-base-manager.js +204 -52
- package/dist/core/multi-project-manager.d.ts +10 -0
- package/dist/core/multi-project-manager.js +21 -2
- package/dist/core/project-config.d.ts +4 -0
- package/dist/core/project-config.js +4 -0
- package/dist/core/project-id.d.ts +5 -0
- package/dist/core/project-id.js +5 -0
- package/dist/core/project-registry.d.ts +1 -0
- package/dist/core/project-registry.js +1 -0
- package/dist/dedup/dedup-engine.d.ts +3 -0
- package/dist/dedup/dedup-engine.js +1 -0
- package/dist/dedup/relationship-detector.d.ts +7 -0
- package/dist/dedup/relationship-detector.js +7 -0
- package/dist/embedding/embedding-provider.d.ts +32 -0
- package/dist/embedding/embedding-provider.js +136 -2
- package/dist/extraction/content-extractor.d.ts +32 -2
- package/dist/extraction/content-extractor.js +524 -124
- package/dist/extraction/external-extractor.d.ts +10 -0
- package/dist/extraction/external-extractor.js +6 -0
- package/dist/extraction/module-resolver.js +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +4 -4
- package/dist/search/federation-search.d.ts +8 -0
- package/dist/search/federation-search.js +2 -0
- package/dist/types.d.ts +10 -0
- package/dist/vector/collection-manager.d.ts +3 -0
- package/dist/vector/collection-manager.js +3 -0
- package/dist/vector/hnsw-vector-store.d.ts +21 -0
- package/dist/vector/hnsw-vector-store.js +108 -0
- package/dist/vector/types.d.ts +8 -0
- package/dist/vector/vector-indexer.d.ts +15 -1
- package/dist/vector/vector-indexer.js +34 -5
- package/models/bge-small-zh-v1.5/config.json +31 -0
- package/models/bge-small-zh-v1.5/onnx/model_quantized.onnx +0 -0
- package/models/bge-small-zh-v1.5/special_tokens_map.json +7 -0
- package/models/bge-small-zh-v1.5/tokenizer.json +21278 -0
- package/models/bge-small-zh-v1.5/tokenizer_config.json +15 -0
- package/package.json +16 -9
- package/scripts/install-hnsw.cjs +47 -0
- package/dist/vector/sqlite-vec-store.d.ts +0 -38
- package/dist/vector/sqlite-vec-store.js +0 -203
|
@@ -10,7 +10,7 @@ import { createEmbeddingProviderFromEnvironment } from '../embedding/embedding-p
|
|
|
10
10
|
import { ContentExtractor } from '../extraction/content-extractor.js';
|
|
11
11
|
import { FederationSearch } from '../search/federation-search.js';
|
|
12
12
|
import { CollectionManager } from '../vector/collection-manager.js';
|
|
13
|
-
import {
|
|
13
|
+
import { HNSWVectorStore } from '../vector/hnsw-vector-store.js';
|
|
14
14
|
import { VectorIndexer } from '../vector/vector-indexer.js';
|
|
15
15
|
import { ChangeTracker } from './change-tracker.js';
|
|
16
16
|
import { KnowledgeFileScanner } from './file-scanner.js';
|
|
@@ -22,7 +22,7 @@ export class KnowledgeBaseManager {
|
|
|
22
22
|
projectId;
|
|
23
23
|
kbPath;
|
|
24
24
|
store;
|
|
25
|
-
|
|
25
|
+
vectorRoot;
|
|
26
26
|
classifier = new FileClassifier();
|
|
27
27
|
scanner = new KnowledgeFileScanner();
|
|
28
28
|
collections = new CollectionManager();
|
|
@@ -61,7 +61,7 @@ export class KnowledgeBaseManager {
|
|
|
61
61
|
dbPath = path.join(storageRoot, 'projects', options.projectId, 'kb.db');
|
|
62
62
|
}
|
|
63
63
|
this.store = new IndexStateStore(dbPath);
|
|
64
|
-
this.
|
|
64
|
+
this.vectorRoot = path.join(path.dirname(dbPath), 'hnsw');
|
|
65
65
|
}
|
|
66
66
|
initialize() {
|
|
67
67
|
if (this.scope === 'project' && this.projectRoot) {
|
|
@@ -76,6 +76,22 @@ export class KnowledgeBaseManager {
|
|
|
76
76
|
fs.mkdirSync(this.kbPath, { recursive: true });
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
|
+
async forceReindexAll(options = {}) {
|
|
80
|
+
this.initialize();
|
|
81
|
+
const records = this.store.listRecords();
|
|
82
|
+
for (const record of records) {
|
|
83
|
+
await this.deleteVectorFile(record.collectionName, record.relativePath);
|
|
84
|
+
this.store.deleteRecord(record.relativePath);
|
|
85
|
+
}
|
|
86
|
+
return this.incrementalIndex(options);
|
|
87
|
+
}
|
|
88
|
+
async consumePendingIndexJobs(options = {}) {
|
|
89
|
+
this.initialize();
|
|
90
|
+
const jobs = this.store.listPendingIndexJobs(options.limit ?? 50);
|
|
91
|
+
if (jobs.length === 0)
|
|
92
|
+
return this.incrementalIndex(options);
|
|
93
|
+
return this.incrementalIndex({ ...options, onlyRelativePaths: jobs.map(job => job.relativePath) });
|
|
94
|
+
}
|
|
79
95
|
async incrementalIndex(options = {}) {
|
|
80
96
|
this.initialize();
|
|
81
97
|
const previousOnProgress = this.onProgress;
|
|
@@ -88,9 +104,22 @@ export class KnowledgeBaseManager {
|
|
|
88
104
|
const diskFiles = await this.scanner.scan(this.kbPath, [...kbIgnore, ...configIgnore]);
|
|
89
105
|
const tracker = new ChangeTracker(this.store);
|
|
90
106
|
const diff = await tracker.computeDiff(diskFiles, this.classifier, this.kbPath);
|
|
107
|
+
const onlyRelativePaths = options.onlyRelativePaths ? new Set(options.onlyRelativePaths) : undefined;
|
|
108
|
+
if (onlyRelativePaths) {
|
|
109
|
+
diff.newFiles = diff.newFiles.filter(file => onlyRelativePaths.has(file.relativePath));
|
|
110
|
+
diff.modifiedFiles = diff.modifiedFiles.filter(file => onlyRelativePaths.has(file.relativePath));
|
|
111
|
+
diff.deletedFiles = diff.deletedFiles.filter(file => onlyRelativePaths.has(file.relativePath));
|
|
112
|
+
diff.hasChanges = diff.newFiles.length + diff.modifiedFiles.length + diff.deletedFiles.length > 0;
|
|
113
|
+
for (const relativePath of onlyRelativePaths) {
|
|
114
|
+
const exists = diff.newFiles.some(file => file.relativePath === relativePath) || diff.modifiedFiles.some(file => file.relativePath === relativePath) || diff.deletedFiles.some(file => file.relativePath === relativePath);
|
|
115
|
+
if (!exists)
|
|
116
|
+
this.updateJobsForFile(relativePath, 'ERROR', 100, '待索引文件不存在或未发生变化', '待索引文件不存在或未发生变化');
|
|
117
|
+
}
|
|
118
|
+
}
|
|
91
119
|
for (const deleted of diff.deletedFiles) {
|
|
92
120
|
await this.deleteVectorFile(deleted.collectionName, deleted.relativePath);
|
|
93
121
|
this.store.deleteRecord(deleted.relativePath);
|
|
122
|
+
this.updateJobsForFile(deleted.relativePath, 'SUCCESS', 100, '文件已删除,索引记录和向量已清理');
|
|
94
123
|
}
|
|
95
124
|
const now = Date.now();
|
|
96
125
|
const indexedBefore = [...this.store.loadActiveRecords().values()];
|
|
@@ -102,12 +131,14 @@ export class KnowledgeBaseManager {
|
|
|
102
131
|
? this.collections.getCollectionName('global', file.category)
|
|
103
132
|
: this.collections.getCollectionName('project', file.category, this.projectId);
|
|
104
133
|
const basePercent = filesToIndex.length === 0 ? 40 : 20 + Math.round((index / filesToIndex.length) * 45);
|
|
134
|
+
this.updateJobsForFile(file.relativePath, 'PARSING', basePercent, `正在解析 ${file.relativePath}`);
|
|
105
135
|
this.reportProgress({ stage: 'parsing', percent: basePercent, message: `正在解析 ${file.relativePath}`, filePath: file.relativePath });
|
|
106
136
|
const extraction = await this.extractor.extract(file);
|
|
107
137
|
extraction.metadata.textLength = extraction.text.length;
|
|
108
138
|
if (!this.hasUsableContent(extraction.text, extraction.metadata)) {
|
|
109
139
|
const reason = extraction.warnings[0] ?? '未解析出可用于模型的正文内容,已跳过向量化';
|
|
110
140
|
diff.skippedFiles.push({ file, reason });
|
|
141
|
+
this.updateJobsForFile(file.relativePath, 'ERROR', 100, reason, reason);
|
|
111
142
|
this.store.upsertRecord({
|
|
112
143
|
relativePath: file.relativePath,
|
|
113
144
|
category: file.category,
|
|
@@ -129,6 +160,7 @@ export class KnowledgeBaseManager {
|
|
|
129
160
|
const normalizedDuplicate = !duplicate && normalizedHash
|
|
130
161
|
? this.store.findNormalizedDuplicate(normalizedHash, file.relativePath)
|
|
131
162
|
: undefined;
|
|
163
|
+
this.updateJobsForFile(file.relativePath, 'CHUNKING', Math.min(80, basePercent + 10), `正在切片 ${file.relativePath}`);
|
|
132
164
|
this.reportProgress({ stage: 'chunking', percent: Math.min(80, basePercent + 10), message: `正在切片 ${file.relativePath}`, filePath: file.relativePath });
|
|
133
165
|
const chunks = this.chunker.chunk(extraction.text, file, extraction.metadata);
|
|
134
166
|
this.reportProgress({ stage: 'chunking', percent: Math.min(84, basePercent + 14), message: `切片完成:${chunks.length} 块`, filePath: file.relativePath, chunkCount: chunks.length });
|
|
@@ -212,20 +244,31 @@ export class KnowledgeBaseManager {
|
|
|
212
244
|
format: file.format,
|
|
213
245
|
collectionName,
|
|
214
246
|
});
|
|
247
|
+
this.updateJobsForFile(file.relativePath, options.vectorMode === 'defer' ? 'SUCCESS' : 'INDEXING', options.vectorMode === 'defer' ? 100 : 85, options.vectorMode === 'defer' ? '解析和切片已完成' : '等待向量入库');
|
|
215
248
|
}
|
|
216
249
|
const stats = this.getStats();
|
|
217
250
|
this.store.setMetadata('last_incremental_index_at', String(now));
|
|
218
251
|
this.store.setMetadata('total_chunks', String(stats.chunkCount));
|
|
219
252
|
this.store.setMetadata('total_files_indexed', String(stats.fileCount));
|
|
253
|
+
const hasIndexChanges = diff.newFiles.length + diff.modifiedFiles.length + diff.deletedFiles.length > 0;
|
|
220
254
|
if (options.vectorMode === 'defer') {
|
|
255
|
+
if (hasIndexChanges)
|
|
256
|
+
this.store.setMetadata('vector_index_status', 'pending');
|
|
221
257
|
this.reportProgress({ stage: 'vectorizing', percent: 85, message: '解析和切片已完成,向量入库转入后台/稍后执行', chunkCount: stats.chunkCount, vectorStatus: this.getVectorStatus() });
|
|
222
|
-
void this.ensureVectorIndexFresh(stats.chunkCount, diff.newFiles.length + diff.modifiedFiles.length + diff.deletedFiles.length > 0).catch(() => undefined);
|
|
223
258
|
}
|
|
224
259
|
else {
|
|
225
|
-
await this.ensureVectorIndexFresh(stats.chunkCount,
|
|
260
|
+
await this.ensureVectorIndexFresh(stats.chunkCount, hasIndexChanges);
|
|
226
261
|
}
|
|
227
262
|
this.lastSkippedFiles = diff.skippedFiles;
|
|
228
263
|
const vectorStatus = this.getVectorStatus();
|
|
264
|
+
if (options.vectorMode !== 'defer') {
|
|
265
|
+
for (const file of filesToIndex) {
|
|
266
|
+
if (vectorStatus.status === 'error')
|
|
267
|
+
this.updateJobsForFile(file.relativePath, 'ERROR', 100, 'HNSWLib 向量入库失败', vectorStatus.error);
|
|
268
|
+
else
|
|
269
|
+
this.updateJobsForFile(file.relativePath, 'SUCCESS', 100, '解析、切片和向量索引完成');
|
|
270
|
+
}
|
|
271
|
+
}
|
|
229
272
|
const vectorDeferred = options.vectorMode === 'defer';
|
|
230
273
|
this.reportProgress({
|
|
231
274
|
stage: vectorDeferred || vectorStatus.status !== 'error' ? 'done' : 'error',
|
|
@@ -233,7 +276,7 @@ export class KnowledgeBaseManager {
|
|
|
233
276
|
message: vectorDeferred
|
|
234
277
|
? '解析、切片和 SQLite 入库已完成,向量入库后台执行'
|
|
235
278
|
: vectorStatus.status === 'error'
|
|
236
|
-
? '解析和切片已完成,
|
|
279
|
+
? '解析和切片已完成,HNSWLib 向量待入库'
|
|
237
280
|
: '知识库索引完成',
|
|
238
281
|
chunkCount: stats.chunkCount,
|
|
239
282
|
vectorStatus,
|
|
@@ -264,8 +307,10 @@ export class KnowledgeBaseManager {
|
|
|
264
307
|
}
|
|
265
308
|
const parentChunks = item.parentId ? this.store.getChunksByParent(item.filePath, item.parentId, 6) : [];
|
|
266
309
|
const chunks = parentChunks.length > 0 ? parentChunks : this.store.getContextChunks(item.filePath, chunkIndex, 1);
|
|
267
|
-
if (chunks.length === 0)
|
|
268
|
-
|
|
310
|
+
if (chunks.length === 0) {
|
|
311
|
+
const document = this.store.getDocumentChunk(item.filePath);
|
|
312
|
+
return document ? { ...item, content: document.content, sectionTitle: item.sectionTitle ?? 'Document Parent' } : item;
|
|
313
|
+
}
|
|
269
314
|
return {
|
|
270
315
|
...item,
|
|
271
316
|
content: chunks.map(chunk => chunk.content).join('\n\n---\n\n'),
|
|
@@ -279,15 +324,19 @@ export class KnowledgeBaseManager {
|
|
|
279
324
|
const start = Date.now();
|
|
280
325
|
const weights = this.retrievalWeights(options.weights);
|
|
281
326
|
const rewrittenQueries = await this.rewriteQueries(query);
|
|
282
|
-
const
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
327
|
+
const rankedLists = [];
|
|
328
|
+
for (const [queryIndex, rewritten] of rewrittenQueries.entries()) {
|
|
329
|
+
rankedLists.push({ source: 'keyword', items: this.keywordSearchItems(rewritten, limit * 3), queryIndex });
|
|
330
|
+
if (queryIndex < 3) {
|
|
331
|
+
try {
|
|
332
|
+
rankedLists.push({ source: 'vector', items: (await this.semanticSearch(rewritten, { ...options, limit: limit * 3 })).results, queryIndex });
|
|
333
|
+
}
|
|
334
|
+
catch { /* 向量搜索在混合搜索中是可选的 */ }
|
|
287
335
|
}
|
|
288
|
-
catch { /* vector search is optional in hybrid search */ }
|
|
289
336
|
}
|
|
290
|
-
const
|
|
337
|
+
const keywordItems = rankedLists.filter(list => list.source === 'keyword').flatMap(list => list.items);
|
|
338
|
+
const vectorItems = rankedLists.filter(list => list.source === 'vector').flatMap(list => list.items);
|
|
339
|
+
const merged = this.mergeContexts(this.mergeHybridRankedLists(rankedLists, limit * 4, weights).map(item => this.expandContext(item)), limit * 2);
|
|
291
340
|
const useLLMRerank = !!this.llmProvider;
|
|
292
341
|
const preReranked = useLLMRerank ? merged : this.heuristicRerank(query, merged);
|
|
293
342
|
const reranked = useLLMRerank ? (await this.llmRerank(query, preReranked)) : preReranked;
|
|
@@ -310,7 +359,7 @@ export class KnowledgeBaseManager {
|
|
|
310
359
|
const queryEmbedding = await this.embeddingProvider.embedQuery(query);
|
|
311
360
|
const search = new FederationSearch(this.vectorStores);
|
|
312
361
|
try {
|
|
313
|
-
|
|
362
|
+
const result = await search.search({
|
|
314
363
|
query,
|
|
315
364
|
queryEmbedding,
|
|
316
365
|
topK: options.limit ?? 10,
|
|
@@ -319,6 +368,7 @@ export class KnowledgeBaseManager {
|
|
|
319
368
|
collections: options.collections,
|
|
320
369
|
filters: options.filters,
|
|
321
370
|
});
|
|
371
|
+
return { ...result, results: this.hydrateVectorResultsFromSqlite(result.results) };
|
|
322
372
|
}
|
|
323
373
|
catch {
|
|
324
374
|
return { results: [], scopesSearched: this.scope === 'global' ? ['global'] : ['project'], queryTimeMs: 0 };
|
|
@@ -372,22 +422,25 @@ export class KnowledgeBaseManager {
|
|
|
372
422
|
async uploadFile(fileName, content, targetRelativePath, onProgress, options = {}) {
|
|
373
423
|
return this.uploadFiles([{ fileName, content, targetRelativePath }], onProgress, options);
|
|
374
424
|
}
|
|
375
|
-
async
|
|
425
|
+
async stageUploadedFiles(files, operationId = `upload-${Date.now()}`) {
|
|
376
426
|
this.initialize();
|
|
377
|
-
const
|
|
378
|
-
for (
|
|
427
|
+
const jobs = [];
|
|
428
|
+
for (let index = 0; index < files.length; index++) {
|
|
429
|
+
const file = files[index];
|
|
379
430
|
const relativePath = this.getUploadRelativePath(file.fileName, file.targetRelativePath);
|
|
380
431
|
const targetPath = this.resolveKbRelativePath(relativePath);
|
|
381
432
|
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
382
433
|
fs.writeFileSync(targetPath, file.content);
|
|
383
|
-
uploadedPaths.push(relativePath);
|
|
384
|
-
}
|
|
385
|
-
for (const relativePath of uploadedPaths) {
|
|
386
434
|
const record = this.store.listRecords().find(item => item.relativePath === relativePath);
|
|
387
435
|
if (record)
|
|
388
436
|
await this.deleteVectorFile(record.collectionName, relativePath);
|
|
389
437
|
this.store.deleteRecord(relativePath);
|
|
438
|
+
jobs.push(this.store.enqueueIndexJob({ id: `${operationId}-${index}`, relativePath, message: '文件已落盘,等待后台解析' }));
|
|
390
439
|
}
|
|
440
|
+
return jobs;
|
|
441
|
+
}
|
|
442
|
+
async uploadFiles(files, onProgress, options = {}) {
|
|
443
|
+
await this.stageUploadedFiles(files);
|
|
391
444
|
return this.incrementalIndex({ onProgress, vectorMode: options.vectorMode });
|
|
392
445
|
}
|
|
393
446
|
listFailedFiles() {
|
|
@@ -398,12 +451,17 @@ export class KnowledgeBaseManager {
|
|
|
398
451
|
const targetPath = this.resolveKbRelativePath(normalized);
|
|
399
452
|
if (fs.existsSync(targetPath))
|
|
400
453
|
fs.unlinkSync(targetPath);
|
|
401
|
-
// 同步删除
|
|
454
|
+
// 同步删除 HNSWLib 向量数据,避免孤儿向量污染搜索结果
|
|
402
455
|
const record = this.store.listRecords().find(r => r.relativePath === normalized);
|
|
403
456
|
if (record) {
|
|
404
457
|
await this.deleteVectorFile(record.collectionName, normalized);
|
|
405
458
|
}
|
|
406
459
|
this.store.deleteRecord(normalized);
|
|
460
|
+
const stats = this.getStats();
|
|
461
|
+
this.store.setMetadata('total_chunks', String(stats.chunkCount));
|
|
462
|
+
this.store.setMetadata('total_files_indexed', String(stats.fileCount));
|
|
463
|
+
this.store.setMetadata('vector_indexed_chunks', String(stats.chunkCount));
|
|
464
|
+
this.store.setMetadata('last_vector_index_at', String(Date.now()));
|
|
407
465
|
}
|
|
408
466
|
tagFile(relativePath, tags) {
|
|
409
467
|
this.store.setTags(this.normalizeRelativePath(relativePath), tags);
|
|
@@ -425,18 +483,39 @@ export class KnowledgeBaseManager {
|
|
|
425
483
|
}
|
|
426
484
|
async indexVectors(options = {}) {
|
|
427
485
|
const chunks = this.store.listChunks(options);
|
|
428
|
-
|
|
486
|
+
const collectionNames = new Set(chunks.map(chunk => chunk.collectionName));
|
|
487
|
+
for (const collectionName of collectionNames)
|
|
429
488
|
this.ensureVectorStore(collectionName);
|
|
430
|
-
|
|
489
|
+
if (!options.relativePath) {
|
|
490
|
+
for (const collectionName of collectionNames)
|
|
491
|
+
await this.vectorStores.get(collectionName)?.clearCollection?.();
|
|
492
|
+
}
|
|
493
|
+
else {
|
|
494
|
+
for (const collectionName of collectionNames)
|
|
495
|
+
await this.vectorStores.get(collectionName)?.deleteByFilePath(options.relativePath);
|
|
496
|
+
}
|
|
497
|
+
this.reportProgress({ stage: 'vectorizing', percent: 85, message: `正在写入 HNSWLib 向量库,共 ${chunks.length} 个切片`, chunkCount: chunks.length });
|
|
431
498
|
const indexer = new VectorIndexer(this.embeddingProvider, this.vectorStores);
|
|
432
499
|
try {
|
|
433
|
-
const results = await indexer.indexChunks(chunks
|
|
434
|
-
|
|
435
|
-
|
|
500
|
+
const results = await indexer.indexChunks(chunks, {
|
|
501
|
+
onProgress: progress => {
|
|
502
|
+
const percent = 85 + Math.round((progress.processedChunks / Math.max(1, progress.totalChunks)) * 14);
|
|
503
|
+
const message = `正在分批向量化并写入:${progress.processedChunks}/${progress.totalChunks} 个切片`;
|
|
504
|
+
this.reportProgress({ stage: 'vectorizing', percent, message, chunkCount: progress.totalChunks });
|
|
505
|
+
if (options.relativePath)
|
|
506
|
+
this.updateJobsForFile(options.relativePath, 'INDEXING', percent, message);
|
|
507
|
+
},
|
|
508
|
+
});
|
|
509
|
+
const actualModel = results[0]?.embeddingModel ?? this.embeddingProvider.model;
|
|
510
|
+
const actualDimension = results[0]?.embeddingDimension ?? this.embeddingProvider.dimensions;
|
|
511
|
+
this.store.setMetadata('embedding_model', actualModel);
|
|
512
|
+
this.store.setMetadata('embedding_dimension', String(actualDimension));
|
|
436
513
|
this.store.setMetadata('vector_indexed_chunks', String(chunks.length));
|
|
437
514
|
this.store.setMetadata('vector_index_status', 'ready');
|
|
438
515
|
this.store.setMetadata('vector_index_error', '');
|
|
439
516
|
this.store.setMetadata('last_vector_index_at', String(Date.now()));
|
|
517
|
+
if (options.relativePath)
|
|
518
|
+
this.updateJobsForFile(options.relativePath, 'SUCCESS', 100, '解析、切片和向量索引完成');
|
|
440
519
|
return results;
|
|
441
520
|
}
|
|
442
521
|
catch (error) {
|
|
@@ -444,7 +523,9 @@ export class KnowledgeBaseManager {
|
|
|
444
523
|
this.store.setMetadata('vector_index_status', 'error');
|
|
445
524
|
this.store.setMetadata('vector_index_error', message);
|
|
446
525
|
this.store.setMetadata('last_vector_index_at', String(Date.now()));
|
|
447
|
-
this.reportProgress({ stage: 'error', percent: 85, message: '
|
|
526
|
+
this.reportProgress({ stage: 'error', percent: 85, message: 'HNSWLib 向量入库失败', chunkCount: chunks.length, vectorStatus: this.getVectorStatus() });
|
|
527
|
+
if (options.relativePath)
|
|
528
|
+
this.updateJobsForFile(options.relativePath, 'ERROR', 100, 'HNSWLib 向量入库失败', message);
|
|
448
529
|
return [];
|
|
449
530
|
}
|
|
450
531
|
}
|
|
@@ -462,13 +543,23 @@ export class KnowledgeBaseManager {
|
|
|
462
543
|
lastIndexedAt: stats.lastIndexedAt,
|
|
463
544
|
};
|
|
464
545
|
}
|
|
546
|
+
listIndexJobsByPrefix(prefix) {
|
|
547
|
+
return this.store.listIndexJobsByPrefix(prefix);
|
|
548
|
+
}
|
|
549
|
+
countPendingIndexJobs() {
|
|
550
|
+
return this.store.countPendingIndexJobs();
|
|
551
|
+
}
|
|
552
|
+
failPendingIndexJobs(message) {
|
|
553
|
+
for (const job of this.store.listPendingIndexJobs(1_000))
|
|
554
|
+
this.store.updateIndexJob(job.id, { status: 'ERROR', percent: 100, message, errorMessage: message });
|
|
555
|
+
}
|
|
465
556
|
getVectorStatus() {
|
|
466
557
|
return {
|
|
467
558
|
status: this.store.getMetadata('vector_index_status') ?? 'pending',
|
|
468
559
|
error: this.store.getMetadata('vector_index_error') || undefined,
|
|
469
560
|
indexedChunks: Number(this.store.getMetadata('vector_indexed_chunks') ?? 0),
|
|
470
561
|
lastIndexedAt: Number(this.store.getMetadata('last_vector_index_at') ?? 0),
|
|
471
|
-
backend: `SQLite +
|
|
562
|
+
backend: `SQLite + HNSWLib (${this.vectorRoot})`,
|
|
472
563
|
};
|
|
473
564
|
}
|
|
474
565
|
async rewriteQueries(query) {
|
|
@@ -599,10 +690,22 @@ ${resultsText}
|
|
|
599
690
|
return this.heuristicRerank(query, items);
|
|
600
691
|
}
|
|
601
692
|
}
|
|
693
|
+
hydrateVectorResultsFromSqlite(items) {
|
|
694
|
+
return items.map(item => {
|
|
695
|
+
if (!item.rowid)
|
|
696
|
+
return item;
|
|
697
|
+
const chunk = this.store.getChunkByRowid(item.rowid);
|
|
698
|
+
if (!chunk)
|
|
699
|
+
return item;
|
|
700
|
+
const hydrated = this.toFederatedItem({ ...chunk, score: item.score, scoreDetails: item.scoreDetails }, 'vector');
|
|
701
|
+
return { ...hydrated, score: item.score, scoreDetails: item.scoreDetails };
|
|
702
|
+
});
|
|
703
|
+
}
|
|
602
704
|
toFederatedItem(result, source) {
|
|
603
705
|
const metadata = this.parseMetadata(result.metadataJson);
|
|
604
706
|
return {
|
|
605
707
|
id: result.id,
|
|
708
|
+
rowid: result.rowid,
|
|
606
709
|
content: result.content,
|
|
607
710
|
filePath: result.relativePath,
|
|
608
711
|
scope: this.scope === 'global' ? 'global' : 'project',
|
|
@@ -618,32 +721,70 @@ ${resultsText}
|
|
|
618
721
|
facets: this.metadataFacets(metadata),
|
|
619
722
|
};
|
|
620
723
|
}
|
|
621
|
-
|
|
724
|
+
mergeHybridRankedLists(lists, limit, weights = this.retrievalWeights()) {
|
|
725
|
+
const k = Number(process.env.KB_RETRIEVAL_RRF_K ?? 60);
|
|
726
|
+
const byKey = new Map();
|
|
727
|
+
for (const list of lists) {
|
|
728
|
+
const sourceWeight = list.source === 'vector' ? (weights.vector ?? 0.9) : (weights.keyword ?? 1);
|
|
729
|
+
const rewriteWeight = list.queryIndex === 0 ? 1 : (weights.rewrite ?? 0.72);
|
|
730
|
+
list.items.forEach((item, index) => {
|
|
731
|
+
const key = this.contextKey(item);
|
|
732
|
+
const rankScore = sourceWeight * rewriteWeight / (k + index + 1);
|
|
733
|
+
const existing = byKey.get(key);
|
|
734
|
+
if (!existing) {
|
|
735
|
+
byKey.set(key, {
|
|
736
|
+
...item,
|
|
737
|
+
score: rankScore,
|
|
738
|
+
source: list.source,
|
|
739
|
+
scoreDetails: { ...item.scoreDetails, hybridScore: rankScore },
|
|
740
|
+
seenSources: new Set([list.source]),
|
|
741
|
+
});
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
existing.score += rankScore;
|
|
745
|
+
existing.seenSources?.add(list.source);
|
|
746
|
+
existing.source = existing.seenSources && existing.seenSources.size > 1 ? 'hybrid' : existing.source;
|
|
747
|
+
existing.scoreDetails = { ...existing.scoreDetails, ...item.scoreDetails, hybridScore: existing.score };
|
|
748
|
+
if (item.score > (existing.scoreDetails?.keywordScore ?? existing.scoreDetails?.vectorScore ?? 0)) {
|
|
749
|
+
existing.content = item.content;
|
|
750
|
+
existing.sectionTitle = item.sectionTitle ?? existing.sectionTitle;
|
|
751
|
+
existing.chunkIndex = item.chunkIndex ?? existing.chunkIndex;
|
|
752
|
+
existing.parentId = item.parentId ?? existing.parentId;
|
|
753
|
+
}
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
const hybridBonus = weights.hybridBonus ?? 0.35;
|
|
757
|
+
return [...byKey.values()].map(item => {
|
|
758
|
+
const bonus = item.seenSources && item.seenSources.size > 1 ? item.score * hybridBonus : 0;
|
|
759
|
+
const { seenSources: _seenSources, ...result } = item;
|
|
760
|
+
return { ...result, score: result.score + bonus, scoreDetails: { ...result.scoreDetails, hybridScore: result.score + bonus } };
|
|
761
|
+
}).sort((a, b) => b.score - a.score).slice(0, limit);
|
|
762
|
+
}
|
|
763
|
+
mergeContexts(items, limit) {
|
|
622
764
|
const byKey = new Map();
|
|
623
765
|
for (const item of items) {
|
|
624
|
-
const key =
|
|
625
|
-
const sourceWeight = item.source === 'vector' ? (weights.vector ?? 0.9) : item.source === 'keyword' ? (weights.keyword ?? 1) : 1.1;
|
|
626
|
-
const weighted = { ...item, score: item.score * sourceWeight, scoreDetails: { ...item.scoreDetails, hybridScore: item.score * sourceWeight } };
|
|
766
|
+
const key = this.contextKey(item);
|
|
627
767
|
const existing = byKey.get(key);
|
|
628
768
|
if (!existing) {
|
|
629
|
-
byKey.set(key,
|
|
630
|
-
|
|
631
|
-
else {
|
|
632
|
-
const score = Math.max(existing.score, weighted.score) + Math.min(existing.score, weighted.score) * (weights.hybridBonus ?? 0.35);
|
|
633
|
-
byKey.set(key, {
|
|
634
|
-
...existing,
|
|
635
|
-
score,
|
|
636
|
-
source: existing.source === weighted.source ? existing.source : 'hybrid',
|
|
637
|
-
scoreDetails: {
|
|
638
|
-
...existing.scoreDetails,
|
|
639
|
-
...weighted.scoreDetails,
|
|
640
|
-
hybridScore: score,
|
|
641
|
-
},
|
|
642
|
-
});
|
|
769
|
+
byKey.set(key, item);
|
|
770
|
+
continue;
|
|
643
771
|
}
|
|
772
|
+
const score = Math.max(existing.score, item.score);
|
|
773
|
+
byKey.set(key, {
|
|
774
|
+
...existing,
|
|
775
|
+
score,
|
|
776
|
+
source: existing.source === item.source ? existing.source : 'hybrid',
|
|
777
|
+
content: existing.content.length >= item.content.length ? existing.content : item.content,
|
|
778
|
+
scoreDetails: { ...existing.scoreDetails, ...item.scoreDetails, hybridScore: score },
|
|
779
|
+
});
|
|
644
780
|
}
|
|
645
781
|
return [...byKey.values()].sort((a, b) => b.score - a.score).slice(0, limit);
|
|
646
782
|
}
|
|
783
|
+
contextKey(item) {
|
|
784
|
+
if (item.rowid)
|
|
785
|
+
return `${item.scope}:rowid:${item.rowid}`;
|
|
786
|
+
return `${item.scope}:${item.filePath}:${item.parentId ?? item.chunkIndex ?? item.id}`;
|
|
787
|
+
}
|
|
647
788
|
parseChunkIndex(id) {
|
|
648
789
|
const match = /#(\d+)$/u.exec(id);
|
|
649
790
|
return match ? Number(match[1]) : 0;
|
|
@@ -688,10 +829,16 @@ ${resultsText}
|
|
|
688
829
|
reportProgress(progress) {
|
|
689
830
|
this.onProgress?.(progress);
|
|
690
831
|
}
|
|
832
|
+
updateJobsForFile(relativePath, status, percent, message, errorMessage) {
|
|
833
|
+
for (const job of this.store.listActiveIndexJobsByPath(relativePath)) {
|
|
834
|
+
this.store.updateIndexJob(job.id, { status, percent, message, errorMessage });
|
|
835
|
+
}
|
|
836
|
+
}
|
|
691
837
|
ensureVectorStore(collectionName) {
|
|
692
838
|
if (this.vectorStores.has(collectionName))
|
|
693
839
|
return;
|
|
694
|
-
|
|
840
|
+
const safeName = collectionName.replace(/[^a-zA-Z0-9_.-]/gu, '_');
|
|
841
|
+
this.vectorStores.set(collectionName, new HNSWVectorStore(collectionName, path.join(this.vectorRoot, `${safeName}.hnsw`), this.embeddingProvider.dimensions));
|
|
695
842
|
}
|
|
696
843
|
async deleteVectorFile(collectionName, relativePath) {
|
|
697
844
|
this.ensureVectorStore(collectionName);
|
|
@@ -706,10 +853,15 @@ ${resultsText}
|
|
|
706
853
|
async ensureVectorIndexFresh(chunkCount, force = false) {
|
|
707
854
|
if (chunkCount === 0)
|
|
708
855
|
return;
|
|
856
|
+
for (const record of this.store.listRecords())
|
|
857
|
+
this.ensureVectorStore(record.collectionName);
|
|
858
|
+
if ([...this.vectorStores.values()].some(store => store.needsRebuild?.())) {
|
|
859
|
+
await this.indexVectors();
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
709
862
|
const indexedChunks = Number(this.store.getMetadata('vector_indexed_chunks') ?? 0);
|
|
710
|
-
const
|
|
711
|
-
|
|
712
|
-
if (!force && indexedChunks === chunkCount && model === this.embeddingProvider.model && dimension === String(this.embeddingProvider.dimensions))
|
|
863
|
+
const status = this.store.getMetadata('vector_index_status');
|
|
864
|
+
if (!force && indexedChunks === chunkCount && status === 'ready')
|
|
713
865
|
return;
|
|
714
866
|
await this.indexVectors();
|
|
715
867
|
}
|
|
@@ -2,15 +2,24 @@ import type { FederatedResult, RetrievalWeights, SearchFilters, SearchScope } fr
|
|
|
2
2
|
import type { CrossProjectDuplicate, ProjectInfo } from '../types.js';
|
|
3
3
|
import { KnowledgeBaseManager } from './knowledge-base-manager.js';
|
|
4
4
|
import type { LLMSearchProvider } from '../llm/llm-search-provider.js';
|
|
5
|
+
/** 多项目管理器,管理多个项目的知识库并支持跨项目搜索 */
|
|
5
6
|
export declare class MultiProjectManager {
|
|
6
7
|
private readonly storageRoot;
|
|
7
8
|
private readonly llmProvider?;
|
|
8
9
|
private readonly registry;
|
|
9
10
|
private readonly configManager;
|
|
10
11
|
private readonly projects;
|
|
12
|
+
private readonly lastSearchIndexCheck;
|
|
11
13
|
private globalKB?;
|
|
12
14
|
constructor(storageRoot?: string, llmProvider?: LLMSearchProvider);
|
|
15
|
+
/**
|
|
16
|
+
* 获取或创建指定项目的知识库管理器
|
|
17
|
+
* @param projectRoot 项目根目录
|
|
18
|
+
*/
|
|
13
19
|
getProject(projectRoot: string): Promise<KnowledgeBaseManager>;
|
|
20
|
+
/**
|
|
21
|
+
* 获取或初始化全局知识库
|
|
22
|
+
*/
|
|
14
23
|
getGlobalKB(): Promise<KnowledgeBaseManager>;
|
|
15
24
|
listProjects(): Promise<ProjectInfo[]>;
|
|
16
25
|
search(projectRoot: string, query: string, options?: {
|
|
@@ -28,6 +37,7 @@ export declare class MultiProjectManager {
|
|
|
28
37
|
forgetProject(projectId: string): Promise<void>;
|
|
29
38
|
closeProject(projectId: string): Promise<void>;
|
|
30
39
|
shutdown(): Promise<void>;
|
|
40
|
+
private ensureFreshForSearch;
|
|
31
41
|
private mergeDebug;
|
|
32
42
|
private updateRegistry;
|
|
33
43
|
}
|
|
@@ -6,12 +6,14 @@ import { KnowledgeBaseManager } from './knowledge-base-manager.js';
|
|
|
6
6
|
import { computeProjectId } from './project-id.js';
|
|
7
7
|
import { getProjectKbPath, ProjectConfigManager } from './project-config.js';
|
|
8
8
|
import { ProjectRegistry } from './project-registry.js';
|
|
9
|
+
/** 多项目管理器,管理多个项目的知识库并支持跨项目搜索 */
|
|
9
10
|
export class MultiProjectManager {
|
|
10
11
|
storageRoot;
|
|
11
12
|
llmProvider;
|
|
12
13
|
registry;
|
|
13
14
|
configManager;
|
|
14
15
|
projects = new Map();
|
|
16
|
+
lastSearchIndexCheck = new Map();
|
|
15
17
|
globalKB;
|
|
16
18
|
constructor(storageRoot = path.join(os.homedir(), USER_DATA_DIR), llmProvider) {
|
|
17
19
|
this.storageRoot = storageRoot;
|
|
@@ -19,6 +21,10 @@ export class MultiProjectManager {
|
|
|
19
21
|
this.registry = new ProjectRegistry(path.join(storageRoot, 'projects', 'registry.db'));
|
|
20
22
|
this.configManager = new ProjectConfigManager(storageRoot);
|
|
21
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* 获取或创建指定项目的知识库管理器
|
|
26
|
+
* @param projectRoot 项目根目录
|
|
27
|
+
*/
|
|
22
28
|
async getProject(projectRoot) {
|
|
23
29
|
const resolvedRoot = path.resolve(projectRoot);
|
|
24
30
|
const projectId = computeProjectId(resolvedRoot);
|
|
@@ -39,6 +45,9 @@ export class MultiProjectManager {
|
|
|
39
45
|
this.updateRegistry(manager, resolvedRoot, config.projectName, config.lastOpenedAt);
|
|
40
46
|
return manager;
|
|
41
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* 获取或初始化全局知识库
|
|
50
|
+
*/
|
|
42
51
|
async getGlobalKB() {
|
|
43
52
|
if (this.globalKB)
|
|
44
53
|
return this.globalKB;
|
|
@@ -55,7 +64,7 @@ export class MultiProjectManager {
|
|
|
55
64
|
const limit = options.limit ?? 10;
|
|
56
65
|
const scope = options.scope ?? 'all';
|
|
57
66
|
const project = await this.getProject(projectRoot);
|
|
58
|
-
await
|
|
67
|
+
await this.ensureFreshForSearch(projectRoot, project);
|
|
59
68
|
if (scope === 'project')
|
|
60
69
|
return project.hybridSearch(query, { limit, weights: options.weights });
|
|
61
70
|
const projectResults = scope === 'all'
|
|
@@ -76,7 +85,7 @@ export class MultiProjectManager {
|
|
|
76
85
|
async semanticSearch(projectRoot, query, options = {}) {
|
|
77
86
|
const scope = options.scope ?? 'all';
|
|
78
87
|
const project = await this.getProject(projectRoot);
|
|
79
|
-
await
|
|
88
|
+
await this.ensureFreshForSearch(projectRoot, project);
|
|
80
89
|
if (scope === 'project') {
|
|
81
90
|
return project.semanticSearch(query, options);
|
|
82
91
|
}
|
|
@@ -141,6 +150,16 @@ export class MultiProjectManager {
|
|
|
141
150
|
this.globalKB = undefined;
|
|
142
151
|
this.registry.close();
|
|
143
152
|
}
|
|
153
|
+
async ensureFreshForSearch(projectRoot, project) {
|
|
154
|
+
const now = Date.now();
|
|
155
|
+
const key = path.resolve(projectRoot);
|
|
156
|
+
const ttlMs = Number(process.env.KB_SEARCH_INDEX_TTL_MS ?? 30000);
|
|
157
|
+
const last = this.lastSearchIndexCheck.get(key) ?? 0;
|
|
158
|
+
if (now - last < ttlMs)
|
|
159
|
+
return;
|
|
160
|
+
this.lastSearchIndexCheck.set(key, now);
|
|
161
|
+
await project.incrementalIndex({ vectorMode: 'defer' });
|
|
162
|
+
}
|
|
144
163
|
mergeDebug(projectDebug, globalDebug) {
|
|
145
164
|
if (!projectDebug && !globalDebug)
|
|
146
165
|
return undefined;
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import type { ProjectConfig } from '../types.js';
|
|
2
|
+
/** 获取项目配置文件路径 */
|
|
2
3
|
export declare function getProjectConfigPath(projectRoot: string, storageRoot?: string): string;
|
|
4
|
+
/** 获取项目知识库目录路径 */
|
|
3
5
|
export declare function getProjectKbPath(projectRoot: string): string;
|
|
6
|
+
/** 确保项目存在 CUSTOMIZE.md 文件(如不存在则创建默认模板) */
|
|
4
7
|
export declare function ensureProjectCustomizeFile(projectRoot: string): void;
|
|
8
|
+
/** 项目配置管理器,负责加载和保存项目配置 */
|
|
5
9
|
export declare class ProjectConfigManager {
|
|
6
10
|
private readonly storageRoot;
|
|
7
11
|
constructor(storageRoot?: string);
|
|
@@ -3,10 +3,12 @@ import * as path from 'node:path';
|
|
|
3
3
|
import * as os from 'node:os';
|
|
4
4
|
import { DEFAULT_CATEGORY_DIRS, KNOWLEDGE_BASE_DIR, USER_DATA_DIR } from '../constants.js';
|
|
5
5
|
import { computeProjectId } from './project-id.js';
|
|
6
|
+
/** 获取项目配置文件路径 */
|
|
6
7
|
export function getProjectConfigPath(projectRoot, storageRoot = path.join(os.homedir(), USER_DATA_DIR)) {
|
|
7
8
|
const projectId = computeProjectId(projectRoot);
|
|
8
9
|
return path.join(storageRoot, 'projects', projectId, 'project.json');
|
|
9
10
|
}
|
|
11
|
+
/** 获取项目知识库目录路径 */
|
|
10
12
|
export function getProjectKbPath(projectRoot) {
|
|
11
13
|
return path.join(projectRoot, KNOWLEDGE_BASE_DIR);
|
|
12
14
|
}
|
|
@@ -27,6 +29,7 @@ const DEFAULT_CUSTOMIZE_MD = `# Customize Agent 配置示例
|
|
|
27
29
|
- 使用中文回复。
|
|
28
30
|
- 重要改动完成后运行必要的类型检查或构建检查。
|
|
29
31
|
`;
|
|
32
|
+
/** 确保项目存在 CUSTOMIZE.md 文件(如不存在则创建默认模板) */
|
|
30
33
|
export function ensureProjectCustomizeFile(projectRoot) {
|
|
31
34
|
const filePath = path.join(projectRoot, 'CUSTOMIZE.md');
|
|
32
35
|
if (!fs.existsSync(filePath)) {
|
|
@@ -34,6 +37,7 @@ export function ensureProjectCustomizeFile(projectRoot) {
|
|
|
34
37
|
fs.writeFileSync(filePath, DEFAULT_CUSTOMIZE_MD, 'utf8');
|
|
35
38
|
}
|
|
36
39
|
}
|
|
40
|
+
/** 项目配置管理器,负责加载和保存项目配置 */
|
|
37
41
|
export class ProjectConfigManager {
|
|
38
42
|
storageRoot;
|
|
39
43
|
constructor(storageRoot = path.join(os.homedir(), USER_DATA_DIR)) {
|
package/dist/core/project-id.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import * as crypto from 'node:crypto';
|
|
2
2
|
import * as path from 'node:path';
|
|
3
|
+
/**
|
|
4
|
+
* 使用项目根目录计算项目唯一标识
|
|
5
|
+
* @param projectRoot 项目根目录路径
|
|
6
|
+
* @returns 项目 ID(SHA-256 前 12 位)
|
|
7
|
+
*/
|
|
3
8
|
export function computeProjectId(projectRoot) {
|
|
4
9
|
return crypto.createHash('sha256')
|
|
5
10
|
.update(path.resolve(projectRoot))
|