@customize-agent/knowledge 4.0.1 → 4.0.3

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.
Files changed (56) hide show
  1. package/dist/chunking/bge-tokenizer.d.ts +12 -0
  2. package/dist/chunking/bge-tokenizer.js +70 -0
  3. package/dist/chunking/text-chunker.d.ts +20 -0
  4. package/dist/chunking/text-chunker.js +166 -53
  5. package/dist/classification/classifier.d.ts +1 -0
  6. package/dist/classification/classifier.js +1 -1
  7. package/dist/constants.d.ts +7 -0
  8. package/dist/constants.js +7 -0
  9. package/dist/core/change-tracker.d.ts +13 -0
  10. package/dist/core/change-tracker.js +13 -0
  11. package/dist/core/file-scanner.d.ts +13 -0
  12. package/dist/core/file-scanner.js +12 -0
  13. package/dist/core/index-state-store.d.ts +67 -0
  14. package/dist/core/index-state-store.js +204 -50
  15. package/dist/core/knowledge-base-manager.d.ts +24 -2
  16. package/dist/core/knowledge-base-manager.js +204 -52
  17. package/dist/core/multi-project-manager.d.ts +10 -0
  18. package/dist/core/multi-project-manager.js +21 -2
  19. package/dist/core/project-config.d.ts +4 -0
  20. package/dist/core/project-config.js +4 -0
  21. package/dist/core/project-id.d.ts +5 -0
  22. package/dist/core/project-id.js +5 -0
  23. package/dist/core/project-registry.d.ts +1 -0
  24. package/dist/core/project-registry.js +1 -0
  25. package/dist/dedup/dedup-engine.d.ts +3 -0
  26. package/dist/dedup/dedup-engine.js +1 -0
  27. package/dist/dedup/relationship-detector.d.ts +7 -0
  28. package/dist/dedup/relationship-detector.js +7 -0
  29. package/dist/embedding/embedding-provider.d.ts +32 -0
  30. package/dist/embedding/embedding-provider.js +136 -2
  31. package/dist/extraction/content-extractor.d.ts +32 -2
  32. package/dist/extraction/content-extractor.js +524 -124
  33. package/dist/extraction/external-extractor.d.ts +10 -0
  34. package/dist/extraction/external-extractor.js +6 -0
  35. package/dist/extraction/module-resolver.js +2 -2
  36. package/dist/index.d.ts +2 -2
  37. package/dist/index.js +4 -4
  38. package/dist/search/federation-search.d.ts +8 -0
  39. package/dist/search/federation-search.js +2 -0
  40. package/dist/types.d.ts +10 -0
  41. package/dist/vector/collection-manager.d.ts +3 -0
  42. package/dist/vector/collection-manager.js +3 -0
  43. package/dist/vector/hnsw-vector-store.d.ts +21 -0
  44. package/dist/vector/hnsw-vector-store.js +108 -0
  45. package/dist/vector/types.d.ts +8 -0
  46. package/dist/vector/vector-indexer.d.ts +15 -1
  47. package/dist/vector/vector-indexer.js +34 -5
  48. package/models/bge-small-zh-v1.5/config.json +31 -0
  49. package/models/bge-small-zh-v1.5/onnx/model_quantized.onnx +0 -0
  50. package/models/bge-small-zh-v1.5/special_tokens_map.json +7 -0
  51. package/models/bge-small-zh-v1.5/tokenizer.json +21278 -0
  52. package/models/bge-small-zh-v1.5/tokenizer_config.json +15 -0
  53. package/package.json +16 -9
  54. package/scripts/install-hnsw.cjs +47 -0
  55. package/dist/vector/sqlite-vec-store.d.ts +0 -38
  56. package/dist/vector/sqlite-vec-store.js +0 -203
