@customize-agent/knowledge 2.0.0 → 2.1.0

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 (37) hide show
  1. package/dist/chunking/text-chunker.d.ts +12 -1
  2. package/dist/chunking/text-chunker.js +219 -41
  3. package/dist/core/change-tracker.d.ts +1 -0
  4. package/dist/core/change-tracker.js +21 -0
  5. package/dist/core/index-state-store.d.ts +35 -1
  6. package/dist/core/index-state-store.js +249 -16
  7. package/dist/core/knowledge-base-manager.d.ts +69 -3
  8. package/dist/core/knowledge-base-manager.js +535 -132
  9. package/dist/core/multi-project-manager.d.ts +7 -4
  10. package/dist/core/multi-project-manager.js +35 -23
  11. package/dist/embedding/embedding-provider.d.ts +1 -0
  12. package/dist/embedding/embedding-provider.js +16 -1
  13. package/dist/extraction/content-extractor.d.ts +2 -0
  14. package/dist/extraction/content-extractor.js +209 -42
  15. package/dist/extraction/module-resolver.d.ts +17 -0
  16. package/dist/extraction/module-resolver.js +113 -0
  17. package/dist/index.d.ts +2 -3
  18. package/dist/index.js +2 -2
  19. package/dist/llm/llm-search-provider.d.ts +23 -0
  20. package/dist/llm/llm-search-provider.js +1 -0
  21. package/dist/search/federation-search.d.ts +29 -0
  22. package/dist/search/federation-search.js +8 -1
  23. package/dist/vector/chroma-store.d.ts +2 -0
  24. package/dist/vector/chroma-store.js +53 -22
  25. package/dist/vector/vector-indexer.d.ts +3 -0
  26. package/dist/vector/vector-indexer.js +23 -0
  27. package/package.json +11 -3
  28. package/dist/server/dashboard-client.d.ts +0 -2
  29. package/dist/server/dashboard-client.js +0 -396
  30. package/dist/server/dashboard-i18n.d.ts +0 -112
  31. package/dist/server/dashboard-i18n.js +0 -220
  32. package/dist/server/dashboard-page.d.ts +0 -6
  33. package/dist/server/dashboard-page.js +0 -138
  34. package/dist/server/dashboard-server.d.ts +0 -13
  35. package/dist/server/dashboard-server.js +0 -225
  36. package/dist/server/dashboard-styles.d.ts +0 -1
  37. package/dist/server/dashboard-styles.js +0 -152
@@ -15,9 +15,20 @@ export interface ChunkConfig {
15
15
  }
