flowmind 1.3.0 → 1.4.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 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: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
10
10
  [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](CONTRIBUTING.md)
11
- [![Version](https://img.shields.io/badge/version-1.1.0-blue)](CHANGELOG.md)
11
+ [![Version](https://img.shields.io/badge/version-1.3.0-blue)](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')
@@ -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;
@@ -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
- try {
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
- } catch (error) {
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
- const provider = this.getDefaultProvider();
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
- } catch (error) {
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
- const provider = this.getDefaultProvider();
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
- } catch (error) {
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
- const provider = this.getDefaultProvider();
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
- } catch (error) {
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
- try {
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
- const response = await provider.chat(messages, {
267
- responseFormat: { type: 'json_object' }
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
- return JSON.parse(response);
271
- } catch (error) {
272
- console.warn(`AI learning feedback analysis failed: ${error.message}`);
273
- return this._fallback('learningFeedback', input, context);
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, input, context) {
282
- if (!this.fallbackToRules) {
283
- return null;
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-3-sonnet-20240229';
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 fetch(`${this.baseUrl}/v1/messages`, {
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 没有 models 端点,直接尝试调用
78
- return true;
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 fetch(`${this.baseUrl}/chat/completions`, {
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
- return true;
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 fetch(
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 fetch(`${this.baseUrl}${endpoint}?access_token=${this.accessToken}`, {
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'
@@ -31,7 +31,7 @@ class GLMProvider extends BaseModel {
31
31
  await this.init();
32
32
  }
33
33
 
34
- const response = await fetch(`${this.baseUrl}/chat/completions`, {
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
- return true;
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 fetch(`${this.baseUrl}/chat/completions`, {
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
- return true;
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 fetch(`${this.baseUrl}/api/generate`, {
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 fetch(`${this.baseUrl}/api/generate`, {
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 fetch(`${this.baseUrl}/api/tags`, {
94
+ const response = await this.fetchWithRetry(`${this.baseUrl}/api/tags`, {
95
95
  method: 'GET',
96
- signal: AbortSignal.timeout(5000) // 5秒超时
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-4';
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 fetch(`${this.baseUrl}/chat/completions`, {
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 fetch(`${this.baseUrl}/models`, {
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 fetch(`${this.baseUrl}/chat/completions`, {
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
- return true;
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/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
  /**
@@ -49,6 +51,19 @@ class FlowMind {
49
51
  return this;
50
52
  }
51
53
 
54
+ /**
55
+ * Process a user request with streaming support
56
+ * Yields progress updates as an async generator
57
+ */
58
+ async *processStream(input, context = {}) {
59
+ yield { type: 'start', input, timestamp: new Date().toISOString() };
60
+
61
+ const result = await this.process(input, context);
62
+
63
+ yield { type: 'progress', message: `Skill: ${result.metadata?.skill || 'unknown'}`, timestamp: new Date().toISOString() };
64
+ yield { type: 'result', data: result, timestamp: new Date().toISOString() };
65
+ }
66
+
52
67
  /**
53
68
  * Process a user request
54
69
  */
@@ -57,20 +72,31 @@ class FlowMind {
57
72
  await this.init();
58
73
  }
59
74
 
75
+ if (!input || typeof input !== 'string') {
76
+ return this.formatError('Invalid input: expected a non-empty string', input);
77
+ }
78
+
60
79
  const startTime = Date.now();
61
80
 
81
+ // Build context with conversation history
82
+ const enhancedContext = {
83
+ ...context,
84
+ conversationHistory: this.conversationHistory.slice(-10),
85
+ sessionId: context.sessionId || 'default'
86
+ };
87
+
62
88
  eventBus.emit('process:start', { input, timestamp: new Date().toISOString() });
63
89
 
64
90
  try {
65
91
  // 1. AI Intent Understanding (if available)
66
- const intent = await this.ai.understandIntent(input, context);
92
+ const intent = await this.ai.understandIntent(input, enhancedContext);
67
93
 
68
94
  // 2. Check for learning patterns (corrections, feedback)
69
95
  // Use AI to analyze learning feedback if available
70
- const aiLearningResult = await this.ai.analyzeLearningFeedback(input, context);
96
+ const aiLearningResult = await this.ai.analyzeLearningFeedback(input, enhancedContext);
71
97
  const learningResult = aiLearningResult?.isLearning
72
98
  ? aiLearningResult
73
- : await this.learning.detectLearning(input, context);
99
+ : await this.learning.detectLearning(input, enhancedContext);
74
100
  if (learningResult) {
75
101
  return this.formatLearningResponse(learningResult);
76
102
  }
@@ -106,12 +132,12 @@ class FlowMind {
106
132
  const extractedParams = await this.ai.extractParameters(input, skill.name);
107
133
 
108
134
  // 6. Execute with learning applied
109
- const enhancedContext = {
110
- ...context,
135
+ const executeContext = {
136
+ ...enhancedContext,
111
137
  ...extractedParams,
112
138
  intent: intent
113
139
  };
114
- const result = await this.executeWithLearning(skill, input, enhancedContext);
140
+ const result = await this.executeWithLearning(skill, input, executeContext);
115
141
 
116
142
  // 7. Generate AI summary (if available)
117
143
  const summary = await this.ai.summarizeResult(result, {
@@ -128,6 +154,17 @@ class FlowMind {
128
154
  aiEnhanced: !!summary
129
155
  });
130
156
 
157
+ // Save to conversation history
158
+ this.conversationHistory.push({
159
+ input,
160
+ output: formatted,
161
+ skill: skill.name,
162
+ timestamp: new Date().toISOString()
163
+ });
164
+ if (this.conversationHistory.length > this.maxHistoryLength) {
165
+ this.conversationHistory.shift();
166
+ }
167
+
131
168
  eventBus.emit('process:complete', {
132
169
  input,
133
170
  skill: skill.name,
@@ -320,6 +357,146 @@ class FlowMind {
320
357
  async importLearnings(data) {
321
358
  return this.learning.import(data);
322
359
  }
360
+
361
+ /**
362
+ * Get conversation history
363
+ */
364
+ getConversationHistory(sessionId) {
365
+ if (sessionId) {
366
+ return this.conversationHistory.filter(h => h.sessionId === sessionId);
367
+ }
368
+ return this.conversationHistory;
369
+ }
370
+
371
+ /**
372
+ * Clear conversation history
373
+ */
374
+ clearHistory() {
375
+ this.conversationHistory = [];
376
+ }
377
+
378
+ /**
379
+ * Graceful shutdown - flush pending data and clean up
380
+ */
381
+ async shutdown() {
382
+ eventBus.emit('shutdown:start', { timestamp: new Date().toISOString() });
383
+
384
+ // Save learning data
385
+ try {
386
+ if (this.learning?.skillBindings) {
387
+ await this.learning.saveSkillBindings();
388
+ }
389
+ if (this.learning?.stats) {
390
+ await this.learning.saveStats();
391
+ }
392
+ } catch (e) {
393
+ console.warn('Failed to save learning data during shutdown:', e.message);
394
+ }
395
+
396
+ // Save honor data
397
+ try {
398
+ if (this.honor?.save) {
399
+ await this.honor.save();
400
+ }
401
+ } catch (e) {
402
+ console.warn('Failed to save honor data during shutdown:', e.message);
403
+ }
404
+
405
+ // Clear conversation history
406
+ this.conversationHistory = [];
407
+
408
+ this.initialized = false;
409
+ eventBus.emit('shutdown:complete', { timestamp: new Date().toISOString() });
410
+ }
411
+
412
+ /**
413
+ * Run system health checks (doctor)
414
+ */
415
+ async doctor() {
416
+ const checks = [];
417
+
418
+ // 1. Config check
419
+ try {
420
+ await this.config.load();
421
+ checks.push({ name: 'Configuration', status: 'ok', message: 'Config loaded successfully' });
422
+ } catch (e) {
423
+ checks.push({ name: 'Configuration', status: 'error', message: e.message });
424
+ }
425
+
426
+ // 2. Skills check
427
+ try {
428
+ const skills = this.skills.list();
429
+ checks.push({ name: 'Skills', status: 'ok', message: `${skills.length} skill(s) loaded` });
430
+ } catch (e) {
431
+ checks.push({ name: 'Skills', status: 'error', message: e.message });
432
+ }
433
+
434
+ // 3. Learning engine check
435
+ try {
436
+ const stats = await this.learning.getStats();
437
+ checks.push({ name: 'Learning Engine', status: 'ok', message: `${stats.totalRecords || 0} records` });
438
+ } catch (e) {
439
+ checks.push({ name: 'Learning Engine', status: 'error', message: e.message });
440
+ }
441
+
442
+ // 4. Honor engine check
443
+ try {
444
+ const honor = this.honor.getData();
445
+ checks.push({ name: 'Honor Engine', status: 'ok', message: `Level ${honor.level}, ${honor.points} pts` });
446
+ } catch (e) {
447
+ checks.push({ name: 'Honor Engine', status: 'error', message: e.message });
448
+ }
449
+
450
+ // 5. AI providers check
451
+ try {
452
+ const aiStatus = this.ai.getStatus();
453
+ const providerCount = Object.keys(aiStatus.providers || {}).length;
454
+ const activeCount = Object.values(aiStatus.providers || {}).filter(p => p.initialized).length;
455
+ checks.push({ name: 'AI Providers', status: activeCount > 0 ? 'ok' : 'warning', message: `${activeCount}/${providerCount} active` });
456
+ } catch (e) {
457
+ checks.push({ name: 'AI Providers', status: 'warning', message: e.message });
458
+ }
459
+
460
+ // 6. Components check
461
+ try {
462
+ const compStatus = this.components.getStatus();
463
+ const compCount = Object.keys(compStatus).length;
464
+ checks.push({ name: 'Components', status: 'ok', message: `${compCount} registered` });
465
+ } catch (e) {
466
+ checks.push({ name: 'Components', status: 'warning', message: e.message });
467
+ }
468
+
469
+ // 7. Node.js version check
470
+ const nodeVersion = process.version;
471
+ const major = parseInt(nodeVersion.slice(1));
472
+ checks.push({
473
+ name: 'Node.js',
474
+ status: major >= 18 ? 'ok' : 'warning',
475
+ message: `${nodeVersion} ${major < 18 ? '(recommend >= 18)' : ''}`
476
+ });
477
+
478
+ // 8. Disk space for learning data
479
+ try {
480
+ const fs = require('fs-extra');
481
+ const learningPath = this.learning.expandPath(this.learning.learningPath);
482
+ if (await fs.pathExists(learningPath)) {
483
+ checks.push({ name: 'Learning Storage', status: 'ok', message: learningPath });
484
+ } else {
485
+ checks.push({ name: 'Learning Storage', status: 'warning', message: 'Directory not yet created' });
486
+ }
487
+ } catch (e) {
488
+ checks.push({ name: 'Learning Storage', status: 'warning', message: e.message });
489
+ }
490
+
491
+ const errors = checks.filter(c => c.status === 'error').length;
492
+ const warnings = checks.filter(c => c.status === 'warning').length;
493
+
494
+ return {
495
+ healthy: errors === 0,
496
+ checks,
497
+ summary: { total: checks.length, ok: checks.filter(c => c.status === 'ok').length, warnings, errors }
498
+ };
499
+ }
323
500
  }
324
501
 
325
502
  module.exports = FlowMind;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flowmind",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "The AI Agent That Learns How You Work - Stop repeating yourself, FlowMind learns your workflows and applies them automatically.",
5
5
  "main": "core/index.js",
6
6
  "bin": {
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.substring(0, 200) + (text.length > 200 ? '...' : ''));
29
+ addResponse(text);
30
30
  } else if (result.type === 'learning') {
31
31
  addResponse(result.message || 'Learning recorded');
32
32
  } else if (result.type === 'error') {