@ppdocs/mcp 3.2.35 → 3.2.36

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.
@@ -1,597 +1,549 @@
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
+ export function registerFlowchartTools(server, _ctx) {
26
139
  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)'),
140
+ server.tool('kg_flowchart', 'Logical flowchart operations: list|get|get_node|update_node|delete_node|batch_add|bind|unbind|orphans|health|create_chart|delete_chart', {
141
+ action: z
142
+ .enum(['list', 'get', 'get_node', 'update_node', 'delete_node', 'batch_add', 'bind', 'unbind', 'orphans', 'health', 'create_chart', 'delete_chart'])
143
+ .describe('action type'),
144
+ chartId: z.string().optional().describe('chart id, default main'),
145
+ nodeId: z.string().optional().describe('node id for get_node/update_node/delete_node/bind/unbind'),
146
+ expand: z.number().optional().describe('get_node expansion depth, default 3'),
147
+ includeDocs: z.boolean().optional().describe('show bound docs in get_node, default true'),
148
+ includeTasks: z.boolean().optional().describe('show bound tasks in get_node, default true'),
149
+ includeFiles: z.boolean().optional().describe('show bound files in get_node, default true'),
150
+ includeDoc: z.boolean().optional().describe('show node docEntries in get_node, default true'),
151
+ label: z.string().optional().describe('update_node label'),
152
+ description: z.string().optional().describe('update_node description'),
153
+ nodeType: z.string().optional().describe('update_node node type'),
154
+ domain: z.string().optional().describe('update_node domain'),
155
+ input: z.array(z.string()).optional().describe('update_node input list'),
156
+ output: z.array(z.string()).optional().describe('update_node output list'),
157
+ subFlowchart: z.string().optional().describe('update_node sub chart id, pass empty string to clear'),
158
+ title: z.string().optional().describe('create_chart title'),
159
+ parentChart: z.string().optional().describe('create_chart parent chart id, default main'),
160
+ parentNode: z.string().optional().describe('create_chart parent node id'),
161
+ docSummary: z.string().optional().describe('update_node doc summary'),
162
+ docContent: z.string().optional().describe('update_node doc content'),
163
+ fullDoc: z.boolean().optional().describe('get_node show all docEntries in full'),
164
+ files: z.array(z.string()).optional().describe('bind/unbind file paths'),
165
+ dirs: z.array(z.string()).optional().describe('bind/unbind directory paths'),
166
+ docs: z.array(z.string()).optional().describe('bind/unbind doc paths'),
167
+ tasks: z.array(z.string()).optional().describe('bind/unbind task ids'),
168
+ nodes: z.array(NodeSchema).optional().describe('batch_add/create_chart nodes'),
169
+ edges: z.array(EdgeSchema).optional().describe('batch_add/create_chart edges'),
75
170
  }, async (args) => safeTool(async () => {
76
171
  const decoded = decodeObjectStrings(args);
77
172
  switch (decoded.action) {
78
173
  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}`);
174
+ const charts = (await client().listFlowcharts());
175
+ if (!charts || charts.length === 0) {
176
+ return wrap('No flowcharts found.');
177
+ }
178
+ const lines = ['Flowcharts:', '', ...charts.map((chart) => {
179
+ const parent = chart.parentChart ? ` parent=${chart.parentChart}` : '';
180
+ return `- ${chart.title} [${chart.id}] ${chart.nodeCount} nodes / ${chart.edgeCount} edges${parent}`;
181
+ })];
182
+ return wrap(lines.join('\n'));
86
183
  }
87
184
  case 'get': {
88
185
  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 || [];
186
+ const chart = (await client().getFlowchart(chartId));
187
+ if (!chart) {
188
+ return wrap(`Flowchart not found: ${chartId}`);
189
+ }
94
190
  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);
191
+ `Flowchart: ${chart.title} [${chart.id}]`,
192
+ chart.parentChart ? `Parent chart: ${chart.parentChart}` : '',
193
+ chart.parentNode ? `Parent node: ${chart.parentNode}` : '',
194
+ '',
195
+ `Nodes (${chart.nodes.length}):`,
196
+ ...chart.nodes.map(formatNodeLine),
197
+ '',
198
+ `Edges (${chart.edges.length}):`,
199
+ ...chart.edges.map(formatEdgeLine),
200
+ ].filter((line) => line.length > 0);
104
201
  return wrap(lines.join('\n'));
105
202
  }
106
203
  case 'get_node': {
107
204
  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;
205
+ if (!decoded.nodeId) {
206
+ return wrap('get_node requires nodeId.');
207
+ }
208
+ const chart = (await client().getFlowchart(chartId));
209
+ if (!chart) {
210
+ return wrap(`Flowchart not found: ${chartId}`);
211
+ }
212
+ const target = chart.nodes.find((node) => node.id === decoded.nodeId);
213
+ if (!target) {
214
+ return wrap(`Node not found: ${decoded.nodeId} in ${chartId}`);
215
+ }
216
+ const node = ensureNodeShape(target);
217
+ const expandDepth = decoded.expand ?? 3;
120
218
  const showDocs = decoded.includeDocs !== false;
121
219
  const showTasks = decoded.includeTasks !== false;
122
220
  const showFiles = decoded.includeFiles !== false;
123
221
  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}`);
222
+ const lines = [
223
+ `Node: ${node.label} [${node.id}]`,
224
+ `Type: ${node.nodeType ?? 'process'} | Domain: ${node.domain ?? 'system'}`,
225
+ ];
226
+ if (node.description) {
227
+ lines.push(`Description: ${node.description}`);
228
+ }
229
+ if (node.input && node.input.length > 0) {
230
+ lines.push(`Input: ${node.input.join(', ')}`);
231
+ }
232
+ if (node.output && node.output.length > 0) {
233
+ lines.push(`Output: ${node.output.join(', ')}`);
234
+ }
235
+ if (node.subFlowchart) {
236
+ lines.push(`Sub flowchart: ${node.subFlowchart}`);
237
+ }
238
+ if (node.lastUpdated) {
239
+ lines.push(`Last updated: ${node.lastUpdated}`);
240
+ }
241
+ if (node.lastQueried) {
242
+ lines.push(`Last queried: ${node.lastQueried}`);
243
+ }
139
244
  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);
245
+ if (showDoc && node.docEntries && node.docEntries.length > 0) {
246
+ lines.push(`Node docs (${node.docEntries.length}):`);
247
+ const showAll = decoded.fullDoc === true;
248
+ node.docEntries.forEach((entry, index) => {
249
+ const isLatest = index === node.docEntries.length - 1;
250
+ if (isLatest || showAll) {
251
+ lines.push(`- ${entry.version} ${entry.date}${isLatest ? ' latest' : ''}`);
252
+ lines.push(` summary: ${entry.summary}`);
253
+ lines.push(...entry.content.split('\n').map((line) => ` ${line}`));
152
254
  }
153
255
  else {
154
- lines.push(` ${e.version} (${e.date}): ${e.summary}`);
256
+ lines.push(`- ${entry.version} ${entry.date}: ${entry.summary}`);
155
257
  }
156
- }
258
+ });
157
259
  lines.push('');
158
260
  }