16
16
  export declare class TextChunker {
17
17
  chunk(text: string, file: ClassifiedFile, metadata?: Record<string, unknown>): TextChunk[];
18
+ private createCandidates;
19
+ private createTextCandidates;
20
+ private createTableCandidates;
21
+ private createDataCandidates;
22
+ private createCodeCandidates;
23
+ private splitIntoSections;
24
+ private mergeLeadingHeader;
25
+ private recursiveSplit;
26
+ private mergeParts;
27
+ private splitByWindow;
28
+ private enforceCandidateLimit;
18
29
  private withHeader;
19
- private splitBySemanticBoundary;
20
30
  private createChunk;
31
+ private kindForCategory;
21
32
  private extractSectionTitle;
22
33
  private takeOverlap;
23
34
  private estimateTokens;
@@ -1,75 +1,253 @@
1
1
  const DEFAULT_CONFIGS = {
2
2
  document: { maxChunkSize: 800, overlap: 100, headerInjection: true },
3
- spreadsheet: { maxChunkSize: 1000, overlap: 200, headerInjection: true },
3
+ spreadsheet: { maxChunkSize: 1000, overlap: 120, headerInjection: true },
4
4
  image: { maxChunkSize: 512, overlap: 0, headerInjection: true },
5
- cad: { maxChunkSize: 600, overlap: 100, headerInjection: true },
6
- code: { maxChunkSize: 1000, overlap: 200, headerInjection: true },
7
- data: { maxChunkSize: 600, overlap: 100, headerInjection: true },
5
+ cad: { maxChunkSize: 600, overlap: 80, headerInjection: true },
6
+ code: { maxChunkSize: 1000, overlap: 120, headerInjection: true },
7
+ data: { maxChunkSize: 600, overlap: 80, headerInjection: true },
8
8
  web: { maxChunkSize: 800, overlap: 100, headerInjection: true },
9
9
  diagram: { maxChunkSize: 512, overlap: 0, headerInjection: true },
10
10
  archive: { maxChunkSize: 500, overlap: 50, headerInjection: false },
11
11
  other: { maxChunkSize: 500, overlap: 50, headerInjection: false },
12
12
  };
13
+ const RECURSIVE_SEPARATORS = [
14
+ /\n(?=#{1,6}\s)/u,
15
+ /\n{2,}/u,
16
+ /\n(?=(?:第[一二三四五六七八九十百千万\d]+[章节条]|[一二三四五六七八九十]+、|\d+[.)、]))/u,
17
+ /(?<=[。!?;])\s*/u,
18
+ /(?<=[,、])\s*/u,
19
+ /\s+/u,
20
+ ];
13
21
  export class TextChunker {
14
22
  chunk(text, file, metadata = {}) {
15
- if (text.trim().length === 0)
23
+ const source = text.trim();
24
+ if (source.length === 0)
16
25
  return [];
17
26
  const config = DEFAULT_CONFIGS[file.category];
18
- const normalized = this.withHeader(text, file, config);
19
- const paragraphs = this.splitBySemanticBoundary(normalized, file.category);
27
+ const normalized = this.withHeader(source, file, config);
28
+ const candidates = this.enforceCandidateLimit(this.createCandidates(normalized, file, config), config);
29
+ return candidates.map((candidate, index) => this.createChunk(index, candidate, file, metadata));
30
+ }
31
+ createCandidates(text, file, config) {
32
+ if (file.category === 'spreadsheet')
33
+ return this.createTableCandidates(text, config);
34
+ if (file.category === 'data')
35
+ return this.createDataCandidates(text, config);
36
+ if (file.category === 'code')
37
+ return this.createCodeCandidates(text, config);
38
+ return this.createTextCandidates(text, file.category, config);
39
+ }
40
+ createTextCandidates(text, category, config) {
41
+ const sections = this.splitIntoSections(text, category);
42
+ const candidates = [];
43
+ sections.forEach((section, parentIndex) => {
44
+ const parentId = `p${parentIndex}`;
45
+ const parts = this.mergeLeadingHeader(this.recursiveSplit(section.text, config.maxChunkSize));
46
+ const merged = this.mergeParts(parts, config.maxChunkSize, config.overlap);
47
+ merged.forEach((part, childIndex) => {
48
+ const localStart = section.text.indexOf(part.replace(/^\s+/u, '').slice(0, 40));
49
+ const startChar = section.startChar + Math.max(0, localStart);
50
+ candidates.push({
51
+ text: part,
52
+ startChar,
53
+ endChar: startChar + part.length,
54
+ sectionTitle: section.title,
55
+ kind: this.kindForCategory(category),
56
+ parentId,
57
+ parentIndex,
58
+ childIndex,
59
+ });
60
+ });
61
+ });
62
+ return candidates;
63
+ }
64
+ 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}`,
97
+ });
98
+ rowStart += Math.max(1, rows.length);
99
+ parentIndex += 1;
100
+ }
101
+ return candidates;
102
+ }
103
+ createDataCandidates(text, config) {
104
+ const sections = text.split(/\n(?=[\w.[\]-]+[::]\s)|\n{2,}/u).map(part => part.trim()).filter(Boolean);
105
+ const parts = sections.length > 1 ? sections : this.recursiveSplit(text, config.maxChunkSize);
106
+ return this.mergeParts(parts, config.maxChunkSize, config.overlap).map((part, index) => ({
107
+ text: part,
108
+ startChar: Math.max(0, text.indexOf(part.slice(0, 40))),
109
+ endChar: Math.max(0, text.indexOf(part.slice(0, 40))) + part.length,
110
+ sectionTitle: this.extractSectionTitle(part),
111
+ kind: 'data',
112
+ parentId: `data-${index}`,
113
+ parentIndex: index,
114
+ childIndex: 0,
115
+ }));
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);
119
+ 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
+ }));
130
+ }
131
+ splitIntoSections(text, category) {
132
+ const pattern = category === 'cad' || category === 'diagram'
133
+ ? /\n{2,}/u
134
+ : /\n(?=#{1,6}\s)|\n{2,}/u;
135
+ const rawSections = this.mergeLeadingHeader(text.split(pattern).map(part => part.trim()).filter(Boolean));
136
+ let cursor = 0;
137
+ return rawSections.map(section => {
138
+ const startChar = Math.max(cursor, text.indexOf(section, cursor));
139
+ cursor = startChar + section.length;
140
+ return { text: section, startChar, title: this.extractSectionTitle(section) };
141
+ });
142
+ }
143
+ mergeLeadingHeader(sections) {
144
+ if (sections.length < 2)
145
+ return sections;
146
+ const first = sections[0];
147
+ if (!first)
148
+ return sections;
149
+ const isHeader = /^文件[::].+\n类型[::]/u.test(first) && this.estimateTokens(first) < 80;
150
+ if (!isHeader)
151
+ return sections;
152
+ return [`${first}\n\n${sections[1]}`, ...sections.slice(2)];
153
+ }
154
+ recursiveSplit(text, maxTokens, separatorIndex = 0) {
155
+ if (this.estimateTokens(text) <= maxTokens)
156
+ return [text.trim()].filter(Boolean);
157
+ if (separatorIndex >= RECURSIVE_SEPARATORS.length)
158
+ return this.splitByWindow(text, maxTokens);
159
+ const separator = RECURSIVE_SEPARATORS[separatorIndex];
160
+ if (!separator)
161
+ return this.splitByWindow(text, maxTokens);
162
+ const parts = text.split(separator).map(part => part.trim()).filter(Boolean);
163
+ if (parts.length <= 1)
164
+ return this.recursiveSplit(text, maxTokens, separatorIndex + 1);
165
+ return parts.flatMap(part => this.recursiveSplit(part, maxTokens, separatorIndex + 1));
166
+ }
167
+ mergeParts(parts, maxTokens, overlapTokens) {
20
168
  const chunks = [];
21
169
  let buffer = '';
22
- let chunkStart = 0;
23
- let cursor = 0;
24
- for (const paragraph of paragraphs) {
25
- const candidate = buffer.length === 0 ? paragraph : `${buffer}\n\n${paragraph}`;
26
- if (this.estimateTokens(candidate) > config.maxChunkSize && buffer.length > 0) {
27
- chunks.push(this.createChunk(chunks.length, buffer, chunkStart, cursor, metadata));
28
- const overlapText = this.takeOverlap(buffer, config.overlap);
29
- buffer = overlapText.length > 0 ? `${overlapText}\n\n${paragraph}` : paragraph;
30
- chunkStart = Math.max(0, cursor - overlapText.length);
170
+ for (const part of parts) {
171
+ const candidate = buffer ? `${buffer}\n${part}` : part;
172
+ if (buffer && this.estimateTokens(candidate) > maxTokens) {
173
+ chunks.push(buffer);
174
+ const overlap = this.takeOverlap(buffer, overlapTokens);
175
+ buffer = overlap ? `${overlap}\n${part}` : part;
31
176
  }
32
177
  else {
33
178
  buffer = candidate;
34
179
  }
35
- cursor += paragraph.length + 2;
36
180
  }
37
- if (buffer.trim().length > 0) {
38
- chunks.push(this.createChunk(chunks.length, buffer, chunkStart, normalized.length, metadata));
181
+ if (buffer.trim())
182
+ chunks.push(buffer);
183
+ return chunks.flatMap(chunk => this.estimateTokens(chunk) > maxTokens ? this.splitByWindow(chunk, maxTokens, overlapTokens) : [chunk]);
184
+ }
185
+ splitByWindow(text, maxTokens, overlapTokens = 0) {
186
+ const maxChars = Math.max(200, maxTokens * 4);
187
+ const overlapChars = Math.max(0, overlapTokens * 4);
188
+ const step = Math.max(1, maxChars - overlapChars);
189
+ const chunks = [];
190
+ for (let start = 0; start < text.length; start += step) {
191
+ chunks.push(text.slice(start, start + maxChars).trim());
192
+ if (start + maxChars >= text.length)
193
+ break;
39
194
  }
40
- return chunks;
195
+ return chunks.filter(Boolean);
196
+ }
197
+ enforceCandidateLimit(candidates, config) {
198
+ return candidates.flatMap(candidate => {
199
+ if (this.estimateTokens(candidate.text) <= config.maxChunkSize)
200
+ return [candidate];
201
+ return this.splitByWindow(candidate.text, config.maxChunkSize, config.overlap).map((text, index) => ({
202
+ ...candidate,
203
+ text,
204
+ childIndex: candidate.childIndex + index,
205
+ startChar: candidate.startChar + Math.max(0, candidate.text.indexOf(text.slice(0, 40))),
206
+ endChar: candidate.startChar + Math.max(0, candidate.text.indexOf(text.slice(0, 40))) + text.length,
207
+ rowRange: candidate.rowRange ? `${candidate.rowRange}#${index + 1}` : undefined,
208
+ }));
209
+ });
41
210
  }
