flowmind 1.3.0 → 1.4.1
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 +46 -0
- package/README.md +1 -1
- package/bin/flowmind.js +46 -0
- package/core/ai/base-model.js +47 -28
- package/core/ai/model-manager.js +100 -90
- package/core/ai/providers/anthropic.js +21 -14
- package/core/ai/providers/deepseek.js +12 -2
- package/core/ai/providers/ernie.js +2 -2
- package/core/ai/providers/glm.js +12 -2
- package/core/ai/providers/mimo.js +12 -2
- package/core/ai/providers/ollama.js +5 -4
- package/core/ai/providers/openai.js +8 -12
- package/core/ai/providers/qwen.js +12 -2
- package/core/honor-engine.js +5 -5
- package/core/index.js +190 -6
- package/core/learning-engine.js +2 -4
- package/core/scene-matcher.js +2 -4
- package/core/skill-loader.js +7 -0
- package/core/utils.js +18 -0
- package/package.json +1 -1
- package/tui/app.jsx +1 -1
- package/tui/components/ChatPanel.jsx +34 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [1.3.0] - 2026-06-26
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- Missing `os` import in config-manager.js
|
|
7
|
+
- MCP Server version hardcoded instead of reading from package.json
|
|
8
|
+
- `config --set` and `skill --set` CLI commands broken (Commander arg parsing)
|
|
9
|
+
- `learn --reset` and `learn --delete` were unimplemented stubs
|
|
10
|
+
- Learning engine race condition on concurrent file writes (added WriteQueue)
|
|
11
|
+
- TUI/Dashboard crash on non-TTY stdin (Ink raw mode error)
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
- All 17 skills now have working `execute()` implementations
|
|
15
|
+
- Global uncaughtException/unhandledRejection handlers
|
|
16
|
+
- Graceful exit handling in TUI via onExit callback
|
|
17
|
+
|
|
18
|
+
## [1.2.2] - 2026-06-26
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
- ChatPanel React import issue with ink-text-input and ink-spinner ESM/CJS interop
|
|
22
|
+
|
|
23
|
+
## [1.2.0] - 2026-06-26
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
- TUI with split panels, skill browser, dragon display
|
|
27
|
+
- Dashboard with real-time activity feed and stats
|
|
28
|
+
- Event system for cross-component communication
|
|
29
|
+
- Update command for auto-updating flowmind
|
|
30
|
+
|
|
31
|
+
## [1.1.0] - 2026-06-25
|
|
32
|
+
|
|
33
|
+
### Added
|
|
34
|
+
- Mainstream AI model providers (GLM, MiMo, Qwen, ERNIE, DeepSeek)
|
|
35
|
+
- AI model integration with rule-based fallback
|
|
36
|
+
- MCP Server for Claude/Codex integration
|
|
37
|
+
|
|
38
|
+
## [1.0.1] - 2026-06-24
|
|
39
|
+
|
|
40
|
+
### Added
|
|
41
|
+
- Initial release
|
|
42
|
+
- Skill loading and execution engine
|
|
43
|
+
- Learning engine for user corrections and preferences
|
|
44
|
+
- Honor engine with dragon totem gamification
|
|
45
|
+
- Scene matcher for workflow automation
|
|
46
|
+
- Configuration manager
|
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
[](LICENSE)
|
|
10
10
|
[](CONTRIBUTING.md)
|
|
11
|
-
[](CHANGELOG.md)
|
|
12
12
|
|
|
13
13
|
[中文](README_CN.md) | [Quick Start](#-quick-start) | [How It Works](#-how-it-works) | [Use Cases](#-use-cases) | [Architecture](#-architecture)
|
|
14
14
|
|
package/bin/flowmind.js
CHANGED
|
@@ -1460,6 +1460,52 @@ program
|
|
|
1460
1460
|
}
|
|
1461
1461
|
});
|
|
1462
1462
|
|
|
1463
|
+
// Doctor command - System health check
|
|
1464
|
+
program
|
|
1465
|
+
.command('doctor')
|
|
1466
|
+
.description('Run system health checks and diagnostics')
|
|
1467
|
+
.option('-j, --json', 'Output as JSON')
|
|
1468
|
+
.action(async (options) => {
|
|
1469
|
+
try {
|
|
1470
|
+
const fm = await initFlowMind();
|
|
1471
|
+
const result = await fm.doctor();
|
|
1472
|
+
|
|
1473
|
+
if (options.json) {
|
|
1474
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1475
|
+
return;
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
console.log(chalk.cyan('\nFlowMind Health Check\n'));
|
|
1479
|
+
console.log('─'.repeat(60));
|
|
1480
|
+
|
|
1481
|
+
for (const check of result.checks) {
|
|
1482
|
+
const icon = check.status === 'ok' ? chalk.green('✓')
|
|
1483
|
+
: check.status === 'warning' ? chalk.yellow('⚠')
|
|
1484
|
+
: chalk.red('✗');
|
|
1485
|
+
console.log(` ${icon} ${check.name.padEnd(20)} ${check.message}`);
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
console.log('─'.repeat(60));
|
|
1489
|
+
const { ok, warnings, errors } = result.summary;
|
|
1490
|
+
const summaryLine = [
|
|
1491
|
+
chalk.green(`${ok} ok`),
|
|
1492
|
+
warnings > 0 ? chalk.yellow(`${warnings} warning(s)`) : null,
|
|
1493
|
+
errors > 0 ? chalk.red(`${errors} error(s)`) : null
|
|
1494
|
+
].filter(Boolean).join(', ');
|
|
1495
|
+
console.log(`\n Result: ${summaryLine}`);
|
|
1496
|
+
|
|
1497
|
+
if (errors > 0) {
|
|
1498
|
+
console.log(chalk.red('\n Some checks failed. Please fix the errors above.'));
|
|
1499
|
+
} else if (warnings > 0) {
|
|
1500
|
+
console.log(chalk.yellow('\n Some warnings detected. Review for potential issues.'));
|
|
1501
|
+
} else {
|
|
1502
|
+
console.log(chalk.green('\n All checks passed!'));
|
|
1503
|
+
}
|
|
1504
|
+
} catch (error) {
|
|
1505
|
+
console.error(chalk.red('Doctor Error:'), error.message);
|
|
1506
|
+
}
|
|
1507
|
+
});
|
|
1508
|
+
|
|
1463
1509
|
// Update command - Auto-update flowmind
|
|
1464
1510
|
program
|
|
1465
1511
|
.command('update')
|
package/core/ai/base-model.js
CHANGED
|
@@ -8,48 +8,27 @@ class BaseModel {
|
|
|
8
8
|
this.name = name;
|
|
9
9
|
this.config = config;
|
|
10
10
|
this.initialized = false;
|
|
11
|
+
this.requestTimeout = config.requestTimeout || 30000;
|
|
12
|
+
this.maxRetries = config.maxRetries || 3;
|
|
13
|
+
this.retryDelay = config.retryDelay || 1000;
|
|
11
14
|
}
|
|
12
15
|
|
|
13
|
-
/**
|
|
14
|
-
* 初始化模型
|
|
15
|
-
* @returns {Promise<void>}
|
|
16
|
-
*/
|
|
17
16
|
async init() {
|
|
18
17
|
throw new Error('init() must be implemented by subclass');
|
|
19
18
|
}
|
|
20
19
|
|
|
21
|
-
/**
|
|
22
|
-
* 发送聊天请求
|
|
23
|
-
* @param {Array} messages - 消息数组 [{role, content}]
|
|
24
|
-
* @param {Object} options - 请求选项
|
|
25
|
-
* @returns {Promise<string>} 模型响应
|
|
26
|
-
*/
|
|
27
20
|
async chat(messages, options = {}) {
|
|
28
21
|
throw new Error('chat() must be implemented by subclass');
|
|
29
22
|
}
|
|
30
23
|
|
|
31
|
-
/**
|
|
32
|
-
* 发送补全请求
|
|
33
|
-
* @param {string} prompt - 提示词
|
|
34
|
-
* @param {Object} options - 请求选项
|
|
35
|
-
* @returns {Promise<string>} 模型响应
|
|
36
|
-
*/
|
|
37
24
|
async complete(prompt, options = {}) {
|
|
38
25
|
throw new Error('complete() must be implemented by subclass');
|
|
39
26
|
}
|
|
40
27
|
|
|
41
|
-
/**
|
|
42
|
-
* 检查模型是否可用
|
|
43
|
-
* @returns {Promise<boolean>}
|
|
44
|
-
*/
|
|
45
28
|
async isAvailable() {
|
|
46
29
|
throw new Error('isAvailable() must be implemented by subclass');
|
|
47
30
|
}
|
|
48
31
|
|
|
49
|
-
/**
|
|
50
|
-
* 获取模型信息
|
|
51
|
-
* @returns {Object}
|
|
52
|
-
*/
|
|
53
32
|
getInfo() {
|
|
54
33
|
return {
|
|
55
34
|
name: this.name,
|
|
@@ -58,13 +37,53 @@ class BaseModel {
|
|
|
58
37
|
};
|
|
59
38
|
}
|
|
60
39
|
|
|
61
|
-
/**
|
|
62
|
-
* 验证配置
|
|
63
|
-
* @returns {boolean}
|
|
64
|
-
*/
|
|
65
40
|
validateConfig() {
|
|
66
41
|
return true;
|
|
67
42
|
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Fetch with retry, timeout, and exponential backoff
|
|
46
|
+
*/
|
|
47
|
+
async fetchWithRetry(url, options = {}) {
|
|
48
|
+
const timeout = options.timeout || this.requestTimeout;
|
|
49
|
+
const maxRetries = options.retries || this.maxRetries;
|
|
50
|
+
|
|
51
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
52
|
+
try {
|
|
53
|
+
const controller = new AbortController();
|
|
54
|
+
const timer = setTimeout(() => controller.abort(), timeout);
|
|
55
|
+
|
|
56
|
+
const response = await fetch(url, {
|
|
57
|
+
...options,
|
|
58
|
+
signal: controller.signal
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
clearTimeout(timer);
|
|
62
|
+
|
|
63
|
+
// Retry on rate limit (429) and server errors (5xx)
|
|
64
|
+
if (response.status === 429 || response.status >= 500) {
|
|
65
|
+
if (attempt < maxRetries) {
|
|
66
|
+
const delay = this.retryDelay * Math.pow(2, attempt);
|
|
67
|
+
await new Promise(r => setTimeout(r, delay));
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return response;
|
|
73
|
+
} catch (error) {
|
|
74
|
+
if (error.name === 'AbortError') {
|
|
75
|
+
if (attempt < maxRetries) continue;
|
|
76
|
+
throw new Error(`Request timeout after ${timeout}ms`);
|
|
77
|
+
}
|
|
78
|
+
if (attempt < maxRetries) {
|
|
79
|
+
const delay = this.retryDelay * Math.pow(2, attempt);
|
|
80
|
+
await new Promise(r => setTimeout(r, delay));
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
68
87
|
}
|
|
69
88
|
|
|
70
89
|
module.exports = BaseModel;
|
package/core/ai/model-manager.js
CHANGED
|
@@ -28,6 +28,9 @@ class ModelManager {
|
|
|
28
28
|
};
|
|
29
29
|
this.fallbackToRules = config.fallbackToRules !== false;
|
|
30
30
|
this.initialized = false;
|
|
31
|
+
this.cache = new Map();
|
|
32
|
+
this.cacheMaxAge = config.cacheMaxAge || 300000; // 5 minutes
|
|
33
|
+
this.cacheMaxSize = config.cacheMaxSize || 100;
|
|
31
34
|
}
|
|
32
35
|
|
|
33
36
|
/**
|
|
@@ -119,30 +122,16 @@ class ModelManager {
|
|
|
119
122
|
* @returns {Promise<Object>} 意图分析结果
|
|
120
123
|
*/
|
|
121
124
|
async understandIntent(input, context = {}) {
|
|
122
|
-
if (!this.features.intentUnderstanding)
|
|
123
|
-
return null;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const provider = this.getDefaultProvider();
|
|
127
|
-
if (!provider) {
|
|
128
|
-
return this._fallback('intentUnderstanding', input, context);
|
|
129
|
-
}
|
|
125
|
+
if (!this.features.intentUnderstanding) return null;
|
|
130
126
|
|
|
131
|
-
|
|
127
|
+
return this._callWithFailover(async (provider) => {
|
|
132
128
|
const messages = [
|
|
133
129
|
{ role: 'system', content: prompts.intent.system },
|
|
134
130
|
{ role: 'user', content: prompts.intent.user(input, context) }
|
|
135
131
|
];
|
|
136
|
-
|
|
137
|
-
const response = await provider.chat(messages, {
|
|
138
|
-
responseFormat: { type: 'json_object' }
|
|
139
|
-
});
|
|
140
|
-
|
|
132
|
+
const response = await provider.chat(messages, { responseFormat: { type: 'json_object' } });
|
|
141
133
|
return JSON.parse(response);
|
|
142
|
-
}
|
|
143
|
-
console.warn(`AI intent understanding failed: ${error.message}`);
|
|
144
|
-
return this._fallback('intentUnderstanding', input, context);
|
|
145
|
-
}
|
|
134
|
+
}, 'intentUnderstanding');
|
|
146
135
|
}
|
|
147
136
|
|
|
148
137
|
/**
|
|
@@ -153,30 +142,16 @@ class ModelManager {
|
|
|
153
142
|
* @returns {Promise<Object>} 提取的参数
|
|
154
143
|
*/
|
|
155
144
|
async extractParameters(input, skillName, skillSchema = {}) {
|
|
156
|
-
if (!this.features.parameterExtraction) {
|
|
157
|
-
return {};
|
|
158
|
-
}
|
|
145
|
+
if (!this.features.parameterExtraction) return {};
|
|
159
146
|
|
|
160
|
-
|
|
161
|
-
if (!provider) {
|
|
162
|
-
return this._fallback('parameterExtraction', input, { skillName });
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
try {
|
|
147
|
+
return this._callWithFailover(async (provider) => {
|
|
166
148
|
const messages = [
|
|
167
149
|
{ role: 'system', content: prompts.extraction.system },
|
|
168
150
|
{ role: 'user', content: prompts.extraction.user(input, skillName, skillSchema) }
|
|
169
151
|
];
|
|
170
|
-
|
|
171
|
-
const response = await provider.chat(messages, {
|
|
172
|
-
responseFormat: { type: 'json_object' }
|
|
173
|
-
});
|
|
174
|
-
|
|
152
|
+
const response = await provider.chat(messages, { responseFormat: { type: 'json_object' } });
|
|
175
153
|
return JSON.parse(response);
|
|
176
|
-
}
|
|
177
|
-
console.warn(`AI parameter extraction failed: ${error.message}`);
|
|
178
|
-
return this._fallback('parameterExtraction', input, { skillName });
|
|
179
|
-
}
|
|
154
|
+
}, 'parameterExtraction');
|
|
180
155
|
}
|
|
181
156
|
|
|
182
157
|
/**
|
|
@@ -186,30 +161,16 @@ class ModelManager {
|
|
|
186
161
|
* @returns {Promise<Object>} 选择的技能
|
|
187
162
|
*/
|
|
188
163
|
async selectSkill(input, candidates) {
|
|
189
|
-
if (!this.features.skillSelection)
|
|
190
|
-
return null;
|
|
191
|
-
}
|
|
164
|
+
if (!this.features.skillSelection) return null;
|
|
192
165
|
|
|
193
|
-
|
|
194
|
-
if (!provider) {
|
|
195
|
-
return this._fallback('skillSelection', input, { candidates });
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
try {
|
|
166
|
+
return this._callWithFailover(async (provider) => {
|
|
199
167
|
const messages = [
|
|
200
168
|
{ role: 'system', content: prompts.selection.system },
|
|
201
169
|
{ role: 'user', content: prompts.selection.user(input, candidates) }
|
|
202
170
|
];
|
|
203
|
-
|
|
204
|
-
const response = await provider.chat(messages, {
|
|
205
|
-
responseFormat: { type: 'json_object' }
|
|
206
|
-
});
|
|
207
|
-
|
|
171
|
+
const response = await provider.chat(messages, { responseFormat: { type: 'json_object' } });
|
|
208
172
|
return JSON.parse(response);
|
|
209
|
-
}
|
|
210
|
-
console.warn(`AI skill selection failed: ${error.message}`);
|
|
211
|
-
return this._fallback('skillSelection', input, { candidates });
|
|
212
|
-
}
|
|
173
|
+
}, 'skillSelection');
|
|
213
174
|
}
|
|
214
175
|
|
|
215
176
|
/**
|
|
@@ -219,26 +180,15 @@ class ModelManager {
|
|
|
219
180
|
* @returns {Promise<string>} 摘要文本
|
|
220
181
|
*/
|
|
221
182
|
async summarizeResult(result, context = {}) {
|
|
222
|
-
if (!this.features.resultSummary)
|
|
223
|
-
return null;
|
|
224
|
-
}
|
|
183
|
+
if (!this.features.resultSummary) return null;
|
|
225
184
|
|
|
226
|
-
|
|
227
|
-
if (!provider) {
|
|
228
|
-
return this._fallback('resultSummary', result, context);
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
try {
|
|
185
|
+
return this._callWithFailover(async (provider) => {
|
|
232
186
|
const messages = [
|
|
233
187
|
{ role: 'system', content: prompts.summary.system },
|
|
234
188
|
{ role: 'user', content: prompts.summary.user(result, context) }
|
|
235
189
|
];
|
|
236
|
-
|
|
237
190
|
return await provider.chat(messages);
|
|
238
|
-
}
|
|
239
|
-
console.warn(`AI result summary failed: ${error.message}`);
|
|
240
|
-
return this._fallback('resultSummary', result, context);
|
|
241
|
-
}
|
|
191
|
+
}, 'resultSummary');
|
|
242
192
|
}
|
|
243
193
|
|
|
244
194
|
/**
|
|
@@ -248,42 +198,102 @@ class ModelManager {
|
|
|
248
198
|
* @returns {Promise<Object>} 学习分析结果
|
|
249
199
|
*/
|
|
250
200
|
async analyzeLearningFeedback(input, context = {}) {
|
|
251
|
-
if (!this.features.learningFeedback)
|
|
252
|
-
return null;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
const provider = this.getDefaultProvider();
|
|
256
|
-
if (!provider) {
|
|
257
|
-
return this._fallback('learningFeedback', input, context);
|
|
258
|
-
}
|
|
201
|
+
if (!this.features.learningFeedback) return null;
|
|
259
202
|
|
|
260
|
-
|
|
203
|
+
return this._callWithFailover(async (provider) => {
|
|
261
204
|
const messages = [
|
|
262
205
|
{ role: 'system', content: prompts.learning.system },
|
|
263
206
|
{ role: 'user', content: prompts.learning.user(input, context) }
|
|
264
207
|
];
|
|
208
|
+
const response = await provider.chat(messages, { responseFormat: { type: 'json_object' } });
|
|
209
|
+
return JSON.parse(response);
|
|
210
|
+
}, 'learningFeedback');
|
|
211
|
+
}
|
|
265
212
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
213
|
+
/**
|
|
214
|
+
* Get cached result if available and not expired
|
|
215
|
+
* @private
|
|
216
|
+
*/
|
|
217
|
+
_getCached(key) {
|
|
218
|
+
const entry = this.cache.get(key);
|
|
219
|
+
if (!entry) return null;
|
|
220
|
+
if (Date.now() - entry.timestamp > this.cacheMaxAge) {
|
|
221
|
+
this.cache.delete(key);
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
return entry.value;
|
|
225
|
+
}
|
|
269
226
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
227
|
+
/**
|
|
228
|
+
* Set cache entry, evicting oldest if at capacity
|
|
229
|
+
* @private
|
|
230
|
+
*/
|
|
231
|
+
_setCache(key, value) {
|
|
232
|
+
if (this.cache.size >= this.cacheMaxSize) {
|
|
233
|
+
const oldest = this.cache.keys().next().value;
|
|
234
|
+
this.cache.delete(oldest);
|
|
235
|
+
}
|
|
236
|
+
this.cache.set(key, { value, timestamp: Date.now() });
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Clear the AI response cache
|
|
241
|
+
*/
|
|
242
|
+
clearCache() {
|
|
243
|
+
this.cache.clear();
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Call AI with automatic failover to other providers
|
|
248
|
+
* @private
|
|
249
|
+
*/
|
|
250
|
+
async _callWithFailover(fn, feature) {
|
|
251
|
+
// Try default provider first
|
|
252
|
+
const defaultProvider = this.getDefaultProvider();
|
|
253
|
+
if (defaultProvider) {
|
|
254
|
+
try {
|
|
255
|
+
return await fn(defaultProvider);
|
|
256
|
+
} catch (error) {
|
|
257
|
+
console.warn(`Default provider (${this.defaultProvider}) failed for ${feature}: ${error.message}`);
|
|
258
|
+
}
|
|
274
259
|
}
|
|
260
|
+
|
|
261
|
+
// Try other providers
|
|
262
|
+
for (const [name, provider] of this.providers) {
|
|
263
|
+
if (name === this.defaultProvider || !provider.initialized) continue;
|
|
264
|
+
try {
|
|
265
|
+
console.warn(`Failing over to provider: ${name} for ${feature}`);
|
|
266
|
+
return await fn(provider);
|
|
267
|
+
} catch (error) {
|
|
268
|
+
console.warn(`Failover provider ${name} failed for ${feature}: ${error.message}`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// All providers failed, use rule-based fallback
|
|
273
|
+
return this._fallback(feature);
|
|
275
274
|
}
|
|
276
275
|
|
|
277
276
|
/**
|
|
278
|
-
*
|
|
277
|
+
* Rule-based fallback when AI is unavailable
|
|
279
278
|
* @private
|
|
280
279
|
*/
|
|
281
|
-
_fallback(feature
|
|
282
|
-
if (!this.fallbackToRules)
|
|
283
|
-
|
|
280
|
+
_fallback(feature) {
|
|
281
|
+
if (!this.fallbackToRules) return null;
|
|
282
|
+
|
|
283
|
+
switch (feature) {
|
|
284
|
+
case 'intentUnderstanding':
|
|
285
|
+
return { intent: 'unknown', confidence: 0, source: 'rules' };
|
|
286
|
+
case 'parameterExtraction':
|
|
287
|
+
return {};
|
|
288
|
+
case 'skillSelection':
|
|
289
|
+
return { selectedSkill: null, confidence: 0, source: 'rules' };
|
|
290
|
+
case 'resultSummary':
|
|
291
|
+
return null;
|
|
292
|
+
case 'learningFeedback':
|
|
293
|
+
return { isLearning: false, source: 'rules' };
|
|
294
|
+
default:
|
|
295
|
+
return null;
|
|
284
296
|
}
|
|
285
|
-
// 返回 null,让调用方使用规则引擎
|
|
286
|
-
return null;
|
|
287
297
|
}
|
|
288
298
|
|
|
289
299
|
/**
|
|
@@ -8,7 +8,7 @@ class AnthropicProvider extends BaseModel {
|
|
|
8
8
|
constructor(config = {}) {
|
|
9
9
|
super('anthropic', config);
|
|
10
10
|
this.apiKey = config.apiKey || process.env.ANTHROPIC_API_KEY;
|
|
11
|
-
this.model = config.model || 'claude-
|
|
11
|
+
this.model = config.model || 'claude-sonnet-4-20250514';
|
|
12
12
|
this.baseUrl = config.baseUrl || 'https://api.anthropic.com';
|
|
13
13
|
this.maxTokens = config.maxTokens ?? 2000;
|
|
14
14
|
this.temperature = config.temperature ?? 0.3;
|
|
@@ -26,14 +26,10 @@ class AnthropicProvider extends BaseModel {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
async chat(messages, options = {}) {
|
|
29
|
-
if (!this.initialized)
|
|
30
|
-
await this.init();
|
|
31
|
-
}
|
|
29
|
+
if (!this.initialized) await this.init();
|
|
32
30
|
|
|
33
|
-
// 转换消息格式:提取 system 消息
|
|
34
31
|
let systemPrompt = '';
|
|
35
32
|
const userMessages = [];
|
|
36
|
-
|
|
37
33
|
for (const msg of messages) {
|
|
38
34
|
if (msg.role === 'system') {
|
|
39
35
|
systemPrompt = msg.content;
|
|
@@ -42,7 +38,7 @@ class AnthropicProvider extends BaseModel {
|
|
|
42
38
|
}
|
|
43
39
|
}
|
|
44
40
|
|
|
45
|
-
const response = await
|
|
41
|
+
const response = await this.fetchWithRetry(`${this.baseUrl}/v1/messages`, {
|
|
46
42
|
method: 'POST',
|
|
47
43
|
headers: {
|
|
48
44
|
'Content-Type': 'application/json',
|
|
@@ -74,19 +70,30 @@ class AnthropicProvider extends BaseModel {
|
|
|
74
70
|
async isAvailable() {
|
|
75
71
|
try {
|
|
76
72
|
if (!this.apiKey) return false;
|
|
77
|
-
// Anthropic
|
|
78
|
-
|
|
73
|
+
// Anthropic has no /models endpoint; make a lightweight messages call to verify
|
|
74
|
+
const response = await this.fetchWithRetry(`${this.baseUrl}/v1/messages`, {
|
|
75
|
+
method: 'POST',
|
|
76
|
+
headers: {
|
|
77
|
+
'Content-Type': 'application/json',
|
|
78
|
+
'x-api-key': this.apiKey,
|
|
79
|
+
'anthropic-version': '2023-06-01'
|
|
80
|
+
},
|
|
81
|
+
body: JSON.stringify({
|
|
82
|
+
model: this.model,
|
|
83
|
+
max_tokens: 1,
|
|
84
|
+
messages: [{ role: 'user', content: 'hi' }]
|
|
85
|
+
}),
|
|
86
|
+
retries: 1,
|
|
87
|
+
timeout: 10000
|
|
88
|
+
});
|
|
89
|
+
return response.ok || response.status === 400; // 400 = bad request but API is reachable
|
|
79
90
|
} catch {
|
|
80
91
|
return false;
|
|
81
92
|
}
|
|
82
93
|
}
|
|
83
94
|
|
|
84
95
|
getInfo() {
|
|
85
|
-
return {
|
|
86
|
-
...super.getInfo(),
|
|
87
|
-
model: this.model,
|
|
88
|
-
baseUrl: this.baseUrl
|
|
89
|
-
};
|
|
96
|
+
return { ...super.getInfo(), model: this.model, baseUrl: this.baseUrl };
|
|
90
97
|
}
|
|
91
98
|
}
|
|
92
99
|
|
|
@@ -31,7 +31,7 @@ class DeepSeekProvider extends BaseModel {
|
|
|
31
31
|
await this.init();
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
const response = await
|
|
34
|
+
const response = await this.fetchWithRetry(`${this.baseUrl}/chat/completions`, {
|
|
35
35
|
method: 'POST',
|
|
36
36
|
headers: {
|
|
37
37
|
'Content-Type': 'application/json',
|
|
@@ -61,7 +61,17 @@ class DeepSeekProvider extends BaseModel {
|
|
|
61
61
|
async isAvailable() {
|
|
62
62
|
try {
|
|
63
63
|
if (!this.apiKey) return false;
|
|
64
|
-
|
|
64
|
+
const response = await this.fetchWithRetry(`${this.baseUrl}/chat/completions`, {
|
|
65
|
+
method: 'POST',
|
|
66
|
+
headers: {
|
|
67
|
+
'Content-Type': 'application/json',
|
|
68
|
+
'Authorization': `Bearer ${this.apiKey}`
|
|
69
|
+
},
|
|
70
|
+
body: JSON.stringify({ model: this.model, messages: [{ role: 'user', content: 'hi' }], max_tokens: 1 }),
|
|
71
|
+
retries: 1,
|
|
72
|
+
timeout: 10000
|
|
73
|
+
});
|
|
74
|
+
return response.ok || response.status === 400;
|
|
65
75
|
} catch {
|
|
66
76
|
return false;
|
|
67
77
|
}
|
|
@@ -26,7 +26,7 @@ class ERNIEProvider extends BaseModel {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
async refreshAccessToken() {
|
|
29
|
-
const response = await
|
|
29
|
+
const response = await this.fetchWithRetry(
|
|
30
30
|
`https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials&client_id=${this.apiKey}&client_secret=${this.secretKey}`,
|
|
31
31
|
{ method: 'POST' }
|
|
32
32
|
);
|
|
@@ -51,7 +51,7 @@ class ERNIEProvider extends BaseModel {
|
|
|
51
51
|
const model = options.model || this.model;
|
|
52
52
|
const endpoint = this.getEndpoint(model);
|
|
53
53
|
|
|
54
|
-
const response = await
|
|
54
|
+
const response = await this.fetchWithRetry(`${this.baseUrl}${endpoint}?access_token=${this.accessToken}`, {
|
|
55
55
|
method: 'POST',
|
|
56
56
|
headers: {
|
|
57
57
|
'Content-Type': 'application/json'
|
package/core/ai/providers/glm.js
CHANGED
|
@@ -31,7 +31,7 @@ class GLMProvider extends BaseModel {
|
|
|
31
31
|
await this.init();
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
const response = await
|
|
34
|
+
const response = await this.fetchWithRetry(`${this.baseUrl}/chat/completions`, {
|
|
35
35
|
method: 'POST',
|
|
36
36
|
headers: {
|
|
37
37
|
'Content-Type': 'application/json',
|
|
@@ -61,7 +61,17 @@ class GLMProvider extends BaseModel {
|
|
|
61
61
|
async isAvailable() {
|
|
62
62
|
try {
|
|
63
63
|
if (!this.apiKey) return false;
|
|
64
|
-
|
|
64
|
+
const response = await this.fetchWithRetry(`${this.baseUrl}/chat/completions`, {
|
|
65
|
+
method: 'POST',
|
|
66
|
+
headers: {
|
|
67
|
+
'Content-Type': 'application/json',
|
|
68
|
+
'Authorization': `Bearer ${this.apiKey}`
|
|
69
|
+
},
|
|
70
|
+
body: JSON.stringify({ model: this.model, messages: [{ role: 'user', content: 'hi' }], max_tokens: 1 }),
|
|
71
|
+
retries: 1,
|
|
72
|
+
timeout: 10000
|
|
73
|
+
});
|
|
74
|
+
return response.ok || response.status === 400;
|
|
65
75
|
} catch {
|
|
66
76
|
return false;
|
|
67
77
|
}
|
|
@@ -31,7 +31,7 @@ class MiMoProvider extends BaseModel {
|
|
|
31
31
|
await this.init();
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
const response = await
|
|
34
|
+
const response = await this.fetchWithRetry(`${this.baseUrl}/chat/completions`, {
|
|
35
35
|
method: 'POST',
|
|
36
36
|
headers: {
|
|
37
37
|
'Content-Type': 'application/json',
|
|
@@ -61,7 +61,17 @@ class MiMoProvider extends BaseModel {
|
|
|
61
61
|
async isAvailable() {
|
|
62
62
|
try {
|
|
63
63
|
if (!this.apiKey) return false;
|
|
64
|
-
|
|
64
|
+
const response = await this.fetchWithRetry(`${this.baseUrl}/chat/completions`, {
|
|
65
|
+
method: 'POST',
|
|
66
|
+
headers: {
|
|
67
|
+
'Content-Type': 'application/json',
|
|
68
|
+
'Authorization': `Bearer ${this.apiKey}`
|
|
69
|
+
},
|
|
70
|
+
body: JSON.stringify({ model: this.model, messages: [{ role: 'user', content: 'hi' }], max_tokens: 1 }),
|
|
71
|
+
retries: 1,
|
|
72
|
+
timeout: 10000
|
|
73
|
+
});
|
|
74
|
+
return response.ok || response.status === 400;
|
|
65
75
|
} catch {
|
|
66
76
|
return false;
|
|
67
77
|
}
|
|
@@ -34,7 +34,7 @@ class OllamaProvider extends BaseModel {
|
|
|
34
34
|
// 转换消息格式为 Ollama 格式
|
|
35
35
|
const prompt = this._convertMessagesToPrompt(messages);
|
|
36
36
|
|
|
37
|
-
const response = await
|
|
37
|
+
const response = await this.fetchWithRetry(`${this.baseUrl}/api/generate`, {
|
|
38
38
|
method: 'POST',
|
|
39
39
|
headers: {
|
|
40
40
|
'Content-Type': 'application/json'
|
|
@@ -64,7 +64,7 @@ class OllamaProvider extends BaseModel {
|
|
|
64
64
|
await this.init();
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
const response = await
|
|
67
|
+
const response = await this.fetchWithRetry(`${this.baseUrl}/api/generate`, {
|
|
68
68
|
method: 'POST',
|
|
69
69
|
headers: {
|
|
70
70
|
'Content-Type': 'application/json'
|
|
@@ -91,9 +91,10 @@ class OllamaProvider extends BaseModel {
|
|
|
91
91
|
|
|
92
92
|
async isAvailable() {
|
|
93
93
|
try {
|
|
94
|
-
const response = await
|
|
94
|
+
const response = await this.fetchWithRetry(`${this.baseUrl}/api/tags`, {
|
|
95
95
|
method: 'GET',
|
|
96
|
-
|
|
96
|
+
retries: 1,
|
|
97
|
+
timeout: 5000
|
|
97
98
|
});
|
|
98
99
|
return response.ok;
|
|
99
100
|
} catch {
|
|
@@ -8,7 +8,7 @@ class OpenAIProvider extends BaseModel {
|
|
|
8
8
|
constructor(config = {}) {
|
|
9
9
|
super('openai', config);
|
|
10
10
|
this.apiKey = config.apiKey || process.env.OPENAI_API_KEY;
|
|
11
|
-
this.model = config.model || 'gpt-
|
|
11
|
+
this.model = config.model || 'gpt-4o';
|
|
12
12
|
this.baseUrl = config.baseUrl || 'https://api.openai.com/v1';
|
|
13
13
|
this.temperature = config.temperature ?? 0.3;
|
|
14
14
|
this.maxTokens = config.maxTokens ?? 2000;
|
|
@@ -26,11 +26,9 @@ class OpenAIProvider extends BaseModel {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
async chat(messages, options = {}) {
|
|
29
|
-
if (!this.initialized)
|
|
30
|
-
await this.init();
|
|
31
|
-
}
|
|
29
|
+
if (!this.initialized) await this.init();
|
|
32
30
|
|
|
33
|
-
const response = await
|
|
31
|
+
const response = await this.fetchWithRetry(`${this.baseUrl}/chat/completions`, {
|
|
34
32
|
method: 'POST',
|
|
35
33
|
headers: {
|
|
36
34
|
'Content-Type': 'application/json',
|
|
@@ -61,8 +59,10 @@ class OpenAIProvider extends BaseModel {
|
|
|
61
59
|
async isAvailable() {
|
|
62
60
|
try {
|
|
63
61
|
if (!this.apiKey) return false;
|
|
64
|
-
const response = await
|
|
65
|
-
headers: { 'Authorization': `Bearer ${this.apiKey}` }
|
|
62
|
+
const response = await this.fetchWithRetry(`${this.baseUrl}/models`, {
|
|
63
|
+
headers: { 'Authorization': `Bearer ${this.apiKey}` },
|
|
64
|
+
retries: 1,
|
|
65
|
+
timeout: 10000
|
|
66
66
|
});
|
|
67
67
|
return response.ok;
|
|
68
68
|
} catch {
|
|
@@ -71,11 +71,7 @@ class OpenAIProvider extends BaseModel {
|
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
getInfo() {
|
|
74
|
-
return {
|
|
75
|
-
...super.getInfo(),
|
|
76
|
-
model: this.model,
|
|
77
|
-
baseUrl: this.baseUrl
|
|
78
|
-
};
|
|
74
|
+
return { ...super.getInfo(), model: this.model, baseUrl: this.baseUrl };
|
|
79
75
|
}
|
|
80
76
|
}
|
|
81
77
|
|
|
@@ -31,7 +31,7 @@ class QwenProvider extends BaseModel {
|
|
|
31
31
|
await this.init();
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
const response = await
|
|
34
|
+
const response = await this.fetchWithRetry(`${this.baseUrl}/chat/completions`, {
|
|
35
35
|
method: 'POST',
|
|
36
36
|
headers: {
|
|
37
37
|
'Content-Type': 'application/json',
|
|
@@ -61,7 +61,17 @@ class QwenProvider extends BaseModel {
|
|
|
61
61
|
async isAvailable() {
|
|
62
62
|
try {
|
|
63
63
|
if (!this.apiKey) return false;
|
|
64
|
-
|
|
64
|
+
const response = await this.fetchWithRetry(`${this.baseUrl}/chat/completions`, {
|
|
65
|
+
method: 'POST',
|
|
66
|
+
headers: {
|
|
67
|
+
'Content-Type': 'application/json',
|
|
68
|
+
'Authorization': `Bearer ${this.apiKey}`
|
|
69
|
+
},
|
|
70
|
+
body: JSON.stringify({ model: this.model, messages: [{ role: 'user', content: 'hi' }], max_tokens: 1 }),
|
|
71
|
+
retries: 1,
|
|
72
|
+
timeout: 10000
|
|
73
|
+
});
|
|
74
|
+
return response.ok || response.status === 400;
|
|
65
75
|
} catch {
|
|
66
76
|
return false;
|
|
67
77
|
}
|
package/core/honor-engine.js
CHANGED
|
@@ -50,7 +50,7 @@ class HonorEngine {
|
|
|
50
50
|
}
|
|
51
51
|
this.initialized = true;
|
|
52
52
|
} catch (error) {
|
|
53
|
-
|
|
53
|
+
console.warn('HonorEngine init failed, using defaults:', error.message);
|
|
54
54
|
this.data = this.createDefaultData();
|
|
55
55
|
this.initialized = true;
|
|
56
56
|
}
|
|
@@ -92,7 +92,7 @@ class HonorEngine {
|
|
|
92
92
|
}
|
|
93
93
|
}
|
|
94
94
|
} catch (error) {
|
|
95
|
-
|
|
95
|
+
console.warn('HonorEngine seedKnownSkills failed:', error.message);
|
|
96
96
|
}
|
|
97
97
|
}
|
|
98
98
|
|
|
@@ -148,7 +148,7 @@ class HonorEngine {
|
|
|
148
148
|
timestamp: this.data.lastUpdated
|
|
149
149
|
});
|
|
150
150
|
} catch (error) {
|
|
151
|
-
|
|
151
|
+
console.warn('HonorEngine award failed:', error.message);
|
|
152
152
|
}
|
|
153
153
|
}
|
|
154
154
|
|
|
@@ -170,7 +170,7 @@ class HonorEngine {
|
|
|
170
170
|
await this.save();
|
|
171
171
|
}
|
|
172
172
|
} catch (error) {
|
|
173
|
-
|
|
173
|
+
console.warn('HonorEngine addKnownSkill failed:', error.message);
|
|
174
174
|
}
|
|
175
175
|
}
|
|
176
176
|
|
|
@@ -247,7 +247,7 @@ class HonorEngine {
|
|
|
247
247
|
await fs.ensureDir(path.dirname(this.honorPath));
|
|
248
248
|
await fs.writeJson(this.honorPath, this.data, { spaces: 2 });
|
|
249
249
|
} catch (error) {
|
|
250
|
-
|
|
250
|
+
console.warn('HonorEngine save failed:', error.message);
|
|
251
251
|
}
|
|
252
252
|
}
|
|
253
253
|
}
|
package/core/index.js
CHANGED
|
@@ -22,6 +22,8 @@ class FlowMind {
|
|
|
22
22
|
this.skills = new SkillLoader(this.config, this.learning, this.components, this.honor);
|
|
23
23
|
this.ai = new ModelManager(options.ai || {});
|
|
24
24
|
this.initialized = false;
|
|
25
|
+
this.conversationHistory = [];
|
|
26
|
+
this.maxHistoryLength = options.maxHistoryLength || 50;
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
/**
|
|
@@ -31,6 +33,13 @@ class FlowMind {
|
|
|
31
33
|
if (this.initialized) return this;
|
|
32
34
|
|
|
33
35
|
await this.config.load();
|
|
36
|
+
|
|
37
|
+
// Validate configuration
|
|
38
|
+
const validation = this.config.validate();
|
|
39
|
+
if (!validation.valid) {
|
|
40
|
+
console.warn('Configuration warnings:', validation.errors.join(', '));
|
|
41
|
+
}
|
|
42
|
+
|
|
34
43
|
await this.components.init();
|
|
35
44
|
await this.components.initAll();
|
|
36
45
|
await this.honor.init();
|
|
@@ -49,6 +58,19 @@ class FlowMind {
|
|
|
49
58
|
return this;
|
|
50
59
|
}
|
|
51
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Process a user request with streaming support
|
|
63
|
+
* Yields progress updates as an async generator
|
|
64
|
+
*/
|
|
65
|
+
async *processStream(input, context = {}) {
|
|
66
|
+
yield { type: 'start', input, timestamp: new Date().toISOString() };
|
|
67
|
+
|
|
68
|
+
const result = await this.process(input, context);
|
|
69
|
+
|
|
70
|
+
yield { type: 'progress', message: `Skill: ${result.metadata?.skill || 'unknown'}`, timestamp: new Date().toISOString() };
|
|
71
|
+
yield { type: 'result', data: result, timestamp: new Date().toISOString() };
|
|
72
|
+
}
|
|
73
|
+
|
|
52
74
|
/**
|
|
53
75
|
* Process a user request
|
|
54
76
|
*/
|
|
@@ -57,20 +79,31 @@ class FlowMind {
|
|
|
57
79
|
await this.init();
|
|
58
80
|
}
|
|
59
81
|
|
|
82
|
+
if (!input || typeof input !== 'string') {
|
|
83
|
+
return this.formatError('Invalid input: expected a non-empty string', input);
|
|
84
|
+
}
|
|
85
|
+
|
|
60
86
|
const startTime = Date.now();
|
|
61
87
|
|
|
88
|
+
// Build context with conversation history
|
|
89
|
+
const enhancedContext = {
|
|
90
|
+
...context,
|
|
91
|
+
conversationHistory: this.conversationHistory.slice(-10),
|
|
92
|
+
sessionId: context.sessionId || 'default'
|
|
93
|
+
};
|
|
94
|
+
|
|
62
95
|
eventBus.emit('process:start', { input, timestamp: new Date().toISOString() });
|
|
63
96
|
|
|
64
97
|
try {
|
|
65
98
|
// 1. AI Intent Understanding (if available)
|
|
66
|
-
const intent = await this.ai.understandIntent(input,
|
|
99
|
+
const intent = await this.ai.understandIntent(input, enhancedContext);
|
|
67
100
|
|
|
68
101
|
// 2. Check for learning patterns (corrections, feedback)
|
|
69
102
|
// Use AI to analyze learning feedback if available
|
|
70
|
-
const aiLearningResult = await this.ai.analyzeLearningFeedback(input,
|
|
103
|
+
const aiLearningResult = await this.ai.analyzeLearningFeedback(input, enhancedContext);
|
|
71
104
|
const learningResult = aiLearningResult?.isLearning
|
|
72
105
|
? aiLearningResult
|
|
73
|
-
: await this.learning.detectLearning(input,
|
|
106
|
+
: await this.learning.detectLearning(input, enhancedContext);
|
|
74
107
|
if (learningResult) {
|
|
75
108
|
return this.formatLearningResponse(learningResult);
|
|
76
109
|
}
|
|
@@ -106,12 +139,12 @@ class FlowMind {
|
|
|
106
139
|
const extractedParams = await this.ai.extractParameters(input, skill.name);
|
|
107
140
|
|
|
108
141
|
// 6. Execute with learning applied
|
|
109
|
-
const
|
|
110
|
-
...
|
|
142
|
+
const executeContext = {
|
|
143
|
+
...enhancedContext,
|
|
111
144
|
...extractedParams,
|
|
112
145
|
intent: intent
|
|
113
146
|
};
|
|
114
|
-
const result = await this.executeWithLearning(skill, input,
|
|
147
|
+
const result = await this.executeWithLearning(skill, input, executeContext);
|
|
115
148
|
|
|
116
149
|
// 7. Generate AI summary (if available)
|
|
117
150
|
const summary = await this.ai.summarizeResult(result, {
|
|
@@ -128,6 +161,17 @@ class FlowMind {
|
|
|
128
161
|
aiEnhanced: !!summary
|
|
129
162
|
});
|
|
130
163
|
|
|
164
|
+
// Save to conversation history
|
|
165
|
+
this.conversationHistory.push({
|
|
166
|
+
input,
|
|
167
|
+
output: formatted,
|
|
168
|
+
skill: skill.name,
|
|
169
|
+
timestamp: new Date().toISOString()
|
|
170
|
+
});
|
|
171
|
+
if (this.conversationHistory.length > this.maxHistoryLength) {
|
|
172
|
+
this.conversationHistory.shift();
|
|
173
|
+
}
|
|
174
|
+
|
|
131
175
|
eventBus.emit('process:complete', {
|
|
132
176
|
input,
|
|
133
177
|
skill: skill.name,
|
|
@@ -320,6 +364,146 @@ class FlowMind {
|
|
|
320
364
|
async importLearnings(data) {
|
|
321
365
|
return this.learning.import(data);
|
|
322
366
|
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Get conversation history
|
|
370
|
+
*/
|
|
371
|
+
getConversationHistory(sessionId) {
|
|
372
|
+
if (sessionId) {
|
|
373
|
+
return this.conversationHistory.filter(h => h.sessionId === sessionId);
|
|
374
|
+
}
|
|
375
|
+
return this.conversationHistory;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Clear conversation history
|
|
380
|
+
*/
|
|
381
|
+
clearHistory() {
|
|
382
|
+
this.conversationHistory = [];
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Graceful shutdown - flush pending data and clean up
|
|
387
|
+
*/
|
|
388
|
+
async shutdown() {
|
|
389
|
+
eventBus.emit('shutdown:start', { timestamp: new Date().toISOString() });
|
|
390
|
+
|
|
391
|
+
// Save learning data
|
|
392
|
+
try {
|
|
393
|
+
if (this.learning?.skillBindings) {
|
|
394
|
+
await this.learning.saveSkillBindings();
|
|
395
|
+
}
|
|
396
|
+
if (this.learning?.stats) {
|
|
397
|
+
await this.learning.saveStats();
|
|
398
|
+
}
|
|
399
|
+
} catch (e) {
|
|
400
|
+
console.warn('Failed to save learning data during shutdown:', e.message);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Save honor data
|
|
404
|
+
try {
|
|
405
|
+
if (this.honor?.save) {
|
|
406
|
+
await this.honor.save();
|
|
407
|
+
}
|
|
408
|
+
} catch (e) {
|
|
409
|
+
console.warn('Failed to save honor data during shutdown:', e.message);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Clear conversation history
|
|
413
|
+
this.conversationHistory = [];
|
|
414
|
+
|
|
415
|
+
this.initialized = false;
|
|
416
|
+
eventBus.emit('shutdown:complete', { timestamp: new Date().toISOString() });
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Run system health checks (doctor)
|
|
421
|
+
*/
|
|
422
|
+
async doctor() {
|
|
423
|
+
const checks = [];
|
|
424
|
+
|
|
425
|
+
// 1. Config check
|
|
426
|
+
try {
|
|
427
|
+
await this.config.load();
|
|
428
|
+
checks.push({ name: 'Configuration', status: 'ok', message: 'Config loaded successfully' });
|
|
429
|
+
} catch (e) {
|
|
430
|
+
checks.push({ name: 'Configuration', status: 'error', message: e.message });
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// 2. Skills check
|
|
434
|
+
try {
|
|
435
|
+
const skills = this.skills.list();
|
|
436
|
+
checks.push({ name: 'Skills', status: 'ok', message: `${skills.length} skill(s) loaded` });
|
|
437
|
+
} catch (e) {
|
|
438
|
+
checks.push({ name: 'Skills', status: 'error', message: e.message });
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// 3. Learning engine check
|
|
442
|
+
try {
|
|
443
|
+
const stats = await this.learning.getStats();
|
|
444
|
+
checks.push({ name: 'Learning Engine', status: 'ok', message: `${stats.totalRecords || 0} records` });
|
|
445
|
+
} catch (e) {
|
|
446
|
+
checks.push({ name: 'Learning Engine', status: 'error', message: e.message });
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// 4. Honor engine check
|
|
450
|
+
try {
|
|
451
|
+
const honor = this.honor.getData();
|
|
452
|
+
checks.push({ name: 'Honor Engine', status: 'ok', message: `Level ${honor.level}, ${honor.points} pts` });
|
|
453
|
+
} catch (e) {
|
|
454
|
+
checks.push({ name: 'Honor Engine', status: 'error', message: e.message });
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// 5. AI providers check
|
|
458
|
+
try {
|
|
459
|
+
const aiStatus = this.ai.getStatus();
|
|
460
|
+
const providerCount = Object.keys(aiStatus.providers || {}).length;
|
|
461
|
+
const activeCount = Object.values(aiStatus.providers || {}).filter(p => p.initialized).length;
|
|
462
|
+
checks.push({ name: 'AI Providers', status: activeCount > 0 ? 'ok' : 'warning', message: `${activeCount}/${providerCount} active` });
|
|
463
|
+
} catch (e) {
|
|
464
|
+
checks.push({ name: 'AI Providers', status: 'warning', message: e.message });
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// 6. Components check
|
|
468
|
+
try {
|
|
469
|
+
const compStatus = this.components.getStatus();
|
|
470
|
+
const compCount = Object.keys(compStatus).length;
|
|
471
|
+
checks.push({ name: 'Components', status: 'ok', message: `${compCount} registered` });
|
|
472
|
+
} catch (e) {
|
|
473
|
+
checks.push({ name: 'Components', status: 'warning', message: e.message });
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// 7. Node.js version check
|
|
477
|
+
const nodeVersion = process.version;
|
|
478
|
+
const major = parseInt(nodeVersion.slice(1));
|
|
479
|
+
checks.push({
|
|
480
|
+
name: 'Node.js',
|
|
481
|
+
status: major >= 18 ? 'ok' : 'warning',
|
|
482
|
+
message: `${nodeVersion} ${major < 18 ? '(recommend >= 18)' : ''}`
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
// 8. Disk space for learning data
|
|
486
|
+
try {
|
|
487
|
+
const fs = require('fs-extra');
|
|
488
|
+
const learningPath = this.learning.expandPath(this.learning.learningPath);
|
|
489
|
+
if (await fs.pathExists(learningPath)) {
|
|
490
|
+
checks.push({ name: 'Learning Storage', status: 'ok', message: learningPath });
|
|
491
|
+
} else {
|
|
492
|
+
checks.push({ name: 'Learning Storage', status: 'warning', message: 'Directory not yet created' });
|
|
493
|
+
}
|
|
494
|
+
} catch (e) {
|
|
495
|
+
checks.push({ name: 'Learning Storage', status: 'warning', message: e.message });
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const errors = checks.filter(c => c.status === 'error').length;
|
|
499
|
+
const warnings = checks.filter(c => c.status === 'warning').length;
|
|
500
|
+
|
|
501
|
+
return {
|
|
502
|
+
healthy: errors === 0,
|
|
503
|
+
checks,
|
|
504
|
+
summary: { total: checks.length, ok: checks.filter(c => c.status === 'ok').length, warnings, errors }
|
|
505
|
+
};
|
|
506
|
+
}
|
|
323
507
|
}
|
|
324
508
|
|
|
325
509
|
module.exports = FlowMind;
|
package/core/learning-engine.js
CHANGED
|
@@ -7,6 +7,7 @@ const fs = require('fs-extra');
|
|
|
7
7
|
const path = require('path');
|
|
8
8
|
const { v4: uuidv4 } = require('uuid');
|
|
9
9
|
const eventBus = require('./event-bus');
|
|
10
|
+
const { expandPath } = require('./utils');
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Per-key write queue to prevent concurrent read-modify-write races
|
|
@@ -580,10 +581,7 @@ class LearningEngine {
|
|
|
580
581
|
* Helper methods
|
|
581
582
|
*/
|
|
582
583
|
expandPath(filePath) {
|
|
583
|
-
|
|
584
|
-
return path.join(process.env.HOME || process.env.USERPROFILE, filePath.slice(1));
|
|
585
|
-
}
|
|
586
|
-
return filePath;
|
|
584
|
+
return expandPath(filePath);
|
|
587
585
|
}
|
|
588
586
|
|
|
589
587
|
extractCondition(input) {
|
package/core/scene-matcher.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
const fs = require('fs-extra');
|
|
7
7
|
const path = require('path');
|
|
8
|
+
const { expandPath } = require('./utils');
|
|
8
9
|
|
|
9
10
|
class SceneMatcher {
|
|
10
11
|
constructor(config, learning) {
|
|
@@ -316,10 +317,7 @@ class SceneMatcher {
|
|
|
316
317
|
* Helper to expand path
|
|
317
318
|
*/
|
|
318
319
|
expandPath(filePath) {
|
|
319
|
-
|
|
320
|
-
return path.join(process.env.HOME || process.env.USERPROFILE, filePath.slice(1));
|
|
321
|
-
}
|
|
322
|
-
return filePath;
|
|
320
|
+
return expandPath(filePath);
|
|
323
321
|
}
|
|
324
322
|
}
|
|
325
323
|
|
package/core/skill-loader.js
CHANGED
|
@@ -327,6 +327,13 @@ class SkillLoader {
|
|
|
327
327
|
const skill = this.skills.get(name);
|
|
328
328
|
if (!skill) return null;
|
|
329
329
|
|
|
330
|
+
// Clear require cache to actually reload the module
|
|
331
|
+
const indexPath = require('path').join(skill.path, 'index.js');
|
|
332
|
+
const resolvedPath = require.resolve(indexPath);
|
|
333
|
+
if (require.cache[resolvedPath]) {
|
|
334
|
+
delete require.cache[resolvedPath];
|
|
335
|
+
}
|
|
336
|
+
|
|
330
337
|
return await this.loadSkill(name, skill.path);
|
|
331
338
|
}
|
|
332
339
|
|
package/core/utils.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared utility functions
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Expand ~ in file paths to home directory
|
|
10
|
+
*/
|
|
11
|
+
function expandPath(filePath) {
|
|
12
|
+
if (filePath.startsWith('~')) {
|
|
13
|
+
return path.join(process.env.HOME || process.env.USERPROFILE || os.homedir(), filePath.slice(1));
|
|
14
|
+
}
|
|
15
|
+
return filePath;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
module.exports = { expandPath };
|
package/package.json
CHANGED
package/tui/app.jsx
CHANGED
|
@@ -26,7 +26,7 @@ function App({ flowmind }) {
|
|
|
26
26
|
if (!mountedRef.current) return;
|
|
27
27
|
if (result.type === 'result') {
|
|
28
28
|
const text = typeof result.data === 'string' ? result.data : JSON.stringify(result.data, null, 2);
|
|
29
|
-
addResponse(text
|
|
29
|
+
addResponse(text);
|
|
30
30
|
} else if (result.type === 'learning') {
|
|
31
31
|
addResponse(result.message || 'Learning recorded');
|
|
32
32
|
} else if (result.type === 'error') {
|
|
@@ -1,20 +1,53 @@
|
|
|
1
1
|
const React = require('react');
|
|
2
|
-
const { Box, Text } = require('ink');
|
|
2
|
+
const { Box, Text, useInput } = require('ink');
|
|
3
3
|
const TextInput = require('ink-text-input').default || require('ink-text-input');
|
|
4
4
|
const Spinner = require('ink-spinner').default || require('ink-spinner');
|
|
5
5
|
|
|
6
6
|
function ChatPanel({ onSubmit, isProcessing, onExit }) {
|
|
7
7
|
const [input, setInput] = React.useState('');
|
|
8
8
|
const [history, setHistory] = React.useState([]);
|
|
9
|
+
const [cmdHistory, setCmdHistory] = React.useState([]);
|
|
10
|
+
const [historyIndex, setHistoryIndex] = React.useState(-1);
|
|
11
|
+
const [savedInput, setSavedInput] = React.useState('');
|
|
9
12
|
const mountedRef = React.useRef(true);
|
|
10
13
|
|
|
11
14
|
React.useEffect(() => {
|
|
12
15
|
return () => { mountedRef.current = false; };
|
|
13
16
|
}, []);
|
|
14
17
|
|
|
18
|
+
// Handle Up/Down arrow for command history
|
|
19
|
+
useInput((ch, key) => {
|
|
20
|
+
if (isProcessing) return;
|
|
21
|
+
|
|
22
|
+
if (key.upArrow && cmdHistory.length > 0) {
|
|
23
|
+
const newIndex = historyIndex === -1
|
|
24
|
+
? cmdHistory.length - 1
|
|
25
|
+
: Math.max(0, historyIndex - 1);
|
|
26
|
+
if (historyIndex === -1) setSavedInput(input);
|
|
27
|
+
setHistoryIndex(newIndex);
|
|
28
|
+
setInput(cmdHistory[newIndex]);
|
|
29
|
+
} else if (key.downArrow) {
|
|
30
|
+
if (historyIndex === -1) return;
|
|
31
|
+
const newIndex = historyIndex + 1;
|
|
32
|
+
if (newIndex >= cmdHistory.length) {
|
|
33
|
+
setHistoryIndex(-1);
|
|
34
|
+
setInput(savedInput);
|
|
35
|
+
} else {
|
|
36
|
+
setHistoryIndex(newIndex);
|
|
37
|
+
setInput(cmdHistory[newIndex]);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
15
42
|
const handleSubmit = (value) => {
|
|
16
43
|
if (!value.trim()) return;
|
|
17
44
|
setHistory(prev => [...prev, { role: 'user', text: value }]);
|
|
45
|
+
// Add to command history (deduplicate consecutive)
|
|
46
|
+
if (cmdHistory.length === 0 || cmdHistory[cmdHistory.length - 1] !== value) {
|
|
47
|
+
setCmdHistory(prev => [...prev, value]);
|
|
48
|
+
}
|
|
49
|
+
setHistoryIndex(-1);
|
|
50
|
+
setSavedInput('');
|
|
18
51
|
setInput('');
|
|
19
52
|
if (value.toLowerCase() === 'exit' || value.toLowerCase() === 'quit') {
|
|
20
53
|
if (onExit) onExit();
|