@ppdocs/mcp 2.6.30 → 2.7.1
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/storage/httpClient.d.ts +34 -0
- package/dist/storage/httpClient.js +18 -0
- package/dist/tools/helpers.d.ts +0 -27
- package/dist/tools/helpers.js +0 -56
- package/dist/tools/index.js +51 -87
- package/dist/vector/manager.d.ts +14 -5
- package/dist/vector/manager.js +89 -5
- package/package.json +1 -2
|
@@ -54,6 +54,20 @@ export declare class PpdocsApiClient {
|
|
|
54
54
|
completeTask(taskId: string, experience: TaskExperience): Promise<Task | null>;
|
|
55
55
|
getRulesApi(ruleType: string): Promise<string[]>;
|
|
56
56
|
saveRulesApi(ruleType: string, rules: string[]): Promise<boolean>;
|
|
57
|
+
vectorSearch(query: string, limit: number, mode: string): Promise<Array<{
|
|
58
|
+
id: string;
|
|
59
|
+
title: string;
|
|
60
|
+
score: number;
|
|
61
|
+
match_type: string;
|
|
62
|
+
keyword_score: number;
|
|
63
|
+
semantic_score: number;
|
|
64
|
+
}>>;
|
|
65
|
+
vectorStats(): Promise<{
|
|
66
|
+
model_downloaded: boolean;
|
|
67
|
+
model_loaded: boolean;
|
|
68
|
+
model_path: string;
|
|
69
|
+
index_count: number;
|
|
70
|
+
}>;
|
|
57
71
|
}
|
|
58
72
|
export declare function initClient(apiUrl: string): void;
|
|
59
73
|
export declare function listNodes(_projectId: string, filter?: ListNodesFilter): Promise<NodeData[]>;
|
|
@@ -96,3 +110,23 @@ export declare function addTaskLog(_projectId: string, taskId: string, logType:
|
|
|
96
110
|
export declare function completeTask(_projectId: string, taskId: string, experience: TaskExperience): Promise<Task | null>;
|
|
97
111
|
export declare function getRules(_projectId: string, ruleType: string): Promise<string[]>;
|
|
98
112
|
export declare function saveRules(_projectId: string, ruleType: string, rules: string[]): Promise<boolean>;
|
|
113
|
+
/** 向量搜索结果 */
|
|
114
|
+
export interface VectorSearchResult {
|
|
115
|
+
id: string;
|
|
116
|
+
title: string;
|
|
117
|
+
score: number;
|
|
118
|
+
match_type: string;
|
|
119
|
+
keyword_score: number;
|
|
120
|
+
semantic_score: number;
|
|
121
|
+
}
|
|
122
|
+
/** 向量索引状态 */
|
|
123
|
+
export interface VectorStats {
|
|
124
|
+
model_downloaded: boolean;
|
|
125
|
+
model_loaded: boolean;
|
|
126
|
+
model_path: string;
|
|
127
|
+
index_count: number;
|
|
128
|
+
}
|
|
129
|
+
/** 混合搜索 (关键词 + 语义) */
|
|
130
|
+
export declare function vectorSearch(_projectId: string, query: string, limit: number, mode: 'hybrid' | 'keyword' | 'semantic'): Promise<VectorSearchResult[]>;
|
|
131
|
+
/** 获取向量索引状态 */
|
|
132
|
+
export declare function vectorStats(_projectId: string): Promise<VectorStats>;
|
|
@@ -360,6 +360,16 @@ export class PpdocsApiClient {
|
|
|
360
360
|
return false;
|
|
361
361
|
}
|
|
362
362
|
}
|
|
363
|
+
// ============ 向量搜索 API ============
|
|
364
|
+
async vectorSearch(query, limit, mode) {
|
|
365
|
+
return this.request('/vector/search', {
|
|
366
|
+
method: 'POST',
|
|
367
|
+
body: JSON.stringify({ query, limit, mode })
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
async vectorStats() {
|
|
371
|
+
return this.request('/vector/stats');
|
|
372
|
+
}
|
|
363
373
|
}
|
|
364
374
|
// ============ 模块级 API (兼容现有 tools/index.ts) ============
|
|
365
375
|
let client = null;
|
|
@@ -429,3 +439,11 @@ export async function getRules(_projectId, ruleType) {
|
|
|
429
439
|
export async function saveRules(_projectId, ruleType, rules) {
|
|
430
440
|
return getClient().saveRulesApi(ruleType, rules);
|
|
431
441
|
}
|
|
442
|
+
/** 混合搜索 (关键词 + 语义) */
|
|
443
|
+
export async function vectorSearch(_projectId, query, limit, mode) {
|
|
444
|
+
return getClient().vectorSearch(query, limit, mode);
|
|
445
|
+
}
|
|
446
|
+
/** 获取向量索引状态 */
|
|
447
|
+
export async function vectorStats(_projectId) {
|
|
448
|
+
return getClient().vectorStats();
|
|
449
|
+
}
|
package/dist/tools/helpers.d.ts
CHANGED
|
@@ -24,33 +24,6 @@ export declare function fetchRelations(projectId: string, nodeId: string, maxDep
|
|
|
24
24
|
outgoing: RelationItem[];
|
|
25
25
|
incoming: RelationItem[];
|
|
26
26
|
}>;
|
|
27
|
-
interface SearchResultItem {
|
|
28
|
-
id: string;
|
|
29
|
-
title: string;
|
|
30
|
-
tags: string[];
|
|
31
|
-
status: string;
|
|
32
|
-
summary: string;
|
|
33
|
-
score: string;
|
|
34
|
-
matchType: 'hybrid' | 'semantic' | 'keyword';
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* 融合关键词和语义搜索结果
|
|
38
|
-
*/
|
|
39
|
-
export declare function mergeSearchResults(keywordResults: Array<{
|
|
40
|
-
id: string;
|
|
41
|
-
title: string;
|
|
42
|
-
tags: string[];
|
|
43
|
-
status: string;
|
|
44
|
-
summary: string;
|
|
45
|
-
score: number;
|
|
46
|
-
}>, semanticResults: Array<{
|
|
47
|
-
id: string;
|
|
48
|
-
title: string;
|
|
49
|
-
tags: string[];
|
|
50
|
-
status: string;
|
|
51
|
-
summary: string;
|
|
52
|
-
similarity: number;
|
|
53
|
-
}>, mode: 'hybrid' | 'keyword' | 'semantic', limit: number): SearchResultItem[];
|
|
54
27
|
/**
|
|
55
28
|
* 格式化节点为 Markdown 输出
|
|
56
29
|
*/
|
package/dist/tools/helpers.js
CHANGED
|
@@ -40,62 +40,6 @@ export async function fetchRelations(projectId, nodeId, maxDepth) {
|
|
|
40
40
|
await recurse(nodeId, 1, 'both');
|
|
41
41
|
return { outgoing, incoming };
|
|
42
42
|
}
|
|
43
|
-
/**
|
|
44
|
-
* 融合关键词和语义搜索结果
|
|
45
|
-
*/
|
|
46
|
-
export function mergeSearchResults(keywordResults, semanticResults, mode, limit) {
|
|
47
|
-
const scoreMap = new Map();
|
|
48
|
-
// 合并关键词结果
|
|
49
|
-
for (const r of keywordResults) {
|
|
50
|
-
scoreMap.set(r.id, {
|
|
51
|
-
id: r.id,
|
|
52
|
-
title: r.title,
|
|
53
|
-
tags: r.tags,
|
|
54
|
-
status: r.status,
|
|
55
|
-
summary: r.summary,
|
|
56
|
-
keywordScore: r.score,
|
|
57
|
-
semanticScore: 0
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
// 合并语义结果
|
|
61
|
-
for (const r of semanticResults) {
|
|
62
|
-
const existing = scoreMap.get(r.id);
|
|
63
|
-
if (existing) {
|
|
64
|
-
existing.semanticScore = r.similarity;
|
|
65
|
-
}
|
|
66
|
-
else {
|
|
67
|
-
scoreMap.set(r.id, {
|
|
68
|
-
id: r.id,
|
|
69
|
-
title: r.title,
|
|
70
|
-
tags: r.tags,
|
|
71
|
-
status: r.status,
|
|
72
|
-
summary: r.summary,
|
|
73
|
-
keywordScore: 0,
|
|
74
|
-
semanticScore: r.similarity
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
// 计算综合得分并排序
|
|
79
|
-
const combined = Array.from(scoreMap.values()).map(item => {
|
|
80
|
-
// 混合权重: 语义 0.6 + 关键词 0.4
|
|
81
|
-
const finalScore = mode === 'keyword' ? item.keywordScore
|
|
82
|
-
: mode === 'semantic' ? item.semanticScore
|
|
83
|
-
: item.semanticScore * 0.6 + item.keywordScore * 0.4;
|
|
84
|
-
return { ...item, finalScore };
|
|
85
|
-
});
|
|
86
|
-
combined.sort((a, b) => b.finalScore - a.finalScore);
|
|
87
|
-
const topResults = combined.slice(0, limit);
|
|
88
|
-
return topResults.map(r => ({
|
|
89
|
-
id: r.id,
|
|
90
|
-
title: r.title,
|
|
91
|
-
tags: r.tags,
|
|
92
|
-
status: r.status,
|
|
93
|
-
summary: r.summary,
|
|
94
|
-
score: `${Math.round(r.finalScore * 100)}%`,
|
|
95
|
-
matchType: (r.keywordScore > 0 && r.semanticScore > 0 ? 'hybrid'
|
|
96
|
-
: r.semanticScore > 0 ? 'semantic' : 'keyword')
|
|
97
|
-
}));
|
|
98
|
-
}
|
|
99
43
|
// ==================== 节点格式化 ====================
|
|
100
44
|
/**
|
|
101
45
|
* 格式化节点为 Markdown 输出
|
package/dist/tools/index.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import * as storage from '../storage/httpClient.js';
|
|
3
3
|
import { decodeUnicodeEscapes, decodeObjectStrings, getRules, RULE_TYPE_LABELS } from '../utils.js';
|
|
4
|
-
|
|
5
|
-
import
|
|
6
|
-
import { wrap, fetchRelations, mergeSearchResults, formatNodeMarkdown, formatRelationsMarkdown, buildDirectoryTree, formatTreeText, countTreeNodes } from './helpers.js';
|
|
4
|
+
// 向量搜索已迁移至 Tauri 后端,通过 HTTP API 调用
|
|
5
|
+
import { wrap, fetchRelations, formatNodeMarkdown, formatRelationsMarkdown, buildDirectoryTree, formatTreeText, countTreeNodes } from './helpers.js';
|
|
7
6
|
export function registerTools(server, projectId, _user) {
|
|
8
7
|
// 1. 创建节点
|
|
9
8
|
server.tool('kg_create_node', '创建知识节点。type: logic=逻辑/函数, data=数据结构, intro=概念介绍。⚠️ tags至少提供3个分类标签', {
|
|
@@ -35,27 +34,13 @@ export function registerTools(server, projectId, _user) {
|
|
|
35
34
|
dependencies: decoded.dependencies || [],
|
|
36
35
|
relatedFiles: decoded.relatedFiles || []
|
|
37
36
|
});
|
|
38
|
-
//
|
|
39
|
-
try {
|
|
40
|
-
await vector.upsertNode(projectId, node.id, node.title, node.description, node.categories);
|
|
41
|
-
}
|
|
42
|
-
catch (e) {
|
|
43
|
-
console.warn('[kg_create_node] Vector index update failed:', e);
|
|
44
|
-
}
|
|
37
|
+
// 向量索引由 Tauri 后端自动维护,MCP 不再手动更新
|
|
45
38
|
return wrap(JSON.stringify(node, null, 2));
|
|
46
39
|
});
|
|
47
40
|
// 2. 删除节点
|
|
48
41
|
server.tool('kg_delete_node', '删除节点(锁定节点和根节点不可删除)', { nodeId: z.string().describe('节点ID') }, async (args) => {
|
|
49
42
|
const success = await storage.deleteNode(projectId, args.nodeId);
|
|
50
|
-
//
|
|
51
|
-
if (success) {
|
|
52
|
-
try {
|
|
53
|
-
await vector.removeNode(projectId, args.nodeId);
|
|
54
|
-
}
|
|
55
|
-
catch (e) {
|
|
56
|
-
console.warn('[kg_delete_node] Vector index removal failed:', e);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
43
|
+
// 向量索引由 Tauri 后端自动维护,MCP 不再手动删除
|
|
59
44
|
return wrap(success ? '删除成功' : '删除失败(节点不存在/已锁定/是根节点)');
|
|
60
45
|
});
|
|
61
46
|
// 3. 更新节点
|
|
@@ -110,15 +95,7 @@ export function registerTools(server, projectId, _user) {
|
|
|
110
95
|
if (lastSyncAt !== undefined)
|
|
111
96
|
updates.lastSyncAt = lastSyncAt;
|
|
112
97
|
const node = await storage.updateNode(projectId, nodeId, updates);
|
|
113
|
-
//
|
|
114
|
-
if (node && (rest.title || rest.description || tags)) {
|
|
115
|
-
try {
|
|
116
|
-
await vector.upsertNode(projectId, node.id, node.title, node.description, node.categories);
|
|
117
|
-
}
|
|
118
|
-
catch (e) {
|
|
119
|
-
console.warn('[kg_update_node] Vector index update failed:', e);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
98
|
+
// 向量索引由 Tauri 后端自动维护,MCP 不再手动更新
|
|
122
99
|
return wrap(node ? JSON.stringify(node, null, 2) : '更新失败(节点不存在或已锁定)');
|
|
123
100
|
});
|
|
124
101
|
// 3.5 更新根节点 (项目介绍)
|
|
@@ -190,56 +167,40 @@ export function registerTools(server, projectId, _user) {
|
|
|
190
167
|
}));
|
|
191
168
|
return wrap(`${JSON.stringify(output, null, 2)}\n\n💡 需要详细内容请使用 kg_read_node(nodeId) 获取节点详情`);
|
|
192
169
|
}
|
|
193
|
-
//
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
170
|
+
// 调用 Tauri 后端混合搜索 API
|
|
171
|
+
try {
|
|
172
|
+
const results = await storage.vectorSearch(projectId, query, limit, mode);
|
|
173
|
+
// 补全节点信息 (tags, status, summary)
|
|
174
|
+
const output = [];
|
|
175
|
+
for (const r of results) {
|
|
176
|
+
const node = await storage.getNode(projectId, r.id);
|
|
177
|
+
output.push({
|
|
178
|
+
id: r.id,
|
|
179
|
+
title: r.title,
|
|
180
|
+
tags: node?.categories || [],
|
|
181
|
+
status: node?.status || 'incomplete',
|
|
182
|
+
summary: node?.summary || '',
|
|
183
|
+
score: `${Math.round(r.score)}%`,
|
|
184
|
+
matchType: r.match_type
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
const json = JSON.stringify(output, null, 2);
|
|
188
|
+
return wrap(`${json}\n\n💡 需要详细内容请使用 kg_read_node(nodeId) 获取节点详情\n🔍 搜索模式: ${mode}`);
|
|
189
|
+
}
|
|
190
|
+
catch (e) {
|
|
191
|
+
// 后端搜索失败,降级为关键词搜索
|
|
192
|
+
console.warn('[kg_search] Vector search failed, fallback to keyword:', e);
|
|
193
|
+
const results = await storage.searchNodes(projectId, args.keywords, limit);
|
|
194
|
+
const output = results.map(r => ({
|
|
198
195
|
id: r.node.id,
|
|
199
196
|
title: r.node.title,
|
|
200
197
|
tags: r.node.categories || [],
|
|
201
198
|
status: r.node.status,
|
|
202
199
|
summary: r.node.summary || '',
|
|
203
|
-
|
|
200
|
+
hitRate: `${Math.round(r.score)}%`
|
|
204
201
|
}));
|
|
202
|
+
return wrap(`${JSON.stringify(output, null, 2)}\n\n💡 需要详细内容请使用 kg_read_node(nodeId) 获取节点详情\n⚠️ 语义搜索不可用,已降级为关键词搜索`);
|
|
205
203
|
}
|
|
206
|
-
// 获取语义结果 (需补全节点信息)
|
|
207
|
-
let semanticResults = [];
|
|
208
|
-
if (mode === 'hybrid' || mode === 'semantic') {
|
|
209
|
-
try {
|
|
210
|
-
// 自动检测索引状态,缺失/不匹配则自动构建
|
|
211
|
-
await vectorManager.ensureIndex(projectId);
|
|
212
|
-
const rawSemantic = await vector.semanticSearch(projectId, query, limit * 2);
|
|
213
|
-
// 补全节点信息 (关键词结果中已有的复用,没有的获取)
|
|
214
|
-
const keywordMap = new Map(keywordResults.map(r => [r.id, r]));
|
|
215
|
-
for (const r of rawSemantic) {
|
|
216
|
-
const existing = keywordMap.get(r.id);
|
|
217
|
-
if (existing) {
|
|
218
|
-
semanticResults.push({ ...existing, similarity: r.similarity });
|
|
219
|
-
}
|
|
220
|
-
else {
|
|
221
|
-
const node = await storage.getNode(projectId, r.id);
|
|
222
|
-
if (node) {
|
|
223
|
-
semanticResults.push({
|
|
224
|
-
id: r.id,
|
|
225
|
-
title: r.title,
|
|
226
|
-
tags: node.categories || [],
|
|
227
|
-
status: node.status,
|
|
228
|
-
summary: node.summary || '',
|
|
229
|
-
similarity: r.similarity
|
|
230
|
-
});
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
catch (e) {
|
|
236
|
-
console.warn('[kg_search] Semantic search failed:', e);
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
// 融合排序
|
|
240
|
-
const output = mergeSearchResults(keywordResults, semanticResults, mode, limit);
|
|
241
|
-
const json = JSON.stringify(output, null, 2);
|
|
242
|
-
return wrap(`${json}\n\n💡 需要详细内容请使用 kg_read_node(nodeId) 获取节点详情\n🔍 搜索模式: ${mode}`);
|
|
243
204
|
});
|
|
244
205
|
// 6. 路径查找
|
|
245
206
|
server.tool('kg_find_path', '查找两节点间的依赖路径', {
|
|
@@ -426,22 +387,25 @@ export function registerTools(server, projectId, _user) {
|
|
|
426
387
|
return wrap(JSON.stringify(result, null, 2));
|
|
427
388
|
});
|
|
428
389
|
// ===================== 向量索引管理 =====================
|
|
429
|
-
//
|
|
430
|
-
|
|
431
|
-
const count = await vectorManager.rebuildIndex(projectId);
|
|
432
|
-
return wrap(`✅ 向量索引构建完成,共 ${count} 个节点`);
|
|
433
|
-
});
|
|
434
|
-
// 17. 查看索引状态
|
|
390
|
+
// 注意: 向量索引由 Tauri 后端自动维护,MCP 只提供只读查询
|
|
391
|
+
// 16. 查看索引状态 (只读)
|
|
435
392
|
server.tool('kg_index_stats', '查看向量索引状态', {}, async () => {
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
}
|
|
393
|
+
try {
|
|
394
|
+
const stats = await storage.vectorStats(projectId);
|
|
395
|
+
return wrap(JSON.stringify({
|
|
396
|
+
model_downloaded: stats.model_downloaded,
|
|
397
|
+
model_loaded: stats.model_loaded,
|
|
398
|
+
model_path: stats.model_path,
|
|
399
|
+
index_count: stats.index_count,
|
|
400
|
+
tip: '向量索引由软件端自动维护,节点增删改时自动更新'
|
|
401
|
+
}, null, 2));
|
|
402
|
+
}
|
|
403
|
+
catch (e) {
|
|
404
|
+
return wrap(JSON.stringify({
|
|
405
|
+
error: '无法获取索引状态',
|
|
406
|
+
message: String(e),
|
|
407
|
+
tip: '请确保软件端已启动'
|
|
408
|
+
}, null, 2));
|
|
409
|
+
}
|
|
446
410
|
});
|
|
447
411
|
}
|
package/dist/vector/manager.d.ts
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 向量索引管理器
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* 向量索引管理器 (已弃用)
|
|
3
|
+
*
|
|
4
|
+
* ⚠️ 自 v2.7.0 起,向量索引由 Tauri 后端自动维护
|
|
5
|
+
* 此模块保留仅用于向后兼容,不再被主动调用
|
|
6
|
+
*
|
|
7
|
+
* @deprecated 请使用 Tauri 后端的 vector 模块
|
|
8
|
+
* @version 0.2
|
|
5
9
|
*/
|
|
6
10
|
interface RebuildReason {
|
|
7
11
|
needed: boolean;
|
|
8
|
-
reason: 'missing' | 'model_mismatch' | 'empty' | 'none';
|
|
12
|
+
reason: 'missing' | 'model_mismatch' | 'empty' | 'count_mismatch' | 'none';
|
|
13
|
+
missingCount?: number;
|
|
9
14
|
}
|
|
10
15
|
interface RebuildResult {
|
|
11
16
|
projectId: string;
|
|
@@ -21,7 +26,11 @@ export declare function needsRebuild(projectId: string): Promise<RebuildReason>;
|
|
|
21
26
|
*/
|
|
22
27
|
export declare function rebuildIndex(projectId: string): Promise<number>;
|
|
23
28
|
/**
|
|
24
|
-
*
|
|
29
|
+
* 增量同步缺失节点 (仅补漏,不全量重建)
|
|
30
|
+
*/
|
|
31
|
+
export declare function syncMissingNodes(projectId: string): Promise<number>;
|
|
32
|
+
/**
|
|
33
|
+
* 确保索引存在 (自动检测 + 按需构建/增量补漏)
|
|
25
34
|
*/
|
|
26
35
|
export declare function ensureIndex(projectId: string): Promise<void>;
|
|
27
36
|
/**
|
package/dist/vector/manager.js
CHANGED
|
@@ -1,12 +1,46 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 向量索引管理器
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* 向量索引管理器 (已弃用)
|
|
3
|
+
*
|
|
4
|
+
* ⚠️ 自 v2.7.0 起,向量索引由 Tauri 后端自动维护
|
|
5
|
+
* 此模块保留仅用于向后兼容,不再被主动调用
|
|
6
|
+
*
|
|
7
|
+
* @deprecated 请使用 Tauri 后端的 vector 模块
|
|
8
|
+
* @version 0.2
|
|
5
9
|
*/
|
|
6
10
|
import * as fs from 'fs';
|
|
7
11
|
import * as path from 'path';
|
|
8
12
|
import * as vectorCore from './index.js';
|
|
9
13
|
// ==================== 检测函数 ====================
|
|
14
|
+
/**
|
|
15
|
+
* 获取节点目录下的节点ID列表
|
|
16
|
+
*/
|
|
17
|
+
function getNodeIdsFromDisk(projectId) {
|
|
18
|
+
const baseDir = process.env.PPDOCS_DATA_DIR || path.join(process.env.HOME || process.env.USERPROFILE || '', '.ppdocs');
|
|
19
|
+
const nodesDir = path.join(baseDir, 'projects', projectId, 'nodes');
|
|
20
|
+
const nodeIds = new Set();
|
|
21
|
+
if (!fs.existsSync(nodesDir))
|
|
22
|
+
return nodeIds;
|
|
23
|
+
try {
|
|
24
|
+
const files = fs.readdirSync(nodesDir).filter(f => f.endsWith('.json'));
|
|
25
|
+
for (const file of files) {
|
|
26
|
+
try {
|
|
27
|
+
const content = fs.readFileSync(path.join(nodesDir, file), 'utf-8');
|
|
28
|
+
const node = JSON.parse(content);
|
|
29
|
+
// 跳过根节点
|
|
30
|
+
if (node.isOrigin || node.id === 'root')
|
|
31
|
+
continue;
|
|
32
|
+
nodeIds.add(node.id);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// 跳过无法解析的文件
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// 忽略读取错误
|
|
41
|
+
}
|
|
42
|
+
return nodeIds;
|
|
43
|
+
}
|
|
10
44
|
/**
|
|
11
45
|
* 检测索引是否需要重建
|
|
12
46
|
*/
|
|
@@ -28,6 +62,13 @@ export async function needsRebuild(projectId) {
|
|
|
28
62
|
if (!data.entries || data.entries.length === 0) {
|
|
29
63
|
return { needed: true, reason: 'empty' };
|
|
30
64
|
}
|
|
65
|
+
// 5. 检测节点数量不匹配 (新增)
|
|
66
|
+
const diskNodeIds = getNodeIdsFromDisk(projectId);
|
|
67
|
+
const indexedIds = new Set(data.entries.map(e => e.id));
|
|
68
|
+
const missingIds = [...diskNodeIds].filter(id => !indexedIds.has(id));
|
|
69
|
+
if (missingIds.length > 0) {
|
|
70
|
+
return { needed: true, reason: 'count_mismatch', missingCount: missingIds.length };
|
|
71
|
+
}
|
|
31
72
|
return { needed: false, reason: 'none' };
|
|
32
73
|
}
|
|
33
74
|
catch {
|
|
@@ -87,11 +128,54 @@ export async function rebuildIndex(projectId) {
|
|
|
87
128
|
return vectorCore.buildIndex(projectId, nodes);
|
|
88
129
|
}
|
|
89
130
|
/**
|
|
90
|
-
*
|
|
131
|
+
* 增量同步缺失节点 (仅补漏,不全量重建)
|
|
132
|
+
*/
|
|
133
|
+
export async function syncMissingNodes(projectId) {
|
|
134
|
+
const indexPath = vectorCore.getIndexPath(projectId);
|
|
135
|
+
// 读取当前索引
|
|
136
|
+
let indexedIds = new Set();
|
|
137
|
+
try {
|
|
138
|
+
if (fs.existsSync(indexPath)) {
|
|
139
|
+
const data = JSON.parse(fs.readFileSync(indexPath, 'utf-8'));
|
|
140
|
+
indexedIds = new Set(data.entries.map(e => e.id));
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
// 索引损坏,需要全量重建
|
|
145
|
+
return rebuildIndex(projectId);
|
|
146
|
+
}
|
|
147
|
+
// 找出缺失的节点
|
|
148
|
+
const allNodes = await fetchNodesForIndex(projectId);
|
|
149
|
+
const missingNodes = allNodes.filter(n => !indexedIds.has(n.id));
|
|
150
|
+
if (missingNodes.length === 0) {
|
|
151
|
+
console.log(`[VectorManager] No missing nodes to sync`);
|
|
152
|
+
return 0;
|
|
153
|
+
}
|
|
154
|
+
console.log(`[VectorManager] Syncing ${missingNodes.length} missing nodes...`);
|
|
155
|
+
// 增量添加缺失节点
|
|
156
|
+
for (let i = 0; i < missingNodes.length; i++) {
|
|
157
|
+
const node = missingNodes[i];
|
|
158
|
+
await vectorCore.upsertNode(projectId, node.id, node.title, node.description, node.categories || []);
|
|
159
|
+
if ((i + 1) % 10 === 0) {
|
|
160
|
+
console.log(`[VectorManager] Synced ${i + 1}/${missingNodes.length}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
console.log(`[VectorManager] Sync complete: ${missingNodes.length} nodes added`);
|
|
164
|
+
return missingNodes.length;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* 确保索引存在 (自动检测 + 按需构建/增量补漏)
|
|
91
168
|
*/
|
|
92
169
|
export async function ensureIndex(projectId) {
|
|
93
170
|
const check = await needsRebuild(projectId);
|
|
94
|
-
if (check.needed)
|
|
171
|
+
if (!check.needed)
|
|
172
|
+
return;
|
|
173
|
+
// count_mismatch 使用增量补漏,其他情况全量重建
|
|
174
|
+
if (check.reason === 'count_mismatch') {
|
|
175
|
+
console.log(`[VectorManager] Auto-syncing ${check.missingCount} missing nodes`);
|
|
176
|
+
await syncMissingNodes(projectId);
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
95
179
|
console.log(`[VectorManager] Auto-rebuilding index, reason: ${check.reason}`);
|
|
96
180
|
await rebuildIndex(projectId);
|
|
97
181
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ppdocs/mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.7.1",
|
|
4
4
|
"description": "ppdocs MCP Server - Knowledge Graph for Claude",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -30,7 +30,6 @@
|
|
|
30
30
|
],
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
33
|
-
"@xenova/transformers": "^2.17.2",
|
|
34
33
|
"proper-lockfile": "^4.1.2",
|
|
35
34
|
"zod": "^4.1.13"
|
|
36
35
|
},
|