@customize-agent/knowledge 4.0.7 → 4.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -19,6 +19,6 @@ export declare class ChangeTracker {
19
19
  * @param filePath 文件路径
20
20
  * @returns SHA-256 哈希字符串
21
21
  */
22
- hashFile(filePath: string): string;
22
+ hashFile(filePath: string): Promise<string>;
23
23
  private parseMetadata;
24
24
  }
@@ -1,5 +1,6 @@
1
1
  import * as crypto from 'node:crypto';
2
2
  import * as fs from 'node:fs';
3
+ import { createReadStream } from 'node:fs';
3
4
  import * as path from 'node:path';
4
5
  /** 文件变更追踪器,用于比对磁盘文件与索引状态之间的差异 */
5
6
  export class ChangeTracker {
@@ -51,7 +52,7 @@ export class ChangeTracker {
51
52
  continue;
52
53
  }
53
54
  if (Math.round(diskStat.mtime) !== Math.round(indexed.mtime) || diskStat.size !== indexed.fileSize) {
54
- const contentHash = this.hashFile(absolutePath);
55
+ const contentHash = await this.hashFile(absolutePath);
55
56
  if (contentHash !== indexed.contentHash) {
56
57
  modifiedFiles.push(classified);
57
58
  }
@@ -86,8 +87,15 @@ export class ChangeTracker {
86
87
  * @param filePath 文件路径
87
88
  * @returns SHA-256 哈希字符串
88
89
  */
89
- hashFile(filePath) {
90
- return crypto.createHash('sha256').update(fs.readFileSync(filePath)).digest('hex');
90
+ async hashFile(filePath) {
91
+ const hash = crypto.createHash('sha256');
92
+ await new Promise((resolve, reject) => {
93
+ const stream = createReadStream(filePath);
94
+ stream.on('data', chunk => hash.update(chunk));
95
+ stream.on('error', reject);
96
+ stream.on('end', resolve);
97
+ });
98
+ return hash.digest('hex');
91
99
  }
92
100
  parseMetadata(metadataJson) {
93
101
  if (!metadataJson)
@@ -59,6 +59,7 @@ export declare class KnowledgeBaseManager {
59
59
  onProgress?: (progress: KnowledgeIndexProgress) => void;
60
60
  vectorMode?: 'sync' | 'defer';
61
61
  limit?: number;
62
+ waitForUploadId?: string;
62
63
  }): Promise<DiffResult>;
63
64
  incrementalIndex(options?: {
64
65
  onProgress?: (progress: KnowledgeIndexProgress) => void;
@@ -112,7 +113,7 @@ export declare class KnowledgeBaseManager {
112
113
  fileName: string;
113
114
  sourcePath: string;
114
115
  targetRelativePath?: string;
115
- }>, operationId?: string, offset?: number): Promise<import("./index-state-store.js").KnowledgeIndexJob[]>;
116
+ }>, operationId?: string, offset?: number, uploadComplete?: boolean): Promise<import("./index-state-store.js").KnowledgeIndexJob[]>;
116
117
  uploadFiles(files: Array<{
117
118
  fileName: string;
118
119
  content: Buffer;
@@ -176,6 +177,10 @@ export declare class KnowledgeBaseManager {
176
177
  private ensureVectorStore;
177
178
  private deleteVectorFile;
178
179
  private ensureVectorIndexFresh;
180
+ uploadSessionIsOpen(operationId: string): boolean;
181
+ private emptyDiff;
182
+ private statRelativePaths;
183
+ private moveUploadedFile;
179
184
  private hasUsableContent;
180
185
  private defaultUploadRelativePath;
181
186
  private validateUploadRelativePath;
@@ -87,9 +87,12 @@ export class KnowledgeBaseManager {
87
87
  }
88
88
  async consumePendingIndexJobs(options = {}) {
89
89
  this.initialize();
90
- const jobs = this.store.listPendingIndexJobs(options.limit ?? 50);
91
- if (jobs.length === 0)
90
+ const jobs = this.store.listPendingIndexJobs(options.limit ?? 500);
91
+ if (jobs.length === 0) {
92
+ if (options.waitForUploadId && this.uploadSessionIsOpen(options.waitForUploadId))
93
+ return this.emptyDiff();
92
94
  return this.incrementalIndex(options);
95
+ }
93
96
  return this.incrementalIndex({ ...options, onlyRelativePaths: jobs.map(job => job.relativePath) });
94
97
  }
95
98
  async incrementalIndex(options = {}) {
@@ -101,10 +104,10 @@ export class KnowledgeBaseManager {
101
104
  this.reportProgress({ stage: 'scanning', percent: 10, message: '正在扫描知识库文件' });
102
105
  const kbIgnore = this.scanner.loadKbIgnore(this.kbPath);
103
106
  const configIgnore = this.projectConfig?.kbignore ?? [];
104
- const diskFiles = await this.scanner.scan(this.kbPath, [...kbIgnore, ...configIgnore]);
107
+ const onlyRelativePaths = options.onlyRelativePaths ? new Set(options.onlyRelativePaths) : undefined;
108
+ const diskFiles = onlyRelativePaths ? this.statRelativePaths([...onlyRelativePaths]) : await this.scanner.scan(this.kbPath, [...kbIgnore, ...configIgnore]);
105
109
  const tracker = new ChangeTracker(this.store);
106
110
  const diff = await tracker.computeDiff(diskFiles, this.classifier, this.kbPath);
107
- const onlyRelativePaths = options.onlyRelativePaths ? new Set(options.onlyRelativePaths) : undefined;
108
111
  let vectorDeletesApplied = 0;
109
112
  if (onlyRelativePaths) {
110
113
  diff.newFiles = diff.newFiles.filter(file => onlyRelativePaths.has(file.relativePath));
@@ -129,7 +132,7 @@ export class KnowledgeBaseManager {
129
132
  const vectorRelativePaths = [];
130
133
  const changedCollectionNames = new Set();
131
134
  for (const [index, file] of filesToIndex.entries()) {
132
- const hash = tracker.hashFile(file.absolutePath);
135
+ const hash = await tracker.hashFile(file.absolutePath);
133
136
  const duplicate = this.store.findExactDuplicate(hash, file.relativePath);
134
137
  const collectionName = this.scope === 'global'
135
138
  ? this.collections.getCollectionName('global', file.category)
@@ -448,15 +451,16 @@ export class KnowledgeBaseManager {
448
451
  }
449
452
  return jobs;
450
453
  }
451
- async stageUploadedFilePaths(files, operationId = `upload-${Date.now()}`, offset = 0) {
454
+ async stageUploadedFilePaths(files, operationId = `upload-${Date.now()}`, offset = 0, uploadComplete = true) {
452
455
  this.initialize();
456
+ this.store.setMetadata(`upload_session:${operationId}`, uploadComplete ? 'complete' : 'open');
453
457
  const jobs = [];
454
458
  for (let index = 0; index < files.length; index++) {
455
459
  const file = files[index];
456
460
  const relativePath = this.getUploadRelativePath(file.fileName, file.targetRelativePath);
457
461
  const targetPath = this.resolveKbRelativePath(relativePath);
458
462
  fs.mkdirSync(path.dirname(targetPath), { recursive: true });
459
- fs.copyFileSync(file.sourcePath, targetPath);
463
+ this.moveUploadedFile(file.sourcePath, targetPath);
460
464
  const record = this.store.listRecords().find(item => item.relativePath === relativePath);
461
465
  if (record)
462
466
  await this.deleteVectorFile(record.collectionName, relativePath);
@@ -927,6 +931,39 @@ ${resultsText}
927
931
  return;
928
932
  await this.indexVectors({ rebuild: true });
929
933
  }
934
+ uploadSessionIsOpen(operationId) {
935
+ return this.store.getMetadata(`upload_session:${operationId}`) === 'open';
936
+ }
937
+ emptyDiff() {
938
+ return { newFiles: [], modifiedFiles: [], deletedFiles: [], unchangedCount: 0, mtimeOnlyCount: 0, skippedFiles: [], hasChanges: false, diffTimeMs: 0 };
939
+ }
940
+ statRelativePaths(relativePaths) {
941
+ const files = new Map();
942
+ for (const relativePath of relativePaths) {
943
+ const normalized = this.normalizeRelativePath(relativePath);
944
+ const absolutePath = this.resolveKbRelativePath(normalized);
945
+ if (!fs.existsSync(absolutePath))
946
+ continue;
947
+ const stat = fs.statSync(absolutePath);
948
+ if (stat.isFile())
949
+ files.set(normalized, { size: stat.size, mtime: stat.mtimeMs });
950
+ }
951
+ return files;
952
+ }
953
+ moveUploadedFile(sourcePath, targetPath) {
954
+ if (fs.existsSync(targetPath))
955
+ fs.rmSync(targetPath, { force: true });
956
+ try {
957
+ fs.renameSync(sourcePath, targetPath);
958
+ }
959
+ catch (error) {
960
+ const code = error && typeof error === 'object' && 'code' in error ? String(error.code) : '';
961
+ if (code !== 'EXDEV')
962
+ throw error;
963
+ fs.copyFileSync(sourcePath, targetPath);
964
+ fs.rmSync(sourcePath, { force: true });
965
+ }
966
+ }
930
967
  hasUsableContent(text, metadata) {
931
968
  const coverage = String(metadata.contentCoverage ?? '');
932
969
  if (['metadata', 'metadata_filename', 'pdf_metadata_only', 'office_zip_empty_text', 'office_zip_failed'].includes(coverage))
@@ -964,6 +1001,6 @@ ${resultsText}
964
1001
  return targetPath;
965
1002
  }
966
1003
  normalizeRelativePath(relativePath) {
967
- return relativePath.replace(/\\/gu, '/').split(path.sep).join('/').replace(/^\/+/, '');
1004
+ return relativePath.replace(/\\/gu, '/').split(path.sep).join('/').replace(/^\/+/, '').replace(/\/+/gu, '/');
968
1005
  }
969
1006
  }
@@ -1,8 +1,8 @@
1
1
  function resolveVectorIndexBatchSize(configured) {
2
2
  const raw = configured ?? Number(process.env.CUSTOMIZE_VECTOR_INDEX_BATCH_SIZE ?? process.env.KB_VECTOR_INDEX_BATCH_SIZE);
3
3
  if (!Number.isFinite(raw) || raw <= 0)
4
- return 128;
5
- return Math.max(1, Math.min(512, Math.floor(raw)));
4
+ return 256;
5
+ return Math.max(1, Math.min(1024, Math.floor(raw)));
6
6
  }
7
7
  /** 向量索引器,负责将文本切片生成 Embedding 并写入向量存储 */
8
8
  export class VectorIndexer {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@customize-agent/knowledge",
3
- "version": "4.0.7",
3
+ "version": "4.0.8",
4
4
  "description": "Local knowledge base infrastructure for customize-agent",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",