@ppdocs/mcp 3.2.22 → 3.2.23

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/cli.js CHANGED
@@ -412,30 +412,29 @@ function generateHooksConfig(cwd) {
412
412
  }
413
413
  };
414
414
  }
415
- /** 生成 MCP 权限配置 (允许全部 ppdocs-kg 方法) */
415
+ /** 生成 MCP 权限配置 (允许全部 ppdocs-kg 工具) */
416
416
  function generateMcpPermissions() {
417
417
  const mcpMethods = [
418
- // 知识图谱操作
419
- 'kg_create_node',
420
- 'kg_delete_node',
421
- 'kg_update_node',
422
- 'kg_update_root',
423
- 'kg_get_rules',
424
- 'kg_save_rules',
425
- 'kg_lock_node',
426
- 'kg_search',
427
- 'kg_find_path',
428
- 'kg_list_nodes',
429
- 'kg_get_relations',
430
- 'kg_read_node',
431
- 'kg_get_tree',
432
- 'kg_find_by_file',
433
- // 任务管理
434
- 'task_create',
435
- 'task_list',
436
- 'task_get',
437
- 'task_add_log',
438
- 'task_complete',
418
+ // 初始化 + 导航
419
+ 'kg_init',
420
+ 'kg_status',
421
+ 'kg_tree',
422
+ // 知识管理
423
+ 'kg_doc',
424
+ 'kg_projects',
425
+ 'kg_rules',
426
+ // 工作流
427
+ 'kg_task',
428
+ 'kg_files',
429
+ 'kg_discuss',
430
+ // 流程图
431
+ 'kg_flowchart',
432
+ // 协作
433
+ 'kg_meeting',
434
+ // 代码分析
435
+ 'code_scan',
436
+ 'code_smart_context',
437
+ 'code_full_path',
439
438
  ];
440
439
  return mcpMethods.map(m => `mcp__ppdocs-kg__${m}`);
441
440
  }