159
- // ======== 爆炸扩展: N层关联节点 ========
160
261
  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);
262
+ const layers = collectNeighborLayers(chart, node.id, expandDepth);
263
+ if (layers.length > 0) {
264
+ lines.push(`Linked nodes (${layers.length} layers):`);
265
+ layers.forEach((layer, index) => {
266
+ lines.push(`L${index + 1}:`);
267
+ lines.push(...layer.map((item) => ` ${item}`));
198
268
  });
199
269
  lines.push('');
200
270
  }
201
271
  }
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
- }
272
+ if (showDocs && node.boundDocs && node.boundDocs.length > 0) {
273
+ lines.push(`Bound docs (${node.boundDocs.length}):`);
274
+ node.boundDocs.forEach((docPath) => lines.push(`- ${docPath}`));
221
275
  lines.push('');
222
276
  }
223
- // ======== 绑定的任务 ========
224
- if (showTasks && target.boundTasks?.length) {
225
- lines.push(`### 📝 绑定任务 (${target.boundTasks.length})`);
226
- for (const taskId of target.boundTasks) {
277
+ if (showTasks && node.boundTasks && node.boundTasks.length > 0) {
278
+ lines.push(`Bound tasks (${node.boundTasks.length}):`);
279
+ for (const taskId of node.boundTasks) {
227
280
  try {
228
- const task = await client().getTask(taskId);
281
+ const task = (await client().getTask(taskId));
229
282
  if (task) {
230
- lines.push(` 📝 [${taskId}] ${task.title} ${task.status}`);
283
+ lines.push(`- [${taskId}] ${task.title ?? ''} ${task.status ? `(${task.status})` : ''}`.trim());
231
284
  }
232
285
  else {
233
- lines.push(` 📝 [${taskId}] (未找到)`);
286
+ lines.push(`- [${taskId}]`);
234
287
  }
235
288
  }
236
289
  catch {
237
- lines.push(` 📝 [${taskId}]`);
290
+ lines.push(`- [${taskId}]`);
238
291
  }
239
292
  }
240
293
  lines.push('');
241
294
  }
242
- // ======== 绑定的代码文件 ========
243
295
  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}`);
296
+ const files = node.boundFiles ?? [];
297
+ const dirs = node.boundDirs ?? [];
298
+ if (files.length > 0 || dirs.length > 0) {
299
+ lines.push(`Bound code (${files.length} files / ${dirs.length} dirs):`);
300
+ files.forEach((filePath) => lines.push(`- file: ${filePath}`));
301
+ dirs.forEach((dirPath) => lines.push(`- dir: ${dirPath}`));
252
302
  lines.push('');
253
303
  }
254
304
  }
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
- }
305
+ if (node.versions && node.versions.length > 0) {
306
+ lines.push(`Versions (${node.versions.length}):`);
307
+ node.versions.slice(-5).forEach((version) => {
308
+ lines.push(`- ${version.version} ${version.date}: ${version.changes}`);
309
+ });
261
310
  }
262
- return wrap(lines.join('\n'));
311
+ return wrap(lines.join('\n').trim());
263
312
  }
264
313
  case 'update_node': {
265
314
  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}" 不存在`);
