@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.
@@ -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
+ }
@@ -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
  */
@@ -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 输出
@@ -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
- import * as vector from '../vector/index.js';
5
- import * as vectorManager from '../vector/manager.js';
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
- let keywordResults = [];
195
- if (mode === 'hybrid' || mode === 'keyword') {
196
- const kResults = await storage.searchNodes(projectId, args.keywords, limit * 2);
197
- keywordResults = kResults.map(r => ({
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
- score: r.score / 100
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
- // 16. 构建向量索引
430
- server.tool('kg_build_index', '构建/重建向量语义索引(首次使用或索引损坏时调用)', {}, async () => {
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
- const stats = await vector.getIndexStats(projectId);
437
- const rebuildCheck = await vectorManager.needsRebuild(projectId);
438
- return wrap(JSON.stringify({
439
- ...stats,
440
- needsRebuild: rebuildCheck.needed,
441
- rebuildReason: rebuildCheck.reason,
442
- tip: rebuildCheck.needed
443
- ? `索引需要重建,原因: ${rebuildCheck.reason}`
444
- : '索引正常 (搜索时会自动检测并重建)'
445
- }, null, 2));
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
  }
@@ -1,11 +1,16 @@
1
1
  /**
2
- * 向量索引管理器
3
- * 负责自动检测、重建索引逻辑
4
- * @version 0.1
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
  /**
@@ -1,12 +1,46 @@
1
1
  /**
2
- * 向量索引管理器
3
- * 负责自动检测、重建索引逻辑
4
- * @version 0.1
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.6.30",
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
  },