@ppdocs/mcp 3.2.36 → 3.2.38

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.
@@ -135,14 +135,133 @@ function collectNeighborLayers(chart, startNodeId, maxDepth) {
135
135
  }
136
136
  return layers;
137
137
  }
138
+ function normalizeText(value) {
139
+ return (value ?? '').trim().toLowerCase();
140
+ }
141
+ function scoreNodeMatch(node, terms) {
142
+ const label = normalizeText(node.label);
143
+ const id = normalizeText(node.id);
144
+ const description = normalizeText(node.description);
145
+ const nodeType = normalizeText(node.nodeType);
146
+ const domain = normalizeText(node.domain);
147
+ const inputs = ensureArray(node.input).join(' ').toLowerCase();
148
+ const outputs = ensureArray(node.output).join(' ').toLowerCase();
149
+ const files = ensureArray(node.boundFiles).join(' ').toLowerCase();
150
+ const dirs = ensureArray(node.boundDirs).join(' ').toLowerCase();
151
+ const docs = ensureArray(node.boundDocs).join(' ').toLowerCase();
152
+ const tasks = ensureArray(node.boundTasks).join(' ').toLowerCase();
153
+ const docEntries = (node.docEntries ?? [])
154
+ .flatMap((entry) => [entry.summary, entry.content])
155
+ .join(' ')
156
+ .toLowerCase();
157
+ let score = 0;
158
+ let matchedTerms = 0;
159
+ for (const term of terms) {
160
+ let matched = false;
161
+ if (label === term || id === term) {
162
+ score += 120;
163
+ matched = true;
164
+ }
165
+ else if (label.includes(term)) {
166
+ score += 80;
167
+ matched = true;
168
+ }
169
+ else if (id.includes(term)) {
170
+ score += 70;
171
+ matched = true;
172
+ }
173
+ if (description.includes(term)) {
174
+ score += 40;
175
+ matched = true;
176
+ }
177
+ if (nodeType.includes(term) || domain.includes(term)) {
178
+ score += 25;
179
+ matched = true;
180
+ }
181
+ if (inputs.includes(term) || outputs.includes(term)) {
182
+ score += 20;
183
+ matched = true;
184
+ }
185
+ if (files.includes(term) || dirs.includes(term)) {
186
+ score += 18;
187
+ matched = true;
188
+ }
189
+ if (docs.includes(term) || tasks.includes(term)) {
190
+ score += 15;
191
+ matched = true;
192
+ }
193
+ if (docEntries.includes(term)) {
194
+ score += 10;
195
+ matched = true;
196
+ }
197
+ if (matched) {
198
+ matchedTerms += 1;
199
+ }
200
+ }
201
+ if (matchedTerms === 0) {
202
+ return 0;
203
+ }
204
+ return score + matchedTerms * 100;
205
+ }
206
+ function collectNodeRelations(chart, nodeId) {
207
+ const nodeMap = new Map(chart.nodes.map((node) => [node.id, node]));
208
+ const incoming = chart.edges
209
+ .filter((edge) => edge.to === nodeId)
210
+ .map((edge) => ({ edge, node: nodeMap.get(edge.from) }));
211
+ const outgoing = chart.edges
212
+ .filter((edge) => edge.from === nodeId)
213
+ .map((edge) => ({ edge, node: nodeMap.get(edge.to) }));
214
+ return { incoming, outgoing };
215
+ }
216
+ function findDirectedPath(chart, fromId, toId) {
217
+ if (fromId === toId) {
218
+ return [];
219
+ }
220
+ const visited = new Set([fromId]);
221
+ const queue = [fromId];
222
+ const prev = new Map();
223
+ while (queue.length > 0) {
224
+ const current = queue.shift();
225
+ const nextEdges = chart.edges.filter((edge) => edge.from === current);
226
+ for (const edge of nextEdges) {
227
+ if (visited.has(edge.to)) {
228
+ continue;
229
+ }
230
+ visited.add(edge.to);
231
+ prev.set(edge.to, { parent: current, edge });
232
+ if (edge.to === toId) {
233
+ const path = [];
234
+ let cursor = toId;
235
+ while (cursor !== fromId) {
236
+ const step = prev.get(cursor);
237
+ if (!step) {
238
+ return null;
239
+ }
240
+ path.push(step.edge);
241
+ cursor = step.parent;
242
+ }
243
+ return path.reverse();
244
+ }
245
+ queue.push(edge.to);
246
+ }
247
+ }
248
+ return null;
249
+ }
138
250
  export function registerFlowchartTools(server, _ctx) {
139
251
  const client = () => getClient();
140
- server.tool('kg_flowchart', 'Logical flowchart operations: list|get|get_node|update_node|delete_node|batch_add|bind|unbind|orphans|health|create_chart|delete_chart', {
252
+ server.tool('kg_flowchart', '🔀 逻辑流程图 项目知识图谱的核心。每个节点代表一个模块/函数/概念,节点可绑定文件、内嵌版本化文档。\n' +
253
+ '⚡ 开始任何任务前必须先查图谱:search 搜关键词 → get_node 看详情 → 有 subFlowchart 则递归下探。\n' +
254
+ '📝 完成修改后必须回写:bind 绑定文件 → update_node 更新描述和文档 → 新模块用 batch_add。\n' +
255
+ 'actions: list|get|search|get_relations|find_path|get_node|update_node|delete_node|batch_add|bind|unbind|orphans|health|create_chart|delete_chart', {
141
256
  action: z
142
- .enum(['list', 'get', 'get_node', 'update_node', 'delete_node', 'batch_add', 'bind', 'unbind', 'orphans', 'health', 'create_chart', 'delete_chart'])
257
+ .enum(['list', 'get', 'search', 'get_relations', 'find_path', 'get_node', 'update_node', 'delete_node', 'batch_add', 'bind', 'unbind', 'orphans', 'health', 'create_chart', 'delete_chart'])
143
258
  .describe('action type'),
144
259
  chartId: z.string().optional().describe('chart id, default main'),
145
- nodeId: z.string().optional().describe('node id for get_node/update_node/delete_node/bind/unbind'),
260
+ nodeId: z.string().optional().describe('node id for get_relations/get_node/update_node/delete_node/bind/unbind'),
261
+ query: z.string().optional().describe('search query for action=search'),
262
+ fromId: z.string().optional().describe('start node id for action=find_path'),
263
+ toId: z.string().optional().describe('target node id for action=find_path'),
264
+ limit: z.number().optional().describe('search result limit, default 10'),
146
265
  expand: z.number().optional().describe('get_node expansion depth, default 3'),
147
266
  includeDocs: z.boolean().optional().describe('show bound docs in get_node, default true'),
148
267
  includeTasks: z.boolean().optional().describe('show bound tasks in get_node, default true'),
@@ -200,6 +319,131 @@ export function registerFlowchartTools(server, _ctx) {
200
319
  ].filter((line) => line.length > 0);
201
320
  return wrap(lines.join('\n'));
202
321
  }
322
+ case 'search': {
323
+ const rawQuery = decoded.query?.trim();
324
+ if (!rawQuery) {
325
+ return wrap('search requires query.');
326
+ }
327
+ const terms = rawQuery
328
+ .toLowerCase()
329
+ .split(/\s+/)
330
+ .filter(Boolean);
331
+ const charts = decoded.chartId
332
+ ? [(await client().getFlowchart(decoded.chartId))].filter((item) => Boolean(item))
333
+ : await (async () => {
334
+ const briefs = (await client().listFlowcharts());
335
+ const loaded = await Promise.all(briefs.map(async (brief) => (await client().getFlowchart(brief.id))));
336
+ return loaded.filter((item) => Boolean(item));
337
+ })();
338
+ const matches = charts
339
+ .flatMap((chart) => chart.nodes.map((rawNode) => {
340
+ const node = ensureNodeShape(rawNode);
341
+ return {
342
+ chart,
343
+ node,
344
+ score: scoreNodeMatch(node, terms),
345
+ };
346
+ }))
347
+ .filter((item) => item.score > 0)
348
+ .sort((a, b) => b.score - a.score)
349
+ .slice(0, Math.max(1, decoded.limit ?? 10));
350
+ if (matches.length === 0) {
351
+ return wrap(`No flowchart nodes matched query: ${rawQuery}`);
352
+ }
353
+ const lines = [
354
+ `Search results for: ${rawQuery}`,
355
+ '',
356
+ ...matches.map(({ chart, node }) => {
357
+ const badges = [
358
+ node.domain ? `domain=${node.domain}` : '',
359
+ node.nodeType ? `type=${node.nodeType}` : '',
360
+ node.subFlowchart ? `sub=${node.subFlowchart}` : '',
361
+ ].filter(Boolean);
362
+ const suffix = badges.length > 0 ? ` | ${badges.join(' ')}` : '';
363
+ const description = node.description ? ` | ${node.description}` : '';
364
+ return `- ${node.label} [${chart.id}/${node.id}]${suffix}${description}`;
365
+ }),
366
+ ];
367
+ return wrap(lines.join('\n'));
368
+ }
369
+ case 'get_relations': {
370
+ const chartId = decoded.chartId || 'main';
371
+ if (!decoded.nodeId) {
372
+ return wrap('get_relations requires nodeId.');
373
+ }
374
+ const chart = (await client().getFlowchart(chartId));
375
+ if (!chart) {
376
+ return wrap(`Flowchart not found: ${chartId}`);
377
+ }
378
+ const nodeMap = new Map(chart.nodes.map((node) => [node.id, node]));
379
+ const node = nodeMap.get(decoded.nodeId);
380
+ if (!node) {
381
+ return wrap(`Node not found: ${decoded.nodeId} in ${chartId}`);
382
+ }
383
+ const relations = collectNodeRelations(chart, decoded.nodeId);
384
+ const lines = [
385
+ `Relations: ${node.label} [${chartId}/${node.id}]`,
386
+ `Incoming: ${relations.incoming.length}`,
387
+ `Outgoing: ${relations.outgoing.length}`,
388
+ ];
389
+ if (relations.incoming.length > 0) {
390
+ lines.push('', 'Incoming edges:');
391
+ relations.incoming.forEach(({ edge, node: source }) => {
392
+ lines.push(`- ${source?.label ?? edge.from} [${edge.from}] -> ${node.label} [${edge.to}]${edge.label ? ` (${edge.label})` : ''}${edge.edgeType ? ` [${edge.edgeType}]` : ''}`);
393
+ });
394
+ }
395
+ if (relations.outgoing.length > 0) {
396
+ lines.push('', 'Outgoing edges:');
397
+ relations.outgoing.forEach(({ edge, node: target }) => {
398
+ lines.push(`- ${node.label} [${edge.from}] -> ${target?.label ?? edge.to} [${edge.to}]${edge.label ? ` (${edge.label})` : ''}${edge.edgeType ? ` [${edge.edgeType}]` : ''}`);
399
+ });
400
+ }
401
+ const linked = new Set([
402
+ ...relations.incoming.map(({ edge }) => edge.from),
403
+ ...relations.outgoing.map(({ edge }) => edge.to),
404
+ ]);
405
+ if (linked.size > 0) {
406
+ lines.push('', `Linked nodes: ${linked.size}`);
407
+ }
408
+ return wrap(lines.join('\n'));
409
+ }
410
+ case 'find_path': {
411
+ const chartId = decoded.chartId || 'main';
412
+ if (!decoded.fromId || !decoded.toId) {
413
+ return wrap('find_path requires fromId and toId.');
414
+ }
415
+ const chart = (await client().getFlowchart(chartId));
416
+ if (!chart) {
417
+ return wrap(`Flowchart not found: ${chartId}`);
418
+ }
419
+ const nodeMap = new Map(chart.nodes.map((node) => [node.id, node]));
420
+ const fromNode = nodeMap.get(decoded.fromId);
421
+ const toNode = nodeMap.get(decoded.toId);
422
+ if (!fromNode || !toNode) {
423
+ return wrap(`find_path could not resolve nodes in ${chartId}: from=${decoded.fromId} to=${decoded.toId}`);
424
+ }
425
+ const path = findDirectedPath(chart, decoded.fromId, decoded.toId);
426
+ if (path === null) {
427
+ return wrap(`No directed path found: [${chartId}/${decoded.fromId}] -> [${chartId}/${decoded.toId}]`);
428
+ }
429
+ if (path.length === 0) {
430
+ return wrap(`Path resolved: ${fromNode.label} [${fromNode.id}] (same node)`);
431
+ }
432
+ const lines = [
433
+ `Directed path: ${fromNode.label} [${fromNode.id}] -> ${toNode.label} [${toNode.id}]`,
434
+ `Hops: ${path.length}`,
435
+ '',
436
+ ];
437
+ let current = fromNode;
438
+ path.forEach((edge, index) => {
439
+ const nextNode = nodeMap.get(edge.to);
440
+ lines.push(`${index + 1}. ${current.label} [${edge.from}] -> ${nextNode?.label ?? edge.to} [${edge.to}]${edge.label ? ` (${edge.label})` : ''}${edge.edgeType ? ` [${edge.edgeType}]` : ''}`);
441
+ if (nextNode) {
442
+ current = nextNode;
443
+ }
444
+ });
445
+ return wrap(lines.join('\n'));
446
+ }
203
447
  case 'get_node': {
204
448
  const chartId = decoded.chartId || 'main';
205
449
  if (!decoded.nodeId) {
@@ -1,11 +1,11 @@
1
1
  /**
2
2
  * MCP 工具注册入口
3
- * 12 个工具, 6 个子模块
3
+ * 13 个工具, 7 个子模块
4
4
  *
5
5
  * 🔗 初始化: kg_init (1个)
6
6
  * 📊 导航: kg_status (1个)
7
- * 📚 知识: kg_projects, kg_rules (2个)
8
- * 📝 工作流: kg_task(任务记录), kg_files, kg_discuss(讨论区) (3个)
7
+ * 📚 知识: kg_projects, kg_workflow (2个)
8
+ * 📝 工作流: kg_task(任务记录), kg_files, kg_discuss(讨论区), kg_ref (4个)
9
9
  * 🔀 关系核心: kg_flowchart(逻辑流程图 — 关系型知识锚点) (1个)
10
10
  * 🔬 代码分析: code_scan, code_smart_context, code_full_path (3个)
11
11
  * 🏛️ 协作: kg_meeting (1个)
@@ -1,11 +1,11 @@
1
1
  /**
2
2
  * MCP 工具注册入口
3
- * 12 个工具, 6 个子模块
3
+ * 13 个工具, 7 个子模块
4
4
  *
5
5
  * 🔗 初始化: kg_init (1个)
6
6
  * 📊 导航: kg_status (1个)
7
- * 📚 知识: kg_projects, kg_rules (2个)
8
- * 📝 工作流: kg_task(任务记录), kg_files, kg_discuss(讨论区) (3个)
7
+ * 📚 知识: kg_projects, kg_workflow (2个)
8
+ * 📝 工作流: kg_task(任务记录), kg_files, kg_discuss(讨论区), kg_ref (4个)
9
9
  * 🔀 关系核心: kg_flowchart(逻辑流程图 — 关系型知识锚点) (1个)
10
10
  * 🔬 代码分析: code_scan, code_smart_context, code_full_path (3个)
11
11
  * 🏛️ 协作: kg_meeting (1个)
@@ -14,10 +14,11 @@ import { createContext } from './shared.js';
14
14
  import { registerInitTool } from './init.js';
15
15
  import { registerStatusTool } from './kg_status.js';
16
16
  import { registerProjectTools } from './projects.js';
17
- import { registerRuleTools } from './rules.js';
17
+ import { registerWorkflowTools } from './workflow.js';
18
18
  import { registerTaskTools } from './tasks.js';
19
19
  import { registerFileTools } from './files.js';
20
20
  import { registerDiscussionTools } from './discussion.js';
21
+ import { registerReferenceTools } from './refs.js';
21
22
  import { registerAnalyzerTools } from './analyzer.js';
22
23
  import { registerMeetingTools } from './meeting.js';
23
24
  import { registerFlowchartTools } from './flowchart.js';
@@ -29,11 +30,12 @@ export function registerTools(server, projectId, user, onProjectChange) {
29
30
  registerStatusTool(server, ctx);
30
31
  // 📚 知识
31
32
  registerProjectTools(server, ctx);
32
- registerRuleTools(server, ctx);
33
+ registerWorkflowTools(server, ctx);
33
34
  // 📝 工作流
34
35
  registerTaskTools(server, ctx);
35
36
  registerFileTools(server);
36
37
  registerDiscussionTools(server, ctx);
38
+ registerReferenceTools(server);
37
39
  // 🔬 代码分析
38
40
  registerAnalyzerTools(server, ctx);
39
41
  // 🏛️ 多AI协作
@@ -0,0 +1,2 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function registerReferenceTools(server: McpServer): void;
@@ -0,0 +1,123 @@
1
+ import { z } from 'zod';
2
+ import { getClient } from '../storage/httpClient.js';
3
+ import { decodeObjectStrings } from '../utils.js';
4
+ import { safeTool, wrap } from './shared.js';
5
+ const linkSchema = z.object({
6
+ url: z.string(),
7
+ label: z.string(),
8
+ });
9
+ const fileSchema = z.object({
10
+ name: z.string(),
11
+ path: z.string(),
12
+ });
13
+ const adoptedNodeSchema = z.object({
14
+ chartId: z.string(),
15
+ nodeId: z.string(),
16
+ nodeLabel: z.string(),
17
+ });
18
+ export function registerReferenceTools(server) {
19
+ const client = () => getClient();
20
+ server.tool('kg_ref', '📎 外部参考 — 管理项目引用的第三方资料、链接、脚本和样例文件。\n' +
21
+ '涉及参考资料时主动调用:list 查看已有 → get 读详情 → read_file 读附件内容 → save 新增。\n' +
22
+ 'actions: list|get|save|delete|read_file', {
23
+ action: z.enum(['list', 'get', 'save', 'delete', 'read_file']).describe('操作类型'),
24
+ id: z.string().optional().describe('参考 ID'),
25
+ title: z.string().optional().describe('参考标题 (save)'),
26
+ summary: z.string().optional().describe('Markdown 总结 (save)'),
27
+ links: z.array(linkSchema).optional().describe('参考链接 (save)'),
28
+ files: z.array(fileSchema).optional().describe('参考文件 (save)'),
29
+ scripts: z.array(fileSchema).optional().describe('解析脚本 (save)'),
30
+ adoptedBy: z.array(adoptedNodeSchema).optional().describe('已采用该参考的节点 (save)'),
31
+ path: z.string().optional().describe('参考文件路径 (read_file)'),
32
+ }, async (args) => safeTool(async () => {
33
+ const decoded = decodeObjectStrings(args);
34
+ switch (decoded.action) {
35
+ case 'list': {
36
+ const refs = await client().listReferences();
37
+ if (refs.length === 0)
38
+ return wrap('当前没有外部参考');
39
+ const lines = [
40
+ `📎 外部参考 (${refs.length})`,
41
+ '',
42
+ '| ID | 标题 | 链接 | 文件 | 已采用 |',
43
+ '|:---|:---|---:|---:|---:|',
44
+ ];
45
+ for (const ref of refs) {
46
+ lines.push(`| ${ref.id} | ${ref.title} | ${ref.links.length} | ${ref.files.length} | ${ref.adoptedBy.length} |`);
47
+ }
48
+ return wrap(lines.join('\n'));
49
+ }
50
+ case 'get': {
51
+ if (!decoded.id)
52
+ return wrap('❌ get 需要 id');
53
+ const ref = await client().getReference(decoded.id);
54
+ if (!ref)
55
+ return wrap(`未找到外部参考: ${decoded.id}`);
56
+ const lines = [
57
+ `# ${ref.title}`,
58
+ '',
59
+ `- ID: ${ref.id}`,
60
+ `- Links: ${ref.links.length}`,
61
+ `- Files: ${ref.files.length}`,
62
+ `- Scripts: ${ref.scripts.length}`,
63
+ `- AdoptedBy: ${ref.adoptedBy.length}`,
64
+ '',
65
+ ref.summary || '(无摘要)',
66
+ ];
67
+ if (ref.links.length > 0) {
68
+ lines.push('', '## Links');
69
+ for (const link of ref.links) {
70
+ lines.push(`- ${link.label}: ${link.url}`);
71
+ }
72
+ }
73
+ if (ref.files.length > 0) {
74
+ lines.push('', '## Files');
75
+ for (const file of ref.files) {
76
+ lines.push(`- ${file.name}: ${file.path}`);
77
+ }
78
+ }
79
+ if (ref.scripts.length > 0) {
80
+ lines.push('', '## Scripts');
81
+ for (const script of ref.scripts) {
82
+ lines.push(`- ${script.name}: ${script.path}`);
83
+ }
84
+ }
85
+ return wrap(lines.join('\n'));
86
+ }
87
+ case 'save': {
88
+ if (!decoded.id)
89
+ return wrap('❌ save 需要 id');
90
+ if (!decoded.title)
91
+ return wrap('❌ save 需要 title');
92
+ const existing = await client().getReference(decoded.id);
93
+ const now = new Date().toISOString();
94
+ await client().saveReference({
95
+ id: decoded.id,
96
+ title: decoded.title,
97
+ summary: decoded.summary || existing?.summary || '',
98
+ links: decoded.links || existing?.links || [],
99
+ files: decoded.files || existing?.files || [],
100
+ scripts: decoded.scripts || existing?.scripts || [],
101
+ adoptedBy: decoded.adoptedBy || existing?.adoptedBy || [],
102
+ createdAt: existing?.createdAt || now,
103
+ updatedAt: now,
104
+ });
105
+ return wrap(`✅ 外部参考已保存 (${decoded.id})`);
106
+ }
107
+ case 'delete': {
108
+ if (!decoded.id)
109
+ return wrap('❌ delete 需要 id');
110
+ const ok = await client().deleteReference(decoded.id);
111
+ return wrap(ok ? `✅ 外部参考已删除 (${decoded.id})` : `ℹ️ 未找到外部参考 (${decoded.id})`);
112
+ }
113
+ case 'read_file': {
114
+ if (!decoded.path)
115
+ return wrap('❌ read_file 需要 path');
116
+ const content = await client().readReferenceFile(decoded.path);
117
+ return wrap(content);
118
+ }
119
+ default:
120
+ return wrap(`❌ 未知 action: ${decoded.action}`);
121
+ }
122
+ }));
123
+ }
@@ -184,14 +184,14 @@ export function registerTaskTools(server, ctx) {
184
184
  return wrap('归档失败(任务不存在或已归档)');
185
185
  return wrap(`✅ 任务已归档: ${task.title}`);
186
186
  }
187
- default:
188
- return wrap(`❌ 未知 action: ${decoded.action}`);
189
187
  case 'delete': {
190
188
  if (!decoded.taskId)
191
189
  return wrap('❌ delete 需要 taskId');
192
190
  const ok = await client().deleteTask(decoded.taskId);
193
191
  return wrap(ok ? `✅ 任务已删除 (ID: ${decoded.taskId})` : '❌ 删除失败(任务不存在)');
194
192
  }
193
+ default:
194
+ return wrap(`❌ 未知 action: ${decoded.action}`);
195
195
  }
196
196
  }));
197
197
  }
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { type McpContext } from './shared.js';
3
+ export declare function registerWorkflowTools(server: McpServer, _ctx: McpContext): void;
@@ -0,0 +1,80 @@
1
+ import { z } from 'zod';
2
+ import { getClient } from '../storage/httpClient.js';
3
+ import { decodeObjectStrings } from '../utils.js';
4
+ import { safeTool, wrap } from './shared.js';
5
+ export function registerWorkflowTools(server, _ctx) {
6
+ const client = () => getClient();
7
+ server.tool('kg_workflow', '📘 文档工作流 — AI 的标准操作手册。每个工作流是一份 Markdown 文档,定义了特定场景下 AI 应该遵循的步骤和规范。\n' +
8
+ '调用方式:空参数=列出所有工作流;传 id=获取工作流正文;save/delete 需显式 action。\n' +
9
+ '典型用法:拿到任务后先 kg_workflow() 看有无相关工作流,有则 kg_workflow(id:"xxx") 读取并遵循执行。', {
10
+ action: z.enum(['list', 'get', 'save', 'delete']).optional().describe('省略时自动推断:无参数=list,有id=get'),
11
+ id: z.string().optional().describe('工作流 ID'),
12
+ scope: z.enum(['all', 'global', 'project']).optional().describe('范围:all(默认)|global|project'),
13
+ title: z.string().optional().describe('标题 (save 必填)'),
14
+ content: z.string().optional().describe('Markdown 正文 (save 必填)'),
15
+ }, async (args) => safeTool(async () => {
16
+ const decoded = decodeObjectStrings(args);
17
+ const action = decoded.action ?? (decoded.id ? 'get' : 'list');
18
+ if (!decoded.action && (decoded.title !== undefined || decoded.content !== undefined)) {
19
+ return wrap('❌ save/delete 需要显式传 action');
20
+ }
21
+ switch (action) {
22
+ case 'list': {
23
+ const workflows = await client().listWorkflows(decoded.scope ?? 'all');
24
+ if (workflows.length === 0)
25
+ return wrap('当前没有可用文档工作流');
26
+ const lines = [
27
+ `📘 文档工作流列表 (${workflows.length})`,
28
+ '',
29
+ '| ID | 范围 | 标题 |',
30
+ '|:---|:---|:---|',
31
+ ];
32
+ for (const w of workflows) {
33
+ lines.push(`| ${w.id} | ${w.scope} | ${w.title} |`);
34
+ }
35
+ return wrap(lines.join('\n'));
36
+ }
37
+ case 'get': {
38
+ if (!decoded.id)
39
+ return wrap('❌ get 需要 id');
40
+ const doc = await client().getWorkflow(decoded.id, decoded.scope ?? 'all');
41
+ if (!doc)
42
+ return wrap(`未找到工作流: ${decoded.id}`);
43
+ return wrap([
44
+ `# ${doc.title}`,
45
+ `> ID: ${doc.id} | 范围: ${doc.scope}`,
46
+ '',
47
+ doc.content,
48
+ ].join('\n'));
49
+ }
50
+ case 'save': {
51
+ if (!decoded.id)
52
+ return wrap('❌ save 需要 id');
53
+ if (!decoded.title)
54
+ return wrap('❌ save 需要 title');
55
+ if (!decoded.content)
56
+ return wrap('❌ save 需要 content');
57
+ const doc = await client().saveWorkflow(decoded.id, {
58
+ scope: decoded.scope === 'global' ? 'global' : 'project',
59
+ title: decoded.title,
60
+ description: '',
61
+ system: '',
62
+ keywords: [],
63
+ minHits: 0,
64
+ always: false,
65
+ content: decoded.content,
66
+ });
67
+ return wrap(`✅ 已保存: ${doc.scope}/${doc.id} — ${doc.title}`);
68
+ }
69
+ case 'delete': {
70
+ if (!decoded.id)
71
+ return wrap('❌ delete 需要 id');
72
+ const scope = decoded.scope === 'global' ? 'global' : 'project';
73
+ const ok = await client().deleteWorkflow(decoded.id, scope);
74
+ return wrap(ok ? `✅ 已删除 ${scope}/${decoded.id}` : `未找到 ${scope}/${decoded.id}`);
75
+ }
76
+ default:
77
+ return wrap(`❌ 未知 action: ${String(decoded.action)}`);
78
+ }
79
+ }));
80
+ }
package/dist/utils.d.ts CHANGED
@@ -1,12 +1,6 @@
1
1
  /**
2
2
  * MCP Server 工具函数
3
3
  */
4
- export type RuleType = string;
5
- export declare const RULE_TYPE_LABELS: Record<string, string>;
6
- /**
7
- * 获取指定类型的规则 (动态: 从 meta 获取标签)
8
- */
9
- export declare function getRules(projectId: string, ruleType?: string): Promise<string>;
10
4
  /**
11
5
  * 解码 Unicode 转义序列
12
6
  * 将 \uXXXX 格式的转义序列转换为实际字符
package/dist/utils.js CHANGED
@@ -1,50 +1,6 @@
1
1
  /**
2
2
  * MCP Server 工具函数
3
3
  */
4
- import { getClient } from './storage/httpClient.js';
5
- // 默认规则类型标签 (fallback)
6
- const DEFAULT_LABELS = {
7
- userStyles: '用户沟通规则',
8
- codeStyle: '编码风格规则',
9
- reviewRules: '代码审查规则',
10
- testRules: '错误分析规则',
11
- unitTests: '代码测试规则',
12
- };
13
- // 兼容导出
14
- export const RULE_TYPE_LABELS = DEFAULT_LABELS;
15
- /**
16
- * 将字符串数组格式化为 Markdown 列表
17
- */
18
- function formatRulesList(rules) {
19
- if (!rules || rules.length === 0)
20
- return '';
21
- return rules.map((s, i) => `${i + 1}. ${s}`).join('\n\n');
22
- }
23
- /**
24
- * 获取指定类型的规则 (动态: 从 meta 获取标签)
25
- */
26
- export async function getRules(projectId, ruleType) {
27
- const client = getClient();
28
- const meta = await client.getRulesMeta();
29
- if (ruleType) {
30
- const rules = await client.getRulesApi(ruleType);
31
- if (!rules || rules.length === 0)
32
- return '';
33
- const label = meta[ruleType]?.label || DEFAULT_LABELS[ruleType] || ruleType;
34
- return `[${label}]\n${formatRulesList(rules)}`;
35
- }
36
- // 返回所有规则 (从 meta 获取所有类型)
37
- const types = Object.keys(meta).length > 0 ? Object.keys(meta) : Object.keys(DEFAULT_LABELS);
38
- const allRules = [];
39
- for (const type of types) {
40
- const rules = await client.getRulesApi(type);
41
- if (rules && rules.length > 0) {
42
- const label = meta[type]?.label || DEFAULT_LABELS[type] || type;
43
- allRules.push(`[${label}]\n${formatRulesList(rules)}`);
44
- }
45
- }
46
- return allRules.join('\n\n---\n\n');
47
- }
48
4
  /**
49
5
  * 解码 Unicode 转义序列
50
6
  * 将 \uXXXX 格式的转义序列转换为实际字符
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ppdocs/mcp",
3
- "version": "3.2.36",
3
+ "version": "3.2.38",
4
4
  "description": "ppdocs MCP Server - Knowledge Graph for Claude",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -12,9 +12,10 @@ kg_discuss(action:"list") -> ???????
12
12
  ## 1. ????????
13
13
 
14
14
  - ??: `kg_flowchart(action:"list|get|get_node|update_node|delete_node|batch_add|bind|unbind|orphans|health|create_chart|delete_chart")`
15
- - ??: `kg_rules(action:"get|save|get_meta|save_meta")`
15
+ - ??: `kg_workflow()` / `kg_workflow(id:"...")` / `kg_workflow(action:"save|delete")`
16
16
  - ??: `kg_task(action:"create|get|update|archive|delete")`
17
17
  - ??: `kg_files(action:"list|read|upload|download|public_*")`
18
+ - ??: `kg_ref(action:"list|get|save|delete|read_file")`
18
19
  - ??/??: `kg_discuss(...)`, `kg_meeting(...)`
19
20
  - ????: `code_scan()`, `code_smart_context(symbolName)`, `code_full_path(symbolA, symbolB)`
20
21
  - ????: ?????????????????????????????? fallback????????????
@@ -22,7 +23,7 @@ kg_discuss(action:"list") -> ???????
22
23
  ## 2. ?????
23
24
 
24
25
  ### Step 1: ????
25
- 1. ?? `kg_flowchart(get)` ????????? `kg_rules(get)` ?????
26
+ 1. ?? `kg_flowchart(get)` ????????? `kg_workflow()` ?????
26
27
  2. ???????? `kg_flowchart(get_node, expand:2, includeDoc:true, includeFiles:true)`?
27
28
  3. ????? `subFlowchart`???????????????
28
29
  4. ??????????? `code_scan()`??? `code_smart_context` / `code_full_path`?