@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.
- package/README.md +53 -27
- package/dist/cli.js +4 -48
- package/dist/storage/httpClient.d.ts +2 -27
- package/dist/storage/httpClient.js +3 -132
- package/dist/storage/types.d.ts +0 -28
- package/dist/storage/types.js +1 -1
- package/dist/tools/flowchart.d.ts +1 -5
- package/dist/tools/flowchart.js +647 -454
- package/dist/tools/index.d.ts +3 -3
- package/dist/tools/index.js +10 -13
- package/dist/tools/kg_status.d.ts +1 -1
- package/dist/tools/kg_status.js +6 -20
- package/dist/tools/projects.d.ts +2 -3
- package/dist/tools/projects.js +2 -3
- package/dist/tools/rules.d.ts +2 -3
- package/dist/tools/rules.js +2 -3
- package/dist/tools/shared.d.ts +0 -12
- package/dist/tools/shared.js +0 -59
- package/package.json +1 -1
- package/templates/AGENT.md +63 -38
- package/templates/commands/init.md +3 -3
- package/templates/commands/pp/diagnose.md +3 -3
- package/templates/commands/pp/init.md +9 -18
- package/templates/commands/pp/sync.md +10 -12
- package/templates/cursorrules.md +63 -64
- package/templates/hooks/SystemPrompt.md +14 -14
- package/templates/kiro-rules/ppdocs.md +63 -142
- package/dist/agent.d.ts +0 -6
- package/dist/agent.js +0 -130
- package/dist/tools/docs.d.ts +0 -8
- package/dist/tools/docs.js +0 -285
- package/dist/tools/helpers.d.ts +0 -37
- package/dist/tools/helpers.js +0 -94
- package/dist/vector/index.d.ts +0 -56
- package/dist/vector/index.js +0 -228
- package/dist/vector/manager.d.ts +0 -48
- package/dist/vector/manager.js +0 -250
- package/dist/web/server.d.ts +0 -43
- package/dist/web/server.js +0 -808
- package/dist/web/ui.d.ts +0 -5
- package/dist/web/ui.js +0 -642
package/dist/tools/flowchart.js
CHANGED
|
@@ -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('
|
|
11
|
-
label: z.string().describe('
|
|
12
|
-
description: z.string().optional().describe('
|
|
13
|
-
nodeType: z.string().optional().describe('
|
|
14
|
-
domain: z.string().optional().describe('
|
|
15
|
-
input: z.array(z.string()).optional(),
|
|
16
|
-
output: z.array(z.string()).optional(),
|
|
17
|
-
affiliation: z.string().optional().describe('
|
|
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('
|
|
21
|
-
to: z.string().describe('
|
|
22
|
-
label: z.string().optional().describe('
|
|
23
|
-
edgeType: z.string().optional().describe('
|
|
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
|
-
|
|
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', '
|
|
28
|
-
action: z
|
|
29
|
-
.
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
nodeId: z.string().optional()
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
includeTasks: z.boolean().optional()
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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(
|
|
92
|
-
|
|
93
|
-
const
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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('
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const target =
|
|
116
|
-
if (!target)
|
|
117
|
-
return wrap(
|
|
118
|
-
|
|
119
|
-
const
|
|
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
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
if (
|
|
132
|
-
lines.push(
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
if (
|
|
138
|
-
lines.push(
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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(
|
|
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
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
204
|
-
lines.push(
|
|
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
|
-
|
|
225
|
-
|
|
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(
|
|
524
|
+
lines.push(`- [${taskId}] ${task.title ?? ''} ${task.status ? `(${task.status})` : ''}`.trim());
|
|
231
525
|
}
|
|
232
526
|
else {
|
|
233
|
-
lines.push(
|
|
527
|
+
lines.push(`- [${taskId}]`);
|
|
234
528
|
}
|
|
235
529
|
}
|
|
236
530
|
catch {
|
|
237
|
-
lines.push(
|
|
531
|
+
lines.push(`- [${taskId}]`);
|
|
238
532
|
}
|
|
239
533
|
}
|
|
240
534
|
lines.push('');
|
|
241
535
|
}
|
|
242
|
-
// ======== 绑定的代码文件 ========
|
|
243
536
|
if (showFiles) {
|
|
244
|
-
const files =
|
|
245
|
-
const dirs =
|
|
246
|
-
if (files.length
|
|
247
|
-
lines.push(
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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('
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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
|
-
|
|
570
|
+
nextNode.label = decoded.label;
|
|
277
571
|
changes.push('label');
|
|
278
572
|
}
|
|
279
573
|
if (decoded.description !== undefined) {
|
|
280
|
-
|
|
574
|
+
nextNode.description = decoded.description;
|
|
281
575
|
changes.push('description');
|
|
282
576
|
}
|
|
283
577
|
if (decoded.nodeType !== undefined) {
|
|
284
|
-
|
|
578
|
+
nextNode.nodeType = decoded.nodeType;
|
|
285
579
|
changes.push('nodeType');
|
|
286
580
|
}
|
|
287
581
|
if (decoded.domain !== undefined) {
|
|
288
|
-
|
|
582
|
+
nextNode.domain = decoded.domain;
|
|
289
583
|
changes.push('domain');
|
|
290
584
|
}
|
|
291
585
|
if (decoded.input !== undefined) {
|
|
292
|
-
|
|
586
|
+
nextNode.input = [...decoded.input];
|
|
293
587
|
changes.push('input');
|
|
294
588
|
}
|
|
295
589
|
if (decoded.output !== undefined) {
|
|
296
|
-
|
|
590
|
+
nextNode.output = [...decoded.output];
|
|
297
591
|
changes.push('output');
|
|
298
592
|
}
|
|
299
593
|
if (decoded.subFlowchart !== undefined) {
|
|
300
|
-
|
|
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('
|
|
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
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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('
|
|
339
|
-
|
|
613
|
+
if (!decoded.nodeId) {
|
|
614
|
+
return wrap('delete_node requires nodeId.');
|
|
615
|
+
}
|
|
340
616
|
await client().deleteFlowchartNode(chartId, decoded.nodeId);
|
|
341
|
-
return wrap(
|
|
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('
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
const
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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
|
-
|
|
380
|
-
result.failedNodes.forEach(([
|
|
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
|
-
|
|
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
|
-
|
|
392
|
-
result.orphanedNodes.forEach(
|
|
393
|
-
report += ` • ${id}\n`;
|
|
394
|
-
});
|
|
639
|
+
lines.push('', 'Orphaned nodes:');
|
|
640
|
+
result.orphanedNodes.forEach((nodeId) => lines.push(`- ${nodeId}`));
|
|
395
641
|
}
|
|
396
642
|
else {
|
|
397
|
-
|
|
643
|
+
lines.push('', 'Orphaned nodes: none');
|
|
398
644
|
}
|
|
399
|
-
return wrap(
|
|
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
|
|
407
|
-
|
|
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
|
|
413
|
-
if (!
|
|
414
|
-
return wrap('
|
|
415
|
-
}
|
|
416
|
-
const
|
|
417
|
-
const
|
|
418
|
-
const active = items.filter(
|
|
419
|
-
const
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
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
|
-
|
|
426
|
-
stale.forEach(
|
|
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(
|
|
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(
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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
|
|
446
|
-
|
|
692
|
+
const mutate = (incoming, field, label) => {
|
|
693
|
+
const items = incoming ?? [];
|
|
694
|
+
if (items.length === 0) {
|
|
447
695
|
return;
|
|
448
|
-
|
|
449
|
-
|
|
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 (!
|
|
454
|
-
|
|
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
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
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
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
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(
|
|
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
|
-
|
|
525
|
-
return wrap(`${
|
|
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('
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
const
|
|
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:
|
|
544
|
-
parentChart:
|
|
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
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
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
|
|
573
|
-
if (
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
await client().
|
|
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
|
-
|
|
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
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
if (chartId === 'main')
|
|
588
|
-
return wrap('
|
|
589
|
-
|
|
590
|
-
await client().deleteFlowchart(chartId);
|
|
591
|
-
return wrap(
|
|
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(
|
|
787
|
+
return wrap(`Unknown action: ${String(decoded.action)}`);
|
|
595
788
|
}
|
|
596
789
|
}));
|
|
597
790
|
}
|