@@ -0,0 +1,12 @@
1
+ /** BGE Tokenizer,使用 tokenizer.json 执行 BGE 模型的真实 Token 化 */
2
+ export declare class BgeTokenizer {
3
+ private readonly vocab;
4
+ private readonly unkToken;
5
+ constructor(tokenizerPath?: string | undefined);
6
+ /** 统计文本的 Token 数量 */
7
+ countTokens(text: string): number;
8
+ encode(text: string): string[];
9
+ private preTokenize;
10
+ private wordPiece;
11
+ private static resolveTokenizerPath;
12
+ }
@@ -0,0 +1,70 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ /** BGE Tokenizer,使用 tokenizer.json 执行 BGE 模型的真实 Token 化 */
5
+ export class BgeTokenizer {
6
+ vocab;
7
+ unkToken;
8
+ constructor(tokenizerPath = BgeTokenizer.resolveTokenizerPath()) {
9
+ if (!tokenizerPath)
10
+ throw new Error('BGE tokenizer.json 缺失,无法执行真实 Token 计数');
11
+ const raw = JSON.parse(fs.readFileSync(tokenizerPath, 'utf8'));
12
+ this.vocab = new Set(Object.keys(raw.model?.vocab ?? {}));
13
+ this.unkToken = raw.model?.unk_token ?? '[UNK]';
14
+ if (this.vocab.size === 0)
15
+ throw new Error('BGE tokenizer vocab 为空,无法执行真实 Token 计数');
16
+ }
17
+ /** 统计文本的 Token 数量 */
18
+ countTokens(text) {
19
+ return this.encode(text).length;
20
+ }
21
+ encode(text) {
22
+ const tokens = [];
23
+ for (const token of this.preTokenize(text))
24
+ tokens.push(...this.wordPiece(token));
25
+ return tokens;
26
+ }
27
+ preTokenize(text) {
28
+ const normalized = Array.from(text, char => {
29
+ const code = char.charCodeAt(0);
30
+ return code < 32 || code === 127 ? ' ' : char;
31
+ }).join('')
32
+ .replace(/([\p{Script=Han}])/gu, ' $1 ')
33
+ .normalize('NFKC');
34
+ return normalized.match(/[\p{Script=Han}]|[\p{Letter}\p{Number}_]+|[^\s\p{Letter}\p{Number}_]/gu) ?? [];
35
+ }
36
+ wordPiece(token) {
37
+ if (this.vocab.has(token))
38
+ return [token];
39
+ const pieces = [];
40
+ let start = 0;
41
+ while (start < token.length) {
42
+ let end = token.length;
43
+ let current;
44
+ while (start < end) {
45
+ const piece = `${start > 0 ? '##' : ''}${token.slice(start, end)}`;
46
+ if (this.vocab.has(piece)) {
47
+ current = piece;
48
+ break;
49
+ }
50
+ end -= 1;
51
+ }
52
+ if (!current)
53
+ return Array.from(token).map(() => this.unkToken);
54
+ pieces.push(current);
55
+ start = end;
56
+ }
57
+ return pieces;
58
+ }
59
+ static resolveTokenizerPath() {
60
+ const currentDir = path.dirname(fileURLToPath(import.meta.url));
61
+ const candidates = [
62
+ process.env.CUSTOMIZE_BGE_TOKENIZER_PATH,
63
+ process.env.KB_BGE_TOKENIZER_PATH,
64
+ path.resolve(process.cwd(), 'packages', 'knowledge', 'models', 'bge-small-zh-v1.5', 'tokenizer.json'),
65
+ path.resolve(process.cwd(), 'models', 'bge-small-zh-v1.5', 'tokenizer.json'),
66
+ path.resolve(currentDir, '..', '..', 'models', 'bge-small-zh-v1.5', 'tokenizer.json'),
67
+ ].filter(Boolean);
68
+ return candidates.find(candidate => fs.existsSync(candidate));
69
+ }
70
+ }
@@ -1,4 +1,5 @@
1
1
  import type { ClassifiedFile } from '../types.js';