315
+ if (!decoded.nodeId) {
316
+ return wrap('update_node requires nodeId.');
317
+ }
318
+ const chart = (await client().getFlowchart(chartId));
319
+ if (!chart) {
320
+ return wrap(`Flowchart not found: ${chartId}`);
321
+ }
322
+ const rawNode = chart.nodes.find((node) => node.id === decoded.nodeId);
323
+ if (!rawNode) {
324
+ return wrap(`Node not found: ${decoded.nodeId}`);
325
+ }
326
+ let nextNode = ensureNodeShape(rawNode);
274
327
  const changes = [];
275
328
  if (decoded.label !== undefined) {
276
- node.label = decoded.label;
329
+ nextNode.label = decoded.label;
277
330
  changes.push('label');
278
331
  }
279
332
  if (decoded.description !== undefined) {
280
- node.description = decoded.description;
333
+ nextNode.description = decoded.description;
281
334
  changes.push('description');
282
335
  }
283
336
  if (decoded.nodeType !== undefined) {
284
- node.nodeType = decoded.nodeType;
337
+ nextNode.nodeType = decoded.nodeType;
285
338
  changes.push('nodeType');
286
339
  }
287
340
  if (decoded.domain !== undefined) {
288
- node.domain = decoded.domain;
341
+ nextNode.domain = decoded.domain;
289
342
  changes.push('domain');
290
343
  }
291
344
  if (decoded.input !== undefined) {
292
- node.input = decoded.input;
345
+ nextNode.input = [...decoded.input];
293
346
  changes.push('input');
294
347
  }
295
348
  if (decoded.output !== undefined) {
296
- node.output = decoded.output;
349
+ nextNode.output = [...decoded.output];
297
350
  changes.push('output');
298
351
  }
299
352
  if (decoded.subFlowchart !== undefined) {
300
- node.subFlowchart = decoded.subFlowchart;
353
+ nextNode.subFlowchart = decoded.subFlowchart || null;
301
354
  changes.push('subFlowchart');
302
355
  }
