@customize-agent/knowledge 4.0.29 → 4.0.31
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/core/index-state-store.d.ts +4 -1
- package/dist/core/index-state-store.js +13 -9
- package/dist/core/knowledge-base-manager.d.ts +2 -2
- package/dist/core/knowledge-base-manager.js +10 -13
- package/dist/core/multi-project-manager.d.ts +1 -3
- package/dist/core/multi-project-manager.js +4 -19
- package/dist/search/federation-search.d.ts +1 -0
- package/dist/search/federation-search.js +4 -1
- package/dist/vector/hnsw-vector-store.js +4 -1
- package/dist/vector/types.d.ts +2 -1
- package/package.json +2 -2
- package/scripts/install-hnsw.cjs +37 -0
|
@@ -148,7 +148,10 @@ export declare class IndexStateStore {
|
|
|
148
148
|
* @param limit 返回结果数量上限
|
|
149
149
|
* @returns 搜索结果列表(按相关性得分排序)
|
|
150
150
|
*/
|
|
151
|
-
searchChunks(query: string, limit?: number
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
|
@@ -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 (
|
|
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]) =>
|
|
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);
|
package/dist/vector/types.d.ts
CHANGED
|
@@ -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,
|
|
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.
|
|
3
|
+
"version": "4.0.31",
|
|
4
4
|
"description": "Local knowledge base infrastructure for customize-agent",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
},
|
|
15
15
|
"license": "MIT",
|
|
16
16
|
"engines": {
|
|
17
|
-
"node": ">=
|
|
17
|
+
"node": ">=20.19.0"
|
|
18
18
|
},
|
|
19
19
|
"author": "Pan-jijian",
|
|
20
20
|
"repository": {
|
package/scripts/install-hnsw.cjs
CHANGED
|
@@ -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 编译工具链。');
|