2
+ /** 文本切片结果 */
2
3
  export interface TextChunk {
3
4
  index: number;
4
5
  text: string;
@@ -8,23 +9,42 @@ export interface TextChunk {
8
9
  sectionTitle?: string;
9
10
  metadata: Record<string, unknown>;
10
11
  }
12
+ /** 切片配置参数 */
11
13
  export interface ChunkConfig {
12
14
  maxChunkSize: number;
13
15
  overlap: number;
14
16
  headerInjection: boolean;
15
17
  }
18
+ /** 文本切片器,支持文档、表格、代码等多类型文件的递归式切片 */
16
19
  export declare class TextChunker {
20
+ private readonly tokenizer;
21
+ /**
22
+ * 将文本内容按类型和配置分割为切片
23
+ * @param text 原始文本内容
24
+ * @param file 已分类的文件信息
25
+ * @param metadata 额外元数据
26
+ * @returns 切片列表
27
+ */
17
28
  chunk(text: string, file: ClassifiedFile, metadata?: Record<string, unknown>): TextChunk[];
18
29
  private createCandidates;
19
30
  private createTextCandidates;
20
31
  private createTableCandidates;
21
32
  private createDataCandidates;
22
33
  private createCodeCandidates;
34
+ private normalizeCodeLanguage;
35
+ private splitCodeByLanguage;
36
+ private collectIndentSensitiveBlocks;
37
+ private collectBraceBalancedBlocks;
38
+ private splitCodeByStructuralFallback;
23
39
  private splitIntoSections;
24
40
  private mergeLeadingHeader;
25
41
  private recursiveSplit;
26
42
  private mergeParts;
27
43
  private splitByWindow;
44
+ private splitBySentenceBoundary;
45
+ private isMarkdownTable;
46
+ private extractMarkdownTableRowRange;
47
+ private splitMarkdownTable;
28
48
  private enforceCandidateLimit;
29
49
  private withHeader;
30
50
  private createChunk;
@@ -1,3 +1,4 @@
1
+ import { BgeTokenizer } from './bge-tokenizer.js';
1
2
  const DEFAULT_CONFIGS = {
2
3
  document: { maxChunkSize: 800, overlap: 100, headerInjection: true },
3
4
  spreadsheet: { maxChunkSize: 1000, overlap: 120, headerInjection: true },
@@ -18,7 +19,26 @@ const RECURSIVE_SEPARATORS = [
18
19
  /(?<=[,、])\s*/u,
19
20
  /\s+/u,
20
21
  ];
22
+ const LANGUAGE_ROUTER = {
23
+ typescript: { delimiters: /\n(?=(?:export\s+)?(?:async\s+)?(?:class|function|interface|type|const|let)\s)/u, blockStart: /\{\s*$/u, indentSensitive: false },
24
+ javascript: { delimiters: /\n(?=(?:export\s+)?(?:async\s+)?(?:class|function|const|let)\s)/u, blockStart: /\{\s*$/u, indentSensitive: false },
25
+ python: { delimiters: /\n(?=(?:class|def)\s)/u, blockStart: /:\s*$/u, indentSensitive: true },
26
+ go: { delimiters: /\n(?=(?:func|type|struct|interface)\s)/u, blockStart: /\{\s*$/u, indentSensitive: false },
27
+ java: { delimiters: /\n(?=(?:public|protected|private|static|final|abstract|\s)*(?:class|interface|enum|(?:\w|<|>|\[|\])+\s+\w+\s*\())/u, blockStart: /\{\s*$/u, indentSensitive: false },
28
+ csharp: { delimiters: /\n(?=(?:public|protected|private|internal|static|sealed|abstract|\s)*(?:class|interface|enum|(?:\w|<|>|\[|\])+\s+\w+\s*\())/u, blockStart: /\{\s*$/u, indentSensitive: false },
29
+ cpp: { delimiters: /\n(?=(?:class|struct|namespace|template)\s|[\w:*&<>]+\s+\w+\s*\()/u, blockStart: /\{\s*$/u, indentSensitive: false },
30
+ c: { delimiters: /\n(?=(?:struct|enum)\s|[\w*]+\s+\w+\s*\()/u, blockStart: /\{\s*$/u, indentSensitive: false },
31
+ };
32
+ /** 文本切片器,支持文档、表格、代码等多类型文件的递归式切片 */
21
33
  export class TextChunker {
34
+ tokenizer = new BgeTokenizer();
35
+ /**
36
+ * 将文本内容按类型和配置分割为切片
37
+ * @param text 原始文本内容
38
+ * @param file 已分类的文件信息
39
+ * @param metadata 额外元数据
40
+ * @returns 切片列表
41
+ */
22
42
  chunk(text, file, metadata = {}) {
23
43
  const source = text.trim();
24
44
  if (source.length === 0)
@@ -34,7 +54,7 @@ export class TextChunker {
34
54
  if (file.category === 'data')
35
55
  return this.createDataCandidates(text, config);
36
56
  if (file.category === 'code')
37
- return this.createCodeCandidates(text, config);
57
+ return this.createCodeCandidates(text, file, config);
38
58
  return this.createTextCandidates(text, file.category, config);
39
59
  }
40
60
  createTextCandidates(text, category, config) {
@@ -62,43 +82,23 @@ export class TextChunker {
62
82
  return candidates;
63
83
  }
64
84
  createTableCandidates(text, config) {
65
- const lines = text.split(/\r?\n/u).map(line => line.trim()).filter(Boolean);
66
- const headerLines = lines.filter(line => /^(文件|类型|Sheet|表格|列|Columns?)[::]/iu.test(line));
67
- const dataLines = lines.filter(line => !headerLines.includes(line));
68
- if (dataLines.length === 0)
69
- return this.createTextCandidates(text, 'spreadsheet', config);
70
- const header = headerLines.join('\n');
71
- const candidates = [];
72
- let rowStart = 0;
73
- let parentIndex = 0;
74
- while (rowStart < dataLines.length) {
75
- const rows = [];
76
- while (rowStart + rows.length < dataLines.length) {
77
- const nextLine = dataLines[rowStart + rows.length];
78
- if (!nextLine)
79
- break;
80
- const nextRows = [...rows, nextLine];
81
- const candidate = [header, `行范围: ${rowStart + 1}-${rowStart + nextRows.length}`, ...nextRows].filter(Boolean).join('\n');
82
- if (rows.length > 0 && this.estimateTokens(candidate) > config.maxChunkSize)
83
- break;
84
- rows.push(nextLine);
85
- }
86
- const textChunk = [header, `行范围: ${rowStart + 1}-${rowStart + rows.length}`, ...rows].filter(Boolean).join('\n');
87
- candidates.push({
88
- text: textChunk,
89
- startChar: text.indexOf(rows[0] ?? ''),
90
- endChar: text.indexOf(rows.at(-1) ?? '') + (rows.at(-1)?.length ?? 0),
91
- sectionTitle: '表格数据',
92
- kind: 'table',
93
- parentId: `table-${parentIndex}`,
94
- parentIndex,
95
- childIndex: 0,
96
- rowRange: `${rowStart + 1}-${rowStart + rows.length}`,
85
+ if (this.isMarkdownTable(text)) {
86
+ return this.splitMarkdownTable(text, config.maxChunkSize).map((part, index) => {
87
+ const startChar = Math.max(0, text.indexOf(part.slice(0, 40)));
88
+ return {
89
+ text: part,
90
+ startChar,
91
+ endChar: startChar + part.length,
92
+ sectionTitle: this.extractSectionTitle(part) ?? '表格数据',
93
+ kind: 'table',
94
+ parentId: `table-${index}`,
95
+ parentIndex: index,
96
+ childIndex: 0,
97
+ rowRange: this.extractMarkdownTableRowRange(part),
98
+ };
97
99
  });
98
- rowStart += Math.max(1, rows.length);
99
- parentIndex += 1;
100
100
  }
101
- return candidates;
101
+ return this.createTextCandidates(text, 'spreadsheet', config);
102
102
  }
103
103
  createDataCandidates(text, config) {
104
104
  const sections = text.split(/\n(?=[\w.[\]-]+[::]\s)|\n{2,}/u).map(part => part.trim()).filter(Boolean);
@@ -114,19 +114,89 @@ export class TextChunker {
114
114
  childIndex: 0,
115
115
  }));
116
116
  }
117
- createCodeCandidates(text, config) {
118
- const blocks = text.split(/\n(?=(?:export\s+)?(?:async\s+)?(?:function|class|interface|type|const|let|var)\s)/u).map(part => part.trim()).filter(Boolean);
117
+ createCodeCandidates(text, file, config) {
118
+ const language = this.normalizeCodeLanguage(file.format);
119
+ const languageConfig = LANGUAGE_ROUTER[language];
120
+ const blocks = languageConfig
121
+ ? this.splitCodeByLanguage(text, languageConfig)
122
+ : this.splitCodeByStructuralFallback(text);
119
123
  const parts = blocks.length > 1 ? blocks : this.recursiveSplit(text, config.maxChunkSize);
120
- return this.mergeParts(parts, config.maxChunkSize, config.overlap).map((part, index) => ({
121
- text: part,
122
- startChar: Math.max(0, text.indexOf(part.slice(0, 40))),
123
- endChar: Math.max(0, text.indexOf(part.slice(0, 40))) + part.length,
124
- sectionTitle: this.extractSectionTitle(part),
125
- kind: 'code',
126
- parentId: `code-${index}`,
127
- parentIndex: index,
128
- childIndex: 0,
129
- }));
124
+ return this.mergeParts(parts, config.maxChunkSize, config.overlap).map((part, index) => {
125
+ const startChar = Math.max(0, text.indexOf(part.slice(0, 40)));
126
+ return {
127
+ text: part,
128
+ startChar,
129
+ endChar: startChar + part.length,
130
+ sectionTitle: this.extractSectionTitle(part),
131
+ kind: 'code',
132
+ parentId: `code-${language}-${index}`,
133
+ parentIndex: index,
134
+ childIndex: 0,
135
+ };
136
+ });
137
+ }
138
+ normalizeCodeLanguage(format) {
139
+ const aliases = {
140
+ ts: 'typescript', tsx: 'typescript', typescript: 'typescript',
141
+ js: 'javascript', jsx: 'javascript', javascript: 'javascript',
142
+ py: 'python', python: 'python',
143
+ golang: 'go', go: 'go',
144
+ java: 'java', cs: 'csharp', csharp: 'csharp',
145
+ cpp: 'cpp', cxx: 'cpp', cc: 'cpp', hpp: 'cpp',
146
+ c: 'c', h: 'c',
147
+ };
148
+ return aliases[format.toLowerCase()] ?? format.toLowerCase();
149
+ }
150
+ splitCodeByLanguage(text, config) {
151
+ const raw = text.split(config.delimiters).map(part => part.trim()).filter(Boolean);
152
+ if (raw.length <= 1)
153
+ return raw;
154
+ return raw.flatMap(block => config.indentSensitive ? this.collectIndentSensitiveBlocks(block, config) : this.collectBraceBalancedBlocks(block, config));
155
+ }
156
+ collectIndentSensitiveBlocks(block, config) {
157
+ const lines = block.split(/\r?\n/u);
158
+ const result = [];
159
+ let current = [];
160
+ let baseIndent;
161
+ for (const line of lines) {
162
+ const indent = line.match(/^\s*/u)?.[0].length ?? 0;
163
+ if (current.length > 0 && baseIndent != null && indent <= baseIndent && config.blockStart.test(current[0] ?? '') && line.trim()) {
164
+ result.push(current.join('\n').trim());
165
+ current = [];
166
+ baseIndent = undefined;
167
+ }
168
+ if (current.length === 0)
169
+ baseIndent = indent;
170
+ current.push(line);
171
+ }
172
+ if (current.length > 0)
173
+ result.push(current.join('\n').trim());
174
+ return result.filter(Boolean);
175
+ }
176
+ collectBraceBalancedBlocks(block, _config) {
177
+ const lines = block.split(/\r?\n/u);
178
+ const result = [];
179
+ let current = [];
180
+ let depth = 0;
181
+ for (const line of lines) {
182
+ current.push(line);
183
+ depth += (line.match(/\{/gu) ?? []).length;
184
+ depth -= (line.match(/\}/gu) ?? []).length;
185
+ if (current.length > 1 && depth <= 0) {
186
+ result.push(current.join('\n').trim());
187
+ current = [];
188
+ depth = 0;
189
+ }
190
+ }
191
+ if (current.length > 0)
192
+ result.push(current.join('\n').trim());
193
+ return result.filter(Boolean);
194
+ }
195
+ splitCodeByStructuralFallback(text) {
196
+ const blocks = this.collectBraceBalancedBlocks(text, { delimiters: /\n/u, blockStart: /\{\s*$/u, indentSensitive: false });
197
+ if (blocks.length > 1)
198
+ return blocks;
199
+ return text.split(/\n(?=\S)/u).map(part => part.trim()).filter(Boolean);
130
200
  }
131
201
  splitIntoSections(text, category) {
132
202
  const pattern = category === 'cad' || category === 'diagram'
@@ -154,11 +224,13 @@ export class TextChunker {
154
224
  recursiveSplit(text, maxTokens, separatorIndex = 0) {
155
225
  if (this.estimateTokens(text) <= maxTokens)
156
226
  return [text.trim()].filter(Boolean);
227
+ if (this.isMarkdownTable(text))
228
+ return this.splitMarkdownTable(text, maxTokens);
157
229
  if (separatorIndex >= RECURSIVE_SEPARATORS.length)
158
- return this.splitByWindow(text, maxTokens);
230
+ return this.splitBySentenceBoundary(text, maxTokens);
159
231
  const separator = RECURSIVE_SEPARATORS[separatorIndex];
160
232
  if (!separator)
161
- return this.splitByWindow(text, maxTokens);
233
+ return this.splitBySentenceBoundary(text, maxTokens);
162
234
  const parts = text.split(separator).map(part => part.trim()).filter(Boolean);
163
235
  if (parts.length <= 1)
164
236
  return this.recursiveSplit(text, maxTokens, separatorIndex + 1);
@@ -183,8 +255,11 @@ export class TextChunker {
183
255
  return chunks.flatMap(chunk => this.estimateTokens(chunk) > maxTokens ? this.splitByWindow(chunk, maxTokens, overlapTokens) : [chunk]);
184
256
  }
185
257
  splitByWindow(text, maxTokens, overlapTokens = 0) {
186
- const maxChars = Math.max(200, maxTokens * 4);
187
- const overlapChars = Math.max(0, overlapTokens * 4);
258
+ const tokens = this.tokenizer.encode(text);
259
+ if (tokens.length <= maxTokens)
260
+ return [text.trim()].filter(Boolean);
261
+ const maxChars = Math.max(200, Math.ceil(text.length * (maxTokens / Math.max(1, tokens.length))));
262
+ const overlapChars = Math.max(0, Math.ceil(text.length * (overlapTokens / Math.max(1, tokens.length))));
188
263
  const step = Math.max(1, maxChars - overlapChars);
189
264
  const chunks = [];
190
265
  for (let start = 0; start < text.length; start += step) {
@@ -194,6 +269,44 @@ export class TextChunker {
194
269
  }
195
270
  return chunks.filter(Boolean);
196
271
  }
272
+ splitBySentenceBoundary(text, maxTokens) {
273
+ const units = text.split(/(?<=[。?!;;.!?])\s+|(?<=[。?!;;.!?])/u).map(part => part.trim()).filter(Boolean);
274
+ if (units.length <= 1)
275
+ return this.splitByWindow(text, maxTokens);
276
+ return this.mergeParts(units, maxTokens, 0);
277
+ }
278
+ isMarkdownTable(text) {
279
+ const lines = text.trim().split(/\r?\n/u);
280
+ return lines.length >= 3 && lines.some(line => /^\s*\|?\s*:?-{3,}:?\s*\|/u.test(line));
281
+ }
282
+ extractMarkdownTableRowRange(text) {
283
+ const lines = text.trim().split(/\r?\n/u).filter(line => /^\s*\|/u.test(line));
284
+ const rowCount = Math.max(0, lines.length - 2);
285
+ return rowCount > 0 ? `1-${rowCount}` : undefined;
286
+ }
287
+ splitMarkdownTable(text, maxTokens) {
288
+ const lines = text.trim().split(/\r?\n/u).filter(Boolean);
289
+ const separatorIndex = lines.findIndex(line => /^\s*\|?\s*:?-{3,}:?\s*\|/u.test(line));
290
+ if (separatorIndex <= 0)
291
+ return this.splitBySentenceBoundary(text, maxTokens);
292
+ const header = lines.slice(0, separatorIndex + 1);
293
+ const rows = lines.slice(separatorIndex + 1);
294
+ const chunks = [];
295
+ let current = [];
296
+ for (const row of rows) {
297
+ const candidate = [...header, ...current, row].join('\n');
298
+ if (current.length > 0 && this.estimateTokens(candidate) > maxTokens) {
299
+ chunks.push([...header, ...current].join('\n'));
300
+ current = [row];
301
+ }
302
+ else {
303
+ current.push(row);
304
+ }
305
+ }
306
+ if (current.length > 0)
307
+ chunks.push([...header, ...current].join('\n'));
308
+ return chunks.flatMap(chunk => this.estimateTokens(chunk) > maxTokens ? this.splitByWindow(chunk, maxTokens) : [chunk]);
309
+ }
197
310
  enforceCandidateLimit(candidates, config) {
198
311
  return candidates.flatMap(candidate => {
199
312
  if (this.estimateTokens(candidate.text) <= config.maxChunkSize)
@@ -263,6 +376,6 @@ export class TextChunker {
263
376
  return text.slice(Math.max(0, text.length - chars));
264
377
  }
265
378
  estimateTokens(text) {
266
- return Math.max(1, Math.ceil(text.length / 4));
379
+ return Math.max(1, this.tokenizer.countTokens(text));
267
380
  }
268
381
  }
@@ -1,5 +1,6 @@
1
1
  import type { Stats } from 'node:fs';
2
2
  import type { ClassifiedFile, FileCategory } from '../types.js';
3
+ /** 文件分类器,根据扩展名将文件归类到预定义的分类体系 */
3
4
  export declare class FileClassifier {
4
5
  private readonly extensionMap;
5
6
  constructor();
@@ -1,5 +1,6 @@
1
1
  import * as path from 'node:path';
2
2
  const DEFAULT_MAX_FILE_SIZE_BYTES = 500 * 1024 * 1024;
3
+ /** 文件分类器,根据扩展名将文件归类到预定义的分类体系 */
3
4
  export class FileClassifier {
4
5
  extensionMap;
5
6
  constructor() {
@@ -76,7 +77,6 @@ export class FileClassifier {
76
77
  ['.html', 'web', 'html'], ['.htm', 'web', 'html'], ['.xhtml', 'web', 'html'], ['.css', 'web', 'stylesheet'], ['.scss', 'web', 'stylesheet'], ['.sass', 'web', 'stylesheet'], ['.less', 'web', 'stylesheet'],
77
78
  ['.hbs', 'web', 'template'], ['.ejs', 'web', 'template'], ['.pug', 'web', 'template'], ['.j2', 'web', 'template'], ['.jinja2', 'web', 'template'],
78
79
  ['.drawio', 'diagram', 'drawio'], ['.dio', 'diagram', 'drawio'], ['.vsdx', 'diagram', 'visio'], ['.vdx', 'diagram', 'visio'], ['.puml', 'diagram', 'plantuml'], ['.plantuml', 'diagram', 'plantuml'], ['.mmd', 'diagram', 'mermaid'], ['.mermaid', 'diagram', 'mermaid'], ['.excalidraw', 'diagram', 'excalidraw'],
79
- ['.zip', 'archive', 'zip'], ['.jar', 'archive', 'zip'], ['.war', 'archive', 'zip'], ['.apk', 'archive', 'zip'], ['.tar', 'archive', 'tar'], ['.gz', 'archive', 'other'], ['.tgz', 'archive', 'tar'], ['.bz2', 'archive', 'other'], ['.rar', 'archive', 'other'], ['.7z', 'archive', 'other'],
80
80
  ];
81
81
  return new Map(entries.map(([ext, category, format]) => [ext, [category, format]]));
82
82
  }
@@ -1,8 +1,15 @@
1
1
  import type { FileCategory } from './types.js';
2
+ /** 知识库数据目录名 */
2
3
  export declare const KNOWLEDGE_BASE_DIR = "knowledgeBase";
4
+ /** 项目配置文件路径(相对于项目根目录) */
3
5
  export declare const PROJECT_CONFIG_PATH: readonly [".customize-agent", "kb", "project.json"];
6
+ /** 自定义 Agent 用户数据目录 */
4
7
  export declare const USER_DATA_DIR = ".customize-agent";
8
+ /** 全局知识库目录名 */
5
9
  export declare const GLOBAL_KNOWLEDGE_DIR = "global-knowledge";
10
+ /** 所有支持的文件分类 */
6
11
  export declare const ALL_CATEGORIES: readonly FileCategory[];
12
+ /** 各文件分类的默认目录(中文名称) */
7
13
  export declare const DEFAULT_CATEGORY_DIRS: Record<FileCategory, string>;
14
+ /** 各文件分类对应的 Vector Collection 名称(英文,供存储使用) */
8
15
  export declare const COLLECTION_CATEGORY_NAMES: Record<FileCategory, string>;
package/dist/constants.js CHANGED
@@ -1,7 +1,12 @@
1
+ /** 知识库数据目录名 */
1
2
  export const KNOWLEDGE_BASE_DIR = 'knowledgeBase';
3
+ /** 项目配置文件路径(相对于项目根目录) */
2
4
  export const PROJECT_CONFIG_PATH = ['.customize-agent', 'kb', 'project.json'];
5
+ /** 自定义 Agent 用户数据目录 */
3
6
  export const USER_DATA_DIR = '.customize-agent';
7
+ /** 全局知识库目录名 */
4
8
  export const GLOBAL_KNOWLEDGE_DIR = 'global-knowledge';
9
+ /** 所有支持的文件分类 */
5
10
  export const ALL_CATEGORIES = [
6
11
  'document',
7
12
  'spreadsheet',
@@ -14,6 +19,7 @@ export const ALL_CATEGORIES = [
14
19
  'archive',
15
20
  'other',
16
21
  ];
22
+ /** 各文件分类的默认目录(中文名称) */
17
23
  export const DEFAULT_CATEGORY_DIRS = {
18
24
  document: '文档资料',
19
25
  spreadsheet: '表格数据',
@@ -26,6 +32,7 @@ export const DEFAULT_CATEGORY_DIRS = {
26
32
  archive: '压缩包',
27
33
  other: '其他文件',
28
34
  };
35
+ /** 各文件分类对应的 Vector Collection 名称(英文,供存储使用) */
29
36
  export const COLLECTION_CATEGORY_NAMES = {
30
37
  document: 'documents',
31
38
  spreadsheet: 'spreadsheets',
@@ -2,10 +2,23 @@ import type { FileClassifier } from '../classification/classifier.js';
2
2
  import type { DiffResult } from '../types.js';
3
3
  import type { DiskFileStat } from './file-scanner.js';
4
4
  import type { IndexStateStore } from './index-state-store.js';
5
+ /** 文件变更追踪器,用于比对磁盘文件与索引状态之间的差异 */
5
6
  export declare class ChangeTracker {
6
7
  private readonly store;
7
8
  constructor(store: IndexStateStore);
9
+ /**
10
+ * 计算磁盘文件与索引状态之间的差异
11
+ * @param diskFiles 磁盘上的文件列表
12
+ * @param classifier 文件分类器
13
+ * @param kbPath 知识库路径
14
+ * @returns 文件差异对比结果
15
+ */
8
16
  computeDiff(diskFiles: Map<string, DiskFileStat>, classifier: FileClassifier, kbPath: string): Promise<DiffResult>;
17
+ /**
18
+ * 计算文件的 SHA-256 哈希值
19
+ * @param filePath 文件路径
20
+ * @returns SHA-256 哈希字符串
21
+ */
9
22
  hashFile(filePath: string): string;
10
23
  private parseMetadata;
11
24
  }
@@ -1,11 +1,19 @@
1
1
  import * as crypto from 'node:crypto';
2
2
  import * as fs from 'node:fs';
3
3
  import * as path from 'node:path';
4
+ /** 文件变更追踪器,用于比对磁盘文件与索引状态之间的差异 */
4
5
  export class ChangeTracker {
5
6
  store;
6
7
  constructor(store) {
7
8
  this.store = store;
8
9
  }
10
+ /**
11
+ * 计算磁盘文件与索引状态之间的差异
12
+ * @param diskFiles 磁盘上的文件列表
13
+ * @param classifier 文件分类器
14
+ * @param kbPath 知识库路径
15
+ * @returns 文件差异对比结果
16
+ */
9
17
  async computeDiff(diskFiles, classifier, kbPath) {
10
18
  const startTime = Date.now();
11
19
  const indexedFiles = this.store.loadActiveRecords();
@@ -73,6 +81,11 @@ export class ChangeTracker {
73
81
  diffTimeMs: Date.now() - startTime,
74
82
  };
75
83
  }
84
+ /**
85
+ * 计算文件的 SHA-256 哈希值
86
+ * @param filePath 文件路径
87
+ * @returns SHA-256 哈希字符串
88
+ */
76
89
  hashFile(filePath) {
77
90
  return crypto.createHash('sha256').update(fs.readFileSync(filePath)).digest('hex');
78
91
  }
@@ -1,8 +1,21 @@
1
+ /** 磁盘文件信息 */
1
2
  export interface DiskFileStat {
2
3
  size: number;
3
4
  mtime: number;
4
5
  }
6
+ /** 知识库文件扫描器,用于扫描和加载知识库目录中的文件 */
5
7
  export declare class KnowledgeFileScanner {
8
+ /**
9
+ * 扫描知识库目录中的所有文件
10
+ * @param kbPath 知识库路径
11
+ * @param ignorePatterns 忽略模式列表
12
+ * @returns 文件相对路径到文件信息的映射
13
+ */
6
14
  scan(kbPath: string, ignorePatterns?: string[]): Promise<Map<string, DiskFileStat>>;
15
+ /**
16
+ * 加载 .kbignore 忽略规则文件
17
+ * @param kbPath 知识库路径
18
+ * @returns 忽略规则列表
19
+ */
7
20
  loadKbIgnore(kbPath: string): string[];
8
21
  }
@@ -1,7 +1,14 @@
1
1
  import * as fs from 'node:fs';
2
2
  import * as path from 'node:path';
3
3
  import fg from 'fast-glob';
4
+ /** 知识库文件扫描器,用于扫描和加载知识库目录中的文件 */
4
5
  export class KnowledgeFileScanner {
6
+ /**
7
+ * 扫描知识库目录中的所有文件
8
+ * @param kbPath 知识库路径
9
+ * @param ignorePatterns 忽略模式列表
10
+ * @returns 文件相对路径到文件信息的映射
11
+ */
5
12
  async scan(kbPath, ignorePatterns = []) {
6
13
  if (!fs.existsSync(kbPath))
7
14
  return new Map();
@@ -20,6 +27,11 @@ export class KnowledgeFileScanner {
20
27
  }
21
28
  return files;
22
29
  }
30
+ /**
31
+ * 加载 .kbignore 忽略规则文件
32
+ * @param kbPath 知识库路径
33
+ * @returns 忽略规则列表
34
+ */
23
35
  loadKbIgnore(kbPath) {
24
36
  const ignorePath = path.join(kbPath, '.kbignore');
25
37
  if (!fs.existsSync(ignorePath))