303
- // append doc entry
304
356
  if (decoded.docSummary || decoded.docContent) {
305
357
  if (!decoded.docSummary || !decoded.docContent) {
306
- return wrap('\u274c update_node: docSummary and docContent must both be provided');
358
+ return wrap('update_node requires both docSummary and docContent when updating docs.');
307
359
  }
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;
316
- }
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(', ')}`);
360
+ const result = appendDocEntry(nextNode, decoded.docSummary, decoded.docContent);
361
+ nextNode = result.node;
362
+ changes.push(`docEntries(${result.version})`);
363
+ }
364
+ if (changes.length === 0) {
365
+ return wrap('No fields were updated.');
366
+ }
367
+ await client().updateFlowchartNode(chartId, decoded.nodeId, nextNode, `MCP update: ${changes.join(', ')}`);
368
+ return wrap(`Node updated: ${nextNode.label} [${chartId}/${decoded.nodeId}]\nFields: ${changes.join(', ')}`);
334
369
  }
335
370
  case 'delete_node': {
336
371
  const chartId = decoded.chartId || 'main';
337
- if (!decoded.nodeId)
338
- return wrap('delete_node 需要 nodeId');
339
- // 原子调用: DELETE /flowcharts/:chartId/nodes/:nodeId (后端自动清理关联边)
372
+ if (!decoded.nodeId) {
373
+ return wrap('delete_node requires nodeId.');
374
+ }
340
375
  await client().deleteFlowchartNode(chartId, decoded.nodeId);
341
- return wrap(`✅ 节点已删除 [${chartId}/${decoded.nodeId}]`);
376
+ return wrap(`Node deleted: [${chartId}/${decoded.nodeId}]`);
342
377
  }
343
378
  case 'batch_add': {
344
379
  const chartId = decoded.chartId || 'main';
345
380
  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`;
381
+ return wrap('batch_add requires nodes.');
382
+ }
383
+ const result = (await client().batchAddToFlowchart(chartId, decoded.nodes.map(buildNode), (decoded.edges ?? []).map(buildEdge)));
384
+ const lines = [
385
+ `Batch add finished: [${chartId}]`,
386
+ `Added nodes: ${result.addedNodes.length}`,
387
+ `Added edges: ${result.addedEdges}`,
388
+ ];
378
389
  if (result.failedNodes.length > 0) {
379
- report += `\n❌ 失败节点:\n`;
380
- result.failedNodes.forEach(([id, reason]) => {
381
- report += ` • ${id}: ${reason}\n`;
382
- });
390
+ lines.push('', 'Failed nodes:');
391
+ result.failedNodes.forEach(([nodeId, reason]) => lines.push(`- ${nodeId}: ${reason}`));
383
392
  }
384
393
  if (result.failedEdges.length > 0) {
385
- report += `\n❌ 失败连线:\n`;
386
- result.failedEdges.forEach(([from, to, reason]) => {
387
- report += ` • ${from} → ${to}: ${reason}\n`;
388
- });
394
+ lines.push('', 'Failed edges:');
395
+ result.failedEdges.forEach(([from, to, reason]) => lines.push(`- ${from} -> ${to}: ${reason}`));
389
396
  }
390
397
  if (result.orphanedNodes.length > 0) {
391
- report += `\n⚠️ 孤立节点 (无任何连线):\n`;
392
- result.orphanedNodes.forEach(id => {
393
- report += ` • ${id}\n`;
394
- });
398
+ lines.push('', 'Orphaned nodes:');
399
+ result.orphanedNodes.forEach((nodeId) => lines.push(`- ${nodeId}`));
395
400
  }
396
401
  else {
397
- report += `\n✅ 无孤立节点`;
402
+ lines.push('', 'Orphaned nodes: none');
398
403
  }
399
- return wrap(report);
404
+ return wrap(lines.join('\n'));
400
405
  }
