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/llm.js
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LLM 适配器 — 连接任何模型,灵魂始终是 midou 自己
|
|
3
|
+
*
|
|
4
|
+
* 支持的提供商(provider):
|
|
5
|
+
* openai → OpenAI / DeepSeek / Moonshot / 智谱 / Ollama / vLLM …
|
|
6
|
+
* anthropic → Anthropic Claude / MiniMax(推荐)…
|
|
7
|
+
*
|
|
8
|
+
* 通过 MIDOU_PROVIDER 环境变量切换,默认 'anthropic'
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import OpenAI from 'openai';
|
|
12
|
+
import Anthropic from '@anthropic-ai/sdk';
|
|
13
|
+
import config from '../midou.config.js';
|
|
14
|
+
|
|
15
|
+
// ────────────────────── 内部状态 ──────────────────────
|
|
16
|
+
|
|
17
|
+
let provider = null; // 'openai' | 'anthropic'
|
|
18
|
+
let openaiClient = null;
|
|
19
|
+
let anthropicClient = null;
|
|
20
|
+
|
|
21
|
+
// ────────────────────── 初始化 ──────────────────────
|
|
22
|
+
|
|
23
|
+
export function initLLM() {
|
|
24
|
+
provider = config.llm.provider;
|
|
25
|
+
|
|
26
|
+
if (provider === 'anthropic') {
|
|
27
|
+
if (!config.llm.anthropic.apiKey) {
|
|
28
|
+
throw new Error(
|
|
29
|
+
'🐱 midou 需要一个 API Key 才能思考!\n' +
|
|
30
|
+
'请设置环境变量 MIDOU_API_KEY 或在 .env 文件中配置'
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
anthropicClient = new Anthropic({
|
|
34
|
+
baseURL: config.llm.anthropic.baseURL,
|
|
35
|
+
apiKey: config.llm.anthropic.apiKey,
|
|
36
|
+
});
|
|
37
|
+
} else {
|
|
38
|
+
// openai 或其他兼容
|
|
39
|
+
if (!config.llm.openai.apiKey) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
'🐱 midou 需要一个 API Key 才能思考!\n' +
|
|
42
|
+
'请设置环境变量 MIDOU_API_KEY 或在 .env 文件中配置'
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
openaiClient = new OpenAI({
|
|
46
|
+
baseURL: config.llm.openai.baseURL,
|
|
47
|
+
apiKey: config.llm.openai.apiKey,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ────────────────────── 工具:Anthropic ↔ OpenAI 消息格式转换 ──────────────────────
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* 从标准 messages 数组中提取 system 消息(Anthropic 需要单独传)
|
|
56
|
+
*/
|
|
57
|
+
function extractSystem(messages) {
|
|
58
|
+
const system = messages.filter(m => m.role === 'system').map(m => m.content).join('\n\n');
|
|
59
|
+
const rest = messages.filter(m => m.role !== 'system');
|
|
60
|
+
return { system, rest };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* 将 OpenAI 风格的 tool 定义转换为 Anthropic 格式
|
|
65
|
+
*/
|
|
66
|
+
function toAnthropicTools(openaiTools) {
|
|
67
|
+
if (!openaiTools?.length) return undefined;
|
|
68
|
+
return openaiTools.map(t => ({
|
|
69
|
+
name: t.function.name,
|
|
70
|
+
description: t.function.description,
|
|
71
|
+
input_schema: t.function.parameters,
|
|
72
|
+
}));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 将 Anthropic 的 tool_use 响应转成 OpenAI message 格式(让上层代码统一处理)
|
|
77
|
+
*/
|
|
78
|
+
function anthropicMsgToOpenAI(msg) {
|
|
79
|
+
const toolCalls = [];
|
|
80
|
+
let textContent = '';
|
|
81
|
+
|
|
82
|
+
for (const block of msg.content) {
|
|
83
|
+
if (block.type === 'text') {
|
|
84
|
+
textContent += block.text;
|
|
85
|
+
} else if (block.type === 'thinking') {
|
|
86
|
+
// thinking 块也当文本输出——让 midou 可以展示思考过程
|
|
87
|
+
// 不做处理,避免干扰最终回复
|
|
88
|
+
} else if (block.type === 'tool_use') {
|
|
89
|
+
toolCalls.push({
|
|
90
|
+
id: block.id,
|
|
91
|
+
type: 'function',
|
|
92
|
+
function: {
|
|
93
|
+
name: block.name,
|
|
94
|
+
arguments: JSON.stringify(block.input),
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
role: 'assistant',
|
|
102
|
+
content: textContent || null,
|
|
103
|
+
tool_calls: toolCalls.length ? toolCalls : undefined,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ────────────────────── 公开 API ──────────────────────
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* 流式对话
|
|
111
|
+
*/
|
|
112
|
+
export async function* chat(messages, options = {}) {
|
|
113
|
+
if (!provider) initLLM();
|
|
114
|
+
const { getModeMaxTokens, getModeTemperature } = await import('./mode.js');
|
|
115
|
+
const model = options.model || config.llm.model;
|
|
116
|
+
const temperature = options.temperature ?? getModeTemperature();
|
|
117
|
+
const maxTokens = options.maxTokens || getModeMaxTokens();
|
|
118
|
+
|
|
119
|
+
if (provider === 'anthropic') {
|
|
120
|
+
const { system, rest } = extractSystem(messages);
|
|
121
|
+
const stream = anthropicClient.messages.stream({
|
|
122
|
+
model,
|
|
123
|
+
system: system || undefined,
|
|
124
|
+
messages: rest,
|
|
125
|
+
max_tokens: maxTokens,
|
|
126
|
+
temperature,
|
|
127
|
+
});
|
|
128
|
+
for await (const event of stream) {
|
|
129
|
+
if (event.type === 'content_block_delta' && event.delta?.type === 'text_delta') {
|
|
130
|
+
yield event.delta.text;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
} else {
|
|
134
|
+
const stream = await openaiClient.chat.completions.create({
|
|
135
|
+
model, messages, temperature, max_tokens: maxTokens, stream: true,
|
|
136
|
+
});
|
|
137
|
+
for await (const chunk of stream) {
|
|
138
|
+
const content = chunk.choices[0]?.delta?.content;
|
|
139
|
+
if (content) yield content;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* 非流式回复(心跳 / 后台任务)
|
|
146
|
+
* options.maxTokens 可由调用方(如心跳)单独指定以节省 token
|
|
147
|
+
*/
|
|
148
|
+
export async function chatSync(messages, options = {}) {
|
|
149
|
+
if (!provider) initLLM();
|
|
150
|
+
const { getModeMaxTokens, getModeTemperature } = await import('./mode.js');
|
|
151
|
+
const model = options.model || config.llm.model;
|
|
152
|
+
const temperature = options.temperature ?? getModeTemperature();
|
|
153
|
+
const maxTokens = options.maxTokens || getModeMaxTokens();
|
|
154
|
+
|
|
155
|
+
if (provider === 'anthropic') {
|
|
156
|
+
const { system, rest } = extractSystem(messages);
|
|
157
|
+
const res = await anthropicClient.messages.create({
|
|
158
|
+
model, system: system || undefined, messages: rest,
|
|
159
|
+
max_tokens: maxTokens, temperature,
|
|
160
|
+
});
|
|
161
|
+
// 拼接 text 块
|
|
162
|
+
return res.content
|
|
163
|
+
.filter(b => b.type === 'text')
|
|
164
|
+
.map(b => b.text)
|
|
165
|
+
.join('') || '';
|
|
166
|
+
} else {
|
|
167
|
+
const res = await openaiClient.chat.completions.create({
|
|
168
|
+
model, messages, temperature, max_tokens: maxTokens, stream: false,
|
|
169
|
+
});
|
|
170
|
+
return res.choices[0]?.message?.content || '';
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* 带工具调用的对话(返回统一的 OpenAI message 格式)
|
|
176
|
+
*/
|
|
177
|
+
export async function chatWithTools(messages, tools, options = {}) {
|
|
178
|
+
if (!provider) initLLM();
|
|
179
|
+
const { getModeMaxTokens, getModeTemperature } = await import('./mode.js');
|
|
180
|
+
const model = options.model || config.llm.model;
|
|
181
|
+
const temperature = options.temperature ?? getModeTemperature();
|
|
182
|
+
const maxTokens = options.maxTokens || getModeMaxTokens();
|
|
183
|
+
|
|
184
|
+
if (provider === 'anthropic') {
|
|
185
|
+
const { system, rest } = extractSystem(messages);
|
|
186
|
+
|
|
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
|
+
});
|
|
215
|
+
|
|
216
|
+
const res = await anthropicClient.messages.create({
|
|
217
|
+
model, system: system || undefined,
|
|
218
|
+
messages: anthropicMessages,
|
|
219
|
+
max_tokens: maxTokens, temperature,
|
|
220
|
+
tools: toAnthropicTools(tools),
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// 统一转成 OpenAI 格式返回
|
|
224
|
+
return anthropicMsgToOpenAI(res);
|
|
225
|
+
} else {
|
|
226
|
+
const res = await openaiClient.chat.completions.create({
|
|
227
|
+
model, messages, temperature, max_tokens: maxTokens,
|
|
228
|
+
tools, tool_choice: 'auto', stream: false,
|
|
229
|
+
});
|
|
230
|
+
return res.choices[0]?.message;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* 获取当前提供商名称
|
|
236
|
+
*/
|
|
237
|
+
export function getProvider() {
|
|
238
|
+
return provider || config.llm.provider;
|
|
239
|
+
}
|
package/src/mcp.js
ADDED
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP 客户端管理器 — midou 的扩展触手
|
|
3
|
+
*
|
|
4
|
+
* 连接外部 MCP 服务器,让 midou 获得更多能力。
|
|
5
|
+
* 就像猫咪伸出爪子探索新事物。
|
|
6
|
+
*
|
|
7
|
+
* 配置文件: ~/.midou/mcp.json
|
|
8
|
+
* 格式:
|
|
9
|
+
* {
|
|
10
|
+
* "mcpServers": {
|
|
11
|
+
* "server-name": {
|
|
12
|
+
* "command": "npx",
|
|
13
|
+
* "args": ["-y", "@modelcontextprotocol/server-filesystem", "/home/user"],
|
|
14
|
+
* "env": { "KEY": "value" }
|
|
15
|
+
* }
|
|
16
|
+
* }
|
|
17
|
+
* }
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { spawn } from 'child_process';
|
|
21
|
+
import fs from 'fs/promises';
|
|
22
|
+
import path from 'path';
|
|
23
|
+
import config from '../midou.config.js';
|
|
24
|
+
|
|
25
|
+
const MCP_CONFIG_FILE = path.join(config.workspace.root, 'mcp.json');
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* MCP 服务器连接
|
|
29
|
+
*/
|
|
30
|
+
class MCPConnection {
|
|
31
|
+
constructor(name, serverConfig) {
|
|
32
|
+
this.name = name;
|
|
33
|
+
this.config = serverConfig;
|
|
34
|
+
this.process = null;
|
|
35
|
+
this.tools = [];
|
|
36
|
+
this.connected = false;
|
|
37
|
+
this._requestId = 0;
|
|
38
|
+
this._pendingRequests = new Map();
|
|
39
|
+
this._buffer = '';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 连接到 MCP 服务器
|
|
44
|
+
*/
|
|
45
|
+
async connect() {
|
|
46
|
+
return new Promise((resolve, reject) => {
|
|
47
|
+
const timeout = setTimeout(() => {
|
|
48
|
+
reject(new Error(`MCP 服务器 ${this.name} 连接超时`));
|
|
49
|
+
}, 15000);
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const env = { ...process.env, ...(this.config.env || {}) };
|
|
53
|
+
this.process = spawn(this.config.command, this.config.args || [], {
|
|
54
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
55
|
+
env,
|
|
56
|
+
cwd: this.config.cwd || undefined,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
this.process.stdout.on('data', (data) => {
|
|
60
|
+
this._handleData(data.toString());
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
this.process.stderr.on('data', (data) => {
|
|
64
|
+
// MCP 服务器的 stderr 通常是日志
|
|
65
|
+
// 不做处理,避免干扰
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
this.process.on('error', (err) => {
|
|
69
|
+
clearTimeout(timeout);
|
|
70
|
+
this.connected = false;
|
|
71
|
+
reject(new Error(`MCP 服务器 ${this.name} 启动失败: ${err.message}`));
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
this.process.on('close', () => {
|
|
75
|
+
this.connected = false;
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// 发送 initialize 请求
|
|
79
|
+
this._sendRequest('initialize', {
|
|
80
|
+
protocolVersion: '2024-11-05',
|
|
81
|
+
capabilities: {},
|
|
82
|
+
clientInfo: { name: 'midou', version: '0.1.0' },
|
|
83
|
+
}).then(async (result) => {
|
|
84
|
+
clearTimeout(timeout);
|
|
85
|
+
this.connected = true;
|
|
86
|
+
|
|
87
|
+
// 发送 initialized 通知
|
|
88
|
+
this._sendNotification('notifications/initialized', {});
|
|
89
|
+
|
|
90
|
+
// 获取工具列表
|
|
91
|
+
await this._discoverTools();
|
|
92
|
+
|
|
93
|
+
resolve(result);
|
|
94
|
+
}).catch((err) => {
|
|
95
|
+
clearTimeout(timeout);
|
|
96
|
+
reject(err);
|
|
97
|
+
});
|
|
98
|
+
} catch (err) {
|
|
99
|
+
clearTimeout(timeout);
|
|
100
|
+
reject(err);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* 处理从服务器收到的数据
|
|
107
|
+
*/
|
|
108
|
+
_handleData(data) {
|
|
109
|
+
this._buffer += data;
|
|
110
|
+
|
|
111
|
+
// JSON-RPC 消息以换行分隔
|
|
112
|
+
const lines = this._buffer.split('\n');
|
|
113
|
+
this._buffer = lines.pop() || '';
|
|
114
|
+
|
|
115
|
+
for (const line of lines) {
|
|
116
|
+
const trimmed = line.trim();
|
|
117
|
+
if (!trimmed) continue;
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
const msg = JSON.parse(trimmed);
|
|
121
|
+
|
|
122
|
+
// 响应消息
|
|
123
|
+
if (msg.id !== undefined && this._pendingRequests.has(msg.id)) {
|
|
124
|
+
const { resolve, reject } = this._pendingRequests.get(msg.id);
|
|
125
|
+
this._pendingRequests.delete(msg.id);
|
|
126
|
+
|
|
127
|
+
if (msg.error) {
|
|
128
|
+
reject(new Error(`MCP Error: ${msg.error.message}`));
|
|
129
|
+
} else {
|
|
130
|
+
resolve(msg.result);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
// 通知消息忽略
|
|
134
|
+
} catch {
|
|
135
|
+
// 解析失败,忽略
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* 发送 JSON-RPC 请求
|
|
142
|
+
*/
|
|
143
|
+
_sendRequest(method, params = {}) {
|
|
144
|
+
return new Promise((resolve, reject) => {
|
|
145
|
+
const id = ++this._requestId;
|
|
146
|
+
this._pendingRequests.set(id, { resolve, reject });
|
|
147
|
+
|
|
148
|
+
const msg = JSON.stringify({
|
|
149
|
+
jsonrpc: '2.0',
|
|
150
|
+
id,
|
|
151
|
+
method,
|
|
152
|
+
params,
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
this.process.stdin.write(msg + '\n');
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* 发送 JSON-RPC 通知(无需响应)
|
|
161
|
+
*/
|
|
162
|
+
_sendNotification(method, params = {}) {
|
|
163
|
+
const msg = JSON.stringify({
|
|
164
|
+
jsonrpc: '2.0',
|
|
165
|
+
method,
|
|
166
|
+
params,
|
|
167
|
+
});
|
|
168
|
+
this.process.stdin.write(msg + '\n');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* 发现服务器提供的工具
|
|
173
|
+
*/
|
|
174
|
+
async _discoverTools() {
|
|
175
|
+
try {
|
|
176
|
+
const result = await this._sendRequest('tools/list', {});
|
|
177
|
+
this.tools = (result.tools || []).map(tool => ({
|
|
178
|
+
...tool,
|
|
179
|
+
_mcpServer: this.name,
|
|
180
|
+
}));
|
|
181
|
+
} catch {
|
|
182
|
+
this.tools = [];
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* 调用一个工具
|
|
188
|
+
*/
|
|
189
|
+
async callTool(toolName, args) {
|
|
190
|
+
if (!this.connected) {
|
|
191
|
+
throw new Error(`MCP 服务器 ${this.name} 未连接`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const result = await this._sendRequest('tools/call', {
|
|
195
|
+
name: toolName,
|
|
196
|
+
arguments: args,
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// 提取文本内容
|
|
200
|
+
if (result.content && Array.isArray(result.content)) {
|
|
201
|
+
return result.content
|
|
202
|
+
.filter(c => c.type === 'text')
|
|
203
|
+
.map(c => c.text)
|
|
204
|
+
.join('\n');
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return JSON.stringify(result);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* 断开连接
|
|
212
|
+
*/
|
|
213
|
+
disconnect() {
|
|
214
|
+
if (this.process) {
|
|
215
|
+
this.process.stdin.end();
|
|
216
|
+
this.process.kill();
|
|
217
|
+
this.process = null;
|
|
218
|
+
}
|
|
219
|
+
this.connected = false;
|
|
220
|
+
this._pendingRequests.clear();
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ────────────────────── MCP 管理器 ──────────────────────
|
|
225
|
+
|
|
226
|
+
let connections = new Map();
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* 加载 MCP 配置
|
|
230
|
+
*/
|
|
231
|
+
async function loadMCPConfig() {
|
|
232
|
+
try {
|
|
233
|
+
const data = await fs.readFile(MCP_CONFIG_FILE, 'utf-8');
|
|
234
|
+
return JSON.parse(data);
|
|
235
|
+
} catch {
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* 连接所有配置的 MCP 服务器
|
|
242
|
+
*/
|
|
243
|
+
export async function connectMCPServers() {
|
|
244
|
+
const mcpConfig = await loadMCPConfig();
|
|
245
|
+
if (!mcpConfig?.mcpServers) return [];
|
|
246
|
+
|
|
247
|
+
const results = [];
|
|
248
|
+
|
|
249
|
+
for (const [name, serverConfig] of Object.entries(mcpConfig.mcpServers)) {
|
|
250
|
+
const conn = new MCPConnection(name, serverConfig);
|
|
251
|
+
try {
|
|
252
|
+
await conn.connect();
|
|
253
|
+
connections.set(name, conn);
|
|
254
|
+
results.push({
|
|
255
|
+
name,
|
|
256
|
+
status: 'connected',
|
|
257
|
+
tools: conn.tools.map(t => t.name),
|
|
258
|
+
});
|
|
259
|
+
} catch (err) {
|
|
260
|
+
results.push({
|
|
261
|
+
name,
|
|
262
|
+
status: 'failed',
|
|
263
|
+
error: err.message,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return results;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* 获取所有已连接服务器的工具列表
|
|
273
|
+
* 返回 OpenAI function calling 格式的工具定义
|
|
274
|
+
*/
|
|
275
|
+
export function getMCPToolDefinitions() {
|
|
276
|
+
const tools = [];
|
|
277
|
+
|
|
278
|
+
for (const [serverName, conn] of connections) {
|
|
279
|
+
if (!conn.connected) continue;
|
|
280
|
+
|
|
281
|
+
for (const tool of conn.tools) {
|
|
282
|
+
tools.push({
|
|
283
|
+
type: 'function',
|
|
284
|
+
function: {
|
|
285
|
+
name: `mcp_${serverName}_${tool.name}`,
|
|
286
|
+
description: `[MCP: ${serverName}] ${tool.description || tool.name}`,
|
|
287
|
+
parameters: tool.inputSchema || { type: 'object', properties: {} },
|
|
288
|
+
},
|
|
289
|
+
_mcpServer: serverName,
|
|
290
|
+
_mcpToolName: tool.name,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return tools;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* 判断一个工具名称是否是 MCP 工具
|
|
300
|
+
*/
|
|
301
|
+
export function isMCPTool(toolName) {
|
|
302
|
+
return toolName.startsWith('mcp_');
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* 执行 MCP 工具调用
|
|
307
|
+
*/
|
|
308
|
+
export async function executeMCPTool(toolName, args) {
|
|
309
|
+
// 解析:mcp_{serverName}_{toolName}
|
|
310
|
+
const parts = toolName.replace(/^mcp_/, '').split('_');
|
|
311
|
+
|
|
312
|
+
// 找到匹配的服务器
|
|
313
|
+
for (const [serverName, conn] of connections) {
|
|
314
|
+
if (toolName.startsWith(`mcp_${serverName}_`)) {
|
|
315
|
+
const actualToolName = toolName.replace(`mcp_${serverName}_`, '');
|
|
316
|
+
return await conn.callTool(actualToolName, args);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
throw new Error(`未找到 MCP 工具: ${toolName}`);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* 断开所有 MCP 连接
|
|
325
|
+
*/
|
|
326
|
+
export function disconnectAll() {
|
|
327
|
+
for (const conn of connections.values()) {
|
|
328
|
+
conn.disconnect();
|
|
329
|
+
}
|
|
330
|
+
connections.clear();
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* 获取 MCP 连接状态
|
|
335
|
+
*/
|
|
336
|
+
export function getMCPStatus() {
|
|
337
|
+
const status = [];
|
|
338
|
+
for (const [name, conn] of connections) {
|
|
339
|
+
status.push({
|
|
340
|
+
name,
|
|
341
|
+
connected: conn.connected,
|
|
342
|
+
toolCount: conn.tools.length,
|
|
343
|
+
tools: conn.tools.map(t => t.name),
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
return status;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* 检查是否有 MCP 配置
|
|
351
|
+
*/
|
|
352
|
+
export async function hasMCPConfig() {
|
|
353
|
+
try {
|
|
354
|
+
await fs.access(MCP_CONFIG_FILE);
|
|
355
|
+
return true;
|
|
356
|
+
} catch {
|
|
357
|
+
return false;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* 构建 MCP 状态提示词
|
|
363
|
+
*/
|
|
364
|
+
export function buildMCPPrompt() {
|
|
365
|
+
const status = getMCPStatus();
|
|
366
|
+
if (status.length === 0) return '';
|
|
367
|
+
|
|
368
|
+
const lines = ['你已连接以下 MCP 扩展服务器:\n'];
|
|
369
|
+
for (const s of status) {
|
|
370
|
+
const state = s.connected ? '✅' : '❌';
|
|
371
|
+
lines.push(`- ${state} **${s.name}** (${s.toolCount} 个工具): ${s.tools.join(', ')}`);
|
|
372
|
+
}
|
|
373
|
+
lines.push('\n使用 MCP 工具时,工具名格式为 `mcp_{服务器名}_{工具名}`。');
|
|
374
|
+
|
|
375
|
+
return lines.join('\n');
|
|
376
|
+
}
|