@customize-agent/knowledge 4.0.29 → 4.0.30

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.
@@ -148,7 +148,10 @@ export declare class IndexStateStore {
148
148
  * @param limit 返回结果数量上限
149
149
  * @returns 搜索结果列表(按相关性得分排序)
150
150
  */
151
- searchChunks(query: string, limit?: number): ChunkSearchResult[];
151
+ searchChunks(query: string, limit?: number, filters?: {
152
+ filePaths?: string[];
153
+ }): ChunkSearchResult[];
154
+ private filePathFilterClause;
152
155
  private searchChunksFts;
153
156
  private searchChunksLike;
154
157
  findExactDuplicate(contentHash: string, excludePath?: string): FileHashRecord | undefined;
@@ -264,17 +264,21 @@ export class IndexStateStore {
264
264
  * @param limit 返回结果数量上限
265
265
  * @returns 搜索结果列表(按相关性得分排序)
266
266
  */
267
- searchChunks(query, limit = 10) {
267
+ searchChunks(query, limit = 10, filters = {}) {
268
268
  const terms = this.expandSearchTerms(query);
269
269
  if (terms.length === 0)
270
270
  return [];
271
+ const filePaths = [...new Set((filters.filePaths ?? []).filter(Boolean))];
271
272
  const results = [
272
- ...(this.ftsEnabled ? this.searchChunksFts(terms, limit) : []),
273
- ...this.searchChunksLike(terms, limit),
273
+ ...(this.ftsEnabled ? this.searchChunksFts(terms, limit, filePaths) : []),
274
+ ...this.searchChunksLike(terms, limit, filePaths),
274
275
  ];
275
276
  return this.mergeKeywordResults(results, limit);
276
277
  }
277
- searchChunksFts(terms, limit) {
278
+ filePathFilterClause(filePaths, column = 'relative_path') {
279
+ return filePaths.length > 0 ? ` AND ${column} IN (${filePaths.map(() => '?').join(', ')})` : '';
280
+ }
281
+ searchChunksFts(terms, limit, filePaths) {
278
282
  try {
279
283
  const matchQuery = this.toFtsQuery(terms);
280
284
  if (!matchQuery)
@@ -283,10 +287,10 @@ export class IndexStateStore {
283
287
  SELECT c.rowid, c.*, bm25(kb_chunks_fts, 1.2, 0.8, 0.6, 1.0, 2.0) as bm25_score
284
288
  FROM kb_chunks_fts
285
289
  INNER JOIN kb_chunks c ON c.id = kb_chunks_fts.id
286
- WHERE kb_chunks_fts MATCH ?
290
+ WHERE kb_chunks_fts MATCH ?${this.filePathFilterClause(filePaths, 'c.relative_path')}
287
291
  ORDER BY bm25_score ASC
288
292
  LIMIT ?
289
- `).all(matchQuery, limit * 8);
293
+ `).all(matchQuery, ...filePaths, limit * 8);
290
294
  return rows
291
295
  .map(row => {
292
296
  const keyword = this.scoreChunkDetailed(this.searchableRowText(row), terms);
@@ -301,13 +305,13 @@ export class IndexStateStore {
301
305
  return [];
302
306
  }
303
307
  }
304
- searchChunksLike(terms, limit) {
308
+ searchChunksLike(terms, limit, filePaths) {
305
309
  const rows = this.db.prepare(`
306
310
  SELECT rowid, * FROM kb_chunks
307
- WHERE ${terms.map(() => '(LOWER(search_content) LIKE ? OR LOWER(content) LIKE ? OR LOWER(relative_path) LIKE ? OR LOWER(category) LIKE ? OR LOWER(format) LIKE ? OR LOWER(COALESCE(title_path, \'\')) LIKE ? OR LOWER(COALESCE(chunk_kind, \'\')) LIKE ?)').join(' OR ')}
311
+ WHERE (${terms.map(() => '(LOWER(search_content) LIKE ? OR LOWER(content) LIKE ? OR LOWER(relative_path) LIKE ? OR LOWER(category) LIKE ? OR LOWER(format) LIKE ? OR LOWER(COALESCE(title_path, \'\')) LIKE ? OR LOWER(COALESCE(chunk_kind, \'\')) LIKE ?)').join(' OR ')})${this.filePathFilterClause(filePaths)}
308
312
  ORDER BY created_at DESC
309
313
  LIMIT ?
310
- `).all(...terms.flatMap(term => [`%${term}%`, `%${term}%`, `%${term}%`, `%${term}%`, `%${term}%`, `%${term}%`, `%${term}%`]), limit * 6);
314
+ `).all(...terms.flatMap(term => [`%${term}%`, `%${term}%`, `%${term}%`, `%${term}%`, `%${term}%`, `%${term}%`, `%${term}%`]), ...filePaths, limit * 6);
311
315
  return rows
312
316
  .map(row => {
313
317
  const keyword = this.scoreChunkDetailed(this.searchableRowText(row), terms);
@@ -64,8 +64,8 @@ export declare class KnowledgeBaseManager {
64
64
  vectorMode?: 'sync' | 'defer';
65
65
  onlyRelativePaths?: string[];
66
66
  }): Promise<DiffResult>;
67
- search(query: string, limit?: number): ChunkSearchResult[];
68
- keywordSearchItems(query: string, limit?: number): FederatedSearchItem[];
67
+ search(query: string, limit?: number, filters?: SearchFilters): ChunkSearchResult[];
68
+ keywordSearchItems(query: string, limit?: number, filters?: SearchFilters): FederatedSearchItem[];
69
69
  expandContext(item: FederatedSearchItem): FederatedSearchItem;
70
70
  hybridSearch(query: string, options?: {
71
71
  limit?: number;
@@ -89,11 +89,8 @@ export class KnowledgeBaseManager {
89
89
  async consumePendingIndexJobs(options = {}) {
90
90
  this.initialize();
91
91
  const jobs = this.store.listPendingIndexJobs(options.limit ?? 500);
92
- if (jobs.length === 0) {
93
- if (options.waitForUploadId && this.uploadSessionIsOpen(options.waitForUploadId))
94
- return this.emptyDiff();
95
- return this.incrementalIndex(options);
96
- }
92
+ if (jobs.length === 0)
93
+ return this.emptyDiff();
97
94
  return this.incrementalIndex({ ...options, onlyRelativePaths: jobs.map(job => job.relativePath) });
98
95
  }
99
96
  async incrementalIndex(options = {}) {
@@ -301,11 +298,11 @@ export class KnowledgeBaseManager {
301
298
  this.onProgress = previousOnProgress;
302
299
  }
303
300
  }
304
- search(query, limit = 10) {
305
- return this.store.searchChunks(query, limit);
301
+ search(query, limit = 10, filters) {
302
+ return this.store.searchChunks(query, limit, { filePaths: filters?.filePaths ?? (filters?.filePath ? [filters.filePath] : undefined) });
306
303
  }
307
- keywordSearchItems(query, limit = 10) {
308
- return this.store.searchChunks(query, limit).map(result => this.toFederatedItem(result, 'keyword'));
304
+ keywordSearchItems(query, limit = 10, filters) {
305
+ return this.store.searchChunks(query, limit, { filePaths: filters?.filePaths ?? (filters?.filePath ? [filters.filePath] : undefined) }).map(result => this.toFederatedItem(result, 'keyword'));
309
306
  }
310
307
  expandContext(item) {
311
308
  const chunkIndex = item.chunkIndex ?? this.parseChunkIndex(item.id);
@@ -345,7 +342,7 @@ export class KnowledgeBaseManager {
345
342
  const vectorMultiplier = options.generationMode ? 3 : 6;
346
343
  const vectorQueryLimit = options.generationMode ? 1 : 3;
347
344
  for (const [queryIndex, rewritten] of rewrittenQueries.entries()) {
348
- rankedLists.push({ source: 'keyword', items: this.keywordSearchItems(rewritten, limit * keywordMultiplier), queryIndex });
345
+ rankedLists.push({ source: 'keyword', items: this.keywordSearchItems(rewritten, limit * keywordMultiplier, options.filters), queryIndex });
349
346
  if (queryIndex < vectorQueryLimit) {
350
347
  try {
351
348
  rankedLists.push({ source: 'vector', items: (await this.semanticSearch(rewritten, { ...options, limit: limit * vectorMultiplier })).results.slice(0, limit * keywordMultiplier), queryIndex });
@@ -460,7 +457,7 @@ export class KnowledgeBaseManager {
460
457
  const targetPath = this.resolveKbRelativePath(relativePath);
461
458
  fs.mkdirSync(path.dirname(targetPath), { recursive: true });
462
459
  fs.copyFileSync(resolvedSource, targetPath);
463
- return this.incrementalIndex();
460
+ return this.incrementalIndex({ onlyRelativePaths: [relativePath] });
464
461
  }
465
462
  getUploadRelativePath(fileName, targetRelativePath) {
466
463
  return this.validateUploadRelativePath(targetRelativePath ? this.normalizeRelativePath(targetRelativePath) : this.defaultUploadRelativePath(fileName));
@@ -504,8 +501,8 @@ export class KnowledgeBaseManager {
504
501
  return jobs;
505
502
  }
506
503
  async uploadFiles(files, onProgress, options = {}) {
507
- await this.stageUploadedFiles(files);
508
- return this.incrementalIndex({ onProgress, vectorMode: options.vectorMode });
504
+ const jobs = await this.stageUploadedFiles(files);
505
+ return this.incrementalIndex({ onProgress, vectorMode: options.vectorMode, onlyRelativePaths: jobs.map(job => job.relativePath) });
509
506
  }
510
507
  listFailedFiles() {
511
508
  return this.lastSkippedFiles;
@@ -9,7 +9,6 @@ export declare class MultiProjectManager {
9
9
  private readonly registry;
10
10
  private readonly configManager;
11
11
  private readonly projects;
12
- private readonly lastSearchIndexCheck;
13
12
  private globalKB?;
14
13
  constructor(storageRoot?: string, llmProvider?: LLMSearchProvider);
15
14
  /**
@@ -25,9 +24,9 @@ export declare class MultiProjectManager {
25
24
  search(projectRoot: string, query: string, options?: {
26
25
  limit?: number;
27
26
  scope?: SearchScope;
27
+ filters?: SearchFilters;
28
28
  weights?: RetrievalWeights;
29
29
  generationMode?: boolean;
30
- skipFreshCheck?: boolean;
31
30
  }): Promise<FederatedResult>;
32
31
  semanticSearch(projectRoot: string, query: string, options?: {
33
32
  limit?: number;
@@ -39,7 +38,6 @@ export declare class MultiProjectManager {
39
38
  forgetProject(projectId: string): Promise<void>;
40
39
  closeProject(projectId: string): Promise<void>;
41
40
  shutdown(): Promise<void>;
42
- private ensureFreshForSearch;
43
41
  private mergeDebug;
44
42
  private updateRegistry;
45
43
  }
@@ -13,7 +13,6 @@ export class MultiProjectManager {
13
13
  registry;
14
14
  configManager;
15
15
  projects = new Map();
16
- lastSearchIndexCheck = new Map();
17
16
  globalKB;
18
17
  constructor(storageRoot = path.join(os.homedir(), USER_DATA_DIR), llmProvider) {
19
18
  this.storageRoot = storageRoot;
@@ -53,7 +52,6 @@ export class MultiProjectManager {
53
52
  return this.globalKB;
54
53
  const manager = new KnowledgeBaseManager({ scope: 'global', storageRoot: this.storageRoot, llmProvider: this.llmProvider });
55
54
  manager.initialize();
56
- await manager.incrementalIndex();
57
55
  this.globalKB = manager;
58
56
  return manager;
59
57
  }
@@ -64,19 +62,17 @@ export class MultiProjectManager {
64
62
  const limit = options.limit ?? 10;
65
63
  const scope = options.scope ?? 'project';
66
64
  const project = await this.getProject(projectRoot);
67
- if (!options.skipFreshCheck)
68
- await this.ensureFreshForSearch(projectRoot, project);
69
65
  if (scope === 'project')
70
- return project.hybridSearch(query, { limit, weights: options.weights, generationMode: options.generationMode });
66
+ return project.hybridSearch(query, { limit, filters: options.filters, weights: options.weights, generationMode: options.generationMode });
71
67
  const projectResults = scope === 'all'
72
- ? await project.hybridSearch(query, { limit, weights: options.weights, generationMode: options.generationMode })
68
+ ? await project.hybridSearch(query, { limit, filters: options.filters, weights: options.weights, generationMode: options.generationMode })
73
69
  : { results: [], scopesSearched: [], queryTimeMs: 0 };
74
70
  if (scope === 'global') {
75
71
  const global = await this.getGlobalKB();
76
- return global.hybridSearch(query, { limit, weights: options.weights, generationMode: options.generationMode });
72
+ return global.hybridSearch(query, { limit, filters: options.filters, weights: options.weights, generationMode: options.generationMode });
77
73
  }
78
74
  const global = await this.getGlobalKB();
79
- const globalResults = await global.hybridSearch(query, { limit, weights: options.weights, generationMode: options.generationMode });
75
+ const globalResults = await global.hybridSearch(query, { limit, filters: options.filters, weights: options.weights, generationMode: options.generationMode });
80
76
  const merged = new FederationSearch().merge([...projectResults.results, ...globalResults.results], limit, 'all');
81
77
  return {
82
78
  ...merged,
@@ -86,7 +82,6 @@ export class MultiProjectManager {
86
82
  async semanticSearch(projectRoot, query, options = {}) {
87
83
  const scope = options.scope ?? 'project';
88
84
  const project = await this.getProject(projectRoot);
89
- await this.ensureFreshForSearch(projectRoot, project);
90
85
  if (scope === 'project') {
91
86
  return project.semanticSearch(query, options);
92
87
  }
@@ -151,16 +146,6 @@ export class MultiProjectManager {
151
146
  this.globalKB = undefined;
152
147
  this.registry.close();
153
148
  }
154
- async ensureFreshForSearch(projectRoot, project) {
155
- const now = Date.now();
156
- const key = path.resolve(projectRoot);
157
- const ttlMs = Number(process.env.KB_SEARCH_INDEX_TTL_MS ?? 30000);
158
- const last = this.lastSearchIndexCheck.get(key) ?? 0;
159
- if (now - last < ttlMs)
160
- return;
161
- this.lastSearchIndexCheck.set(key, now);
162
- await project.incrementalIndex({ vectorMode: 'defer' });
163
- }
164
149
  mergeDebug(projectDebug, globalDebug) {
165
150
  if (!projectDebug && !globalDebug)
166
151
  return undefined;
@@ -48,6 +48,7 @@ export interface FederatedResult {
48
48
  export interface SearchFilters {
49
49
  category?: string;
50
50
  filePath?: string;
51
+ filePaths?: string[];
51
52
  }
52
53
  /** 各检索方式权重配置 */
53
54
  export interface RetrievalWeights {
@@ -65,9 +65,12 @@ export class FederationSearch {
65
65
  if (!filters)
66
66
  return undefined;
67
67
  const where = {};
68
+ const filePaths = filters.filePaths?.filter(Boolean);
68
69
  if (filters.category)
69
70
  where.category = filters.category;
70
- if (filters.filePath)
71
+ if (filePaths?.length)
72
+ where.file_path = filePaths;
73
+ else if (filters.filePath)
71
74
  where.file_path = filters.filePath;
72
75
  return Object.keys(where).length > 0 ? where : undefined;
73
76
  }
@@ -96,7 +96,10 @@ export class HNSWVectorStore {
96
96
  matchesWhere(document, where) {
97
97
  if (!where)
98
98
  return true;
99
- return Object.entries(where).every(([key, value]) => document.metadata[key] === value);
99
+ return Object.entries(where).every(([key, value]) => {
100
+ const actual = document.metadata[key];
101
+ return Array.isArray(value) ? value.includes(actual) : actual === value;
102
+ });
100
103
  }
101
104
  persist() {
102
105
  this.index.writeIndexSync(this.indexPath);
@@ -6,10 +6,11 @@ export interface VectorDocument {
6
6
  metadata: Record<string, string | number | boolean | null>;
7
7
  }
8
8
  /** 向量搜索查询参数 */
9
+ export type VectorFilterValue = string | number | boolean | Array<string | number | boolean>;
9
10
  export interface VectorSearchQuery {
10
11
  queryEmbedding: number[];
11
12
  topK: number;
12
- where?: Record<string, string | number | boolean>;
13
+ where?: Record<string, VectorFilterValue>;
13
14
  }
14
15
  /** 向量搜索结果 */
15
16
  export interface VectorSearchResult {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@customize-agent/knowledge",
3
- "version": "4.0.29",
3
+ "version": "4.0.30",
4
4
  "description": "Local knowledge base infrastructure for customize-agent",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -13,6 +13,33 @@ function run(command, args, cwd) {
13
13
  if (result.status !== 0) process.exit(result.status || 1);
14
14
  }
15
15
 
16
+ function markerPath(hnswDir) {
17
+ return path.join(hnswDir, 'build', '.customize-agent-hnsw-ok');
18
+ }
19
+
20
+ function findNativeBinding(hnswDir) {
21
+ const candidates = [
22
+ path.join(hnswDir, 'build', 'Release', 'addon.node'),
23
+ path.join(hnswDir, 'prebuilds', `${process.platform}-${process.arch}`, 'node.napi.node'),
24
+ ];
25
+ return candidates.find(candidate => fs.existsSync(candidate));
26
+ }
27
+
28
+ function isMarkerFresh(hnswDir) {
29
+ const marker = markerPath(hnswDir);
30
+ const binding = findNativeBinding(hnswDir);
31
+ if (!fs.existsSync(marker) || !binding) return false;
32
+ const markerStat = fs.statSync(marker);
33
+ const bindingStat = fs.statSync(binding);
34
+ return markerStat.mtimeMs >= bindingStat.mtimeMs;
35
+ }
36
+
37
+ function writeMarker(hnswDir) {
38
+ const marker = markerPath(hnswDir);
39
+ fs.mkdirSync(path.dirname(marker), { recursive: true });
40
+ fs.writeFileSync(marker, JSON.stringify({ node: process.version, platform: process.platform, arch: process.arch, updatedAt: Date.now() }));
41
+ }
42
+
16
43
  function verify(hnswDir) {
17
44
  const hnsw = require(path.join(hnswDir, 'lib/index.js'));
18
45
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'hnsw-doctor-'));
@@ -35,10 +62,20 @@ function verify(hnswDir) {
35
62
 
36
63
  try {
37
64
  const hnswDir = packageDir('hnswlib-node');
65
+ if (isMarkerFresh(hnswDir)) {
66
+ try {
67
+ verify(hnswDir);
68
+ console.log('[hnsw] hnswlib-node 已可用,跳过 native rebuild');
69
+ process.exit(0);
70
+ } catch {
71
+ // 标记存在但运行验证失败,继续 rebuild。
72
+ }
73
+ }
38
74
  const nodeGypBin = require.resolve('node-gyp/bin/node-gyp.js', { paths: [process.cwd(), __dirname] });
39
75
  console.log(`[hnsw] 构建 hnswlib-node native binding: ${hnswDir}`);
40
76
  run(process.execPath, [nodeGypBin, 'rebuild'], hnswDir);
41
77
  verify(hnswDir);
78
+ writeMarker(hnswDir);
42
79
  console.log('[hnsw] hnswlib-node 安装和运行验证通过');
43
80
  } catch (error) {
44
81
  console.error('[hnsw] hnswlib-node 安装或运行验证失败。请确认当前平台已安装 native 编译工具链。');