@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.
- package/dist/chunking/bge-tokenizer.d.ts +12 -0
- package/dist/chunking/bge-tokenizer.js +70 -0
- package/dist/chunking/text-chunker.d.ts +20 -0
- package/dist/chunking/text-chunker.js +166 -53
- package/dist/classification/classifier.d.ts +1 -0
- package/dist/classification/classifier.js +1 -1
- package/dist/constants.d.ts +7 -0
- package/dist/constants.js +7 -0
- package/dist/core/change-tracker.d.ts +13 -0
- package/dist/core/change-tracker.js +13 -0
- package/dist/core/file-scanner.d.ts +13 -0
- package/dist/core/file-scanner.js +12 -0
- package/dist/core/index-state-store.d.ts +67 -0
- package/dist/core/index-state-store.js +204 -50
- package/dist/core/knowledge-base-manager.d.ts +24 -2
- package/dist/core/knowledge-base-manager.js +204 -52
- package/dist/core/multi-project-manager.d.ts +10 -0
- package/dist/core/multi-project-manager.js +21 -2
- package/dist/core/project-config.d.ts +4 -0
- package/dist/core/project-config.js +4 -0
- package/dist/core/project-id.d.ts +5 -0
- package/dist/core/project-id.js +5 -0
- package/dist/core/project-registry.d.ts +1 -0
- package/dist/core/project-registry.js +1 -0
- package/dist/dedup/dedup-engine.d.ts +3 -0
- package/dist/dedup/dedup-engine.js +1 -0
- package/dist/dedup/relationship-detector.d.ts +7 -0
- package/dist/dedup/relationship-detector.js +7 -0
- package/dist/embedding/embedding-provider.d.ts +32 -0
- package/dist/embedding/embedding-provider.js +136 -2
- package/dist/extraction/content-extractor.d.ts +32 -2
- package/dist/extraction/content-extractor.js +524 -124
- package/dist/extraction/external-extractor.d.ts +10 -0
- package/dist/extraction/external-extractor.js +6 -0
- package/dist/extraction/module-resolver.js +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +4 -4
- package/dist/search/federation-search.d.ts +8 -0
- package/dist/search/federation-search.js +2 -0
- package/dist/types.d.ts +10 -0
- package/dist/vector/collection-manager.d.ts +3 -0
- package/dist/vector/collection-manager.js +3 -0
- package/dist/vector/hnsw-vector-store.d.ts +21 -0
- package/dist/vector/hnsw-vector-store.js +108 -0
- package/dist/vector/types.d.ts +8 -0
- package/dist/vector/vector-indexer.d.ts +15 -1
- package/dist/vector/vector-indexer.js +34 -5
- package/models/bge-small-zh-v1.5/config.json +31 -0
- package/models/bge-small-zh-v1.5/onnx/model_quantized.onnx +0 -0
- package/models/bge-small-zh-v1.5/special_tokens_map.json +7 -0
- package/models/bge-small-zh-v1.5/tokenizer.json +21278 -0
- package/models/bge-small-zh-v1.5/tokenizer_config.json +15 -0
- package/package.json +16 -9
- package/scripts/install-hnsw.cjs +47 -0
- package/dist/vector/sqlite-vec-store.d.ts +0 -38
- 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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
|
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
|
|
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
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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.
|
|
230
|
+
return this.splitBySentenceBoundary(text, maxTokens);
|
|
159
231
|
const separator = RECURSIVE_SEPARATORS[separatorIndex];
|
|
160
232
|
if (!separator)
|
|
161
|
-
return this.
|
|
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
|
|
187
|
-
|
|
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,
|
|
379
|
+
return Math.max(1, this.tokenizer.countTokens(text));
|
|
267
380
|
}
|
|
268
381
|
}
|
|
@@ -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
|
}
|
package/dist/constants.d.ts
CHANGED
|
@@ -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))
|