@ppdocs/mcp 2.6.27 → 2.6.29

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.
@@ -0,0 +1,81 @@
1
+ /**
2
+ * MCP 工具辅助函数
3
+ * 提取自 index.ts,避免重复代码
4
+ */
5
+ import type { NodeData } from '../storage/types.js';
6
+ /**
7
+ * 包装返回结果为 MCP 格式
8
+ */
9
+ export declare function wrap(text: string): {
10
+ content: {
11
+ type: "text";
12
+ text: string;
13
+ }[];
14
+ };
15
+ interface RelationItem {
16
+ id: string;
17
+ title: string;
18
+ edge: string;
19
+ }
20
+ /**
21
+ * 递归获取节点的上下游关系
22
+ */
23
+ export declare function fetchRelations(projectId: string, nodeId: string, maxDepth: number): Promise<{
24
+ outgoing: RelationItem[];
25
+ incoming: RelationItem[];
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
+ /**
55
+ * 格式化节点为 Markdown 输出
56
+ */
57
+ export declare function formatNodeMarkdown(node: NodeData): string[];
58
+ /**
59
+ * 格式化关系输出
60
+ */
61
+ export declare function formatRelationsMarkdown(outgoing: RelationItem[], incoming: RelationItem[]): string[];
62
+ interface TreeNode {
63
+ name: string;
64
+ type: 'folder' | 'node';
65
+ nodeId?: string;
66
+ status?: string;
67
+ children?: TreeNode[];
68
+ }
69
+ /**
70
+ * 构建目录树结构
71
+ */
72
+ export declare function buildDirectoryTree(nodes: NodeData[]): TreeNode;
73
+ /**
74
+ * 格式化树为文本
75
+ */
76
+ export declare function formatTreeText(tree: TreeNode): string;
77
+ /**
78
+ * 统计树中的节点数
79
+ */
80
+ export declare function countTreeNodes(tree: TreeNode): number;
81
+ export {};
@@ -0,0 +1,263 @@
1
+ /**
2
+ * MCP 工具辅助函数
3
+ * 提取自 index.ts,避免重复代码
4
+ */
5
+ import * as storage from '../storage/httpClient.js';
6
+ // ==================== 通用包装 ====================
7
+ /**
8
+ * 包装返回结果为 MCP 格式
9
+ */
10
+ export function wrap(text) {
11
+ return { content: [{ type: 'text', text }] };
12
+ }
13
+ /**
14
+ * 递归获取节点的上下游关系
15
+ */
16
+ export async function fetchRelations(projectId, nodeId, maxDepth) {
17
+ const visited = new Set();
18
+ const outgoing = [];
19
+ const incoming = [];
20
+ async function recurse(currentNodeId, currentDepth, direction) {
21
+ if (currentDepth > maxDepth || visited.has(`${currentNodeId}-${direction}`))
22
+ return;
23
+ visited.add(`${currentNodeId}-${direction}`);
24
+ const relations = await storage.getRelations(projectId, currentNodeId);
25
+ for (const r of relations) {
26
+ if (r.direction === 'outgoing' && (direction === 'outgoing' || direction === 'both')) {
27
+ if (!outgoing.some(o => o.id === r.nodeId)) {
28
+ outgoing.push({ id: r.nodeId, title: r.title, edge: r.edgeType });
29
+ await recurse(r.nodeId, currentDepth + 1, 'outgoing');
30
+ }
31
+ }
32
+ if (r.direction === 'incoming' && (direction === 'incoming' || direction === 'both')) {
33
+ if (!incoming.some(i => i.id === r.nodeId)) {
34
+ incoming.push({ id: r.nodeId, title: r.title, edge: r.edgeType });
35
+ await recurse(r.nodeId, currentDepth + 1, 'incoming');
36
+ }
37
+ }
38
+ }
39
+ }
40
+ await recurse(nodeId, 1, 'both');
41
+ return { outgoing, incoming };
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
+ // ==================== 节点格式化 ====================
100
+ /**
101
+ * 格式化节点为 Markdown 输出
102
+ */
103
+ export function formatNodeMarkdown(node) {
104
+ const lines = [];
105
+ // 基础信息
106
+ lines.push(`## ${node.title}\n`);
107
+ if (node.summary) {
108
+ lines.push(`> ${node.summary}\n`);
109
+ }
110
+ lines.push('**基础信息**');
111
+ lines.push(`| 字段 | 值 |`);
112
+ lines.push(`|:---|:---|`);
113
+ lines.push(`| ID | ${node.id} |`);
114
+ lines.push(`| 类型 | ${node.type} |`);
115
+ lines.push(`| 状态 | ${node.status} |`);
116
+ lines.push(`| 签名 | ${node.signature} |`);
117
+ if (node.categories && node.categories.length > 0) {
118
+ lines.push(`| 标签 | ${node.categories.join(', ')} |`);
119
+ }
120
+ lines.push('');
121
+ // 关联文件
122
+ if (node.relatedFiles && node.relatedFiles.length > 0) {
123
+ lines.push('**关联文件**');
124
+ node.relatedFiles.forEach((f) => lines.push(`- ${f}`));
125
+ lines.push('');
126
+ }
127
+ // 依赖关系
128
+ if (node.dependencies && node.dependencies.length > 0) {
129
+ lines.push('**依赖关系**');
130
+ node.dependencies.forEach((d) => lines.push(`- ${d.name}: ${d.description}`));
131
+ lines.push('');
132
+ }
133
+ // 描述内容
134
+ if (node.description) {
135
+ lines.push('**描述内容**');
136
+ lines.push(node.description);
137
+ lines.push('');
138
+ }
139
+ // 更新历史
140
+ if (node.versions && node.versions.length > 0) {
141
+ lines.push('**更新历史**');
142
+ lines.push('| 版本 | 日期 | 变更 |');
143
+ lines.push('|:---|:---|:---|');
144
+ node.versions.forEach((v) => lines.push(`| ${v.version} | ${v.date} | ${v.changes} |`));
145
+ lines.push('');
146
+ }
147
+ // 修复历史
148
+ if (node.bugfixes && node.bugfixes.length > 0) {
149
+ lines.push('**修复历史**');
150
+ lines.push('| ID | 日期 | 问题 | 方案 |');
151
+ lines.push('|:---|:---|:---|:---|');
152
+ node.bugfixes.forEach((b) => lines.push(`| ${b.id} | ${b.date} | ${b.issue} | ${b.solution} |`));
153
+ lines.push('');
154
+ }
155
+ return lines;
156
+ }
157
+ /**
158
+ * 格式化关系输出
159
+ */
160
+ export function formatRelationsMarkdown(outgoing, incoming) {
161
+ const lines = [];
162
+ if (outgoing.length > 0 || incoming.length > 0) {
163
+ lines.push('**上下游关系**');
164
+ if (outgoing.length > 0) {
165
+ lines.push(`- 依赖 (${outgoing.length}): ${outgoing.map(o => o.title).join(', ')}`);
166
+ }
167
+ if (incoming.length > 0) {
168
+ lines.push(`- 被依赖 (${incoming.length}): ${incoming.map(i => i.title).join(', ')}`);
169
+ }
170
+ lines.push('');
171
+ }
172
+ return lines;
173
+ }
174
+ /**
175
+ * 构建目录树结构
176
+ */
177
+ export function buildDirectoryTree(nodes) {
178
+ const root = { name: '/', type: 'folder', children: [] };
179
+ // 过滤掉根节点
180
+ const docNodes = nodes.filter(n => !n.isOrigin && n.id !== 'root');
181
+ for (const node of docNodes) {
182
+ const pathSegments = (node.path || '').split('/').filter(Boolean);
183
+ let current = root;
184
+ // 遍历路径段,创建目录结构
185
+ for (const segment of pathSegments) {
186
+ if (!current.children)
187
+ current.children = [];
188
+ let child = current.children.find(c => c.name === segment && c.type === 'folder');
189
+ if (!child) {
190
+ child = { name: segment, type: 'folder', children: [] };
191
+ current.children.push(child);
192
+ }
193
+ current = child;
194
+ }
195
+ // 添加节点
196
+ if (!current.children)
197
+ current.children = [];
198
+ current.children.push({
199
+ name: node.title,
200
+ type: 'node',
201
+ nodeId: node.id,
202
+ status: node.status
203
+ });
204
+ }
205
+ // 递归排序 (目录在前,节点在后)
206
+ sortTree(root);
207
+ return root;
208
+ }
209
+ function sortTree(node) {
210
+ if (!node.children)
211
+ return;
212
+ node.children.sort((a, b) => {
213
+ if (a.type === 'folder' && b.type !== 'folder')
214
+ return -1;
215
+ if (a.type !== 'folder' && b.type === 'folder')
216
+ return 1;
217
+ return a.name.localeCompare(b.name, 'zh-CN');
218
+ });
219
+ for (const child of node.children) {
220
+ if (child.type === 'folder')
221
+ sortTree(child);
222
+ }
223
+ }
224
+ /**
225
+ * 格式化树为文本
226
+ */
227
+ export function formatTreeText(tree) {
228
+ function format(node, prefix = '', isLast = true) {
229
+ const lines = [];
230
+ const connector = isLast ? '└── ' : '├── ';
231
+ const childPrefix = isLast ? ' ' : '│ ';
232
+ if (node.name !== '/') {
233
+ const icon = node.type === 'folder' ? '📁' : '📄';
234
+ const status = node.status ? ` [${node.status}]` : '';
235
+ lines.push(`${prefix}${connector}${icon} ${node.name}${status}`);
236
+ }
237
+ if (node.children) {
238
+ for (let i = 0; i < node.children.length; i++) {
239
+ const child = node.children[i];
240
+ const childIsLast = i === node.children.length - 1;
241
+ const newPrefix = node.name === '/' ? '' : prefix + childPrefix;
242
+ lines.push(...format(child, newPrefix, childIsLast));
243
+ }
244
+ }
245
+ return lines;
246
+ }
247
+ return format(tree).join('\n');
248
+ }
249
+ /**
250
+ * 统计树中的节点数
251
+ */
252
+ export function countTreeNodes(tree) {
253
+ let count = 0;
254
+ function traverse(node) {
255
+ if (node.type === 'node')
256
+ count++;
257
+ if (node.children) {
258
+ node.children.forEach(traverse);
259
+ }
260
+ }
261
+ traverse(tree);
262
+ return count;
263
+ }
@@ -2,10 +2,8 @@ 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
4
  import * as vector from '../vector/index.js';
5
- // 辅助函数: 包装返回结果
6
- function wrap(_projectId, text) {
7
- return { content: [{ type: 'text', text }] };
8
- }
5
+ import * as vectorManager from '../vector/manager.js';
6
+ import { wrap, fetchRelations, mergeSearchResults, formatNodeMarkdown, formatRelationsMarkdown, buildDirectoryTree, formatTreeText, countTreeNodes } from './helpers.js';
9
7
  export function registerTools(server, projectId, _user) {
10
8
  // 1. 创建节点
11
9
  server.tool('kg_create_node', '创建知识节点。type: logic=逻辑/函数, data=数据结构, intro=概念介绍。⚠️ tags至少提供3个分类标签', {
@@ -44,7 +42,7 @@ export function registerTools(server, projectId, _user) {
44
42
  catch (e) {
45
43
  console.warn('[kg_create_node] Vector index update failed:', e);
46
44
  }
47
- return wrap(projectId, JSON.stringify(node, null, 2));
45
+ return wrap(JSON.stringify(node, null, 2));
48
46
  });
49
47
  // 2. 删除节点
50
48
  server.tool('kg_delete_node', '删除节点(锁定节点和根节点不可删除)', { nodeId: z.string().describe('节点ID') }, async (args) => {
@@ -58,7 +56,7 @@ export function registerTools(server, projectId, _user) {
58
56
  console.warn('[kg_delete_node] Vector index removal failed:', e);
59
57
  }
60
58
  }
61
- return wrap(projectId, success ? '删除成功' : '删除失败(节点不存在/已锁定/是根节点)');
59
+ return wrap(success ? '删除成功' : '删除失败(节点不存在/已锁定/是根节点)');
62
60
  });
63
61
  // 3. 更新节点
64
62
  server.tool('kg_update_node', '更新节点内容(锁定节点不可更新)。⚠️ 更新tags时至少提供3个分类标签', {
@@ -95,7 +93,7 @@ export function registerTools(server, projectId, _user) {
95
93
  const { nodeId, tags, relatedFiles, versions, bugfixes, path, lastSyncAt, ...rest } = decoded;
96
94
  // 根节点必须使用 kg_update_root 更新
97
95
  if (nodeId === 'root') {
98
- return wrap(projectId, '❌ 根节点请使用 kg_update_root 方法更新');
96
+ return wrap('❌ 根节点请使用 kg_update_root 方法更新');
99
97
  }
100
98
  // API 参数转换
101
99
  let updates = { ...rest };
@@ -121,7 +119,7 @@ export function registerTools(server, projectId, _user) {
121
119
  console.warn('[kg_update_node] Vector index update failed:', e);
122
120
  }
123
121
  }
124
- return wrap(projectId, node ? JSON.stringify(node, null, 2) : '更新失败(节点不存在或已锁定)');
122
+ return wrap(node ? JSON.stringify(node, null, 2) : '更新失败(节点不存在或已锁定)');
125
123
  });
126
124
  // 3.5 更新根节点 (项目介绍)
127
125
  server.tool('kg_update_root', '更新项目介绍(根节点描述,锁定时不可更新)', {
@@ -130,13 +128,13 @@ export function registerTools(server, projectId, _user) {
130
128
  }, async (args) => {
131
129
  const decoded = decodeObjectStrings(args);
132
130
  if (decoded.title === undefined && decoded.description === undefined) {
133
- return wrap(projectId, '❌ 请至少提供 title 或 description');
131
+ return wrap('❌ 请至少提供 title 或 description');
134
132
  }
135
133
  const node = await storage.updateRoot(projectId, {
136
134
  title: decoded.title,
137
135
  description: decoded.description
138
136
  });
139
- return wrap(projectId, node ? '✅ 项目介绍已更新' : '更新失败(根节点已锁定)');
137
+ return wrap(node ? '✅ 项目介绍已更新' : '更新失败(根节点已锁定)');
140
138
  });
141
139
  // 3.6 获取项目规则 (独立方法,按需调用)
142
140
  server.tool('kg_get_rules', '获取项目规则(可指定类型或获取全部)', {
@@ -159,25 +157,25 @@ export function registerTools(server, projectId, _user) {
159
157
  const decoded = decodeObjectStrings(args);
160
158
  const success = await storage.saveRules(projectId, decoded.ruleType, decoded.rules);
161
159
  if (!success) {
162
- return wrap(projectId, '❌ 保存失败');
160
+ return wrap('❌ 保存失败');
163
161
  }
164
- return wrap(projectId, `✅ ${RULE_TYPE_LABELS[decoded.ruleType]}已保存 (${decoded.rules.length} 条)`);
162
+ return wrap(`✅ ${RULE_TYPE_LABELS[decoded.ruleType]}已保存 (${decoded.rules.length} 条)`);
165
163
  });
166
164
  // 4. 锁定节点 (只能锁定,解锁需用户在前端手动操作)
167
165
  server.tool('kg_lock_node', '锁定节点(锁定后只能读取,解锁需用户在前端手动操作)', {
168
166
  nodeId: z.string().describe('节点ID')
169
167
  }, async (args) => {
170
168
  const node = await storage.lockNode(projectId, args.nodeId, true); // 强制锁定
171
- return wrap(projectId, node ? JSON.stringify(node, null, 2) : '操作失败');
169
+ return wrap(node ? JSON.stringify(node, null, 2) : '操作失败');
172
170
  });
173
171
  // 5. 搜索节点 (混合检索: 向量语义 + 关键词)
174
172
  server.tool('kg_search', '智能搜索节点(语义+关键词混合)。支持中英文语义匹配,空白关键词[""]返回全部节点', {
175
- keywords: z.array(z.string()).describe('搜索词列表,支持语义匹配(如"登录"可找到auth)'),
173
+ keywords: z.array(z.string()).describe('搜索词列表(⚠️建议3个以上关键词提高准确率),支持语义匹配(如"登录"可找到auth)'),
176
174
  limit: z.number().optional().describe('返回数量(默认10)'),
177
175
  mode: z.enum(['hybrid', 'keyword', 'semantic']).optional().describe('搜索模式: hybrid=混合(默认), keyword=仅关键词, semantic=仅语义')
178
176
  }, async (args) => {
179
177
  const limit = args.limit || 10;
180
- const mode = args.mode || 'hybrid';
178
+ const mode = (args.mode || 'hybrid');
181
179
  const query = args.keywords.filter(k => k.trim()).join(' ');
182
180
  // 空白关键词返回全部
183
181
  if (!query) {
@@ -190,7 +188,7 @@ export function registerTools(server, projectId, _user) {
190
188
  summary: r.node.summary || '',
191
189
  hitRate: `${Math.round(r.score)}%`
192
190
  }));
193
- return wrap(projectId, `${JSON.stringify(output, null, 2)}\n\n💡 需要详细内容请使用 kg_read_node(nodeId) 获取节点详情`);
191
+ return wrap(`${JSON.stringify(output, null, 2)}\n\n💡 需要详细内容请使用 kg_read_node(nodeId) 获取节点详情`);
194
192
  }
195
193
  // 获取关键词结果
196
194
  let keywordResults = [];
@@ -202,77 +200,46 @@ export function registerTools(server, projectId, _user) {
202
200
  tags: r.node.categories || [],
203
201
  status: r.node.status,
204
202
  summary: r.node.summary || '',
205
- score: r.score / 100 // 归一化到 0-1
203
+ score: r.score / 100
206
204
  }));
207
205
  }
208
- // 获取语义结果
206
+ // 获取语义结果 (需补全节点信息)
209
207
  let semanticResults = [];
210
208
  if (mode === 'hybrid' || mode === 'semantic') {
211
209
  try {
212
- semanticResults = await vector.semanticSearch(projectId, query, limit * 2);
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
+ }
213
234
  }
214
235
  catch (e) {
215
- console.warn('[kg_search] Semantic search failed, falling back to keyword:', e);
216
- }
217
- }
218
- // 融合排序 (混合模式)
219
- const scoreMap = new Map();
220
- // 合并关键词结果
221
- for (const r of keywordResults) {
222
- scoreMap.set(r.id, {
223
- id: r.id,
224
- title: r.title,
225
- tags: r.tags,
226
- status: r.status,
227
- summary: r.summary,
228
- keywordScore: r.score,
229
- semanticScore: 0
230
- });
231
- }
232
- // 合并语义结果
233
- for (const r of semanticResults) {
234
- const existing = scoreMap.get(r.id);
235
- if (existing) {
236
- existing.semanticScore = r.similarity;
237
- }
238
- else {
239
- // 需要获取节点详情
240
- const node = await storage.getNode(projectId, r.id);
241
- if (node) {
242
- scoreMap.set(r.id, {
243
- id: r.id,
244
- title: r.title,
245
- tags: node.categories || [],
246
- status: node.status,
247
- summary: node.summary || '',
248
- keywordScore: 0,
249
- semanticScore: r.similarity
250
- });
251
- }
236
+ console.warn('[kg_search] Semantic search failed:', e);
252
237
  }
253
238
  }
254
- // 计算综合得分并排序
255
- const combined = Array.from(scoreMap.values()).map(item => {
256
- // 混合权重: 语义 0.6 + 关键词 0.4
257
- const finalScore = mode === 'keyword' ? item.keywordScore
258
- : mode === 'semantic' ? item.semanticScore
259
- : item.semanticScore * 0.6 + item.keywordScore * 0.4;
260
- return { ...item, finalScore };
261
- });
262
- combined.sort((a, b) => b.finalScore - a.finalScore);
263
- const topResults = combined.slice(0, limit);
264
- const output = topResults.map(r => ({
265
- id: r.id,
266
- title: r.title,
267
- tags: r.tags,
268
- status: r.status,
269
- summary: r.summary,
270
- score: `${Math.round(r.finalScore * 100)}%`,
271
- matchType: r.keywordScore > 0 && r.semanticScore > 0 ? 'hybrid'
272
- : r.semanticScore > 0 ? 'semantic' : 'keyword'
273
- }));
239
+ // 融合排序
240
+ const output = mergeSearchResults(keywordResults, semanticResults, mode, limit);
274
241
  const json = JSON.stringify(output, null, 2);
275
- return wrap(projectId, `${json}\n\n💡 需要详细内容请使用 kg_read_node(nodeId) 获取节点详情\n🔍 搜索模式: ${mode}`);
242
+ return wrap(`${json}\n\n💡 需要详细内容请使用 kg_read_node(nodeId) 获取节点详情\n🔍 搜索模式: ${mode}`);
276
243
  });