@@ -34,14 +34,25 @@ export declare class PpdocsApiClient {
34
34
  getGlobalRulesMeta(): Promise<Record<string, RuleMeta>>;
35
35
  saveGlobalRulesMeta(meta: Record<string, RuleMeta>): Promise<boolean>;
36
36
  listTasks(status?: 'active' | 'archived'): Promise<TaskSummary[]>;
37
- getTask(taskId: string): Promise<Task | null>;
37
+ getTask(taskId: string, mode?: 'smart' | 'full'): Promise<Task | null>;
38
38
  createTask(task: {
39
39
  title: string;
40
40
  description: string;
41
41
  goals: string[];
42
42
  }, creator: string): Promise<Task>;
43
- addTaskLog(taskId: string, logType: TaskLogType, content: string): Promise<Task | null>;
43
+ addTaskLog(taskId: string, content: string, logType?: TaskLogType): Promise<Task | null>;
44
44
  completeTask(taskId: string, experience: TaskExperience): Promise<Task | null>;
45
+ deleteTask(taskId: string): Promise<boolean>;
46
+ listFlowcharts(): Promise<unknown[]>;
47
+ getFlowchart(chartId: string): Promise<unknown>;
48
+ batchAddToFlowchart(chartId: string, nodes: unknown[], edges: unknown[]): Promise<unknown>;
49
+ saveFlowchart(chartId: string, data: unknown): Promise<unknown>;
50
+ /** 原子更新单个节点 (后端自动处理并发) */
51
+ updateFlowchartNode(chartId: string, nodeId: string, node: unknown, changeDesc?: string): Promise<unknown>;
52
+ /** 原子删除单个节点 (后端自动清理关联边) */
53
+ deleteFlowchartNode(chartId: string, nodeId: string): Promise<unknown>;
54
+ getFlowchartOrphans(): Promise<unknown[]>;
55
+ getFlowchartHealth(): Promise<unknown[]>;
45
56
  /** 列出所有可访问的项目 */
46
57
  crossListProjects(): Promise<{
47
58
  id: string;
@@ -108,10 +119,6 @@ export declare class PpdocsApiClient {
108
119
  discussionComplete(id: string): Promise<boolean>;
109
120
  /** 删除讨论 (公开路由) */
110
121
  discussionDelete(id: string): Promise<boolean>;
111
- /** 结案归档讨论 (认证路由) */
112
- discussionClose(id: string, conclusion: string): Promise<{
113
- archived_path: string;
114
- }>;
115
122
  /** 列出公共文件 */
116
123
  publicFilesList(dir?: string): Promise<FileInfo[]>;
117
124
  /** 读取公共文件 (文本) */
@@ -277,9 +277,10 @@ export class PpdocsApiClient {
277
277
  const query = status ? `?status=${status}` : '';
278
278
  return this.request(`/tasks${query}`);
279
279
  }
280
- async getTask(taskId) {
280
+ async getTask(taskId, mode) {
281
281
  try {
282
- return await this.request(`/tasks/${taskId}`);
282
+ const query = mode ? `?mode=${mode}` : '';
283
+ return await this.request(`/tasks/${taskId}${query}`);
283
284
  }
284
285
  catch {
285
286
  return null;
@@ -300,11 +301,14 @@ export class PpdocsApiClient {
300
301
  body: JSON.stringify(payload)
301
302
  });
302
303
  }
303
- async addTaskLog(taskId, logType, content) {
304
+ async addTaskLog(taskId, content, logType) {
304
305
  try {
306
+ const body = { content };
307
+ if (logType)
308
+ body.log_type = logType;
305
309
  return await this.request(`/tasks/${taskId}/logs`, {
306
310
  method: 'POST',
307
- body: JSON.stringify({ log_type: logType, content })
311
+ body: JSON.stringify(body)
308
312
  });
309
313
  }
310
314
  catch {
@@ -322,6 +326,58 @@ export class PpdocsApiClient {
322
326
  return null;
323
327
  }
324
328
  }
329
+ async deleteTask(taskId) {
330
+ try {
331
+ await this.request(`/tasks/${taskId}`, { method: 'DELETE' });
332
+ return true;
333
+ }
334
+ catch {
335
+ return false;
336
+ }
337
+ }
338
+ // ============ 流程图管理 ============
339
+ async listFlowcharts() {
340
+ return this.request('/flowcharts');
341
+ }
342
+ async getFlowchart(chartId) {
343
+ try {
344
+ return await this.request(`/flowcharts/${chartId}`);
345
+ }
346
+ catch {
347
+ return null;
348
+ }
349
+ }
350
+ async batchAddToFlowchart(chartId, nodes, edges) {
351
+ return this.request(`/flowcharts/${chartId}/batch`, {
352
+ method: 'POST',
353
+ body: JSON.stringify({ nodes, edges })
354
+ });
355
+ }
356
+ async saveFlowchart(chartId, data) {
357
+ return this.request(`/flowcharts/${chartId}`, {
358
+ method: 'PUT',
359
+ body: JSON.stringify(data)
360
+ });
361
+ }
362
+ /** 原子更新单个节点 (后端自动处理并发) */
363
+ async updateFlowchartNode(chartId, nodeId, node, changeDesc) {
364
+ return this.request(`/flowcharts/${chartId}/nodes/${nodeId}`, {
365
+ method: 'PUT',
366
+ body: JSON.stringify({ node, change_desc: changeDesc || 'MCP update' })
367
+ });
368
+ }
369
+ /** 原子删除单个节点 (后端自动清理关联边) */
370
+ async deleteFlowchartNode(chartId, nodeId) {
371
+ return this.request(`/flowcharts/${chartId}/nodes/${nodeId}`, {
372
+ method: 'DELETE'
373
+ });
374
+ }
375
+ async getFlowchartOrphans() {
376
+ return this.request('/flowcharts/orphans');
377
+ }
378
+ async getFlowchartHealth() {
379
+ return this.request('/flowcharts/health');
380
+ }
325
381
  // ============ 跨项目只读访问 ============
326
382
  /** 列出所有可访问的项目 */
327
383
  async crossListProjects() {
@@ -539,13 +595,6 @@ export class PpdocsApiClient {
539
595
  method: 'DELETE',
540
596
  });
541
597
  }
542
- /** 结案归档讨论 (认证路由) */
543
- async discussionClose(id, conclusion) {
544
- return this.request(`/discussions/${encodeURIComponent(id)}/close`, {
545
- method: 'POST',
546
- body: JSON.stringify({ conclusion }),
547
- });
548
- }
549
598
  // ============ 公共文件池 ============
550
599
  /** 列出公共文件 */
551
600
  async publicFilesList(dir) {
@@ -1,8 +1,14 @@
1
1
  /**
2
- * 💬 kg_discuss (6→1)
3
- * 合并: list, read, create, reply, close, delete
2
+ * 💬 kg_discuss (8 actions)
3
+ * 合并: list, read, create, reply, close, delete, complete, history
4
4
  * 统一走 HTTP → Rust 后端 (单一写入者)
5
5
  * sender 格式: "projectId:user" (如 "p-ca3sgejg:张三")
6
+ *
7
+ * 权限模型:
8
+ * list/history — 任何人可列出(全局可见)
9
+ * create — 任何人可发起(指定参与项目ID)
10
+ * read/reply — 仅讨论组内成员(initiator + participants)
11
+ * complete/close/delete — 仅发起人
6
12
  */
7
13
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
8
14
  import { type McpContext } from './shared.js';
@@ -1,8 +1,14 @@
1
1
  /**
2
- * 💬 kg_discuss (6→1)
3
- * 合并: list, read, create, reply, close, delete
2
+ * 💬 kg_discuss (8 actions)
3
+ * 合并: list, read, create, reply, close, delete, complete, history
4
4
  * 统一走 HTTP → Rust 后端 (单一写入者)
5
5
  * sender 格式: "projectId:user" (如 "p-ca3sgejg:张三")
6
+ *
7
+ * 权限模型:
8
+ * list/history — 任何人可列出(全局可见)
9
+ * create — 任何人可发起(指定参与项目ID)
10
+ * read/reply — 仅讨论组内成员(initiator + participants)
11
+ * complete/close/delete — 仅发起人
6
12
  */
7
13
  import { z } from 'zod';
8
14
  import { getClient } from '../storage/httpClient.js';
@@ -11,6 +17,34 @@ import { wrap, safeTool } from './shared.js';
11
17
  function sender(ctx) {
12
18
  return `${ctx.projectId}:${ctx.user}`;
13
19
  }
20
+ /** 提取 sender 中的 projectId */
21
+ function senderProjectId(senderStr) {
22
+ return senderStr.split(':')[0] || senderStr;
23
+ }
24
+ /** 检查项目是否是讨论的成员 (发起方 或 参与方) */
25
+ function isMember(brief, projectId) {
26
+ const initiatorPid = senderProjectId(brief.initiator);
27
+ if (initiatorPid === projectId)
28
+ return true;
29
+ return brief.participants.some(p => {
30
+ // participants 可能是 "projectId" 或 "projectId:user"
31
+ return senderProjectId(p) === projectId;
32
+ });
33
+ }
34
+ /** 检查是否是讨论的发起人 */
35
+ function isInitiator(brief, me, projectId) {
36
+ return brief.initiator === me || senderProjectId(brief.initiator) === projectId;
37
+ }
38
+ /** 根据 ID 获取讨论摘要 (用于权限检查) */
39
+ async function getBrief(discussionId) {
40
+ try {
41
+ const all = await getClient().discussionList();
42
+ return all.find(d => d.id === discussionId) || null;
43
+ }
44
+ catch {
45
+ return null;
46
+ }
47
+ }
14
48
  function relativeTime(iso) {
15
49
  try {
16
50
  const diff = Date.now() - new Date(iso).getTime();
@@ -37,8 +71,11 @@ function formatList(items, ctx) {
37
71
  ];
38
72
  for (const d of items) {
39
73
  const others = d.participants.filter(p => p !== d.initiator).join(', ') || '—';
40
- lines.push(`| ${d.id} | ${d.title} | ${d.initiator} | ${others} | ${d.summary} | ${d.status} | ${relativeTime(d.updatedAt)} |`);
74
+ const memberTag = isMember(d, ctx.projectId) ? '✅' : '🔒';
75
+ lines.push(`| ${d.id} | ${memberTag} ${d.title} | ${d.initiator} | ${others} | ${d.summary} | ${d.status} | ${relativeTime(d.updatedAt)} |`);
41
76
  }
77
+ lines.push('');
78
+ lines.push('✅ = 你是成员(可读写) | 🔒 = 非成员(仅可查看标题)');
42
79
  return lines.join('\n');
43
80
  }
44
81
  function formatDetailView(d) {
@@ -66,7 +103,7 @@ function formatDetailView(d) {
66
103
  }
67
104
  export function registerDiscussionTools(server, ctx) {
68
105
  const client = () => getClient();
69
- server.tool('kg_discuss', '💬 跨项目讨论 — 发起、回复、归档协同讨论。action: list(列出活跃讨论)|read(读取详情,自动追踪已读)|create(发起)|reply(回复)|complete(标记完成)|close(结案归档)|delete(删除)|history(查看历史讨论)', {
106
+ server.tool('kg_discuss', '💬 跨项目讨论 — action: list|read|create|reply|complete|close|delete|history。权限: 仅成员可读写,仅发起人可删除/归档', {
70
107
  action: z.enum(['list', 'read', 'create', 'reply', 'complete', 'close', 'delete', 'history'])
71
108
  .describe('操作类型'),
72
109
  id: z.string().optional()
@@ -90,41 +127,15 @@ export function registerDiscussionTools(server, ctx) {
90
127
  }, async (args) => safeTool(async () => {
91
128
  const decoded = decodeObjectStrings(args);
92
129
  const me = sender(ctx);
130
+ const myPid = ctx.projectId;
93
131
  switch (decoded.action) {
132
+ // ============ 公开操作 ============
94
133
  case 'list': {
95
134
  const active = await client().discussionList();
96
135
  if (!Array.isArray(active) || active.length === 0)
97
136
  return wrap(`当前无活跃的讨论 (本项目: ${me})`);
98
137
  return wrap(formatList(active, ctx));
99
138
  }
100
- case 'read': {
101
- const readIds = decoded.ids || (decoded.id ? [decoded.id] : []);
102
- if (readIds.length === 0)
103
- return wrap('❌ read 需要 id 或 ids');
104
- const readMode = decoded.mode || 'auto';
105
- const discussions = await client().discussionReadByIds(readIds, me, readMode);
106
- if (!Array.isArray(discussions) || discussions.length === 0)
107
- return wrap('未找到对应的讨论记录');
108
- return wrap(discussions.map(formatDetailView).join('\n\n━━━━━━━━━━━━━━━━━━━━\n\n'));
109
- }
110
- case 'create': {
111
- if (!decoded.title)
112
- return wrap('❌ create 需要 title');
113
- if (!decoded.participants)
114
- return wrap('❌ create 需要 participants');
115
- if (!decoded.content)
116
- return wrap('❌ create 需要 content');
117
- const result = await client().discussionCreate(decoded.title, me, decoded.participants, decoded.content, decoded.summary);
118
- return wrap(`✅ 讨论已发起\nID: ${result.id}\n发起方: ${me}\n参与方: ${decoded.participants.join(', ')}`);
119
- }
120
- case 'reply': {
121
- if (!decoded.id)
122
- return wrap('❌ reply 需要 id');
123
- if (!decoded.content)
124
- return wrap('❌ reply 需要 content');
125
- await client().discussionReply(decoded.id, me, decoded.content, decoded.summary, decoded.newSummary);
126
- return wrap(`✅ 回复成功 (ID: ${decoded.id}, 身份: ${me})`);
127
- }
128
139
  case 'history': {
129
140
  const all = await client().discussionListAll(me);
130
141
  if (!Array.isArray(all) || all.length === 0)
@@ -143,9 +154,72 @@ export function registerDiscussionTools(server, ctx) {
143
154
  }
144
155
  return wrap(lines.join('\n'));
145
156
  }
157
+ case 'create': {
158
+ if (!decoded.title)
159
+ return wrap('❌ create 需要 title');
160
+ if (!decoded.participants)
161
+ return wrap('❌ create 需要 participants');
162
+ if (!decoded.content)
163
+ return wrap('❌ create 需要 content');
164
+ const result = await client().discussionCreate(decoded.title, me, decoded.participants, decoded.content, decoded.summary);
165
+ return wrap(`✅ 讨论已发起\nID: ${result.id}\n发起方: ${me}\n参与方: ${decoded.participants.join(', ')}\n\n🔒 权限: 仅以上项目可读写此讨论,仅发起方可删除/归档`);
166
+ }
167
+ // ============ 成员操作 (需验证成员身份) ============
168
+ case 'read': {
169
+ const readIds = decoded.ids || (decoded.id ? [decoded.id] : []);
170
+ if (readIds.length === 0)
171
+ return wrap('❌ read 需要 id 或 ids');
172
+ // 权限检查: 逐个验证成员身份
173
+ const allActive = await client().discussionList();
174
+ const deniedIds = [];
175
+ const allowedIds = [];
176
+ for (const rid of readIds) {
177
+ const brief = allActive.find(d => d.id === rid);
178
+ if (!brief) {
179
+ allowedIds.push(rid); // 找不到就放行(可能是历史讨论)
180
+ }
181
+ else if (isMember(brief, myPid)) {
182
+ allowedIds.push(rid);
183
+ }
184
+ else {
185
+ deniedIds.push(rid);
186
+ }
187
+ }
188
+ if (deniedIds.length > 0 && allowedIds.length === 0) {
189
+ return wrap(`🔒 无权限: 你 (${myPid}) 不是这些讨论的成员\n拒绝: ${deniedIds.join(', ')}`);
190
+ }
191
+ const readMode = decoded.mode || 'auto';
192
+ const discussions = await client().discussionReadByIds(allowedIds, me, readMode);
193
+ if (!Array.isArray(discussions) || discussions.length === 0)
194
+ return wrap('未找到对应的讨论记录');
195
+ let result = discussions.map(formatDetailView).join('\n\n━━━━━━━━━━━━━━━━━━━━\n\n');
196
+ if (deniedIds.length > 0) {
197
+ result += `\n\n🔒 已跳过 ${deniedIds.length} 个无权限的讨论: ${deniedIds.join(', ')}`;
198
+ }
199
+ return wrap(result);
200
+ }
201
+ case 'reply': {
202
+ if (!decoded.id)
203
+ return wrap('❌ reply 需要 id');
204
+ if (!decoded.content)
205
+ return wrap('❌ reply 需要 content');
206
+ // 权限检查: 仅成员可回复
207
+ const brief = await getBrief(decoded.id);
208
+ if (brief && !isMember(brief, myPid)) {
209
+ return wrap(`🔒 无权限: 你 (${myPid}) 不是讨论 "${brief.title}" 的成员\n成员: ${brief.initiator}, ${brief.participants.join(', ')}`);
210
+ }
211
+ await client().discussionReply(decoded.id, me, decoded.content, decoded.summary, decoded.newSummary);
212
+ return wrap(`✅ 回复成功 (ID: ${decoded.id}, 身份: ${me})`);
213
+ }
214
+ // ============ 发起人操作 (需验证发起人身份) ============
146
215
  case 'complete': {
147
216
  if (!decoded.id)
148
217
  return wrap('❌ complete 需要 id');
218
+ // 权限检查: 仅发起人可标记完成
219
+ const brief = await getBrief(decoded.id);
220
+ if (brief && !isInitiator(brief, me, myPid)) {
221
+ return wrap(`🔒 无权限: 仅发起人 (${brief.initiator}) 可标记完成\n你的身份: ${me}`);
222
+ }
149
223
  await client().discussionComplete(decoded.id);
150
224
  return wrap(`✅ 讨论已标记完成 (ID: ${decoded.id})`);
151
225
  }
@@ -154,12 +228,27 @@ export function registerDiscussionTools(server, ctx) {
154
228
  return wrap('❌ close 需要 id');
155
229
  if (!decoded.conclusion)
156
230
  return wrap('❌ close 需要 conclusion');
157
- const result = await client().discussionClose(decoded.id, decoded.conclusion);
158
- return wrap(`✅ 讨论已结案并归档: \`${result.archived_path}\``);
231
+ // 权限检查: 仅发起人可归档
232
+ const brief = await getBrief(decoded.id);
233
+ if (brief && !isInitiator(brief, me, myPid)) {
234
+ return wrap(`🔒 无权限: 仅发起人 (${brief.initiator}) 可归档讨论\n你的身份: ${me}`);
235
+ }
236
+ // 先追加结案总结
237
+ try {
238
+ await client().discussionReply(decoded.id, me, `📋 结案总结:\n\n${decoded.conclusion}`, '结案总结');
239
+ }
240
+ catch { /* 忽略回复失败 */ }
241
+ await client().discussionComplete(decoded.id);
242
+ return wrap(`✅ 讨论已结案 (ID: ${decoded.id})\n📋 ${decoded.conclusion}`);
159
243
  }
160
244
  case 'delete': {
161
245
  if (!decoded.id)
162
246
  return wrap('❌ delete 需要 id');
247
+ // 权限检查: 仅发起人可删除
248
+ const brief = await getBrief(decoded.id);
249
+ if (brief && !isInitiator(brief, me, myPid)) {
250
+ return wrap(`🔒 无权限: 仅发起人 (${brief.initiator}) 可删除讨论\n你的身份: ${me}`);
251
+ }
163
252
  await client().discussionDelete(decoded.id);
164
253
  return wrap(`✅ 讨论已删除 (ID: ${decoded.id})`);
165
254
  }
@@ -10,7 +10,7 @@ import { wrap, safeTool, crossPrefix, formatDocMarkdown, formatTreeText, countTr
10
10
  export function registerDocTools(server, ctx) {
11
11
  const client = () => getClient();
12
12
  // ========== kg_doc: 文档 CRUD 统一入口 ==========
13
- server.tool('kg_doc', '📄 知识文档操作创建、读取、更新、删除、复制文档。action: create(创建)|read(读取)|update(更新)|delete(删除)|batch_update(批量更新)|copy(跨项目复制)', {
13
+ server.tool('kg_doc', '📄 参考文档 — action: create(创建,建议bindTo关联节点)|read|update|delete|batch_update|copy。⚠️ 不绑定节点的文档会成为孤岛!', {
14
14
  action: z.enum(['create', 'read', 'update', 'delete', 'batch_update', 'copy'])
15
15
  .describe('操作类型'),
16
16
  path: z.string().optional()
@@ -47,6 +47,10 @@ export function registerDocTools(server, ctx) {
47
47
  .describe('复制源路径 (copy, 单个或数组)'),
48
48
  targetPath: z.string().optional()
49
49
  .describe('复制目标路径前缀 (copy)'),
50
+ bindTo: z.string().optional()
51
+ .describe('绑定到流程图节点ID (create/update, 如"n_frontend")'),
52
+ bindChart: z.string().optional()
53
+ .describe('绑定目标流程图ID (create/update, 默认"main")'),
50
54
  }, async (args) => safeTool(async () => {
51
55
  const d = decodeObjectStrings(args);
52
56
  switch (d.action) {
@@ -63,7 +67,39 @@ export function registerDocTools(server, ctx) {
63
67
  bugfixes: [],
64
68
  status: d.status || '已完成'
65
69
  });
66
- return wrap(`✅ 文档已创建: ${d.path}\n\n${JSON.stringify(doc, null, 2)}`);
70
+ // 绑定到流程图节点
71
+ let bindMsg = '';
72
+ if (d.bindTo) {
73
+ try {
74
+ const chartId = d.bindChart || 'main';
75
+ const chart = await client().getFlowchart(chartId);
76
+ if (chart) {
77
+ const node = chart.nodes?.find((n) => n.id === d.bindTo);
78
+ if (node) {
79
+ if (!node.boundDocs)
80
+ node.boundDocs = [];
81
+ if (!node.boundDocs.includes(d.path)) {
82
+ node.boundDocs.push(d.path);
83
+ }
84
+ // Save back via PUT (use the existing save method)
85
+ chart.id = chartId;
86
+ await client().saveFlowchart(chartId, chart);
87
+ bindMsg = `\n🔗 已绑定到节点: ${d.bindTo} [${chartId}]`;
88
+ }
89
+ else {
90
+ bindMsg = `\n⚠️ 节点 "${d.bindTo}" 不存在于流程图 "${chartId}"`;
91
+ }
92
+ }
93
+ }
94
+ catch {
95
+ bindMsg = `\n⚠️ 绑定失败(流程图操作异常)`;
96
+ }
97
+ }
98
+ // 约束: 未绑定流程图节点 → 强制警告
99
+ if (!d.bindTo) {
100
+ bindMsg = `\n\n⚠️ 【未关联知识锚点】文档未绑定到流程图节点!\n💡 建议: 使用 kg_flowchart(bind, nodeId, docs:["${d.path}"]) 绑定\n📌 孤立文档无法被 AI 通过节点关联发现`;
101
+ }
102
+ return wrap(`✅ 文档已创建: ${d.path}${bindMsg}`);
67
103
  }
68
104
  // ---- READ ----
69
105
  case 'read': {
@@ -109,7 +145,9 @@ export function registerDocTools(server, ctx) {
109
145
  if (d.bugfixes !== undefined)
110
146
  updates.bugfixes = d.bugfixes;
111
147
  const doc = await client().updateDoc(d.path, updates);
112
- return wrap(doc ? JSON.stringify(doc, null, 2) : '更新失败');
148
+ if (!doc)
149
+ return wrap('更新失败');
150
+ return wrap(`✅ 文档已更新: ${d.path}\n状态: ${doc.status || '已完成'}\n摘要: ${doc.summary || ''}`);
113
151
  }
114
152
  // ---- DELETE ----
115
153
  case 'delete': {
@@ -0,0 +1,7 @@
1
+ /**
2
+ * 🔀 kg_flowchart — 逻辑流程图批量操作
3
+ * AI 一次提交所有节点+边, 后端原子处理, 返回孤立检测结果
4
+ */
5
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
6
+ import { type McpContext } from './shared.js';
7
+ export declare function registerFlowchartTools(server: McpServer, ctx: McpContext): void;
@@ -0,0 +1,483 @@
1
+ /**
2
+ * 🔀 kg_flowchart — 逻辑流程图批量操作
3
+ * AI 一次提交所有节点+边, 后端原子处理, 返回孤立检测结果
4
+ */
5
+ import { z } from 'zod';
6
+ import { getClient } from '../storage/httpClient.js';
7
+ import { decodeObjectStrings } from '../utils.js';
8
+ import { wrap, safeTool } from './shared.js';
9
+ const NodeSchema = z.object({
10
+ id: z.string().describe('节点唯一ID'),
11
+ label: z.string().describe('节点标签'),
12
+ description: z.string().optional().describe('节点描述'),
13
+ nodeType: z.string().optional().describe('节点类型: super|process|data|entry'),
14
+ domain: z.string().optional().describe('领域: frontend|backend|mcp|system|infra'),
15
+ input: z.array(z.string()).optional(),
16
+ output: z.array(z.string()).optional(),
17
+ affiliation: z.string().optional().describe('父节点ID, 默认root'),
18
+ });
19
+ const EdgeSchema = z.object({
20
+ from: z.string().describe('起点节点ID'),
21
+ to: z.string().describe('终点节点ID'),
22
+ label: z.string().optional().describe('连线标签'),
23
+ edgeType: z.string().optional().describe('类型: call|event|data|reference'),
24
+ });
25
+ export function registerFlowchartTools(server, ctx) {
26
+ const client = () => getClient();
27
+ server.tool('kg_flowchart', '🔀 逻辑流程图(关系型知识锚点) — action: list|get|get_node(★爆炸式上下文)|update_node|delete_node|batch_add|bind|unbind|orphans|health', {
28
+ action: z.enum(['list', 'get', 'get_node', 'update_node', 'delete_node', 'batch_add', 'bind', 'unbind', 'orphans', 'health'])
29
+ .describe('操作类型'),
30
+ chartId: z.string().optional()
31
+ .describe('流程图ID (默认"main")'),
32
+ nodeId: z.string().optional()
33
+ .describe('节点ID (get_node/update_node/delete_node/bind/unbind)'),
34
+ // get_node 爆炸扩展控制 (全部默认开启, AI可选择性关闭)
35
+ expand: z.number().optional()
36
+ .describe('get_node: 连线扩展层数(默认3, 0=不扩展)'),
37
+ includeDocs: z.boolean().optional()
38
+ .describe('get_node: 显示绑定的参考文档摘要(默认true)'),
39
+ includeTasks: z.boolean().optional()
40
+ .describe('get_node: 显示绑定的任务摘要(默认true)'),
41
+ includeFiles: z.boolean().optional()
42
+ .describe('get_node: 显示绑定的代码文件(默认true)'),
43
+ includeDoc: z.boolean().optional()
44
+ .describe('get_node: 显示节点技术文档(默认true)'),
45
+ // update_node 更新字段
46
+ label: z.string().optional().describe('update_node: 新标签'),
47
+ description: z.string().optional().describe('update_node: 新描述'),
48
+ nodeType: z.string().optional().describe('update_node: 新类型'),
49
+ domain: z.string().optional().describe('update_node: 新领域'),
50
+ input: z.array(z.string()).optional().describe('update_node: 新输入 / bind的源代码文件'),
51
+ output: z.array(z.string()).optional().describe('update_node: 新输出'),
52
+ // bind/unbind
53
+ files: z.array(z.string()).optional()
54
+ .describe('源代码文件路径 (bind/unbind)'),
55
+ dirs: z.array(z.string()).optional()
56
+ .describe('源代码目录路径 (bind/unbind)'),
57
+ docs: z.array(z.string()).optional()
58
+ .describe('知识文档路径 (bind/unbind)'),
59
+ tasks: z.array(z.string()).optional()
60
+ .describe('任务ID (bind/unbind)'),
61
+ // batch_add
62
+ nodes: z.array(NodeSchema).optional()
63
+ .describe('批量添加的节点数组 (batch_add)'),
64
+ edges: z.array(EdgeSchema).optional()
65
+ .describe('批量添加的边数组 (batch_add)'),
66
+ }, async (args) => safeTool(async () => {
67
+ const decoded = decodeObjectStrings(args);
68
+ switch (decoded.action) {
69
+ case 'list': {
70
+ const charts = await client().listFlowcharts();
71
+ if (!charts || charts.length === 0)
72
+ return wrap('📋 暂无流程图');
73
+ const list = charts
74
+ .map(c => `• ${c.title} [${c.id}] — ${c.nodeCount}节点 ${c.edgeCount}连线`)
75
+ .join('\n');
76
+ return wrap(`📋 流程图列表:\n\n${list}`);
77
+ }
78
+ case 'get': {
79
+ const chartId = decoded.chartId || 'main';
80
+ const chart = await client().getFlowchart(chartId);
81
+ if (!chart)
82
+ return wrap(`❌ 流程图 "${chartId}" 不存在`);
83
+ const nodes = chart.nodes || [];
84
+ const edges = chart.edges || [];
85
+ const lines = [
86
+ `📊 ${chart.title || chartId} [${chartId}]`,
87
+ chart.parentChart ? `↩ 父图: ${chart.parentChart}` : '',
88
+ ``,
89
+ `### 节点 (${nodes.length})`,
90
+ ...nodes.map((n) => ` • ${n.label} [${n.id}] (${n.nodeType}/${n.domain}) ${n.boundDocs?.length ? `📄${n.boundDocs.length}` : ''} ${n.boundTasks?.length ? `📝${n.boundTasks.length}` : ''} ${n.boundFiles?.length ? `📂${n.boundFiles.length}` : ''}`),
91
+ ``,
92
+ `### 连线 (${edges.length})`,
93
+ ...edges.map((e) => ` ${e.from} →${e.label ? `[${e.label}]` : ''} ${e.to}`),
94
+ ].filter(Boolean);
95
+ return wrap(lines.join('\n'));
96
+ }
97
+ case 'get_node': {
98
+ const chartId = decoded.chartId || 'main';
99
+ if (!decoded.nodeId)
100
+ return wrap('❌ get_node 需要 nodeId');
101
+ const chart = await client().getFlowchart(chartId);
102
+ if (!chart)
103
+ return wrap(`❌ 流程图 "${chartId}" 不存在`);
104
+ const allNodes = chart.nodes || [];
105
+ const allEdges = chart.edges || [];
106
+ const target = allNodes.find((n) => n.id === decoded.nodeId);
107
+ if (!target)
108
+ return wrap(`❌ 节点 "${decoded.nodeId}" 不存在于 "${chartId}"`);
109
+ // 扩展参数 (全部默认开启)
110
+ const expandDepth = decoded.expand !== undefined ? Number(decoded.expand) : 3;
111
+ const showDocs = decoded.includeDocs !== false;
112
+ const showTasks = decoded.includeTasks !== false;
113
+ const showFiles = decoded.includeFiles !== false;
114
+ const showDoc = decoded.includeDoc !== false;
115
+ const nodeMap = new Map(allNodes.map((n) => [n.id, n]));
116
+ const lines = [];
117
+ // ======== 节点基础信息 ========
118
+ lines.push(`🔍 节点详情: ${target.label} [${target.id}]`);
119
+ lines.push(`类型: ${target.nodeType || 'process'} | 领域: ${target.domain || 'system'}`);
120
+ if (target.description)
121
+ lines.push(`📝 ${target.description}`);
122
+ if (target.input?.length)
123
+ lines.push(`📥 输入: ${target.input.join(', ')}`);
124
+ if (target.output?.length)
125
+ lines.push(`📤 输出: ${target.output.join(', ')}`);
126
+ if (target.subFlowchart)
127
+ lines.push(`📂 子流程图: ${target.subFlowchart}`);
128
+ if (target.lastUpdated)
129
+ lines.push(`🕐 最后更新: ${target.lastUpdated}`);
130
+ lines.push('');
131
+ // ======== 节点技术文档 ========
132
+ if (showDoc && target.doc) {
133
+ lines.push(`### 📖 技术文档`);
134
+ lines.push(`版本: ${target.doc.version} | ${target.doc.date}`);
135
+ if (target.doc.overview)
136
+ lines.push(target.doc.overview);
137
+ if (target.doc.details?.length) {
138
+ for (const d of target.doc.details) {
139
+ lines.push(` **${d.section}**: ${d.content?.substring(0, 200)}`);
140
+ }
141
+ }
142
+ lines.push('');
143
+ }
144
+ // ======== 爆炸扩展: N层关联节点 ========
145
+ if (expandDepth > 0) {
146
+ const visited = new Set([target.id]);
147
+ let frontier = [target.id];
148
+ const layerResults = [];
149
+ for (let depth = 1; depth <= expandDepth; depth++) {
150
+ const nextFrontier = [];
151
+ const layerLines = [];
152
+ for (const currentId of frontier) {
153
+ // 出边
154
+ for (const e of allEdges) {
155
+ if (e.from === currentId && !visited.has(e.to)) {
156
+ visited.add(e.to);
157
+ nextFrontier.push(e.to);
158
+ const neighbor = nodeMap.get(e.to);
159
+ const label = neighbor?.label || e.to;
160
+ layerLines.push(` → ${label} [${e.to}] ${e.label ? `(${e.label})` : ''} ${e.edgeType ? `[${e.edgeType}]` : ''}`);
161
+ }
162
+ // 入边
163
+ if (e.to === currentId && !visited.has(e.from)) {
164
+ visited.add(e.from);
165
+ nextFrontier.push(e.from);
166
+ const neighbor = nodeMap.get(e.from);
167
+ const label = neighbor?.label || e.from;
168
+ layerLines.push(` ← ${label} [${e.from}] ${e.label ? `(${e.label})` : ''} ${e.edgeType ? `[${e.edgeType}]` : ''}`);
169
+ }
170
+ }
171
+ }
172
+ if (layerLines.length > 0)
173
+ layerResults.push(layerLines);
174
+ frontier = nextFrontier;
175
+ if (frontier.length === 0)
176
+ break;
177
+ }
178
+ if (layerResults.length > 0) {
179
+ lines.push(`### 🔗 关联节点 (扩展${layerResults.length}层, 共${visited.size - 1}个)`);
180
+ layerResults.forEach((layer, i) => {
181
+ lines.push(`**L${i + 1}** (${layer.length}个):`);
182
+ lines.push(...layer);
183
+ });
184
+ lines.push('');
185
+ }
186
+ }
187
+ // ======== 绑定的参考文档 ========
188
+ if (showDocs && target.boundDocs?.length) {
189
+ lines.push(`### 📄 绑定参考文档 (${target.boundDocs.length})`);
190
+ for (const docPath of target.boundDocs) {
191
+ try {
192
+ const doc = await client().getDoc(docPath);
193
+ if (doc) {
194
+ lines.push(` 📄 ${docPath} — ${doc.summary || '无摘要'}`);
195
+ if (doc.status)
196
+ lines.push(` 状态: ${doc.status}`);
197
+ }
198
+ else {
199
+ lines.push(` 📄 ${docPath} (未找到)`);
200
+ }
201
+ }
202
+ catch {
203
+ lines.push(` 📄 ${docPath}`);
204
+ }
205
+ }
206
+ lines.push('');
207
+ }
208
+ // ======== 绑定的任务 ========
209
+ if (showTasks && target.boundTasks?.length) {
210
+ lines.push(`### 📝 绑定任务 (${target.boundTasks.length})`);
211
+ for (const taskId of target.boundTasks) {
212
+ try {
213
+ const task = await client().getTask(taskId);
214
+ if (task) {
215
+ lines.push(` 📝 [${taskId}] ${task.title} — ${task.status}`);
216
+ }
217
+ else {
218
+ lines.push(` 📝 [${taskId}] (未找到)`);
219
+ }
220
+ }
221
+ catch {
222
+ lines.push(` 📝 [${taskId}]`);
223
+ }
224
+ }
225
+ lines.push('');
226
+ }
227
+ // ======== 绑定的代码文件 ========
228
+ if (showFiles) {
229
+ const files = target.boundFiles || [];
230
+ const dirs = target.boundDirs || [];
231
+ if (files.length + dirs.length > 0) {
232
+ lines.push(`### 📂 绑定代码 (${files.length}文件 ${dirs.length}目录)`);
233
+ for (const f of files)
234
+ lines.push(` 📄 ${f}`);
235
+ for (const d of dirs)
236
+ lines.push(` 📁 ${d}`);
237
+ lines.push('');
238
+ }
239
+ }
240
+ // ======== 版本历史 ========
241
+ if (target.versions?.length) {
242
+ lines.push(`### 📋 版本历史 (${target.versions.length})`);
243
+ for (const v of target.versions.slice(-5)) {
244
+ lines.push(` v${v.version} (${v.date}): ${v.changes}`);
245
+ }
246
+ }
247
+ return wrap(lines.join('\n'));
248
+ }
249
+ case 'update_node': {
250
+ const chartId = decoded.chartId || 'main';
251
+ if (!decoded.nodeId)
252
+ return wrap('❌ update_node 需要 nodeId');
253
+ // 构建待更新的节点字段
254
+ const updates = { id: decoded.nodeId };
255
+ const changes = [];
256
+ if (decoded.label !== undefined) {
257
+ updates.label = decoded.label;
258
+ changes.push('label');
259
+ }
260
+ if (decoded.description !== undefined) {
261
+ updates.description = decoded.description;
262
+ changes.push('description');
263
+ }
264
+ if (decoded.nodeType !== undefined) {
265
+ updates.nodeType = decoded.nodeType;
266
+ changes.push('nodeType');
267
+ }
268
+ if (decoded.domain !== undefined) {
269
+ updates.domain = decoded.domain;
270
+ changes.push('domain');
271
+ }
272
+ if (decoded.input !== undefined) {
273
+ updates.input = decoded.input;
274
+ changes.push('input');
275
+ }
276
+ if (decoded.output !== undefined) {
277
+ updates.output = decoded.output;
278
+ changes.push('output');
279
+ }
280
+ if (changes.length === 0)
281
+ return wrap('ℹ️ 无变更 — 请提供要更新的字段');
282
+ updates.lastUpdated = new Date().toISOString();
283
+ // 原子调用: PUT /flowcharts/:chartId/nodes/:nodeId
284
+ await client().updateFlowchartNode(chartId, decoded.nodeId, updates, `MCP更新: ${changes.join(', ')}`);
285
+ return wrap(`✅ 节点已更新: ${decoded.label || decoded.nodeId} [${chartId}/${decoded.nodeId}]\n更新字段: ${changes.join(', ')}`);
286
+ }
287
+ case 'delete_node': {
288
+ const chartId = decoded.chartId || 'main';
289
+ if (!decoded.nodeId)
290
+ return wrap('❌ delete_node 需要 nodeId');
291
+ // 原子调用: DELETE /flowcharts/:chartId/nodes/:nodeId (后端自动清理关联边)
292
+ await client().deleteFlowchartNode(chartId, decoded.nodeId);
293
+ return wrap(`✅ 节点已删除 [${chartId}/${decoded.nodeId}]`);
294
+ }
295
+ case 'batch_add': {
296
+ const chartId = decoded.chartId || 'main';
297
+ if (!decoded.nodes || decoded.nodes.length === 0) {
298
+ return wrap('❌ batch_add 需要 nodes 数组');
299
+ }
300
+ // 自动填充默认值 — AI 只需填 id+label+description
301
+ const fullNodes = decoded.nodes.map((n) => ({
302
+ id: n.id,
303
+ label: n.label,
304
+ nodeType: n.nodeType || 'process',
305
+ domain: n.domain || 'system',
306
+ input: n.input || [],
307
+ output: n.output || [],
308
+ description: n.description || '',
309
+ affiliation: n.affiliation || 'root',
310
+ boundDocs: [],
311
+ boundFiles: [],
312
+ boundDirs: [],
313
+ boundTasks: [],
314
+ subFlowchart: null,
315
+ position: null,
316
+ versions: [],
317
+ lastUpdated: '',
318
+ lastQueried: '',
319
+ }));
320
+ const fullEdges = (decoded.edges || []).map((e) => ({
321
+ from: e.from,
322
+ to: e.to,
323
+ label: e.label || null,
324
+ edgeType: e.edgeType || 'call',
325
+ }));
326
+ const result = await client().batchAddToFlowchart(chartId, fullNodes, fullEdges);
327
+ // 构建报告
328
+ let report = `✅ 批量操作完成 [${chartId}]\n`;
329
+ report += ` 添加: ${result.addedNodes.length}节点 ${result.addedEdges}连线\n`;
330
+ if (result.failedNodes.length > 0) {
331
+ report += `\n❌ 失败节点:\n`;
332
+ result.failedNodes.forEach(([id, reason]) => {
333
+ report += ` • ${id}: ${reason}\n`;
334
+ });
335
+ }
336
+ if (result.failedEdges.length > 0) {
337
+ report += `\n❌ 失败连线:\n`;
338
+ result.failedEdges.forEach(([from, to, reason]) => {
339
+ report += ` • ${from} → ${to}: ${reason}\n`;
340
+ });
341
+ }
342
+ if (result.orphanedNodes.length > 0) {
343
+ report += `\n⚠️ 孤立节点 (无任何连线):\n`;
344
+ result.orphanedNodes.forEach(id => {
345
+ report += ` • ${id}\n`;
346
+ });
347
+ }
348
+ else {
349
+ report += `\n✅ 无孤立节点`;
350
+ }
351
+ return wrap(report);
352
+ }
353
+ case 'orphans': {
354
+ const orphans = await client().getFlowchartOrphans();
355
+ if (!orphans || orphans.length === 0) {
356
+ return wrap('✅ 无孤立文档 — 所有文档均已绑定到流程图节点');
357
+ }
358
+ const list = orphans
359
+ .map(o => `• ${o.docPath}${o.docSummary ? ` — ${o.docSummary}` : ''}`)
360
+ .join('\n');
361
+ return wrap(`⚠️ 发现 ${orphans.length} 个孤立文档 (未绑定到任何流程图节点):\n\n${list}\n\n💡 使用 kg_doc(update, bindTo: "节点ID") 绑定`);
362
+ }
363
+ case 'health': {
364
+ const health = await client().getFlowchartHealth();
365
+ if (!health || health.length === 0) {
366
+ return wrap('📊 暂无节点健康数据');
367
+ }
368
+ const items = health;
369
+ const stale = items.filter(h => h.status === 'stale');
370
+ const active = items.filter(h => h.status === 'active');
371
+ const normal = items.filter(h => h.status === 'normal');
372
+ let report = `📊 知识冷热分布 (${items.length}节点):\n`;
373
+ report += ` 🟢 active (7天内查询): ${active.length}\n`;
374
+ report += ` 🟡 normal (30天内): ${normal.length}\n`;
375
+ report += ` 🔴 stale (30天+未查询): ${stale.length}\n`;
376
+ if (stale.length > 0) {
377
+ report += `\n🔴 冷知识节点:\n`;
378
+ stale.forEach(h => {
379
+ report += ` • ${h.nodeLabel} [${h.chartId}/${h.nodeId}] — ${h.daysSinceQuery}天未查询\n`;
380
+ });
381
+ }
382
+ return wrap(report);
383
+ }
384
+ case 'bind':
385
+ case 'unbind': {
386
+ const chartId = decoded.chartId || 'main';
387
+ if (!decoded.nodeId)
388
+ return wrap(`❌ ${decoded.action} 需要 nodeId`);
389
+ const chart = await client().getFlowchart(chartId);
390
+ if (!chart)
391
+ return wrap(`❌ 流程图 "${chartId}" 不存在`);
392
+ const node = chart.nodes?.find((n) => n.id === decoded.nodeId);
393
+ if (!node)
394
+ return wrap(`❌ 节点 "${decoded.nodeId}" 不存在于流程图 "${chartId}"`);
395
+ const isBind = decoded.action === 'bind';
396
+ const changes = [];
397
+ const modify = (items, field, label) => {
398
+ if (!items || items.length === 0)
399
+ return;
400
+ if (!node[field])
401
+ node[field] = [];
402
+ if (isBind) {
403
+ let added = 0;
404
+ for (const item of items) {
405
+ if (!node[field].includes(item)) {
406
+ node[field].push(item);
407
+ added++;
408
+ }
409
+ }
410
+ if (added > 0)
411
+ changes.push(`+${added} ${label}`);
412
+ }
413
+ else {
414
+ const before = node[field].length;
415
+ node[field] = node[field].filter((x) => !items.includes(x));
416
+ const removed = before - node[field].length;
417
+ if (removed > 0)
418
+ changes.push(`-${removed} ${label}`);
419
+ }
420
+ };
421
+ modify(decoded.files, 'boundFiles', '文件');
422
+ modify(decoded.dirs, 'boundDirs', '目录');
423
+ modify(decoded.docs, 'boundDocs', '文档');
424
+ modify(decoded.tasks, 'boundTasks', '任务');
425
+ if (changes.length === 0) {
426
+ return wrap(`ℹ️ 无变更 — 请提供 files/dirs/docs/tasks 参数`);
427
+ }
428
+ await client().saveFlowchart(chartId, chart);
429
+ // P2: 双向同步 — 更新参考文档的 adoptedBy
430
+ const docsList = decoded.docs;
431
+ if (docsList && docsList.length > 0) {
432
+ const nodeId = decoded.nodeId;
433
+ for (const docPath of docsList) {
434
+ try {
435
+ const doc = await client().getDoc(docPath);
436
+ if (!doc?.content)
437
+ continue;
438
+ const content = doc.content;
439
+ // 解析 frontmatter
440
+ if (!content.startsWith('---'))
441
+ continue;
442
+ const endIdx = content.indexOf('---', 3);
443
+ if (endIdx === -1)
444
+ continue;
445
+ const yaml = content.slice(3, endIdx);
446
+ const body = content.slice(endIdx + 3);
447
+ // 提取现有 adoptedBy
448
+ const adoptedMatch = yaml.match(/adoptedBy:\s*\[(.*?)\]/);
449
+ let adopted = [];
450
+ if (adoptedMatch) {
451
+ adopted = adoptedMatch[1].split(',').map(s => s.trim()).filter(Boolean);
452
+ }
453
+ // 修改 adoptedBy
454
+ if (isBind) {
455
+ if (!adopted.includes(nodeId))
456
+ adopted.push(nodeId);
457
+ }
458
+ else {
459
+ adopted = adopted.filter(id => id !== nodeId);
460
+ }
461
+ // 重建 frontmatter
462
+ const newAdoptedLine = `adoptedBy: [${adopted.join(', ')}]`;
463
+ let newYaml;
464
+ if (adoptedMatch) {
465
+ newYaml = yaml.replace(/adoptedBy:\s*\[.*?\]/, newAdoptedLine);
466
+ }
467
+ else {
468
+ newYaml = yaml.trimEnd() + '\n' + newAdoptedLine + '\n';
469
+ }
470
+ const newContent = `---${newYaml}---${body}`;
471
+ await client().updateDoc(docPath, { content: newContent });
472
+ }
473
+ catch { /* 单个文档同步失败不阻断 */ }
474
+ }
475
+ }
476
+ const icon = isBind ? '🔗' : '⛓️';
477
+ return wrap(`${icon} ${isBind ? '绑定' : '解绑'}完成 [${decoded.nodeId}]: ${changes.join(', ')}`);
478
+ }
479
+ default:
480
+ return wrap(`❌ 未知 action: ${decoded.action}`);
481
+ }
482
+ }));
483
+ }
@@ -2,12 +2,13 @@
2
2
  * MCP 工具注册入口
3
3
  * 14 个工具, 7 个子模块
4
4
  *
5
- * 🔗 初始化: kg_init (1个)
6
- * 📊 导航: kg_status, kg_tree (2个)
7
- * 📚 知识: kg_doc, kg_projects, kg_rules (3个)
8
- * 📝 工作流: kg_task, kg_files, kg_discuss (3个)
9
- * 🔬 代码分析: code_scan, code_smart_context, code_full_path (3个)
10
- * 🏛️ 协作: kg_meeting (1个)
5
+ * 🔗 初始化: kg_init (1个)
6
+ * 📊 导航: kg_status, kg_tree (2个)
7
+ * 📚 知识: kg_doc(参考文档), kg_projects, kg_rules (3个)
8
+ * 📝 工作流: kg_task(任务记录), kg_files, kg_discuss(讨论区) (3个)
9
+ * 🔀 关系核心: kg_flowchart(逻辑流程图 关系型知识锚点) (1个)
10
+ * 🔬 代码分析: code_scan, code_smart_context, code_full_path (3个)
11
+ * 🏛️ 协作: kg_meeting (1个)
11
12
  */
12
13
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
13
14
  export declare function registerTools(server: McpServer, projectId: string, user: string, onProjectChange?: (newProjectId: string, newApiUrl: string) => void): void;
@@ -2,12 +2,13 @@
2
2
  * MCP 工具注册入口
3
3
  * 14 个工具, 7 个子模块
4
4
  *
5
- * 🔗 初始化: kg_init (1个)
6
- * 📊 导航: kg_status, kg_tree (2个)
7
- * 📚 知识: kg_doc, kg_projects, kg_rules (3个)
8
- * 📝 工作流: kg_task, kg_files, kg_discuss (3个)
9
- * 🔬 代码分析: code_scan, code_smart_context, code_full_path (3个)
10
- * 🏛️ 协作: kg_meeting (1个)
5
+ * 🔗 初始化: kg_init (1个)
6
+ * 📊 导航: kg_status, kg_tree (2个)
7
+ * 📚 知识: kg_doc(参考文档), kg_projects, kg_rules (3个)
8
+ * 📝 工作流: kg_task(任务记录), kg_files, kg_discuss(讨论区) (3个)
9
+ * 🔀 关系核心: kg_flowchart(逻辑流程图 关系型知识锚点) (1个)
10
+ * 🔬 代码分析: code_scan, code_smart_context, code_full_path (3个)
11
+ * 🏛️ 协作: kg_meeting (1个)
11
12
  */
12
13
  import { createContext } from './shared.js';
13
14
  import { registerInitTool } from './init.js';
@@ -20,6 +21,7 @@ import { registerFileTools } from './files.js';
20
21
  import { registerDiscussionTools } from './discussion.js';
21
22
  import { registerAnalyzerTools } from './analyzer.js';
22
23
  import { registerMeetingTools } from './meeting.js';
24
+ import { registerFlowchartTools } from './flowchart.js';
23
25
  export function registerTools(server, projectId, user, onProjectChange) {
24
26
  // 创建共享可变上下文 — 所有工具捕获此对象引用
25
27
  const ctx = createContext(projectId, user);
@@ -39,4 +41,6 @@ export function registerTools(server, projectId, user, onProjectChange) {
39
41
  registerAnalyzerTools(server, ctx);
40
42
  // 🏛️ 多AI协作 (kg_meeting)
41
43
  registerMeetingTools(server, ctx);
44
+ // 🔀 流程图 (kg_flowchart)
45
+ registerFlowchartTools(server, ctx);
42
46
  }
@@ -10,7 +10,7 @@ import { wrap, safeTool, crossPrefix } from './shared.js';
10
10
  export function registerRuleTools(server, ctx) {
11
11
  const client = () => getClient();
12
12
  server.tool('kg_rules', '📏 项目规则管理 — 读取或保存代码风格、审查规则等。action: get(读取规则)|save(保存规则)|get_meta(读取触发配置)|save_meta(保存触发配置)', {
13
- action: z.enum(['get', 'save', 'get_meta', 'save_meta'])
13
+ action: z.enum(['get', 'save', 'get_meta', 'save_meta', 'delete'])
14
14
  .describe('操作类型'),
15
15
  ruleType: z.string().optional()
16
16
  .describe('规则类型(如 userStyles, codeStyle, reviewRules)。get/save 时使用,不传则获取全部'),
@@ -96,6 +96,24 @@ export function registerRuleTools(server, ctx) {
96
96
  return wrap('❌ 保存失败');
97
97
  return wrap(`✅ 触发配置已保存 (更新${Object.keys(decoded.meta).length}个类型, 共${Object.keys(merged).length}个类型)`);
98
98
  }
99
+ case 'delete': {
100
+ if (!decoded.ruleType)
101
+ return wrap('❌ delete 需要 ruleType');
102
+ if (!decoded.rules || decoded.rules.length === 0)
103
+ return wrap('❌ delete 需要 rules 数组(要删除的规则内容)');
104
+ const existing = await client().getRulesApi(decoded.ruleType);
105
+ const toDelete = new Set(decoded.rules.map((r) => r.trim()));
106
+ const filtered = existing.filter((r) => !toDelete.has(r.trim()));
107
+ const removed = existing.length - filtered.length;
108
+ if (removed === 0)
109
+ return wrap('ℹ️ 未找到匹配的规则');
110
+ const success = await client().saveRulesApi(decoded.ruleType, filtered);
111
+ if (!success)
112
+ return wrap('❌ 删除失败');
113
+ const meta = await client().getRulesMeta();
114
+ const label = meta[decoded.ruleType]?.label || decoded.ruleType;
115
+ return wrap(`✅ ${label}: 已删除${removed}条, 剩余${filtered.length}条`);
116
+ }
99
117
  default:
100
118
  return wrap(`❌ 未知 action: ${decoded.action}`);
101
119
  }
@@ -1,6 +1,6 @@
1
1
  /**
2
- * 📝 kg_task (4→1)
3
- * 合并: task_create, task_get, task_update, task_archive
2
+ * 📝 kg_task (4→1) — 极简任务管理
3
+ * AI 只需说人话写内容,后端负责所有格式化、分类、时间戳
4
4
  */
5
5
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
6
6
  import { type McpContext } from './shared.js';
@@ -1,6 +1,6 @@
1
1
  /**
2
- * 📝 kg_task (4→1)
3
- * 合并: task_create, task_get, task_update, task_archive
2
+ * 📝 kg_task (4→1) — 极简任务管理
3
+ * AI 只需说人话写内容,后端负责所有格式化、分类、时间戳
4
4
  */
5
5
  import { z } from 'zod';
6
6
  import { getClient } from '../storage/httpClient.js';
@@ -8,21 +8,21 @@ import { decodeObjectStrings } from '../utils.js';
8
8
  import { wrap, safeTool } from './shared.js';
9
9
  export function registerTaskTools(server, ctx) {
10
10
  const client = () => getClient();
11
- server.tool('kg_task', '📝 任务管理创建、查询、更新进展、归档。action: create(创建)|get(查询)|update(添加日志)|archive(归档)', {
12
- action: z.enum(['create', 'get', 'update', 'archive'])
11
+ server.tool('kg_task', '📝 任务记录 — action: create(创建,建议bindTo关联节点)|get(查询)|update(追加进度)|archive(归档)|delete(删除)。⚠️ 每完成一个步骤必须立即update!', {
12
+ action: z.enum(['create', 'get', 'update', 'archive', 'delete'])
13
13
  .describe('操作类型'),
14
14
  title: z.string().optional()
15
- .describe('任务标题 (create/get)'),
15
+ .describe('任务标题 (create/get按标题搜)'),
16
16
  description: z.string().optional()
17
17
  .describe('任务描述Markdown (create)'),
18
18
  goals: z.array(z.string()).optional()
19
19
  .describe('目标清单 (create)'),
20
20
  taskId: z.string().optional()
21
- .describe('任务ID (update/archive)'),
21
+ .describe('任务ID (get/update/archive)'),
22
22
  log_type: z.enum(['progress', 'issue', 'solution', 'reference']).optional()
23
- .describe('日志类型 (update)'),
23
+ .describe('日志类型 (update, 可选-不传则自动检测)'),
24
24
  content: z.string().optional()
25
- .describe('日志内容Markdown (update)'),
25
+ .describe('日志内容Markdown (update, 内容长短由AI决定,支持完整Markdown)'),
26
26
  summary: z.string().optional()
27
27
  .describe('经验总结Markdown (archive)'),
28
28
  difficulties: z.array(z.string()).optional()
@@ -30,7 +30,11 @@ export function registerTaskTools(server, ctx) {
30
30
  solutions: z.array(z.string()).optional()
31
31
  .describe('解决方案 (archive)'),
32
32
  status: z.enum(['active', 'archived', 'all']).optional()
33
- .describe('状态筛选 (get, 默认all)'),
33
+ .describe('状态筛选 (get, 默认active)'),
34
+ bindTo: z.string().optional()
35
+ .describe('绑定到流程图节点ID (create, 如"n_entry")'),
36
+ bindChart: z.string().optional()
37
+ .describe('绑定目标流程图ID (create, 默认"main")'),
34
38
  }, async (args) => safeTool(async () => {
35
39
  const decoded = decodeObjectStrings(args);
36
40
  switch (decoded.action) {
@@ -44,13 +48,85 @@ export function registerTaskTools(server, ctx) {
44
48
  description: decoded.description,
45
49
  goals: decoded.goals || []
46
50
  }, ctx.user);
47
- return wrap(JSON.stringify(task, null, 2));
51
+ // 自动绑定到流程图节点
52
+ let bindMsg = '';
53
+ if (decoded.bindTo) {
54
+ try {
55
+ const chartId = decoded.bindChart || 'main';
56
+ const chart = await client().getFlowchart(chartId);
57
+ if (chart) {
58
+ const node = chart.nodes?.find((n) => n.id === decoded.bindTo);
59
+ if (node) {
60
+ if (!node.boundTasks)
61
+ node.boundTasks = [];
62
+ if (!node.boundTasks.includes(task.id)) {
63
+ node.boundTasks.push(task.id);
64
+ }
65
+ await client().saveFlowchart(chartId, chart);
66
+ bindMsg = `\n🔗 已绑定到节点: ${decoded.bindTo} [${chartId}]`;
67
+ }
68
+ else {
69
+ bindMsg = `\n⚠️ 节点 "${decoded.bindTo}" 不存在于流程图 "${chartId}"`;
70
+ }
71
+ }
72
+ }
73
+ catch {
74
+ bindMsg = `\n⚠️ 绑定失败`;
75
+ }
76
+ }
77
+ // 约束: 未绑定流程图 → 强制警告
78
+ if (!decoded.bindTo) {
79
+ bindMsg = `\n\n⚠️ 【未关联知识锚点】此任务未绑定到任何流程图节点!\n💡 建议: 使用 kg_flowchart(get) 查看节点列表, 然后用 kg_flowchart(bind, nodeId, tasks:["${task.id}"]) 绑定\n📌 不关联的任务会成为孤岛数据, 无法被其他AI发现`;
80
+ }
81
+ return wrap(`✅ 任务已创建\nID: ${task.id}\n标题: ${task.title}\n验收标准: ${task.detail.goals.length}条${bindMsg}`);
48
82
  }
49
83
  case 'get': {
50
- if (!decoded.title)
51
- return wrap('❌ get 需要 title');
52
- const filterStatus = decoded.status || 'all';
84
+ // 无参数 = 列出活跃任务
85
+ if (!decoded.taskId && !decoded.title) {
86
+ const status = decoded.status || 'active';
87
+ let tasks;
88
+ if (status === 'all') {
89
+ const [active, archived] = await Promise.all([
90
+ client().listTasks('active'),
91
+ client().listTasks('archived')
92
+ ]);
93
+ tasks = [...active, ...archived];
94
+ }
95
+ else {
96
+ tasks = await client().listTasks(status);
97
+ }
98
+ if (tasks.length === 0)
99
+ return wrap(`📋 ${status === 'archived' ? '归档' : '活跃'}任务为空`);
100
+ const list = tasks.map(t => `• [${t.status}] ${t.title}\n ID: ${t.id} | 更新: ${t.updated_at.split('T')[0]} | ${t.last_log || ''}`).join('\n');
101
+ return wrap(`📋 共 ${tasks.length} 个任务:\n\n${list}`);
102
+ }
103
+ // 有 taskId = 获取详情 (结构化摘要)
104
+ if (decoded.taskId) {
105
+ const task = await client().getTask(decoded.taskId, 'smart');
106
+ if (!task)
107
+ return wrap('❌ 任务未找到');
108
+ const lines = [
109
+ `📝 ${task.title}`,
110
+ `ID: ${task.id} | 状态: ${task.status}`,
111
+ `创建: ${task.created_at?.split('T')[0] || ''}`,
112
+ ];
113
+ if (task.detail?.description)
114
+ lines.push(`\n📋 ${task.detail.description}`);
115
+ if (task.detail?.goals?.length) {
116
+ lines.push(`\n🎯 验收标准:`);
117
+ task.detail.goals.forEach((g, i) => lines.push(` ${i + 1}. ☐ ${g}`));
118
+ }
119
+ if (task.logs?.length) {
120
+ lines.push(`\n📊 进度日志 (${task.logs.length}条, 最近3条):`);
121
+ for (const log of task.logs.slice(-3)) {
122
+ lines.push(` [${log.log_type}] ${log.time?.split('T')[0] || ''}: ${log.content?.substring(0, 120)}`);
123
+ }
124
+ }
125
+ return wrap(lines.join('\n'));
126
+ }
127
+ // 有 title = 按标题搜索
53
128
  const searchTitle = decoded.title.toLowerCase();
129
+ const filterStatus = decoded.status || 'all';
54
130
  let tasks;
55
131
  if (filterStatus === 'all') {
56
132
  const [active, archived] = await Promise.all([
@@ -66,26 +142,32 @@ export function registerTaskTools(server, ctx) {
66
142
  if (matched.length === 0)
67
143
  return wrap(`未找到匹配 "${decoded.title}" 的任务`);
68
144
  if (matched.length === 1) {
69
- const task = await client().getTask(matched[0].id);
70
- return wrap(task ? JSON.stringify(task, null, 2) : '任务详情获取失败');
145
+ const task = await client().getTask(matched[0].id, 'smart');
146
+ if (!task)
147
+ return wrap('任务详情获取失败');
148
+ return wrap(`📝 ${task.title}\nID: ${task.id} | 状态: ${task.status}`);
71
149
  }
72
- const list = matched.map(t => ({
73
- id: t.id, title: t.title, status: t.status,
74
- creator: t.creator, created_at: t.created_at
75
- }));
76
- return wrap(`找到 ${matched.length} 个匹配任务:\n${JSON.stringify(list, null, 2)}\n\n请提供更精确的任务名称`);
150
+ const list = matched.map(t => `• [${t.status}] ${t.title}\n ID: ${t.id}`).join('\n');
151
+ return wrap(`找到 ${matched.length} 个匹配任务:\n\n${list}\n\n请使用 taskId 获取详情`);
77
152
  }
78
153
  case 'update': {
79
154
  if (!decoded.taskId)
80
155
  return wrap('❌ update 需要 taskId');
81
- if (!decoded.log_type)
82
- return wrap('❌ update 需要 log_type');
83
156
  if (!decoded.content)
84
157
  return wrap('❌ update 需要 content');
85
- const task = await client().addTaskLog(decoded.taskId, decoded.log_type, decoded.content);
158
+ // log_type 可选 不传则后端自动检测关键词
159
+ const task = await client().addTaskLog(decoded.taskId, decoded.content, decoded.log_type);
86
160
  if (!task)
87
161
  return wrap('更新失败(任务不存在或已归档)');
88
- return wrap(`✅ 日志已添加,任务共有 ${task.logs.length} 条日志`);
162
+ const lastLog = task.logs[task.logs.length - 1];
163
+ // 返回任务摘要 + 验收清单,让 AI 始终知道剩余目标
164
+ const goalsDisplay = task.detail.goals.length > 0
165
+ ? task.detail.goals.map((g, i) => ` ${i + 1}. ☐ ${g}`).join('\n')
166
+ : ' (无验收标准)';
167
+ return wrap(`✅ 进度已记录 [${lastLog?.log_type || 'progress'}]\n` +
168
+ `📋 ${task.title} | 日志: ${task.logs.length}条\n` +
169
+ `\n🎯 验收标准:\n${goalsDisplay}\n` +
170
+ `\n⚠️ 提示: 每完成一个步骤请立即追加进度,确保时间线完整`);
89
171
  }
90
172
  case 'archive': {
91
173
  if (!decoded.taskId)
@@ -104,6 +186,12 @@ export function registerTaskTools(server, ctx) {
104
186
  }
105
187
  default:
106
188
  return wrap(`❌ 未知 action: ${decoded.action}`);
189
+ case 'delete': {
190
+ if (!decoded.taskId)
191
+ return wrap('❌ delete 需要 taskId');
192
+ const ok = await client().deleteTask(decoded.taskId);
193
+ return wrap(ok ? `✅ 任务已删除 (ID: ${decoded.taskId})` : '❌ 删除失败(任务不存在)');
194
+ }
107
195
  }
108
196
  }));
109
197
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ppdocs/mcp",
3
- "version": "3.2.22",
3
+ "version": "3.2.23",
4
4
  "description": "ppdocs MCP Server - Knowledge Graph for Claude",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",