@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.
@@ -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, hasIndexChanges);
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 = this.store.listChunks(options);
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
- for (const collectionName of collectionNames)
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 collectionName of collectionNames)
495
- await this.vectorStores.get(collectionName)?.deleteByFilePath(options.relativePath);
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(chunks.length));
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, force = false) {
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
- if (!force && indexedChunks === chunkCount && status === 'ready')
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
- this.persist();
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
- this.persist();
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();
@@ -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[]>;
@@ -16,6 +16,7 @@ export interface VectorIndexProgress {
16
16
  }
17
17
  export interface VectorIndexOptions {
18
18
  batchSize?: number;
19
+ persistEachBatch?: boolean;
19
20
  onProgress?: (progress: VectorIndexProgress) => void;
20
21
  }
21
22
  /** 向量索引器,负责将文本切片生成 Embedding 并写入向量存储 */
@@ -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 32;
5
- return Math.max(1, Math.min(256, Math.floor(raw)));
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@customize-agent/knowledge",
3
- "version": "4.0.4",
3
+ "version": "4.0.6",
4
4
  "description": "Local knowledge base infrastructure for customize-agent",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",