flowmind 1.5.2 → 1.5.3

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.
@@ -3,6 +3,8 @@
3
3
  * Verify SQL queries, Redis operations, and data processing logic against real data via MCP
4
4
  */
5
5
 
6
+ const { inferSourceContext } = require('../../core/source-inference');
7
+
6
8
  module.exports = {
7
9
  canHandle(input, context) {
8
10
  if (!input) return false;
@@ -13,6 +15,7 @@ module.exports = {
13
15
  const registry = context.componentRegistry;
14
16
  const params = parseValidationParams(input);
15
17
  const learnedBinding = context.resourceBinding;
18
+ const sourceInference = context.sourceInference || await safeInferSourceContext(input);
16
19
 
17
20
  // Check MCP component availability
18
21
  const dbManager = registry?.getAdapter('databaseManager');
@@ -26,7 +29,7 @@ module.exports = {
26
29
  };
27
30
 
28
31
  if (params.action === 'sql') {
29
- if (!dbQuery && !learnedBinding) {
32
+ if (!dbQuery) {
30
33
  return {
31
34
  type: 'result',
32
35
  skill: 'data-logic-validation',
@@ -37,16 +40,22 @@ module.exports = {
37
40
  };
38
41
  }
39
42
 
43
+ const target = resolveDatabaseTarget(params, learnedBinding, sourceInference);
44
+ const sql = params.query || 'SELECT 1';
45
+ const execution = await dbQuery.queryExec(buildSqlExecutionParams(target, sql));
46
+
40
47
  return {
41
48
  type: 'result',
42
49
  skill: 'data-logic-validation',
43
- message: `SQL validation ready for: ${params.query || 'provided query'}`,
50
+ message: `SQL validation executed for: ${target.project || target.database || 'resolved target'}`,
44
51
  data: {
45
52
  action: 'validate_sql',
46
- query: params.query,
47
- mcpTool: 'mcpQueryExec',
53
+ query: sql,
54
+ target,
55
+ execution,
48
56
  availableComponents,
49
- binding: learnedBinding
57
+ binding: learnedBinding,
58
+ inferredSource: summarizeSourceInference(sourceInference)
50
59
  },
51
60
  input,
52
61
  timestamp: new Date().toISOString()
@@ -54,27 +63,33 @@ module.exports = {
54
63
  }
55
64
 
56
65
  if (params.action === 'redis') {
57
- if (!redisMonitor && !learnedBinding) {
66
+ if (!dbQuery) {
58
67
  return {
59
68
  type: 'result',
60
69
  skill: 'data-logic-validation',
61
- message: 'Redis monitor not configured (need friday-aliyun-sz-rds-redis MCP)',
62
- data: { availableComponents, hint: 'Configure Redis monitor MCP server first' },
70
+ message: 'Redis direct query service not configured (need friday-rds-redis-query MCP)',
71
+ data: { availableComponents, hint: 'Configure database query MCP server first' },
63
72
  input,
64
73
  timestamp: new Date().toISOString()
65
74
  };
66
75
  }
67
76
 
77
+ const target = resolveRedisTarget(params, learnedBinding, sourceInference);
78
+ const execution = await executeRedisAction(dbQuery, params, target);
79
+
68
80
  return {
69
81
  type: 'result',
70
82
  skill: 'data-logic-validation',
71
- message: `Redis validation ready for key: ${params.key || 'provided key'}`,
83
+ message: `Redis validation executed for: ${target.project || target.host || 'resolved target'}`,
72
84
  data: {
73
85
  action: 'validate_redis',
74
86
  key: params.key,
75
- mcpTools: ['mcpRedisKeyGet', 'mcpRedisKeyInfo', 'mcpRedisKeys'],
87
+ target,
88
+ execution,
76
89
  availableComponents,
77
- binding: learnedBinding
90
+ binding: learnedBinding,
91
+ inferredSource: summarizeSourceInference(sourceInference),
92
+ monitorProvider: redisMonitor?.providerName || null
78
93
  },
79
94
  input,
80
95
  timestamp: new Date().toISOString()
@@ -89,7 +104,8 @@ module.exports = {
89
104
  actions: ['sql - Validate SQL queries', 'redis - Validate Redis operations'],
90
105
  availableComponents,
91
106
  requiredMCP: ['databaseManager', 'databaseQuery', 'redisMonitor'],
92
- binding: learnedBinding
107
+ binding: learnedBinding,
108
+ inferredSource: summarizeSourceInference(sourceInference)
93
109
  },
94
110
  input,
95
111
  timestamp: new Date().toISOString()
@@ -108,5 +124,110 @@ function parseValidationParams(input) {
108
124
  const keyMatch = input.match(/(?:key|键)\s*[:=]?\s*(\S+)/i);
109
125
  if (keyMatch) params.key = keyMatch[1];
110
126
 
127
+ const projectMatch = input.match(/(?:project|项目)\s*[:=]?\s*([\u4e00-\u9fa5A-Za-z0-9._-]+)/i);
128
+ if (projectMatch) params.project = projectMatch[1];
129
+
130
+ const envMatch = input.match(/(?:env|环境)\s*[:=]?\s*([A-Za-z_-]+)/i);
131
+ if (envMatch) params.env = envMatch[1].toLowerCase();
132
+
111
133
  return params;
112
134
  }
135
+
136
+ async function safeInferSourceContext(input) {
137
+ try {
138
+ return await inferSourceContext(input);
139
+ } catch (error) {
140
+ return null;
141
+ }
142
+ }
143
+
144
+ function resolveDatabaseTarget(params, learnedBinding, sourceInference) {
145
+ const bindingConnection = learnedBinding?.connection || {};
146
+ const inferredDatabase = sourceInference?.database || {};
147
+
148
+ return compactObject({
149
+ project: params.project || sourceInference?.project || bindingConnection.project || null,
150
+ environment: params.env || sourceInference?.environment || bindingConnection.env || inferredDatabase.envType || null,
151
+ sourceId: bindingConnection.sourceId || inferredDatabase.sourceId || null,
152
+ databaseId: bindingConnection.databaseId || inferredDatabase.databaseId || null,
153
+ database: inferredDatabase.database || bindingConnection.database || null,
154
+ host: bindingConnection.host || inferredDatabase.host || null,
155
+ port: bindingConnection.port || inferredDatabase.port || null,
156
+ username: bindingConnection.username || bindingConnection.dbUser || inferredDatabase.username || inferredDatabase.dbUser || null,
157
+ password: bindingConnection.password || bindingConnection.dbPassword || inferredDatabase.password || inferredDatabase.dbPassword || null
158
+ });
159
+ }
160
+
161
+ function resolveRedisTarget(params, learnedBinding, sourceInference) {
162
+ const bindingConnection = learnedBinding?.connection || {};
163
+ const inferredRedis = sourceInference?.redis || {};
164
+
165
+ return compactObject({
166
+ project: params.project || sourceInference?.project || bindingConnection.project || null,
167
+ environment: params.env || sourceInference?.environment || bindingConnection.env || inferredRedis.envType || null,
168
+ sourceId: bindingConnection.sourceId || inferredRedis.sourceId || null,
169
+ host: bindingConnection.host || inferredRedis.host || null,
170
+ port: bindingConnection.port || inferredRedis.port || null,
171
+ password: bindingConnection.password || inferredRedis.password || null,
172
+ database: inferredRedis.database ?? bindingConnection.database ?? 1
173
+ });
174
+ }
175
+
176
+ function buildSqlExecutionParams(target, sql) {
177
+ return compactObject({
178
+ source_id: target.sourceId || null,
179
+ database_id: target.databaseId || null,
180
+ schema: target.database || null,
181
+ sql,
182
+ env: target.environment || null,
183
+ host: target.host || null,
184
+ port: target.port || null,
185
+ username: target.username || null,
186
+ password: target.password || null
187
+ });
188
+ }
189
+
190
+ async function executeRedisAction(dbQuery, params, target) {
191
+ const baseParams = compactObject({
192
+ source_id: target.sourceId || null,
193
+ env: target.environment || null,
194
+ host: target.host || null,
195
+ port: target.port || null,
196
+ password: target.password || null,
197
+ database: target.database ?? 1
198
+ });
199
+
200
+ if (params.key && !/[*?]/.test(params.key)) {
201
+ return dbQuery.redisKeyGet({
202
+ ...baseParams,
203
+ key: params.key
204
+ });
205
+ }
206
+
207
+ return dbQuery.redisScan({
208
+ ...baseParams,
209
+ pattern: params.key || '*',
210
+ count: 50
211
+ });
212
+ }
213
+
214
+ function summarizeSourceInference(sourceInference) {
215
+ if (!sourceInference?.matched) {
216
+ return sourceInference?.environment
217
+ ? { matched: false, environment: sourceInference.environment }
218
+ : null;
219
+ }
220
+
221
+ return {
222
+ matched: true,
223
+ project: sourceInference.project,
224
+ environment: sourceInference.environment,
225
+ database: sourceInference.database?.database || null,
226
+ redisDatabase: sourceInference.redis?.database ?? null,
227
+ matchedEntries: sourceInference.matchedEntries?.map((entry) => entry.title) || []
228
+ };
229
+ }
230
+
231
+ function compactObject(value) {
232
+ return Object.fromEntries(Object.entries(value).filter(([, fieldValue]) => fieldValue !== null && fieldValue !== undefined && fieldValue !== ''));
233
+ }
@@ -3,6 +3,8 @@
3
3
  * Manage database, Redis, API, and other external resource connections
4
4
  */
5
5
 
6
+ const { inferSourceContext } = require('../../core/source-inference');
7
+
6
8
  const PROVIDER_MAP = {
7
9
  'yapi-mcp': { componentType: 'apiDoc', provider: 'yapi', mcpServer: 'yapi-mcp' },
8
10
  yapi: { componentType: 'apiDoc', provider: 'yapi', mcpServer: 'yapi-mcp' },
@@ -27,9 +29,10 @@ module.exports = {
27
29
  async execute(input, context) {
28
30
  const registry = context.componentRegistry;
29
31
  const params = parseResourceParams(input);
32
+ const sourceInference = context.sourceInference || await safeInferSourceContext(input);
30
33
 
31
34
  if (params.action === 'bind') {
32
- return bindResource(input, context, params);
35
+ return bindResource(input, context, params, sourceInference);
33
36
  }
34
37
 
35
38
  if (params.action === 'list') {
@@ -48,7 +51,8 @@ module.exports = {
48
51
  initialized: c.initialized,
49
52
  mcpServer: c.mcpServer
50
53
  })),
51
- bindings
54
+ bindings,
55
+ inferredSource: summarizeSourceInference(sourceInference)
52
56
  },
53
57
  input,
54
58
  timestamp: new Date().toISOString()
@@ -69,7 +73,8 @@ module.exports = {
69
73
  : 'Resource connection status',
70
74
  data: {
71
75
  status,
72
- matchedBinding
76
+ matchedBinding,
77
+ inferredSource: summarizeSourceInference(sourceInference)
73
78
  },
74
79
  input,
75
80
  timestamp: new Date().toISOString()
@@ -87,7 +92,8 @@ module.exports = {
87
92
  'status - Show connection status and matched binding',
88
93
  'use - Resolve a learned binding for the current business'
89
94
  ],
90
- supportedTypes: ['MySQL', 'PostgreSQL', 'Redis', 'REST API', 'YApi', 'Yuque', 'Workflow']
95
+ supportedTypes: ['MySQL', 'PostgreSQL', 'Redis', 'REST API', 'YApi', 'Yuque', 'Workflow'],
96
+ inferredSource: summarizeSourceInference(sourceInference)
91
97
  },
92
98
  input,
93
99
  timestamp: new Date().toISOString()
@@ -95,7 +101,7 @@ module.exports = {
95
101
  }
96
102
  };
97
103
 
98
- async function bindResource(input, context, params) {
104
+ async function bindResource(input, context, params, sourceInference) {
99
105
  const learning = context.flowmind?.learning;
100
106
  if (!learning?.saveResourceBinding) {
101
107
  return {
@@ -107,7 +113,7 @@ async function bindResource(input, context, params) {
107
113
  };
108
114
  }
109
115
 
110
- const binding = buildBinding(input, params, context);
116
+ const binding = buildBinding(input, params, context, sourceInference);
111
117
  const saved = await learning.saveResourceBinding(binding, {
112
118
  currentSkill: 'resource-bind'
113
119
  });
@@ -126,10 +132,20 @@ async function bindResource(input, context, params) {
126
132
  };
127
133
  }
128
134
 
129
- function buildBinding(input, params, context) {
135
+ function buildBinding(input, params, context, sourceInference) {
130
136
  const inferred = resolveProviderInfo(params);
131
- const business = inferBusiness(input, params, inferred);
132
- const aliases = [business, params.project, params.namespace, params.alias, params.env].filter(Boolean);
137
+ const inferredDatabase = sourceInference?.database || null;
138
+ const inferredRedis = sourceInference?.redis || null;
139
+ const business = inferBusiness(input, params, inferred, sourceInference);
140
+ const aliases = [
141
+ business,
142
+ params.project,
143
+ sourceInference?.project,
144
+ params.namespace,
145
+ params.alias,
146
+ params.env,
147
+ sourceInference?.environment
148
+ ].filter(Boolean);
133
149
  const keywords = extractKeywords(input, aliases);
134
150
 
135
151
  return {
@@ -140,18 +156,20 @@ function buildBinding(input, params, context) {
140
156
  mcpServer: params.mcpServer || inferred.mcpServer || null,
141
157
  connection: {
142
158
  address: params.address || params.url || params.endpoint || null,
143
- host: params.host || null,
144
- port: params.port || null,
159
+ host: params.host || inferredDatabase?.host || inferredRedis?.host || null,
160
+ port: params.port || inferredDatabase?.port || inferredRedis?.port || null,
145
161
  token: params.token || null,
146
162
  apiKey: params.apiKey || null,
147
163
  namespace: params.namespace || null,
148
- project: params.project || null,
149
- sourceId: params.sourceId || null,
150
- databaseId: params.databaseId || null,
151
- env: params.env || null
164
+ project: params.project || sourceInference?.project || null,
165
+ database: inferredDatabase?.database || null,
166
+ sourceId: params.sourceId || inferredDatabase?.sourceId || inferredRedis?.sourceId || null,
167
+ databaseId: params.databaseId || inferredDatabase?.databaseId || null,
168
+ env: params.env || sourceInference?.environment || null
152
169
  },
153
170
  metadata: {
154
- input
171
+ input,
172
+ sourceInference: summarizeSourceInference(sourceInference)
155
173
  },
156
174
  keywords
157
175
  };
@@ -231,8 +249,8 @@ function resolveProviderInfo(params) {
231
249
  return {};
232
250
  }
233
251
 
234
- function inferBusiness(input, params, inferred = {}) {
235
- const explicit = params.business || params.project || params.namespace;
252
+ function inferBusiness(input, params, inferred = {}, sourceInference = null) {
253
+ const explicit = params.business || params.project || sourceInference?.project || params.namespace;
236
254
  if (explicit) return explicit;
237
255
 
238
256
  const keywordMatch = String(input || '').match(/(?:绑定|bind)\s*([\u4e00-\u9fa5A-Za-z0-9._-]+?)\s*(平台|服务|环境|工程|项目)/i);
@@ -259,3 +277,28 @@ function extractKeywords(input, aliases = []) {
259
277
  function escapeRegex(value) {
260
278
  return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
261
279
  }
280
+
281
+ async function safeInferSourceContext(input) {
282
+ try {
283
+ return await inferSourceContext(input);
284
+ } catch (error) {
285
+ return null;
286
+ }
287
+ }
288
+
289
+ function summarizeSourceInference(sourceInference) {
290
+ if (!sourceInference?.matched) {
291
+ return sourceInference?.environment
292
+ ? { matched: false, environment: sourceInference.environment }
293
+ : null;
294
+ }
295
+
296
+ return {
297
+ matched: true,
298
+ project: sourceInference.project,
299
+ environment: sourceInference.environment,
300
+ database: sourceInference.database?.database || null,
301
+ redisDatabase: sourceInference.redis?.database ?? null,
302
+ matchedEntries: sourceInference.matchedEntries?.map((entry) => entry.title) || []
303
+ };
304
+ }
package/tui/app.jsx CHANGED
@@ -6,6 +6,7 @@ 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'
@@ -19,7 +20,7 @@ function App({ flowmind, asciiMode = false }) {
19
20
 
20
21
  const appendMessage = React.useCallback((role, text, metadata = {}) => {
21
22
  const id = ++messageIdRef.current;
22
- setMessages(prev => [...prev, { id, role, text, metadata }]);
23
+ setMessages(prev => [...prev, { id, role, text, metadata }].slice(-MAX_MESSAGES));
23
24
  }, []);
24
25
 
25
26
  // Ctrl+C always exits; Tab switches focus between panels
@@ -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') return value;
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
- return [indent + value.map(formatScalar).join(', ')];
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 = {