@customize-agent/knowledge 4.0.6 → 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;
@@ -108,6 +109,11 @@ export declare class KnowledgeBaseManager {
108
109
  content: Buffer;
109
110
  targetRelativePath?: string;
110
111
  }>, operationId?: string): Promise<import("./index-state-store.js").KnowledgeIndexJob[]>;
112
+ stageUploadedFilePaths(files: Array<{
113
+ fileName: string;
114
+ sourcePath: string;
115
+ targetRelativePath?: string;
116
+ }>, operationId?: string, offset?: number, uploadComplete?: boolean): Promise<import("./index-state-store.js").KnowledgeIndexJob[]>;
111
117
  uploadFiles(files: Array<{
112
118
  fileName: string;
113
119
  content: Buffer;
@@ -171,8 +177,13 @@ export declare class KnowledgeBaseManager {
171
177
  private ensureVectorStore;
172
178
  private deleteVectorFile;
173
179
  private ensureVectorIndexFresh;
180
+ uploadSessionIsOpen(operationId: string): boolean;
181
+ private emptyDiff;
182
+ private statRelativePaths;
183
+ private moveUploadedFile;
174
184
  private hasUsableContent;
175
185
  private defaultUploadRelativePath;
186
+ private validateUploadRelativePath;
176
187
  private resolveKbRelativePath;
177
188
  private normalizeRelativePath;
178
189
  }
@@ -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)
@@ -426,7 +429,7 @@ export class KnowledgeBaseManager {
426
429
  return this.incrementalIndex();
427
430
  }
428
431
  getUploadRelativePath(fileName, targetRelativePath) {
429
- return targetRelativePath ? this.normalizeRelativePath(targetRelativePath) : this.defaultUploadRelativePath(fileName);
432
+ return this.validateUploadRelativePath(targetRelativePath ? this.normalizeRelativePath(targetRelativePath) : this.defaultUploadRelativePath(fileName));
430
433
  }
431
434
  async uploadFile(fileName, content, targetRelativePath, onProgress, options = {}) {
432
435
  return this.uploadFiles([{ fileName, content, targetRelativePath }], onProgress, options);
@@ -448,6 +451,24 @@ export class KnowledgeBaseManager {
448
451
  }
449
452
  return jobs;
450
453
  }
454
+ async stageUploadedFilePaths(files, operationId = `upload-${Date.now()}`, offset = 0, uploadComplete = true) {
455
+ this.initialize();
456
+ this.store.setMetadata(`upload_session:${operationId}`, uploadComplete ? 'complete' : 'open');
457
+ const jobs = [];
458
+ for (let index = 0; index < files.length; index++) {
459
+ const file = files[index];
460
+ const relativePath = this.getUploadRelativePath(file.fileName, file.targetRelativePath);
461
+ const targetPath = this.resolveKbRelativePath(relativePath);
462
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
463
+ this.moveUploadedFile(file.sourcePath, targetPath);
464
+ const record = this.store.listRecords().find(item => item.relativePath === relativePath);
465
+ if (record)
466
+ await this.deleteVectorFile(record.collectionName, relativePath);
467
+ this.store.deleteRecord(relativePath);
468
+ jobs.push(this.store.enqueueIndexJob({ id: `${operationId}-${offset + index}`, relativePath, message: '文件已落盘,等待后台解析' }));
469
+ }
470
+ return jobs;
471
+ }
451
472
  async uploadFiles(files, onProgress, options = {}) {
452
473
  await this.stageUploadedFiles(files);
453
474
  return this.incrementalIndex({ onProgress, vectorMode: options.vectorMode });
@@ -910,6 +931,39 @@ ${resultsText}
910
931
  return;
911
932
  await this.indexVectors({ rebuild: true });
912
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
+ }
913
967
  hasUsableContent(text, metadata) {
914
968
  const coverage = String(metadata.contentCoverage ?? '');
915
969
  if (['metadata', 'metadata_filename', 'pdf_metadata_only', 'office_zip_empty_text', 'office_zip_failed'].includes(coverage))
@@ -922,6 +976,21 @@ ${resultsText}
922
976
  const dir = configDirs[classification.category] ?? DEFAULT_CATEGORY_DIRS[classification.category];
923
977
  return `${dir}/${path.basename(fileName)}`;
924
978
  }
979
+ validateUploadRelativePath(relativePath) {
980
+ const normalized = this.normalizeRelativePath(relativePath);
981
+ if (!normalized || normalized === '.')
982
+ throw new Error('上传文件路径无效');
983
+ if (normalized.length > 1000)
984
+ throw new Error('上传文件路径过长,请缩短文件夹层级或文件名');
985
+ if (normalized.includes('\0'))
986
+ throw new Error('上传文件路径包含非法字符');
987
+ const parts = normalized.split('/');
988
+ if (parts.some(part => !part || part === '..'))
989
+ throw new Error('上传文件路径无效');
990
+ if (parts.some(part => part.length > 255))
991
+ throw new Error('上传文件名过长,请缩短文件名后重试');
992
+ return normalized;
993
+ }
925
994
  resolveKbRelativePath(relativePath) {
926
995
  const normalized = this.normalizeRelativePath(relativePath);
927
996
  const targetPath = path.resolve(this.kbPath, normalized);
@@ -932,6 +1001,6 @@ ${resultsText}
932
1001
  return targetPath;
933
1002
  }
934
1003
  normalizeRelativePath(relativePath) {
935
- return relativePath.replace(/\\/gu, '/').split(path.sep).join('/').replace(/^\/+/, '');
1004
+ return relativePath.replace(/\\/gu, '/').split(path.sep).join('/').replace(/^\/+/, '').replace(/\/+/gu, '/');
936
1005
  }
937
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.6",
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",