277
244
  // 5.5 列出所有标签
278
245
  server.tool('kg_list_tags', '获取所有节点使用过的标签列表(去重)', {}, async () => {
@@ -284,7 +251,7 @@ export function registerTools(server, projectId, _user) {
284
251
  }
285
252
  }
286
253
  const tags = Array.from(tagSet).sort();
287
- return wrap(projectId, JSON.stringify({ total: tags.length, tags }, null, 2));
254
+ return wrap(JSON.stringify({ total: tags.length, tags }, null, 2));
288
255
  });
289
256
  // 6. 路径查找
290
257
  server.tool('kg_find_path', '查找两节点间的依赖路径', {
@@ -293,13 +260,13 @@ export function registerTools(server, projectId, _user) {
293
260
  }, async (args) => {
294
261
  const result = await storage.findPath(projectId, args.startId, args.endId);
295
262
  if (!result)
296
- return wrap(projectId, '未找到路径');
263
+ return wrap('未找到路径');
297
264
  const output = {
298
265
  pathLength: result.path.length,
299
266
  nodes: result.path.map(n => ({ id: n.id, title: n.title, type: n.type })),
300
267
  edges: result.edges.map(e => ({ from: e.source, to: e.target, type: e.type }))
301
268
  };
302
- return wrap(projectId, JSON.stringify(output, null, 2));
269
+ return wrap(JSON.stringify(output, null, 2));
303
270
  });
304
271
  // 7. 列出节点 (支持过滤)
305
272
  server.tool('kg_list_nodes', '列出节点,支持按状态/连接数过滤。maxEdges=0 可查孤立节点', {
@@ -312,7 +279,7 @@ export function registerTools(server, projectId, _user) {
312
279
  : undefined;
313
280
  const nodes = await storage.listNodes(projectId, filter);
314
281
  const output = nodes.map(n => ({ id: n.id, title: n.title, type: n.type, status: n.status, locked: n.locked, summary: n.summary || '' }));
315
- return wrap(projectId, JSON.stringify(output, null, 2));
282
+ return wrap(JSON.stringify(output, null, 2));
316
283
  });
317
284
  // 8. 查询节点关系网 (支持多层)
318
285
  server.tool('kg_get_relations', '获取节点的上下游关系(谁依赖它/它依赖谁),支持多层查询', {
@@ -320,35 +287,11 @@ export function registerTools(server, projectId, _user) {
320
287
  depth: z.number().min(1).max(3).optional().describe('查询层数(默认1,最大3)')
321
288
  }, async (args) => {
322
289
  const maxDepth = Math.min(args.depth || 1, 3);
323
- const visited = new Set();
324
- const outgoing = [];
325
- const incoming = [];
326
- // 递归获取关系
327
- async function fetchRelations(nodeId, currentDepth, direction) {
328
- if (currentDepth > maxDepth || visited.has(`${nodeId}-${direction}`))
329
- return;
330
- visited.add(`${nodeId}-${direction}`);
331
- const relations = await storage.getRelations(projectId, nodeId);
332
- for (const r of relations) {
333
- if (r.direction === 'outgoing' && (direction === 'outgoing' || direction === 'both')) {
334
- if (!outgoing.some(o => o.id === r.nodeId)) {
335
- outgoing.push({ id: r.nodeId, title: r.title, edge: r.edgeType });
336
- await fetchRelations(r.nodeId, currentDepth + 1, 'outgoing');
337
- }
338
- }
339
- if (r.direction === 'incoming' && (direction === 'incoming' || direction === 'both')) {
340
- if (!incoming.some(i => i.id === r.nodeId)) {
341
- incoming.push({ id: r.nodeId, title: r.title, edge: r.edgeType });
342
- await fetchRelations(r.nodeId, currentDepth + 1, 'incoming');
343
- }
344
- }
345
- }
346
- }
347
- await fetchRelations(args.nodeId, 1, 'both');
290
+ const { outgoing, incoming } = await fetchRelations(projectId, args.nodeId, maxDepth);
348
291
  if (outgoing.length === 0 && incoming.length === 0) {
349
- return wrap(projectId, '该节点没有任何连线');
292
+ return wrap('该节点没有任何连线');
350
293
  }
351
- return wrap(projectId, JSON.stringify({ outgoing, incoming }, null, 2));
294
+ return wrap(JSON.stringify({ outgoing, incoming }, null, 2));
352
295
  });
353
296
  // 9. 读取单个节点详情
354
297
  server.tool('kg_read_node', '读取单个节点的完整内容(描述、关联文件、依赖、历史记录),可选包含上下游关系', {
@@ -357,171 +300,25 @@ export function registerTools(server, projectId, _user) {
357
300
  }, async (args) => {
358
301
  const node = await storage.getNode(projectId, args.nodeId);
359
302
  if (!node) {
360
- return wrap(projectId, '节点不存在');
361
- }
362
- // 构建结构化输出
363
- const lines = [];
364
- // 基础信息
365
- lines.push(`## ${node.title}\n`);
366
- if (node.summary) {
367
- lines.push(`> ${node.summary}\n`);
368
- }
369
- lines.push('**基础信息**');
370
- lines.push(`| 字段 | 值 |`);
371
- lines.push(`|:---|:---|`);
372
- lines.push(`| ID | ${node.id} |`);
373
- lines.push(`| 类型 | ${node.type} |`);
374
- lines.push(`| 状态 | ${node.status} |`);
375
- lines.push(`| 签名 | ${node.signature} |`);
376
- if (node.categories && node.categories.length > 0) {
377
- lines.push(`| 标签 | ${node.categories.join(', ')} |`);
378
- }
379
- lines.push('');
380
- // 关联文件
381
- if (node.relatedFiles && node.relatedFiles.length > 0) {
382
- lines.push('**关联文件**');
383
- node.relatedFiles.forEach(f => lines.push(`- ${f}`));
384
- lines.push('');
385
- }
386
- // 依赖关系
387
- if (node.dependencies && node.dependencies.length > 0) {
388
- lines.push('**依赖关系**');
389
- node.dependencies.forEach(d => lines.push(`- ${d.name}: ${d.description}`));
390
- lines.push('');
391
- }
392
- // 描述内容
393
- if (node.description) {
394
- lines.push('**描述内容**');
395
- lines.push(node.description);
396
- lines.push('');
397
- }
398
- // 更新历史
399
- if (node.versions && node.versions.length > 0) {
400
- lines.push('**更新历史**');
401
- lines.push('| 版本 | 日期 | 变更 |');
402
- lines.push('|:---|:---|:---|');
403
- node.versions.forEach(v => lines.push(`| ${v.version} | ${v.date} | ${v.changes} |`));
404
- lines.push('');
405
- }
406
- // 修复历史
407
- if (node.bugfixes && node.bugfixes.length > 0) {
408
- lines.push('**修复历史**');
409
- lines.push('| ID | 日期 | 问题 | 方案 |');
410
- lines.push('|:---|:---|:---|:---|');
411
- node.bugfixes.forEach(b => lines.push(`| ${b.id} | ${b.date} | ${b.issue} | ${b.solution} |`));
412
- lines.push('');
303
+ return wrap('节点不存在');
413
304
  }
305
+ // 格式化节点基础信息
306
+ const lines = formatNodeMarkdown(node);
414
307
  // 上下游关系 (depth > 0 时获取)
415
308
  const depth = args.depth || 0;
416
309
  if (depth > 0) {
417
- const visited = new Set();
418
- const outgoing = [];
419
- const incoming = [];
420
- async function fetchRelations(nodeId, currentDepth, direction) {
421
- if (currentDepth > depth || visited.has(`${nodeId}-${direction}`))
422
- return;
423
- visited.add(`${nodeId}-${direction}`);
424
- const relations = await storage.getRelations(projectId, nodeId);
425
- for (const r of relations) {
426
- if (r.direction === 'outgoing' && (direction === 'outgoing' || direction === 'both')) {
427
- if (!outgoing.some(o => o.id === r.nodeId)) {
428
- outgoing.push({ id: r.nodeId, title: r.title, edge: r.edgeType });
429
- await fetchRelations(r.nodeId, currentDepth + 1, 'outgoing');
430
- }
431
- }
432
- if (r.direction === 'incoming' && (direction === 'incoming' || direction === 'both')) {
433
- if (!incoming.some(i => i.id === r.nodeId)) {
434
- incoming.push({ id: r.nodeId, title: r.title, edge: r.edgeType });
435
- await fetchRelations(r.nodeId, currentDepth + 1, 'incoming');
436
- }
437
- }
438
- }
439
- }
440
- await fetchRelations(args.nodeId, 1, 'both');
441
- if (outgoing.length > 0 || incoming.length > 0) {
442
- lines.push('**上下游关系**');
443
- if (outgoing.length > 0) {
444
- lines.push(`- 依赖 (${outgoing.length}): ${outgoing.map(o => o.title).join(', ')}`);
445
- }
446
- if (incoming.length > 0) {
447
- lines.push(`- 被依赖 (${incoming.length}): ${incoming.map(i => i.title).join(', ')}`);
448
- }
449
- lines.push('');
450
- }
310
+ const { outgoing, incoming } = await fetchRelations(projectId, args.nodeId, depth);
311
+ lines.push(...formatRelationsMarkdown(outgoing, incoming));
451
312
  }
452
- return wrap(projectId, lines.join('\n'));
313
+ return wrap(lines.join('\n'));
453
314
  });
454
315
  // 10. 获取知识库树状图
455
316
  server.tool('kg_get_tree', '获取知识库的目录树结构(按 path 分组)', {}, async () => {
456
317
  const nodes = await storage.listNodes(projectId);
457
- const root = { name: '/', type: 'folder', children: [] };
458
- // 过滤掉根节点
459
- const docNodes = nodes.filter(n => !n.isOrigin && n.id !== 'root');
460
- for (const node of docNodes) {
461
- const pathSegments = (node.path || '').split('/').filter(Boolean);
462
- let current = root;
463
- // 遍历路径段,创建目录结构
464
- for (const segment of pathSegments) {
465
- if (!current.children)
466
- current.children = [];
467
- let child = current.children.find(c => c.name === segment && c.type === 'folder');
468
- if (!child) {
469
- child = { name: segment, type: 'folder', children: [] };
470
- current.children.push(child);
471
- }
472
- current = child;
473
- }
474
- // 添加节点
475
- if (!current.children)
476
- current.children = [];
477
- current.children.push({
478
- name: node.title,
479
- type: 'node',
480
- nodeId: node.id,
481
- status: node.status
482
- });
483
- }
484
- // 递归排序 (目录在前,节点在后)
485
- function sortTree(node) {
486
- if (!node.children)
487
- return;
488
- node.children.sort((a, b) => {
489
- if (a.type === 'folder' && b.type !== 'folder')
490
- return -1;
491
- if (a.type !== 'folder' && b.type === 'folder')
492
- return 1;
493
- return a.name.localeCompare(b.name, 'zh-CN');
494
- });
495
- for (const child of node.children) {
496
- if (child.type === 'folder')
497
- sortTree(child);
498
- }
499
- }
500
- sortTree(root);
501
- // 格式化为文本树
502
- function formatTree(node, prefix = '', isLast = true) {
503
- const lines = [];
504
- const connector = isLast ? '└── ' : '├── ';
505
- const childPrefix = isLast ? ' ' : '│ ';
506
- if (node.name !== '/') {
507
- const icon = node.type === 'folder' ? '📁' : '📄';
508
- const status = node.status ? ` [${node.status}]` : '';
509
- lines.push(`${prefix}${connector}${icon} ${node.name}${status}`);
510
- }
511
- if (node.children) {
512
- const children = node.children;
513
- for (let i = 0; i < children.length; i++) {
514
- const child = children[i];
515
- const childIsLast = i === children.length - 1;
516
- const newPrefix = node.name === '/' ? '' : prefix + childPrefix;
517
- lines.push(...formatTree(child, newPrefix, childIsLast));
518
- }
519
- }
520
- return lines;
521
- }
522
- const treeText = formatTree(root).join('\n');
523
- const summary = `共 ${docNodes.length} 个节点\n\n${treeText}`;
524
- return wrap(projectId, summary);
318
+ const tree = buildDirectoryTree(nodes);
319
+ const treeText = formatTreeText(tree);
320
+ const nodeCount = countTreeNodes(tree);
321
+ return wrap(`共 ${nodeCount} 个节点\n\n${treeText}`);
525
322
  });
526
323
  // ===================== 任务管理工具 =====================
527
324
  // 11. 创建任务
@@ -539,7 +336,7 @@ export function registerTools(server, projectId, _user) {
539
336
  goals: decoded.goals || [],
540
337
  related_nodes: decoded.related_nodes
541
338
  }, _user);
542
- return wrap(projectId, JSON.stringify(task, null, 2));
339
+ return wrap(JSON.stringify(task, null, 2));
543
340
  });
544
341
  // 11. 列出任务
545
342
  server.tool('task_list', '列出任务(active=进行中,archived=已归档)', {
@@ -554,7 +351,7 @@ export function registerTools(server, projectId, _user) {
554
351
  created_at: t.created_at,
555
352
  last_log: t.last_log
556
353
  }));
557
- return wrap(projectId, JSON.stringify(output, null, 2));
354
+ return wrap(JSON.stringify(output, null, 2));
558
355
  });