401
406
  case 'orphans': {
402
- const orphans = await client().getFlowchartOrphans();
407
+ const orphans = (await client().getFlowchartOrphans());
403
408
  if (!orphans || orphans.length === 0) {
404
- return wrap(' 无孤立文档 所有文档均已绑定到流程图节点');
409
+ return wrap('No orphan reference docs reported by backend.');
405
410
  }
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") 绑定`);
411
+ const lines = ['Orphan reference docs:', '', ...orphans.map((item) => `- ${item.docPath}${item.docSummary ? ` | ${item.docSummary}` : ''}`)];
412
+ return wrap(lines.join('\n'));
410
413
  }
411
414
  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`;
415
+ const items = (await client().getFlowchartHealth());
416
+ if (!items || items.length === 0) {
417
+ return wrap('No flowchart health data reported by backend.');
418
+ }
419
+ const stale = items.filter((item) => item.status === 'stale');
420
+ const normal = items.filter((item) => item.status === 'normal');
421
+ const active = items.filter((item) => item.status === 'active');
422
+ const lines = [
423
+ `Node health: ${items.length} nodes`,
424
+ `active: ${active.length}`,
425
+ `normal: ${normal.length}`,
426
+ `stale: ${stale.length}`,
427
+ ];
424
428
  if (stale.length > 0) {
425
- report += `\n🔴 冷知识节点:\n`;
426
- stale.forEach(h => {
427
- report += ` • ${h.nodeLabel} [${h.chartId}/${h.nodeId}] — ${h.daysSinceQuery}天未查询\n`;
428
- });
429
+ lines.push('', 'Stale nodes:');
430
+ stale.forEach((item) => lines.push(`- ${item.nodeLabel} [${item.chartId}/${item.nodeId}] ${item.daysSinceQuery} days`));
429
431
  }
430
- return wrap(report);
432
+ return wrap(lines.join('\n'));
431
433
  }
432
434
  case 'bind':
433
435
  case 'unbind': {
434
436
  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}"`);
437
+ if (!decoded.nodeId) {
438
+ return wrap(`${decoded.action} requires nodeId.`);
439
+ }
440
+ const chart = (await client().getFlowchart(chartId));
441
+ if (!chart) {
442
+ return wrap(`Flowchart not found: ${chartId}`);
443
+ }
444
+ const rawNode = chart.nodes.find((node) => node.id === decoded.nodeId);
445
+ if (!rawNode) {
446
+ return wrap(`Node not found: ${decoded.nodeId} in ${chartId}`);
447
+ }
448
+ const node = ensureNodeShape(rawNode);
443
449
  const isBind = decoded.action === 'bind';
444
450
  const changes = [];
445
- const modify = (items, field, label) => {
446
- if (!items || items.length === 0)
451
+ const mutate = (incoming, field, label) => {
452
+ const items = incoming ?? [];
453
+ if (items.length === 0) {
447
454
  return;
448
- if (!node[field])
449
- node[field] = [];
455
+ }
456
+ const current = new Set(node[field] ?? []);
450
457
  if (isBind) {
451
458
  let added = 0;
452
459
  for (const item of items) {
453
- if (!node[field].includes(item)) {
454
- node[field].push(item);
455
- added++;
460
+ if (!current.has(item)) {
461
+ current.add(item);
462
+ added += 1;
456
463
  }
457
464
  }
458
- if (added > 0)
465
+ if (added > 0) {
459
466
  changes.push(`+${added} ${label}`);
467
+ }
460
468
  }
461
469
  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)
470
+ let removed = 0;
471
+ for (const item of items) {
472
+ if (current.delete(item)) {
473
+ removed += 1;
474
+ }
475
+ }
476
+ if (removed > 0) {
466
477
  changes.push(`-${removed} ${label}`);
478
+ }
467
479
  }
480
+ node[field] = [...current];
468
481
  };
469
- modify(decoded.files, 'boundFiles', '文件');
470
- modify(decoded.dirs, 'boundDirs', '目录');
471
- modify(decoded.docs, 'boundDocs', '文档');
472
- modify(decoded.tasks, 'boundTasks', '任务');
482
+ mutate(decoded.files, 'boundFiles', 'files');
483
+ mutate(decoded.dirs, 'boundDirs', 'dirs');
484
+ mutate(decoded.docs, 'boundDocs', 'docs');
485
+ mutate(decoded.tasks, 'boundTasks', 'tasks');
473
486
  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
