@customize-agent/knowledge 4.0.7 → 4.0.9

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.
@@ -324,7 +324,7 @@ export class TextChunker {
324
324
  withHeader(text, file, config) {
325
325
  if (!config.headerInjection)
326
326
  return text;
327
- return `文件: ${file.relativePath}\n类型: ${file.category}/${file.format}\n\n${text}`;
327
+ return `资料类型: ${file.category}/${file.format}\n\n${text}`;
328
328
  }
329
329
  createChunk(index, candidate, file, metadata) {
330
330
  const text = candidate.text.trim();
@@ -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)
@@ -278,7 +278,7 @@ export class IndexStateStore {
278
278
  `).all(matchQuery, limit * 8);
279
279
  return rows
280
280
  .map(row => {
281
- const keyword = this.scoreChunkDetailed(`${String(row.relative_path)}\n${String(row.category)}\n${String(row.format)}\n${String(row.content)}`, terms);
281
+ const keyword = this.scoreChunkDetailed(`${String(row.section_title ?? '')}\n${String(row.category)}\n${String(row.format)}\n${String(row.content)}`, terms);
282
282
  const bm25Score = this.bm25ToPositiveScore(Number(row.bm25_score));
283
283
  return this.rowToChunk(row, keyword.keywordScore + bm25Score, { ...keyword, bm25Score });
284
284
  })
@@ -299,7 +299,7 @@ export class IndexStateStore {
299
299
  `).all(...terms.flatMap(term => [`%${term}%`, `%${term}%`, `%${term}%`, `%${term}%`]), limit * 6);
300
300
  return rows
301
301
  .map(row => {
302
- const keyword = this.scoreChunkDetailed(`${String(row.relative_path)}\n${String(row.category)}\n${String(row.format)}\n${String(row.content)}`, terms);
302
+ const keyword = this.scoreChunkDetailed(`${String(row.section_title ?? '')}\n${String(row.category)}\n${String(row.format)}\n${String(row.content)}`, terms);
303
303
  return this.rowToChunk(row, keyword.keywordScore, keyword);
304
304
  })
305
305
  .filter(row => row.score > 0)
@@ -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
  }
