@customize-agent/knowledge 4.0.5 → 4.0.7

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.
@@ -108,6 +108,11 @@ export declare class KnowledgeBaseManager {
108
108
  content: Buffer;
109
109
  targetRelativePath?: string;
110
110
  }>, operationId?: string): Promise<import("./index-state-store.js").KnowledgeIndexJob[]>;
111
+ stageUploadedFilePaths(files: Array<{
112
+ fileName: string;
113
+ sourcePath: string;
114
+ targetRelativePath?: string;
115
+ }>, operationId?: string, offset?: number): Promise<import("./index-state-store.js").KnowledgeIndexJob[]>;
111
116
  uploadFiles(files: Array<{
112
117
  fileName: string;
113
118
  content: Buffer;
@@ -133,7 +138,10 @@ export declare class KnowledgeBaseManager {
133
138
  indexVectors(options?: {
134
139
  collectionName?: string;
135
140
  relativePath?: string;
141
+ relativePaths?: string[];
142
+ cleanupCollectionNames?: Iterable<string>;
136
143
  limit?: number;
144
+ rebuild?: boolean;
137
145
  }): Promise<VectorIndexResult[]>;
138
146
  getProjectConfig(): ProjectConfig | undefined;
139
147
  getStats(): KnowledgeBaseStats;
@@ -170,6 +178,7 @@ export declare class KnowledgeBaseManager {
170
178
  private ensureVectorIndexFresh;
171
179
  private hasUsableContent;
172
180
  private defaultUploadRelativePath;
181
+ private validateUploadRelativePath;
173
182
  private resolveKbRelativePath;
174
183
  private normalizeRelativePath;
175
184
  }
@@ -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();
@@ -417,7 +426,7 @@ export class KnowledgeBaseManager {
417
426
  return this.incrementalIndex();
418
427
  }
419
428
  getUploadRelativePath(fileName, targetRelativePath) {
420
- return targetRelativePath ? this.normalizeRelativePath(targetRelativePath) : this.defaultUploadRelativePath(fileName);
429
+ return this.validateUploadRelativePath(targetRelativePath ? this.normalizeRelativePath(targetRelativePath) : this.defaultUploadRelativePath(fileName));
421
430
  }
422
431
  async uploadFile(fileName, content, targetRelativePath, onProgress, options = {}) {
423
432
  return this.uploadFiles([{ fileName, content, targetRelativePath }], onProgress, options);
@@ -439,6 +448,23 @@ export class KnowledgeBaseManager {
439
448
  }
440
449
  return jobs;
441
450
  }
451
+ async stageUploadedFilePaths(files, operationId = `upload-${Date.now()}`, offset = 0) {
452
+ this.initialize();
453
+ const jobs = [];
454
+ for (let index = 0; index < files.length; index++) {
455
+ const file = files[index];
456
+ const relativePath = this.getUploadRelativePath(file.fileName, file.targetRelativePath);
457
+ const targetPath = this.resolveKbRelativePath(relativePath);
458
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
459
+ fs.copyFileSync(file.sourcePath, targetPath);
460
+ const record = this.store.listRecords().find(item => item.relativePath === relativePath);
461
+ if (record)
462
+ await this.deleteVectorFile(record.collectionName, relativePath);
463
+ this.store.deleteRecord(relativePath);
464
+ jobs.push(this.store.enqueueIndexJob({ id: `${operationId}-${offset + index}`, relativePath, message: '文件已落盘,等待后台解析' }));
465
+ }
466
+ return jobs;
467
+ }
442
468
  async uploadFiles(files, onProgress, options = {}) {
443
469
  await this.stageUploadedFiles(files);
444
470
  return this.incrementalIndex({ onProgress, vectorMode: options.vectorMode });
@@ -482,19 +508,36 @@ export class KnowledgeBaseManager {
482
508
  return this.store.listIgnoreRules();
483
509
  }
484
510
  async indexVectors(options = {}) {
485
- const chunks = this.store.listChunks(options);
511
+ const chunks = options.relativePaths?.length
512
+ ? options.relativePaths.flatMap(relativePath => this.store.listChunks({ collectionName: options.collectionName, relativePath }))
513
+ : this.store.listChunks(options);
486
514
  const collectionNames = new Set(chunks.map(chunk => chunk.collectionName));
487
- for (const collectionName of collectionNames)
515
+ const cleanupCollectionNames = new Set([...collectionNames, ...(options.cleanupCollectionNames ?? [])]);
516
+ for (const collectionName of cleanupCollectionNames)
488
517
  this.ensureVectorStore(collectionName);
489
- if (!options.relativePath) {
518
+ if (options.rebuild || (!options.relativePath && !options.relativePaths?.length)) {
490
519
  for (const collectionName of collectionNames)
491
520
  await this.vectorStores.get(collectionName)?.clearCollection?.();
492
521
  }
493
522
  else {
494
- for (const collectionName of collectionNames)
495
- await this.vectorStores.get(collectionName)?.deleteByFilePath(options.relativePath);
523
+ for (const relativePath of options.relativePaths ?? [options.relativePath].filter(Boolean)) {
524
+ for (const collectionName of cleanupCollectionNames)
525
+ await this.vectorStores.get(collectionName)?.deleteByFilePath(relativePath, { persist: false });
526
+ }
527
+ if (chunks.length === 0) {
528
+ for (const collectionName of cleanupCollectionNames)
529
+ await this.vectorStores.get(collectionName)?.flush?.();
530
+ }
496
531
  }
497
532
  this.reportProgress({ stage: 'vectorizing', percent: 85, message: `正在写入 HNSWLib 向量库,共 ${chunks.length} 个切片`, chunkCount: chunks.length });
533
+ if (chunks.length === 0) {
534
+ const totalChunks = this.getStats().chunkCount;
535
+ this.store.setMetadata('vector_indexed_chunks', String(totalChunks));
536
+ this.store.setMetadata('vector_index_status', 'ready');
537
+ this.store.setMetadata('vector_index_error', '');
538
+ this.store.setMetadata('last_vector_index_at', String(Date.now()));
539
+ return [];
540
+ }
498
541
  const indexer = new VectorIndexer(this.embeddingProvider, this.vectorStores);
499
542
  let lastVectorPercent = -1;
500
543
  try {
@@ -512,9 +555,10 @@ export class KnowledgeBaseManager {
512
555
  });
513
556
  const actualModel = results[0]?.embeddingModel ?? this.embeddingProvider.model;
514
557
  const actualDimension = results[0]?.embeddingDimension ?? this.embeddingProvider.dimensions;
558
+ const totalChunks = this.getStats().chunkCount;
515
559
  this.store.setMetadata('embedding_model', actualModel);
516
560
  this.store.setMetadata('embedding_dimension', String(actualDimension));
517
- this.store.setMetadata('vector_indexed_chunks', String(chunks.length));
561
+ this.store.setMetadata('vector_indexed_chunks', String(totalChunks));
518
562
  this.store.setMetadata('vector_index_status', 'ready');
519
563
  this.store.setMetadata('vector_index_error', '');
520
564
  this.store.setMetadata('last_vector_index_at', String(Date.now()));
@@ -854,20 +898,34 @@ ${resultsText}
854
898
  this.store.setMetadata('vector_index_error', error instanceof Error ? error.message : String(error));
855
899
  }
856
900
  }
857
- async ensureVectorIndexFresh(chunkCount, force = false) {
901
+ async ensureVectorIndexFresh(chunkCount, options = {}) {
858
902
  if (chunkCount === 0)
859
903
  return;
860
904
  for (const record of this.store.listRecords())
861
905
  this.ensureVectorStore(record.collectionName);
862
- if ([...this.vectorStores.values()].some(store => store.needsRebuild?.())) {
863
- await this.indexVectors();
906
+ if (options.rebuild || [...this.vectorStores.values()].some(store => store.needsRebuild?.())) {
907
+ await this.indexVectors({ rebuild: true });
864
908
  return;
865
909
  }
866
910
  const indexedChunks = Number(this.store.getMetadata('vector_indexed_chunks') ?? 0);
867
911
  const status = this.store.getMetadata('vector_index_status');
868
- if (!force && indexedChunks === chunkCount && status === 'ready')
912
+ const changedRelativePaths = [...new Set(options.changedRelativePaths ?? [])];
913
+ if (changedRelativePaths.length === 0 && options.deletesApplied && status === 'ready') {
914
+ this.store.setMetadata('vector_indexed_chunks', String(chunkCount));
915
+ this.store.setMetadata('vector_index_status', 'ready');
916
+ this.store.setMetadata('vector_index_error', '');
917
+ this.store.setMetadata('last_vector_index_at', String(Date.now()));
918
+ return;
919
+ }
920
+ if (changedRelativePaths.length > 0 && status === 'ready') {
921
+ for (const collectionName of options.changedCollectionNames ?? [])
922
+ this.ensureVectorStore(collectionName);
923
+ await this.indexVectors({ relativePaths: changedRelativePaths, cleanupCollectionNames: options.changedCollectionNames });
869
924
  return;
870
- await this.indexVectors();
925
+ }
926
+ if (indexedChunks === chunkCount && status === 'ready')
927
+ return;
928
+ await this.indexVectors({ rebuild: true });
871
929
  }
872
930
  hasUsableContent(text, metadata) {
873
931
  const coverage = String(metadata.contentCoverage ?? '');
@@ -881,6 +939,21 @@ ${resultsText}
881
939
  const dir = configDirs[classification.category] ?? DEFAULT_CATEGORY_DIRS[classification.category];
882
940
  return `${dir}/${path.basename(fileName)}`;
883
941
  }
942
+ validateUploadRelativePath(relativePath) {
943
+ const normalized = this.normalizeRelativePath(relativePath);
944
+ if (!normalized || normalized === '.')
945
+ throw new Error('上传文件路径无效');
946
+ if (normalized.length > 1000)
947
+ throw new Error('上传文件路径过长,请缩短文件夹层级或文件名');
948
+ if (normalized.includes('\0'))
949
+ throw new Error('上传文件路径包含非法字符');
950
+ const parts = normalized.split('/');
951
+ if (parts.some(part => !part || part === '..'))
952
+ throw new Error('上传文件路径无效');
953
+ if (parts.some(part => part.length > 255))
954
+ throw new Error('上传文件名过长,请缩短文件名后重试');
955
+ return normalized;
956
+ }
884
957
  resolveKbRelativePath(relativePath) {
885
958
  const normalized = this.normalizeRelativePath(relativePath);
886
959
  const targetPath = path.resolve(this.kbPath, normalized);
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 并写入向量存储 */
@@ -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.5",
3
+ "version": "4.0.7",
4
4
  "description": "Local knowledge base infrastructure for customize-agent",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",