flowmind 1.5.2 → 1.5.4
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/CHANGELOG.md +14 -0
- package/bin/flowmind.js +85 -8
- package/core/adapters/mcp-adapter.js +26 -0
- package/core/adapters/workflow-adapter.js +26 -0
- package/core/ai/providers/mimo.js +1 -1
- package/core/component-registry.js +19 -1
- package/core/config-manager.js +7 -2
- package/core/index.js +153 -2
- package/core/mcp-http-client.js +63 -0
- package/core/providers/aliyun/dms-adapter.js +7 -35
- package/core/providers/aliyun/rds-query-adapter.js +70 -0
- package/core/providers/aliyun/redis-adapter.js +4 -20
- package/core/providers/aliyun/sls-adapter.js +3 -10
- package/core/providers/friday/flow-adapter.js +19 -30
- package/core/providers/friday/report-adapter.js +5 -25
- package/core/providers/yapi/yapi-adapter.js +6 -30
- package/core/providers/yuque/yuque-adapter.js +7 -35
- package/core/sdd-agent-sync.js +240 -16
- package/core/source-inference.js +324 -0
- package/package.json +1 -1
- package/skills/auto-flow/index.js +903 -74
- package/skills/data-logic-validation/index.js +133 -12
- package/skills/log-audit/index.js +94 -1
- package/skills/resource-bind/index.js +61 -18
- package/skills/yapi-sync-interface/index.js +146 -13
- package/skills/yuque-sync-design/index.js +130 -11
- package/tui/app.jsx +15 -5
- package/tui/components/ChatPanel.jsx +1 -1
- package/tui/format-result.js +43 -4
|
@@ -13,7 +13,9 @@ module.exports = {
|
|
|
13
13
|
},
|
|
14
14
|
|
|
15
15
|
async execute(input, context) {
|
|
16
|
-
const knowledgeBase = context.componentRegistry?.getAdapter('knowledgeBase')
|
|
16
|
+
const knowledgeBase = context.componentRegistry?.getAdapter('knowledgeBase')
|
|
17
|
+
|| context.componentRegistry?.getAdapterByProvider?.('knowledgeBase', 'yuque')
|
|
18
|
+
|| null;
|
|
17
19
|
const learnedBinding = context.resourceBinding?.componentType === 'knowledgeBase'
|
|
18
20
|
? context.resourceBinding
|
|
19
21
|
: null;
|
|
@@ -29,17 +31,33 @@ module.exports = {
|
|
|
29
31
|
};
|
|
30
32
|
}
|
|
31
33
|
|
|
34
|
+
if (!knowledgeBase) {
|
|
35
|
+
return {
|
|
36
|
+
type: 'result',
|
|
37
|
+
skill: 'yuque-sync-design',
|
|
38
|
+
message: 'Yuque adapter is not available in the current registry.',
|
|
39
|
+
data: {
|
|
40
|
+
hint: 'Make sure the knowledgeBase component is configured and loaded',
|
|
41
|
+
binding: learnedBinding
|
|
42
|
+
},
|
|
43
|
+
input,
|
|
44
|
+
timestamp: new Date().toISOString()
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
32
48
|
const params = parseYuqueParams(input);
|
|
33
49
|
|
|
34
50
|
if (params.action === 'search') {
|
|
51
|
+
const execution = await knowledgeBase.search(params.keyword, 'doc');
|
|
35
52
|
return {
|
|
36
53
|
type: 'result',
|
|
37
54
|
skill: 'yuque-sync-design',
|
|
38
55
|
message: `Searching Yuque for: ${params.keyword || 'all'}`,
|
|
39
56
|
data: {
|
|
40
|
-
|
|
57
|
+
action: 'search',
|
|
41
58
|
args: { q: params.keyword, type: 'doc' },
|
|
42
|
-
binding: learnedBinding
|
|
59
|
+
binding: learnedBinding,
|
|
60
|
+
execution: summarizeYuqueExecution('search', execution)
|
|
43
61
|
},
|
|
44
62
|
input,
|
|
45
63
|
timestamp: new Date().toISOString()
|
|
@@ -47,14 +65,16 @@ module.exports = {
|
|
|
47
65
|
}
|
|
48
66
|
|
|
49
67
|
if (params.action === 'list') {
|
|
68
|
+
const execution = await knowledgeBase.getRepos();
|
|
50
69
|
return {
|
|
51
70
|
type: 'result',
|
|
52
71
|
skill: 'yuque-sync-design',
|
|
53
72
|
message: 'Listing Yuque repositories',
|
|
54
73
|
data: {
|
|
55
|
-
|
|
74
|
+
action: 'list',
|
|
56
75
|
args: {},
|
|
57
|
-
binding: learnedBinding
|
|
76
|
+
binding: learnedBinding,
|
|
77
|
+
execution: summarizeYuqueExecution('list', execution)
|
|
58
78
|
},
|
|
59
79
|
input,
|
|
60
80
|
timestamp: new Date().toISOString()
|
|
@@ -62,7 +82,7 @@ module.exports = {
|
|
|
62
82
|
}
|
|
63
83
|
|
|
64
84
|
if (params.action === 'sync') {
|
|
65
|
-
return syncDesignDoc(params, input, learnedBinding);
|
|
85
|
+
return syncDesignDoc(params, input, learnedBinding, knowledgeBase);
|
|
66
86
|
}
|
|
67
87
|
|
|
68
88
|
return {
|
|
@@ -98,7 +118,7 @@ function parseYuqueParams(input) {
|
|
|
98
118
|
return params;
|
|
99
119
|
}
|
|
100
120
|
|
|
101
|
-
async function syncDesignDoc(params, input, learnedBinding) {
|
|
121
|
+
async function syncDesignDoc(params, input, learnedBinding, knowledgeBase) {
|
|
102
122
|
const filePath = params.path || 'DESIGN.md';
|
|
103
123
|
|
|
104
124
|
if (!(await fs.pathExists(filePath))) {
|
|
@@ -113,27 +133,126 @@ async function syncDesignDoc(params, input, learnedBinding) {
|
|
|
113
133
|
|
|
114
134
|
const content = await fs.readFile(filePath, 'utf-8');
|
|
115
135
|
const title = extractTitle(content) || path.basename(filePath, '.md');
|
|
136
|
+
const namespace = params.repo || learnedBinding?.connection?.namespace;
|
|
137
|
+
const execution = await knowledgeBase.createDoc(namespace, {
|
|
138
|
+
slug: title.toLowerCase().replace(/\s+/g, '-'),
|
|
139
|
+
title,
|
|
140
|
+
body: content,
|
|
141
|
+
format: 'markdown'
|
|
142
|
+
});
|
|
116
143
|
|
|
117
144
|
return {
|
|
118
145
|
type: 'result',
|
|
119
146
|
skill: 'yuque-sync-design',
|
|
120
147
|
message: `Ready to sync "${title}" to Yuque`,
|
|
121
148
|
data: {
|
|
122
|
-
|
|
149
|
+
action: 'sync',
|
|
123
150
|
args: {
|
|
124
|
-
namespace
|
|
151
|
+
namespace,
|
|
125
152
|
slug: title.toLowerCase().replace(/\s+/g, '-'),
|
|
126
153
|
title,
|
|
127
|
-
body: content,
|
|
128
154
|
format: 'markdown'
|
|
129
155
|
},
|
|
130
|
-
binding: learnedBinding
|
|
156
|
+
binding: learnedBinding,
|
|
157
|
+
execution: summarizeYuqueExecution('sync', execution)
|
|
131
158
|
},
|
|
132
159
|
input,
|
|
133
160
|
timestamp: new Date().toISOString()
|
|
134
161
|
};
|
|
135
162
|
}
|
|
136
163
|
|
|
164
|
+
function summarizeYuqueExecution(action, execution) {
|
|
165
|
+
const payload = unwrapPayload(execution);
|
|
166
|
+
const data = payload && typeof payload === 'object' ? payload : {};
|
|
167
|
+
const items = extractItems(data);
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
action,
|
|
171
|
+
status: extractStatus(data) || 'ok',
|
|
172
|
+
total: extractTotal(data, items),
|
|
173
|
+
items: items.slice(0, 5).map(summarizeItem),
|
|
174
|
+
id: data.id || data.doc_id || data.slug || null
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function unwrapPayload(payload) {
|
|
179
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
|
180
|
+
return payload;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const textPayload = extractContentText(payload);
|
|
184
|
+
if (textPayload) {
|
|
185
|
+
return parseMaybeJson(textPayload);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (payload.data && typeof payload.data === 'object' && !Array.isArray(payload.data)) {
|
|
189
|
+
return payload.data;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return payload;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function extractItems(data) {
|
|
196
|
+
const candidates = [data.list, data.items, data.repos, data.docs, data.result];
|
|
197
|
+
for (const candidate of candidates) {
|
|
198
|
+
if (Array.isArray(candidate)) {
|
|
199
|
+
return candidate;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return [];
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function extractStatus(data) {
|
|
207
|
+
if (!data || typeof data !== 'object') {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (typeof data.status === 'string') {
|
|
212
|
+
return data.status;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (data.success === false) {
|
|
216
|
+
return 'error';
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function extractTotal(data, items) {
|
|
223
|
+
return data.total || items.length || null;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function extractContentText(payload) {
|
|
227
|
+
const content = Array.isArray(payload.content) ? payload.content : [];
|
|
228
|
+
const textItem = content.find((item) => item && typeof item.text === 'string');
|
|
229
|
+
return textItem ? textItem.text : null;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function parseMaybeJson(value) {
|
|
233
|
+
if (typeof value !== 'string') {
|
|
234
|
+
return value;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
try {
|
|
238
|
+
return JSON.parse(value);
|
|
239
|
+
} catch (error) {
|
|
240
|
+
return { text: value };
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function summarizeItem(item) {
|
|
245
|
+
if (!item || typeof item !== 'object') {
|
|
246
|
+
return item;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
id: item.id || item.slug || item.namespace || null,
|
|
251
|
+
title: item.title || item.name || item.slug || null,
|
|
252
|
+
type: item.type || item.format || null
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
137
256
|
function extractTitle(content) {
|
|
138
257
|
const match = content.match(/^#\s+(.+)$/m);
|
|
139
258
|
return match ? match[1].trim() : null;
|
package/tui/app.jsx
CHANGED
|
@@ -6,11 +6,14 @@ const StatusBar = require('./components/StatusBar.jsx');
|
|
|
6
6
|
const { formatResultText } = require('./format-result');
|
|
7
7
|
|
|
8
8
|
function App({ flowmind, asciiMode = false }) {
|
|
9
|
+
const MAX_MESSAGES = 60;
|
|
9
10
|
const [messages, setMessages] = React.useState([]);
|
|
10
11
|
const [isProcessing, setIsProcessing] = React.useState(false);
|
|
11
12
|
const [focusPanel, setFocusPanel] = React.useState('chat'); // 'chat' | 'sidebar'
|
|
12
13
|
const mountedRef = React.useRef(true);
|
|
13
14
|
const messageIdRef = React.useRef(0);
|
|
15
|
+
const commandIdRef = React.useRef(0);
|
|
16
|
+
const activeCommandRef = React.useRef(false);
|
|
14
17
|
const { exit } = useApp();
|
|
15
18
|
|
|
16
19
|
React.useEffect(() => {
|
|
@@ -19,7 +22,7 @@ function App({ flowmind, asciiMode = false }) {
|
|
|
19
22
|
|
|
20
23
|
const appendMessage = React.useCallback((role, text, metadata = {}) => {
|
|
21
24
|
const id = ++messageIdRef.current;
|
|
22
|
-
setMessages(prev => [...prev, { id, role, text, metadata }]);
|
|
25
|
+
setMessages(prev => [...prev, { id, role, text, metadata }].slice(-MAX_MESSAGES));
|
|
23
26
|
}, []);
|
|
24
27
|
|
|
25
28
|
// Ctrl+C always exits; Tab switches focus between panels
|
|
@@ -31,13 +34,15 @@ function App({ flowmind, asciiMode = false }) {
|
|
|
31
34
|
});
|
|
32
35
|
|
|
33
36
|
const handleCommand = React.useCallback(async (input) => {
|
|
34
|
-
if (!mountedRef.current) return;
|
|
37
|
+
if (!mountedRef.current || activeCommandRef.current) return;
|
|
38
|
+
activeCommandRef.current = true;
|
|
39
|
+
const commandId = ++commandIdRef.current;
|
|
35
40
|
appendMessage('user', input);
|
|
36
41
|
setIsProcessing(true);
|
|
37
42
|
|
|
38
43
|
try {
|
|
39
44
|
const result = await flowmind.process(input);
|
|
40
|
-
if (!mountedRef.current) return;
|
|
45
|
+
if (!mountedRef.current || commandId !== commandIdRef.current) return;
|
|
41
46
|
|
|
42
47
|
appendMessage('flowmind', formatResultText(result), {
|
|
43
48
|
type: result.type,
|
|
@@ -46,9 +51,14 @@ function App({ flowmind, asciiMode = false }) {
|
|
|
46
51
|
scene: result.metadata?.sceneMatch?.scene?.name
|
|
47
52
|
});
|
|
48
53
|
} catch (e) {
|
|
49
|
-
if (mountedRef.current
|
|
54
|
+
if (mountedRef.current && commandId === commandIdRef.current) {
|
|
55
|
+
appendMessage('flowmind', 'Error: ' + e.message, { type: 'error' });
|
|
56
|
+
}
|
|
50
57
|
} finally {
|
|
51
|
-
if (mountedRef.current)
|
|
58
|
+
if (mountedRef.current && commandId === commandIdRef.current) {
|
|
59
|
+
setIsProcessing(false);
|
|
60
|
+
}
|
|
61
|
+
activeCommandRef.current = false;
|
|
52
62
|
}
|
|
53
63
|
}, [appendMessage, flowmind]);
|
|
54
64
|
|
|
@@ -36,7 +36,7 @@ function ChatPanel({ messages, onSubmit, isProcessing, onExit, focused, asciiMod
|
|
|
36
36
|
|
|
37
37
|
const handleSubmit = (value) => {
|
|
38
38
|
const normalized = value.trim();
|
|
39
|
-
if (!normalized) return;
|
|
39
|
+
if (!normalized || isProcessing) return;
|
|
40
40
|
// Add to command history (deduplicate consecutive)
|
|
41
41
|
if (cmdHistory.length === 0 || cmdHistory[cmdHistory.length - 1] !== normalized) {
|
|
42
42
|
setCmdHistory(prev => [...prev, normalized]);
|
package/tui/format-result.js
CHANGED
|
@@ -2,6 +2,12 @@ function isPlainObject(value) {
|
|
|
2
2
|
return value && typeof value === 'object' && !Array.isArray(value);
|
|
3
3
|
}
|
|
4
4
|
|
|
5
|
+
const MAX_RENDER_LINES = 120;
|
|
6
|
+
const MAX_LINE_LENGTH = 220;
|
|
7
|
+
const MAX_RENDER_CHARS = 8000;
|
|
8
|
+
const MAX_ARRAY_ITEMS = 20;
|
|
9
|
+
const MAX_SCALAR_LENGTH = 160;
|
|
10
|
+
|
|
5
11
|
function unwrapResultPayload(result) {
|
|
6
12
|
let current = result;
|
|
7
13
|
|
|
@@ -58,11 +64,40 @@ function pruneDisplayValue(value) {
|
|
|
58
64
|
}
|
|
59
65
|
|
|
60
66
|
function formatScalar(value) {
|
|
61
|
-
if (typeof value === 'string')
|
|
67
|
+
if (typeof value === 'string') {
|
|
68
|
+
if (value.length <= MAX_SCALAR_LENGTH) return value;
|
|
69
|
+
return `${value.slice(0, MAX_SCALAR_LENGTH - 1)}…`;
|
|
70
|
+
}
|
|
62
71
|
if (typeof value === 'number' || typeof value === 'boolean') return String(value);
|
|
63
72
|
return JSON.stringify(value);
|
|
64
73
|
}
|
|
65
74
|
|
|
75
|
+
function sanitizeForTerminal(text) {
|
|
76
|
+
return String(text)
|
|
77
|
+
.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, ' ')
|
|
78
|
+
.replace(/\r\n/g, '\n');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function truncateRenderedText(text) {
|
|
82
|
+
const sanitized = sanitizeForTerminal(text);
|
|
83
|
+
const lines = sanitized.split('\n');
|
|
84
|
+
const clippedLines = lines.slice(0, MAX_RENDER_LINES).map((line) => {
|
|
85
|
+
if (line.length <= MAX_LINE_LENGTH) return line;
|
|
86
|
+
return `${line.slice(0, MAX_LINE_LENGTH - 1)}…`;
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
if (lines.length > MAX_RENDER_LINES) {
|
|
90
|
+
clippedLines.push(`… truncated ${lines.length - MAX_RENDER_LINES} line(s)`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
let output = clippedLines.join('\n');
|
|
94
|
+
if (output.length > MAX_RENDER_CHARS) {
|
|
95
|
+
output = `${output.slice(0, MAX_RENDER_CHARS - 1)}…`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return output;
|
|
99
|
+
}
|
|
100
|
+
|
|
66
101
|
function renderValueLines(value, indent = '') {
|
|
67
102
|
if (value === undefined) return [];
|
|
68
103
|
|
|
@@ -71,7 +106,11 @@ function renderValueLines(value, indent = '') {
|
|
|
71
106
|
|
|
72
107
|
const allScalars = value.every((item) => !isPlainObject(item) && !Array.isArray(item));
|
|
73
108
|
if (allScalars) {
|
|
74
|
-
|
|
109
|
+
const shown = value.slice(0, MAX_ARRAY_ITEMS).map(formatScalar);
|
|
110
|
+
if (value.length > MAX_ARRAY_ITEMS) {
|
|
111
|
+
shown.push(`… +${value.length - MAX_ARRAY_ITEMS} more`);
|
|
112
|
+
}
|
|
113
|
+
return [indent + shown.join(', ')];
|
|
75
114
|
}
|
|
76
115
|
|
|
77
116
|
const lines = [];
|
|
@@ -150,10 +189,10 @@ function formatResultText(result) {
|
|
|
150
189
|
}
|
|
151
190
|
|
|
152
191
|
if (detailLines.length > 0) {
|
|
153
|
-
return detailLines.join('\n');
|
|
192
|
+
return truncateRenderedText(detailLines.join('\n'));
|
|
154
193
|
}
|
|
155
194
|
|
|
156
|
-
return JSON.stringify(payload, null, 2);
|
|
195
|
+
return truncateRenderedText(JSON.stringify(payload, null, 2));
|
|
157
196
|
}
|
|
158
197
|
|
|
159
198
|
module.exports = {
|