@ppdocs/mcp 3.2.35 → 3.2.37

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.
Files changed (41) hide show
  1. package/README.md +53 -27
  2. package/dist/cli.js +4 -48
  3. package/dist/storage/httpClient.d.ts +2 -27
  4. package/dist/storage/httpClient.js +3 -132
  5. package/dist/storage/types.d.ts +0 -28
  6. package/dist/storage/types.js +1 -1
  7. package/dist/tools/flowchart.d.ts +1 -5
  8. package/dist/tools/flowchart.js +647 -454
  9. package/dist/tools/index.d.ts +3 -3
  10. package/dist/tools/index.js +10 -13
  11. package/dist/tools/kg_status.d.ts +1 -1
  12. package/dist/tools/kg_status.js +6 -20
  13. package/dist/tools/projects.d.ts +2 -3
  14. package/dist/tools/projects.js +2 -3
  15. package/dist/tools/rules.d.ts +2 -3
  16. package/dist/tools/rules.js +2 -3
  17. package/dist/tools/shared.d.ts +0 -12
  18. package/dist/tools/shared.js +0 -59
  19. package/package.json +1 -1
  20. package/templates/AGENT.md +63 -38
  21. package/templates/commands/init.md +3 -3
  22. package/templates/commands/pp/diagnose.md +3 -3
  23. package/templates/commands/pp/init.md +9 -18
  24. package/templates/commands/pp/sync.md +10 -12
  25. package/templates/cursorrules.md +63 -64
  26. package/templates/hooks/SystemPrompt.md +14 -14
  27. package/templates/kiro-rules/ppdocs.md +63 -142
  28. package/dist/agent.d.ts +0 -6
  29. package/dist/agent.js +0 -130
  30. package/dist/tools/docs.d.ts +0 -8
  31. package/dist/tools/docs.js +0 -285
  32. package/dist/tools/helpers.d.ts +0 -37
  33. package/dist/tools/helpers.js +0 -94
  34. package/dist/vector/index.d.ts +0 -56
  35. package/dist/vector/index.js +0 -228
  36. package/dist/vector/manager.d.ts +0 -48
  37. package/dist/vector/manager.js +0 -250
  38. package/dist/web/server.d.ts +0 -43
  39. package/dist/web/server.js +0 -808
  40. package/dist/web/ui.d.ts +0 -5
  41. package/dist/web/ui.js +0 -642
@@ -1,597 +1,790 @@
1
- /**
2
- * 🔀 kg_flowchart — 逻辑流程图批量操作
3
- * AI 一次提交所有节点+边, 后端原子处理, 返回孤立检测结果
4
- */
5
1
  import { z } from 'zod';
6
2
  import { getClient } from '../storage/httpClient.js';
7
3
  import { decodeObjectStrings } from '../utils.js';
8
4
  import { wrap, safeTool } from './shared.js';
9
5
  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'),
6
+ id: z.string().describe('node id'),
7
+ label: z.string().describe('node label'),
8
+ description: z.string().optional().describe('node description'),
9
+ nodeType: z.string().optional().describe('node type: super|process|data|entry'),
10
+ domain: z.string().optional().describe('domain: frontend|backend|mcp|system|infra'),
11
+ input: z.array(z.string()).optional().describe('input list'),
12
+ output: z.array(z.string()).optional().describe('output list'),
13
+ affiliation: z.string().optional().describe('parent node id, default root'),
14
+ subFlowchart: z.string().optional().describe('optional sub-chart id'),
18
15
  });
19
16
  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'),
17
+ from: z.string().describe('from node id'),
18
+ to: z.string().describe('to node id'),
19
+ label: z.string().optional().describe('edge label'),
20
+ edgeType: z.string().optional().describe('edge type: call|event|data|reference'),
24
21
  });
