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.
- package/CHANGELOG.md +46 -0
- package/README.md +1 -1
- package/bin/flowmind.js +69 -16
- 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/config-manager.js +1 -0
- package/core/index.js +183 -6
- package/core/learning-engine.js +122 -30
- package/mcp/server.js +2 -1
- package/package.json +1 -1
- package/skills/api-sync/index.js +130 -0
- package/skills/archive-change/index.js +104 -0
- package/skills/auto-flow/index.js +124 -0
- package/skills/code-review/index.js +79 -0
- package/skills/code-review-audit/index.js +77 -0
- package/skills/data-logic-validation/index.js +108 -0
- package/skills/data-validation/index.js +72 -0
- package/skills/git-review/index.js +73 -0
- package/skills/learning-engine/index.js +50 -0
- package/skills/learning-feedback/index.js +83 -0
- package/skills/log-audit/index.js +88 -0
- package/skills/project-review/index.js +105 -0
- package/skills/requirement-analyst/index.js +88 -0
- package/skills/resource-bind/index.js +60 -0
- package/skills/sls-log-audit/index.js +120 -0
- package/skills/yapi-sync-interface/index.js +101 -0
- package/skills/yuque-sync-design/index.js +133 -0
- package/tui/app.jsx +1 -1
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/config-manager.js
CHANGED
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,
|
|
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,
|
|
96
|
+
const aiLearningResult = await this.ai.analyzeLearningFeedback(input, enhancedContext);
|
|
71
97
|
const learningResult = aiLearningResult?.isLearning
|
|
72
98
|
? aiLearningResult
|
|
73
|
-
: await this.learning.detectLearning(input,
|
|
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
|
|
110
|
-
...
|
|
135
|
+
const executeContext = {
|
|
136
|
+
...enhancedContext,
|
|
111
137
|
...extractedParams,
|
|
112
138
|
intent: intent
|
|
113
139
|
};
|
|
114
|
-
const result = await this.executeWithLearning(skill, input,
|
|
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/core/learning-engine.js
CHANGED
|
@@ -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().
|
|
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().
|
|
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().
|
|
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
|
-
|
|
314
|
-
|
|
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
|
-
|
|
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
|
-
|
|
336
|
-
|
|
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
|
-
|
|
344
|
-
|
|
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
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
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:
|
|
280
|
+
version: version
|
|
280
281
|
}
|
|
281
282
|
}
|
|
282
283
|
};
|
package/package.json
CHANGED