protocol-proxy 2.8.3 → 2.10.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.
@@ -148,13 +148,14 @@ function normalizeConfig(config) {
148
148
  ...config,
149
149
  providers: providers.map(normalizeProvider),
150
150
  proxies: proxies.map(normalizeProxy),
151
+ mcpServers: config?.mcpServers && typeof config.mcpServers === 'object' ? config.mcpServers : {},
151
152
  };
152
153
  }
153
154
 
154
155
  function loadConfig() {
155
156
  try {
156
157
  if (!fs.existsSync(CONFIG_PATH)) {
157
- configCache = { providers: [], proxies: [], settings: {} };
158
+ configCache = { providers: [], proxies: [], settings: {}, mcpServers: {} };
158
159
  return configCache;
159
160
  }
160
161
  const raw = fs.readFileSync(CONFIG_PATH, 'utf-8');
@@ -165,7 +166,7 @@ function loadConfig() {
165
166
  return configCache;
166
167
  } catch (err) {
167
168
  console.error('加载配置失败:', err.message);
168
- return configCache || { providers: [], proxies: [], settings: {} };
169
+ return configCache || { providers: [], proxies: [], settings: {}, mcpServers: {} };
169
170
  }
170
171
  }
171
172
 
@@ -353,6 +354,43 @@ function removeProxy(id) {
353
354
  return removed;
354
355
  }
355
356
 
357
+ // --- MCP Server CRUD ---
358
+
359
+ function getMcpServers() {
360
+ const config = loadConfig();
361
+ return config.mcpServers || {};
362
+ }
363
+
364
+ function getMcpServer(name) {
365
+ const servers = getMcpServers();
366
+ return servers[name] || null;
367
+ }
368
+
369
+ function addMcpServer(name, serverConfig) {
370
+ const config = loadConfig();
371
+ if (!config.mcpServers) config.mcpServers = {};
372
+ if (config.mcpServers[name]) return null;
373
+ config.mcpServers[name] = serverConfig;
374
+ saveConfig(config);
375
+ return serverConfig;
376
+ }
377
+
378
+ function updateMcpServer(name, updates) {
379
+ const config = loadConfig();
380
+ if (!config.mcpServers || !config.mcpServers[name]) return null;
381
+ config.mcpServers[name] = { ...config.mcpServers[name], ...updates };
382
+ saveConfig(config);
383
+ return config.mcpServers[name];
384
+ }
385
+
386
+ function removeMcpServer(name) {
387
+ const config = loadConfig();
388
+ if (!config.mcpServers || !config.mcpServers[name]) return false;
389
+ delete config.mcpServers[name];
390
+ saveConfig(config);
391
+ return true;
392
+ }
393
+
356
394
  module.exports = {
357
395
  loadConfig,
358
396
  saveConfig,
@@ -373,4 +411,9 @@ module.exports = {
373
411
  removeProxy,
374
412
  normalizeRoutingStrategy,
375
413
  normalizeProviderPool,
414
+ getMcpServers,
415
+ getMcpServer,
416
+ addMcpServer,
417
+ updateMcpServer,
418
+ removeMcpServer,
376
419
  };
@@ -0,0 +1,108 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const os = require('os');
4
+
5
+ const DATA_DIR = path.join(os.homedir(), '.protocol-proxy', 'conversations');
6
+
7
+ // 内存缓存:conversationId → { id, proxyId, messages, createdAt, lastActivity }
8
+ let conversations = {};
9
+
10
+ // debounce 写入:500ms 内同一 conv 只写一次
11
+ const pendingWrites = new Map(); // convId → timer
12
+
13
+ function scheduleSave(conv) {
14
+ if (pendingWrites.has(conv.id)) clearTimeout(pendingWrites.get(conv.id));
15
+ pendingWrites.set(conv.id, setTimeout(() => {
16
+ pendingWrites.delete(conv.id);
17
+ fs.writeFile(path.join(DATA_DIR, conv.id + '.json'), JSON.stringify(conv), 'utf8', () => {});
18
+ }, 500));
19
+ }
20
+
21
+ function saveImmediate(conv) {
22
+ if (pendingWrites.has(conv.id)) {
23
+ clearTimeout(pendingWrites.get(conv.id));
24
+ pendingWrites.delete(conv.id);
25
+ }
26
+ try {
27
+ fs.writeFileSync(path.join(DATA_DIR, conv.id + '.json'), JSON.stringify(conv), 'utf8');
28
+ } catch {}
29
+ }
30
+
31
+ function flushAll() {
32
+ for (const [id, timer] of pendingWrites) {
33
+ clearTimeout(timer);
34
+ const conv = conversations[id];
35
+ if (conv) {
36
+ try { fs.writeFileSync(path.join(DATA_DIR, id + '.json'), JSON.stringify(conv), 'utf8'); } catch {}
37
+ }
38
+ }
39
+ pendingWrites.clear();
40
+ }
41
+
42
+ function init() {
43
+ if (!fs.existsSync(DATA_DIR)) {
44
+ fs.mkdirSync(DATA_DIR, { recursive: true });
45
+ }
46
+ try {
47
+ const files = fs.readdirSync(DATA_DIR).filter(f => f.endsWith('.json'));
48
+ for (const file of files) {
49
+ try {
50
+ const data = JSON.parse(fs.readFileSync(path.join(DATA_DIR, file), 'utf8'));
51
+ if (data.id) conversations[data.id] = data;
52
+ } catch {}
53
+ }
54
+ } catch {}
55
+ }
56
+
57
+ function get(id) {
58
+ return conversations[id] || null;
59
+ }
60
+
61
+ function create(proxyId, maxConversations) {
62
+ // 超过最大会话数时删除最早的
63
+ if (maxConversations > 0) {
64
+ const all = list();
65
+ while (all.length >= maxConversations) {
66
+ const oldest = all.shift(); // list() 已按 lastActivity 升序
67
+ remove(oldest.id);
68
+ }
69
+ }
70
+ const id = 'conv-' + Date.now() + '-' + Math.random().toString(36).slice(2, 8);
71
+ const conv = { id, proxyId, messages: [], createdAt: Date.now(), lastActivity: Date.now() };
72
+ conversations[id] = conv;
73
+ saveImmediate(conv); // 新建立即写入
74
+ return conv;
75
+ }
76
+
77
+ function touch(conv) {
78
+ conv.lastActivity = Date.now();
79
+ scheduleSave(conv); // debounce 异步写入
80
+ }
81
+
82
+ function remove(id) {
83
+ delete conversations[id];
84
+ if (pendingWrites.has(id)) { clearTimeout(pendingWrites.get(id)); pendingWrites.delete(id); }
85
+ fs.unlink(path.join(DATA_DIR, id + '.json'), () => {});
86
+ }
87
+
88
+ // 返回会话列表(按 lastActivity 升序,不含 messages),用于前端展示
89
+ function list() {
90
+ return Object.values(conversations)
91
+ .map(c => ({
92
+ id: c.id,
93
+ proxyId: c.proxyId,
94
+ createdAt: c.createdAt,
95
+ lastActivity: c.lastActivity,
96
+ messageCount: (c.messages || []).length,
97
+ // 取第一条 user 消息作为标题预览
98
+ preview: ((c.messages || []).find(m => m.role === 'user')?.content || '').slice(0, 60),
99
+ }))
100
+ .sort((a, b) => a.lastActivity - b.lastActivity);
101
+ }
102
+
103
+ // 进程退出时确保所有 debounce 中的数据落盘
104
+ process.on('exit', flushAll);
105
+ process.on('SIGINT', () => { flushAll(); process.exit(0); });
106
+ process.on('SIGTERM', () => { flushAll(); process.exit(0); });
107
+
108
+ module.exports = { init, get, create, touch, remove, list, flushAll };
@@ -0,0 +1,423 @@
1
+ const { Client } = require('@modelcontextprotocol/sdk/client');
2
+ const { StdioClientTransport } = require('@modelcontextprotocol/sdk/client/stdio.js');
3
+ const { StreamableHTTPClientTransport } = require('@modelcontextprotocol/sdk/client/streamableHttp.js');
4
+ const { SSEClientTransport } = require('@modelcontextprotocol/sdk/client/sse.js');
5
+ const crypto = require('crypto');
6
+ const configStore = require('./config-store');
7
+
8
+ const CALL_TIMEOUT = 30000;
9
+ const INITIALIZE_TIMEOUT = 60000; // 首次连接需要下载依赖,延长超时
10
+ const LIST_TOOLS_TIMEOUT = 30000;
11
+ const MAX_RECONNECT_ATTEMPTS = 10;
12
+ const BASE_RECONNECT_DELAY = 5000;
13
+ const MAX_RECONNECT_DELAY = 60000;
14
+
15
+ // Map<name, { client, transport, config, status, tools, lastError, reconnectTimer, reconnectAttempts }>
16
+ const servers = new Map();
17
+ let onUpdateCallback = null;
18
+ let _defsCache = null;
19
+ let _handlerCache = null;
20
+
21
+ function sanitizeName(name) {
22
+ return name.replace(/[^a-zA-Z0-9_-]/g, '_').replace(/-/g, '_');
23
+ }
24
+
25
+ function notifyUpdate(name) {
26
+ _defsCache = null;
27
+ _handlerCache = null;
28
+ const entry = servers.get(name);
29
+ if (onUpdateCallback && entry) {
30
+ onUpdateCallback(name, {
31
+ status: entry.status,
32
+ toolCount: entry.tools.length,
33
+ lastError: entry.lastError,
34
+ });
35
+ }
36
+ }
37
+
38
+ function createTransport(name, config) {
39
+ if (config.url) {
40
+ const url = new URL(config.url);
41
+ const opts = {};
42
+ if (config.headers && Object.keys(config.headers).length) {
43
+ opts.requestInit = { headers: config.headers };
44
+ }
45
+ // 检测 SSE 端点(高德等使用 /sse 路径的服务)
46
+ if (config.url.includes('/sse')) {
47
+ console.log(`[mcp] ${name} 使用 SSE 传输模式`);
48
+ return new SSEClientTransport(url, opts);
49
+ }
50
+ return new StreamableHTTPClientTransport(url, opts);
51
+ }
52
+ if (config.command) {
53
+ return new StdioClientTransport({
54
+ command: config.command,
55
+ args: config.args || [],
56
+ env: config.env ? { ...process.env, ...config.env } : undefined,
57
+ cwd: config.cwd || undefined,
58
+ });
59
+ }
60
+ throw new Error('MCP 服务器配置需要 command(本地进程)或 url(远程 HTTP)');
61
+ }
62
+
63
+ async function connectServer(name, config) {
64
+ let entry = servers.get(name);
65
+ if (entry) {
66
+ // 清理旧连接
67
+ if (entry.reconnectTimer) {
68
+ clearTimeout(entry.reconnectTimer);
69
+ entry.reconnectTimer = null;
70
+ }
71
+ try {
72
+ if (entry.client) await entry.client.close().catch(() => {});
73
+ } catch (closeErr) { console.warn(`[mcp] 关闭旧连接 ${name} 失败: ${closeErr.message}`); }
74
+ }
75
+
76
+ entry = {
77
+ client: null,
78
+ transport: null,
79
+ config,
80
+ status: 'connecting',
81
+ tools: [],
82
+ lastError: null,
83
+ lastConnected: null,
84
+ reconnectTimer: null,
85
+ reconnectAttempts: 0,
86
+ configHash: null,
87
+ };
88
+ servers.set(name, entry);
89
+ notifyUpdate(name);
90
+
91
+ const connectWithRetry = async (transport, client, retries = 1) => {
92
+ const errors = [];
93
+ for (let i = 0; i <= retries; i++) {
94
+ try {
95
+ if (i > 0) console.log(`[mcp] ${name} 正在重连(第 ${i} 次)...`);
96
+ await Promise.race([
97
+ client.connect(transport),
98
+ new Promise((_, rej) => setTimeout(() => rej(new Error(`握手超时(${INITIALIZE_TIMEOUT / 1000}s)`)), INITIALIZE_TIMEOUT))
99
+ ]);
100
+ return true;
101
+ } catch (err) {
102
+ errors.push(err.message);
103
+ // 如果是 stdio 模式且首次超时,可能是正在下载依赖,给一次重试机会
104
+ if (config.command && i < retries && err.message.includes('超时')) {
105
+ console.log(`[mcp] ${name} 首次连接超时,可能是正在下载依赖,将重试...`);
106
+ continue;
107
+ }
108
+ throw err;
109
+ }
110
+ }
111
+ throw new Error(errors.join('; '));
112
+ };
113
+
114
+ try {
115
+ const transport = createTransport(name, config);
116
+ const client = new Client(
117
+ { name: 'protocol-proxy', version: '1.0.0' },
118
+ { capabilities: {} }
119
+ );
120
+
121
+ await connectWithRetry(transport, client, 1);
122
+
123
+ // 列出工具
124
+ let allTools = [];
125
+ let cursor;
126
+ do {
127
+ try {
128
+ const result = await Promise.race([
129
+ client.listTools(cursor ? { cursor } : undefined),
130
+ new Promise((_, rej) => setTimeout(() => rej(new Error(`列出工具超时(${LIST_TOOLS_TIMEOUT / 1000}s)`)), LIST_TOOLS_TIMEOUT))
131
+ ]);
132
+ allTools = allTools.concat(result.tools || []);
133
+ cursor = result.nextCursor;
134
+ } catch (err) {
135
+ throw new Error(`列出工具失败: ${err.message}`);
136
+ }
137
+ } while (cursor);
138
+
139
+ entry.client = client;
140
+ entry.transport = transport;
141
+ entry.tools = allTools;
142
+ entry.status = 'connected';
143
+ entry.lastConnected = new Date();
144
+ entry.lastError = null;
145
+ entry.reconnectAttempts = 0;
146
+ entry.configHash = configHashFn(config);
147
+
148
+ console.log(`[mcp] 已连接 ${name}: ${allTools.length} 个工具`);
149
+ notifyUpdate(name);
150
+ return allTools;
151
+ } catch (err) {
152
+ entry.status = 'error';
153
+ entry.lastError = err.message;
154
+ console.error(`[mcp] 连接 ${name} 失败: ${err.message}`);
155
+ notifyUpdate(name);
156
+ throw err;
157
+ }
158
+ }
159
+
160
+ async function disconnectServer(name) {
161
+ const entry = servers.get(name);
162
+ if (!entry) return;
163
+
164
+ if (entry.reconnectTimer) {
165
+ clearTimeout(entry.reconnectTimer);
166
+ entry.reconnectTimer = null;
167
+ }
168
+
169
+ try {
170
+ if (entry.client) await entry.client.close().catch(() => {});
171
+ } catch (closeErr) { console.warn(`[mcp] 断开 ${name} 时关闭连接失败: ${closeErr.message}`); }
172
+
173
+ entry.status = 'disconnected';
174
+ entry.client = null;
175
+ entry.transport = null;
176
+ entry.tools = [];
177
+ console.log(`[mcp] 已断开 ${name}`);
178
+ notifyUpdate(name);
179
+ }
180
+
181
+ async function reconnectServer(name) {
182
+ const entry = servers.get(name);
183
+ if (!entry) return;
184
+ const config = entry.config;
185
+ entry.reconnectAttempts = 0;
186
+ await disconnectServer(name);
187
+ await connectServer(name, config);
188
+ }
189
+
190
+ function scheduleReconnect(name) {
191
+ const entry = servers.get(name);
192
+ if (!entry || entry.reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) return;
193
+
194
+ if (entry.reconnectTimer) {
195
+ clearTimeout(entry.reconnectTimer);
196
+ entry.reconnectTimer = null;
197
+ }
198
+
199
+ const delay = Math.min(BASE_RECONNECT_DELAY * Math.pow(2, entry.reconnectAttempts), MAX_RECONNECT_DELAY);
200
+ entry.reconnectTimer = setTimeout(async () => {
201
+ entry.reconnectAttempts++;
202
+ try {
203
+ await connectServer(name, entry.config);
204
+ } catch {
205
+ scheduleReconnect(name);
206
+ }
207
+ }, delay);
208
+ console.log(`[mcp] ${name} 将在 ${Math.round(delay / 1000)}s 后重连 (第 ${entry.reconnectAttempts + 1} 次)`);
209
+ }
210
+
211
+ async function callTool(serverName, toolName, args) {
212
+ const entry = servers.get(serverName);
213
+ if (!entry || entry.status !== 'connected' || !entry.client) {
214
+ return { error: `MCP 服务 "${serverName}" 未连接` };
215
+ }
216
+
217
+ try {
218
+ const controller = new AbortController();
219
+ const callTimeout = entry.config.toolCallTimeoutMs || CALL_TIMEOUT;
220
+ const timer = setTimeout(() => controller.abort(), callTimeout);
221
+
222
+ let result;
223
+ try {
224
+ result = await entry.client.callTool({ name: toolName, arguments: args }, undefined, { signal: controller.signal });
225
+ } finally {
226
+ clearTimeout(timer);
227
+ }
228
+
229
+ if (result.isError) {
230
+ const text = (result.content || []).map(c => c.text || JSON.stringify(c)).join('\n');
231
+ return { error: text || 'MCP 工具执行失败' };
232
+ }
233
+
234
+ const text = (result.content || []).map(c => {
235
+ if (c.type === 'text') return c.text;
236
+ return JSON.stringify(c);
237
+ }).join('\n');
238
+
239
+ try { return JSON.parse(text); } catch { return { result: text }; }
240
+ } catch (err) {
241
+ if (err.name === 'AbortError') {
242
+ const timeout = entry.config.toolCallTimeoutMs || CALL_TIMEOUT;
243
+ return { error: `MCP 工具调用超时 (${timeout / 1000}s)` };
244
+ }
245
+ // 连接断开时尝试重连
246
+ if (entry.status === 'connected') {
247
+ entry.status = 'error';
248
+ entry.lastError = err.message;
249
+ notifyUpdate(serverName);
250
+ scheduleReconnect(serverName);
251
+ }
252
+ return { error: `MCP 工具调用失败: ${err.message}` };
253
+ }
254
+ }
255
+
256
+ function fixSchemaType(schema) {
257
+ if (!schema || typeof schema !== 'object') return schema;
258
+ // 修复非法的类型值
259
+ if (schema.type === 'bool') schema.type = 'boolean';
260
+ // 修复 properties 中每个属性的 type
261
+ if (schema.properties && typeof schema.properties === 'object') {
262
+ for (const key of Object.keys(schema.properties)) {
263
+ const prop = schema.properties[key];
264
+ if (prop && prop.type === 'bool') prop.type = 'boolean';
265
+ // 递归修复嵌套属性
266
+ if (prop && prop.properties) fixSchemaType(prop);
267
+ // 修复 items
268
+ if (prop && prop.items && prop.items.type === 'bool') prop.items.type = 'boolean';
269
+ }
270
+ }
271
+ return schema;
272
+ }
273
+
274
+ function getToolDefinitions() {
275
+ if (_defsCache) return _defsCache;
276
+ const defs = [];
277
+ for (const [serverName, entry] of servers) {
278
+ if (entry.status !== 'connected') continue;
279
+ const prefix = `mcp__${sanitizeName(serverName)}__`;
280
+ for (const tool of entry.tools) {
281
+ const rawSchema = tool.inputSchema || {};
282
+ const schema = fixSchemaType(JSON.parse(JSON.stringify(rawSchema))); // 深拷贝避免修改原对象
283
+ defs.push({
284
+ type: 'function',
285
+ function: {
286
+ name: `${prefix}${tool.name}`,
287
+ description: `[MCP:${serverName}] ${tool.description || tool.name}`,
288
+ parameters: {
289
+ type: schema.type || 'object',
290
+ properties: schema.properties || {},
291
+ required: schema.required || [],
292
+ },
293
+ },
294
+ });
295
+ }
296
+ }
297
+ _defsCache = defs;
298
+ return defs;
299
+ }
300
+
301
+ function getToolHandlerMap() {
302
+ if (_handlerCache) return _handlerCache;
303
+ const map = {};
304
+ for (const [serverName, entry] of servers) {
305
+ if (entry.status !== 'connected') continue;
306
+ const prefix = `mcp__${sanitizeName(serverName)}__`;
307
+ for (const tool of entry.tools) {
308
+ const fullName = `${prefix}${tool.name}`;
309
+ const sn = serverName;
310
+ const tn = tool.name;
311
+ map[fullName] = (args) => callTool(sn, tn, args);
312
+ }
313
+ }
314
+ _handlerCache = map;
315
+ return map;
316
+ }
317
+
318
+ function getStatus() {
319
+ return Array.from(servers.entries()).map(([name, entry]) => ({
320
+ name,
321
+ status: entry.status,
322
+ tools: entry.tools.map(t => ({ name: t.name, description: t.description || '' })),
323
+ lastError: entry.lastError,
324
+ lastConnected: entry.lastConnected,
325
+ transport: entry.config?.url ? 'http' : 'stdio',
326
+ }));
327
+ }
328
+
329
+ function getServerStatus(name) {
330
+ const entry = servers.get(name);
331
+ return entry ? entry.status : null;
332
+ }
333
+
334
+ function refreshTools() {
335
+ _defsCache = null;
336
+ _handlerCache = null;
337
+ return {
338
+ definitions: getToolDefinitions(),
339
+ handlers: getToolHandlerMap(),
340
+ };
341
+ }
342
+
343
+ function configHashFn(config) {
344
+ return crypto.createHash('md5').update(JSON.stringify(config)).digest('hex');
345
+ }
346
+
347
+ async function reconnectIfChanged(name, newConfig) {
348
+ const entry = servers.get(name);
349
+ if (!entry) {
350
+ await connectServer(name, newConfig);
351
+ return { changed: true };
352
+ }
353
+ const newHash = configHashFn(newConfig);
354
+ if (entry.configHash && entry.configHash === newHash) {
355
+ console.log(`[mcp] ${name} 配置未变更,跳过重连`);
356
+ return { changed: false };
357
+ }
358
+ await reconnectServer(name);
359
+ return { changed: true };
360
+ }
361
+
362
+ async function init({ onUpdate } = {}) {
363
+ onUpdateCallback = onUpdate || null;
364
+
365
+ const mcpServers = configStore.getMcpServers();
366
+ const names = Object.keys(mcpServers);
367
+
368
+ if (names.length === 0) {
369
+ console.log('[mcp] 未配置 MCP 服务器');
370
+ return;
371
+ }
372
+
373
+ console.log(`[mcp] 正在连接 ${names.length} 个 MCP 服务器...`);
374
+
375
+ const results = await Promise.allSettled(
376
+ names.map(name => {
377
+ const config = mcpServers[name];
378
+ if (config.enabled === false) {
379
+ servers.set(name, {
380
+ client: null, transport: null, config, status: 'disconnected',
381
+ tools: [], lastError: null, lastConnected: null,
382
+ reconnectTimer: null, reconnectAttempts: 0, configHash: null,
383
+ });
384
+ return Promise.resolve();
385
+ }
386
+ return connectServer(name, config).catch(() => {});
387
+ })
388
+ );
389
+
390
+ const connected = results.filter(r => r.status === 'fulfilled' && r.value).length;
391
+ console.log(`[mcp] 初始化完成: ${connected}/${names.length} 已连接`);
392
+ }
393
+
394
+ async function shutdown() {
395
+ for (const [name, entry] of servers) {
396
+ if (entry.reconnectTimer) {
397
+ clearTimeout(entry.reconnectTimer);
398
+ entry.reconnectTimer = null;
399
+ }
400
+ try {
401
+ if (entry.client) await entry.client.close().catch(() => {});
402
+ } catch (closeErr) { console.warn(`[mcp] 关闭 ${name} 失败: ${closeErr.message}`); }
403
+ }
404
+ servers.clear();
405
+ _defsCache = null;
406
+ _handlerCache = null;
407
+ console.log('[mcp] 已关闭所有 MCP 连接');
408
+ }
409
+
410
+ module.exports = {
411
+ init,
412
+ connectServer,
413
+ disconnectServer,
414
+ reconnectServer,
415
+ reconnectIfChanged,
416
+ callTool,
417
+ getToolDefinitions,
418
+ getToolHandlerMap,
419
+ getStatus,
420
+ getServerStatus,
421
+ refreshTools,
422
+ shutdown,
423
+ };
@@ -0,0 +1,94 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const os = require('os');
4
+
5
+ const INSTRUCTIONS_PATH = path.join(os.homedir(), '.protocol-proxy', 'instructions.md');
6
+ const MAX_INSTRUCTIONS_CHARS = 4000;
7
+
8
+ /**
9
+ * 读取用户自定义指令文件
10
+ * 不存在时返回空字符串
11
+ */
12
+ function loadUserInstructions() {
13
+ try {
14
+ if (!fs.existsSync(INSTRUCTIONS_PATH)) return '';
15
+ const content = fs.readFileSync(INSTRUCTIONS_PATH, 'utf8').trim();
16
+ if (!content) return '';
17
+ const truncated = content.length > MAX_INSTRUCTIONS_CHARS
18
+ ? content.slice(0, MAX_INSTRUCTIONS_CHARS) + '\n...(已截断)'
19
+ : content;
20
+ return `用户自定义指令:\n${truncated}`;
21
+ } catch {
22
+ return '';
23
+ }
24
+ }
25
+
26
+ /**
27
+ * 构建技能段落
28
+ */
29
+ function buildSkillsSection(skillStore) {
30
+ const skills = skillStore.getAvailableForChat();
31
+ const skillList = skills.length
32
+ ? skills.map(s => `- /${s.name}: ${s.description}`).join('\n')
33
+ : '(暂无可用技能)';
34
+ return `可用技能(当用户输入 /技能名 时,调用 invoke_skill 获取指令并遵循执行):\n${skillList}`;
35
+ }
36
+
37
+ /**
38
+ * 构建 MCP 工具段落
39
+ */
40
+ function buildMcpSection(mcpClient) {
41
+ const mcpStatus = mcpClient.getStatus();
42
+ const connected = mcpStatus.filter(s => s.status === 'connected' && s.tools.length);
43
+ const degraded = mcpStatus.filter(s => s.status !== 'connected');
44
+
45
+ let out = connected.length
46
+ ? connected.map(s => `- [${s.name}] ${s.tools.map(t => t.name + (t.description ? ': ' + t.description : '')).join(', ')}`).join('\n')
47
+ : '(暂无已连接的 MCP 服务)';
48
+
49
+ if (degraded.length) {
50
+ out += '\n\n⚠️ 以下 MCP 服务当前不可用,相关工具无法使用:\n'
51
+ + degraded.map(s => `- ${s.name}(${s.status}${s.lastError ? ': ' + s.lastError : ''})`).join('\n');
52
+ }
53
+
54
+ return `MCP 外部工具(通过 MCP 协议接入的第三方工具,名称以 mcp__ 开头):\n${out}`;
55
+ }
56
+
57
+ /**
58
+ * 组装完整系统提示词
59
+ * 工具信息由 TOOL_DEFINITIONS 通过 API tools 参数单独传递,不在提示词中重复
60
+ * @param {Object} opts
61
+ * @param {Object} opts.skillStore - skillStore 模块
62
+ * @param {Object} opts.mcpClient - mcpClient 模块
63
+ */
64
+ function buildSystemPrompt({ skillStore, mcpClient }) {
65
+ const now = new Date().toLocaleString('zh-CN', { hour12: false });
66
+
67
+ const sections = [
68
+ // 角色声明
69
+ `你是 Protocol Proxy 的智能助手,帮助管理员管理和运维代理系统。当前时间:${now}`,
70
+
71
+ // 行为规则
72
+ `规则:\n- 当用户询问系统状态、代理、供应商、日志、用量等运维相关问题时,调用工具获取实时数据后再回答\n- 当用户需要创建、修改、删除供应商或代理时,使用对应的管理工具直接操作\n- 当用户需要查看或修改文件、执行命令时,使用对应的文件和命令工具\n- 当用户只是打招呼、闲聊、或询问与系统无关的问题时,直接回答,不要调用工具\n- 不要凭空猜测系统状态,需要数据时必须调用工具\n- 执行写操作或危险命令前,先告知用户将要做什么并确认`,
73
+
74
+ // 职责
75
+ `你的职责:\n1. 回答关于代理配置和运行状态的问题\n2. 分析日志,指出异常和可能原因\n3. 根据数据给出优化建议(负载均衡、模型选择、故障切换策略)\n4. 帮助用户管理供应商、代理、MCP 服务器和技能\n5. 用自然语言解释技术问题\n6. 如果发现问题,给出具体的修复步骤`,
76
+
77
+ // 用户自定义指令(可选)
78
+ loadUserInstructions(),
79
+
80
+ // 技能列表
81
+ buildSkillsSection(skillStore),
82
+
83
+ // MCP 工具列表
84
+ buildMcpSection(mcpClient),
85
+
86
+ // 结束语
87
+ '请用中文回答,保持专业且易懂。',
88
+ ];
89
+
90
+ // 过滤空段落,用双换行拼接
91
+ return sections.filter(Boolean).join('\n\n');
92
+ }
93
+
94
+ module.exports = { buildSystemPrompt };