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 +12 -1
- package/package.json +1 -1
- package/src/boot.js +14 -13
- package/src/chat.js +94 -29
- package/src/index.js +100 -57
- package/src/llm.js +207 -31
- package/src/mcp.js +15 -8
- package/src/scheduler.js +6 -1
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
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')(
|
|
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('
|
|
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.
|
|
82
|
+
console.log(chalk.dim(' ') + chalk.green('●') + chalk.dim(` ${r.name} (${r.tools.length} 工具)`));
|
|
84
83
|
} else {
|
|
85
|
-
console.log(chalk.
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
console.log(
|
|
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,
|
|
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
|
-
*
|
|
62
|
-
*
|
|
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
|
|
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 (!
|
|
80
|
-
fullResponse =
|
|
81
|
-
|
|
82
|
-
|
|
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(
|
|
151
|
+
// 有工具调用 → 执行工具,然后继续下一轮流式
|
|
152
|
+
messages.push(completeMessage);
|
|
89
153
|
|
|
90
|
-
for (const
|
|
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
|
-
|
|
100
|
-
const
|
|
101
|
-
|
|
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:
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
70
|
-
|
|
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(
|
|
87
|
-
console.log(
|
|
88
|
-
console.log(
|
|
89
|
-
console.log(
|
|
90
|
-
console.log(
|
|
91
|
-
|
|
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(
|
|
98
|
-
|
|
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(
|
|
119
|
+
console.log(chalk.dim(' MCP ') + chalk.cyan(`${connected}/${mcpStatus.length}`) + chalk.dim(' 已连接'));
|
|
102
120
|
} else {
|
|
103
|
-
console.log(
|
|
121
|
+
console.log(chalk.dim(' MCP 未配置'));
|
|
104
122
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
console.log(
|
|
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
|
-
|
|
170
|
-
|
|
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('#
|
|
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')(
|
|
224
|
+
console.log(chalk.hex('#FFD700')(' ⏰ ') + chalk.bold(reminder.text));
|
|
203
225
|
if (reminder.repeat) {
|
|
204
|
-
console.log(chalk.dim(`
|
|
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')('
|
|
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('
|
|
328
|
+
console.log(chalk.dim(' 🧬 midou 正在自我反思…'));
|
|
307
329
|
console.log('');
|
|
308
|
-
process.stdout.write(chalk.hex('#FFB347')('
|
|
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.
|
|
317
|
-
console.log(chalk.dim(
|
|
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('
|
|
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
|
|
380
|
-
const
|
|
381
|
-
const
|
|
382
|
-
const
|
|
383
|
-
const newPrompt = buildSystemPrompt(
|
|
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
|
|
395
|
-
|
|
396
|
-
|
|
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')('
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
315
|
-
|
|
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(() =>
|
|
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,
|