42
211
  withHeader(text, file, config) {
43
212
  if (!config.headerInjection)
44
213
  return text;
45
214
  return `文件: ${file.relativePath}\n类型: ${file.category}/${file.format}\n\n${text}`;
46
215
  }
47
- splitBySemanticBoundary(text, category) {
48
- if (category === 'document') {
49
- return text.split(/\n(?=#{1,6}\s)|\n{2,}/u).map(part => part.trim()).filter(Boolean);
50
- }
51
- if (category === 'code') {
52
- return text.split(/\n(?=(export\s+)?(async\s+)?(function|class|interface|type|const|let|var)\s)/u).map(part => part.trim()).filter(Boolean);
53
- }
54
- if (category === 'data') {
55
- return text.split(/\n(?=[\w.[\]-]+:\s)|\n{2,}/u).map(part => part.trim()).filter(Boolean);
56
- }
57
- if (category === 'cad' || category === 'diagram') {
58
- return text.split(/\n(?=(?:CAD|STEP|IGES|Mesh|Draw\.io|Excalidraw|SVG)\b)|\n{2,}/u).map(part => part.trim()).filter(Boolean);
59
- }
60
- return text.split(/\n{2,}/u).map(part => part.trim()).filter(Boolean);
61
- }
62
- createChunk(index, text, startChar, endChar, metadata) {
216
+ createChunk(index, candidate, file, metadata) {
217
+ const text = candidate.text.trim();
63
218
  return {
64
219
  index,
65
- text: text.trim(),
66
- startChar,
67
- endChar,
220
+ text,
221
+ startChar: candidate.startChar,
222
+ endChar: candidate.endChar,
68
223
  tokenCount: this.estimateTokens(text),
69
- sectionTitle: this.extractSectionTitle(text),
70
- metadata,
224
+ sectionTitle: candidate.sectionTitle ?? this.extractSectionTitle(text),
225
+ metadata: {
226
+ ...metadata,
227
+ chunkType: 'child',
228
+ chunkKind: candidate.kind,
229
+ parentId: `${file.relativePath}#${candidate.parentId}`,
230
+ parentIndex: candidate.parentIndex,
231
+ childIndex: candidate.childIndex,
232
+ rowRange: candidate.rowRange,
233
+ sectionTitle: candidate.sectionTitle ?? this.extractSectionTitle(text),
234
+ startChar: candidate.startChar,
235
+ endChar: candidate.endChar,
236
+ splitStrategy: 'recursive_parent_child_v1',
237
+ },
71
238
  };
72
239
  }
240
+ kindForCategory(category) {
241
+ if (category === 'image' || category === 'cad' || category === 'diagram')
242
+ return 'metadata';
243
+ if (category === 'code')
244
+ return 'code';
245
+ if (category === 'data')
246
+ return 'data';
247
+ if (category === 'spreadsheet')
248
+ return 'table';
249
+ return 'text';
250
+ }
73
251
  extractSectionTitle(text) {
74
252
  const firstLine = text.trim().split(/\r?\n/u)[0]?.trim();
75
253
  if (!firstLine)
@@ -7,4 +7,5 @@ export declare class ChangeTracker {
7
7
  constructor(store: IndexStateStore);
8
8
  computeDiff(diskFiles: Map<string, DiskFileStat>, classifier: FileClassifier, kbPath: string): Promise<DiffResult>;
9
9
  hashFile(filePath: string): string;
10
+ private parseMetadata;
10
11
  }
@@ -29,6 +29,16 @@ export class ChangeTracker {
29
29
  newFiles.push(classified);
30
30
  continue;
31
31
  }
32
+ const metadata = this.parseMetadata(indexed.metadataJson);
33
+ const needsReindex = indexed.status === 'error'
34
+ || indexed.chunkCount === 0
35
+ || (classified.format === 'pdf' && indexed.chunkCount <= 1)
36
+ || metadata.contentCoverage === 'metadata_filename'
37
+ || metadata.extractionMode === 'pdf_metadata_only';
38
+ if (needsReindex) {
39
+ modifiedFiles.push(classified);
40
+ continue;
41
+ }
32
42
  if (Math.round(diskStat.mtime) !== Math.round(indexed.mtime) || diskStat.size !== indexed.fileSize) {
33
43
  const contentHash = this.hashFile(absolutePath);
34
44
  if (contentHash !== indexed.contentHash) {
@@ -63,4 +73,15 @@ export class ChangeTracker {
63
73
  hashFile(filePath) {
64
74
  return crypto.createHash('sha256').update(fs.readFileSync(filePath)).digest('hex');
65
75
  }
76
+ parseMetadata(metadataJson) {
77
+ if (!metadataJson)
78
+ return {};
79
+ try {
80
+ const parsed = JSON.parse(metadataJson);
81
+ return parsed && typeof parsed === 'object' ? parsed : {};
82
+ }
83
+ catch {
84
+ return {};
85
+ }
86
+ }
66
87
  }
@@ -15,6 +15,24 @@ export interface StoredChunk {
15
15
  }
16
16
  export interface ChunkSearchResult extends StoredChunk {
17
17
  score: number;
18
+ scoreDetails?: {
19
+ keywordScore?: number;
20
+ bm25Score?: number;
21
+ exactPhraseBoost?: number;
22
+ };
23
+ }
24
+ export interface StoredParentChunk {
25
+ id: string;
26
+ relativePath: string;
27
+ parentId: string;
28
+ content: string;
29
+ category: FileCategory;
30
+ format: string;
31
+ collectionName: string;
32
+ sectionTitle?: string;
33
+ chunkCount: number;
34
+ metadataJson?: string;
35
+ createdAt: number;
18
36
  }
19
37
  export interface FileHashRecord {
20
38
  contentHash: string;
@@ -44,6 +62,7 @@ export interface FileRelationship {
44
62
  }
45
63
  export declare class IndexStateStore {
46
64
  private readonly db;
65
+ private ftsEnabled;
47
66
  constructor(dbPath: string);
48
67
  loadActiveRecords(): Map<string, IndexStateRecord>;
49
68
  upsertRecord(record: IndexStateRecord): void;
@@ -59,7 +78,13 @@ export declare class IndexStateStore {
59
78
  relativePath?: string;
60
79
  limit?: number;
61
80
  }): StoredChunk[];
81
+ getContextChunks(relativePath: string, chunkIndex: number, window?: number): StoredChunk[];
82
+ listParentChunks(relativePath: string): StoredParentChunk[];
83
+ getParentChunk(relativePath: string, parentId: string): StoredParentChunk | undefined;
84
+ getChunksByParent(relativePath: string, parentId: string, limit?: number): StoredChunk[];
62
85
  searchChunks(query: string, limit?: number): ChunkSearchResult[];
86
+ private searchChunksFts;
87
+ private searchChunksLike;
63
88
  findExactDuplicate(contentHash: string, excludePath?: string): FileHashRecord | undefined;
64
89
  findNormalizedDuplicate(normalizedHash: string, excludePath?: string): FileHashRecord | undefined;
65
90
  upsertFileHash(record: Omit<FileHashRecord, 'createdAt' | 'updatedAt'>): void;
@@ -83,6 +108,7 @@ export declare class IndexStateStore {
83
108
  }>;
84
109
  deleteRecord(relativePath: string): void;
85
110
  setMetadata(key: string, value: string): void;
111
+ getMetadata(key: string): string | undefined;
86
112
  getStats(): {
87
113
  fileCount: number;
88
114
  chunkCount: number;
@@ -95,11 +121,19 @@ export declare class IndexStateStore {
95
121
  }>;
96
122
  close(): void;
97
123
  private initTables;
124
+ private initFts;
125
+ private rebuildFtsIfNeeded;
98
126
  private rowToMinHash;
99
127
  private rowToFileHash;
100
128
  private rowToRelationship;
129
+ private rowToParentChunk;
130
+ private splitParentGroups;
131
+ private metadataString;
101
132
  private rowToChunk;
102
133
  private expandSearchTerms;
103
- private scoreChunk;
134
+ private toFtsQuery;
135
+ private bm25ToPositiveScore;
136
+ private scoreChunkDetailed;
137
+ private countOccurrences;
104
138
  private rowToRecord;
105
139
  }