25
- export function registerFlowchartTools(server, ctx) {
22
+ function ensureArray(value) {
23
+ return Array.isArray(value) ? [...value] : [];
24
+ }
25
+ function ensureNodeShape(node) {
26
+ return {
27
+ ...node,
28
+ input: ensureArray(node.input),
29
+ output: ensureArray(node.output),
30
+ boundDocs: ensureArray(node.boundDocs),
31
+ boundFiles: ensureArray(node.boundFiles),
32
+ boundDirs: ensureArray(node.boundDirs),
33
+ boundTasks: ensureArray(node.boundTasks),
34
+ docEntries: Array.isArray(node.docEntries) ? [...node.docEntries] : [],
35
+ versions: Array.isArray(node.versions) ? [...node.versions] : [],
36
+ };
37
+ }
38
+ function buildNode(input) {
39
+ const now = new Date().toISOString();
40
+ return {
41
+ id: input.id,
42
+ label: input.label,
43
+ description: input.description ?? '',
44
+ nodeType: input.nodeType ?? 'process',
45
+ domain: input.domain ?? 'system',
46
+ input: ensureArray(input.input),
47
+ output: ensureArray(input.output),
48
+ affiliation: input.affiliation ?? 'root',
49
+ boundDocs: [],
50
+ boundFiles: [],
51
+ boundDirs: [],
52
+ boundTasks: [],
53
+ subFlowchart: input.subFlowchart || null,
54
+ docEntries: [],
55
+ versions: [{ version: 'v1.0', date: now, changes: 'initial' }],
56
+ lastUpdated: now,
57
+ lastQueried: '',
58
+ };
59
+ }
60
+ function buildEdge(input) {
61
+ return {
62
+ from: input.from,
63
+ to: input.to,
64
+ label: input.label ?? null,
65
+ edgeType: input.edgeType ?? 'call',
66
+ };
67
+ }
68
+ function formatNodeLine(node) {
69
+ const docCount = node.boundDocs?.length ?? 0;
70
+ const taskCount = node.boundTasks?.length ?? 0;
71
+ const fileCount = (node.boundFiles?.length ?? 0) + (node.boundDirs?.length ?? 0);
72
+ const badges = [
73
+ docCount > 0 ? `docs=${docCount}` : '',
74
+ taskCount > 0 ? `tasks=${taskCount}` : '',
75
+ fileCount > 0 ? `files=${fileCount}` : '',
76
+ node.subFlowchart ? `sub=${node.subFlowchart}` : '',
77
+ ].filter(Boolean);
78
+ const suffix = badges.length > 0 ? ` | ${badges.join(' ')}` : '';
79
+ return `- ${node.label} [${node.id}] (${node.nodeType ?? 'process'}/${node.domain ?? 'system'})${suffix}`;
80
+ }
81
+ function formatEdgeLine(edge) {
82
+ const label = edge.label ? ` [${edge.label}]` : '';
83
+ const type = edge.edgeType ? ` (${edge.edgeType})` : '';
84
+ return `- ${edge.from} -> ${edge.to}${label}${type}`;
85
+ }
86
+ function appendDocEntry(node, summary, content) {
87
+ const nextNode = ensureNodeShape(node);
88
+ const entries = nextNode.docEntries ?? [];
89
+ let nextVer = 0.1;
90
+ if (entries.length > 0) {
91
+ const last = entries[entries.length - 1]?.version ?? '';
92
+ const parsed = Number.parseFloat(last.replace(/^v/, ''));
93
+ if (!Number.isNaN(parsed)) {
94
+ nextVer = Math.round((parsed + 0.1) * 10) / 10;
95
+ }
96
+ }
97
+ entries.push({
98
+ version: `v${nextVer.toFixed(1)}`,
99
+ date: new Date().toISOString(),
100
+ summary: summary.replace(/\\n/g, '\n'),
101
+ content: content.replace(/\\n/g, '\n'),
102
+ });
103
+ nextNode.docEntries = entries;
104
+ return { node: nextNode, version: `v${nextVer.toFixed(1)}` };
105
+ }
106
+ function collectNeighborLayers(chart, startNodeId, maxDepth) {
107
+ const nodeMap = new Map(chart.nodes.map((node) => [node.id, node]));
108
+ const visited = new Set([startNodeId]);
109
+ let frontier = [startNodeId];
110
+ const layers = [];
111
+ for (let depth = 1; depth <= maxDepth; depth += 1) {
112
+ const layer = [];
113
+ const nextFrontier = [];
114
+ for (const currentId of frontier) {
115
+ for (const edge of chart.edges) {
116
+ if (edge.from === currentId && !visited.has(edge.to)) {
117
+ visited.add(edge.to);
118
+ nextFrontier.push(edge.to);
119
+ const node = nodeMap.get(edge.to);
120
+ layer.push(`- ${node?.label ?? edge.to} [${edge.to}]${edge.label ? ` (${edge.label})` : ''}${edge.edgeType ? ` [${edge.edgeType}]` : ''}`);
121
+ }
122
+ if (edge.to === currentId && !visited.has(edge.from)) {
123
+ visited.add(edge.from);
124
+ nextFrontier.push(edge.from);
125
+ const node = nodeMap.get(edge.from);
126
+ layer.push(`- ${node?.label ?? edge.from} [${edge.from}]${edge.label ? ` (${edge.label})` : ''}${edge.edgeType ? ` [${edge.edgeType}]` : ''}`);
127
+ }
128
+ }
129
+ }
130
+ if (layer.length === 0) {
131
+ break;
132
+ }
133
+ layers.push(layer);
134
+ frontier = nextFrontier;
135
+ }
136
+ return layers;
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
+ }
250
+ export function registerFlowchartTools(server, _ctx) {
26
251
  const client = () => getClient();
27
- server.tool('kg_flowchart', '🔀 逻辑流程图(关系型知识锚点) — action: list|get|get_node(★爆炸式上下文)|update_node|delete_node|batch_add|bind|unbind|orphans|health|create_chart|delete_chart', {
28
- action: z.enum(['list', 'get', 'get_node', 'update_node', 'delete_node', 'batch_add', 'bind', 'unbind', 'orphans', 'health', 'create_chart', 'delete_chart'])
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
- subFlowchart: z.string().optional().describe('update_node: bind sub-chart ID / auto-set on create_chart'),
53
- title: z.string().optional().describe('create_chart: sub-chart title'),
54
- parentChart: z.string().optional().describe('create_chart: parent chart ID (default main)'),
55
- parentNode: z.string().optional().describe('create_chart: mount to parent node ID'),
56
- // update_node: append doc entry
57
- docSummary: z.string().optional().describe('update_node: doc summary (one-line)'),
58
- docContent: z.string().optional().describe('update_node: doc content (full markdown)'),
59
- // get_node: doc display control
60
- fullDoc: z.boolean().optional().describe('get_node: show ALL doc entries in full (default false, only latest full)'),
61
- // bind/unbind
62
- files: z.array(z.string()).optional()
63
- .describe('源代码文件路径 (bind/unbind)'),
64
- dirs: z.array(z.string()).optional()
65
- .describe('源代码目录路径 (bind/unbind)'),
66
- docs: z.array(z.string()).optional()
67
- .describe('知识文档路径 (bind/unbind)'),
68
- tasks: z.array(z.string()).optional()
69
- .describe('任务ID (bind/unbind)'),
70
- // batch_add
71
- nodes: z.array(NodeSchema).optional()
72
- .describe('批量添加的节点数组 (batch_add)'),
73
- edges: z.array(EdgeSchema).optional()
74
- .describe('批量添加的边数组 (batch_add)'),
252
+ server.tool('kg_flowchart', 'Logical flowchart operations: list|get|search|get_relations|find_path|get_node|update_node|delete_node|batch_add|bind|unbind|orphans|health|create_chart|delete_chart', {
253
+ action: z
254
+ .enum(['list', 'get', 'search', 'get_relations', 'find_path', 'get_node', 'update_node', 'delete_node', 'batch_add', 'bind', 'unbind', 'orphans', 'health', 'create_chart', 'delete_chart'])
255
+ .describe('action type'),
256
+ chartId: z.string().optional().describe('chart id, default main'),
257
+ nodeId: z.string().optional().describe('node id for get_relations/get_node/update_node/delete_node/bind/unbind'),
258
+ query: z.string().optional().describe('search query for action=search'),
259
+ fromId: z.string().optional().describe('start node id for action=find_path'),
260
+ toId: z.string().optional().describe('target node id for action=find_path'),
261
+ limit: z.number().optional().describe('search result limit, default 10'),
262
+ expand: z.number().optional().describe('get_node expansion depth, default 3'),
263
+ includeDocs: z.boolean().optional().describe('show bound docs in get_node, default true'),
264
+ includeTasks: z.boolean().optional().describe('show bound tasks in get_node, default true'),
265
+ includeFiles: z.boolean().optional().describe('show bound files in get_node, default true'),
266
+ includeDoc: z.boolean().optional().describe('show node docEntries in get_node, default true'),
267
+ label: z.string().optional().describe('update_node label'),
268
+ description: z.string().optional().describe('update_node description'),
269
+ nodeType: z.string().optional().describe('update_node node type'),
270
+ domain: z.string().optional().describe('update_node domain'),
271
+ input: z.array(z.string()).optional().describe('update_node input list'),
272
+ output: z.array(z.string()).optional().describe('update_node output list'),
273
+ subFlowchart: z.string().optional().describe('update_node sub chart id, pass empty string to clear'),
274
+ title: z.string().optional().describe('create_chart title'),
275
+ parentChart: z.string().optional().describe('create_chart parent chart id, default main'),
276
+ parentNode: z.string().optional().describe('create_chart parent node id'),
277
+ docSummary: z.string().optional().describe('update_node doc summary'),
278
+ docContent: z.string().optional().describe('update_node doc content'),
279
+ fullDoc: z.boolean().optional().describe('get_node show all docEntries in full'),
280
+ files: z.array(z.string()).optional().describe('bind/unbind file paths'),
281
+ dirs: z.array(z.string()).optional().describe('bind/unbind directory paths'),
282
+ docs: z.array(z.string()).optional().describe('bind/unbind doc paths'),
283
+ tasks: z.array(z.string()).optional().describe('bind/unbind task ids'),
284
+ nodes: z.array(NodeSchema).optional().describe('batch_add/create_chart nodes'),
285
+ edges: z.array(EdgeSchema).optional().describe('batch_add/create_chart edges'),
75
286
  }, async (args) => safeTool(async () => {
76
287
  const decoded = decodeObjectStrings(args);
77
288
  switch (decoded.action) {
78
289
  case 'list': {
79
- const charts = await client().listFlowcharts();
80
- if (!charts || charts.length === 0)
81
- return wrap('📋 暂无流程图');
82
- const list = charts
83
- .map(c => `• ${c.title} [${c.id}] ${c.nodeCount}节点 ${c.edgeCount}连线`)
84
- .join('\n');
85
- return wrap(`📋 流程图列表:\n\n${list}`);
290
+ const charts = (await client().listFlowcharts());
291
+ if (!charts || charts.length === 0) {
292
+ return wrap('No flowcharts found.');
293
+ }
294
+ const lines = ['Flowcharts:', '', ...charts.map((chart) => {
295
+ const parent = chart.parentChart ? ` parent=${chart.parentChart}` : '';
296
+ return `- ${chart.title} [${chart.id}] ${chart.nodeCount} nodes / ${chart.edgeCount} edges${parent}`;
297
+ })];
298
+ return wrap(lines.join('\n'));
86
299
  }
87
300
  case 'get': {
88
301
  const chartId = decoded.chartId || 'main';
89
- const chart = await client().getFlowchart(chartId);
90
- if (!chart)
91
- return wrap(`❌ 流程图 "${chartId}" 不存在`);
92
- const nodes = chart.nodes || [];
93
- const edges = chart.edges || [];
302
+ const chart = (await client().getFlowchart(chartId));
303
+ if (!chart) {
304
+ return wrap(`Flowchart not found: ${chartId}`);
305
+ }
306
+ const lines = [
307
+ `Flowchart: ${chart.title} [${chart.id}]`,
308
+ chart.parentChart ? `Parent chart: ${chart.parentChart}` : '',
309
+ chart.parentNode ? `Parent node: ${chart.parentNode}` : '',
310
+ '',
311
+ `Nodes (${chart.nodes.length}):`,
312
+ ...chart.nodes.map(formatNodeLine),
313
+ '',
314
+ `Edges (${chart.edges.length}):`,
315
+ ...chart.edges.map(formatEdgeLine),
316
+ ].filter((line) => line.length > 0);
317
+ return wrap(lines.join('\n'));
318
+ }
319
+ case 'search': {
320
+ const rawQuery = decoded.query?.trim();
321
+ if (!rawQuery) {
322
+ return wrap('search requires query.');
323
+ }
324
+ const terms = rawQuery
325
+ .toLowerCase()
326
+ .split(/\s+/)
327
+ .filter(Boolean);
328
+ const charts = decoded.chartId
329
+ ? [(await client().getFlowchart(decoded.chartId))].filter((item) => Boolean(item))
330
+ : await (async () => {
331
+ const briefs = (await client().listFlowcharts());
332
+ const loaded = await Promise.all(briefs.map(async (brief) => (await client().getFlowchart(brief.id))));
333
+ return loaded.filter((item) => Boolean(item));
334
+ })();
335
+ const matches = charts
336
+ .flatMap((chart) => chart.nodes.map((rawNode) => {
337
+ const node = ensureNodeShape(rawNode);
338
+ return {
339
+ chart,
340
+ node,
341
+ score: scoreNodeMatch(node, terms),
342
+ };
343
+ }))
344
+ .filter((item) => item.score > 0)
345
+ .sort((a, b) => b.score - a.score)
346
+ .slice(0, Math.max(1, decoded.limit ?? 10));
347
+ if (matches.length === 0) {
348
+ return wrap(`No flowchart nodes matched query: ${rawQuery}`);
349
+ }
350
+ const lines = [
351
+ `Search results for: ${rawQuery}`,
352
+ '',
353
+ ...matches.map(({ chart, node }) => {
354
+ const badges = [
355
+ node.domain ? `domain=${node.domain}` : '',
356
+ node.nodeType ? `type=${node.nodeType}` : '',
357
+ node.subFlowchart ? `sub=${node.subFlowchart}` : '',
358
+ ].filter(Boolean);
359
+ const suffix = badges.length > 0 ? ` | ${badges.join(' ')}` : '';
360
+ const description = node.description ? ` | ${node.description}` : '';
361
+ return `- ${node.label} [${chart.id}/${node.id}]${suffix}${description}`;
362
+ }),
363
+ ];
364
+ return wrap(lines.join('\n'));
365
+ }
366
+ case 'get_relations': {
367
+ const chartId = decoded.chartId || 'main';
368
+ if (!decoded.nodeId) {
369
+ return wrap('get_relations requires nodeId.');
370
+ }
371
+ const chart = (await client().getFlowchart(chartId));
372
+ if (!chart) {
373
+ return wrap(`Flowchart not found: ${chartId}`);
374
+ }
375
+ const nodeMap = new Map(chart.nodes.map((node) => [node.id, node]));
376
+ const node = nodeMap.get(decoded.nodeId);
377
+ if (!node) {
378
+ return wrap(`Node not found: ${decoded.nodeId} in ${chartId}`);
379
+ }
380
+ const relations = collectNodeRelations(chart, decoded.nodeId);
381
+ const lines = [
382
+ `Relations: ${node.label} [${chartId}/${node.id}]`,
383
+ `Incoming: ${relations.incoming.length}`,
384
+ `Outgoing: ${relations.outgoing.length}`,
385
+ ];
386
+ if (relations.incoming.length > 0) {
387
+ lines.push('', 'Incoming edges:');
388
+ relations.incoming.forEach(({ edge, node: source }) => {
389
+ lines.push(`- ${source?.label ?? edge.from} [${edge.from}] -> ${node.label} [${edge.to}]${edge.label ? ` (${edge.label})` : ''}${edge.edgeType ? ` [${edge.edgeType}]` : ''}`);
390
+ });
391
+ }
392
+ if (relations.outgoing.length > 0) {
393
+ lines.push('', 'Outgoing edges:');
394
+ relations.outgoing.forEach(({ edge, node: target }) => {
395
+ lines.push(`- ${node.label} [${edge.from}] -> ${target?.label ?? edge.to} [${edge.to}]${edge.label ? ` (${edge.label})` : ''}${edge.edgeType ? ` [${edge.edgeType}]` : ''}`);
396
+ });
397
+ }
398
+ const linked = new Set([
399
+ ...relations.incoming.map(({ edge }) => edge.from),
400
+ ...relations.outgoing.map(({ edge }) => edge.to),
401
+ ]);
402
+ if (linked.size > 0) {
403
+ lines.push('', `Linked nodes: ${linked.size}`);
404
+ }
405
+ return wrap(lines.join('\n'));
406
+ }
407
+ case 'find_path': {
408
+ const chartId = decoded.chartId || 'main';
409
+ if (!decoded.fromId || !decoded.toId) {
410
+ return wrap('find_path requires fromId and toId.');
411
+ }
412
+ const chart = (await client().getFlowchart(chartId));
413
+ if (!chart) {
414
+ return wrap(`Flowchart not found: ${chartId}`);
415
+ }
416
+ const nodeMap = new Map(chart.nodes.map((node) => [node.id, node]));
417
+ const fromNode = nodeMap.get(decoded.fromId);
418
+ const toNode = nodeMap.get(decoded.toId);
419
+ if (!fromNode || !toNode) {
420
+ return wrap(`find_path could not resolve nodes in ${chartId}: from=${decoded.fromId} to=${decoded.toId}`);
421
+ }
422
+ const path = findDirectedPath(chart, decoded.fromId, decoded.toId);
423
+ if (path === null) {
424
+ return wrap(`No directed path found: [${chartId}/${decoded.fromId}] -> [${chartId}/${decoded.toId}]`);
425
+ }
426
+ if (path.length === 0) {
427
+ return wrap(`Path resolved: ${fromNode.label} [${fromNode.id}] (same node)`);
428
+ }
94
429
  const lines = [
95
- `📊 ${chart.title || chartId} [${chartId}]`,
96
- chart.parentChart ? `↩ 父图: ${chart.parentChart}` : '',
97
- ``,
98
- `### 节点 (${nodes.length})`,
99
- ...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}` : ''}`),
100
- ``,
101
- `### 连线 (${edges.length})`,
102
- ...edges.map((e) => ` ${e.from} →${e.label ? `[${e.label}]` : ''} ${e.to}`),
103
- ].filter(Boolean);
430
+ `Directed path: ${fromNode.label} [${fromNode.id}] -> ${toNode.label} [${toNode.id}]`,
431
+ `Hops: ${path.length}`,
432
+ '',
433
+ ];
434
+ let current = fromNode;
435
+ path.forEach((edge, index) => {
436
+ const nextNode = nodeMap.get(edge.to);
437
+ lines.push(`${index + 1}. ${current.label} [${edge.from}] -> ${nextNode?.label ?? edge.to} [${edge.to}]${edge.label ? ` (${edge.label})` : ''}${edge.edgeType ? ` [${edge.edgeType}]` : ''}`);
438
+ if (nextNode) {
439
+ current = nextNode;
440
+ }
441
+ });
104
442
  return wrap(lines.join('\n'));
105
443
  }
106
444
  case 'get_node': {
107
445
  const chartId = decoded.chartId || 'main';
108
- if (!decoded.nodeId)
109
- return wrap('get_node 需要 nodeId');
110
- const chart = await client().getFlowchart(chartId);
111
- if (!chart)
112
- return wrap(`❌ 流程图 "${chartId}" 不存在`);
113
- const allNodes = chart.nodes || [];
114
- const allEdges = chart.edges || [];
115
- const target = allNodes.find((n) => n.id === decoded.nodeId);
116
- if (!target)
117
- return wrap(`❌ 节点 "${decoded.nodeId}" 不存在于 "${chartId}"`);
118
- // 扩展参数 (全部默认开启)
119
- const expandDepth = decoded.expand !== undefined ? Number(decoded.expand) : 3;
446
+ if (!decoded.nodeId) {
447
+ return wrap('get_node requires nodeId.');
448
+ }
449
+ const chart = (await client().getFlowchart(chartId));
450
+ if (!chart) {
451
+ return wrap(`Flowchart not found: ${chartId}`);
452
+ }
453
+ const target = chart.nodes.find((node) => node.id === decoded.nodeId);
454
+ if (!target) {
455
+ return wrap(`Node not found: ${decoded.nodeId} in ${chartId}`);
456
+ }
457
+ const node = ensureNodeShape(target);
458
+ const expandDepth = decoded.expand ?? 3;
120
459
  const showDocs = decoded.includeDocs !== false;
121
460
  const showTasks = decoded.includeTasks !== false;
122
461
  const showFiles = decoded.includeFiles !== false;
123
462
  const showDoc = decoded.includeDoc !== false;
124
- const nodeMap = new Map(allNodes.map((n) => [n.id, n]));
125
- const lines = [];
126
- // ======== 节点基础信息 ========
127
- lines.push(`🔍 节点详情: ${target.label} [${target.id}]`);
128
- lines.push(`类型: ${target.nodeType || 'process'} | 领域: ${target.domain || 'system'}`);
129
- if (target.description)
130
- lines.push(`📝 ${target.description}`);
131
- if (target.input?.length)
132
- lines.push(`📥 输入: ${target.input.join(', ')}`);
133
- if (target.output?.length)
134
- lines.push(`📤 输出: ${target.output.join(', ')}`);
135
- if (target.subFlowchart)
136
- lines.push(`📂 子流程图: ${target.subFlowchart}`);
137
- if (target.lastUpdated)
138
- lines.push(`🕐 最后更新: ${target.lastUpdated}`);
463
+ const lines = [
464
+ `Node: ${node.label} [${node.id}]`,
465
+ `Type: ${node.nodeType ?? 'process'} | Domain: ${node.domain ?? 'system'}`,
466
+ ];
467
+ if (node.description) {
468
+ lines.push(`Description: ${node.description}`);
469
+ }
470
+ if (node.input && node.input.length > 0) {
471
+ lines.push(`Input: ${node.input.join(', ')}`);
472
+ }
473
+ if (node.output && node.output.length > 0) {
474
+ lines.push(`Output: ${node.output.join(', ')}`);
475
+ }
476
+ if (node.subFlowchart) {
477
+ lines.push(`Sub flowchart: ${node.subFlowchart}`);
478
+ }
479
+ if (node.lastUpdated) {
480
+ lines.push(`Last updated: ${node.lastUpdated}`);
481
+ }
482
+ if (node.lastQueried) {
483
+ lines.push(`Last queried: ${node.lastQueried}`);
484
+ }
139
485
  lines.push('');
140
- // ======== node doc entries ========
141
- if (showDoc && target.docEntries?.length) {
142
- const entries = target.docEntries;
143
- const showFullAll = decoded.fullDoc === true;
144
- lines.push(`### 📖 Node Documentation (${entries.length} entries)`);
145
- for (let i = 0; i < entries.length; i++) {
146
- const e = entries[i];
147
- const isLatest = i === entries.length - 1;
148
- if (isLatest || showFullAll) {
149
- lines.push(`\n**${e.version}** (${e.date})${isLatest ? ' ★LATEST' : ''}`);
150
- lines.push(`> ${e.summary}`);
151
- lines.push(e.content);
486
+ if (showDoc && node.docEntries && node.docEntries.length > 0) {
487
+ lines.push(`Node docs (${node.docEntries.length}):`);
488
+ const showAll = decoded.fullDoc === true;
489
+ node.docEntries.forEach((entry, index) => {
490
+ const isLatest = index === node.docEntries.length - 1;
491
+ if (isLatest || showAll) {
492
+ lines.push(`- ${entry.version} ${entry.date}${isLatest ? ' latest' : ''}`);
493
+ lines.push(` summary: ${entry.summary}`);
494
+ lines.push(...entry.content.split('\n').map((line) => ` ${line}`));
152
495
  }
153
496
  else {
154
- lines.push(` ${e.version} (${e.date}): ${e.summary}`);
497
+ lines.push(`- ${entry.version} ${entry.date}: ${entry.summary}`);
155
498
  }
156
- }
499
+ });
157
500
  lines.push('');
158
501
  }
159
- // ======== 爆炸扩展: N层关联节点 ========
160
502
  if (expandDepth > 0) {
161
- const visited = new Set([target.id]);
162
- let frontier = [target.id];
163
- const layerResults = [];
164
- for (let depth = 1; depth <= expandDepth; depth++) {
165
- const nextFrontier = [];
166
- const layerLines = [];
167
- for (const currentId of frontier) {
168
- // 出边
169
- for (const e of allEdges) {
170
- if (e.from === currentId && !visited.has(e.to)) {
171
- visited.add(e.to);
172
- nextFrontier.push(e.to);
173
- const neighbor = nodeMap.get(e.to);
174
- const label = neighbor?.label || e.to;
175
- layerLines.push(` → ${label} [${e.to}] ${e.label ? `(${e.label})` : ''} ${e.edgeType ? `[${e.edgeType}]` : ''}`);
176
- }
177
- // 入边
178
- if (e.to === currentId && !visited.has(e.from)) {
179
- visited.add(e.from);
180
- nextFrontier.push(e.from);
181
- const neighbor = nodeMap.get(e.from);
182
- const label = neighbor?.label || e.from;
183
- layerLines.push(` ← ${label} [${e.from}] ${e.label ? `(${e.label})` : ''} ${e.edgeType ? `[${e.edgeType}]` : ''}`);
184
- }
185
- }
186
- }
187
- if (layerLines.length > 0)
188
- layerResults.push(layerLines);
189
- frontier = nextFrontier;
190
- if (frontier.length === 0)
191
- break;
192
- }
193
- if (layerResults.length > 0) {
194
- lines.push(`### 🔗 关联节点 (扩展${layerResults.length}层, 共${visited.size - 1}个)`);
195
- layerResults.forEach((layer, i) => {
196
- lines.push(`**L${i + 1}** (${layer.length}个):`);
197
- lines.push(...layer);
503
+ const layers = collectNeighborLayers(chart, node.id, expandDepth);
504
+ if (layers.length > 0) {
505
+ lines.push(`Linked nodes (${layers.length} layers):`);
506
+ layers.forEach((layer, index) => {
507
+ lines.push(`L${index + 1}:`);
508
+ lines.push(...layer.map((item) => ` ${item}`));
198
509
  });
199
510
  lines.push('');
200
511
  }
201
512
  }
202
- // ======== 绑定的参考文档 ========
203
- if (showDocs && target.boundDocs?.length) {
204
- lines.push(`### 📄 绑定参考文档 (${target.boundDocs.length})`);
205
- for (const docPath of target.boundDocs) {
206
- try {
207
- const doc = await client().getDoc(docPath);
208
- if (doc) {
209
- lines.push(` 📄 ${docPath} — ${doc.summary || '无摘要'}`);
210
- if (doc.status)
211
- lines.push(` 状态: ${doc.status}`);
212
- }
213
- else {
214
- lines.push(` 📄 ${docPath} (未找到)`);
215
- }
216
- }
217
- catch {
218
- lines.push(` 📄 ${docPath}`);
219
- }
220
- }
513
+ if (showDocs && node.boundDocs && node.boundDocs.length > 0) {
514
+ lines.push(`Bound docs (${node.boundDocs.length}):`);
515
+ node.boundDocs.forEach((docPath) => lines.push(`- ${docPath}`));
221
516
  lines.push('');
222
517
  }
223
- // ======== 绑定的任务 ========
224
- if (showTasks && target.boundTasks?.length) {
225
- lines.push(`### 📝 绑定任务 (${target.boundTasks.length})`);
226
- for (const taskId of target.boundTasks) {
518
+ if (showTasks && node.boundTasks && node.boundTasks.length > 0) {
519
+ lines.push(`Bound tasks (${node.boundTasks.length}):`);
520
+ for (const taskId of node.boundTasks) {
227
521
  try {
228
- const task = await client().getTask(taskId);
522
+ const task = (await client().getTask(taskId));
229
523
  if (task) {
230
- lines.push(` 📝 [${taskId}] ${task.title} ${task.status}`);
524
+ lines.push(`- [${taskId}] ${task.title ?? ''} ${task.status ? `(${task.status})` : ''}`.trim());
231
525
  }
232
526
  else {
233
- lines.push(` 📝 [${taskId}] (未找到)`);
527
+ lines.push(`- [${taskId}]`);
234
528
  }
235
529
  }
236
530
  catch {
237
- lines.push(` 📝 [${taskId}]`);
531
+ lines.push(`- [${taskId}]`);
238
532
  }
239
533
  }
240
534
  lines.push('');
241
535
  }
242
- // ======== 绑定的代码文件 ========
243
536
  if (showFiles) {
244
- const files = target.boundFiles || [];
245
- const dirs = target.boundDirs || [];
246
- if (files.length + dirs.length > 0) {
247
- lines.push(`### 📂 绑定代码 (${files.length}文件 ${dirs.length}目录)`);
248
- for (const f of files)
249
- lines.push(` 📄 ${f}`);
250
- for (const d of dirs)
251
- lines.push(` 📁 ${d}`);
537
+ const files = node.boundFiles ?? [];
538
+ const dirs = node.boundDirs ?? [];
539
+ if (files.length > 0 || dirs.length > 0) {
540
+ lines.push(`Bound code (${files.length} files / ${dirs.length} dirs):`);
541
+ files.forEach((filePath) => lines.push(`- file: ${filePath}`));
542
+ dirs.forEach((dirPath) => lines.push(`- dir: ${dirPath}`));
252
543
  lines.push('');
253
544
  }
254
545
  }
255
- // ======== 版本历史 ========
256
- if (target.versions?.length) {
257
- lines.push(`### 📋 版本历史 (${target.versions.length})`);
258
- for (const v of target.versions.slice(-5)) {
259
- lines.push(` v${v.version} (${v.date}): ${v.changes}`);
260
- }
546
+ if (node.versions && node.versions.length > 0) {
547
+ lines.push(`Versions (${node.versions.length}):`);
548
+ node.versions.slice(-5).forEach((version) => {
549
+ lines.push(`- ${version.version} ${version.date}: ${version.changes}`);
550
+ });
261
551
  }
262
- return wrap(lines.join('\n'));
552
+ return wrap(lines.join('\n').trim());
263
553
  }
264
554
  case 'update_node': {
265
555
  const chartId = decoded.chartId || 'main';
266
- if (!decoded.nodeId)
267
- return wrap('update_node 需要 nodeId');
268
- const chart = await client().getFlowchart(chartId);
269
- if (!chart)
270
- return wrap(`❌ 流程图 "${chartId}" 不存在`);
271
- const node = (chart.nodes || []).find((n) => n.id === decoded.nodeId);
272
- if (!node)
273
- return wrap(`❌ 节点 "${decoded.nodeId}" 不存在`);
556
+ if (!decoded.nodeId) {
557
+ return wrap('update_node requires nodeId.');
558
+ }
559
+ const chart = (await client().getFlowchart(chartId));
560
+ if (!chart) {
561
+ return wrap(`Flowchart not found: ${chartId}`);
562
+ }
563
+ const rawNode = chart.nodes.find((node) => node.id === decoded.nodeId);
564
+ if (!rawNode) {
565
+ return wrap(`Node not found: ${decoded.nodeId}`);
566
+ }
567
+ let nextNode = ensureNodeShape(rawNode);
274
568
  const changes = [];
275
569
  if (decoded.label !== undefined) {
276
- node.label = decoded.label;
570
+ nextNode.label = decoded.label;
277
571
  changes.push('label');
278
572
  }
279
573
  if (decoded.description !== undefined) {
280
- node.description = decoded.description;
574
+ nextNode.description = decoded.description;
281
575
  changes.push('description');
282
576
  }
283
577
  if (decoded.nodeType !== undefined) {
284
- node.nodeType = decoded.nodeType;
578
+ nextNode.nodeType = decoded.nodeType;
285
579
  changes.push('nodeType');
286
580
  }
287
581
  if (decoded.domain !== undefined) {
288
- node.domain = decoded.domain;
582
+ nextNode.domain = decoded.domain;
289
583
  changes.push('domain');
290
584
  }
291
585
  if (decoded.input !== undefined) {
292
- node.input = decoded.input;
586
+ nextNode.input = [...decoded.input];
293
587
  changes.push('input');
294
588
  }
295
589
  if (decoded.output !== undefined) {
296
- node.output = decoded.output;
590
+ nextNode.output = [...decoded.output];
297
591
  changes.push('output');
298
592
  }
299
593
  if (decoded.subFlowchart !== undefined) {
300
- node.subFlowchart = decoded.subFlowchart;
594
+ nextNode.subFlowchart = decoded.subFlowchart || null;
301
595
  changes.push('subFlowchart');
302
596
  }
303
- // append doc entry
304
597
  if (decoded.docSummary || decoded.docContent) {
305
598
  if (!decoded.docSummary || !decoded.docContent) {
306
- return wrap('\u274c update_node: docSummary and docContent must both be provided');
307
- }
308
- const docEntries = node.docEntries || [];
309
- // auto-increment version: v0.1, v0.2, ...
310
- let nextVer = 0.1;
311
- if (docEntries.length > 0) {
312
- const lastVer = docEntries[docEntries.length - 1].version;
313
- const num = parseFloat(lastVer.replace('v', ''));
314
- if (!isNaN(num))
315
- nextVer = Math.round((num + 0.1) * 10) / 10;
599
+ return wrap('update_node requires both docSummary and docContent when updating docs.');
316
600
  }
317
- // normalize escaped newlines from AI clients (e.g. "\\n" → actual newline)
318
- const normalizedContent = decoded.docContent.replace(/\\n/g, '\n');
319
- const normalizedSummary = decoded.docSummary.replace(/\\n/g, '\n');
320
- docEntries.push({
321
- version: `v${nextVer.toFixed(1)}`,
322
- date: new Date().toISOString(),
323
- summary: normalizedSummary,
324
- content: normalizedContent,
325
- });
326
- node.docEntries = docEntries;
327
- changes.push(`docEntries(v${nextVer.toFixed(1)})`);
328
- }
329
- if (changes.length === 0)
330
- return wrap('ℹ️ 无变更 — 请提供要更新的字段');
331
- node.lastUpdated = new Date().toISOString();
332
- await client().saveFlowchart(chartId, chart);
333
- return wrap(`✅ 节点已更新: ${node.label} [${chartId}/${decoded.nodeId}]\n更新字段: ${changes.join(', ')}`);
601
+ const result = appendDocEntry(nextNode, decoded.docSummary, decoded.docContent);
602
+ nextNode = result.node;
603
+ changes.push(`docEntries(${result.version})`);
604
+ }
605
+ if (changes.length === 0) {
606
+ return wrap('No fields were updated.');
607
+ }
608
+ await client().updateFlowchartNode(chartId, decoded.nodeId, nextNode, `MCP update: ${changes.join(', ')}`);
609
+ return wrap(`Node updated: ${nextNode.label} [${chartId}/${decoded.nodeId}]\nFields: ${changes.join(', ')}`);
334
610
  }
335
611
  case 'delete_node': {
336
612
  const chartId = decoded.chartId || 'main';
337
- if (!decoded.nodeId)
338
- return wrap('delete_node 需要 nodeId');
339
- // 原子调用: DELETE /flowcharts/:chartId/nodes/:nodeId (后端自动清理关联边)
613
+ if (!decoded.nodeId) {
614
+ return wrap('delete_node requires nodeId.');
615
+ }
340
616
  await client().deleteFlowchartNode(chartId, decoded.nodeId);
341
- return wrap(`✅ 节点已删除 [${chartId}/${decoded.nodeId}]`);
617
+ return wrap(`Node deleted: [${chartId}/${decoded.nodeId}]`);
342
618
  }
343
619
  case 'batch_add': {
344
620
  const chartId = decoded.chartId || 'main';
345
621
  if (!decoded.nodes || decoded.nodes.length === 0) {
346
- return wrap('batch_add 需要 nodes 数组');
347
- }
348
- // 自动填充默认值 AI 只需填 id+label+description
349
- const fullNodes = decoded.nodes.map((n) => ({
350
- id: n.id,
351
- label: n.label,
352
- nodeType: n.nodeType || 'process',
353
- domain: n.domain || 'system',
354
- input: n.input || [],
355
- output: n.output || [],
356
- description: n.description || '',
357
- affiliation: n.affiliation || 'root',
358
- boundDocs: [],
359
- boundFiles: [],
360
- boundDirs: [],
361
- boundTasks: [],
362
- subFlowchart: n.subFlowchart || null,
363
- position: null,
364
- versions: [{ version: 'v1.0', date: new Date().toISOString(), changes: 'initial' }],
365
- lastUpdated: new Date().toISOString(),
366
- lastQueried: '',
367
- }));
368
- const fullEdges = (decoded.edges || []).map((e) => ({
369
- from: e.from,
370
- to: e.to,
371
- label: e.label || null,
372
- edgeType: e.edgeType || 'call',
373
- }));
374
- const result = await client().batchAddToFlowchart(chartId, fullNodes, fullEdges);
375
- // 构建报告
376
- let report = `✅ 批量操作完成 [${chartId}]\n`;
377
- report += ` 添加: ${result.addedNodes.length}节点 ${result.addedEdges}连线\n`;
622
+ return wrap('batch_add requires nodes.');
623
+ }
624
+ const result = (await client().batchAddToFlowchart(chartId, decoded.nodes.map(buildNode), (decoded.edges ?? []).map(buildEdge)));
625
+ const lines = [
626
+ `Batch add finished: [${chartId}]`,
627
+ `Added nodes: ${result.addedNodes.length}`,
628
+ `Added edges: ${result.addedEdges}`,
629
+ ];
378
630
  if (result.failedNodes.length > 0) {
379
- report += `\n❌ 失败节点:\n`;
380
- result.failedNodes.forEach(([id, reason]) => {
381
- report += ` • ${id}: ${reason}\n`;
382
- });
631
+ lines.push('', 'Failed nodes:');
632
+ result.failedNodes.forEach(([nodeId, reason]) => lines.push(`- ${nodeId}: ${reason}`));
383
633
  }
384
634
  if (result.failedEdges.length > 0) {
385
- report += `\n❌ 失败连线:\n`;
386
- result.failedEdges.forEach(([from, to, reason]) => {
387
- report += ` • ${from} → ${to}: ${reason}\n`;
388
- });
635
+ lines.push('', 'Failed edges:');
636
+ result.failedEdges.forEach(([from, to, reason]) => lines.push(`- ${from} -> ${to}: ${reason}`));
389
637
  }
390
638
  if (result.orphanedNodes.length > 0) {
391
- report += `\n⚠️ 孤立节点 (无任何连线):\n`;
392
- result.orphanedNodes.forEach(id => {
393
- report += ` • ${id}\n`;
394
- });
639
+ lines.push('', 'Orphaned nodes:');
640
+ result.orphanedNodes.forEach((nodeId) => lines.push(`- ${nodeId}`));
395
641
  }
396
642
  else {
397
- report += `\n✅ 无孤立节点`;
643
+ lines.push('', 'Orphaned nodes: none');
398
644
  }
399
- return wrap(report);
645
+ return wrap(lines.join('\n'));
400
646
  }
401
647
  case 'orphans': {
402
- const orphans = await client().getFlowchartOrphans();
648
+ const orphans = (await client().getFlowchartOrphans());
403
649
  if (!orphans || orphans.length === 0) {
404
- return wrap(' 无孤立文档 所有文档均已绑定到流程图节点');
650
+ return wrap('No orphan reference docs reported by backend.');
405
651
  }
406
- const list = orphans
407
- .map(o => `• ${o.docPath}${o.docSummary ? ` — ${o.docSummary}` : ''}`)
408
- .join('\n');
409
- return wrap(`⚠️ 发现 ${orphans.length} 个孤立文档 (未绑定到任何流程图节点):\n\n${list}\n\n💡 使用 kg_doc(update, bindTo: "节点ID") 绑定`);
652
+ const lines = ['Orphan reference docs:', '', ...orphans.map((item) => `- ${item.docPath}${item.docSummary ? ` | ${item.docSummary}` : ''}`)];
653
+ return wrap(lines.join('\n'));
410
654
  }
411
655
  case 'health': {
412
- const health = await client().getFlowchartHealth();
413
- if (!health || health.length === 0) {
414
- return wrap('📊 暂无节点健康数据');
415
- }
416
- const items = health;
417
- const stale = items.filter(h => h.status === 'stale');
418
- const active = items.filter(h => h.status === 'active');
419
- const normal = items.filter(h => h.status === 'normal');
420
- let report = `📊 知识冷热分布 (${items.length}节点):\n`;
421
- report += ` 🟢 active (7天内查询): ${active.length}\n`;
422
- report += ` 🟡 normal (30天内): ${normal.length}\n`;
423
- report += ` 🔴 stale (30天+未查询): ${stale.length}\n`;
656
+ const items = (await client().getFlowchartHealth());
657
+ if (!items || items.length === 0) {
658
+ return wrap('No flowchart health data reported by backend.');
659
+ }
660
+ const stale = items.filter((item) => item.status === 'stale');
661
+ const normal = items.filter((item) => item.status === 'normal');
662
+ const active = items.filter((item) => item.status === 'active');
663
+ const lines = [
664
+ `Node health: ${items.length} nodes`,
665
+ `active: ${active.length}`,
666
+ `normal: ${normal.length}`,
667
+ `stale: ${stale.length}`,
668
+ ];
424
669
  if (stale.length > 0) {
425
- report += `\n🔴 冷知识节点:\n`;
426
- stale.forEach(h => {
427
- report += ` • ${h.nodeLabel} [${h.chartId}/${h.nodeId}] — ${h.daysSinceQuery}天未查询\n`;
428
- });
670
+ lines.push('', 'Stale nodes:');
671
+ stale.forEach((item) => lines.push(`- ${item.nodeLabel} [${item.chartId}/${item.nodeId}] ${item.daysSinceQuery} days`));
429
672
  }
430
- return wrap(report);
673
+ return wrap(lines.join('\n'));
431
674
  }
432
675
  case 'bind':
433
676
  case 'unbind': {
434
677
  const chartId = decoded.chartId || 'main';
435
- if (!decoded.nodeId)
436
- return wrap(`❌ ${decoded.action} 需要 nodeId`);
437
- const chart = await client().getFlowchart(chartId);
438
- if (!chart)
439
- return wrap(`❌ 流程图 "${chartId}" 不存在`);
440
- const node = chart.nodes?.find((n) => n.id === decoded.nodeId);
441
- if (!node)
442
- return wrap(`❌ 节点 "${decoded.nodeId}" 不存在于流程图 "${chartId}"`);
678
+ if (!decoded.nodeId) {
679
+ return wrap(`${decoded.action} requires nodeId.`);
680
+ }
681
+ const chart = (await client().getFlowchart(chartId));
682
+ if (!chart) {
683
+ return wrap(`Flowchart not found: ${chartId}`);
684
+ }
685
+ const rawNode = chart.nodes.find((node) => node.id === decoded.nodeId);
686
+ if (!rawNode) {
687
+ return wrap(`Node not found: ${decoded.nodeId} in ${chartId}`);
688
+ }
689
+ const node = ensureNodeShape(rawNode);
443
690
  const isBind = decoded.action === 'bind';
444
691
  const changes = [];
445
- const modify = (items, field, label) => {
446
- if (!items || items.length === 0)
692
+ const mutate = (incoming, field, label) => {
693
+ const items = incoming ?? [];
694
+ if (items.length === 0) {
447
695
  return;
448
- if (!node[field])
449
- node[field] = [];
696
+ }
697
+ const current = new Set(node[field] ?? []);
450
698
  if (isBind) {
451
699
  let added = 0;
452
700
  for (const item of items) {
453
- if (!node[field].includes(item)) {
454
- node[field].push(item);
455
- added++;
701
+ if (!current.has(item)) {
702
+ current.add(item);
703
+ added += 1;
456
704
  }
457
705
  }
458
- if (added > 0)
706
+ if (added > 0) {
459
707
  changes.push(`+${added} ${label}`);
708
+ }
460
709
  }
461
710
  else {
462
- const before = node[field].length;
463
- node[field] = node[field].filter((x) => !items.includes(x));
464
- const removed = before - node[field].length;
465
- if (removed > 0)
711
+ let removed = 0;
712
+ for (const item of items) {
713
+ if (current.delete(item)) {
714
+ removed += 1;
715
+ }
716
+ }
717
+ if (removed > 0) {
466
718
  changes.push(`-${removed} ${label}`);
719
+ }
467
720
  }
721
+ node[field] = [...current];
468
722
  };
469
- modify(decoded.files, 'boundFiles', '文件');
470
- modify(decoded.dirs, 'boundDirs', '目录');
471
- modify(decoded.docs, 'boundDocs', '文档');
472
- modify(decoded.tasks, 'boundTasks', '任务');
723
+ mutate(decoded.files, 'boundFiles', 'files');
724
+ mutate(decoded.dirs, 'boundDirs', 'dirs');
725
+ mutate(decoded.docs, 'boundDocs', 'docs');
726
+ mutate(decoded.tasks, 'boundTasks', 'tasks');
473
727
  if (changes.length === 0) {
474
- return wrap(`ℹ️ 无变更 — 请提供 files/dirs/docs/tasks 参数`);
475
- }
476
- await client().saveFlowchart(chartId, chart);
477
- // P2: 双向同步 — 更新参考文档的 adoptedBy
478
- const docsList = decoded.docs;
479
- if (docsList && docsList.length > 0) {
480
- const nodeId = decoded.nodeId;
481
- for (const docPath of docsList) {
482
- try {
483
- const doc = await client().getDoc(docPath);
484
- if (!doc?.content)
485
- continue;
486
- const content = doc.content;
487
- // 解析 frontmatter
488
- if (!content.startsWith('---'))
489
- continue;
490
- const endIdx = content.indexOf('---', 3);
491
- if (endIdx === -1)
492
- continue;
493
- const yaml = content.slice(3, endIdx);
494
- const body = content.slice(endIdx + 3);
495
- // 提取现有 adoptedBy
496
- const adoptedMatch = yaml.match(/adoptedBy:\s*\[(.*?)\]/);
497
- let adopted = [];
498
- if (adoptedMatch) {
499
- adopted = adoptedMatch[1].split(',').map(s => s.trim()).filter(Boolean);
500
- }
501
- // 修改 adoptedBy
502
- if (isBind) {
503
- if (!adopted.includes(nodeId))
504
- adopted.push(nodeId);
505
- }
506
- else {
507
- adopted = adopted.filter(id => id !== nodeId);
508
- }
509
- // 重建 frontmatter
510
- const newAdoptedLine = `adoptedBy: [${adopted.join(', ')}]`;
511
- let newYaml;
512
- if (adoptedMatch) {
513
- newYaml = yaml.replace(/adoptedBy:\s*\[.*?\]/, newAdoptedLine);
514
- }
515
- else {
516
- newYaml = yaml.trimEnd() + '\n' + newAdoptedLine + '\n';
517
- }
518
- const newContent = `---${newYaml}---${body}`;
519
- await client().updateDoc(docPath, { content: newContent });
520
- }
521
- catch { /* 单个文档同步失败不阻断 */ }
522
- }
728
+ return wrap('No bindings changed.');
523
729
  }
524
- const icon = isBind ? '🔗' : '⛓️';
525
- return wrap(`${icon} ${isBind ? '绑定' : '解绑'}完成 [${decoded.nodeId}]: ${changes.join(', ')}`);
730
+ await client().updateFlowchartNode(chartId, decoded.nodeId, node, `${isBind ? 'Bind' : 'Unbind'} resources: ${changes.join(', ')}`);
731
+ return wrap(`${isBind ? 'Bound' : 'Unbound'} resources for [${decoded.nodeId}]: ${changes.join(', ')}`);
526
732
  }
527
733
  case 'create_chart': {
528
734
  const newChartId = decoded.chartId;
529
- if (!newChartId)
530
- return wrap('create_chart 需要 chartId (子图ID)');
531
- if (newChartId === 'main')
532
- return wrap('❌ 不能创建 main,主图已存在');
533
- // 检查是否已存在
534
- const existing = await client().getFlowchart(newChartId);
535
- if (existing)
536
- return wrap(`❌ 流程图 "${newChartId}" 已存在`);
537
- const pChart = decoded.parentChart || 'main';
538
- const pNode = decoded.parentNode;
539
- // 构建子图数据
540
- const subChartData = {
735
+ if (!newChartId) {
736
+ return wrap('create_chart requires chartId.');
737
+ }
738
+ if (newChartId === 'main') {
739
+ return wrap('create_chart cannot create main.');
740
+ }
741
+ const existing = (await client().getFlowchart(newChartId));
742
+ if (existing) {
743
+ return wrap(`Flowchart already exists: ${newChartId}`);
744
+ }
745
+ const parentChartId = decoded.parentChart || 'main';
746
+ const subChart = {
541
747
  id: newChartId,
542
748
  title: decoded.title || newChartId,
543
- parentNode: pNode || null,
544
- parentChart: pChart,
545
- nodes: [],
546
- edges: [],
749
+ parentNode: decoded.parentNode || null,
750
+ parentChart: parentChartId,
751
+ nodes: (decoded.nodes ?? []).map(buildNode),
752
+ edges: (decoded.edges ?? []).map(buildEdge),
547
753
  };
548
- // 如果提供了 nodes/edges,填充进去
549
- if (decoded.nodes && decoded.nodes.length > 0) {
550
- subChartData.nodes = decoded.nodes.map((n) => ({
551
- id: n.id, label: n.label,
552
- nodeType: n.nodeType || 'process', domain: n.domain || 'system',
553
- input: n.input || [], output: n.output || [],
554
- description: n.description || '', affiliation: n.affiliation || 'root',
555
- boundDocs: [], boundFiles: [], boundDirs: [], boundTasks: [],
556
- subFlowchart: null, position: null,
557
- versions: [{ version: 'v1.0', date: new Date().toISOString(), changes: 'initial' }],
558
- lastUpdated: new Date().toISOString(), lastQueried: '',
559
- }));
560
- }
561
- if (decoded.edges && decoded.edges.length > 0) {
562
- subChartData.edges = decoded.edges.map(e => ({
563
- from: e.from, to: e.to, label: e.label || null, edgeType: e.edgeType || 'call',
564
- }));
565
- }
566
- // 保存子图
567
- await client().saveFlowchart(newChartId, subChartData);
568
- // 自动更新父节点的 subFlowchart 引用
569
- if (pNode) {
570
- const parentChart = await client().getFlowchart(pChart);
754
+ await client().saveFlowchart(newChartId, subChart);
755
+ let parentMessage = `Parent chart: ${parentChartId}`;
756
+ if (decoded.parentNode) {
757
+ const parentChart = (await client().getFlowchart(parentChartId));
571
758
  if (parentChart) {
572
- const parentNodeObj = (parentChart.nodes || []).find((n) => n.id === pNode);
573
- if (parentNodeObj) {
574
- parentNodeObj.subFlowchart = newChartId;
575
- parentNodeObj.lastUpdated = new Date().toISOString();
576
- await client().saveFlowchart(pChart, parentChart);
759
+ const parentNode = parentChart.nodes.find((node) => node.id === decoded.parentNode);
760
+ if (parentNode) {
761
+ const updatedParent = ensureNodeShape(parentNode);
762
+ updatedParent.subFlowchart = newChartId;
763
+ await client().updateFlowchartNode(parentChartId, decoded.parentNode, updatedParent, `Attach sub flowchart ${newChartId}`);
764
+ parentMessage = `Parent chart: ${parentChartId} | Parent node: ${decoded.parentNode}`;
765
+ }
766
+ else {
767
+ parentMessage = `Parent chart: ${parentChartId} | Parent node not found: ${decoded.parentNode}`;
577
768
  }
578
769
  }
770
+ else {
771
+ parentMessage = `Parent chart not found: ${parentChartId}`;
772
+ }
579
773
  }
580
- const nodeCount = subChartData.nodes.length;
581
- return wrap(`✅ 子图已创建: ${decoded.title || newChartId} [${newChartId}]\n父图: ${pChart}${pNode ? ` → 节点: ${pNode}` : ''}\n初始: ${nodeCount}节点`);
774
+ return wrap(`Sub chart created: ${subChart.title} [${newChartId}]\n${parentMessage}\nInitial nodes: ${subChart.nodes.length}\nInitial edges: ${subChart.edges.length}`);
582
775
  }
583
776
  case 'delete_chart': {
584
- const chartId = decoded.chartId;
585
- if (!chartId)
586
- return wrap('❌ delete_chart 需要 chartId');
587
- if (chartId === 'main')
588
- return wrap(' 不能删除主图 main');
589
- // 后端自动级联: 清父节点引用 + 删子图的子图
590
- await client().deleteFlowchart(chartId);
591
- return wrap(`✅ 流程图已删除 [${chartId}] (子图已级联清理)`);
777
+ if (!decoded.chartId) {
778
+ return wrap('delete_chart requires chartId.');
779
+ }
780
+ if (decoded.chartId === 'main') {
781
+ return wrap('delete_chart cannot remove main.');
782
+ }
783
+ await client().deleteFlowchart(decoded.chartId);
784
+ return wrap(`Flowchart deleted: [${decoded.chartId}]`);
592
785
  }
593
786
  default:
594
- return wrap(`❌ 未知 action: ${decoded.action}`);
787
+ return wrap(`Unknown action: ${String(decoded.action)}`);
595
788
  }
596
789
  }));
597
790
  }