flowmind 1.2.3 → 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.
Files changed (36) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +1 -1
  3. package/bin/flowmind.js +69 -16
  4. package/core/ai/base-model.js +47 -28
  5. package/core/ai/model-manager.js +100 -90
  6. package/core/ai/providers/anthropic.js +21 -14
  7. package/core/ai/providers/deepseek.js +12 -2
  8. package/core/ai/providers/ernie.js +2 -2
  9. package/core/ai/providers/glm.js +12 -2
  10. package/core/ai/providers/mimo.js +12 -2
  11. package/core/ai/providers/ollama.js +5 -4
  12. package/core/ai/providers/openai.js +8 -12
  13. package/core/ai/providers/qwen.js +12 -2
  14. package/core/config-manager.js +1 -0
  15. package/core/index.js +183 -6
  16. package/core/learning-engine.js +122 -30
  17. package/mcp/server.js +2 -1
  18. package/package.json +1 -1
  19. package/skills/api-sync/index.js +130 -0
  20. package/skills/archive-change/index.js +104 -0
  21. package/skills/auto-flow/index.js +124 -0
  22. package/skills/code-review/index.js +79 -0
  23. package/skills/code-review-audit/index.js +77 -0
  24. package/skills/data-logic-validation/index.js +108 -0
  25. package/skills/data-validation/index.js +72 -0
  26. package/skills/git-review/index.js +73 -0
  27. package/skills/learning-engine/index.js +50 -0
  28. package/skills/learning-feedback/index.js +83 -0
  29. package/skills/log-audit/index.js +88 -0
  30. package/skills/project-review/index.js +105 -0
  31. package/skills/requirement-analyst/index.js +88 -0
  32. package/skills/resource-bind/index.js +60 -0
  33. package/skills/sls-log-audit/index.js +120 -0
  34. package/skills/yapi-sync-interface/index.js +101 -0
  35. package/skills/yuque-sync-design/index.js +133 -0
  36. package/tui/app.jsx +1 -1
@@ -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
  }
@@ -5,6 +5,7 @@
5
5
 
6
6
  const fs = require('fs-extra');
7
7
  const path = require('path');
8
+ const os = require('os');
8
9
 
