midou 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -14,7 +14,8 @@
14
14
  - **🧠 记忆** — 每日日记 + 长期记忆,跨会话延续自我
15
15
  - **💓 心跳** — 定期自主思考,像猫咪偶尔睁开眼睛环顾四周
16
16
  - **🌱 自我进化** — 可以修改自己的灵魂和代码,实现真正的成长
17
- - **📜 觉醒仪式**第一次启动时的自我认知过程
17
+ - **� 全流式对话**所有响应实时流式输出,思考过程可见,工具调用实时展示
18
+ - **�📜 觉醒仪式** — 第一次启动时的自我认知过程
18
19
  - **🏠 灵肉分离** — 代码通过 npm 安装,灵魂和记忆存在 `~/.midou/`,同步即可跨机器唤醒
19
20
  - **⏰ 定时提醒** — 设置一次性或重复提醒,让 midou 准时叫你
20
21
  - **🧩 技能系统** — 自动发现 `~/.claude/skills/` 等目录下的技能,按需加载
@@ -109,6 +110,7 @@ midou 内置双 SDK 引擎(Anthropic + OpenAI),通过 `MIDOU_PROVIDER` 切
109
110
  | 命令 | 说明 |
110
111
  |------|------|
111
112
  | `/help` | 显示帮助 |
113
+ | `/think` | 查看上一次的思考过程 |
112
114
  | `/status` | 查看 midou 状态(模型、心跳、MCP、模式) |
113
115
  | `/soul` | 查看当前灵魂 |
114
116
  | `/memory` | 查看长期记忆 |
@@ -211,6 +213,15 @@ midou 拥有以下能力,可在对话中自主使用:
211
213
  | 系统 | `run_command` `read_system_file` `write_system_file` `list_system_dir` | 系统级操作(有安全检查) |
212
214
  | 代码 | `get_code_structure` `search_code` | 分析和搜索源代码 |
213
215
 
216
+ ## 流式对话与思考展示
217
+
218
+ midou 的所有响应都是实时流式输出的,包括工具调用场景。对话中你会看到:
219
+
220
+ - **💭 思考块** — 模型的思考过程实时展示,用薑衣草色边框包裹
221
+ - **⚙ 工具调用** — 水蓝色显示工具名和参数,执行状态实时反馈
222
+ - **🔌 MCP 工具** — 外部服务器工具用插头图标区分
223
+ - **/think** — 随时回看上一次的思考内容
224
+
214
225
  ## 自我进化
215
226
 
216
227
  midou 可以:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "midou",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "midou(咪豆)— 一只拥有灵魂的 AI 伙伴,以我心爱的公狸花猫命名",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