@@ -16,6 +16,9 @@ export declare class ContentExtractor {
16
16
  private extractTextClipping;
17
17
  private swapUtf16Bytes;
18
18
  private extractReadableFragments;
19
+ private cleanCadReadableText;
20
+ private isReadableCadValue;
21
+ private cleanExtractedText;
19
22
  private textScore;
20
23
  private extractCad;
21
24
  private extractDxf;
@@ -5,6 +5,8 @@ import { tmpdir } from 'node:os';
5
5
  import { pathToFileURL } from 'node:url';
6
6
  import { ExternalExtractorRegistry } from './external-extractor.js';
7
7
  import { resolveAndImport, resolvePackage, getNodeModulesRoot } from './module-resolver.js';
8
+ const CAD_INTERNAL_TOKEN_RE = /\b(?:TDbPipe|TDbPipeValve|TDbPipeFitting|TDbWellh|AcDb\w+|Dwg\w+|ObjectId|Handle|ByLayer|Continuous|Model|Layout\d*)\b/giu;
9
+ const CAD_INTERNAL_LINE_RE = /^(?:TDb\w+|AcDb\w+|[A-F0-9]{8,}|\d+|Model|Layout\d*|ByLayer|Continuous)$/iu;
8
10
  /** 文件内容提取器,支持文档、表格、图片、CAD 等多种文件格式的内容抽取 */
9
11
  export class ContentExtractor {
10
12
  externalExtractors;
@@ -102,7 +104,7 @@ export class ContentExtractor {
102
104
  }
103
105
  }
104
106
  return {
105
- text: text.trim(),
107
+ text: this.cleanExtractedText(text, file).trim(),
106
108
  metadata,
107
109
  warnings,
108
110
  extractionTimeMs: Date.now() - start,
@@ -177,9 +179,36 @@ export class ContentExtractor {
177
179
  return value
178
180
  .replace(/[^\p{L}\p{N}\p{P}\p{S}\s]/gu, '\n')
179
181
  .split(/[\r\n]+/u)
180
- .map(line => line.replace(/\s+/gu, ' ').trim())
182
+ .map(line => this.cleanCadReadableText(line))
181
183
  .filter(line => line.length >= 2 && /[\p{L}\p{N}]/u.test(line));
182
184
  }
185
+ cleanCadReadableText(value) {
186
+ return value
187
+ .replace(CAD_INTERNAL_TOKEN_RE, '')
188
+ .replace(/\b(?:LINE|LWPOLYLINE|POLYLINE|INSERT|HATCH|CIRCLE|ARC|DIMENSION|TEXT|MTEXT)\b/giu, '')
189
+ .replace(/\s+/gu, ' ')
190
+ .trim();
191
+ }
192
+ isReadableCadValue(value) {
193
+ const cleaned = this.cleanCadReadableText(value);
194
+ return cleaned.length >= 2 && !CAD_INTERNAL_LINE_RE.test(cleaned) && /[\p{Script=Han}\p{Letter}\d]/u.test(cleaned);
195
+ }
196
+ cleanExtractedText(value, file) {
197
+ const normalized = [...value]
198
+ .filter(char => {
199
+ const code = char.charCodeAt(0);
200
+ return code === 9 || code === 10 || code === 13 || code >= 32;
201
+ })
202
+ .join('');
203
+ if (file.category !== 'cad')
204
+ return normalized.replace(/\n{3,}/gu, '\n\n');
205
+ return normalized
206
+ .split(/\r?\n/u)
207
+ .map(line => this.cleanCadReadableText(line))
208
+ .filter(line => line && !CAD_INTERNAL_LINE_RE.test(line))
209
+ .join('\n')
210
+ .replace(/\n{3,}/gu, '\n\n');
211
+ }
183
212
  textScore(value) {
184
213
  const cjk = (value.match(/[\p{Script=Han}]/gu) ?? []).length;
185
214
  const alnum = (value.match(/[\p{L}\p{N}]/gu) ?? []).length;
@@ -202,9 +231,9 @@ export class ContentExtractor {
202
231
  }
203
232
  if (file.format === 'autocad' && ext === '.dxf') {
204
233
  const raw = fs.readFileSync(file.absolutePath, 'utf8');
205
- const layers = this.matchAll(raw, /\n\s*8\s*\n([^\n]+)/gu).slice(0, 300);
234
+ const layers = this.matchAll(raw, /\n\s*8\s*\n([^\n]+)/gu).filter(value => this.isReadableCadValue(value)).slice(0, 300);
206
235
  const textEntities = this.extractDxfTextAnnotations(raw).slice(0, 500);
207
- const blocks = this.matchAll(raw, /\n\s*2\s*\n([^\n]+)/gu).slice(0, 300);
236
+ const blocks = this.matchAll(raw, /\n\s*2\s*\n([^\n]+)/gu).filter(value => this.isReadableCadValue(value)).slice(0, 300);
208
237
  const entityTypes = this.matchAll(raw, /\n\s*0\s*\n([A-Z][A-Z0-9_]+)/gu).slice(0, 1000);
209
238
  const uniqueLayers = Array.from(new Set(layers));
210
239
  const uniqueBlocks = Array.from(new Set(blocks));
@@ -278,7 +307,7 @@ export class ContentExtractor {
278
307
  if (result.text.trim())
279
308
  return result;
280
309
  }
281
- const readable = this.extractBinaryReadableFragments(file.absolutePath).slice(0, 800);
310
+ const readable = this.extractBinaryReadableFragments(file.absolutePath).filter(value => this.isReadableCadValue(value)).slice(0, 5000);
282
311
  metadata.extractionMode = 'builtin_cad_readable_fragments';
283
312
  metadata.contentCoverage = readable.length > 0 ? 'cad_readable_text_fragments' : 'metadata';
284
313
  metadata.stringCount = readable.length;
@@ -303,10 +332,10 @@ export class ContentExtractor {
303
332
  catch {
304
333
  warnings.push('dxf-parser 解析失败,已使用 DXF 文本结构抽取回退');
305
334
  }
306
- const layers = this.matchAll(raw, /\n\s*8\s*\n([^\n]+)/gu).slice(0, 300);
307
- const textEntities = this.extractDxfTextAnnotations(raw).slice(0, 800);
308
- const blocks = this.matchAll(raw, /\n\s*2\s*\n([^\n]+)/gu).slice(0, 300);
309
- const entityTypes = this.matchAll(raw, /\n\s*0\s*\n([A-Z][A-Z0-9_]+)/gu).slice(0, 1200);
335
+ const layers = this.matchAll(raw, /(?:^|\r?\n)\s*8\s*\r?\n([^\r\n]+)/gu).filter(value => this.isReadableCadValue(value)).slice(0, 300);
336
+ const textEntities = this.extractDxfTextAnnotations(raw).slice(0, 5000);
337
+ const blocks = this.matchAll(raw, /(?:^|\r?\n)\s*2\s*\r?\n([^\r\n]+)/gu).filter(value => this.isReadableCadValue(value)).slice(0, 300);
338
+ const entityTypes = this.matchAll(raw, /(?:^|\r?\n)\s*0\s*\r?\n([A-Z][A-Z0-9_]+)/gu).slice(0, 1200);
310
339
  const uniqueLayers = Array.from(new Set(layers));
311
340
  const uniqueBlocks = Array.from(new Set(blocks));
312
341
  const uniqueEntityTypes = Array.from(new Set(entityTypes));
@@ -350,18 +379,18 @@ export class ContentExtractor {
350
379
  });
351
380
  }
352
381
  extractDxfTextAnnotations(raw) {
353
- const entities = raw.split(/\n\s*0\s*\n/u).filter(section => /^(?:TEXT|MTEXT|DIMENSION|LEADER)/u.test(section.trim()));
382
+ const entities = raw.split(/(?:^|\r?\n)\s*0\s*\r?\n/u).filter(section => /^(?:TEXT|MTEXT|DIMENSION|LEADER)/u.test(section.trim()));
354
383
  return entities.flatMap(section => {
355
- const text = /\n\s*(?:1|3)\s*\n([^\n]+)/u.exec(section)?.[1]?.trim();
356
- if (!text)
384
+ const text = this.cleanCadReadableText(/(?:^|\r?\n)\s*(?:1|3)\s*\r?\n([^\r\n]+)/u.exec(section)?.[1] ?? '');
385
+ if (!text || !this.isReadableCadValue(text))
357
386
  return [];
358
387
  return [{
359
388
  text,
360
- layer: /\n\s*8\s*\n([^\n]+)/u.exec(section)?.[1]?.trim(),
361
- block: /\n\s*2\s*\n([^\n]+)/u.exec(section)?.[1]?.trim(),
389
+ layer: /(?:^|\r?\n)\s*8\s*\r?\n([^\r\n]+)/u.exec(section)?.[1]?.trim(),
390
+ block: /(?:^|\r?\n)\s*2\s*\r?\n([^\r\n]+)/u.exec(section)?.[1]?.trim(),
362
391
  entityType: section.trim().split(/\s+/u)[0],
363
- x: Number(/\n\s*10\s*\n([^\n]+)/u.exec(section)?.[1]),
364
- y: Number(/\n\s*20\s*\n([^\n]+)/u.exec(section)?.[1]),
392
+ x: Number(/(?:^|\r?\n)\s*10\s*\r?\n([^\r\n]+)/u.exec(section)?.[1]),
393
+ y: Number(/(?:^|\r?\n)\s*20\s*\r?\n([^\r\n]+)/u.exec(section)?.[1]),
365
394
  }].map(item => ({ ...item, x: Number.isFinite(item.x) ? item.x : undefined, y: Number.isFinite(item.y) ? item.y : undefined }));
366
395
  });
367
396
  }
@@ -1165,7 +1194,7 @@ process.stdout.write(JSON.stringify({ pageCount: doc.numPages, pageLimit, text:
1165
1194
  if (pages.length > 0) {
1166
1195
  const combined = pages.join('\n\n');
1167
1196
  if (combined.trim())
1168
- return combined.slice(0, 250_000);
1197
+ return combined;
1169
1198
  }
1170
1199
  }
1171
1200
  catch (e) {
@@ -1179,7 +1208,7 @@ process.stdout.write(JSON.stringify({ pageCount: doc.numPages, pageLimit, text:
1179
1208
  if (pdfParse) {
1180
1209
  const result = await pdfParse(buffer);
1181
1210
  if (result.text.trim())
1182
- return result.text.slice(0, 250_000);
1211
+ return result.text;
1183
1212
  }
1184
1213
  }
1185
1214
  catch {
@@ -1450,15 +1479,8 @@ process.stdout.write(JSON.stringify({ pageCount: doc.numPages, pageLimit, text:
1450
1479
  return lines.filter(line => !line.endsWith(':'));
1451
1480
  }
1452
1481
  metadataOnlyText(file) {
1453
- const fileName = path.basename(file.relativePath);
1454
- const directory = path.dirname(file.relativePath);
1455
- const searchableName = fileName.replace(/[_\-.]+/gu, ' ');
1456
1482
  return [
1457
- `文件名: ${fileName}`,
1458
- `文件路径: ${file.relativePath}`,
1459
- `所在目录: ${directory === '.' ? 'knowledgeBase' : directory}`,
1460
- `可搜索名称: ${searchableName}`,
1461
- `文件类型: ${file.category}/${file.format}`,
1483
+ `资料类型: ${file.category}/${file.format}`,
1462
1484
  `MIME: ${file.mimeType}`,
1463
1485
  `文件大小: ${file.fileSize} bytes`,
1464
1486
  ].join('\n');
@@ -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.9",
4
4
  "description": "Local knowledge base infrastructure for customize-agent",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",