559
356
  // 12. 获取任务详情
560
357
  server.tool('task_get', '获取任务完整信息(含全部日志)', {
@@ -562,9 +359,9 @@ export function registerTools(server, projectId, _user) {
562
359
  }, async (args) => {
563
360
  const task = await storage.getTask(projectId, args.taskId);
564
361
  if (!task) {
565
- return wrap(projectId, '任务不存在');
362
+ return wrap('任务不存在');
566
363
  }
567
- return wrap(projectId, JSON.stringify(task, null, 2));
364
+ return wrap(JSON.stringify(task, null, 2));
568
365
  });
569
366
  // 13. 添加任务日志
570
367
  server.tool('task_add_log', '记录任务进展/问题/方案/参考', {
@@ -576,9 +373,9 @@ export function registerTools(server, projectId, _user) {
576
373
  const decodedContent = decodeUnicodeEscapes(args.content);
577
374
  const task = await storage.addTaskLog(projectId, args.taskId, args.log_type, decodedContent);
578
375
  if (!task) {
579
- return wrap(projectId, '添加失败(任务不存在或已归档)');
376
+ return wrap('添加失败(任务不存在或已归档)');
580
377
  }
581
- return wrap(projectId, `日志已添加,任务共有 ${task.logs.length} 条日志`);
378
+ return wrap(`日志已添加,任务共有 ${task.logs.length} 条日志`);
582
379
  });
583
380
  // 14. 完成任务
584
381
  server.tool('task_complete', '完成任务并归档,填写经验总结', {
@@ -600,9 +397,9 @@ export function registerTools(server, projectId, _user) {
600
397
  });
601
398
  const task = await storage.completeTask(projectId, args.taskId, decoded);
602
399
  if (!task) {
603
- return wrap(projectId, '完成失败(任务不存在或已归档)');
400
+ return wrap('完成失败(任务不存在或已归档)');
604
401
  }
605
- return wrap(projectId, `任务已完成归档: ${task.title}`);
402
+ return wrap(`任务已完成归档: ${task.title}`);
606
403
  });
607
404
  // 15. 查询文件关联的节点
608
405
  server.tool('kg_find_by_file', '查询哪些节点绑定了指定文件(删除/重命名前检查)', {
@@ -638,26 +435,25 @@ export function registerTools(server, projectId, _user) {
638
435
  ? `⚠️ 该文件被 ${boundBy.length} 个节点引用,删除前请更新节点`
639
436
  : '✅ 该文件无 KG 引用,可安全操作'
640
437
  };
641
- return wrap(projectId, JSON.stringify(result, null, 2));
438
+ return wrap(JSON.stringify(result, null, 2));
642
439
  });
643
440
  // ===================== 向量索引管理 =====================
644
441
  // 16. 构建向量索引
645
442
  server.tool('kg_build_index', '构建/重建向量语义索引(首次使用或索引损坏时调用)', {}, async () => {
646
- const nodes = await storage.listNodes(projectId);
647
- const count = await vector.buildIndex(projectId, nodes.map(n => ({
648
- id: n.id,
649
- title: n.title,
650
- description: n.description,
651
- categories: n.categories
652
- })));
653
- return wrap(projectId, `✅ 向量索引构建完成,共 ${count} 个节点`);
443
+ const count = await vectorManager.rebuildIndex(projectId);
444
+ return wrap(`✅ 向量索引构建完成,共 ${count} 个节点`);
654
445
  });
655
446
  // 17. 查看索引状态
656
447
  server.tool('kg_index_stats', '查看向量索引状态', {}, async () => {
657
448
  const stats = await vector.getIndexStats(projectId);
658
- return wrap(projectId, JSON.stringify({
449
+ const rebuildCheck = await vectorManager.needsRebuild(projectId);
450
+ return wrap(JSON.stringify({
659
451
  ...stats,
660
- tip: stats.count === 0 ? '索引为空,请运行 kg_build_index 构建索引' : '索引正常'
452
+ needsRebuild: rebuildCheck.needed,
453
+ rebuildReason: rebuildCheck.reason,
454
+ tip: rebuildCheck.needed
455
+ ? `索引需要重建,原因: ${rebuildCheck.reason}`
456
+ : '索引正常 (搜索时会自动检测并重建)'
661
457
  }, null, 2));
662
458
  });
663
459
  }
@@ -1,12 +1,24 @@
1
1
  /**
2
2
  * 向量检索服务 - 基于 transformers.js 的语义搜索
3
- * @version 0.1
3
+ * @version 0.2
4
4
  */
5
+ interface VectorConfig {
6
+ model: string;
7
+ quantized: boolean;
8
+ }
9
+ /**
10
+ * 获取向量配置 (从 ~/.ppdocs/config/vector.json 读取)
11
+ */
12
+ export declare function getVectorConfig(): VectorConfig;
5
13
  interface VectorSearchResult {
6
14
  id: string;
7
15
  title: string;
8
16
  similarity: number;
9
17
  }
18
+ /**
19
+ * 获取索引文件路径
20
+ */
21
+ export declare function getIndexPath(projectId: string): string;
10
22
  /**
11
23
  * 添加或更新节点向量
12
24
  */
@@ -1,13 +1,32 @@
1
1
  /**
2
2
  * 向量检索服务 - 基于 transformers.js 的语义搜索
3
- * @version 0.1
3
+ * @version 0.2
4
4
  */
5
5
  import { pipeline } from '@xenova/transformers';
6
6
  import * as fs from 'fs';
7
7
  import * as path from 'path';
8
- // 模型配置
9
- const MODEL_NAME = 'Xenova/multilingual-e5-small'; // 120MB, 384维, 100+语言(含中文)
8
+ // 默认模型配置
9
+ const DEFAULT_MODEL = 'Xenova/multilingual-e5-small';
10
10
  const VECTOR_DIM = 384;
11
+ // 当前加载的模型名
12
+ let currentModel = null;
13
+ /**
14
+ * 获取向量配置 (从 ~/.ppdocs/config/vector.json 读取)
15
+ */
16
+ export function getVectorConfig() {
17
+ const baseDir = process.env.PPDOCS_DATA_DIR || path.join(process.env.HOME || process.env.USERPROFILE || '', '.ppdocs');
18
+ const configPath = path.join(baseDir, 'config', 'vector.json');
19
+ try {
20
+ if (fs.existsSync(configPath)) {
21
+ const content = fs.readFileSync(configPath, 'utf-8');
22
+ return JSON.parse(content);
23
+ }
24
+ }
25
+ catch (e) {
26
+ console.warn('[Vector] Failed to load config:', e);
27
+ }
28
+ return { model: DEFAULT_MODEL, quantized: true };
29
+ }
11
30
  // 单例模式
12
31
  let extractor = null;
13
32
  let isLoading = false;
@@ -18,15 +37,19 @@ const vectorIndex = new Map();
18
37
  * 加载嵌入模型 (懒加载, 首次调用时下载)
19
38
  */
20
39
  async function getExtractor() {
21
- if (extractor)
40
+ const config = getVectorConfig();
41
+ // 如果模型变更,重新加载
42
+ if (extractor && currentModel === config.model) {
22
43
  return extractor;
23
- if (isLoading && loadPromise) {
44
+ }
45
+ if (isLoading && loadPromise && currentModel === config.model) {
24
46
  return loadPromise;
25
47
  }
26
48
  isLoading = true;
27
- console.log('[Vector] Loading embedding model...');
28
- loadPromise = pipeline('feature-extraction', MODEL_NAME, {
29
- quantized: true, // 使用量化版本,更小更快
49
+ currentModel = config.model;
50
+ console.log(`[Vector] Loading embedding model: ${config.model}...`);
51
+ loadPromise = pipeline('feature-extraction', config.model, {
52
+ quantized: config.quantized,
30
53
  });
31
54
  extractor = await loadPromise;
32
55
  isLoading = false;
@@ -61,7 +84,7 @@ function cosineSimilarity(a, b) {
61
84
  /**
62
85
  * 获取索引文件路径
63
86
  */
64
- function getIndexPath(projectId) {
87
+ export function getIndexPath(projectId) {
65
88
  const baseDir = process.env.PPDOCS_DATA_DIR || path.join(process.env.HOME || process.env.USERPROFILE || '', '.ppdocs');
66
89
  return path.join(baseDir, 'projects', projectId, 'vector-index.json');
67
90
  }
@@ -100,9 +123,10 @@ async function saveIndex(projectId) {
100
123
  if (!fs.existsSync(dir)) {
101
124
  fs.mkdirSync(dir, { recursive: true });
102
125
  }
126
+ const config = getVectorConfig();
103
127
  fs.writeFileSync(indexPath, JSON.stringify({
104
128
  version: 1,
105
- model: MODEL_NAME,
129
+ model: config.model,
106
130
  dimension: VECTOR_DIM,
107
131
  updatedAt: new Date().toISOString(),
108
132
  entries
@@ -188,9 +212,10 @@ export async function buildIndex(projectId, nodes) {
188
212
  */
189
213
  export async function getIndexStats(projectId) {
190
214
  const entries = await loadIndex(projectId);
215
+ const config = getVectorConfig();
191
216
  return {
192
217
  count: entries.length,
193
- model: MODEL_NAME,
218
+ model: config.model,
194
219
  dimension: VECTOR_DIM,
195
220
  loaded: extractor !== null
196
221
  };
@@ -0,0 +1,39 @@
1
+ /**
2
+ * 向量索引管理器
3
+ * 负责自动检测、重建索引逻辑
4
+ * @version 0.1
5
+ */
6
+ interface RebuildReason {
7
+ needed: boolean;
8
+ reason: 'missing' | 'model_mismatch' | 'empty' | 'none';
9
+ }
10
+ interface RebuildResult {
11
+ projectId: string;
12
+ count: number;
13
+ reason: string;
14
+ }
15
+ /**
16
+ * 检测索引是否需要重建
17
+ */
18
+ export declare function needsRebuild(projectId: string): Promise<RebuildReason>;
19
+ /**
20
+ * 强制重建项目索引
21
+ */
22
+ export declare function rebuildIndex(projectId: string): Promise<number>;
23
+ /**
24
+ * 确保索引存在 (自动检测 + 按需构建)
25
+ */
26
+ export declare function ensureIndex(projectId: string): Promise<void>;
27
+ /**
28
+ * 重建所有项目索引
29
+ */
30
+ export declare function rebuildAllIndexes(): Promise<RebuildResult[]>;
31
+ /**
32
+ * 获取所有项目的索引状态
33
+ */
34
+ export declare function getAllIndexStats(): Promise<Array<{
35
+ projectId: string;
36
+ status: RebuildReason;
37
+ nodeCount: number;
38
+ }>>;
39
+ export {};
@@ -0,0 +1,166 @@
1
+ /**
2
+ * 向量索引管理器
3
+ * 负责自动检测、重建索引逻辑
4
+ * @version 0.1
5
+ */
6
+ import * as fs from 'fs';
7
+ import * as path from 'path';
8
+ import * as vectorCore from './index.js';
9
+ // ==================== 检测函数 ====================
10
+ /**
11
+ * 检测索引是否需要重建
12
+ */
13
+ export async function needsRebuild(projectId) {
14
+ const indexPath = vectorCore.getIndexPath(projectId);
15
+ const config = vectorCore.getVectorConfig();
16
+ // 1. 索引文件不存在
17
+ if (!fs.existsSync(indexPath)) {
18
+ return { needed: true, reason: 'missing' };
19
+ }
20
+ // 2. 读取索引元数据
21
+ try {
22
+ const data = JSON.parse(fs.readFileSync(indexPath, 'utf-8'));
23
+ // 3. 模型不匹配
24
+ if (data.model !== config.model) {
25
+ return { needed: true, reason: 'model_mismatch' };
26
+ }
27
+ // 4. 索引为空
28
+ if (!data.entries || data.entries.length === 0) {
29
+ return { needed: true, reason: 'empty' };
30
+ }
31
+ return { needed: false, reason: 'none' };
32
+ }
33
+ catch {
34
+ // 读取失败视为需要重建
35
+ return { needed: true, reason: 'missing' };
36
+ }
37
+ }
38
+ // ==================== 重建函数 ====================
39
+ /**
40
+ * 获取节点数据 (用于重建索引)
41
+ */
42
+ async function fetchNodesForIndex(projectId) {
43
+ // 从项目目录读取节点文件
44
+ const baseDir = process.env.PPDOCS_DATA_DIR || path.join(process.env.HOME || process.env.USERPROFILE || '', '.ppdocs');
45
+ const nodesDir = path.join(baseDir, 'projects', projectId, 'nodes');
46
+ if (!fs.existsSync(nodesDir)) {
47
+ console.log(`[VectorManager] No nodes directory found for project ${projectId}`);
48
+ return [];
49
+ }
50
+ const nodes = [];
51
+ try {
52
+ const files = fs.readdirSync(nodesDir).filter(f => f.endsWith('.json'));
53
+ for (const file of files) {
54
+ try {
55
+ const content = fs.readFileSync(path.join(nodesDir, file), 'utf-8');
56
+ const node = JSON.parse(content);
57
+ // 跳过根节点
58
+ if (node.isOrigin || node.id === 'root')
59
+ continue;
60
+ nodes.push({
61
+ id: node.id,
62
+ title: node.title || '',
63
+ description: node.description || '',
64
+ categories: node.categories || node.tags || []
65
+ });
66
+ }
67
+ catch {
68
+ // 跳过无法解析的文件
69
+ }
70
+ }
71
+ }
72
+ catch (e) {
73
+ console.warn(`[VectorManager] Failed to read nodes:`, e);
74
+ }
75
+ return nodes;
76
+ }
77
+ /**
78
+ * 强制重建项目索引
79
+ */
80
+ export async function rebuildIndex(projectId) {
81
+ console.log(`[VectorManager] Rebuilding index for project: ${projectId}`);
82
+ const nodes = await fetchNodesForIndex(projectId);
83
+ if (nodes.length === 0) {
84
+ console.log(`[VectorManager] No nodes to index`);
85
+ return 0;
86
+ }
87
+ return vectorCore.buildIndex(projectId, nodes);
88
+ }
89
+ /**
90
+ * 确保索引存在 (自动检测 + 按需构建)
91
+ */
92
+ export async function ensureIndex(projectId) {
93
+ const check = await needsRebuild(projectId);
94
+ if (check.needed) {
95
+ console.log(`[VectorManager] Auto-rebuilding index, reason: ${check.reason}`);
96
+ await rebuildIndex(projectId);
97
+ }
98
+ }
99
+ // ==================== 批量操作 ====================
100
+ /**
101
+ * 获取所有项目 ID
102
+ */
103
+ function listAllProjects() {
104
+ const baseDir = process.env.PPDOCS_DATA_DIR || path.join(process.env.HOME || process.env.USERPROFILE || '', '.ppdocs');
105
+ const projectsDir = path.join(baseDir, 'projects');
106
+ if (!fs.existsSync(projectsDir)) {
107
+ return [];
108
+ }
109
+ try {
110
+ return fs.readdirSync(projectsDir)
111
+ .filter(name => {
112
+ const stat = fs.statSync(path.join(projectsDir, name));
113
+ return stat.isDirectory() && name.startsWith('project-');
114
+ });
115
+ }
116
+ catch {
117
+ return [];
118
+ }
119
+ }
120
+ /**
121
+ * 重建所有项目索引
122
+ */
123
+ export async function rebuildAllIndexes() {
124
+ const projects = listAllProjects();
125
+ console.log(`[VectorManager] Rebuilding indexes for ${projects.length} projects...`);
126
+ const results = [];
127
+ for (const projectId of projects) {
128
+ try {
129
+ const check = await needsRebuild(projectId);
130
+ const count = await rebuildIndex(projectId);
131
+ results.push({
132
+ projectId,
133
+ count,
134
+ reason: check.reason
135
+ });
136
+ console.log(`[VectorManager] Project ${projectId}: ${count} nodes indexed`);
137
+ }
138
+ catch (e) {
139
+ console.error(`[VectorManager] Failed to rebuild ${projectId}:`, e);
140
+ results.push({
141
+ projectId,
142
+ count: 0,
143
+ reason: 'error'
144
+ });
145
+ }
146
+ }
147
+ console.log(`[VectorManager] All indexes rebuilt`);
148
+ return results;
149
+ }
150
+ /**
151
+ * 获取所有项目的索引状态
152
+ */
153
+ export async function getAllIndexStats() {
154
+ const projects = listAllProjects();
155
+ const stats = [];
156
+ for (const projectId of projects) {
157
+ const status = await needsRebuild(projectId);
158
+ const indexStats = await vectorCore.getIndexStats(projectId);
159
+ stats.push({
160
+ projectId,
161
+ status,
162
+ nodeCount: indexStats.count
163
+ });
164
+ }
165
+ return stats;
166
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ppdocs/mcp",
3
- "version": "2.6.27",
3
+ "version": "2.6.29",
4
4
  "description": "ppdocs MCP Server - Knowledge Graph for Claude",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",