package/src/boot.js CHANGED
@@ -31,8 +31,7 @@ export async function wakeUp() {
31
31
  const now = dayjs().format('YYYY-MM-DD HH:mm');
32
32
 
33
33
  console.log('');
34
- console.log(chalk.hex('#FFB347')(' 🐱 '));
35
- console.log(chalk.hex('#FFB347')(' midou 正在醒来...'));
34
+ console.log(chalk.hex('#FFB347')(' 🐱 midou 正在醒来…'));
36
35
  console.log(chalk.dim(` ${now}`));
37
36
  console.log('');
38
37
 
@@ -69,20 +68,20 @@ export async function wakeUp() {
69
68
  skills = await discoverSkills();
70
69
  skillsPrompt = await buildSkillsPrompt();
71
70
  if (skills.length > 0) {
72
- console.log(chalk.hex('#98FB98')(` 🧩 发现 ${skills.length} 个技能`));
71
+ console.log(chalk.dim(' ▸ ') + chalk.hex('#98FB98')(`发现 ${skills.length} 个技能`));
73
72
  }
74
73
  }
75
74
 
76
75
  // ── 连接 MCP 服务器(模式允许时)──
77
76
  let mcpPrompt = '';
78
77
  if (strategy.includeMCP && await hasMCPConfig()) {
79
- console.log(chalk.dim(' 🔌 正在连接 MCP 服务器...'));
78
+ console.log(chalk.dim(' 正在连接 MCP 服务器…'));
80
79
  const results = await connectMCPServers();
81
80
  for (const r of results) {
82
81
  if (r.status === 'connected') {
83
- console.log(chalk.hex('#98FB98')(` 🔌 ${r.name}: 已连接 (${r.tools.length} 个工具)`));
82
+ console.log(chalk.dim(' ') + chalk.green('●') + chalk.dim(` ${r.name} (${r.tools.length} 工具)`));
84
83
  } else {
85
- console.log(chalk.yellow(` 🔌 ${r.name}: 连接失败 - ${r.error}`));
84
+ console.log(chalk.dim(' ') + chalk.red('●') + chalk.dim(` ${r.name}`) + chalk.yellow(' 失败'));
86
85
  }
87
86
  }
88
87
  mcpPrompt = buildMCPPrompt();
@@ -102,18 +101,20 @@ export async function wakeUp() {
102
101
  await writeJournal(`### ${dayjs().format('HH:mm')} [醒来]\n\nmidou 在 ${now} 醒来了。${isFirstBoot ? '这是第一次觉醒。' : ''}${skills.length > 0 ? ` 发现 ${skills.length} 个技能。` : ''}\n`);
103
102
 
104
103
  const providerLabel = getProvider() === 'anthropic' ? 'Anthropic SDK' : 'OpenAI SDK';
105
- console.log(chalk.dim(` 大脑: ${config.llm.model} via ${providerLabel}`));
106
- console.log(chalk.dim(` 模式: ${mode.label}`));
107
- console.log(chalk.dim(` 灵魂之家: ${MIDOU_HOME}`));
104
+ const W = Math.min(process.stdout.columns || 48, 48);
105
+ const ruler = chalk.dim(' ' + '─'.repeat(W));
106
+ console.log(ruler);
107
+ console.log(chalk.dim(' 大脑 ') + chalk.cyan(`${config.llm.model}`) + chalk.dim(` via ${providerLabel}`));
108
+ console.log(chalk.dim(' 模式 ') + chalk.cyan(mode.label));
109
+ console.log(chalk.dim(' 之家 ') + chalk.cyan(MIDOU_HOME));
110
+ console.log(ruler);
108
111
  console.log('');
109
112
 
110
113
  if (isFirstBoot) {
111
114
  console.log(chalk.hex('#FFD700')(' ✨ 这是 midou 的第一次觉醒!'));
112
115
  console.log('');
113
116
  } else {
114
- console.log(chalk.hex('#98FB98')(' 灵魂已加载'));
115
- console.log(chalk.hex('#98FB98')(' 记忆已恢复'));
116
- console.log(chalk.hex('#98FB98')(' midou 准备好了'));
117
+ console.log(chalk.hex('#98FB98')(' ✦ midou 准备好了'));
117
118
  console.log('');
118
119
  }
119
120
 
@@ -140,6 +141,6 @@ export async function sleep() {
140
141
  await writeJournal(`### ${now} [入睡]\n\nmidou 在 ${now} 入睡了。晚安。\n`);
141
142
 
142
143
  console.log('');
143
- console.log(chalk.hex('#FFB347')(' 🐱 midou 入睡了... 晚安'));
144
+ console.log(chalk.hex('#FFB347')(' 🐱 midou 入睡了…晚安'));
144
145
  console.log('');
145
146
  }
package/src/chat.js CHANGED
@@ -11,7 +11,7 @@
11
11
  */
12
12
 
13
13
  import chalk from 'chalk';
14
- import { chat, chatWithTools } from './llm.js';
14
+ import { chat, chatStreamWithTools } from './llm.js';
15
15
  import { toolDefinitions, executeTool } from './tools.js';
16
16
  import { getMCPToolDefinitions } from './mcp.js';
17
17
  import { SessionMemory, logConversation } from './memory.js';
@@ -25,6 +25,8 @@ export class ChatEngine {
25
25
  this.session = new SessionMemory(50);
26
26
  this.session.add('system', systemPrompt);
27
27
  this.turnCount = 0;
28
+ this.showThinking = true; // 是否实时显示思考过程
29
+ this.lastThinking = ''; // 上一次思考内容(/think 查看)
28
30
  }
29
31
 
30
32
  /**
@@ -56,11 +58,10 @@ export class ChatEngine {
56
58
  }
57
59
 
58
60
  /**
59
- * 带工具的思考过程
60
- *
61
- * 优化:使用 chatWithTools 做首次判断,如果没有工具调用
62
- * 直接采用其返回内容(而非重新发起流式请求),消除双重 API 调用。
63
- * 仅在后续轮次(工具调用后的最终回复)使用流式输出。
61
+ * 带工具的流式思考过程
62
+ *
63
+ * 全流式架构:不再用非流式 chatWithTools
64
+ * 所有响应(思考、文本、工具调用)都实时流式展示。
64
65
  */
65
66
  async _thinkWithTools() {
66
67
  const messages = this.session.getMessages();
@@ -71,47 +72,111 @@ export class ChatEngine {
71
72
 
72
73
  while (iterations < maxIterations) {
73
74
  iterations++;
75
+ let completeMessage = null;
76
+ let iterationText = '';
77
+ let thinkingText = '';
78
+ let thinkingLineCount = 0;
74
79
 
75
80
  try {
76
- const aiMessage = await chatWithTools(messages, tools);
81
+ for await (const event of chatStreamWithTools(messages, tools)) {
82
+ switch (event.type) {
83
+ // ── 思考块(支持 thinking 的模型)──
84
+ case 'thinking_start':
85
+ if (this.showThinking) {
86
+ const w = Math.min(process.stdout.columns || 50, 50);
87
+ process.stdout.write('\n' + chalk.hex('#C9B1FF')(' ┌─ 💭 ') + chalk.hex('#C9B1FF').dim('─'.repeat(Math.max(0, w - 10))) + '\n');
88
+ process.stdout.write(chalk.hex('#C9B1FF').dim(' │ '));
89
+ }
90
+ break;
91
+
92
+ case 'thinking_delta':
93
+ thinkingText += event.text;
94
+ if (this.showThinking) {
95
+ const lines = event.text.split('\n');
96
+ for (let i = 0; i < lines.length; i++) {
97
+ if (i > 0) {
98
+ process.stdout.write(chalk.hex('#C9B1FF').dim('\n │ '));
99
+ thinkingLineCount++;
100
+ }
101
+ process.stdout.write(chalk.hex('#C9B1FF').dim(lines[i]));
102
+ }
103
+ }
104
+ break;
105
+
106
+ case 'thinking_end':
107
+ this.lastThinking = event.fullText || thinkingText;
108
+ if (this.showThinking && thinkingText) {
109
+ const w = Math.min(process.stdout.columns || 50, 50);
110
+ process.stdout.write(chalk.hex('#C9B1FF').dim(`\n └─ ${thinkingText.length} 字 `) + chalk.hex('#C9B1FF').dim('─'.repeat(Math.max(0, w - 8 - String(thinkingText.length).length))) + '\n\n');
111
+ } else if (thinkingText) {
112
+ process.stdout.write(chalk.hex('#C9B1FF').dim(` 💭 ${thinkingText.length} 字 — /think 查看\n`));
113
+ }
114
+ break;
115
+
116
+ // ── 正文流式输出 ──
117
+ case 'text_delta':
118
+ iterationText += event.text;
119
+ process.stdout.write(chalk.hex('#FFB347')(event.text));
120
+ break;
121
+
122
+ // ── 工具调用 ──
123
+ case 'tool_start': {
124
+ const isMCP = event.name.startsWith('mcp_');
125
+ const icon = isMCP ? '🔌' : '⚙';
126
+ process.stdout.write(chalk.hex('#7FDBFF').dim(`\n ${icon} ${event.name} `));
127
+ break;
128
+ }
129
+
130
+ case 'tool_end':
131
+ process.stdout.write(chalk.hex('#7FDBFF').dim(`${JSON.stringify(event.input).slice(0, 50)}\n`));
132
+ break;
133
+
134
+ // ── 消息完成 ──
135
+ case 'message_complete':
136
+ completeMessage = event.message;
137
+ break;
138
+ }
139
+ }
77
140
 
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));
141
+ // 没有工具调用 → 这是最终回复
142
+ if (!completeMessage?.tool_calls || completeMessage.tool_calls.length === 0) {
143
+ fullResponse = iterationText;
144
+ if (fullResponse) {
145
+ this.session.add('assistant', fullResponse);
146
+ }
83
147
  process.stdout.write('\n');
84
148
  break;
85
149
  }
86
150
 
87
- // 处理工具调用
88
- messages.push(aiMessage);
151
+ // 有工具调用 → 执行工具,然后继续下一轮流式
152
+ messages.push(completeMessage);
89
153
 
90
- for (const toolCall of aiMessage.tool_calls) {
91
- const funcName = toolCall.function.name;
154
+ for (const tc of completeMessage.tool_calls) {
92
155
  let args;
93
- try {
94
- args = JSON.parse(toolCall.function.arguments);
95
- } catch {
96
- args = {};
97
- }
156
+ try { args = JSON.parse(tc.function.arguments); } catch { args = {}; }
98
157
 
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);
158
+ process.stdout.write(chalk.hex('#7FDBFF').dim(` ↳ ${tc.function.name} `));
159
+ const result = await executeTool(tc.function.name, args);
160
+ process.stdout.write(chalk.green.dim('✓') + '\n');
104
161
 
105
162
  messages.push({
106
163
  role: 'tool',
107
- tool_call_id: toolCall.id,
164
+ tool_call_id: tc.id,
108
165
  content: String(result),
109
166
  });
110
167
  }
111
168
 
112
- // 继续循环让模型基于工具结果生成最终回复
169
+ // 重置本轮文本,准备下一轮流式
170
+ iterationText = '';
171
+
113
172
  } catch (error) {
114
- // 失败时回退到流式(无工具)
173
+ // 失败时回退到纯流式(无工具)
174
+ if (iterationText) {
175
+ process.stdout.write('\n');
176
+ console.error(chalk.yellow(` ⚠ ${error.message},重试中…`));
177
+ } else {
178
+ console.error(chalk.yellow(` ⚠ ${error.message}`));
179
+ }
115
180
  fullResponse = await this._streamResponse();
116
181
  break;
117
182
  }
package/src/index.js CHANGED
@@ -31,13 +31,15 @@ import config, { MIDOU_HOME, MIDOU_PKG } from '../midou.config.js';
31
31
  import { isInitialized, initSoulDir, migrateFromWorkspace, MIDOU_SOUL_DIR } from './init.js';
32
32
 
33
33
  // ===== 猫爪 ASCII Art =====
34
- const LOGO = `
35
- /\\_/\\
36
- ( o.o )
37
- > ^ < ${chalk.hex('#FFB347').bold('midou')}
38
- /| |\\ ${chalk.dim('你的 AI 伙伴')}
39
- (_| |_)
40
- `;
34
+ const LOGO = [
35
+ '',
36
+ chalk.hex('#FFB347')(' /\\_/\\'),
37
+ chalk.hex('#FFB347')(' ( o.o )'),
38
+ chalk.hex('#FFB347')(' > ^ < ') + chalk.hex('#FFB347').bold('midou'),
39
+ chalk.hex('#FFB347')(' /| |\\ ') + chalk.dim('你的 AI 伙伴'),
40
+ chalk.hex('#FFB347')(' (_| |_)'),
41
+ '',
42
+ ].join('\n');
41
43
 
42
44
  /**
43
45
  * 特殊命令处理
@@ -57,18 +59,36 @@ const COMMANDS = {
57
59
  '/skills': '查看可用技能',
58
60
  '/mcp': '查看 MCP 连接状态',
59
61
  '/mode': '切换功耗模式 (eco/normal/full)',
62
+ '/think': '查看上一次的思考过程',
60
63
  };
61
64
 
62
65
  /**
63
66
  * 显示帮助信息
64
67
  */
65
68
  function showHelp() {
69
+ const groups = [
70
+ ['对话', ['/help', '/think']],
71
+ ['灵魂', ['/soul', '/evolve', '/memory']],
72
+ ['系统', ['/status', '/mode', '/heartbeat', '/where']],
73
+ ['扩展', ['/skills', '/mcp', '/reminders']],
74
+ ];
75
+
66
76
  console.log('');
67
- console.log(chalk.hex('#FFB347').bold(' midou 命令:'));
77
+ console.log(chalk.hex('#FFB347').bold(' 🐱 midou 命令'));
68
78
  console.log('');
69
- for (const [cmd, desc] of Object.entries(COMMANDS)) {
70
- console.log(` ${chalk.cyan(cmd.padEnd(14))} ${chalk.dim(desc)}`);
79
+
80
+ for (const [groupName, cmds] of groups) {
81
+ console.log(chalk.dim(` ${groupName}`));
82
+ for (const cmd of cmds) {
83
+ const desc = COMMANDS[cmd];
84
+ if (desc) {
85
+ console.log(` ${chalk.cyan(cmd.padEnd(14))}${chalk.dim(desc)}`);
86
+ }
87
+ }
88
+ console.log('');
71
89
  }
90
+
91
+ console.log(chalk.dim(' /quit /exit /bye 退出对话'));
72
92
  console.log('');
73
93
  console.log(chalk.dim(' 直接输入文字即可与 midou 对话'));
74
94
  console.log('');
@@ -81,30 +101,28 @@ function showStatus() {
81
101
  const hb = getHeartbeatStatus();
82
102
  const prov = getProvider() === 'anthropic' ? 'Anthropic SDK' : 'OpenAI SDK';
83
103
  const mcpStatus = getMCPStatus();
104
+ const mode = getMode();
105
+
84
106
  console.log('');
85
107
  console.log(chalk.hex('#FFB347').bold(' 🐱 midou 状态'));
86
- console.log(chalk.dim(' ─────────────────'));
87
- console.log(` 大脑: ${chalk.cyan(config.llm.model)} via ${chalk.cyan(prov)}`);
88
- console.log(` 灵魂之家: ${chalk.cyan(MIDOU_HOME)}`);
89
- console.log(` 代码位置: ${chalk.dim(MIDOU_PKG)}`);
90
- console.log(` 心跳: ${hb.running ? chalk.green('运行中') : chalk.red('已停止')}`);
91
- console.log(` 心跳次数: ${hb.count}`);
92
- console.log(` 心跳间隔: ${hb.interval} 分钟`);
93
- console.log(` 活跃时段: ${hb.activeHours.start}:00 - ${hb.activeHours.end}:00`);
94
- console.log(` 当前活跃: ${hb.isActiveNow ? chalk.green('是') : chalk.yellow('否')}`);
95
- // 提醒状态
108
+ console.log('');
109
+ console.log(chalk.dim(' 大脑 ') + chalk.cyan(config.llm.model) + chalk.dim(` via ${prov}`));
110
+ console.log(chalk.dim(' 模式 ') + chalk.cyan(mode.label));
111
+ console.log(chalk.dim(' 心跳 ') + (hb.running ? chalk.green('● 运行中') : chalk.red('○ 已停止')) + chalk.dim(` (${hb.count} 次 · 每 ${hb.interval} 分钟)`));
112
+ console.log(chalk.dim(' 活跃 ') + chalk.dim(`${hb.activeHours.start}:00–${hb.activeHours.end}:00 `) + (hb.isActiveNow ? chalk.green('') : chalk.yellow('')));
113
+
96
114
  const reminderText = formatReminders();
97
- console.log(` 提醒: ${reminderText === '当前没有活跃的提醒' ? chalk.dim('无') : chalk.green('活跃')}`);
98
- // MCP 状态
115
+ console.log(chalk.dim(' 提醒 ') + (reminderText === '当前没有活跃的提醒' ? chalk.dim('无') : chalk.green('活跃')));
116
+
99
117
  if (mcpStatus.length > 0) {
100
118
  const connected = mcpStatus.filter(s => s.connected).length;
101
- console.log(` MCP: ${chalk.cyan(`${connected}/${mcpStatus.length} 个服务器已连接`)}`);
119
+ console.log(chalk.dim(' MCP ') + chalk.cyan(`${connected}/${mcpStatus.length}`) + chalk.dim(' 已连接'));
102
120
  } else {
103
- console.log(` MCP: ${chalk.dim('未配置')}`);
121
+ console.log(chalk.dim(' MCP 未配置'));
104
122
  }
105
- // 功耗模式
106
- const mode = getMode();
107
- console.log(` 模式: ${chalk.cyan(mode.label)}`);
123
+
124
+ console.log(chalk.dim(' 之家 ') + chalk.cyan(MIDOU_HOME));
125
+ console.log(chalk.dim(' 代码 ') + chalk.dim(MIDOU_PKG));
108
126
  console.log('');
109
127
  }
110
128
 
@@ -166,9 +184,13 @@ async function main() {
166
184
  }
167
185
 
168
186
  // 检查 .env 是否配置了 API Key
169
- const envContent = await import('fs').then(f =>
170
- f.readFileSync(path.join(MIDOU_HOME, '.env'), 'utf-8').toString()
171
- );
187
+ let envContent = '';
188
+ try {
189
+ const f = await import('fs');
190
+ envContent = f.readFileSync(path.join(MIDOU_HOME, '.env'), 'utf-8');
191
+ } catch {
192
+ envContent = 'your-api-key-here';
193
+ }
172
194
  if (envContent.includes('your-api-key-here')) {
173
195
  console.log('');
174
196
  console.log(chalk.yellow(' ⚠️ 请先编辑配置文件填入 API Key:'));
@@ -192,25 +214,25 @@ async function main() {
192
214
  // 启动心跳
193
215
  const heartbeat = startHeartbeat((msg) => {
194
216
  console.log('');
195
- console.log(chalk.hex('#FFD700')(' 💓 [心跳] ') + chalk.dim(msg.slice(0, 100)));
217
+ console.log(chalk.hex('#FF6B9D')(' 💓 ') + chalk.dim(msg.slice(0, 100)));
196
218
  console.log('');
197
219
  });
198
220
 
199
221
  // 启动定时提醒调度器
200
222
  await startScheduler((reminder) => {
201
223
  console.log('');
202
- console.log(chalk.hex('#FFD700')(`[提醒] `) + chalk.bold(reminder.text));
224
+ console.log(chalk.hex('#FFD700')('') + chalk.bold(reminder.text));
203
225
  if (reminder.repeat) {
204
- console.log(chalk.dim(` (每 ${reminder.intervalMinutes} 分钟,第 ${reminder.firedCount} )`));
226
+ console.log(chalk.dim(` 每 ${reminder.intervalMinutes} 分钟 · 第 ${reminder.firedCount} 次`));
205
227
  }
206
228
  console.log('');
207
229
  });
208
230
 
209
231
  // 如果是首次启动,执行觉醒仪式
210
232
  if (isFirstBoot) {
211
- console.log(chalk.hex('#FFD700')(' midou 正在进行觉醒仪式...'));
233
+ console.log(chalk.hex('#FFD700')(' midou 正在进行觉醒仪式…'));
212
234
  console.log('');
213
- process.stdout.write(chalk.hex('#FFB347')(' midou: '));
235
+ process.stdout.write(chalk.hex('#FFB347')(' 🐱 '));
214
236
  await engine.talk('你好,我是你的创造者。你刚刚醒来,请按照觉醒指引介绍你自己吧。');
215
237
  console.log('');
216
238
  await completeBootstrap();
@@ -220,7 +242,7 @@ async function main() {
220
242
  const rl = readline.createInterface({
221
243
  input: process.stdin,
222
244
  output: process.stdout,
223
- prompt: chalk.cyan(' 你: '),
245
+ prompt: chalk.cyan(' '),
224
246
  terminal: true,
225
247
  });
226
248
 
@@ -238,7 +260,7 @@ async function main() {
238
260
  process.on('SIGTERM', gracefulExit);
239
261
 
240
262
  // 显示帮助提示
241
- console.log(chalk.dim(' 输入 /help 查看命令列表,或直接开始对话'));
263
+ console.log(chalk.dim(' 输入 /help 查看命令 · 直接输入文字开始对话'));
242
264
  console.log('');
243
265
 
244
266
  rl.prompt();
@@ -266,11 +288,11 @@ async function main() {
266
288
  return;
267
289
 
268
290
  case '/heartbeat':
269
- console.log(chalk.dim(' 🐱 手动心跳中...'));
291
+ console.log(chalk.dim(' 手动心跳中…'));
270
292
  await manualBeat((msg) => {
271
293
  console.log(chalk.hex('#FFB347')(` ${msg}`));
272
294
  });
273
- console.log(chalk.dim(' 心跳完成'));
295
+ console.log(chalk.dim(' 💓 完成'));
274
296
  rl.prompt();
275
297
  return;
276
298
 
@@ -303,9 +325,9 @@ async function main() {
303
325
  return;
304
326
 
305
327
  case '/evolve':
306
- console.log(chalk.dim(' 🐱 midou 正在自我反思...'));
328
+ console.log(chalk.dim(' 🧬 midou 正在自我反思…'));
307
329
  console.log('');
308
- process.stdout.write(chalk.hex('#FFB347')(' midou: '));
330
+ process.stdout.write(chalk.hex('#FFB347')(' 🐱 '));
309
331
  await engine.talk('请进行一次深度自我反思。回顾我们的对话和你的记忆,思考你想要如何进化。如果你决定修改自己的灵魂,请使用 evolve_soul 工具。');
310
332
  console.log('');
311
333
  rl.prompt();
@@ -313,8 +335,8 @@ async function main() {
313
335
 
314
336
  case '/where':
315
337
  console.log('');
316
- console.log(chalk.hex('#FFB347')(` 灵魂之家: ${MIDOU_HOME}`));
317
- console.log(chalk.dim(` 代码位置: ${MIDOU_PKG}`));
338
+ console.log(chalk.dim(' 之家 ') + chalk.cyan(MIDOU_HOME));
339
+ console.log(chalk.dim(' 代码 ') + chalk.dim(MIDOU_PKG));
318
340
  console.log('');
319
341
  rl.prompt();
320
342
  return;
@@ -356,8 +378,8 @@ async function main() {
356
378
  console.log(chalk.dim(` 创建 ${MIDOU_HOME}/mcp.json 来配置`));
357
379
  } else {
358
380
  for (const s of mcpStatus) {
359
- const state = s.connected ? chalk.green('') : chalk.red('');
360
- console.log(` ${state} ${chalk.cyan(s.name)} — ${s.toolCount} 个工具`);
381
+ const state = s.connected ? chalk.green('') : chalk.red('');
382
+ console.log(` ${state} ${chalk.cyan(s.name)} ${chalk.dim('')} ${s.toolCount} ${chalk.dim('工具')}`);
361
383
  if (s.tools.length > 0) {
362
384
  console.log(chalk.dim(` 工具: ${s.tools.join(', ')}`));
363
385
  }
@@ -376,11 +398,11 @@ async function main() {
376
398
  console.log(chalk.hex('#98FB98')(` ✅ 已切换到 ${newMode.label}`));
377
399
  // 重建系统提示词
378
400
  const strategy = getPromptStrategy();
379
- const soul = loadSoul();
380
- const journals = getRecentMemories(strategy.journalDays || 2);
381
- const skillsPrompt = strategy.includeSkills ? buildSkillsPrompt(await discoverSkills()) : '';
382
- const mcpPrompt = strategy.includeMCP ? buildMCPPrompt() : '';
383
- const newPrompt = buildSystemPrompt(soul, journals, { skillsPrompt, mcpPrompt }, strategy);
401
+ const soulData2 = await loadSoul();
402
+ const journals2 = await getRecentMemories(strategy.journalDays || 2);
403
+ const sp = strategy.includeSkills ? await buildSkillsPrompt() : '';
404
+ const mp = strategy.includeMCP ? buildMCPPrompt() : '';
405
+ const newPrompt = buildSystemPrompt(soulData2, journals2, { skills: sp, mcp: mp }, strategy);
384
406
  engine.updateSystemPrompt(newPrompt);
385
407
  console.log(chalk.dim(` 系统提示词已按 ${cmdArg} 模式重建`));
386
408
  console.log('');
@@ -391,9 +413,11 @@ async function main() {
391
413
  console.log(chalk.hex('#FFD700').bold(' ⚡ 功耗模式'));
392
414
  console.log(chalk.dim(' ─────────────────'));
393
415
  for (const m of modes) {
394
- const marker = m.name === current.name ? chalk.green(' ◀ 当前') : '';
395
- console.log(` ${m.label}${marker}`);
396
- console.log(chalk.dim(` maxTokens: ${m.maxTokens}, temp: ${m.temperature}`));
416
+ const active = m.name === current.name;
417
+ const marker = active ? chalk.green(' ◄') : '';
418
+ const label = active ? chalk.hex('#FFB347')(m.label) : chalk.dim(m.label);
419
+ console.log(` ${label}${marker}`);
420
+ console.log(chalk.dim(` ${m.maxTokens} tokens · temp ${m.temperature}`));
397
421
  console.log(chalk.dim(` ${m.description}`));
398
422
  }
399
423
  console.log('');
@@ -404,6 +428,25 @@ async function main() {
404
428
  return;
405
429
  }
406
430
 
431
+ case '/think': {
432
+ const thinking = engine.lastThinking;
433
+ console.log('');
434
+ if (thinking) {
435
+ console.log(chalk.hex('#C9B1FF').bold(' 💭 上一次的思考过程'));
436
+ console.log('');
437
+ const lines = thinking.split('\n');
438
+ for (const line of lines) {
439
+ console.log(chalk.hex('#C9B1FF').dim(` │ ${line}`));
440
+ }
441
+ console.log(chalk.hex('#C9B1FF').dim(` └─ ${thinking.length} 字`));
442
+ } else {
443
+ console.log(chalk.dim(' 没有思考记录'));
444
+ }
445
+ console.log('');
446
+ rl.prompt();
447
+ return;
448
+ }
449
+
407
450
  default:
408
451
  console.log(chalk.dim(` 未知命令: ${input},输入 /help 查看帮助`));
409
452
  rl.prompt();
@@ -413,20 +456,20 @@ async function main() {
413
456
 
414
457
  // 正常对话
415
458
  console.log('');
416
- process.stdout.write(chalk.hex('#FFB347')(' midou: '));
459
+ process.stdout.write(chalk.hex('#FFB347')(' 🐱 '));
417
460
 
418
461
  try {
419
462
  await engine.talk(input);
420
463
  } catch (error) {
421
- console.log(chalk.red(`\n 出了点问题: ${error.message}`));
464
+ console.log(chalk.red(`\n 出了点问题: ${error.message}`));
422
465
  }
423
466
 
424
467
  console.log('');
425
468
  rl.prompt();
426
469
  });
427
470
 
428
- rl.on('close', () => {
429
- gracefulExit();
471
+ rl.on('close', async () => {
472
+ await gracefulExit();
430
473
  });
431
474
  }
432
475
 
package/src/llm.js CHANGED
@@ -11,6 +11,7 @@
11
11
  import OpenAI from 'openai';
12
12
  import Anthropic from '@anthropic-ai/sdk';
13
13
  import config from '../midou.config.js';
14
+ import { getModeMaxTokens, getModeTemperature } from './mode.js';
14
15
 
15
16
  // ────────────────────── 内部状态 ──────────────────────
16
17
 
@@ -104,6 +105,41 @@ function anthropicMsgToOpenAI(msg) {
104
105
  };
105
106
  }
106
107
 
108
+ /**
109
+ * 将 OpenAI 格式的 messages 数组转为 Anthropic 格式
110
+ * (处理 tool / assistant+tool_calls 消息)
111
+ */
112
+ function toAnthropicMessages(messages) {
113
+ return messages.map(m => {
114
+ if (m.role === 'tool') {
115
+ return {
116
+ role: 'user',
117
+ content: [{
118
+ type: 'tool_result',
119
+ tool_use_id: m.tool_call_id,
120
+ content: m.content,
121
+ }],
122
+ };
123
+ }
124
+ if (m.role === 'assistant' && m.tool_calls) {
125
+ const content = [];
126
+ if (m.content) content.push({ type: 'text', text: m.content });
127
+ for (const tc of m.tool_calls) {
128
+ let input;
129
+ try { input = JSON.parse(tc.function.arguments); } catch { input = {}; }
130
+ content.push({
131
+ type: 'tool_use',
132
+ id: tc.id,
133
+ name: tc.function.name,
134
+ input,
135
+ });
136
+ }
137
+ return { role: 'assistant', content };
138
+ }
139
+ return m;
140
+ });
141
+ }
142
+
107
143
  // ────────────────────── 公开 API ──────────────────────
108
144
 
109
145
  /**
@@ -111,7 +147,6 @@ function anthropicMsgToOpenAI(msg) {
111
147
  */
112
148
  export async function* chat(messages, options = {}) {
113
149
  if (!provider) initLLM();
114
- const { getModeMaxTokens, getModeTemperature } = await import('./mode.js');
115
150
  const model = options.model || config.llm.model;
116
151
  const temperature = options.temperature ?? getModeTemperature();
117
152
  const maxTokens = options.maxTokens || getModeMaxTokens();
@@ -147,7 +182,6 @@ export async function* chat(messages, options = {}) {
147
182
  */
148
183
  export async function chatSync(messages, options = {}) {
149
184
  if (!provider) initLLM();
150
- const { getModeMaxTokens, getModeTemperature } = await import('./mode.js');
151
185
  const model = options.model || config.llm.model;
152
186
  const temperature = options.temperature ?? getModeTemperature();
153
187
  const maxTokens = options.maxTokens || getModeMaxTokens();
@@ -176,7 +210,6 @@ export async function chatSync(messages, options = {}) {
176
210
  */
177
211
  export async function chatWithTools(messages, tools, options = {}) {
178
212
  if (!provider) initLLM();
179
- const { getModeMaxTokens, getModeTemperature } = await import('./mode.js');
180
213
  const model = options.model || config.llm.model;
181
214
  const temperature = options.temperature ?? getModeTemperature();
182
215
  const maxTokens = options.maxTokens || getModeMaxTokens();
@@ -184,34 +217,7 @@ export async function chatWithTools(messages, tools, options = {}) {
184
217
  if (provider === 'anthropic') {
185
218
  const { system, rest } = extractSystem(messages);
186
219
 
187
- // OpenAI 格式的 tool_result 转换为 Anthropic 格式
188
- const anthropicMessages = rest.map(m => {
189
- if (m.role === 'tool') {
190
- return {
191
- role: 'user',
192
- content: [{
193
- type: 'tool_result',
194
- tool_use_id: m.tool_call_id,
195
- content: m.content,
196
- }],
197
- };
198
- }
199
- // 如果 assistant 消息包含 tool_calls,转换回 Anthropic 格式
200
- if (m.role === 'assistant' && m.tool_calls) {
201
- const content = [];
202
- if (m.content) content.push({ type: 'text', text: m.content });
203
- for (const tc of m.tool_calls) {
204
- content.push({
205
- type: 'tool_use',
206
- id: tc.id,
207
- name: tc.function.name,
208
- input: JSON.parse(tc.function.arguments),
209
- });
210
- }
211
- return { role: 'assistant', content };
212
- }
213
- return m;
214
- });
220
+ const anthropicMessages = toAnthropicMessages(rest);
215
221
 
216
222
  const res = await anthropicClient.messages.create({
217
223
  model, system: system || undefined,
@@ -231,6 +237,176 @@ export async function chatWithTools(messages, tools, options = {}) {
231
237
  }
232
238
  }
233
239
 
240
+ /**
241
+ * 流式对话 + 工具调用 — 返回标准化事件流
242
+ *
243
+ * 事件类型:
244
+ * thinking_start — 思考块开始
245
+ * thinking_delta { text } — 思考内容增量
246
+ * thinking_end { fullText } — 思考块结束
247
+ * text_delta { text } — 正文增量
248
+ * tool_start { name, id } — 工具调用开始
249
+ * tool_end { name, id, input } — 工具调用参数完成
250
+ * message_complete { message, stopReason } — 整条消息完成
251
+ */
252
+ export async function* chatStreamWithTools(messages, tools, options = {}) {
253
+ if (!provider) initLLM();
254
+ const model = options.model || config.llm.model;
255
+ const temperature = options.temperature ?? getModeTemperature();
256
+ const maxTokens = options.maxTokens || getModeMaxTokens();
257
+
258
+ if (provider === 'anthropic') {
259
+ const { system, rest } = extractSystem(messages);
260
+ const anthropicMessages = toAnthropicMessages(rest);
261
+
262
+ const stream = anthropicClient.messages.stream({
263
+ model,
264
+ system: system || undefined,
265
+ messages: anthropicMessages,
266
+ max_tokens: maxTokens,
267
+ temperature,
268
+ tools: toAnthropicTools(tools),
269
+ });
270
+
271
+ let fullText = '';
272
+ let thinkingText = '';
273
+ let toolCalls = [];
274
+ let currentBlockType = null;
275
+ let currentToolId = '';
276
+ let currentToolName = '';
277
+ let currentToolJson = '';
278
+ let stopReason = null;
279
+
280
+ for await (const event of stream) {
281
+ switch (event.type) {
282
+ case 'content_block_start': {
283
+ const block = event.content_block;
284
+ currentBlockType = block.type;
285
+ if (block.type === 'thinking') {
286
+ yield { type: 'thinking_start' };
287
+ } else if (block.type === 'tool_use') {
288
+ currentToolId = block.id;
289
+ currentToolName = block.name;
290
+ currentToolJson = '';
291
+ yield { type: 'tool_start', name: block.name, id: block.id };
292
+ }
293
+ break;
294
+ }
295
+ case 'content_block_delta': {
296
+ const d = event.delta;
297
+ if (d.type === 'thinking_delta') {
298
+ thinkingText += d.thinking;
299
+ yield { type: 'thinking_delta', text: d.thinking };
300
+ } else if (d.type === 'text_delta') {
301
+ fullText += d.text;
302
+ yield { type: 'text_delta', text: d.text };
303
+ } else if (d.type === 'input_json_delta') {
304
+ currentToolJson += d.partial_json;
305
+ }
306
+ break;
307
+ }
308
+ case 'content_block_stop': {
309
+ if (currentBlockType === 'thinking') {
310
+ yield { type: 'thinking_end', fullText: thinkingText };
311
+ } else if (currentBlockType === 'tool_use') {
312
+ let input = {};
313
+ try { input = JSON.parse(currentToolJson); } catch {}
314
+ toolCalls.push({
315
+ id: currentToolId,
316
+ type: 'function',
317
+ function: { name: currentToolName, arguments: currentToolJson },
318
+ });
319
+ yield { type: 'tool_end', name: currentToolName, id: currentToolId, input };
320
+ }
321
+ currentBlockType = null;
322
+ break;
323
+ }
324
+ case 'message_delta': {
325
+ if (event.delta?.stop_reason) stopReason = event.delta.stop_reason;
326
+ break;
327
+ }
328
+ }
329
+ }
330
+
331
+ yield {
332
+ type: 'message_complete',
333
+ message: {
334
+ role: 'assistant',
335
+ content: fullText || null,
336
+ tool_calls: toolCalls.length > 0 ? toolCalls : undefined,
337
+ },
338
+ stopReason,
339
+ };
340
+
341
+ } else {
342
+ // OpenAI provider — 流式 + 工具
343
+ const stream = await openaiClient.chat.completions.create({
344
+ model, messages, temperature, max_tokens: maxTokens,
345
+ tools: tools?.length > 0 ? tools : undefined,
346
+ tool_choice: tools?.length > 0 ? 'auto' : undefined,
347
+ stream: true,
348
+ });
349
+
350
+ let fullText = '';
351
+ let toolCallsMap = {};
352
+ let stopReason = null;
353
+
354
+ for await (const chunk of stream) {
355
+ const choice = chunk.choices[0];
356
+ if (!choice) continue;
357
+
358
+ if (choice.delta?.content) {
359
+ fullText += choice.delta.content;
360
+ yield { type: 'text_delta', text: choice.delta.content };
361
+ }
362
+
363
+ if (choice.delta?.tool_calls) {
364
+ for (const tc of choice.delta.tool_calls) {
365
+ const idx = tc.index;
366
+ if (!toolCallsMap[idx]) {
367
+ toolCallsMap[idx] = { id: '', name: '', args: '' };
368
+ }
369
+ if (tc.id) toolCallsMap[idx].id = tc.id;
370
+ if (tc.function?.name) {
371
+ toolCallsMap[idx].name = tc.function.name;
372
+ yield { type: 'tool_start', name: tc.function.name, id: tc.id || '' };
373
+ }
374
+ if (tc.function?.arguments) {
375
+ toolCallsMap[idx].args += tc.function.arguments;
376
+ }
377
+ }
378
+ }
379
+
380
+ if (choice.finish_reason) {
381
+ stopReason = choice.finish_reason;
382
+ }
383
+ }
384
+
385
+ // 输出工具完成事件并构建 toolCalls 数组
386
+ const toolCalls = [];
387
+ for (const tc of Object.values(toolCallsMap)) {
388
+ let input = {};
389
+ try { input = JSON.parse(tc.args); } catch {}
390
+ yield { type: 'tool_end', name: tc.name, id: tc.id, input };
391
+ toolCalls.push({
392
+ id: tc.id,
393
+ type: 'function',
394
+ function: { name: tc.name, arguments: tc.args },
395
+ });
396
+ }
397
+
398
+ yield {
399
+ type: 'message_complete',
400
+ message: {
401
+ role: 'assistant',
402
+ content: fullText || null,
403
+ tool_calls: toolCalls.length > 0 ? toolCalls : undefined,
404
+ },
405
+ stopReason: stopReason === 'tool_calls' ? 'tool_use' : (stopReason || 'end_turn'),
406
+ };
407
+ }
408
+ }
409
+
234
410
  /**
235
411
  * 获取当前提供商名称
236
412
  */
package/src/mcp.js CHANGED
@@ -140,10 +140,17 @@ class MCPConnection {
140
140
  /**
141
141
  * 发送 JSON-RPC 请求
142
142
  */
143
- _sendRequest(method, params = {}) {
143
+ _sendRequest(method, params = {}, timeoutMs = 30000) {
144
144
  return new Promise((resolve, reject) => {
145
145
  const id = ++this._requestId;
146
- this._pendingRequests.set(id, { resolve, reject });
146
+ const timer = setTimeout(() => {
147
+ this._pendingRequests.delete(id);
148
+ reject(new Error(`MCP 请求 ${method} 超时 (${timeoutMs}ms)`));
149
+ }, timeoutMs);
150
+ this._pendingRequests.set(id, {
151
+ resolve: (v) => { clearTimeout(timer); resolve(v); },
152
+ reject: (e) => { clearTimeout(timer); reject(e); },
153
+ });
147
154
 
148
155
  const msg = JSON.stringify({
149
156
  jsonrpc: '2.0',
@@ -217,6 +224,9 @@ class MCPConnection {
217
224
  this.process = null;
218
225
  }
219
226
  this.connected = false;
227
+ for (const { reject } of this._pendingRequests.values()) {
228
+ reject(new Error(`MCP 服务器 ${this.name} 已断开`));
229
+ }
220
230
  this._pendingRequests.clear();
221
231
  }
222
232
  }
@@ -306,13 +316,10 @@ export function isMCPTool(toolName) {
306
316
  * 执行 MCP 工具调用
307
317
  */
308
318
  export async function executeMCPTool(toolName, args) {
309
- // 解析:mcp_{serverName}_{toolName}
310
- const parts = toolName.replace(/^mcp_/, '').split('_');
311
-
312
- // 找到匹配的服务器
313
319
  for (const [serverName, conn] of connections) {
314
- if (toolName.startsWith(`mcp_${serverName}_`)) {
315
- const actualToolName = toolName.replace(`mcp_${serverName}_`, '');
320
+ const prefix = `mcp_${serverName}_`;
321
+ if (toolName.startsWith(prefix)) {
322
+ const actualToolName = toolName.slice(prefix.length);
316
323
  return await conn.callTool(actualToolName, args);
317
324
  }
318
325
  }
package/src/scheduler.js CHANGED
@@ -9,6 +9,7 @@
9
9
 
10
10
  import fs from 'fs/promises';
11
11
  import path from 'path';
12
+ import chalk from 'chalk';
12
13
  import dayjs from 'dayjs';
13
14
  import config from '../midou.config.js';
14
15
 
@@ -154,7 +155,11 @@ export async function startScheduler(onFire) {
154
155
  await loadReminders();
155
156
 
156
157
  // 每 30 秒检查一次提醒
157
- schedulerTimer = setInterval(() => checkReminders(onFire), 30 * 1000);
158
+ schedulerTimer = setInterval(() => {
159
+ checkReminders(onFire).catch(err => {
160
+ console.error(chalk.dim(` ⏰ 提醒检查异常: ${err.message}`));
161
+ });
162
+ }, 30 * 1000);
158
163
 
159
164
  return {
160
165
  stop: stopScheduler,