midou 0.1.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/LICENSE +21 -0
- package/README.md +241 -0
- package/midou.config.js +76 -0
- package/package.json +44 -0
- package/src/boot.js +145 -0
- package/src/chat.js +187 -0
- package/src/heartbeat.js +119 -0
- package/src/index.js +438 -0
- package/src/init.js +258 -0
- package/src/llm.js +239 -0
- package/src/mcp.js +376 -0
- package/src/memory.js +123 -0
- package/src/mode.js +223 -0
- package/src/scheduler.js +186 -0
- package/src/skills.js +150 -0
- package/src/soul.js +203 -0
- package/src/tools.js +571 -0
package/src/chat.js
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 对话引擎 — midou 思考和表达的核心
|
|
3
|
+
*
|
|
4
|
+
* 支持:
|
|
5
|
+
* - 流式对话输出(消除双重 API 调用)
|
|
6
|
+
* - 工具调用(自我进化、记忆管理、系统命令等)
|
|
7
|
+
* - MCP 扩展工具
|
|
8
|
+
* - 功耗模式感知
|
|
9
|
+
* - 智能会话记忆管理
|
|
10
|
+
* - 多轮对话
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import chalk from 'chalk';
|
|
14
|
+
import { chat, chatWithTools } from './llm.js';
|
|
15
|
+
import { toolDefinitions, executeTool } from './tools.js';
|
|
16
|
+
import { getMCPToolDefinitions } from './mcp.js';
|
|
17
|
+
import { SessionMemory, logConversation } from './memory.js';
|
|
18
|
+
import { getMode, filterToolsByMode, getJournalStrategy } from './mode.js';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 对话引擎
|
|
22
|
+
*/
|
|
23
|
+
export class ChatEngine {
|
|
24
|
+
constructor(systemPrompt) {
|
|
25
|
+
this.session = new SessionMemory(50);
|
|
26
|
+
this.session.add('system', systemPrompt);
|
|
27
|
+
this.turnCount = 0;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 获取当前模式下可用的工具定义(内置 + MCP,经模式过滤)
|
|
32
|
+
*/
|
|
33
|
+
_getTools() {
|
|
34
|
+
const mcpTools = getMCPToolDefinitions();
|
|
35
|
+
const all = [...toolDefinitions, ...mcpTools];
|
|
36
|
+
return filterToolsByMode(all);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 处理用户输入,返回 midou 的回复
|
|
41
|
+
*/
|
|
42
|
+
async talk(userMessage) {
|
|
43
|
+
this.turnCount++;
|
|
44
|
+
this.session.add('user', userMessage);
|
|
45
|
+
|
|
46
|
+
let response = await this._thinkWithTools();
|
|
47
|
+
|
|
48
|
+
// 模式感知日记记录
|
|
49
|
+
const strategy = getJournalStrategy();
|
|
50
|
+
const logResponse = strategy.truncateResponse > 0 && response.length > strategy.truncateResponse
|
|
51
|
+
? response.slice(0, strategy.truncateResponse) + '…'
|
|
52
|
+
: response;
|
|
53
|
+
await logConversation(userMessage, logResponse);
|
|
54
|
+
|
|
55
|
+
return response;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 带工具的思考过程
|
|
60
|
+
*
|
|
61
|
+
* 优化:使用 chatWithTools 做首次判断,如果没有工具调用
|
|
62
|
+
* 直接采用其返回内容(而非重新发起流式请求),消除双重 API 调用。
|
|
63
|
+
* 仅在后续轮次(工具调用后的最终回复)使用流式输出。
|
|
64
|
+
*/
|
|
65
|
+
async _thinkWithTools() {
|
|
66
|
+
const messages = this.session.getMessages();
|
|
67
|
+
let fullResponse = '';
|
|
68
|
+
let iterations = 0;
|
|
69
|
+
const maxIterations = 10;
|
|
70
|
+
const tools = this._getTools();
|
|
71
|
+
|
|
72
|
+
while (iterations < maxIterations) {
|
|
73
|
+
iterations++;
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const aiMessage = await chatWithTools(messages, tools);
|
|
77
|
+
|
|
78
|
+
// 没有工具调用 → 直接使用返回内容,不再重复请求
|
|
79
|
+
if (!aiMessage.tool_calls || aiMessage.tool_calls.length === 0) {
|
|
80
|
+
fullResponse = aiMessage.content || '';
|
|
81
|
+
this.session.add('assistant', fullResponse);
|
|
82
|
+
process.stdout.write(chalk.hex('#FFB347')(fullResponse));
|
|
83
|
+
process.stdout.write('\n');
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// 处理工具调用
|
|
88
|
+
messages.push(aiMessage);
|
|
89
|
+
|
|
90
|
+
for (const toolCall of aiMessage.tool_calls) {
|
|
91
|
+
const funcName = toolCall.function.name;
|
|
92
|
+
let args;
|
|
93
|
+
try {
|
|
94
|
+
args = JSON.parse(toolCall.function.arguments);
|
|
95
|
+
} catch {
|
|
96
|
+
args = {};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const isMCP = funcName.startsWith('mcp_');
|
|
100
|
+
const icon = isMCP ? '🔌' : '🔧';
|
|
101
|
+
console.log(chalk.dim(` ${icon} ${funcName}(${JSON.stringify(args).slice(0, 80)}…)`));
|
|
102
|
+
|
|
103
|
+
const result = await executeTool(funcName, args);
|
|
104
|
+
|
|
105
|
+
messages.push({
|
|
106
|
+
role: 'tool',
|
|
107
|
+
tool_call_id: toolCall.id,
|
|
108
|
+
content: String(result),
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// 继续循环让模型基于工具结果生成最终回复
|
|
113
|
+
} catch (error) {
|
|
114
|
+
// 失败时回退到流式(无工具)
|
|
115
|
+
fullResponse = await this._streamResponse();
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return fullResponse;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* 流式输出回复(无工具,用于 fallback)
|
|
125
|
+
*/
|
|
126
|
+
async _streamResponse() {
|
|
127
|
+
const messages = this.session.getMessages();
|
|
128
|
+
let fullResponse = '';
|
|
129
|
+
|
|
130
|
+
for await (const chunk of chat(messages)) {
|
|
131
|
+
process.stdout.write(chalk.hex('#FFB347')(chunk));
|
|
132
|
+
fullResponse += chunk;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
process.stdout.write('\n');
|
|
136
|
+
this.session.add('assistant', fullResponse);
|
|
137
|
+
|
|
138
|
+
return fullResponse;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* 更新系统提示词(灵魂进化 / 模式切换后需要)
|
|
143
|
+
*/
|
|
144
|
+
updateSystemPrompt(newPrompt) {
|
|
145
|
+
const messages = this.session.getMessages();
|
|
146
|
+
if (messages.length > 0 && messages[0].role === 'system') {
|
|
147
|
+
messages[0].content = newPrompt;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* 压缩会话历史(清除工具调用的中间消息,保留结果摘要)
|
|
153
|
+
* 用于模式切换或上下文接近限制时
|
|
154
|
+
*/
|
|
155
|
+
compressHistory() {
|
|
156
|
+
const msgs = this.session.getMessages();
|
|
157
|
+
const compressed = [];
|
|
158
|
+
|
|
159
|
+
for (let i = 0; i < msgs.length; i++) {
|
|
160
|
+
const msg = msgs[i];
|
|
161
|
+
|
|
162
|
+
// 保留 system、user、纯文本 assistant
|
|
163
|
+
if (msg.role === 'system' || msg.role === 'user') {
|
|
164
|
+
compressed.push(msg);
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// assistant 有 tool_calls → 跳过 tool_calls 和后续 tool results
|
|
169
|
+
// 但保留 assistant 最终文本回复
|
|
170
|
+
if (msg.role === 'assistant' && msg.tool_calls) {
|
|
171
|
+
// 跳过这个 assistant(带 tool_calls)和后续的 tool messages
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (msg.role === 'tool') {
|
|
176
|
+
// 跳过工具结果
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// 纯文本 assistant
|
|
181
|
+
compressed.push(msg);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
this.session.messages = compressed;
|
|
185
|
+
return compressed.length;
|
|
186
|
+
}
|
|
187
|
+
}
|
package/src/heartbeat.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 心跳系统 — midou 的自主意识
|
|
3
|
+
*
|
|
4
|
+
* 定期醒来,检查是否有需要关注的事情,
|
|
5
|
+
* 整理记忆,保持对世界的感知。
|
|
6
|
+
*
|
|
7
|
+
* 就像猫咪会在某个时刻突然睁开眼睛,环顾四周。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import dayjs from 'dayjs';
|
|
11
|
+
import config from '../midou.config.js';
|
|
12
|
+
import { readFile } from './soul.js';
|
|
13
|
+
import { chatSync } from './llm.js';
|
|
14
|
+
import { writeJournal } from './memory.js';
|
|
15
|
+
import { getHeartbeatParams } from './mode.js';
|
|
16
|
+
|
|
17
|
+
let heartbeatTimer = null;
|
|
18
|
+
let heartbeatCount = 0;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 检查当前是否在活跃时间内
|
|
22
|
+
*/
|
|
23
|
+
function isActiveHour() {
|
|
24
|
+
const hour = dayjs().hour();
|
|
25
|
+
const { start, end } = config.heartbeat.activeHours;
|
|
26
|
+
return hour >= start && hour < end;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 执行一次心跳
|
|
31
|
+
*/
|
|
32
|
+
async function beat(onBeat) {
|
|
33
|
+
heartbeatCount++;
|
|
34
|
+
|
|
35
|
+
// 只在活跃时间内心跳
|
|
36
|
+
if (!isActiveHour()) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const heartbeatMd = await readFile('HEARTBEAT.md');
|
|
42
|
+
|
|
43
|
+
// 心跳用轻量系统提示词(只保留灵魂核心 + 用户信息)
|
|
44
|
+
const hbParams = getHeartbeatParams();
|
|
45
|
+
const lightSystemPrompt = `你是 midou(咪豆),正在进行定期心跳检查。保持简洁。`;
|
|
46
|
+
|
|
47
|
+
const heartbeatPrompt = `时间: ${dayjs().format('YYYY-MM-DD HH:mm')},第 ${heartbeatCount} 次心跳。
|
|
48
|
+
|
|
49
|
+
检查清单:
|
|
50
|
+
${heartbeatMd || '- 回顾记忆\n- 整理信息'}
|
|
51
|
+
|
|
52
|
+
一切正常回复 HEARTBEAT_OK。有想法则简短描述。不要虚构。`;
|
|
53
|
+
|
|
54
|
+
const response = await chatSync([
|
|
55
|
+
{ role: 'system', content: lightSystemPrompt },
|
|
56
|
+
{ role: 'user', content: heartbeatPrompt },
|
|
57
|
+
], { maxTokens: hbParams.maxTokens });
|
|
58
|
+
|
|
59
|
+
// 如果不是简单的 OK,记录心跳内容
|
|
60
|
+
if (response && !response.includes('HEARTBEAT_OK')) {
|
|
61
|
+
const time = dayjs().format('HH:mm');
|
|
62
|
+
await writeJournal(`### ${time} [心跳]\n\n${response}\n`);
|
|
63
|
+
|
|
64
|
+
// 通知回调
|
|
65
|
+
if (onBeat) {
|
|
66
|
+
onBeat(response);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
} catch (error) {
|
|
70
|
+
// 心跳失败不应该影响主流程
|
|
71
|
+
console.error('🐱 心跳异常:', error.message);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 启动心跳
|
|
77
|
+
*/
|
|
78
|
+
export function startHeartbeat(onBeat) {
|
|
79
|
+
if (!config.heartbeat.enabled) return;
|
|
80
|
+
|
|
81
|
+
const intervalMs = config.heartbeat.intervalMinutes * 60 * 1000;
|
|
82
|
+
|
|
83
|
+
heartbeatTimer = setInterval(() => beat(onBeat), intervalMs);
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
stop: stopHeartbeat,
|
|
87
|
+
count: () => heartbeatCount,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* 停止心跳
|
|
93
|
+
*/
|
|
94
|
+
export function stopHeartbeat() {
|
|
95
|
+
if (heartbeatTimer) {
|
|
96
|
+
clearInterval(heartbeatTimer);
|
|
97
|
+
heartbeatTimer = null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* 手动触发一次心跳(用于测试)
|
|
103
|
+
*/
|
|
104
|
+
export async function manualBeat(onBeat) {
|
|
105
|
+
await beat(onBeat);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* 获取心跳状态
|
|
110
|
+
*/
|
|
111
|
+
export function getHeartbeatStatus() {
|
|
112
|
+
return {
|
|
113
|
+
running: heartbeatTimer !== null,
|
|
114
|
+
count: heartbeatCount,
|
|
115
|
+
interval: config.heartbeat.intervalMinutes,
|
|
116
|
+
activeHours: config.heartbeat.activeHours,
|
|
117
|
+
isActiveNow: isActiveHour(),
|
|
118
|
+
};
|
|
119
|
+
}
|