flowmind 1.4.8 → 1.5.0
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 +21 -0
- package/bin/flowmind.js +219 -55
- package/core/adapters/workflow-adapter.js +9 -0
- package/core/config-manager.js +8 -1
- package/core/index.js +33 -17
- package/core/learning-engine.js +406 -0
- package/core/providers/friday/flow-adapter.js +8 -0
- package/core/scene-matcher.js +5 -1
- package/core/sdd-agent-sync.js +467 -0
- package/core/skill-loader.js +51 -10
- package/core/utils.js +55 -1
- package/dashboard/app.jsx +5 -5
- package/dashboard/components/ActivityFeed.jsx +7 -6
- package/dashboard/components/DragonPanel.jsx +4 -16
- package/dashboard/components/McpStatusBar.jsx +3 -2
- package/dashboard/components/StatsRow.jsx +6 -8
- package/package.json +2 -2
- package/skills/auto-flow/index.js +320 -45
- package/skills/data-logic-validation/index.js +9 -5
- package/skills/resource-bind/SKILL.md +21 -1
- package/skills/resource-bind/index.js +206 -14
- package/skills/yapi-sync-interface/index.js +14 -7
- package/skills/yuque-sync-design/index.js +15 -8
- package/tui/app.jsx +44 -28
- package/tui/components/ChatPanel.jsx +55 -43
- package/tui/components/DragonTotem.jsx +4 -73
- package/tui/components/Sidebar.jsx +7 -7
- package/tui/components/StatusBar.jsx +5 -6
- package/tui/format-result.js +164 -0
- package/tui/ui.js +186 -0
|
@@ -1,20 +1,8 @@
|
|
|
1
1
|
const React = require('react');
|
|
2
2
|
const { Box, Text } = require('ink');
|
|
3
|
+
const { LEVEL_COLORS, LEVEL_NAMES, LEVEL_STATES, getBorderStyle, getDragonArt } = require('../../tui/ui');
|
|
3
4
|
|
|
4
|
-
|
|
5
|
-
0: [' ╭─────╮ ',' ╱ ╭─╮ ╲ ',' │ │ │ │ ',' │ │ ◎ │ │ ',' │ ╰─╯ │ ',' ╲ ╱ ',' ╰─────╯ '],
|
|
6
|
-
1: [' ╭──╮ ',' ╭────╯ ╰───╮ ',' ╱ ◎ ╰─╯ ╲ ',' ╱ ▽ ╲ ',' ╲ ╱╲ ╱╲ ╱ ',' ╲╱╱ ╲╱╱ ╲╱╲╱ '],
|
|
7
|
-
2: [' ╭─╮ ╭─╮ ',' ╭────╯ ╰──╯ ╰───╮ ',' ╱ ◎ ╰──╯ ╲ ',' ╱ ╭────────╮ ╲ ',' ╲ ╱ ╱╱╱╱╱╱╱╱ ╲ ╱ ',' ╲───╯ ╱╱╱╱╱╱╱╱╱╱ ╰──╱ ',' ╰─╯ ╰─╯ '],
|
|
8
|
-
3: [' ╭───╮ ╭───╮ ',' ╭───╯ ╰──╯ ╰───╮ ',' ╱ ◎ ╰───╯ ╲ ','│ ╭──────────╮ │ ','│ ╱ ╱╱╱╱╱╱╱╱╱╱ ╲ │ ',' ╲──╯ ╱╱╱╱╱╱╱╱╱╱╱╱ ╰───╯ ',' ╲ ╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ ╱ ',' ╲╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ ',' ╰───╯ ╰───╯ '],
|
|
9
|
-
4: [' ╭───╮ ╭───╮ ','╭───╯ ╰──────╯ ╰───╮ ','│ ◎ ╰───╯ │ ','│ ╭────────────╮ │ ','│ ╱ ╱╱╱╱╱╱╱╱╱╱╱╱ ╲ │ ',' ╲───╯ ╱╱╱╱╱╱╱╱╱╱╱╱╱╱ ╰──╯ ',' ╲ ╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ ╲ ',' ╲─╯╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╰─╲ ',' ╲╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ ',' ╰───╯ ╰───╯ '],
|
|
10
|
-
5: [' ★ ╭───╮ ╭───╮ ★ ','╭─╯ ╰──╯ ╰──╯ ╰─╮ ','│ ◎ ╰───╯ │ ','│ ╭──────────────╮ │ ','│ ╱ ★╱╱╱╱╱╱╱╱╱╱★╱╱ ╲ │ ',' ╲────╯ ╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ ╰───╯ ',' ╲ ╱╱╱╱★╱╱╱╱╱╱╱╱★╱╱╱╱╱ ╲ ',' ╲──╯╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╰──╲ ',' ╲─╯╱╱╱★╱╱╱╱╱╱╱╱★╱╱╱╱╱╰──╲ ',' ★ ╲╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ ★ ',' ╰───╯ ╰───╯ '],
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
const LEVEL_NAMES = ['Egg', 'Hatchling', 'Juvenile', 'Adult', 'Elder', 'Ascended'];
|
|
14
|
-
const LEVEL_STATES = ['dormant', 'awakening', 'growing', 'soaring', 'wise', 'transcendent'];
|
|
15
|
-
const LEVEL_COLORS = ['gray', 'cyan', 'cyan', 'cyanBright', 'cyanBright', 'cyanBright'];
|
|
16
|
-
|
|
17
|
-
function DragonPanel({ flowmind }) {
|
|
5
|
+
function DragonPanel({ flowmind, asciiMode = false }) {
|
|
18
6
|
const [honorData, setHonorData] = React.useState({ points: 0, level: 0, stats: {} });
|
|
19
7
|
|
|
20
8
|
React.useEffect(() => {
|
|
@@ -28,7 +16,7 @@ function DragonPanel({ flowmind }) {
|
|
|
28
16
|
}, [flowmind]);
|
|
29
17
|
|
|
30
18
|
const level = honorData.level || 0;
|
|
31
|
-
const art =
|
|
19
|
+
const art = getDragonArt(level, { asciiMode });
|
|
32
20
|
const color = LEVEL_COLORS[level] || 'gray';
|
|
33
21
|
const levelName = LEVEL_NAMES[level] || 'Unknown';
|
|
34
22
|
const state = LEVEL_STATES[level] || 'unknown';
|
|
@@ -37,7 +25,7 @@ function DragonPanel({ flowmind }) {
|
|
|
37
25
|
const pointsToNext = nextPoints !== null ? nextPoints - honorData.points : 0;
|
|
38
26
|
|
|
39
27
|
return (
|
|
40
|
-
React.createElement(Box, { flexDirection: 'column', borderStyle:
|
|
28
|
+
React.createElement(Box, { flexDirection: 'column', borderStyle: getBorderStyle(asciiMode), borderColor: 'cyan', paddingX: 1, flexGrow: 1 },
|
|
41
29
|
React.createElement(Text, { bold: true, color: 'cyan' }, 'Dragon Totem'),
|
|
42
30
|
React.createElement(Box, { flexDirection: 'row', marginTop: 1 },
|
|
43
31
|
React.createElement(Box, { flexDirection: 'column' },
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
const React = require('react');
|
|
2
2
|
const { Box, Text } = require('ink');
|
|
3
|
+
const { getBorderStyle } = require('../../tui/ui');
|
|
3
4
|
|
|
4
|
-
function McpStatusBar({ eventBus }) {
|
|
5
|
+
function McpStatusBar({ eventBus, asciiMode = false }) {
|
|
5
6
|
const [toolCount, setToolCount] = React.useState(0);
|
|
6
7
|
const [lastCall, setLastCall] = React.useState(null);
|
|
7
8
|
const [serverState, setServerState] = React.useState('running');
|
|
@@ -19,7 +20,7 @@ function McpStatusBar({ eventBus }) {
|
|
|
19
20
|
const formatTime = (ts) => ts ? new Date(ts).toTimeString().substring(0, 8) : 'none';
|
|
20
21
|
|
|
21
22
|
return (
|
|
22
|
-
React.createElement(Box, { borderStyle:
|
|
23
|
+
React.createElement(Box, { borderStyle: getBorderStyle(asciiMode), borderColor: 'gray', paddingX: 1, justifyContent: 'space-between' },
|
|
23
24
|
React.createElement(Text, null,
|
|
24
25
|
React.createElement(Text, { color: 'gray' }, 'MCP Server: '),
|
|
25
26
|
React.createElement(Text, { color: 'green' }, serverState)
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
const React = require('react');
|
|
2
2
|
const { Box, Text } = require('ink');
|
|
3
|
+
const { LEVEL_NAMES, getBorderStyle, getProgressBar } = require('../../tui/ui');
|
|
3
4
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
function StatsRow({ flowmind }) {
|
|
5
|
+
function StatsRow({ flowmind, asciiMode = false }) {
|
|
7
6
|
const [honorData, setHonorData] = React.useState({ points: 0, level: 0, stats: {} });
|
|
8
7
|
const [learningStats, setLearningStats] = React.useState({ totalRecords: 0, byType: {} });
|
|
9
8
|
const [aiStatus, setAiStatus] = React.useState({ initialized: false, defaultProvider: 'none' });
|
|
@@ -22,12 +21,11 @@ function StatsRow({ flowmind }) {
|
|
|
22
21
|
|
|
23
22
|
const barWidth = 16;
|
|
24
23
|
const progress = honorData.points > 0 ? Math.min(1, honorData.points / 100) : 0;
|
|
25
|
-
const
|
|
26
|
-
const progressBar = '\u2588'.repeat(filled) + '\u2591'.repeat(barWidth - filled);
|
|
24
|
+
const progressBar = getProgressBar(barWidth, progress, asciiMode);
|
|
27
25
|
|
|
28
26
|
return (
|
|
29
27
|
React.createElement(Box, { flexDirection: 'row' },
|
|
30
|
-
React.createElement(Box, { flexDirection: 'column', borderStyle:
|
|
28
|
+
React.createElement(Box, { flexDirection: 'column', borderStyle: getBorderStyle(asciiMode), borderColor: 'yellow', paddingX: 1, width: '33%' },
|
|
31
29
|
React.createElement(Text, { bold: true, color: 'yellow' }, 'Honor'),
|
|
32
30
|
React.createElement(Text, null,
|
|
33
31
|
React.createElement(Text, { color: 'yellow' }, LEVEL_NAMES[honorData.level] || 'Egg'),
|
|
@@ -36,7 +34,7 @@ function StatsRow({ flowmind }) {
|
|
|
36
34
|
React.createElement(Text, { color: 'green' }, progressBar),
|
|
37
35
|
React.createElement(Text, { color: 'gray' }, honorData.points + '/100 pts')
|
|
38
36
|
),
|
|
39
|
-
React.createElement(Box, { flexDirection: 'column', borderStyle:
|
|
37
|
+
React.createElement(Box, { flexDirection: 'column', borderStyle: getBorderStyle(asciiMode), borderColor: 'cyan', paddingX: 1, width: '33%' },
|
|
40
38
|
React.createElement(Text, { bold: true, color: 'cyan' }, 'Learning'),
|
|
41
39
|
React.createElement(Text, null,
|
|
42
40
|
React.createElement(Text, { color: 'white' }, '' + (learningStats.totalRecords || 0)),
|
|
@@ -50,7 +48,7 @@ function StatsRow({ flowmind }) {
|
|
|
50
48
|
React.createElement(Text, { color: aiStatus.initialized ? 'green' : 'red' }, aiStatus.initialized ? 'ok' : 'off')
|
|
51
49
|
)
|
|
52
50
|
),
|
|
53
|
-
React.createElement(Box, { flexDirection: 'column', borderStyle:
|
|
51
|
+
React.createElement(Box, { flexDirection: 'column', borderStyle: getBorderStyle(asciiMode), borderColor: 'blue', paddingX: 1, width: '33%' },
|
|
54
52
|
React.createElement(Text, { bold: true, color: 'blue' }, 'Components'),
|
|
55
53
|
React.createElement(Text, { color: 'gray' }, 'Registry loaded'),
|
|
56
54
|
React.createElement(Text, null,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flowmind",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Memory and workflow automation for MCP, Codex, and Claude Code. Reuse repeatable developer operations through skills and explicit feedback.",
|
|
5
5
|
"main": "core/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"license": "MIT",
|
|
39
39
|
"repository": {
|
|
40
40
|
"type": "git",
|
|
41
|
-
"url": "https://github.com/Eleven-M/flowmind.git"
|
|
41
|
+
"url": "git+https://github.com/Eleven-M/flowmind.git"
|
|
42
42
|
},
|
|
43
43
|
"bugs": {
|
|
44
44
|
"url": "https://github.com/Eleven-M/flowmind/issues"
|
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Auto Flow Skill
|
|
3
|
-
*
|
|
3
|
+
* Route deployment and workflow requests to the configured workflow MCP adapter.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
const fs = require('fs-extra');
|
|
7
|
-
const path = require('path');
|
|
8
|
-
|
|
9
6
|
const BUILTIN_WORKFLOWS = {
|
|
10
7
|
'dev-workflow': {
|
|
11
8
|
name: 'Development Workflow',
|
|
@@ -28,69 +25,95 @@ const BUILTIN_WORKFLOWS = {
|
|
|
28
25
|
}
|
|
29
26
|
};
|
|
30
27
|
|
|
28
|
+
const GENERIC_SERVICE_TOKENS = new Set([
|
|
29
|
+
'flowmind', 'auto', 'flow', 'workflow', 'workflows', 'pipeline', 'pipelines',
|
|
30
|
+
'deploy', 'deployment', 'run', 'start', 'status', 'list', 'show',
|
|
31
|
+
'service', 'services', 'skill', 'skills', 'prod', 'gray', 'uat', 'test'
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
const TOOL_NAMES = {
|
|
35
|
+
listPipelines: 'flowListPipelines',
|
|
36
|
+
startPipelineRun: 'flowStartPipelineRun',
|
|
37
|
+
startBatchPipelineRun: 'flowStartBatchPipelineRun',
|
|
38
|
+
getPipelineRun: 'flowGetPipelineRun',
|
|
39
|
+
listPipelineRuns: 'flowListPipelineRuns'
|
|
40
|
+
};
|
|
41
|
+
|
|
31
42
|
module.exports = {
|
|
32
|
-
canHandle(input
|
|
43
|
+
canHandle(input) {
|
|
33
44
|
if (!input) return false;
|
|
34
|
-
|
|
45
|
+
if (/绑定|bind|保存|记住/i.test(input) && /(mcp|token|地址|host|endpoint|url)/i.test(input)) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
return /自动化|自动部署|auto.*flow|工作流|workflow|pipeline|部署|发布|上线|执行流程|运行流程/i.test(input);
|
|
35
49
|
},
|
|
36
50
|
|
|
37
51
|
async execute(input, context) {
|
|
38
52
|
const params = parseFlowParams(input);
|
|
53
|
+
const workflow = createWorkflowClient(context);
|
|
39
54
|
|
|
40
|
-
if (params.action === '
|
|
55
|
+
if (params.action === 'define') {
|
|
41
56
|
return {
|
|
42
57
|
type: 'result',
|
|
43
58
|
skill: 'auto-flow',
|
|
44
|
-
message:
|
|
45
|
-
data: {
|
|
59
|
+
message: 'Define a workflow in YAML format',
|
|
60
|
+
data: {
|
|
61
|
+
format: 'YAML workflow definition',
|
|
62
|
+
example: {
|
|
63
|
+
name: 'My Workflow',
|
|
64
|
+
steps: [
|
|
65
|
+
{ id: 'step1', action: 'run-command', command: 'echo hello' },
|
|
66
|
+
{ id: 'step2', skill: 'code-review', depends_on: ['step1'] }
|
|
67
|
+
]
|
|
68
|
+
}
|
|
69
|
+
},
|
|
46
70
|
input,
|
|
47
71
|
timestamp: new Date().toISOString()
|
|
48
72
|
};
|
|
49
73
|
}
|
|
50
74
|
|
|
51
|
-
if (
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
};
|
|
59
|
-
}
|
|
75
|
+
if (!workflow.client) {
|
|
76
|
+
return buildNoAdapterResult(input, params);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (params.action === 'status') {
|
|
80
|
+
return executeStatus(workflow, params, input);
|
|
81
|
+
}
|
|
60
82
|
|
|
83
|
+
if (params.action === 'deploy' || params.action === 'run') {
|
|
84
|
+
return executeDeploy(workflow, params, input);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (params.action === 'list' || params.serviceNames.length > 0) {
|
|
88
|
+
const execution = await workflow.client.listPipelines(buildListPipelineParams(params));
|
|
61
89
|
return {
|
|
62
90
|
type: 'result',
|
|
63
91
|
skill: 'auto-flow',
|
|
64
|
-
message:
|
|
92
|
+
message: params.serviceNames.length > 0
|
|
93
|
+
? `Resolved workflow query for: ${params.serviceNames.join(', ')}`
|
|
94
|
+
: 'Listing available deployment pipelines',
|
|
65
95
|
data: {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
status: 'pending'
|
|
72
|
-
})),
|
|
73
|
-
status: 'started'
|
|
96
|
+
action: 'list',
|
|
97
|
+
provider: workflow.provider,
|
|
98
|
+
binding: workflow.binding,
|
|
99
|
+
filters: buildListPipelineParams(params),
|
|
100
|
+
execution
|
|
74
101
|
},
|
|
75
102
|
input,
|
|
76
103
|
timestamp: new Date().toISOString()
|
|
77
104
|
};
|
|
78
105
|
}
|
|
79
106
|
|
|
80
|
-
if (params.
|
|
107
|
+
if (params.workflow && BUILTIN_WORKFLOWS[params.workflow]) {
|
|
108
|
+
const workflowDef = BUILTIN_WORKFLOWS[params.workflow];
|
|
81
109
|
return {
|
|
82
110
|
type: 'result',
|
|
83
111
|
skill: 'auto-flow',
|
|
84
|
-
message:
|
|
112
|
+
message: `Workflow ready: ${workflowDef.name}`,
|
|
85
113
|
data: {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
steps: [
|
|
90
|
-
{ id: 'step1', action: 'run-command', command: 'echo hello' },
|
|
91
|
-
{ id: 'step2', skill: 'code-review', depends_on: ['step1'] }
|
|
92
|
-
]
|
|
93
|
-
}
|
|
114
|
+
workflow: params.workflow,
|
|
115
|
+
steps: workflowDef.steps,
|
|
116
|
+
provider: workflow.provider
|
|
94
117
|
},
|
|
95
118
|
input,
|
|
96
119
|
timestamp: new Date().toISOString()
|
|
@@ -100,10 +123,17 @@ module.exports = {
|
|
|
100
123
|
return {
|
|
101
124
|
type: 'result',
|
|
102
125
|
skill: 'auto-flow',
|
|
103
|
-
message: 'Auto Flow. Available actions:
|
|
126
|
+
message: 'Auto Flow. Available actions: deploy, status, list, define',
|
|
104
127
|
data: {
|
|
105
|
-
actions: [
|
|
106
|
-
|
|
128
|
+
actions: [
|
|
129
|
+
'deploy - Start pipeline run by service/pipeline',
|
|
130
|
+
'status - Query pipeline run status',
|
|
131
|
+
'list - Search available pipelines',
|
|
132
|
+
'define - Define a custom workflow'
|
|
133
|
+
],
|
|
134
|
+
builtinWorkflows: Object.keys(BUILTIN_WORKFLOWS),
|
|
135
|
+
provider: workflow.provider,
|
|
136
|
+
binding: workflow.binding
|
|
107
137
|
},
|
|
108
138
|
input,
|
|
109
139
|
timestamp: new Date().toISOString()
|
|
@@ -111,14 +141,259 @@ module.exports = {
|
|
|
111
141
|
}
|
|
112
142
|
};
|
|
113
143
|
|
|
144
|
+
function createWorkflowClient(context) {
|
|
145
|
+
const adapter = context.componentRegistry?.getAdapter('workflow');
|
|
146
|
+
if (adapter) {
|
|
147
|
+
return {
|
|
148
|
+
client: adapter,
|
|
149
|
+
provider: adapter.providerName,
|
|
150
|
+
binding: context.resourceBinding?.componentType === 'workflow' ? context.resourceBinding : null
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const binding = context.resourceBinding?.componentType === 'workflow'
|
|
155
|
+
? context.resourceBinding
|
|
156
|
+
: null;
|
|
157
|
+
|
|
158
|
+
if (!binding?.mcpServer) {
|
|
159
|
+
return { client: null, provider: null, binding: null };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
client: {
|
|
164
|
+
providerName: binding.provider || 'workflow-binding',
|
|
165
|
+
async listPipelines(params) {
|
|
166
|
+
return { mcpServer: binding.mcpServer, tool: TOOL_NAMES.listPipelines, params };
|
|
167
|
+
},
|
|
168
|
+
async startPipelineRun(pipelineId) {
|
|
169
|
+
return { mcpServer: binding.mcpServer, tool: TOOL_NAMES.startPipelineRun, params: { pipelineId } };
|
|
170
|
+
},
|
|
171
|
+
async startBatchPipelineRun(params) {
|
|
172
|
+
return { mcpServer: binding.mcpServer, tool: TOOL_NAMES.startBatchPipelineRun, params };
|
|
173
|
+
},
|
|
174
|
+
async getPipelineRun(pipelineId, runId) {
|
|
175
|
+
return { mcpServer: binding.mcpServer, tool: TOOL_NAMES.getPipelineRun, params: { pipelineId, pipelineRunId: runId } };
|
|
176
|
+
},
|
|
177
|
+
async listPipelineRuns(pipelineId, params) {
|
|
178
|
+
return { mcpServer: binding.mcpServer, tool: TOOL_NAMES.listPipelineRuns, params: { pipelineId, ...(params || {}) } };
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
provider: binding.provider || 'workflow-binding',
|
|
182
|
+
binding
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function buildNoAdapterResult(input, params) {
|
|
187
|
+
if (params.workflow && BUILTIN_WORKFLOWS[params.workflow]) {
|
|
188
|
+
const workflow = BUILTIN_WORKFLOWS[params.workflow];
|
|
189
|
+
return {
|
|
190
|
+
type: 'result',
|
|
191
|
+
skill: 'auto-flow',
|
|
192
|
+
message: `Workflow ready: ${workflow.name}`,
|
|
193
|
+
data: {
|
|
194
|
+
workflow: params.workflow,
|
|
195
|
+
steps: workflow.steps
|
|
196
|
+
},
|
|
197
|
+
input,
|
|
198
|
+
timestamp: new Date().toISOString()
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
type: 'result',
|
|
204
|
+
skill: 'auto-flow',
|
|
205
|
+
message: 'Workflow service not configured. Connect friday-auto-flow first.',
|
|
206
|
+
data: {
|
|
207
|
+
params,
|
|
208
|
+
hint: 'Run `flowmind resource` to review current bindings, then save one like: `flowmind "绑定发布业务 mcp=friday-auto-flow token=xxx env=uat"`'
|
|
209
|
+
},
|
|
210
|
+
input,
|
|
211
|
+
timestamp: new Date().toISOString()
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async function executeDeploy(workflow, params, input) {
|
|
216
|
+
if (params.pipelineId) {
|
|
217
|
+
const execution = await workflow.client.startPipelineRun(params.pipelineId);
|
|
218
|
+
return {
|
|
219
|
+
type: 'result',
|
|
220
|
+
skill: 'auto-flow',
|
|
221
|
+
message: `Starting pipeline ${params.pipelineId}`,
|
|
222
|
+
data: {
|
|
223
|
+
action: 'deploy',
|
|
224
|
+
pipelineId: params.pipelineId,
|
|
225
|
+
environment: params.environment,
|
|
226
|
+
provider: workflow.provider,
|
|
227
|
+
binding: workflow.binding,
|
|
228
|
+
execution
|
|
229
|
+
},
|
|
230
|
+
input,
|
|
231
|
+
timestamp: new Date().toISOString()
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const listParams = buildListPipelineParams(params);
|
|
236
|
+
const resolveExecution = await workflow.client.listPipelines(listParams);
|
|
237
|
+
const batchParams = buildBatchRunParams(params);
|
|
238
|
+
const execution = await workflow.client.startBatchPipelineRun(batchParams);
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
type: 'result',
|
|
242
|
+
skill: 'auto-flow',
|
|
243
|
+
message: params.serviceNames.length > 0
|
|
244
|
+
? `Prepared deployment for: ${params.serviceNames.join(', ')}`
|
|
245
|
+
: `Prepared workflow deployment${params.workflow ? `: ${params.workflow}` : ''}`,
|
|
246
|
+
data: {
|
|
247
|
+
action: 'deploy',
|
|
248
|
+
services: params.serviceNames,
|
|
249
|
+
workflow: params.workflow,
|
|
250
|
+
environment: params.environment,
|
|
251
|
+
provider: workflow.provider,
|
|
252
|
+
binding: workflow.binding,
|
|
253
|
+
resolution: {
|
|
254
|
+
filters: listParams,
|
|
255
|
+
execution: resolveExecution
|
|
256
|
+
},
|
|
257
|
+
execution
|
|
258
|
+
},
|
|
259
|
+
input,
|
|
260
|
+
timestamp: new Date().toISOString()
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async function executeStatus(workflow, params, input) {
|
|
265
|
+
let execution;
|
|
266
|
+
if (params.pipelineId && params.runId) {
|
|
267
|
+
execution = await workflow.client.getPipelineRun(params.pipelineId, params.runId);
|
|
268
|
+
} else if (params.pipelineId) {
|
|
269
|
+
execution = await workflow.client.listPipelineRuns(params.pipelineId, {
|
|
270
|
+
env: params.environment
|
|
271
|
+
});
|
|
272
|
+
} else {
|
|
273
|
+
execution = await workflow.client.listPipelines(buildListPipelineParams(params));
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return {
|
|
277
|
+
type: 'result',
|
|
278
|
+
skill: 'auto-flow',
|
|
279
|
+
message: params.runId
|
|
280
|
+
? `Querying run status for ${params.runId}`
|
|
281
|
+
: 'Querying pipeline status',
|
|
282
|
+
data: {
|
|
283
|
+
action: 'status',
|
|
284
|
+
services: params.serviceNames,
|
|
285
|
+
pipelineId: params.pipelineId,
|
|
286
|
+
runId: params.runId,
|
|
287
|
+
environment: params.environment,
|
|
288
|
+
provider: workflow.provider,
|
|
289
|
+
binding: workflow.binding,
|
|
290
|
+
execution
|
|
291
|
+
},
|
|
292
|
+
input,
|
|
293
|
+
timestamp: new Date().toISOString()
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
114
297
|
function parseFlowParams(input) {
|
|
115
|
-
const params = {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
298
|
+
const params = {
|
|
299
|
+
action: null,
|
|
300
|
+
workflow: null,
|
|
301
|
+
serviceNames: [],
|
|
302
|
+
environment: null,
|
|
303
|
+
pipelineId: null,
|
|
304
|
+
runId: null
|
|
305
|
+
};
|
|
119
306
|
|
|
120
|
-
|
|
307
|
+
if (/列表|list|查看.*流程|查看.*pipeline|查询.*流程|搜索.*流程|show|find|search/i.test(input)) {
|
|
308
|
+
params.action = 'list';
|
|
309
|
+
}
|
|
310
|
+
if (/状态|status|进度|运行记录|run record|record/i.test(input)) {
|
|
311
|
+
params.action = 'status';
|
|
312
|
+
}
|
|
313
|
+
if (/定义|define|创建流程|create.*workflow/i.test(input)) {
|
|
314
|
+
params.action = 'define';
|
|
315
|
+
}
|
|
316
|
+
if (/部署|发布|上线|deploy|release/i.test(input)) {
|
|
317
|
+
params.action = 'deploy';
|
|
318
|
+
} else if (/执行|run|start|运行/i.test(input)) {
|
|
319
|
+
params.action = 'run';
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const workflowMatch = input.match(/(?:workflow|流程|工作流)\s*[:=]?\s*([A-Za-z0-9._-]+)/i);
|
|
121
323
|
if (workflowMatch) params.workflow = workflowMatch[1];
|
|
122
324
|
|
|
325
|
+
const pipelineIdMatch = input.match(/(?:pipelineId|pipeline_id|pipeline)\s*[:=]?\s*([A-Za-z0-9._-]+)/i);
|
|
326
|
+
if (pipelineIdMatch) params.pipelineId = pipelineIdMatch[1];
|
|
327
|
+
|
|
328
|
+
const runIdMatch = input.match(/(?:runId|run_id|pipelineRunId|运行id)\s*[:=]?\s*([A-Za-z0-9._-]+)/i);
|
|
329
|
+
if (runIdMatch) params.runId = runIdMatch[1];
|
|
330
|
+
|
|
331
|
+
const envMatch = input.match(/(?:环境|env|environment)\s*[:=]?\s*(test|uat|gray|prod|dev|sit|staging|production|测试|预发|灰度|生产)/i);
|
|
332
|
+
if (envMatch) {
|
|
333
|
+
params.environment = normalizeEnvironment(envMatch[1]);
|
|
334
|
+
} else {
|
|
335
|
+
const inferredEnv = inferEnvironmentFromText(input);
|
|
336
|
+
if (inferredEnv) params.environment = inferredEnv;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
params.serviceNames = extractServiceNames(input);
|
|
340
|
+
|
|
341
|
+
if (!params.workflow && /deploy-workflow|dev-workflow/i.test(input)) {
|
|
342
|
+
params.workflow = input.match(/deploy-workflow|dev-workflow/i)[0];
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (!params.action && params.serviceNames.length > 0) {
|
|
346
|
+
params.action = 'list';
|
|
347
|
+
}
|
|
348
|
+
|
|
123
349
|
return params;
|
|
124
350
|
}
|
|
351
|
+
|
|
352
|
+
function buildListPipelineParams(params) {
|
|
353
|
+
return compactObject({
|
|
354
|
+
keyword: params.serviceNames.join(',') || params.workflow || undefined,
|
|
355
|
+
env: params.environment,
|
|
356
|
+
services: params.serviceNames.length > 0 ? params.serviceNames : undefined,
|
|
357
|
+
pipelineId: params.pipelineId
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function buildBatchRunParams(params) {
|
|
362
|
+
return compactObject({
|
|
363
|
+
env: params.environment,
|
|
364
|
+
services: params.serviceNames.length > 0 ? params.serviceNames : undefined,
|
|
365
|
+
pipelineNames: params.serviceNames.length > 0 ? params.serviceNames : undefined,
|
|
366
|
+
workflow: params.workflow,
|
|
367
|
+
pipelineId: params.pipelineId
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function extractServiceNames(input) {
|
|
372
|
+
const rawTokens = input.match(/[A-Za-z][A-Za-z0-9._-]{2,}/g) || [];
|
|
373
|
+
return [...new Set(rawTokens.filter((token) => {
|
|
374
|
+
const normalized = token.toLowerCase();
|
|
375
|
+
if (GENERIC_SERVICE_TOKENS.has(normalized)) return false;
|
|
376
|
+
return normalized.includes('-') || normalized.includes('_') || normalized.endsWith('service');
|
|
377
|
+
}))];
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function inferEnvironmentFromText(input) {
|
|
381
|
+
if (/生产|prod|production/i.test(input)) return 'prod';
|
|
382
|
+
if (/灰度|gray/i.test(input)) return 'gray';
|
|
383
|
+
if (/预发|uat/i.test(input)) return 'uat';
|
|
384
|
+
if (/测试|test|dev|sit/i.test(input)) return 'test';
|
|
385
|
+
return null;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function normalizeEnvironment(value) {
|
|
389
|
+
const normalized = String(value).toLowerCase();
|
|
390
|
+
if (['生产', 'prod', 'production'].includes(normalized)) return 'prod';
|
|
391
|
+
if (['灰度', 'gray'].includes(normalized)) return 'gray';
|
|
392
|
+
if (['预发', 'uat', 'staging'].includes(normalized)) return 'uat';
|
|
393
|
+
if (['测试', 'test', 'dev', 'sit'].includes(normalized)) return 'test';
|
|
394
|
+
return normalized;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function compactObject(value) {
|
|
398
|
+
return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined && item !== null && item !== ''));
|
|
399
|
+
}
|
|
@@ -12,6 +12,7 @@ module.exports = {
|
|
|
12
12
|
async execute(input, context) {
|
|
13
13
|
const registry = context.componentRegistry;
|
|
14
14
|
const params = parseValidationParams(input);
|
|
15
|
+
const learnedBinding = context.resourceBinding;
|
|
15
16
|
|
|
16
17
|
// Check MCP component availability
|
|
17
18
|
const dbManager = registry?.getAdapter('databaseManager');
|
|
@@ -25,7 +26,7 @@ module.exports = {
|
|
|
25
26
|
};
|
|
26
27
|
|
|
27
28
|
if (params.action === 'sql') {
|
|
28
|
-
if (!dbQuery) {
|
|
29
|
+
if (!dbQuery && !learnedBinding) {
|
|
29
30
|
return {
|
|
30
31
|
type: 'result',
|
|
31
32
|
skill: 'data-logic-validation',
|
|
@@ -44,7 +45,8 @@ module.exports = {
|
|
|
44
45
|
action: 'validate_sql',
|
|
45
46
|
query: params.query,
|
|
46
47
|
mcpTool: 'mcpQueryExec',
|
|
47
|
-
availableComponents
|
|
48
|
+
availableComponents,
|
|
49
|
+
binding: learnedBinding
|
|
48
50
|
},
|
|
49
51
|
input,
|
|
50
52
|
timestamp: new Date().toISOString()
|
|
@@ -52,7 +54,7 @@ module.exports = {
|
|
|
52
54
|
}
|
|
53
55
|
|
|
54
56
|
if (params.action === 'redis') {
|
|
55
|
-
if (!redisMonitor) {
|
|
57
|
+
if (!redisMonitor && !learnedBinding) {
|
|
56
58
|
return {
|
|
57
59
|
type: 'result',
|
|
58
60
|
skill: 'data-logic-validation',
|
|
@@ -71,7 +73,8 @@ module.exports = {
|
|
|
71
73
|
action: 'validate_redis',
|
|
72
74
|
key: params.key,
|
|
73
75
|
mcpTools: ['mcpRedisKeyGet', 'mcpRedisKeyInfo', 'mcpRedisKeys'],
|
|
74
|
-
availableComponents
|
|
76
|
+
availableComponents,
|
|
77
|
+
binding: learnedBinding
|
|
75
78
|
},
|
|
76
79
|
input,
|
|
77
80
|
timestamp: new Date().toISOString()
|
|
@@ -85,7 +88,8 @@ module.exports = {
|
|
|
85
88
|
data: {
|
|
86
89
|
actions: ['sql - Validate SQL queries', 'redis - Validate Redis operations'],
|
|
87
90
|
availableComponents,
|
|
88
|
-
requiredMCP: ['databaseManager', 'databaseQuery', 'redisMonitor']
|
|
91
|
+
requiredMCP: ['databaseManager', 'databaseQuery', 'redisMonitor'],
|
|
92
|
+
binding: learnedBinding
|
|
89
93
|
},
|
|
90
94
|
input,
|
|
91
95
|
timestamp: new Date().toISOString()
|
|
@@ -29,6 +29,11 @@ Manage database, Redis, API, and other external resource connections.
|
|
|
29
29
|
- Authentication management
|
|
30
30
|
- Request/response handling
|
|
31
31
|
|
|
32
|
+
### 🧠 Business Binding Memory
|
|
33
|
+
- Save MCP server, address, token, namespace, project, and similar connection fields by business
|
|
34
|
+
- Reuse learned bindings automatically when the same business appears again
|
|
35
|
+
- Keep binding records in local learning files so users do not need to re-enter the same setup repeatedly
|
|
36
|
+
|
|
32
37
|
## Trigger Patterns
|
|
33
38
|
|
|
34
39
|
```
|
|
@@ -102,6 +107,7 @@ This skill supports FlowMind learning:
|
|
|
102
107
|
- **Connection Method**: Learns preferred connection method
|
|
103
108
|
- **Default Database**: Learns frequently used databases
|
|
104
109
|
- **Query Style**: Learns query formatting preferences
|
|
110
|
+
- **Business Resource Binding**: Learns MCP/address/token configuration by business context
|
|
105
111
|
|
|
106
112
|
```
|
|
107
113
|
User: "用 source_id 连接"
|
|
@@ -111,6 +117,20 @@ User: [Next connection]
|
|
|
111
117
|
FlowMind: [Uses source_id automatically]
|
|
112
118
|
```
|
|
113
119
|
|
|
120
|
+
### Example 3: Business Binding
|
|
121
|
+
|
|
122
|
+
```
|
|
123
|
+
User: 绑定零售业务 mcp=yapi-mcp 地址=https://yapi.example.com token=xxx project=retail
|
|
124
|
+
|
|
125
|
+
FlowMind:
|
|
126
|
+
✓ Saved learned binding for business: 零售
|
|
127
|
+
|
|
128
|
+
User: 同步零售业务接口到 YApi
|
|
129
|
+
|
|
130
|
+
FlowMind:
|
|
131
|
+
[Reuses learned YApi MCP/address/project binding automatically]
|
|
132
|
+
```
|
|
133
|
+
|
|
114
134
|
## Examples
|
|
115
135
|
|
|
116
136
|
### Example 1: Database Connection
|
|
@@ -219,4 +239,4 @@ FlowMind:
|
|
|
219
239
|
- Passwords are encrypted at rest
|
|
220
240
|
- Connections use TLS when available
|
|
221
241
|
- Credentials are never logged
|
|
222
|
-
- Connection pooling prevents exhaustion
|
|
242
|
+
- Connection pooling prevents exhaustion
|