9
10
  class ConfigManager {
10
11
  constructor(configPath = null) {
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;
@@ -8,11 +8,31 @@ const path = require('path');
8
8
  const { v4: uuidv4 } = require('uuid');
9
9
  const eventBus = require('./event-bus');
10
10
 
11
+ /**
12
+ * Per-key write queue to prevent concurrent read-modify-write races
13
+ */
14
+ class WriteQueue {
15
+ constructor() {
16
+ this.queues = new Map();
17
+ }
18
+
19
+ async run(key, fn) {
20
+ if (!this.queues.has(key)) {
21
+ this.queues.set(key, Promise.resolve());
22
+ }
23
+ const prev = this.queues.get(key);
24
+ const next = prev.then(fn, fn);
25
+ this.queues.set(key, next);
26
+ return next;
27
+ }
28
+ }
29
+
11
30
  class LearningEngine {
12
31
  constructor(config, honorEngine = null) {
13
32
  this.config = config;
14
33
  this.honorEngine = honorEngine;
15
34
  this.learningPath = config.get('learning.storagePath', '~/.flowmind/learning');
35
+ this.writeQueue = new WriteQueue();
16
36
  this.records = {};
17
37
  this.skillBindings = {};
18
38
  this.stats = {};
@@ -144,7 +164,7 @@ class LearningEngine {
144
164
  */
145
165
  async recordCorrection(correction, context) {
146
166
  const record = {
147
- id: `learn-${Date.now()}-${uuidv4().substr(0, 8)}`,
167
+ id: `learn-${Date.now()}-${uuidv4().slice(0, 8)}`,
148
168
  timestamp: new Date().toISOString(),
149
169
  type: correction.type,
150
170
  severity: correction.severity,
@@ -191,7 +211,7 @@ class LearningEngine {
191
211
  */
192
212
  async recordSceneMapping(sceneMapping, context) {
193
213
  const record = {
194
- id: `scene-${Date.now()}-${uuidv4().substr(0, 8)}`,
214
+ id: `scene-${Date.now()}-${uuidv4().slice(0, 8)}`,
195
215
  timestamp: new Date().toISOString(),
196
216
  type: 'scene_mapping',
197
217
  input: sceneMapping.input,
@@ -232,7 +252,7 @@ class LearningEngine {
232
252
  */
233
253
  async recordPreference(preference, context) {
234
254
  const record = {
235
- id: `pref-${Date.now()}-${uuidv4().substr(0, 8)}`,
255
+ id: `pref-${Date.now()}-${uuidv4().slice(0, 8)}`,
236
256
  timestamp: new Date().toISOString(),
237
257
  type: 'preference',
238
258
  preferenceType: preference.preferenceType,
@@ -309,16 +329,17 @@ class LearningEngine {
309
329
  */
310
330
  async saveSceneMapping(record) {
311
331
  const scenesPath = path.join(this.expandPath(this.learningPath), 'scenes.json');
332
+ await this.writeQueue.run('scenes.json', async () => {
333
+ let scenes = { version: '1.0', mappings: [] };
334
+ if (await fs.pathExists(scenesPath)) {
335
+ scenes = await fs.readJson(scenesPath);
336
+ }
312
337
 
313
- let scenes = { version: '1.0', mappings: [] };
314
- if (await fs.pathExists(scenesPath)) {
315
- scenes = await fs.readJson(scenesPath);
316
- }
317
-
318
- scenes.mappings.push(record);
319
- scenes.lastUpdated = new Date().toISOString();
338
+ scenes.mappings.push(record);
339
+ scenes.lastUpdated = new Date().toISOString();
320
340
 
321
- await fs.writeJson(scenesPath, scenes, { spaces: 2 });
341
+ await fs.writeJson(scenesPath, scenes, { spaces: 2 });
342
+ });
322
343
  }
323
344
 
324
345
  /**
@@ -331,17 +352,19 @@ class LearningEngine {
331
352
  record.skill,
332
353
  'preferences.json'
333
354
  );
355
+ const queueKey = `prefs:${record.skill}`;
356
+ await this.writeQueue.run(queueKey, async () => {
357
+ let prefs = {};
358
+ if (await fs.pathExists(prefsPath)) {
359
+ prefs = await fs.readJson(prefsPath);
360
+ }
334
361
 
335
- let prefs = {};
336
- if (await fs.pathExists(prefsPath)) {
337
- prefs = await fs.readJson(prefsPath);
338
- }
339
-
340
- prefs[record.preferenceType] = record.value;
341
- prefs.lastUpdated = new Date().toISOString();
362
+ prefs[record.preferenceType] = record.value;
363
+ prefs.lastUpdated = new Date().toISOString();
342
364
 
343
- await fs.ensureDir(path.dirname(prefsPath));
344
- await fs.writeJson(prefsPath, prefs, { spaces: 2 });
365
+ await fs.ensureDir(path.dirname(prefsPath));
366
+ await fs.writeJson(prefsPath, prefs, { spaces: 2 });
367
+ });
345
368
  }
346
369
 
347
370
  /**
@@ -387,9 +410,11 @@ class LearningEngine {
387
410
  * Save skill bindings
388
411
  */
389
412
  async saveSkillBindings() {
390
- const bindingsPath = path.join(this.expandPath(this.learningPath), 'skill-bindings.json');
391
- this.skillBindings.lastUpdated = new Date().toISOString();
392
- await fs.writeJson(bindingsPath, this.skillBindings, { spaces: 2 });
413
+ await this.writeQueue.run('bindings', async () => {
414
+ const bindingsPath = path.join(this.expandPath(this.learningPath), 'skill-bindings.json');
415
+ this.skillBindings.lastUpdated = new Date().toISOString();
416
+ await fs.writeJson(bindingsPath, this.skillBindings, { spaces: 2 });
417
+ });
393
418
  }
394
419
 
395
420
  /**
@@ -414,13 +439,15 @@ class LearningEngine {
414
439
  * Update stats
415
440
  */
416
441
  async updateStats(type, skill) {
417
- this.stats.totalRecords++;
418
- this.stats.byType[type] = (this.stats.byType[type] || 0) + 1;
419
- this.stats.bySkill[skill] = (this.stats.bySkill[skill] || 0) + 1;
420
- this.stats.lastLearning = new Date().toISOString();
421
-
422
- const statsPath = path.join(this.expandPath(this.learningPath), 'stats.json');
423
- await fs.writeJson(statsPath, this.stats, { spaces: 2 });
442
+ await this.writeQueue.run('stats', async () => {
443
+ this.stats.totalRecords++;
444
+ this.stats.byType[type] = (this.stats.byType[type] || 0) + 1;
445
+ this.stats.bySkill[skill] = (this.stats.bySkill[skill] || 0) + 1;
446
+ this.stats.lastLearning = new Date().toISOString();
447
+
448
+ const statsPath = path.join(this.expandPath(this.learningPath), 'stats.json');
449
+ await fs.writeJson(statsPath, this.stats, { spaces: 2 });
450
+ });
424
451
 
425
452
  // Award honor points for learning
426
453
  if (this.honorEngine) {
@@ -484,6 +511,71 @@ class LearningEngine {
484
511
  return { success: true, imported: data.stats.totalRecords };
485
512
  }
486
513
 
514
+ /**
515
+ * Reset all learnings for a specific skill
516
+ */
517
+ async resetSkill(skillName) {
518
+ const basePath = this.expandPath(this.learningPath);
519
+
520
+ // Delete records directory for this skill
521
+ const recordsDir = path.join(basePath, 'records', skillName);
522
+ if (await fs.pathExists(recordsDir)) {
523
+ await fs.remove(recordsDir);
524
+ }
525
+
526
+ // Remove from skill bindings
527
+ if (this.skillBindings.bindings && this.skillBindings.bindings[skillName]) {
528
+ delete this.skillBindings.bindings[skillName];
529
+ await this.saveSkillBindings();
530
+ }
531
+
532
+ // Update stats
533
+ const count = (this.records[skillName] || []).length;
534
+ if (count > 0 && this.stats.totalRecords) {
535
+ this.stats.totalRecords = Math.max(0, this.stats.totalRecords - count);
536
+ }
537
+ if (this.stats.bySkill && this.stats.bySkill[skillName]) {
538
+ delete this.stats.bySkill[skillName];
539
+ }
540
+ await this.saveStats();
541
+ delete this.records[skillName];
542
+
543
+ return count;
544
+ }
545
+
546
+ /**
547
+ * Delete a specific learning record by ID
548
+ */
549
+ async deleteRecord(recordId) {
550
+ const basePath = path.join(this.expandPath(this.learningPath), 'records');
551
+ if (!(await fs.pathExists(basePath))) return false;
552
+
553
+ const skillDirs = await fs.readdir(basePath);
554
+ for (const skill of skillDirs) {
555
+ const recordPath = path.join(basePath, skill, `${recordId}.json`);
556
+ if (await fs.pathExists(recordPath)) {
557
+ await fs.remove(recordPath);
558
+ // Remove from memory cache
559
+ if (this.records[skill]) {
560
+ this.records[skill] = this.records[skill].filter(r => r.id !== recordId);
561
+ }
562
+ // Update stats
563
+ if (this.stats.totalRecords) this.stats.totalRecords--;
564
+ await this.saveStats();
565
+ return true;
566
+ }
567
+ }
568
+ return false;
569
+ }
570
+
571
+ /**
572
+ * Save stats to disk
573
+ */
574
+ async saveStats() {
575
+ const statsPath = path.join(this.expandPath(this.learningPath), 'stats.json');
576
+ await fs.writeJson(statsPath, this.stats, { spaces: 2 });
577
+ }
578
+
487
579
  /**
488
580
  * Helper methods
489
581
  */
package/mcp/server.js CHANGED
@@ -7,6 +7,7 @@
7
7
 
8
8
  const FlowMind = require('../core');
9
9
  const eventBus = require('../core/event-bus');
10
+ const { version } = require('../package.json');
10
11
 
11
12
  // MCP Server 实现
12
13
  class FlowMindMCPServer {
@@ -276,7 +277,7 @@ async function main() {
276
277
  },
277
278
  serverInfo: {
278
279
  name: 'flowmind',
279
- version: '1.0.1'
280
+ version: version
280
281
  }
281
282
  }
282
283
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flowmind",
3
- "version": "1.2.3",
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": {