@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
@@ -1,6 +1,18 @@
1
1
  import type { TextChunk } from '../chunking/text-chunker.js';
2
2
  import type { FileCategory, IndexStateRecord } from '../types.js';
3
+ export type KnowledgeJobStatus = 'PENDING' | 'PARSING' | 'CHUNKING' | 'INDEXING' | 'SUCCESS' | 'ERROR';
4
+ export interface KnowledgeIndexJob {
5
+ id: string;
6
+ relativePath: string;
7
+ status: KnowledgeJobStatus;
8
+ percent: number;
9
+ message: string;
10
+ errorMessage?: string;
11
+ createdAt: number;
12
+ updatedAt: number;
13
+ }
3
14
  export interface StoredChunk {
15
+ rowid: number;
4
16
  id: string;
5
17
  relativePath: string;
6
18
  chunkIndex: number;
@@ -34,6 +46,18 @@ export interface StoredParentChunk {
34
46
  metadataJson?: string;
35
47
  createdAt: number;
36
48
  }
49
+ export interface StoredDocumentChunk {
50
+ id: string;
51
+ relativePath: string;
52
+ content: string;
53
+ category: FileCategory;
54
+ format: string;
55
+ collectionName: string;
56
+ parentCount: number;
57
+ chunkCount: number;
58
+ metadataJson?: string;
59
+ createdAt: number;
60
+ }
37
61
  export interface FileHashRecord {
38
62
  contentHash: string;
39
63
  filePath: string;
@@ -60,14 +84,40 @@ export interface FileRelationship {
60
84
  userConfirmed: number;
61
85
  createdAt: number;
62
86
  }
87
+ /** 索引状态存储器,使用 SQLite 管理知识库索引的持久化状态 */
63
88
  export declare class IndexStateStore {
64
89
  private readonly db;
65
90
  private ftsEnabled;
66
91
  constructor(dbPath: string);
92
+ /** 加载所有活跃的索引记录 */
67
93
  loadActiveRecords(): Map<string, IndexStateRecord>;
94
+ /** 插入或更新索引记录 */
68
95
  upsertRecord(record: IndexStateRecord): void;
96
+ /** 更新已验证文件的时间戳和状态 */
69
97
  updateVerified(relativePath: string, mtime: number): void;
70
98
  listRecords(): IndexStateRecord[];
99
+ enqueueIndexJob(job: {
100
+ id: string;
101
+ relativePath: string;
102
+ message?: string;
103
+ }): KnowledgeIndexJob;
104
+ updateIndexJob(id: string, patch: {
105
+ status?: KnowledgeJobStatus;
106
+ percent?: number;
107
+ message?: string;
108
+ errorMessage?: string;
109
+ }): void;
110
+ getIndexJob(id: string): KnowledgeIndexJob | undefined;
111
+ listPendingIndexJobs(limit?: number): KnowledgeIndexJob[];
112
+ countPendingIndexJobs(): number;
113
+ listActiveIndexJobsByPath(relativePath: string): KnowledgeIndexJob[];
114
+ listIndexJobsByPrefix(prefix: string): KnowledgeIndexJob[];
115
+ /**
116
+ * 替换指定文件的切片数据(使用事务批量更新)
117
+ * @param relativePath 文件相对路径
118
+ * @param chunks 文本切片列表
119
+ * @param file 文件分类信息
120
+ */
71
121
  replaceChunks(relativePath: string, chunks: TextChunk[], file: {
72
122
  category: FileCategory;
73
123
  format: string;
@@ -78,10 +128,19 @@ export declare class IndexStateStore {
78
128
  relativePath?: string;
79
129
  limit?: number;
80
130
  }): StoredChunk[];
131
+ getChunkByRowid(rowid: number): StoredChunk | undefined;
132
+ getChunksByRowids(rowids: number[]): StoredChunk[];
81
133
  getContextChunks(relativePath: string, chunkIndex: number, window?: number): StoredChunk[];
82
134
  listParentChunks(relativePath: string): StoredParentChunk[];
83
135
  getParentChunk(relativePath: string, parentId: string): StoredParentChunk | undefined;
136
+ getDocumentChunk(relativePath: string): StoredDocumentChunk | undefined;
84
137
  getChunksByParent(relativePath: string, parentId: string, limit?: number): StoredChunk[];
138
+ /**
139
+ * 使用关键词搜索切片(支持 FTS5 全文搜索和 LIKE 模糊匹配)
140
+ * @param query 搜索查询
141
+ * @param limit 返回结果数量上限
142
+ * @returns 搜索结果列表(按相关性得分排序)
143
+ */
85
144
  searchChunks(query: string, limit?: number): ChunkSearchResult[];
86
145
  private searchChunksFts;
87
146
  private searchChunksLike;
@@ -106,6 +165,10 @@ export declare class IndexStateStore {
106
165
  enabled: boolean;
107
166
  createdAt: number;
108
167
  }>;
168
+ /**
169
+ * 删除指定文件的全部索引数据(含切片、哈希、MinHash、标签、关系等)
170
+ * @param relativePath 文件相对路径
171
+ */
109
172
  deleteRecord(relativePath: string): void;
110
173
  setMetadata(key: string, value: string): void;
111
174
  getMetadata(key: string): string | undefined;
@@ -127,11 +190,15 @@ export declare class IndexStateStore {
127
190
  private rowToFileHash;
128
191
  private rowToRelationship;
129
192
  private rowToParentChunk;
193
+ private rowToDocumentChunk;
130
194
  private splitParentGroups;
195
+ private rowToJob;
131
196
  private metadataString;
132
197
  private rowToChunk;
133
198
  private expandSearchTerms;
134
199
  private toFtsQuery;
200
+ private mergeKeywordResults;
201
+ private chineseNgrams;
135
202
  private bm25ToPositiveScore;
136
203
  private scoreChunkDetailed;
137
204
  private countOccurrences;
@@ -1,6 +1,7 @@
1
1
  import Database from 'better-sqlite3';
2
2
  import * as fs from 'node:fs';
3
3
  import * as path from 'node:path';
4
+ /** 索引状态存储器,使用 SQLite 管理知识库索引的持久化状态 */
4
5
  export class IndexStateStore {
5
6
  db;
6
7
  ftsEnabled = false;
@@ -10,6 +11,7 @@ export class IndexStateStore {
10
11
  this.db.pragma('journal_mode = WAL');
11
12
  this.initTables();
12
13
  }
14
+ /** 加载所有活跃的索引记录 */
13
15
  loadActiveRecords() {
14
16
  const rows = this.db.prepare(`
15
17
  SELECT * FROM kb_index_state
@@ -20,6 +22,7 @@ export class IndexStateStore {
20
22
  return [record.relativePath, record];
21
23
  }));
22
24
  }
25
+ /** 插入或更新索引记录 */
23
26
  upsertRecord(record) {
24
27
  this.db.prepare(`
25
28
  INSERT INTO kb_index_state (
@@ -41,6 +44,7 @@ export class IndexStateStore {
41
44
  metadata_json = excluded.metadata_json
42
45
  `).run(record.relativePath, record.category, record.format, record.contentHash, record.fileSize, Math.round(record.mtime), record.chunkCount, record.collectionName, record.indexedAt, record.lastVerifiedAt, record.status, record.errorMessage ?? null, record.metadataJson ?? null);
43
46
  }
47
+ /** 更新已验证文件的时间戳和状态 */
44
48
  updateVerified(relativePath, mtime) {
45
49
  this.db.prepare(`
46
50
  UPDATE kb_index_state
@@ -56,11 +60,70 @@ export class IndexStateStore {
56
60
  `).all();
57
61
  return rows.map(row => this.rowToRecord(row));
58
62
  }
63
+ enqueueIndexJob(job) {
64
+ const now = Date.now();
65
+ this.db.prepare(`
66
+ INSERT INTO kb_index_jobs (id, relative_path, status, percent, message, created_at, updated_at)
67
+ VALUES (?, ?, 'PENDING', 0, ?, ?, ?)
68
+ ON CONFLICT(id) DO UPDATE SET status = 'PENDING', percent = 0, message = excluded.message, error_message = NULL, updated_at = excluded.updated_at
69
+ `).run(job.id, job.relativePath, job.message ?? '等待后台索引', now, now);
70
+ return this.getIndexJob(job.id);
71
+ }
72
+ updateIndexJob(id, patch) {
73
+ const current = this.getIndexJob(id);
74
+ if (!current)
75
+ return;
76
+ this.db.prepare(`
77
+ UPDATE kb_index_jobs
78
+ SET status = ?, percent = ?, message = ?, error_message = ?, updated_at = ?
79
+ WHERE id = ?
80
+ `).run(patch.status ?? current.status, patch.percent ?? current.percent, patch.message ?? current.message, patch.errorMessage ?? null, Date.now(), id);
81
+ }
82
+ getIndexJob(id) {
83
+ const row = this.db.prepare('SELECT * FROM kb_index_jobs WHERE id = ?').get(id);
84
+ return row ? this.rowToJob(row) : undefined;
85
+ }
86
+ listPendingIndexJobs(limit = 20) {
87
+ const rows = this.db.prepare(`
88
+ SELECT * FROM kb_index_jobs
89
+ WHERE status = 'PENDING'
90
+ ORDER BY created_at ASC
91
+ LIMIT ?
92
+ `).all(limit);
93
+ return rows.map(row => this.rowToJob(row));
94
+ }
95
+ countPendingIndexJobs() {
96
+ const row = this.db.prepare("SELECT COUNT(*) as count FROM kb_index_jobs WHERE status = 'PENDING'").get();
97
+ return Number(row?.count ?? 0);
98
+ }
99
+ listActiveIndexJobsByPath(relativePath) {
100
+ const rows = this.db.prepare(`
101
+ SELECT * FROM kb_index_jobs
102
+ WHERE relative_path = ? AND status IN ('PENDING', 'PARSING', 'CHUNKING', 'INDEXING')
103
+ ORDER BY created_at ASC
104
+ `).all(relativePath);
105
+ return rows.map(row => this.rowToJob(row));
106
+ }
107
+ listIndexJobsByPrefix(prefix) {
108
+ const rows = this.db.prepare(`
109
+ SELECT * FROM kb_index_jobs
110
+ WHERE id LIKE ?
111
+ ORDER BY created_at ASC
112
+ `).all(`${prefix}%`);
113
+ return rows.map(row => this.rowToJob(row));
114
+ }
115
+ /**
116
+ * 替换指定文件的切片数据(使用事务批量更新)
117
+ * @param relativePath 文件相对路径
118
+ * @param chunks 文本切片列表
119
+ * @param file 文件分类信息
120
+ */
59
121
  replaceChunks(relativePath, chunks, file) {
60
122
  const now = Date.now();
61
123
  const transaction = this.db.transaction(() => {
62
124
  this.db.prepare('DELETE FROM kb_chunks WHERE relative_path = ?').run(relativePath);
63
125
  this.db.prepare('DELETE FROM kb_parent_chunks WHERE relative_path = ?').run(relativePath);
126
+ this.db.prepare('DELETE FROM kb_document_chunks WHERE relative_path = ?').run(relativePath);
64
127
  if (this.ftsEnabled)
65
128
  this.db.prepare('DELETE FROM kb_chunks_fts WHERE relative_path = ?').run(relativePath);
66
129
  const insert = this.db.prepare(`
@@ -78,6 +141,12 @@ export class IndexStateStore {
78
141
  id, relative_path, parent_id, content, category, format,
79
142
  collection_name, section_title, chunk_count, metadata_json, created_at
80
143
  ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
144
+ `);
145
+ const insertDocument = this.db.prepare(`
146
+ INSERT INTO kb_document_chunks (
147
+ id, relative_path, content, category, format,
148
+ collection_name, parent_count, chunk_count, metadata_json, created_at
149
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
81
150
  `);
82
151
  const parentGroups = new Map();
83
152
  const groupedChunks = this.splitParentGroups(relativePath, chunks);
@@ -87,9 +156,13 @@ export class IndexStateStore {
87
156
  group.push(chunk);
88
157
  parentGroups.set(parentId, group);
89
158
  }
159
+ const parentContents = [];
90
160
  for (const [parentId, group] of parentGroups.entries()) {
91
- insertParent.run(parentId, relativePath, parentId, group.map(chunk => chunk.text).join('\n\n---\n\n'), file.category, file.format, file.collectionName, group.find(chunk => chunk.sectionTitle)?.sectionTitle ?? null, group.length, JSON.stringify({ parentId, splitStrategy: this.metadataString(group[0]?.metadata.splitStrategy), chunkKind: this.metadataString(group[0]?.metadata.chunkKind) }), now);
161
+ const parentContent = group.map(chunk => chunk.text).join('\n\n---\n\n');
162
+ parentContents.push(parentContent);
163
+ insertParent.run(parentId, relativePath, parentId, parentContent, file.category, file.format, file.collectionName, group.find(chunk => chunk.sectionTitle)?.sectionTitle ?? null, group.length, JSON.stringify({ parentId, splitStrategy: this.metadataString(group[0]?.metadata.splitStrategy), chunkKind: this.metadataString(group[0]?.metadata.chunkKind) }), now);
92
164
  }
165
+ insertDocument.run(`${relativePath}#document`, relativePath, parentContents.join('\n\n=== SECTION ===\n\n'), file.category, file.format, file.collectionName, parentGroups.size, groupedChunks.length, JSON.stringify({ parentType: 'document', splitStrategy: 'document_section_child_v1' }), now);
93
166
  for (const chunk of groupedChunks) {
94
167
  const chunkId = `${relativePath}#${chunk.index}`;
95
168
  insert.run(chunkId, relativePath, chunk.index, chunk.text, file.category, file.format, file.collectionName, chunk.tokenCount, chunk.sectionTitle ?? null, JSON.stringify(chunk.metadata), now);
@@ -114,16 +187,28 @@ export class IndexStateStore {
114
187
  if (options.limit)
115
188
  params.push(options.limit);
116
189
  const rows = this.db.prepare(`
117
- SELECT * FROM kb_chunks
190
+ SELECT rowid, * FROM kb_chunks
118
191
  ${where}
119
192
  ORDER BY relative_path, chunk_index
120
193
  ${limit}
121
194
  `).all(...params);
122
195
  return rows.map(row => this.rowToChunk(row, 0));
123
196
  }
197
+ getChunkByRowid(rowid) {
198
+ const row = this.db.prepare('SELECT rowid, * FROM kb_chunks WHERE rowid = ?').get(rowid);
199
+ return row ? this.rowToChunk(row, 0) : undefined;
200
+ }
201
+ getChunksByRowids(rowids) {
202
+ if (rowids.length === 0)
203
+ return [];
204
+ const placeholders = rowids.map(() => '?').join(',');
205
+ const rows = this.db.prepare(`SELECT rowid, * FROM kb_chunks WHERE rowid IN (${placeholders})`).all(...rowids);
206
+ const byRowid = new Map(rows.map(row => [Number(row.rowid), this.rowToChunk(row, 0)]));
207
+ return rowids.flatMap(rowid => byRowid.get(rowid) ?? []);
208
+ }
124
209
  getContextChunks(relativePath, chunkIndex, window = 1) {
125
210
  const rows = this.db.prepare(`
126
- SELECT * FROM kb_chunks
211
+ SELECT rowid, * FROM kb_chunks
127
212
  WHERE relative_path = ? AND chunk_index BETWEEN ? AND ?
128
213
  ORDER BY chunk_index
129
214
  `).all(relativePath, Math.max(0, chunkIndex - window), chunkIndex + window);
@@ -145,25 +230,38 @@ export class IndexStateStore {
145
230
  `).get(relativePath, parentId);
146
231
  return row ? this.rowToParentChunk(row) : undefined;
147
232
  }
233
+ getDocumentChunk(relativePath) {
234
+ const row = this.db.prepare(`
235
+ SELECT * FROM kb_document_chunks
236
+ WHERE relative_path = ?
237
+ LIMIT 1
238
+ `).get(relativePath);
239
+ return row ? this.rowToDocumentChunk(row) : undefined;
240
+ }
148
241
  getChunksByParent(relativePath, parentId, limit = 6) {
149
242
  const rows = this.db.prepare(`
150
- SELECT * FROM kb_chunks
243
+ SELECT rowid, * FROM kb_chunks
151
244
  WHERE relative_path = ? AND metadata_json LIKE ?
152
245
  ORDER BY chunk_index
153
246
  LIMIT ?
154
247
  `).all(relativePath, `%"parentId":"${parentId.replace(/[%_]/gu, '')}"%`, limit);
155
248
  return rows.map(row => this.rowToChunk(row, 0));
156
249
  }
250
+ /**
251
+ * 使用关键词搜索切片(支持 FTS5 全文搜索和 LIKE 模糊匹配)
252
+ * @param query 搜索查询
253
+ * @param limit 返回结果数量上限
254
+ * @returns 搜索结果列表(按相关性得分排序)
255
+ */
157
256
  searchChunks(query, limit = 10) {
158
257
  const terms = this.expandSearchTerms(query);
159
258
  if (terms.length === 0)
160
259
  return [];
161
- if (this.ftsEnabled) {
162
- const ftsResults = this.searchChunksFts(terms, limit);
163
- if (ftsResults.length > 0)
164
- return ftsResults;
165
- }
166
- return this.searchChunksLike(terms, limit);
260
+ const results = [
261
+ ...(this.ftsEnabled ? this.searchChunksFts(terms, limit) : []),
262
+ ...this.searchChunksLike(terms, limit),
263
+ ];
264
+ return this.mergeKeywordResults(results, limit);
167
265
  }
168
266
  searchChunksFts(terms, limit) {
169
267
  try {
@@ -171,7 +269,7 @@ export class IndexStateStore {
171
269
  if (!matchQuery)
172
270
  return [];
173
271
  const rows = this.db.prepare(`
174
- SELECT c.*, bm25(kb_chunks_fts, 1.2, 0.8, 0.6, 1.0, 2.0) as bm25_score
272
+ SELECT c.rowid, c.*, bm25(kb_chunks_fts, 1.2, 0.8, 0.6, 1.0, 2.0) as bm25_score
175
273
  FROM kb_chunks_fts
176
274
  INNER JOIN kb_chunks c ON c.id = kb_chunks_fts.id
177
275
  WHERE kb_chunks_fts MATCH ?
@@ -194,7 +292,7 @@ export class IndexStateStore {
194
292
  }
195
293
  searchChunksLike(terms, limit) {
196
294
  const rows = this.db.prepare(`
197
- SELECT * FROM kb_chunks
295
+ SELECT rowid, * FROM kb_chunks
198
296
  WHERE ${terms.map(() => '(LOWER(content) LIKE ? OR LOWER(relative_path) LIKE ? OR LOWER(category) LIKE ? OR LOWER(format) LIKE ?)').join(' OR ')}
199
297
  ORDER BY created_at DESC
200
298
  LIMIT ?
@@ -328,9 +426,14 @@ export class IndexStateStore {
328
426
  createdAt: Number(row.created_at),
329
427
  }));
330
428
  }
429
+ /**
430
+ * 删除指定文件的全部索引数据(含切片、哈希、MinHash、标签、关系等)
431
+ * @param relativePath 文件相对路径
432
+ */
331
433
  deleteRecord(relativePath) {
332
434
  this.db.prepare('DELETE FROM kb_chunks WHERE relative_path = ?').run(relativePath);
333
435
  this.db.prepare('DELETE FROM kb_parent_chunks WHERE relative_path = ?').run(relativePath);
436
+ this.db.prepare('DELETE FROM kb_document_chunks WHERE relative_path = ?').run(relativePath);
334
437
  if (this.ftsEnabled)
335
438
  this.db.prepare('DELETE FROM kb_chunks_fts WHERE relative_path = ?').run(relativePath);
336
439
  this.db.prepare('DELETE FROM kb_index_state WHERE relative_path = ?').run(relativePath);
@@ -398,7 +501,8 @@ export class IndexStateStore {
398
501
  CREATE INDEX IF NOT EXISTS idx_kb_state_collection ON kb_index_state(collection_name);
399
502
 
400
503
  CREATE TABLE IF NOT EXISTS kb_chunks (
401
- id TEXT PRIMARY KEY,
504
+ rowid INTEGER PRIMARY KEY AUTOINCREMENT,
505
+ id TEXT NOT NULL UNIQUE,
402
506
  relative_path TEXT NOT NULL,
403
507
  chunk_index INTEGER NOT NULL,
404
508
  content TEXT NOT NULL,
@@ -430,6 +534,20 @@ export class IndexStateStore {
430
534
  CREATE INDEX IF NOT EXISTS idx_kb_parent_path ON kb_parent_chunks(relative_path);
431
535
  CREATE INDEX IF NOT EXISTS idx_kb_parent_id ON kb_parent_chunks(parent_id);
432
536
 
537
+ CREATE TABLE IF NOT EXISTS kb_document_chunks (
538
+ id TEXT PRIMARY KEY,
539
+ relative_path TEXT NOT NULL,
540
+ content TEXT NOT NULL,
541
+ category TEXT NOT NULL,
542
+ format TEXT NOT NULL,
543
+ collection_name TEXT NOT NULL,
544
+ parent_count INTEGER NOT NULL,
545
+ chunk_count INTEGER NOT NULL,
546
+ metadata_json TEXT,
547
+ created_at INTEGER NOT NULL
548
+ );
549
+ CREATE INDEX IF NOT EXISTS idx_kb_document_path ON kb_document_chunks(relative_path);
550
+
433
551
  CREATE TABLE IF NOT EXISTS kb_file_hashes (
434
552
  content_hash TEXT NOT NULL,
435
553
  file_path TEXT PRIMARY KEY,
@@ -481,6 +599,19 @@ export class IndexStateStore {
481
599
  );
482
600
  CREATE INDEX IF NOT EXISTS idx_tags_tag ON kb_tags(tag);
483
601
 
602
+ CREATE TABLE IF NOT EXISTS kb_index_jobs (
603
+ id TEXT PRIMARY KEY,
604
+ relative_path TEXT NOT NULL,
605
+ status TEXT NOT NULL,
606
+ percent INTEGER NOT NULL DEFAULT 0,
607
+ message TEXT NOT NULL DEFAULT '',
608
+ error_message TEXT,
609
+ created_at INTEGER NOT NULL,
610
+ updated_at INTEGER NOT NULL
611
+ );
612
+ CREATE INDEX IF NOT EXISTS idx_kb_jobs_status ON kb_index_jobs(status);
613
+ CREATE INDEX IF NOT EXISTS idx_kb_jobs_path ON kb_index_jobs(relative_path);
614
+
484
615
  CREATE TABLE IF NOT EXISTS kb_metadata (
485
616
  key TEXT PRIMARY KEY,
486
617
  value TEXT NOT NULL
@@ -575,7 +706,7 @@ export class IndexStateStore {
575
706
  relativePath: String(row.relative_path),
576
707
  parentId: String(row.parent_id),
577
708
  content: String(row.content),
578
- category: String(row.category),
709
+ category: row.category,
579
710
  format: String(row.format),
580
711
  collectionName: String(row.collection_name),
581
712
  sectionTitle: row.section_title == null ? undefined : String(row.section_title),
@@ -584,45 +715,41 @@ export class IndexStateStore {
584
715
  createdAt: Number(row.created_at),
585
716
  };
586
717
  }
587
- splitParentGroups(relativePath, chunks) {
588
- const maxChildrenPerParent = 12;
589
- const maxTokensPerParent = 4_000;
590
- const grouped = new Map();
591
- for (const chunk of chunks) {
592
- const parentId = this.metadataString(chunk.metadata.parentId) ?? `${relativePath}#parent-${chunk.index}`;
593
- const list = grouped.get(parentId) ?? [];
594
- list.push(chunk);
595
- grouped.set(parentId, list);
596
- }
597
- const result = [];
598
- for (const [parentId, list] of grouped.entries()) {
599
- let batch = [];
600
- let tokenCount = 0;
601
- let batchIndex = 0;
602
- const flush = () => {
603
- if (batch.length === 0)
604
- return;
605
- const nextParentId = list.length <= maxChildrenPerParent && tokenCount <= maxTokensPerParent ? parentId : `${parentId}@${batchIndex + 1}`;
606
- result.push(...batch.map(chunk => ({ ...chunk, metadata: { ...chunk.metadata, parentId: nextParentId, parentGroupIndex: batchIndex } })));
607
- batch = [];
608
- tokenCount = 0;
609
- batchIndex += 1;
610
- };
611
- for (const chunk of list) {
612
- if (batch.length > 0 && (batch.length >= maxChildrenPerParent || tokenCount + chunk.tokenCount > maxTokensPerParent))
613
- flush();
614
- batch.push(chunk);
615
- tokenCount += chunk.tokenCount;
616
- }
617
- flush();
618
- }
619
- return result.sort((a, b) => a.index - b.index);
718
+ rowToDocumentChunk(row) {
719
+ return {
720
+ id: String(row.id),
721
+ relativePath: String(row.relative_path),
722
+ content: String(row.content),
723
+ category: row.category,
724
+ format: String(row.format),
725
+ collectionName: String(row.collection_name),
726
+ parentCount: Number(row.parent_count),
727
+ chunkCount: Number(row.chunk_count),
728
+ metadataJson: row.metadata_json == null ? undefined : String(row.metadata_json),
729
+ createdAt: Number(row.created_at),
730
+ };
731
+ }
732
+ splitParentGroups(_relativePath, chunks) {
733
+ return chunks;
734
+ }
735
+ rowToJob(row) {
736
+ return {
737
+ id: String(row.id),
738
+ relativePath: String(row.relative_path),
739
+ status: String(row.status),
740
+ percent: Number(row.percent),
741
+ message: String(row.message ?? ''),
742
+ errorMessage: row.error_message == null ? undefined : String(row.error_message),
743
+ createdAt: Number(row.created_at),
744
+ updatedAt: Number(row.updated_at),
745
+ };
620
746
  }
621
747
  metadataString(value) {
622
748
  return typeof value === 'string' ? value : undefined;
623
749
  }
624
750
  rowToChunk(row, score, scoreDetails) {
625
751
  return {
752
+ rowid: Number(row.rowid ?? 0),
626
753
  id: String(row.id),
627
754
  relativePath: String(row.relative_path),
628
755
  chunkIndex: Number(row.chunk_index),
@@ -641,8 +768,13 @@ export class IndexStateStore {
641
768
  expandSearchTerms(query) {
642
769
  const normalized = query.toLowerCase().trim();
643
770
  const terms = new Set(normalized ? [normalized] : []);
644
- for (const term of normalized.split(/[\s,,。;;::、]+/u).filter(Boolean))
771
+ for (const term of normalized.split(/[\s,,。;;::、]+/u).filter(Boolean)) {
645
772
  terms.add(term);
773
+ for (const gram of this.chineseNgrams(term))
774
+ terms.add(gram);
775
+ }
776
+ for (const gram of this.chineseNgrams(normalized))
777
+ terms.add(gram);
646
778
  const synonyms = {
647
779
  招标: ['招标', '投标', '标书', '招标文件', '投标文件', 'bid', 'tender', 'bidding'],
648
780
  投标: ['招标', '投标', '标书', '招标文件', '投标文件', 'bid', 'tender', 'bidding'],
@@ -656,7 +788,7 @@ export class IndexStateStore {
656
788
  for (const value of values)
657
789
  terms.add(value.toLowerCase());
658
790
  }
659
- return [...terms];
791
+ return [...terms].filter(term => term.length > 0).slice(0, 40);
660
792
  }
661
793
  toFtsQuery(terms) {
662
794
  const normalized = terms.map(term => term.replace(/["*^:(){}\]\\[]/gu, ' ').trim()).filter(term => term.length > 0);
@@ -664,10 +796,32 @@ export class IndexStateStore {
664
796
  const weak = normalized.slice(1).filter(term => term.length >= 2).slice(0, 12);
665
797
  return [exact ? `"${exact}"` : '', ...weak.map(term => `"${term}"`)].filter(Boolean).join(' OR ');
666
798
  }
799
+ mergeKeywordResults(results, limit) {
800
+ const byId = new Map();
801
+ for (const result of results) {
802
+ const existing = byId.get(result.id);
803
+ if (!existing || result.score > existing.score)
804
+ byId.set(result.id, result);
805
+ }
806
+ return [...byId.values()].sort((a, b) => b.score - a.score).slice(0, limit);
807
+ }
808
+ chineseNgrams(text) {
809
+ const han = text.match(/[\p{Script=Han}]+/gu) ?? [];
810
+ const grams = [];
811
+ for (const token of han) {
812
+ for (let size = 2; size <= 3; size++) {
813
+ if (token.length <= size)
814
+ continue;
815
+ for (let index = 0; index <= token.length - size; index++)
816
+ grams.push(token.slice(index, index + size));
817
+ }
818
+ }
819
+ return grams;
820
+ }
667
821
  bm25ToPositiveScore(score) {
668
822
  if (!Number.isFinite(score))
669
823
  return 0;
670
- return 1 / (1 + Math.max(0, score));
824
+ return score < 0 ? -score : 1 / (1 + score);
671
825
  }
672
826
  scoreChunkDetailed(content, terms) {
673
827
  const lower = content.toLowerCase();
@@ -34,7 +34,7 @@ export declare class KnowledgeBaseManager {
34
34
  readonly projectId?: string;
35
35
  readonly kbPath: string;
36
36
  readonly store: IndexStateStore;
37
- private readonly sqliteVecClient;
37
+ private readonly vectorRoot;
38
38
  private readonly classifier;
39
39
  private readonly scanner;
40
40
  private readonly collections;
@@ -51,9 +51,19 @@ export declare class KnowledgeBaseManager {
51
51
  private onProgress?;
52
52
  constructor(options: KnowledgeBaseManagerOptions);
53
53
  initialize(): void;
54
+ forceReindexAll(options?: {
55
+ onProgress?: (progress: KnowledgeIndexProgress) => void;
56
+ vectorMode?: 'sync' | 'defer';
57
+ }): Promise<DiffResult>;
58
+ consumePendingIndexJobs(options?: {
59
+ onProgress?: (progress: KnowledgeIndexProgress) => void;
60
+ vectorMode?: 'sync' | 'defer';
61
+ limit?: number;
62
+ }): Promise<DiffResult>;
54
63
  incrementalIndex(options?: {
55
64
  onProgress?: (progress: KnowledgeIndexProgress) => void;
56
65
  vectorMode?: 'sync' | 'defer';
66
+ onlyRelativePaths?: string[];
57
67
  }): Promise<DiffResult>;
58
68
  search(query: string, limit?: number): ChunkSearchResult[];
59
69
  keywordSearchItems(query: string, limit?: number): FederatedSearchItem[];
@@ -90,6 +100,11 @@ export declare class KnowledgeBaseManager {
90
100
  uploadFile(fileName: string, content: Buffer, targetRelativePath?: string, onProgress?: (progress: KnowledgeIndexProgress) => void, options?: {
91
101
  vectorMode?: 'sync' | 'defer';
92
102
  }): Promise<DiffResult>;
103
+ stageUploadedFiles(files: Array<{
104
+ fileName: string;
105
+ content: Buffer;
106
+ targetRelativePath?: string;
107
+ }>, operationId?: string): Promise<import("./index-state-store.js").KnowledgeIndexJob[]>;
93
108
  uploadFiles(files: Array<{
94
109
  fileName: string;
95
110
  content: Buffer;
@@ -119,6 +134,9 @@ export declare class KnowledgeBaseManager {
119
134
  }): Promise<VectorIndexResult[]>;
120
135
  getProjectConfig(): ProjectConfig | undefined;
121
136
  getStats(): KnowledgeBaseStats;
137
+ listIndexJobsByPrefix(prefix: string): import("./index-state-store.js").KnowledgeIndexJob[];
138
+ countPendingIndexJobs(): number;
139
+ failPendingIndexJobs(message: string): void;
122
140
  getVectorStatus(): {
123
141
  status: string;
124
142
  error?: string;
@@ -131,8 +149,11 @@ export declare class KnowledgeBaseManager {
131
149
  private retrievalWeights;
132
150
  private heuristicRerank;
133
151
  private llmRerank;
152
+ private hydrateVectorResultsFromSqlite;
134
153
  private toFederatedItem;
135
- private mergeHybridItems;
154
+ private mergeHybridRankedLists;
155
+ private mergeContexts;
156
+ private contextKey;
136
157
  private parseChunkIndex;
137
158
  private parseMetadataString;
138
159
  private parseMetadata;
@@ -140,6 +161,7 @@ export declare class KnowledgeBaseManager {
140
161
  private metadataFacets;
141
162
  close(): void;
142
163
  private reportProgress;
164
+ private updateJobsForFile;
143
165
  private ensureVectorStore;
144
166
  private deleteVectorFile;
145
167
  private ensureVectorIndexFresh;