@customize-agent/knowledge 4.0.4 → 4.0.6
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 +3 -0
- package/dist/core/knowledge-base-manager.js +57 -12
- package/dist/index.d.ts +1 -1
- package/dist/vector/hnsw-vector-store.d.ts +6 -3
- package/dist/vector/hnsw-vector-store.js +25 -6
- package/dist/vector/types.d.ts +6 -2
- package/dist/vector/vector-indexer.d.ts +1 -0
- package/dist/vector/vector-indexer.js +4 -3
- package/package.json +1 -1
|
@@ -133,7 +133,10 @@ export declare class KnowledgeBaseManager {
|
|
|
133
133
|
indexVectors(options?: {
|
|
134
134
|
collectionName?: string;
|
|
135
135
|
relativePath?: string;
|
|
136
|
+
relativePaths?: string[];
|
|
137
|
+
cleanupCollectionNames?: Iterable<string>;
|
|
136
138
|
limit?: number;
|
|
139
|
+
rebuild?: boolean;
|
|
137
140
|
}): Promise<VectorIndexResult[]>;
|
|
138
141
|
getProjectConfig(): ProjectConfig | undefined;
|
|
139
142
|
getStats(): KnowledgeBaseStats;
|
|
@@ -105,6 +105,7 @@ export class KnowledgeBaseManager {
|
|
|
105
105
|
const tracker = new ChangeTracker(this.store);
|
|
106
106
|
const diff = await tracker.computeDiff(diskFiles, this.classifier, this.kbPath);
|
|
107
107
|
const onlyRelativePaths = options.onlyRelativePaths ? new Set(options.onlyRelativePaths) : undefined;
|
|
108
|
+
let vectorDeletesApplied = 0;
|
|
108
109
|
if (onlyRelativePaths) {
|
|
109
110
|
diff.newFiles = diff.newFiles.filter(file => onlyRelativePaths.has(file.relativePath));
|
|
110
111
|
diff.modifiedFiles = diff.modifiedFiles.filter(file => onlyRelativePaths.has(file.relativePath));
|
|
@@ -118,18 +119,24 @@ export class KnowledgeBaseManager {
|
|
|
118
119
|
}
|
|
119
120
|
for (const deleted of diff.deletedFiles) {
|
|
120
121
|
await this.deleteVectorFile(deleted.collectionName, deleted.relativePath);
|
|
122
|
+
vectorDeletesApplied += 1;
|
|
121
123
|
this.store.deleteRecord(deleted.relativePath);
|
|
122
124
|
this.updateJobsForFile(deleted.relativePath, 'SUCCESS', 100, '文件已删除,索引记录和向量已清理');
|
|
123
125
|
}
|
|
124
126
|
const now = Date.now();
|
|
125
127
|
const indexedBefore = [...this.store.loadActiveRecords().values()];
|
|
126
128
|
const filesToIndex = [...diff.newFiles, ...diff.modifiedFiles];
|
|
129
|
+
const vectorRelativePaths = [];
|
|
130
|
+
const changedCollectionNames = new Set();
|
|
127
131
|
for (const [index, file] of filesToIndex.entries()) {
|
|
128
132
|
const hash = tracker.hashFile(file.absolutePath);
|
|
129
133
|
const duplicate = this.store.findExactDuplicate(hash, file.relativePath);
|
|
130
134
|
const collectionName = this.scope === 'global'
|
|
131
135
|
? this.collections.getCollectionName('global', file.category)
|
|
132
136
|
: this.collections.getCollectionName('project', file.category, this.projectId);
|
|
137
|
+
const previousRecord = indexedBefore.find(record => record.relativePath === file.relativePath);
|
|
138
|
+
if (previousRecord?.collectionName)
|
|
139
|
+
changedCollectionNames.add(previousRecord.collectionName);
|
|
133
140
|
const basePercent = filesToIndex.length === 0 ? 40 : 20 + Math.round((index / filesToIndex.length) * 45);
|
|
134
141
|
this.updateJobsForFile(file.relativePath, 'PARSING', basePercent, `正在解析 ${file.relativePath}`);
|
|
135
142
|
this.reportProgress({ stage: 'parsing', percent: basePercent, message: `正在解析 ${file.relativePath}`, filePath: file.relativePath });
|
|
@@ -244,6 +251,8 @@ export class KnowledgeBaseManager {
|
|
|
244
251
|
format: file.format,
|
|
245
252
|
collectionName,
|
|
246
253
|
});
|
|
254
|
+
vectorRelativePaths.push(file.relativePath);
|
|
255
|
+
changedCollectionNames.add(collectionName);
|
|
247
256
|
this.updateJobsForFile(file.relativePath, options.vectorMode === 'defer' ? 'SUCCESS' : 'INDEXING', options.vectorMode === 'defer' ? 100 : 85, options.vectorMode === 'defer' ? '解析和切片已完成' : '等待向量入库');
|
|
248
257
|
}
|
|
249
258
|
const stats = this.getStats();
|
|
@@ -257,7 +266,7 @@ export class KnowledgeBaseManager {
|
|
|
257
266
|
this.reportProgress({ stage: 'vectorizing', percent: 85, message: '解析和切片已完成,向量入库转入后台/稍后执行', chunkCount: stats.chunkCount, vectorStatus: this.getVectorStatus() });
|
|
258
267
|
}
|
|
259
268
|
else {
|
|
260
|
-
await this.ensureVectorIndexFresh(stats.chunkCount,
|
|
269
|
+
await this.ensureVectorIndexFresh(stats.chunkCount, { changedRelativePaths: vectorRelativePaths, changedCollectionNames, deletesApplied: vectorDeletesApplied });
|
|
261
270
|
}
|
|
262
271
|
this.lastSkippedFiles = diff.skippedFiles;
|
|
263
272
|
const vectorStatus = this.getVectorStatus();
|
|
@@ -482,24 +491,45 @@ export class KnowledgeBaseManager {
|
|
|
482
491
|
return this.store.listIgnoreRules();
|
|
483
492
|
}
|
|
484
493
|
async indexVectors(options = {}) {
|
|
485
|
-
const chunks =
|
|
494
|
+
const chunks = options.relativePaths?.length
|
|
495
|
+
? options.relativePaths.flatMap(relativePath => this.store.listChunks({ collectionName: options.collectionName, relativePath }))
|
|
496
|
+
: this.store.listChunks(options);
|
|
486
497
|
const collectionNames = new Set(chunks.map(chunk => chunk.collectionName));
|
|
487
|
-
|
|
498
|
+
const cleanupCollectionNames = new Set([...collectionNames, ...(options.cleanupCollectionNames ?? [])]);
|
|
499
|
+
for (const collectionName of cleanupCollectionNames)
|
|
488
500
|
this.ensureVectorStore(collectionName);
|
|
489
|
-
if (!options.relativePath) {
|
|
501
|
+
if (options.rebuild || (!options.relativePath && !options.relativePaths?.length)) {
|
|
490
502
|
for (const collectionName of collectionNames)
|
|
491
503
|
await this.vectorStores.get(collectionName)?.clearCollection?.();
|
|
492
504
|
}
|
|
493
505
|
else {
|
|
494
|
-
for (const
|
|
495
|
-
|
|
506
|
+
for (const relativePath of options.relativePaths ?? [options.relativePath].filter(Boolean)) {
|
|
507
|
+
for (const collectionName of cleanupCollectionNames)
|
|
508
|
+
await this.vectorStores.get(collectionName)?.deleteByFilePath(relativePath, { persist: false });
|
|
509
|
+
}
|
|
510
|
+
if (chunks.length === 0) {
|
|
511
|
+
for (const collectionName of cleanupCollectionNames)
|
|
512
|
+
await this.vectorStores.get(collectionName)?.flush?.();
|
|
513
|
+
}
|
|
496
514
|
}
|
|
497
515
|
this.reportProgress({ stage: 'vectorizing', percent: 85, message: `正在写入 HNSWLib 向量库,共 ${chunks.length} 个切片`, chunkCount: chunks.length });
|
|
516
|
+
if (chunks.length === 0) {
|
|
517
|
+
const totalChunks = this.getStats().chunkCount;
|
|
518
|
+
this.store.setMetadata('vector_indexed_chunks', String(totalChunks));
|
|
519
|
+
this.store.setMetadata('vector_index_status', 'ready');
|
|
520
|
+
this.store.setMetadata('vector_index_error', '');
|
|
521
|
+
this.store.setMetadata('last_vector_index_at', String(Date.now()));
|
|
522
|
+
return [];
|
|
523
|
+
}
|
|
498
524
|
const indexer = new VectorIndexer(this.embeddingProvider, this.vectorStores);
|
|
525
|
+
let lastVectorPercent = -1;
|
|
499
526
|
try {
|
|
500
527
|
const results = await indexer.indexChunks(chunks, {
|
|
501
528
|
onProgress: progress => {
|
|
502
529
|
const percent = 85 + Math.round((progress.processedChunks / Math.max(1, progress.totalChunks)) * 14);
|
|
530
|
+
if (percent === lastVectorPercent && progress.processedChunks < progress.totalChunks)
|
|
531
|
+
return;
|
|
532
|
+
lastVectorPercent = percent;
|
|
503
533
|
const message = `正在分批向量化并写入:${progress.processedChunks}/${progress.totalChunks} 个切片`;
|
|
504
534
|
this.reportProgress({ stage: 'vectorizing', percent, message, chunkCount: progress.totalChunks });
|
|
505
535
|
if (options.relativePath)
|
|
@@ -508,9 +538,10 @@ export class KnowledgeBaseManager {
|
|
|
508
538
|
});
|
|
509
539
|
const actualModel = results[0]?.embeddingModel ?? this.embeddingProvider.model;
|
|
510
540
|
const actualDimension = results[0]?.embeddingDimension ?? this.embeddingProvider.dimensions;
|
|
541
|
+
const totalChunks = this.getStats().chunkCount;
|
|
511
542
|
this.store.setMetadata('embedding_model', actualModel);
|
|
512
543
|
this.store.setMetadata('embedding_dimension', String(actualDimension));
|
|
513
|
-
this.store.setMetadata('vector_indexed_chunks', String(
|
|
544
|
+
this.store.setMetadata('vector_indexed_chunks', String(totalChunks));
|
|
514
545
|
this.store.setMetadata('vector_index_status', 'ready');
|
|
515
546
|
this.store.setMetadata('vector_index_error', '');
|
|
516
547
|
this.store.setMetadata('last_vector_index_at', String(Date.now()));
|
|
@@ -850,20 +881,34 @@ ${resultsText}
|
|
|
850
881
|
this.store.setMetadata('vector_index_error', error instanceof Error ? error.message : String(error));
|
|
851
882
|
}
|
|
852
883
|
}
|
|
853
|
-
async ensureVectorIndexFresh(chunkCount,
|
|
884
|
+
async ensureVectorIndexFresh(chunkCount, options = {}) {
|
|
854
885
|
if (chunkCount === 0)
|
|
855
886
|
return;
|
|
856
887
|
for (const record of this.store.listRecords())
|
|
857
888
|
this.ensureVectorStore(record.collectionName);
|
|
858
|
-
if ([...this.vectorStores.values()].some(store => store.needsRebuild?.())) {
|
|
859
|
-
await this.indexVectors();
|
|
889
|
+
if (options.rebuild || [...this.vectorStores.values()].some(store => store.needsRebuild?.())) {
|
|
890
|
+
await this.indexVectors({ rebuild: true });
|
|
860
891
|
return;
|
|
861
892
|
}
|
|
862
893
|
const indexedChunks = Number(this.store.getMetadata('vector_indexed_chunks') ?? 0);
|
|
863
894
|
const status = this.store.getMetadata('vector_index_status');
|
|
864
|
-
|
|
895
|
+
const changedRelativePaths = [...new Set(options.changedRelativePaths ?? [])];
|
|
896
|
+
if (changedRelativePaths.length === 0 && options.deletesApplied && status === 'ready') {
|
|
897
|
+
this.store.setMetadata('vector_indexed_chunks', String(chunkCount));
|
|
898
|
+
this.store.setMetadata('vector_index_status', 'ready');
|
|
899
|
+
this.store.setMetadata('vector_index_error', '');
|
|
900
|
+
this.store.setMetadata('last_vector_index_at', String(Date.now()));
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
if (changedRelativePaths.length > 0 && status === 'ready') {
|
|
904
|
+
for (const collectionName of options.changedCollectionNames ?? [])
|
|
905
|
+
this.ensureVectorStore(collectionName);
|
|
906
|
+
await this.indexVectors({ relativePaths: changedRelativePaths, cleanupCollectionNames: options.changedCollectionNames });
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
909
|
+
if (indexedChunks === chunkCount && status === 'ready')
|
|
865
910
|
return;
|
|
866
|
-
await this.indexVectors();
|
|
911
|
+
await this.indexVectors({ rebuild: true });
|
|
867
912
|
}
|
|
868
913
|
hasUsableContent(text, metadata) {
|
|
869
914
|
const coverage = String(metadata.contentCoverage ?? '');
|
package/dist/index.d.ts
CHANGED
|
@@ -15,7 +15,7 @@ export { ensureProjectCustomizeFile, getProjectConfigPath, getProjectKbPath, Pro
|
|
|
15
15
|
export { ProjectRegistry } from './core/project-registry.js';
|
|
16
16
|
export { HNSWVectorStore } from './vector/hnsw-vector-store.js';
|
|
17
17
|
export { CollectionManager, globalCollectionName, projectCollectionName } from './vector/collection-manager.js';
|
|
18
|
-
export type { CollectionClient, VectorCollectionInfo, VectorDocument, VectorSearchQuery, VectorSearchResult, VectorStoreInterface } from './vector/types.js';
|
|
18
|
+
export type { CollectionClient, VectorCollectionInfo, VectorDocument, VectorSearchQuery, VectorSearchResult, VectorStoreInterface, VectorWriteOptions } from './vector/types.js';
|
|
19
19
|
export { VectorIndexer, type VectorIndexResult } from './vector/vector-indexer.js';
|
|
20
20
|
export { FederationSearch, type FederatedQuery, type FederatedResult, type FederatedSearchItem, type SearchFilters, type SearchScope } from './search/federation-search.js';
|
|
21
21
|
export type { LLMChatMessage, LLMChatOptions, LLMChatResponse, LLMSearchProvider } from './llm/llm-search-provider.js';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { VectorDocument, VectorSearchQuery, VectorSearchResult, VectorStoreInterface } from './types.js';
|
|
1
|
+
import type { VectorDocument, VectorSearchQuery, VectorSearchResult, VectorStoreInterface, VectorWriteOptions } from './types.js';
|
|
2
2
|
/** HNSW(分层可导航小世界图)向量存储,基于 hnswlib-node 实现的高效近似最近邻搜索 */
|
|
3
3
|
export declare class HNSWVectorStore implements VectorStoreInterface {
|
|
4
4
|
readonly collectionName: string;
|
|
@@ -7,15 +7,18 @@ export declare class HNSWVectorStore implements VectorStoreInterface {
|
|
|
7
7
|
private readonly maxElements;
|
|
8
8
|
private index?;
|
|
9
9
|
private deletedSinceRebuild;
|
|
10
|
+
private dirty;
|
|
10
11
|
private readonly documents;
|
|
11
12
|
constructor(collectionName: string, indexPath: string, dimensions?: number, maxElements?: number);
|
|
12
13
|
ensureCollection(): Promise<void>;
|
|
13
|
-
upsert(documents: VectorDocument[]): Promise<void>;
|
|
14
|
+
upsert(documents: VectorDocument[], options?: VectorWriteOptions): Promise<void>;
|
|
14
15
|
clearCollection(): Promise<void>;
|
|
15
|
-
deleteByFilePath(filePath: string): Promise<void>;
|
|
16
|
+
deleteByFilePath(filePath: string, options?: VectorWriteOptions): Promise<void>;
|
|
17
|
+
flush(): Promise<void>;
|
|
16
18
|
needsRebuild(): boolean;
|
|
17
19
|
search(query: VectorSearchQuery): Promise<VectorSearchResult[]>;
|
|
18
20
|
private persist;
|
|
21
|
+
private toStoredDocument;
|
|
19
22
|
private loadDocuments;
|
|
20
23
|
private metadataPath;
|
|
21
24
|
}
|
|
@@ -10,6 +10,7 @@ export class HNSWVectorStore {
|
|
|
10
10
|
maxElements;
|
|
11
11
|
index;
|
|
12
12
|
deletedSinceRebuild = 0;
|
|
13
|
+
dirty = false;
|
|
13
14
|
documents = new Map();
|
|
14
15
|
constructor(collectionName, indexPath, dimensions = 512, maxElements = 500_000) {
|
|
15
16
|
this.collectionName = collectionName;
|
|
@@ -29,16 +30,18 @@ export class HNSWVectorStore {
|
|
|
29
30
|
else
|
|
30
31
|
this.index.initIndex(this.maxElements, 16, 200, 100, true);
|
|
31
32
|
}
|
|
32
|
-
async upsert(documents) {
|
|
33
|
+
async upsert(documents, options = {}) {
|
|
33
34
|
await this.ensureCollection();
|
|
34
35
|
for (const document of documents) {
|
|
35
36
|
const rowid = Number(document.metadata.sqlite_rowid);
|
|
36
37
|
if (!Number.isFinite(rowid) || rowid <= 0)
|
|
37
38
|
throw new Error(`HNSW 向量写入缺少有效 sqlite_rowid: ${document.id}`);
|
|
38
39
|
this.index.addPoint(document.embedding, rowid, true);
|
|
39
|
-
this.documents.set(rowid, document);
|
|
40
|
+
this.documents.set(rowid, this.toStoredDocument(document));
|
|
41
|
+
this.dirty = true;
|
|
40
42
|
}
|
|
41
|
-
|
|
43
|
+
if (options.persist !== false)
|
|
44
|
+
this.persist();
|
|
42
45
|
}
|
|
43
46
|
async clearCollection() {
|
|
44
47
|
if (fs.existsSync(this.indexPath))
|
|
@@ -47,10 +50,11 @@ export class HNSWVectorStore {
|
|
|
47
50
|
fs.rmSync(this.metadataPath(), { force: true });
|
|
48
51
|
this.documents.clear();
|
|
49
52
|
this.deletedSinceRebuild = 0;
|
|
53
|
+
this.dirty = false;
|
|
50
54
|
this.index = undefined;
|
|
51
55
|
await this.ensureCollection();
|
|
52
56
|
}
|
|
53
|
-
async deleteByFilePath(filePath) {
|
|
57
|
+
async deleteByFilePath(filePath, options = {}) {
|
|
54
58
|
await this.ensureCollection();
|
|
55
59
|
for (const [rowid, document] of this.documents.entries()) {
|
|
56
60
|
if (document.metadata.file_path === filePath) {
|
|
@@ -60,9 +64,16 @@ export class HNSWVectorStore {
|
|
|
60
64
|
}
|
|
61
65
|
catch { /* 忽略缺失的标签 */ }
|
|
62
66
|
this.documents.delete(rowid);
|
|
67
|
+
this.dirty = true;
|
|
63
68
|
}
|
|
64
69
|
}
|
|
65
|
-
|
|
70
|
+
if (options.persist !== false)
|
|
71
|
+
this.persist();
|
|
72
|
+
}
|
|
73
|
+
async flush() {
|
|
74
|
+
await this.ensureCollection();
|
|
75
|
+
if (this.dirty)
|
|
76
|
+
this.persist();
|
|
66
77
|
}
|
|
67
78
|
needsRebuild() {
|
|
68
79
|
const total = this.documents.size + this.deletedSinceRebuild;
|
|
@@ -85,6 +96,14 @@ export class HNSWVectorStore {
|
|
|
85
96
|
persist() {
|
|
86
97
|
this.index.writeIndexSync(this.indexPath);
|
|
87
98
|
fs.writeFileSync(this.metadataPath(), JSON.stringify({ deletedSinceRebuild: this.deletedSinceRebuild, documents: [...this.documents.entries()] }), 'utf8');
|
|
99
|
+
this.dirty = false;
|
|
100
|
+
}
|
|
101
|
+
toStoredDocument(document) {
|
|
102
|
+
return {
|
|
103
|
+
id: document.id,
|
|
104
|
+
content: '',
|
|
105
|
+
metadata: document.metadata,
|
|
106
|
+
};
|
|
88
107
|
}
|
|
89
108
|
loadDocuments() {
|
|
90
109
|
const file = this.metadataPath();
|
|
@@ -96,7 +115,7 @@ export class HNSWVectorStore {
|
|
|
96
115
|
this.deletedSinceRebuild = Array.isArray(parsed) ? 0 : Number(parsed.deletedSinceRebuild ?? 0);
|
|
97
116
|
this.documents.clear();
|
|
98
117
|
for (const [rowid, document] of entries)
|
|
99
|
-
this.documents.set(Number(rowid), document);
|
|
118
|
+
this.documents.set(Number(rowid), { id: document.id, content: document.content ?? '', metadata: document.metadata });
|
|
100
119
|
}
|
|
101
120
|
catch {
|
|
102
121
|
this.documents.clear();
|
package/dist/vector/types.d.ts
CHANGED
|
@@ -24,11 +24,15 @@ export interface VectorCollectionInfo {
|
|
|
24
24
|
metadata?: Record<string, unknown>;
|
|
25
25
|
}
|
|
26
26
|
/** 向量存储接口,定义所有向量存储实现必须支持的方法 */
|
|
27
|
+
export interface VectorWriteOptions {
|
|
28
|
+
persist?: boolean;
|
|
29
|
+
}
|
|
27
30
|
export interface VectorStoreInterface {
|
|
28
31
|
readonly collectionName: string;
|
|
29
32
|
ensureCollection(metadata?: Record<string, unknown>): Promise<void>;
|
|
30
|
-
upsert(documents: VectorDocument[]): Promise<void>;
|
|
31
|
-
deleteByFilePath(filePath: string): Promise<void>;
|
|
33
|
+
upsert(documents: VectorDocument[], options?: VectorWriteOptions): Promise<void>;
|
|
34
|
+
deleteByFilePath(filePath: string, options?: VectorWriteOptions): Promise<void>;
|
|
35
|
+
flush?(): Promise<void>;
|
|
32
36
|
clearCollection?(): Promise<void>;
|
|
33
37
|
needsRebuild?(): boolean;
|
|
34
38
|
search(query: VectorSearchQuery): Promise<VectorSearchResult[]>;
|
|
@@ -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 128;
|
|
5
|
+
return Math.max(1, Math.min(512, Math.floor(raw)));
|
|
6
6
|
}
|
|
7
7
|
/** 向量索引器,负责将文本切片生成 Embedding 并写入向量存储 */
|
|
8
8
|
export class VectorIndexer {
|
|
@@ -32,11 +32,12 @@ export class VectorIndexer {
|
|
|
32
32
|
const texts = batchChunks.map(chunk => chunk.content);
|
|
33
33
|
const embeddings = await this.embedDocuments(texts);
|
|
34
34
|
const documents = batchChunks.map((chunk, index) => this.toVectorDocument(chunk, embeddings[index] ?? []));
|
|
35
|
-
await store.upsert(documents);
|
|
35
|
+
await store.upsert(documents, { persist: options.persistEachBatch === true });
|
|
36
36
|
processedChunks += documents.length;
|
|
37
37
|
processedTotalChunks += documents.length;
|
|
38
38
|
options.onProgress?.({ collectionName, processedChunks: processedTotalChunks, totalChunks, batchSize: documents.length });
|
|
39
39
|
}
|
|
40
|
+
await store.flush?.();
|
|
40
41
|
results.push({
|
|
41
42
|
collectionName,
|
|
42
43
|
chunkCount: processedChunks,
|