flowmind 1.5.3 → 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.
@@ -10,7 +10,9 @@ module.exports = {
10
10
  },
11
11
 
12
12
  async execute(input, context) {
13
- const apiDoc = context.componentRegistry?.getAdapter('apiDoc');
13
+ const apiDoc = context.componentRegistry?.getAdapter('apiDoc')
14
+ || context.componentRegistry?.getAdapterByProvider?.('apiDoc', 'yapi')
15
+ || null;
14
16
  const learnedBinding = context.resourceBinding?.componentType === 'apiDoc'
15
17
  ? context.resourceBinding
16
18
  : null;
@@ -26,17 +28,38 @@ module.exports = {
26
28
  };
27
29
  }
28
30
 
31
+ if (!apiDoc) {
32
+ return {
33
+ type: 'result',
34
+ skill: 'yapi-sync-interface',
35
+ message: 'YApi adapter is not available in the current registry.',
36
+ data: {
37
+ hint: 'Make sure the apiDoc component is configured and loaded',
38
+ binding: learnedBinding
39
+ },
40
+ input,
41
+ timestamp: new Date().toISOString()
42
+ };
43
+ }
44
+
29
45
  const params = parseYApiParams(input);
30
46
 
31
47
  if (params.action === 'search') {
48
+ const args = {
49
+ projectKeyword: params.project || learnedBinding?.connection?.project,
50
+ nameKeyword: params.keyword,
51
+ limit: 20
52
+ };
53
+ const execution = await apiDoc.searchApis(args);
32
54
  return {
33
55
  type: 'result',
34
56
  skill: 'yapi-sync-interface',
35
57
  message: `Searching YApi for: ${params.keyword || 'all'}`,
36
58
  data: {
37
- mcpTool: 'yapi_search_apis',
38
- args: { projectKeyword: params.project || learnedBinding?.connection?.project, nameKeyword: params.keyword, limit: 20 },
39
- binding: learnedBinding
59
+ action: 'search',
60
+ args,
61
+ binding: learnedBinding,
62
+ execution: summarizeYApiExecution('search', execution)
40
63
  },
41
64
  input,
42
65
  timestamp: new Date().toISOString()
@@ -44,14 +67,17 @@ module.exports = {
44
67
  }
45
68
 
46
69
  if (params.action === 'list') {
70
+ const projectId = params.project || learnedBinding?.connection?.project;
71
+ const execution = await apiDoc.getCategories(projectId);
47
72
  return {
48
73
  type: 'result',
49
74
  skill: 'yapi-sync-interface',
50
- message: `Listing YApi categories for project: ${params.project || learnedBinding?.connection?.project || 'default'}`,
75
+ message: `Listing YApi categories for project: ${projectId || 'default'}`,
51
76
  data: {
52
- mcpTool: 'yapi_get_categories',
53
- args: { projectId: params.project || learnedBinding?.connection?.project },
54
- binding: learnedBinding
77
+ action: 'list',
78
+ args: { projectId },
79
+ binding: learnedBinding,
80
+ execution: summarizeYApiExecution('list', execution)
55
81
  },
56
82
  input,
57
83
  timestamp: new Date().toISOString()
@@ -59,14 +85,17 @@ module.exports = {
59
85
  }
60
86
 
61
87
  if (params.action === 'export') {
88
+ const projectId = params.project || learnedBinding?.connection?.project;
89
+ const execution = await apiDoc.exportProject(projectId, params.format || 'swagger');
62
90
  return {
63
91
  type: 'result',
64
92
  skill: 'yapi-sync-interface',
65
- message: `Exporting YApi project: ${params.project || learnedBinding?.connection?.project || 'default'}`,
93
+ message: `Exporting YApi project: ${projectId || 'default'}`,
66
94
  data: {
67
- mcpTool: 'yapi_export_project',
68
- args: { projectId: params.project || learnedBinding?.connection?.project, type: params.format || 'swagger' },
69
- binding: learnedBinding
95
+ action: 'export',
96
+ args: { projectId, type: params.format || 'swagger' },
97
+ binding: learnedBinding,
98
+ execution: summarizeYApiExecution('export', execution)
70
99
  },
71
100
  input,
72
101
  timestamp: new Date().toISOString()
@@ -98,7 +127,7 @@ function parseYApiParams(input) {
98
127
  const projectMatch = input.match(/(?:项目|project)\s*[:=]?\s*(\S+)/i);
99
128
  if (projectMatch) params.project = projectMatch[1];
100
129
 
101
- const keywordMatch = input.match(/(?:关键词|keyword|搜索|名称|name)\s*[:=]?\s*(.+?)(?:\s*$)/i);
130
+ const keywordMatch = input.match(/(?:关键词|keyword|名称|name)\s*[:=]?\s*(.+?)(?=\s+(?:项目|project|格式|format)\s*[:=]|\s*$)/i);
102
131
  if (keywordMatch) params.keyword = keywordMatch[1].trim();
103
132
 
104
133
  const formatMatch = input.match(/(?:格式|format)\s*[:=]?\s*(json|markdown|swagger)/i);
@@ -106,3 +135,107 @@ function parseYApiParams(input) {
106
135
 
107
136
  return params;
108
137
  }
138
+
139
+ function summarizeYApiExecution(action, execution) {
140
+ const payload = unwrapPayload(execution);
141
+ const data = getPayloadData(payload);
142
+ const items = extractListItems(data);
143
+
144
+ return {
145
+ action,
146
+ status: extractStatus(data) || extractStatus(payload) || 'ok',
147
+ total: extractTotal(data, payload, items),
148
+ items: items.slice(0, 5).map(summarizeItem),
149
+ runId: extractRunId(data) || extractRunId(payload) || null
150
+ };
151
+ }
152
+
153
+ function unwrapPayload(payload) {
154
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
155
+ return payload;
156
+ }
157
+
158
+ const textPayload = extractContentText(payload);
159
+ if (textPayload) {
160
+ return parseMaybeJson(textPayload);
161
+ }
162
+
163
+ if ('data' in payload && payload.data && typeof payload.data === 'object' && !Array.isArray(payload.data)) {
164
+ return payload.data;
165
+ }
166
+
167
+ return payload;
168
+ }
169
+
170
+ function getPayloadData(payload) {
171
+ return payload && typeof payload === 'object' && !Array.isArray(payload) ? payload : {};
172
+ }
173
+
174
+ function extractListItems(data) {
175
+ const candidates = [data.list, data.items, data.records, data.result, data.data];
176
+ for (const candidate of candidates) {
177
+ if (Array.isArray(candidate)) {
178
+ return candidate;
179
+ }
180
+ }
181
+
182
+ return [];
183
+ }
184
+
185
+ function extractStatus(payload) {
186
+ if (!payload || typeof payload !== 'object') {
187
+ return null;
188
+ }
189
+
190
+ if (typeof payload.status === 'string') {
191
+ return payload.status;
192
+ }
193
+
194
+ if (payload.success === false) {
195
+ return 'error';
196
+ }
197
+
198
+ return typeof payload.code === 'string' ? payload.code : null;
199
+ }
200
+
201
+ function extractTotal(data, payload, items) {
202
+ return data.total || payload.total || items.length || null;
203
+ }
204
+
205
+ function extractRunId(payload) {
206
+ if (!payload || typeof payload !== 'object') {
207
+ return null;
208
+ }
209
+
210
+ return payload.runId || payload.id || payload.requestId || null;
211
+ }
212
+
213
+ function summarizeItem(item) {
214
+ if (!item || typeof item !== 'object') {
215
+ return item;
216
+ }
217
+
218
+ return {
219
+ id: item._id || item.id || item.api_id || item.catid || null,
220
+ name: item.name || item.title || item.path || item.url || null,
221
+ method: item.method || item.req_method || null
222
+ };
223
+ }
224
+
225
+ function extractContentText(payload) {
226
+ const content = Array.isArray(payload.content) ? payload.content : [];
227
+ const textItem = content.find((item) => item && typeof item.text === 'string');
228
+ return textItem ? textItem.text : null;
229
+ }
230
+
231
+ function parseMaybeJson(value) {
232
+ if (typeof value !== 'string') {
233
+ return value;
234
+ }
235
+
236
+ try {
237
+ return JSON.parse(value);
238
+ } catch (error) {
239
+ return { text: value };
240
+ }
241
+ }
@@ -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
- mcpTool: 'search',
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
- mcpTool: 'get_user_repos',
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
- mcpTool: 'create_doc',
149
+ action: 'sync',
123
150
  args: {
124
- namespace: params.repo || learnedBinding?.connection?.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
@@ -12,6 +12,8 @@ function App({ flowmind, asciiMode = false }) {
12
12
  const [focusPanel, setFocusPanel] = React.useState('chat'); // 'chat' | 'sidebar'
13
13
  const mountedRef = React.useRef(true);
14
14
  const messageIdRef = React.useRef(0);
15
+ const commandIdRef = React.useRef(0);
16
+ const activeCommandRef = React.useRef(false);
15
17
  const { exit } = useApp();
16
18
 
17
19
  React.useEffect(() => {
@@ -32,13 +34,15 @@ function App({ flowmind, asciiMode = false }) {
32
34
  });
33
35
 
34
36
  const handleCommand = React.useCallback(async (input) => {
35
- if (!mountedRef.current) return;
37
+ if (!mountedRef.current || activeCommandRef.current) return;
38
+ activeCommandRef.current = true;
39
+ const commandId = ++commandIdRef.current;
36
40
  appendMessage('user', input);
37
41
  setIsProcessing(true);
38
42
 
39
43
  try {
40
44
  const result = await flowmind.process(input);
41
- if (!mountedRef.current) return;
45
+ if (!mountedRef.current || commandId !== commandIdRef.current) return;
42
46
 
43
47
  appendMessage('flowmind', formatResultText(result), {
44
48
  type: result.type,
@@ -47,9 +51,14 @@ function App({ flowmind, asciiMode = false }) {
47
51
  scene: result.metadata?.sceneMatch?.scene?.name
48
52
  });
49
53
  } catch (e) {
50
- if (mountedRef.current) appendMessage('flowmind', 'Error: ' + e.message, { type: 'error' });
54
+ if (mountedRef.current && commandId === commandIdRef.current) {
55
+ appendMessage('flowmind', 'Error: ' + e.message, { type: 'error' });
56
+ }
51
57
  } finally {
52
- if (mountedRef.current) setIsProcessing(false);
58
+ if (mountedRef.current && commandId === commandIdRef.current) {
59
+ setIsProcessing(false);
60
+ }
61
+ activeCommandRef.current = false;
53
62
  }
54
63
  }, [appendMessage, flowmind]);
55
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]);