- }
487
+ return wrap('No bindings changed.');
523
488
  }
524
- const icon = isBind ? '🔗' : '⛓️';
525
- return wrap(`${icon} ${isBind ? '绑定' : '解绑'}完成 [${decoded.nodeId}]: ${changes.join(', ')}`);
489
+ await client().updateFlowchartNode(chartId, decoded.nodeId, node, `${isBind ? 'Bind' : 'Unbind'} resources: ${changes.join(', ')}`);
490
+ return wrap(`${isBind ? 'Bound' : 'Unbound'} resources for [${decoded.nodeId}]: ${changes.join(', ')}`);
526
491
  }
527
492
  case 'create_chart': {
528
493
  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 = {
494
+ if (!newChartId) {
495
+ return wrap('create_chart requires chartId.');
496
+ }
497
+ if (newChartId === 'main') {
498
+ return wrap('create_chart cannot create main.');
499
+ }
500
+ const existing = (await client().getFlowchart(newChartId));
501
+ if (existing) {
502
+ return wrap(`Flowchart already exists: ${newChartId}`);
503
+ }
504
+ const parentChartId = decoded.parentChart || 'main';
505
+ const subChart = {
541
506
  id: newChartId,
542
507
  title: decoded.title || newChartId,
543
- parentNode: pNode || null,
544
- parentChart: pChart,
545
- nodes: [],
546
- edges: [],
508
+ parentNode: decoded.parentNode || null,
509
+ parentChart: parentChartId,
510
+ nodes: (decoded.nodes ?? []).map(buildNode),
511
+ edges: (decoded.edges ?? []).map(buildEdge),
547
512
  };
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);
513
+ await client().saveFlowchart(newChartId, subChart);
514
+ let parentMessage = `Parent chart: ${parentChartId}`;
515
+ if (decoded.parentNode) {
516
+ const parentChart = (await client().getFlowchart(parentChartId));
571
517
  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);
518
+ const parentNode = parentChart.nodes.find((node) => node.id === decoded.parentNode);
519
+ if (parentNode) {
520
+ const updatedParent = ensureNodeShape(parentNode);
521
+ updatedParent.subFlowchart = newChartId;
522
+ await client().updateFlowchartNode(parentChartId, decoded.parentNode, updatedParent, `Attach sub flowchart ${newChartId}`);
523
+ parentMessage = `Parent chart: ${parentChartId} | Parent node: ${decoded.parentNode}`;
577
524
  }
525
+ else {
526
+ parentMessage = `Parent chart: ${parentChartId} | Parent node not found: ${decoded.parentNode}`;
527
+ }
528
+ }
529
+ else {
530
+ parentMessage = `Parent chart not found: ${parentChartId}`;
578
531
  }
579
532
  }
580
- const nodeCount = subChartData.nodes.length;
581
- return wrap(`✅ 子图已创建: ${decoded.title || newChartId} [${newChartId}]\n父图: ${pChart}${pNode ? ` → 节点: ${pNode}` : ''}\n初始: ${nodeCount}节点`);
533
+ return wrap(`Sub chart created: ${subChart.title} [${newChartId}]\n${parentMessage}\nInitial nodes: ${subChart.nodes.length}\nInitial edges: ${subChart.edges.length}`);
582
534
  }
583
535
  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}] (子图已级联清理)`);
536
+ if (!decoded.chartId) {
537
+ return wrap('delete_chart requires chartId.');
538
+ }
539
+ if (decoded.chartId === 'main') {
540
+ return wrap('delete_chart cannot remove main.');
541
+ }
542
+ await client().deleteFlowchart(decoded.chartId);
543
+ return wrap(`Flowchart deleted: [${decoded.chartId}]`);
592
544
  }
593
545
  default:
594
- return wrap(`❌ 未知 action: ${decoded.action}`);
546
+ return wrap(`Unknown action: ${String(decoded.action)}`);
595
547
  }
596
548
  }));
597
549
  }