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.
package/server.js CHANGED
@@ -123,6 +123,7 @@ async function init() {
123
123
  const configStore = require('./lib/config-store');
124
124
  const proxyManager = require('./lib/proxy-manager');
125
125
  const statsStore = require('./lib/stats-store');
126
+ const mcpClient = require('./lib/mcp-client');
126
127
 
127
128
  const app = express();
128
129
  const PORT = process.env.ADMIN_PORT || 3000;
@@ -143,7 +144,7 @@ async function init() {
143
144
  }
144
145
 
145
146
  app.use(cors());
146
- app.use(express.json());
147
+ app.use(express.json({ limit: '10mb' }));
147
148
 
148
149
  // 访问日志
149
150
  app.use((req, res, next) => {
@@ -242,8 +243,150 @@ async function init() {
242
243
  : 'primary_fallback';
243
244
  }
244
245
 
246
+ // ==================== Token 估算与会话压缩 ====================
247
+
248
+ function estimateMessageTokens(msg) {
249
+ const len = (s) => (typeof s === 'string' ? s.length : JSON.stringify(s || '').length);
250
+ let chars = 0;
251
+ if (typeof msg.content === 'string') chars += len(msg.content);
252
+ else if (Array.isArray(msg.content)) {
253
+ for (const block of msg.content) {
254
+ // 多模态格式:只取文本内容,不序列化整个对象
255
+ if (typeof block === 'string') chars += len(block);
256
+ else if (block?.text) chars += len(block.text);
257
+ else if (block?.content) chars += len(block.content);
258
+ else chars += len(block); // fallback
259
+ }
260
+ }
261
+ if (msg.reasoning_content) chars += len(msg.reasoning_content);
262
+ if (msg.tool_calls) {
263
+ for (const tc of msg.tool_calls) {
264
+ chars += len(tc.function?.name || '') + len(tc.function?.arguments || '');
265
+ }
266
+ }
267
+ // chars/2 对中文更保守(中文 ~1-2 token/字),宁可高估触发压缩也别低估撑爆上下文
268
+ return Math.ceil(chars / 2) + 4;
269
+ }
270
+
271
+ function estimateConversationTokens(messages) {
272
+ return messages.reduce((sum, m) => sum + estimateMessageTokens(m), 0);
273
+ }
274
+
275
+ async function compressConversation(conv, maxContext, proxyUrl, proxyHeaders, defaultModel) {
276
+ const messages = conv.messages;
277
+ const PRESERVE_RECENT = 6;
278
+
279
+ // 提取之前的压缩摘要(存储在 conv.compressionSummary 中)
280
+ let existingSummary = conv.compressionSummary || '';
281
+
282
+ // 分割:旧消息(压缩)和新消息(保留)
283
+ let keepFrom = messages.length - PRESERVE_RECENT;
284
+ // 边界处理:向后扫描,不拆开 assistant(tool_calls) + tool 配对
285
+ while (keepFrom > 0) {
286
+ const msg = messages[keepFrom];
287
+ if (msg?.role === 'tool') {
288
+ let j = keepFrom - 1;
289
+ while (j > 0 && messages[j]?.role === 'tool') j--;
290
+ if (messages[j]?.role === 'assistant' && messages[j]?.tool_calls) {
291
+ keepFrom = j;
292
+ }
293
+ break;
294
+ }
295
+ break;
296
+ }
297
+
298
+ const oldMessages = messages.slice(0, keepFrom);
299
+ const recentMessages = messages.slice(keepFrom);
300
+
301
+ if (oldMessages.length === 0) return null;
302
+
303
+ // 构建启发式摘要信息
304
+ const userMsgs = oldMessages.filter(m => m.role === 'user').length;
305
+ const assistantMsgs = oldMessages.filter(m => m.role === 'assistant').length;
306
+ const toolMsgs = oldMessages.filter(m => m.role === 'tool').length;
307
+ const toolNames = [...new Set(
308
+ oldMessages.filter(m => m.tool_calls).flatMap(m => m.tool_calls.map(tc => tc.function?.name)).filter(Boolean)
309
+ )];
310
+
311
+ const stats = [
312
+ `- 范围: ${oldMessages.length} 条旧消息 (user=${userMsgs}, assistant=${assistantMsgs}, tool=${toolMsgs})`,
313
+ toolNames.length > 0 ? `- 使用的工具: ${toolNames.join(', ')}` : null,
314
+ existingSummary ? `- 之前的摘要:\n${existingSummary}` : null,
315
+ ].filter(Boolean).join('\n');
316
+
317
+ const recentUserMsgs = oldMessages.filter(m => m.role === 'user').slice(-3)
318
+ .map(m => typeof m.content === 'string' ? m.content.slice(0, 200) : '').filter(Boolean);
319
+
320
+ // 调用 LLM 生成摘要
321
+ const compressPrompt = `请将以下对话历史压缩为简洁的摘要。保留所有关键信息:用户的问题意图、发现的问题、工具调用的关键结果、得出的结论和建议。
322
+
323
+ 对话统计:
324
+ ${stats}
325
+
326
+ 最近的用户问题:
327
+ ${recentUserMsgs.map((m, i) => `${i + 1}. ${m}`).join('\n')}
328
+
329
+ 请用中文输出摘要,格式:
330
+ 1. 用户的主要目标/问题
331
+ 2. 已完成的调查/操作
332
+ 3. 关键发现和结论
333
+ 4. 未完成的工作(如有)
334
+
335
+ 摘要控制在 500 字以内。`;
336
+
337
+ let summary;
338
+ try {
339
+ const res = await fetch(proxyUrl, {
340
+ method: 'POST',
341
+ headers: proxyHeaders,
342
+ signal: AbortSignal.timeout(60000),
343
+ body: JSON.stringify({
344
+ model: defaultModel || 'gpt-4o',
345
+ messages: [
346
+ { role: 'system', content: '你是一个对话摘要助手。简洁准确地总结对话要点。' },
347
+ { role: 'user', content: compressPrompt },
348
+ ],
349
+ max_tokens: 1024,
350
+ stream: false,
351
+ }),
352
+ });
353
+ if (res.ok) {
354
+ const data = await res.json();
355
+ summary = data.choices?.[0]?.message?.content || '';
356
+ }
357
+ } catch (err) {
358
+ logger.log(`[compress] LLM 摘要失败: ${err.message}`);
359
+ }
360
+
361
+ // LLM 失败 → 启发式降级
362
+ if (!summary) {
363
+ const lastAssistant = oldMessages.filter(m => m.role === 'assistant' && m.content).pop();
364
+ const userQuestions = oldMessages.filter(m => m.role === 'user')
365
+ .map(m => typeof m.content === 'string' ? m.content.slice(0, 100) : '')
366
+ .filter(Boolean).slice(-3);
367
+ summary = stats +
368
+ (userQuestions.length ? '\n- 最近用户问题:\n' + userQuestions.map((q, i) => ` ${i + 1}. ${q}`).join('\n') : '') +
369
+ '\n- 最近内容: ' + (lastAssistant?.content || '').slice(0, 300);
370
+ logger.log('[compress] 使用启发式降级摘要');
371
+ }
372
+
373
+ // 重建消息数组(不含 system 消息,由 buildMessages() 负责注入)
374
+ const newMessages = [...recentMessages];
375
+ const newTokens = estimateConversationTokens(newMessages);
376
+ return { messages: newMessages, summary, removedCount: oldMessages.length, newTokens };
377
+ }
378
+
245
379
  // ==================== 助手工具定义与执行器 ====================
246
380
 
381
+ const MAX_TOOL_OUTPUT = 16384; // 16KB — 防止工具输出撑爆 LLM 上下文
382
+
383
+ function truncateOutput(obj) {
384
+ const str = typeof obj === 'string' ? obj : JSON.stringify(obj);
385
+ if (str.length <= MAX_TOOL_OUTPUT) return obj;
386
+ const truncated = str.slice(0, MAX_TOOL_OUTPUT);
387
+ return { _truncated: true, _original_bytes: str.length, _preview: truncated + '\n... [截断,原始输出 ' + str.length + ' 字符]' };
388
+ }
389
+
247
390
  const TOOL_DEFINITIONS = [
248
391
  {
249
392
  type: 'function',
@@ -438,174 +581,1184 @@ async function init() {
438
581
  },
439
582
  },
440
583
  },
441
- ];
442
-
443
- const TOOL_HANDLERS = {
444
- get_system_status: async () => {
445
- const proxies = configStore.getProxies().map(p => {
446
- const provider = configStore.getProviderById(p.providerId);
447
- return { name: p.name, port: p.port, running: proxyManager.isRunning(p.id), providerName: provider?.name || '' };
448
- });
449
- return { proxies, providerCount: configStore.getProviders().length, uptime: Math.floor(process.uptime()) };
584
+ {
585
+ type: 'function',
586
+ function: {
587
+ name: 'edit_file',
588
+ description: '精确替换文件中的字符串。比 write_file 更安全,只替换匹配的内容,不会覆盖整个文件。',
589
+ parameters: {
590
+ type: 'object',
591
+ properties: {
592
+ path: { type: 'string', description: '文件路径' },
593
+ old_string: { type: 'string', description: '要被替换的原始字符串(必须精确匹配)' },
594
+ new_string: { type: 'string', description: '替换后的新字符串' },
595
+ replace_all: { type: 'boolean', description: '是否替换所有匹配项,默认 false(只替换第一个)' },
596
+ },
597
+ required: ['path', 'old_string', 'new_string'],
598
+ },
599
+ },
450
600
  },
451
-
452
- get_providers: async () => {
453
- return configStore.getProviders().map(p => {
454
- const h = keyHealth.get(p.id);
455
- let healthStatus = '未检测';
456
- if (h) {
457
- const ok = h.keys?.filter(k => k.ok).length || 0;
458
- const total = h.keys?.length || 0;
459
- healthStatus = h.status === 'healthy' ? `健康 (${ok}/${total})` :
460
- h.status === 'partial' ? `部分异常 (${ok}/${total})` :
461
- h.status === 'unhealthy' ? `异常 (${ok}/${total})` : '未检测';
462
- }
463
- return { id: p.id, name: p.name, url: p.url, protocol: p.protocol, keyCount: (p.apiKeys || []).length, health: healthStatus };
464
- });
601
+ {
602
+ type: 'function',
603
+ function: {
604
+ name: 'grep_search',
605
+ description: '在文件内容中搜索正则表达式模式。用于查找代码、日志关键字等。',
606
+ parameters: {
607
+ type: 'object',
608
+ properties: {
609
+ pattern: { type: 'string', description: '正则表达式模式' },
610
+ path: { type: 'string', description: '搜索目录或文件路径,默认当前工作目录' },
611
+ glob: { type: 'string', description: '文件名过滤,如 "*.js" "*.log"' },
612
+ max_results: { type: 'number', description: '最大返回匹配数,默认 50' },
613
+ },
614
+ required: ['pattern'],
615
+ },
616
+ },
465
617
  },
466
-
467
- get_provider: async (args) => {
468
- const p = configStore.getProviderById(args.providerId);
469
- if (!p) return { error: `供应商 ${args.providerId} 不存在` };
470
- const h = keyHealth.get(p.id);
471
- return { id: p.id, name: p.name, url: p.url, protocol: p.protocol, apiKeys: (p.apiKeys || []).map((k, i) => ({ index: i, alias: k.alias || '', enabled: k.enabled !== false })), health: h || null };
618
+ {
619
+ type: 'function',
620
+ function: {
621
+ name: 'invoke_skill',
622
+ description: '调用指定的技能,获取其指令内容。当用户输入 /技能名 或需要执行预定义流程时使用。',
623
+ parameters: {
624
+ type: 'object',
625
+ properties: {
626
+ name: { type: 'string', description: '技能名称' },
627
+ },
628
+ required: ['name'],
629
+ },
630
+ },
472
631
  },
473
-
474
- get_proxies: async () => {
475
- return configStore.getProxies().map(p => {
476
- const provider = configStore.getProviderById(p.providerId);
477
- return { id: p.id, name: p.name, port: p.port, running: proxyManager.isRunning(p.id), providerName: provider?.name || '', protocol: provider?.protocol || '', defaultModel: p.defaultModel || '', routingStrategy: p.routingStrategy || 'primary_fallback' };
478
- });
632
+ // --- 供应商管理 ---
633
+ {
634
+ type: 'function',
635
+ function: {
636
+ name: 'create_provider',
637
+ description: '创建新的供应商。需提供名称、URL 和协议(默认自动检测)。',
638
+ parameters: {
639
+ type: 'object',
640
+ properties: {
641
+ name: { type: 'string', description: '供应商名称' },
642
+ url: { type: 'string', description: '供应商 API 地址' },
643
+ protocol: { type: 'string', enum: ['openai', 'anthropic', 'gemini'], description: '协议类型,默认自动检测' },
644
+ apiKey: { type: 'string', description: 'API Key(单个)' },
645
+ apiKeys: { type: 'array', items: { type: 'object', properties: { key: { type: 'string' }, alias: { type: 'string' } } }, description: '多个 API Key 数组' },
646
+ models: { type: 'array', items: { type: 'string' }, description: '可用模型列表' },
647
+ },
648
+ required: ['name', 'url'],
649
+ },
650
+ },
479
651
  },
480
-
481
- get_proxy: async (args) => {
482
- const p = configStore.getProxyById(args.proxyId);
483
- if (!p) return { error: `代理 ${args.proxyId} 不存在` };
484
- const provider = configStore.getProviderById(p.providerId);
485
- return { id: p.id, name: p.name, port: p.port, running: proxyManager.isRunning(p.id), providerName: provider?.name || '', protocol: provider?.protocol || '', defaultModel: p.defaultModel || '', routingStrategy: p.routingStrategy || 'primary_fallback', requireAuth: !!p.requireAuth };
652
+ {
653
+ type: 'function',
654
+ function: {
655
+ name: 'update_provider',
656
+ description: '更新供应商配置。只传需要修改的字段即可。',
657
+ parameters: {
658
+ type: 'object',
659
+ properties: {
660
+ providerId: { type: 'string', description: '供应商 ID' },
661
+ name: { type: 'string', description: '新的名称' },
662
+ url: { type: 'string', description: '新的 URL' },
663
+ protocol: { type: 'string', enum: ['openai', 'anthropic', 'gemini'], description: '新的协议' },
664
+ apiKey: { type: 'string', description: '新的 API Key' },
665
+ models: { type: 'array', items: { type: 'string' }, description: '新的模型列表' },
666
+ },
667
+ required: ['providerId'],
668
+ },
669
+ },
670
+ },
671
+ {
672
+ type: 'function',
673
+ function: {
674
+ name: 'delete_provider',
675
+ description: '删除供应商。如果有代理正在使用该供应商则无法删除。',
676
+ parameters: {
677
+ type: 'object',
678
+ properties: {
679
+ providerId: { type: 'string', description: '供应商 ID' },
680
+ },
681
+ required: ['providerId'],
682
+ },
683
+ },
684
+ },
685
+ {
686
+ type: 'function',
687
+ function: {
688
+ name: 'test_provider_keys',
689
+ description: '测试供应商的 API Key 是否可用,返回每个 Key 的连通状态和延迟。',
690
+ parameters: {
691
+ type: 'object',
692
+ properties: {
693
+ providerId: { type: 'string', description: '供应商 ID' },
694
+ },
695
+ required: ['providerId'],
696
+ },
697
+ },
698
+ },
699
+ {
700
+ type: 'function',
701
+ function: {
702
+ name: 'get_provider_models',
703
+ description: '从供应商 API 拉取实际可用的模型列表。',
704
+ parameters: {
705
+ type: 'object',
706
+ properties: {
707
+ providerId: { type: 'string', description: '供应商 ID' },
708
+ },
709
+ required: ['providerId'],
710
+ },
711
+ },
712
+ },
713
+ // --- 代理管理 ---
714
+ {
715
+ type: 'function',
716
+ function: {
717
+ name: 'create_proxy',
718
+ description: '创建新代理并自动启动。需要指定名称、端口和关联的供应商 ID。',
719
+ parameters: {
720
+ type: 'object',
721
+ properties: {
722
+ name: { type: 'string', description: '代理名称' },
723
+ port: { type: 'number', description: '监听端口(不能与已有代理冲突)' },
724
+ providerId: { type: 'string', description: '关联的供应商 ID' },
725
+ defaultModel: { type: 'string', description: '默认模型名' },
726
+ routingStrategy: { type: 'string', enum: ['primary_fallback', 'round_robin', 'weighted', 'fastest'], description: '路由策略' },
727
+ },
728
+ required: ['name', 'port', 'providerId'],
729
+ },
730
+ },
731
+ },
732
+ {
733
+ type: 'function',
734
+ function: {
735
+ name: 'update_proxy',
736
+ description: '更新代理配置。只传需要修改的字段即可。修改端口会自动重启代理。',
737
+ parameters: {
738
+ type: 'object',
739
+ properties: {
740
+ proxyId: { type: 'string', description: '代理 ID' },
741
+ name: { type: 'string', description: '新名称' },
742
+ port: { type: 'number', description: '新端口' },
743
+ providerId: { type: 'string', description: '新的供应商 ID' },
744
+ defaultModel: { type: 'string', description: '新的默认模型' },
745
+ routingStrategy: { type: 'string', enum: ['primary_fallback', 'round_robin', 'weighted', 'fastest'], description: '新的路由策略' },
746
+ },
747
+ required: ['proxyId'],
748
+ },
749
+ },
750
+ },
751
+ {
752
+ type: 'function',
753
+ function: {
754
+ name: 'delete_proxy',
755
+ description: '删除代理,会先停止其运行。',
756
+ parameters: {
757
+ type: 'object',
758
+ properties: {
759
+ proxyId: { type: 'string', description: '代理 ID' },
760
+ },
761
+ required: ['proxyId'],
762
+ },
763
+ },
764
+ },
765
+ {
766
+ type: 'function',
767
+ function: {
768
+ name: 'start_proxy',
769
+ description: '启动指定代理。',
770
+ parameters: {
771
+ type: 'object',
772
+ properties: {
773
+ proxyId: { type: 'string', description: '代理 ID' },
774
+ },
775
+ required: ['proxyId'],
776
+ },
777
+ },
778
+ },
779
+ {
780
+ type: 'function',
781
+ function: {
782
+ name: 'stop_proxy',
783
+ description: '停止指定代理。',
784
+ parameters: {
785
+ type: 'object',
786
+ properties: {
787
+ proxyId: { type: 'string', description: '代理 ID' },
788
+ },
789
+ required: ['proxyId'],
790
+ },
791
+ },
792
+ },
793
+ {
794
+ type: 'function',
795
+ function: {
796
+ name: 'start_all_proxies',
797
+ description: '批量启动所有代理。已在运行中的会跳过。',
798
+ parameters: { type: 'object', properties: {}, required: [] },
799
+ },
800
+ },
801
+ {
802
+ type: 'function',
803
+ function: {
804
+ name: 'stop_all_proxies',
805
+ description: '批量停止所有运行中的代理。',
806
+ parameters: { type: 'object', properties: {}, required: [] },
807
+ },
808
+ },
809
+ // --- MCP 服务器管理 ---
810
+ {
811
+ type: 'function',
812
+ function: {
813
+ name: 'get_mcp_servers',
814
+ description: '获取所有 MCP 服务器列表及运行状态。',
815
+ parameters: { type: 'object', properties: {}, required: [] },
816
+ },
817
+ },
818
+ {
819
+ type: 'function',
820
+ function: {
821
+ name: 'add_mcp_server',
822
+ description: '添加新的 MCP 服务器。本地进程用 command,远程服务用 url。',
823
+ parameters: {
824
+ type: 'object',
825
+ properties: {
826
+ name: { type: 'string', description: '服务名称' },
827
+ command: { type: 'string', description: '本地进程启动命令(如 npx、uvx)' },
828
+ args: { type: 'array', items: { type: 'string' }, description: '命令参数' },
829
+ env: { type: 'object', description: '环境变量' },
830
+ url: { type: 'string', description: '远程 MCP 服务 URL' },
831
+ headers: { type: 'object', description: 'HTTP 请求头' },
832
+ },
833
+ required: ['name'],
834
+ },
835
+ },
836
+ },
837
+ {
838
+ type: 'function',
839
+ function: {
840
+ name: 'update_mcp_server',
841
+ description: '更新 MCP 服务器配置。',
842
+ parameters: {
843
+ type: 'object',
844
+ properties: {
845
+ name: { type: 'string', description: '服务名称' },
846
+ command: { type: 'string', description: '新的启动命令' },
847
+ args: { type: 'array', items: { type: 'string' }, description: '新的参数' },
848
+ env: { type: 'object', description: '新的环境变量' },
849
+ url: { type: 'string', description: '新的 URL' },
850
+ enabled: { type: 'boolean', description: '是否启用' },
851
+ },
852
+ required: ['name'],
853
+ },
854
+ },
855
+ },
856
+ {
857
+ type: 'function',
858
+ function: {
859
+ name: 'delete_mcp_server',
860
+ description: '删除 MCP 服务器,会先断开连接。',
861
+ parameters: {
862
+ type: 'object',
863
+ properties: {
864
+ name: { type: 'string', description: '服务名称' },
865
+ },
866
+ required: ['name'],
867
+ },
868
+ },
869
+ },
870
+ {
871
+ type: 'function',
872
+ function: {
873
+ name: 'connect_mcp_server',
874
+ description: '连接指定的 MCP 服务器。',
875
+ parameters: {
876
+ type: 'object',
877
+ properties: {
878
+ name: { type: 'string', description: '服务名称' },
879
+ },
880
+ required: ['name'],
881
+ },
882
+ },
883
+ },
884
+ {
885
+ type: 'function',
886
+ function: {
887
+ name: 'disconnect_mcp_server',
888
+ description: '断开指定的 MCP 服务器。',
889
+ parameters: {
890
+ type: 'object',
891
+ properties: {
892
+ name: { type: 'string', description: '服务名称' },
893
+ },
894
+ required: ['name'],
895
+ },
896
+ },
897
+ },
898
+ {
899
+ type: 'function',
900
+ function: {
901
+ name: 'get_mcp_tools',
902
+ description: '获取所有已连接 MCP 服务器提供的工具列表。',
903
+ parameters: { type: 'object', properties: {}, required: [] },
904
+ },
905
+ },
906
+ // --- 技能管理 ---
907
+ {
908
+ type: 'function',
909
+ function: {
910
+ name: 'get_skills',
911
+ description: '获取所有已创建的技能列表。',
912
+ parameters: { type: 'object', properties: {}, required: [] },
913
+ },
914
+ },
915
+ {
916
+ type: 'function',
917
+ function: {
918
+ name: 'create_skill',
919
+ description: '创建新技能。技能是预定义的指令模板,用户可通过 /技能名 触发。',
920
+ parameters: {
921
+ type: 'object',
922
+ properties: {
923
+ name: { type: 'string', description: '技能名称(英文、数字、下划线、连字符)' },
924
+ description: { type: 'string', description: '技能描述' },
925
+ content: { type: 'string', description: '技能指令内容(Markdown 格式)' },
926
+ },
927
+ required: ['name', 'content'],
928
+ },
929
+ },
930
+ },
931
+ {
932
+ type: 'function',
933
+ function: {
934
+ name: 'update_skill',
935
+ description: '更新现有技能的描述或指令内容。',
936
+ parameters: {
937
+ type: 'object',
938
+ properties: {
939
+ name: { type: 'string', description: '技能名称' },
940
+ description: { type: 'string', description: '新的描述' },
941
+ content: { type: 'string', description: '新的指令内容' },
942
+ },
943
+ required: ['name'],
944
+ },
945
+ },
946
+ },
947
+ {
948
+ type: 'function',
949
+ function: {
950
+ name: 'delete_skill',
951
+ description: '删除技能。系统级技能不可删除。',
952
+ parameters: {
953
+ type: 'object',
954
+ properties: {
955
+ name: { type: 'string', description: '技能名称' },
956
+ },
957
+ required: ['name'],
958
+ },
959
+ },
960
+ },
961
+ // --- 配置管理 ---
962
+ {
963
+ type: 'function',
964
+ function: {
965
+ name: 'export_config',
966
+ description: '导出当前系统配置(供应商和代理),可用于备份或迁移。',
967
+ parameters: { type: 'object', properties: {}, required: [] },
968
+ },
969
+ },
970
+ {
971
+ type: 'function',
972
+ function: {
973
+ name: 'import_config',
974
+ description: '导入配置。overwrite 模式替换全部,merge 模式按 ID 合并。',
975
+ parameters: {
976
+ type: 'object',
977
+ properties: {
978
+ config: {
979
+ type: 'object',
980
+ description: '配置对象,包含 providers 和 proxies 数组',
981
+ properties: {
982
+ providers: { type: 'array', description: '供应商数组' },
983
+ proxies: { type: 'array', description: '代理数组' },
984
+ },
985
+ },
986
+ mode: { type: 'string', enum: ['overwrite', 'merge'], description: '导入模式' },
987
+ },
988
+ required: ['config', 'mode'],
989
+ },
990
+ },
991
+ },
992
+ {
993
+ type: 'function',
994
+ function: {
995
+ name: 'rollback_config',
996
+ description: '回滚到指定的配置快照。',
997
+ parameters: {
998
+ type: 'object',
999
+ properties: {
1000
+ file: { type: 'string', description: '快照文件名(从 get_config_history 获取)' },
1001
+ },
1002
+ required: ['file'],
1003
+ },
1004
+ },
1005
+ },
1006
+ // --- 系统操作 ---
1007
+ {
1008
+ type: 'function',
1009
+ function: {
1010
+ name: 'update_settings',
1011
+ description: '更新系统设置。传入需要修改的键值对即可。',
1012
+ parameters: {
1013
+ type: 'object',
1014
+ properties: {
1015
+ settings: { type: 'object', description: '要更新的设置键值对' },
1016
+ },
1017
+ required: ['settings'],
1018
+ },
1019
+ },
1020
+ },
1021
+ {
1022
+ type: 'function',
1023
+ function: {
1024
+ name: 'trigger_key_health_check',
1025
+ description: '手动触发所有供应商的 API Key 健康检查。',
1026
+ parameters: { type: 'object', properties: {}, required: [] },
1027
+ },
1028
+ },
1029
+ {
1030
+ type: 'function',
1031
+ function: {
1032
+ name: 'check_health',
1033
+ description: '系统健康检查,返回版本、运行时长、代理状态。',
1034
+ parameters: { type: 'object', properties: {}, required: [] },
1035
+ },
1036
+ },
1037
+ ];
1038
+
1039
+ const TOOL_HANDLERS = {
1040
+ get_system_status: async () => {
1041
+ const proxies = configStore.getProxies().map(p => {
1042
+ const provider = configStore.getProviderById(p.providerId);
1043
+ return { name: p.name, port: p.port, running: proxyManager.isRunning(p.id), providerName: provider?.name || '' };
1044
+ });
1045
+ return { proxies, providerCount: configStore.getProviders().length, uptime: Math.floor(process.uptime()) };
1046
+ },
1047
+
1048
+ get_providers: async () => {
1049
+ return configStore.getProviders().map(p => {
1050
+ const h = keyHealth.get(p.id);
1051
+ let healthStatus = '未检测';
1052
+ if (h) {
1053
+ const ok = h.keys?.filter(k => k.ok).length || 0;
1054
+ const total = h.keys?.length || 0;
1055
+ healthStatus = h.status === 'healthy' ? `健康 (${ok}/${total})` :
1056
+ h.status === 'partial' ? `部分异常 (${ok}/${total})` :
1057
+ h.status === 'unhealthy' ? `异常 (${ok}/${total})` : '未检测';
1058
+ }
1059
+ return { id: p.id, name: p.name, url: p.url, protocol: p.protocol, keyCount: (p.apiKeys || []).length, health: healthStatus };
1060
+ });
1061
+ },
1062
+
1063
+ get_provider: async (args) => {
1064
+ const p = configStore.getProviderById(args.providerId);
1065
+ if (!p) return { error: `供应商 ${args.providerId} 不存在` };
1066
+ const h = keyHealth.get(p.id);
1067
+ return { id: p.id, name: p.name, url: p.url, protocol: p.protocol, apiKeys: (p.apiKeys || []).map((k, i) => ({ index: i, alias: k.alias || '', enabled: k.enabled !== false })), health: h || null };
1068
+ },
1069
+
1070
+ get_proxies: async () => {
1071
+ return configStore.getProxies().map(p => {
1072
+ const provider = configStore.getProviderById(p.providerId);
1073
+ return { id: p.id, name: p.name, port: p.port, running: proxyManager.isRunning(p.id), providerName: provider?.name || '', protocol: provider?.protocol || '', defaultModel: p.defaultModel || '', routingStrategy: p.routingStrategy || 'primary_fallback' };
1074
+ });
1075
+ },
1076
+
1077
+ get_proxy: async (args) => {
1078
+ const p = configStore.getProxyById(args.proxyId);
1079
+ if (!p) return { error: `代理 ${args.proxyId} 不存在` };
1080
+ const provider = configStore.getProviderById(p.providerId);
1081
+ return { id: p.id, name: p.name, port: p.port, running: proxyManager.isRunning(p.id), providerName: provider?.name || '', protocol: provider?.protocol || '', defaultModel: p.defaultModel || '', routingStrategy: p.routingStrategy || 'primary_fallback', requireAuth: !!p.requireAuth };
1082
+ },
1083
+
1084
+ get_usage_stats: async (args) => {
1085
+ return statsStore.getStats({ range: args.range || 'daily', startDate: args.startDate, endDate: args.endDate, proxyId: args.proxyId });
1086
+ },
1087
+
1088
+ get_recent_requests: async (args) => {
1089
+ const limit = Math.min(Math.max(1, parseInt(args.limit) || 20), 100);
1090
+ return { entries: requestLog.getAll(limit) };
1091
+ },
1092
+
1093
+ get_system_logs: async (args) => {
1094
+ const limit = Math.min(Math.max(1, parseInt(args.limit) || 30), 100);
1095
+ try {
1096
+ const content = await fs.promises.readFile(logger.LOG_FILE, 'utf8');
1097
+ const allLines = content.split('\n').filter(l => l.trim());
1098
+ return { lines: allLines.slice(-limit) };
1099
+ } catch {
1100
+ return { lines: [] };
1101
+ }
1102
+ },
1103
+
1104
+ get_key_health: async () => {
1105
+ const result = {};
1106
+ for (const [providerId, health] of keyHealth) {
1107
+ result[providerId] = health;
1108
+ }
1109
+ return result;
1110
+ },
1111
+
1112
+ get_settings: async () => {
1113
+ return configStore.getSettings();
1114
+ },
1115
+
1116
+ get_config_history: async () => {
1117
+ return { snapshots: configStore.getSnapshots() };
1118
+ },
1119
+
1120
+ read_file: async (args) => {
1121
+ const filePath = path.resolve(args.path);
1122
+ try {
1123
+ // 二进制检测:检查前 8KB 是否含 NUL 字节
1124
+ const stat = await fs.promises.stat(filePath);
1125
+ const peekSize = Math.min(8192, stat.size);
1126
+ if (peekSize > 0) {
1127
+ const fd = await fs.promises.open(filePath, 'r');
1128
+ try {
1129
+ const buf = Buffer.alloc(peekSize);
1130
+ await fd.read(buf, 0, peekSize, 0);
1131
+ if (buf.includes(0)) {
1132
+ return { error: `二进制文件,无法以文本方式读取 (${filePath}, ${stat.size} bytes)` };
1133
+ }
1134
+ } finally {
1135
+ await fd.close();
1136
+ }
1137
+ }
1138
+ const content = await fs.promises.readFile(filePath, 'utf8');
1139
+ const lines = content.split('\n');
1140
+ const offset = Math.max(0, parseInt(args.offset) || 0);
1141
+ const limit = Math.min(Math.max(1, parseInt(args.limit) || 500), 2000);
1142
+ const sliced = lines.slice(offset, offset + limit);
1143
+ return { content: sliced.join('\n'), totalLines: lines.length, offset, returnedLines: sliced.length };
1144
+ } catch (err) {
1145
+ return { error: err.message };
1146
+ }
1147
+ },
1148
+
1149
+ write_file: async (args) => {
1150
+ const filePath = path.resolve(args.path);
1151
+ try {
1152
+ await fs.promises.mkdir(path.dirname(filePath), { recursive: true });
1153
+ await fs.promises.writeFile(filePath, args.content, 'utf8');
1154
+ return { success: true, path: filePath, bytes: Buffer.byteLength(args.content, 'utf8') };
1155
+ } catch (err) {
1156
+ return { error: err.message };
1157
+ }
1158
+ },
1159
+
1160
+ list_directory: async (args) => {
1161
+ const dirPath = path.resolve(args.path || '.');
1162
+ try {
1163
+ const entries = await fs.promises.readdir(dirPath, { withFileTypes: true });
1164
+ return {
1165
+ path: dirPath,
1166
+ entries: entries.map(e => ({
1167
+ name: e.name,
1168
+ type: e.isDirectory() ? 'directory' : 'file',
1169
+ })),
1170
+ };
1171
+ } catch (err) {
1172
+ return { error: err.message };
1173
+ }
1174
+ },
1175
+
1176
+ search_files: async (args) => {
1177
+ const root = path.resolve(args.path || '.');
1178
+ const pattern = args.pattern;
1179
+ try {
1180
+ const results = [];
1181
+ const globToRegex = (g) => {
1182
+ const r = g.replace(/\*\*/g, '§GLOBSTAR§')
1183
+ .replace(/\*/g, '[^/]*')
1184
+ .replace(/\?/g, '[^/]')
1185
+ .replace(/§GLOBSTAR§/g, '.*');
1186
+ return new RegExp('^' + r + '$');
1187
+ };
1188
+ const regex = globToRegex(pattern);
1189
+ const walk = async (dir, rel) => {
1190
+ const entries = await fs.promises.readdir(dir, { withFileTypes: true });
1191
+ for (const e of entries) {
1192
+ const fullPath = path.join(dir, e.name);
1193
+ const relPath = rel ? `${rel}/${e.name}` : e.name;
1194
+ if (e.isDirectory()) {
1195
+ if (e.name === 'node_modules' || e.name === '.git') continue;
1196
+ await walk(fullPath, relPath);
1197
+ } else if (regex.test(relPath)) {
1198
+ results.push(relPath);
1199
+ }
1200
+ }
1201
+ };
1202
+ await walk(root, '');
1203
+ return { pattern, root, matches: results.slice(0, 200), total: results.length };
1204
+ } catch (err) {
1205
+ return { error: err.message };
1206
+ }
1207
+ },
1208
+
1209
+ execute_command: async (args) => {
1210
+ const timeout = Math.min(Math.max(1000, parseInt(args.timeout) || 30000), 120000);
1211
+ return new Promise((resolve) => {
1212
+ exec(args.command, { cwd: args.cwd || process.cwd(), timeout, maxBuffer: 1024 * 1024 }, (err, stdout, stderr) => {
1213
+ if (err) {
1214
+ resolve({ exitCode: err.code || 1, stdout: stdout || '', stderr: stderr || err.message });
1215
+ } else {
1216
+ resolve({ exitCode: 0, stdout: stdout || '', stderr: stderr || '' });
1217
+ }
1218
+ });
1219
+ });
1220
+ },
1221
+
1222
+ edit_file: async (args) => {
1223
+ const filePath = path.resolve(args.path);
1224
+ try {
1225
+ const content = await fs.promises.readFile(filePath, 'utf8');
1226
+ const { old_string, new_string } = args;
1227
+ if (old_string === new_string) return { error: 'old_string 和 new_string 不能相同' };
1228
+ if (!content.includes(old_string)) return { error: `文件中未找到匹配的字符串` };
1229
+ const replaceAll = !!args.replace_all;
1230
+ const newContent = replaceAll
1231
+ ? content.split(old_string).join(new_string)
1232
+ : content.replace(old_string, new_string);
1233
+ const count = replaceAll
1234
+ ? content.split(old_string).length - 1
1235
+ : 1;
1236
+ await fs.promises.writeFile(filePath, newContent, 'utf8');
1237
+ return { success: true, path: filePath, replacements: count };
1238
+ } catch (err) {
1239
+ return { error: err.message };
1240
+ }
1241
+ },
1242
+
1243
+ grep_search: async (args) => {
1244
+ const root = path.resolve(args.path || '.');
1245
+ const pattern = args.pattern;
1246
+ const maxResults = Math.min(Math.max(1, parseInt(args.max_results) || 50), 200);
1247
+ const globFilter = args.glob || '';
1248
+ try {
1249
+ const regex = new RegExp(pattern, 'gi');
1250
+ const results = [];
1251
+ const walk = async (dir) => {
1252
+ if (results.length >= maxResults) return;
1253
+ const entries = await fs.promises.readdir(dir, { withFileTypes: true });
1254
+ for (const e of entries) {
1255
+ if (results.length >= maxResults) break;
1256
+ const fullPath = path.join(dir, e.name);
1257
+ if (e.isDirectory()) {
1258
+ if (['node_modules', '.git', 'dist', 'build', '.next'].includes(e.name)) continue;
1259
+ await walk(fullPath);
1260
+ } else if (e.isFile()) {
1261
+ if (globFilter) {
1262
+ const ext = '.' + e.name.split('.').pop();
1263
+ if (!globFilter.includes(ext) && !globFilter.includes(e.name) && !globFilter.includes('*')) continue;
1264
+ }
1265
+ try {
1266
+ const content = await fs.promises.readFile(fullPath, 'utf8');
1267
+ const lines = content.split('\n');
1268
+ for (let i = 0; i < lines.length; i++) {
1269
+ if (results.length >= maxResults) break;
1270
+ if (regex.test(lines[i])) {
1271
+ results.push({
1272
+ file: path.relative(root, fullPath),
1273
+ line: i + 1,
1274
+ content: lines[i].trim().slice(0, 300),
1275
+ });
1276
+ regex.lastIndex = 0;
1277
+ }
1278
+ }
1279
+ } catch {}
1280
+ }
1281
+ }
1282
+ };
1283
+ await walk(root);
1284
+ return { pattern, matches: results, total: results.length };
1285
+ } catch (err) {
1286
+ return { error: err.message };
1287
+ }
1288
+ },
1289
+ invoke_skill: async (args) => {
1290
+ const skill = skillStore.get(args.name);
1291
+ if (!skill) return { error: `技能 "${args.name}" 不存在` };
1292
+ const result = { name: skill.name, description: skill.description, content: skill.content, dirPath: skill.dirPath };
1293
+ if (skill.scripts.length > 0) result.scripts = skill.scripts.map(f => `scripts/${f}`);
1294
+ if (skill.references.length > 0) result.references = skill.references.map(f => `reference/${f}`);
1295
+ // 读取 reference 文件内容(文本文件)
1296
+ for (const ref of skill.references) {
1297
+ try {
1298
+ const refPath = path.join(skill.dirPath, 'reference', ref);
1299
+ const stat = fs.statSync(refPath);
1300
+ if (stat.size < 50000) { // 只读小于 50KB 的文本文件
1301
+ result[`reference:${ref}`] = fs.readFileSync(refPath, 'utf8');
1302
+ }
1303
+ } catch {}
1304
+ }
1305
+ return result;
1306
+ },
1307
+
1308
+ // --- 供应商管理 ---
1309
+ create_provider: async (args) => {
1310
+ if (!args.name || !args.url) return { error: 'name 和 url 是必填项' };
1311
+ const provider = configStore.addProvider({
1312
+ name: args.name,
1313
+ url: args.url,
1314
+ protocol: args.protocol || (/anthropic/i.test(args.url) ? 'anthropic' : 'openai'),
1315
+ apiKey: args.apiKey || '',
1316
+ apiKeys: Array.isArray(args.apiKeys) ? args.apiKeys.filter(k => k && k.key && k.key.trim()) : [],
1317
+ models: args.models || [],
1318
+ });
1319
+ return { success: true, id: provider.id, name: provider.name };
1320
+ },
1321
+
1322
+ update_provider: async (args) => {
1323
+ const existing = configStore.getProviderById(args.providerId);
1324
+ if (!existing) return { error: `供应商 ${args.providerId} 不存在` };
1325
+ const updates = {};
1326
+ if (args.name !== undefined) updates.name = args.name;
1327
+ if (args.url !== undefined) updates.url = args.url;
1328
+ if (args.protocol !== undefined) updates.protocol = args.protocol;
1329
+ if (args.apiKey !== undefined && args.apiKey !== '') updates.apiKey = args.apiKey;
1330
+ if (args.apiKeys !== undefined) {
1331
+ updates.apiKeys = Array.isArray(args.apiKeys) ? args.apiKeys.filter(k => k && k.key && k.key.trim()) : [];
1332
+ }
1333
+ if (args.models !== undefined) updates.models = args.models;
1334
+ const updated = configStore.updateProvider(args.providerId, updates);
1335
+ // 同步更新引用此供应商的运行中代理
1336
+ const affectedProxies = configStore.getProxies().filter(p => p.providerId === args.providerId);
1337
+ for (const proxy of affectedProxies) {
1338
+ if (!proxyManager.isRunning(proxy.id)) continue;
1339
+ const target = resolveTarget(proxy);
1340
+ if (target) proxyManager.updateProxyConfig({ ...proxy, target });
1341
+ }
1342
+ return { success: true, id: updated.id, name: updated.name };
1343
+ },
1344
+
1345
+ delete_provider: async (args) => {
1346
+ const existing = configStore.getProviderById(args.providerId);
1347
+ if (!existing) return { error: `供应商 ${args.providerId} 不存在` };
1348
+ const inUse = configStore.getProxies().some(p => p.providerId === args.providerId);
1349
+ if (inUse) return { error: '该供应商正在被代理使用,无法删除' };
1350
+ configStore.removeProvider(args.providerId);
1351
+ return { success: true };
1352
+ },
1353
+
1354
+ test_provider_keys: async (args) => {
1355
+ const provider = configStore.getProviderById(args.providerId);
1356
+ if (!provider) return { error: `供应商 ${args.providerId} 不存在` };
1357
+ const existingKeys = provider.apiKeys || [];
1358
+ if (existingKeys.length === 0) return { ok: false, message: '没有可用的 API Key', results: [] };
1359
+ const protocol = provider.protocol || 'openai';
1360
+ const base = provider.url.replace(/\/$/, '');
1361
+ const hasV1Suffix = base.endsWith('/v1');
1362
+ const isAzure = protocol === 'openai' && !!provider.azureDeployment;
1363
+
1364
+ function buildTestUrl(key) {
1365
+ if (protocol === 'openai') {
1366
+ if (isAzure) {
1367
+ const ver = provider.azureApiVersion || '2024-02-01';
1368
+ return { url: `${base}/openai/deployments/${provider.azureDeployment}/models?api-version=${ver}`, opts: { headers: { 'api-key': key } } };
1369
+ }
1370
+ return { url: hasV1Suffix ? `${base}/models` : `${base}/v1/models`, opts: { headers: { 'Authorization': `Bearer ${key}` } } };
1371
+ }
1372
+ if (protocol === 'anthropic') {
1373
+ const testModel = (provider.models && provider.models[0]) || 'claude-3-haiku-20240307';
1374
+ return { url: hasV1Suffix ? `${base}/messages` : `${base}/v1/messages`, opts: { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-api-key': key, 'anthropic-version': '2023-06-01' }, body: JSON.stringify({ model: testModel, max_tokens: 1, messages: [{ role: 'user', content: 'hi' }] }) } };
1375
+ }
1376
+ if (protocol === 'gemini') return { url: `${base}/v1beta/models?key=${key}`, opts: {} };
1377
+ return null;
1378
+ }
1379
+
1380
+ const results = await Promise.all(existingKeys.map(async (k, i) => {
1381
+ const { url: testUrl, opts: fetchOpts } = buildTestUrl(k.key);
1382
+ if (!testUrl) return { ok: false, index: i, message: `不支持的协议: ${protocol}` };
1383
+ try {
1384
+ const startedAt = Date.now();
1385
+ const fetchRes = await fetch(testUrl, { ...fetchOpts, signal: AbortSignal.timeout(15000) });
1386
+ const latencyMs = Date.now() - startedAt;
1387
+ if (!fetchRes.ok) {
1388
+ const errText = await fetchRes.text().catch(() => '');
1389
+ const hint = fetchRes.status === 401 || fetchRes.status === 403 ? 'API Key 无效或无权限' : `HTTP ${fetchRes.status}`;
1390
+ return { ok: false, alias: k.alias || '', index: i, message: hint, latencyMs };
1391
+ }
1392
+ return { ok: true, alias: k.alias || '', index: i, latencyMs };
1393
+ } catch (err) {
1394
+ const msg = err.name === 'TimeoutError' ? '连接超时 (15s)' : `连接失败: ${err.message}`;
1395
+ return { ok: false, alias: k.alias || '', index: i, message: msg };
1396
+ }
1397
+ }));
1398
+ const passed = results.filter(r => r.ok).length;
1399
+ return { ok: passed === existingKeys.length, passed, failed: existingKeys.length - passed, results };
1400
+ },
1401
+
1402
+ get_provider_models: async (args) => {
1403
+ const provider = configStore.getProviderById(args.providerId);
1404
+ if (!provider) return { error: `供应商 ${args.providerId} 不存在` };
1405
+ const enabledKeys = (provider.apiKeys || []).filter(k => k.enabled !== false).map(k => k.key);
1406
+ if (enabledKeys.length === 0) return { error: '没有可用的 API Key' };
1407
+ const key = enabledKeys[0];
1408
+ const protocol = provider.protocol || 'openai';
1409
+ const base = provider.url.replace(/\/$/, '');
1410
+ const hasV1Suffix = base.endsWith('/v1');
1411
+ const isAzure = protocol === 'openai' && !!provider.azureDeployment;
1412
+ let url, headers = {};
1413
+ if (protocol === 'openai') {
1414
+ if (isAzure) {
1415
+ const ver = provider.azureApiVersion || '2024-02-01';
1416
+ url = `${base}/openai/deployments/${provider.azureDeployment}/models?api-version=${ver}`;
1417
+ headers['api-key'] = key;
1418
+ } else {
1419
+ url = hasV1Suffix ? `${base}/models` : `${base}/v1/models`;
1420
+ headers['Authorization'] = `Bearer ${key}`;
1421
+ }
1422
+ } else if (protocol === 'anthropic') {
1423
+ return { models: provider.models || [], message: 'Anthropic 不支持模型列表查询' };
1424
+ } else if (protocol === 'gemini') {
1425
+ url = `${base}/v1beta/models?key=${key}`;
1426
+ } else {
1427
+ return { error: `不支持的协议: ${protocol}` };
1428
+ }
1429
+ try {
1430
+ const res = await fetch(url, { headers, signal: AbortSignal.timeout(15000) });
1431
+ if (!res.ok) return { error: `HTTP ${res.status}: ${await res.text().catch(() => '')}` };
1432
+ const data = await res.json();
1433
+ const models = (data.data || data.models || []).map(m => m.id || m.name).filter(Boolean);
1434
+ return { models };
1435
+ } catch (err) {
1436
+ return { error: err.message };
1437
+ }
1438
+ },
1439
+
1440
+ // --- 代理管理 ---
1441
+ create_proxy: async (args) => {
1442
+ if (!args.name || !args.port || !args.providerId) return { error: 'name, port, providerId 是必填项' };
1443
+ const provider = configStore.getProviderById(args.providerId);
1444
+ if (!provider) return { error: '供应商不存在' };
1445
+ const parsedPort = parseInt(args.port);
1446
+ const existing = configStore.getProxies().find(p => p.port === parsedPort);
1447
+ if (existing) return { error: `端口 ${parsedPort} 已被代理「${existing.name}」占用` };
1448
+ configStore.saveSnapshot('create-proxy');
1449
+ const proxy = configStore.addProxy({
1450
+ name: args.name,
1451
+ port: parsedPort,
1452
+ providerId: args.providerId,
1453
+ defaultModel: args.defaultModel || '',
1454
+ providerWeight: 1,
1455
+ routingStrategy: normalizeRoutingStrategyInput(args.routingStrategy),
1456
+ providerPool: normalizeProviderPoolInput(args.providerPool),
1457
+ });
1458
+ try {
1459
+ await startProxyWithProvider(proxy);
1460
+ return { success: true, id: proxy.id, name: proxy.name, port: proxy.port, running: true };
1461
+ } catch (err) {
1462
+ configStore.removeProxy(proxy.id);
1463
+ return { error: `代理启动失败: ${err.message}` };
1464
+ }
1465
+ },
1466
+
1467
+ update_proxy: async (args) => {
1468
+ const existing = configStore.getProxyById(args.proxyId);
1469
+ if (!existing) return { error: `代理 ${args.proxyId} 不存在` };
1470
+ configStore.saveSnapshot('update-proxy');
1471
+ const updates = {};
1472
+ if (args.name !== undefined) updates.name = args.name;
1473
+ if (args.port !== undefined) updates.port = parseInt(args.port);
1474
+ if (args.providerId !== undefined) {
1475
+ if (!configStore.getProviderById(args.providerId)) return { error: '供应商不存在' };
1476
+ updates.providerId = args.providerId;
1477
+ }
1478
+ if (args.defaultModel !== undefined) updates.defaultModel = args.defaultModel;
1479
+ if (args.routingStrategy !== undefined) updates.routingStrategy = normalizeRoutingStrategyInput(args.routingStrategy);
1480
+ const needRestart = updates.port !== undefined && updates.port !== existing.port;
1481
+ if (needRestart) {
1482
+ const conflict = configStore.getProxies().find(p => p.id !== args.proxyId && p.port === updates.port);
1483
+ if (conflict) return { error: `端口 ${updates.port} 已被代理「${conflict.name}」占用` };
1484
+ }
1485
+ const updated = configStore.updateProxy(args.proxyId, updates);
1486
+ if (needRestart) {
1487
+ try {
1488
+ await startProxyWithProvider(updated);
1489
+ } catch (err) {
1490
+ return { error: `代理重启失败: ${err.message}` };
1491
+ }
1492
+ } else {
1493
+ const target = resolveTarget(updated);
1494
+ if (target) proxyManager.updateProxyConfig({ ...updated, target });
1495
+ }
1496
+ return { success: true, id: updated.id, name: updated.name, running: proxyManager.isRunning(updated.id) };
1497
+ },
1498
+
1499
+ delete_proxy: async (args) => {
1500
+ const existing = configStore.getProxyById(args.proxyId);
1501
+ if (!existing) return { error: `代理 ${args.proxyId} 不存在` };
1502
+ configStore.saveSnapshot('delete-proxy');
1503
+ await proxyManager.stopProxy(args.proxyId);
1504
+ configStore.removeProxy(args.proxyId);
1505
+ return { success: true };
1506
+ },
1507
+
1508
+ start_proxy: async (args) => {
1509
+ const proxy = configStore.getProxyById(args.proxyId);
1510
+ if (!proxy) return { error: `代理 ${args.proxyId} 不存在` };
1511
+ try {
1512
+ await startProxyWithProvider(proxy);
1513
+ return { success: true, running: true };
1514
+ } catch (err) {
1515
+ return { error: `启动失败: ${err.message}` };
1516
+ }
1517
+ },
1518
+
1519
+ stop_proxy: async (args) => {
1520
+ const proxy = configStore.getProxyById(args.proxyId);
1521
+ if (!proxy) return { error: `代理 ${args.proxyId} 不存在` };
1522
+ await proxyManager.stopProxy(args.proxyId);
1523
+ return { success: true, running: false };
1524
+ },
1525
+
1526
+ start_all_proxies: async () => {
1527
+ const proxies = configStore.getProxies();
1528
+ const results = [];
1529
+ for (const proxy of proxies) {
1530
+ if (proxyManager.isRunning(proxy.id)) {
1531
+ results.push({ id: proxy.id, name: proxy.name, skipped: true });
1532
+ continue;
1533
+ }
1534
+ try {
1535
+ await startProxyWithProvider(proxy);
1536
+ results.push({ id: proxy.id, name: proxy.name, success: true });
1537
+ } catch (err) {
1538
+ results.push({ id: proxy.id, name: proxy.name, success: false, error: err.message });
1539
+ }
1540
+ }
1541
+ return { results };
1542
+ },
1543
+
1544
+ stop_all_proxies: async () => {
1545
+ const running = proxyManager.getRunningPorts();
1546
+ const results = [];
1547
+ for (const r of running) {
1548
+ await proxyManager.stopProxy(r.id);
1549
+ results.push({ id: r.id, name: r.name, success: true });
1550
+ }
1551
+ return { results };
1552
+ },
1553
+
1554
+ // --- MCP 服务器管理 ---
1555
+ get_mcp_servers: async () => {
1556
+ const servers = configStore.getMcpServers();
1557
+ const status = mcpClient.getStatus();
1558
+ const statusMap = Object.fromEntries(status.map(s => [s.name, s]));
1559
+ return Object.entries(servers).map(([name, config]) => ({
1560
+ name,
1561
+ enabled: config.enabled !== false,
1562
+ transport: config.url ? 'http' : 'stdio',
1563
+ command: config.command,
1564
+ url: config.url,
1565
+ ...(statusMap[name] || { status: 'disconnected', tools: [], lastError: null }),
1566
+ }));
1567
+ },
1568
+
1569
+ add_mcp_server: async (args) => {
1570
+ if (!args.name) return { error: '需要服务名称' };
1571
+ if (!args.command && !args.url) return { error: '需要 command(本地)或 url(远程)' };
1572
+ const existing = configStore.getMcpServer(args.name);
1573
+ if (existing) return { error: '服务名已存在' };
1574
+ const serverConfig = {};
1575
+ if (args.url) {
1576
+ serverConfig.url = args.url;
1577
+ if (args.headers) serverConfig.headers = args.headers;
1578
+ } else {
1579
+ serverConfig.command = args.command;
1580
+ if (args.args) serverConfig.args = Array.isArray(args.args) ? args.args : String(args.args).split(/\s+/).filter(Boolean);
1581
+ if (args.env && Object.keys(args.env).length) serverConfig.env = args.env;
1582
+ }
1583
+ serverConfig.enabled = args.enabled !== false;
1584
+ configStore.addMcpServer(args.name, serverConfig);
1585
+ if (serverConfig.enabled) {
1586
+ mcpClient.connectServer(args.name, serverConfig).catch(err => {
1587
+ logger.error(`[MCP] 后台连接 ${args.name} 失败: ${err.message}`);
1588
+ });
1589
+ }
1590
+ return { success: true, name: args.name };
486
1591
  },
487
1592
 
488
- get_usage_stats: async (args) => {
489
- return statsStore.getStats({ range: args.range || 'daily', startDate: args.startDate, endDate: args.endDate, proxyId: args.proxyId });
1593
+ update_mcp_server: async (args) => {
1594
+ const existing = configStore.getMcpServer(args.name);
1595
+ if (!existing) return { error: `MCP 服务 "${args.name}" 不存在` };
1596
+ const updates = {};
1597
+ if (args.url !== undefined) {
1598
+ updates.url = args.url;
1599
+ if (args.headers !== undefined) updates.headers = args.headers;
1600
+ delete updates.command;
1601
+ delete updates.args;
1602
+ delete updates.env;
1603
+ }
1604
+ if (args.command !== undefined) {
1605
+ updates.command = args.command;
1606
+ if (args.args !== undefined) updates.args = Array.isArray(args.args) ? args.args : String(args.args).split(/\s+/).filter(Boolean);
1607
+ if (args.env !== undefined) updates.env = args.env;
1608
+ delete updates.url;
1609
+ delete updates.headers;
1610
+ }
1611
+ if (args.enabled !== undefined) updates.enabled = args.enabled;
1612
+ configStore.updateMcpServer(args.name, updates);
1613
+ const newConfig = configStore.getMcpServer(args.name);
1614
+ if (newConfig.enabled) {
1615
+ await mcpClient.reconnectIfChanged(args.name, newConfig).catch(() => {});
1616
+ } else {
1617
+ await mcpClient.disconnectServer(args.name);
1618
+ }
1619
+ return { success: true };
490
1620
  },
491
1621
 
492
- get_recent_requests: async (args) => {
493
- const limit = Math.min(Math.max(1, parseInt(args.limit) || 20), 100);
494
- return { entries: requestLog.getAll(limit) };
1622
+ delete_mcp_server: async (args) => {
1623
+ const existing = configStore.getMcpServer(args.name);
1624
+ if (!existing) return { error: `MCP 服务 "${args.name}" 不存在` };
1625
+ await mcpClient.disconnectServer(args.name);
1626
+ configStore.removeMcpServer(args.name);
1627
+ return { success: true };
495
1628
  },
496
1629
 
497
- get_system_logs: async (args) => {
498
- const limit = Math.min(Math.max(1, parseInt(args.limit) || 30), 100);
1630
+ connect_mcp_server: async (args) => {
1631
+ const config = configStore.getMcpServer(args.name);
1632
+ if (!config) return { error: `MCP 服务 "${args.name}" 不存在` };
499
1633
  try {
500
- const content = await fs.promises.readFile(logger.LOG_FILE, 'utf8');
501
- const allLines = content.split('\n').filter(l => l.trim());
502
- return { lines: allLines.slice(-limit) };
503
- } catch {
504
- return { lines: [] };
1634
+ await mcpClient.connectServer(args.name, config);
1635
+ const status = mcpClient.getStatus().find(s => s.name === args.name);
1636
+ return status || { status: 'error', lastError: '连接失败' };
1637
+ } catch (err) {
1638
+ return { error: err.message };
505
1639
  }
506
1640
  },
507
1641
 
508
- get_key_health: async () => {
509
- const result = {};
510
- for (const [providerId, health] of keyHealth) {
511
- result[providerId] = health;
512
- }
513
- return result;
1642
+ disconnect_mcp_server: async (args) => {
1643
+ await mcpClient.disconnectServer(args.name);
1644
+ return { success: true };
514
1645
  },
515
1646
 
516
- get_settings: async () => {
517
- return configStore.getSettings();
1647
+ get_mcp_tools: async () => {
1648
+ const status = mcpClient.getStatus();
1649
+ return status.filter(s => s.status === 'connected').flatMap(s =>
1650
+ s.tools.map(t => ({ ...t, server: s.name, transport: s.transport }))
1651
+ );
518
1652
  },
519
1653
 
520
- get_config_history: async () => {
521
- return { snapshots: configStore.getSnapshots() };
1654
+ // --- 技能管理 ---
1655
+ get_skills: async () => {
1656
+ return { skills: skillStore.list() };
522
1657
  },
523
1658
 
524
- read_file: async (args) => {
525
- const filePath = path.resolve(args.path);
526
- try {
527
- const content = await fs.promises.readFile(filePath, 'utf8');
528
- const lines = content.split('\n');
529
- const offset = Math.max(0, parseInt(args.offset) || 0);
530
- const limit = Math.min(Math.max(1, parseInt(args.limit) || 500), 2000);
531
- const sliced = lines.slice(offset, offset + limit);
532
- return { content: sliced.join('\n'), totalLines: lines.length, offset, returnedLines: sliced.length };
533
- } catch (err) {
534
- return { error: err.message };
535
- }
1659
+ create_skill: async (args) => {
1660
+ if (!args.name || !args.content) return { error: '需要 name 和 content' };
1661
+ const skill = skillStore.create(args.name, args.description || '', args.content);
1662
+ if (!skill) return { error: '技能已存在' };
1663
+ return { success: true, name: skill.name };
536
1664
  },
537
1665
 
538
- write_file: async (args) => {
539
- const filePath = path.resolve(args.path);
540
- try {
541
- await fs.promises.mkdir(path.dirname(filePath), { recursive: true });
542
- await fs.promises.writeFile(filePath, args.content, 'utf8');
543
- return { success: true, path: filePath, bytes: Buffer.byteLength(args.content, 'utf8') };
544
- } catch (err) {
545
- return { error: err.message };
546
- }
1666
+ update_skill: async (args) => {
1667
+ const skill = skillStore.update(args.name, args.description || '', args.content || '');
1668
+ if (!skill) return { error: `技能 "${args.name}" 不存在或不可编辑` };
1669
+ return { success: true, name: skill.name };
547
1670
  },
548
1671
 
549
- list_directory: async (args) => {
550
- const dirPath = path.resolve(args.path || '.');
551
- try {
552
- const entries = await fs.promises.readdir(dirPath, { withFileTypes: true });
553
- return {
554
- path: dirPath,
555
- entries: entries.map(e => ({
556
- name: e.name,
557
- type: e.isDirectory() ? 'directory' : 'file',
1672
+ delete_skill: async (args) => {
1673
+ const skill = skillStore.get(args.name);
1674
+ if (!skill) return { error: `技能 "${args.name}" 不存在` };
1675
+ if (skill.category === 'system') return { error: '系统级技能不可删除' };
1676
+ if (!skillStore.remove(args.name)) return { error: '删除失败' };
1677
+ return { success: true };
1678
+ },
1679
+
1680
+ // --- 配置管理 ---
1681
+ export_config: async () => {
1682
+ const providers = configStore.getProviders();
1683
+ const proxies = configStore.getProxies().map(p => {
1684
+ const provider = configStore.getProviderById(p.providerId);
1685
+ return { id: p.id, name: p.name, port: p.port, providerId: p.providerId, defaultModel: p.defaultModel || '', routingStrategy: p.routingStrategy || 'primary_fallback', providerName: provider?.name || '' };
1686
+ });
1687
+ return { providers, proxies, exportedAt: new Date().toISOString() };
1688
+ },
1689
+
1690
+ import_config: async (args) => {
1691
+ if (!args.config || !args.mode || !['overwrite', 'merge'].includes(args.mode)) {
1692
+ return { error: '需要 config 和 mode(overwrite/merge)' };
1693
+ }
1694
+ if (!Array.isArray(args.config.providers) || !Array.isArray(args.config.proxies)) {
1695
+ return { error: '配置格式错误:需要 providers 和 proxies 数组' };
1696
+ }
1697
+ configStore.saveSnapshot('import-' + args.mode);
1698
+ if (args.mode === 'overwrite') {
1699
+ const newConfig = {
1700
+ providers: args.config.providers.map(p => ({
1701
+ id: p.id, name: p.name, url: p.url, protocol: p.protocol,
1702
+ apiKey: p.apiKey || '', models: Array.isArray(p.models) ? p.models : [],
1703
+ })),
1704
+ proxies: args.config.proxies.map(p => ({
1705
+ id: p.id, name: p.name, port: p.port, providerId: p.providerId,
1706
+ defaultModel: p.defaultModel || '', routingStrategy: normalizeRoutingStrategyInput(p.routingStrategy),
1707
+ providerPool: normalizeProviderPoolInput(p.providerPool),
558
1708
  })),
559
1709
  };
560
- } catch (err) {
561
- return { error: err.message };
1710
+ configStore.saveConfig(newConfig);
1711
+ return { success: true, mode: 'overwrite', providers: newConfig.providers.length, proxies: newConfig.proxies.length };
1712
+ }
1713
+ // merge 模式
1714
+ const existingProviders = configStore.getProviders();
1715
+ const existingProxies = configStore.getProxies();
1716
+ const providerMap = new Map(existingProviders.map(p => [p.id, p]));
1717
+ for (const p of args.config.providers) {
1718
+ providerMap.set(p.id, { id: p.id, name: p.name, url: p.url, protocol: p.protocol, apiKey: p.apiKey || '', models: Array.isArray(p.models) ? p.models : [] });
562
1719
  }
1720
+ const proxyMap = new Map(existingProxies.map(p => [p.id, p]));
1721
+ for (const p of args.config.proxies) {
1722
+ const conflict = proxyMap.get(p.id) ? null : Array.from(proxyMap.values()).find(ep => ep.port === p.port);
1723
+ if (conflict) return { error: `端口 ${p.port} 已被代理「${conflict.name}」占用` };
1724
+ proxyMap.set(p.id, { id: p.id, name: p.name, port: p.port, providerId: p.providerId, defaultModel: p.defaultModel || '', routingStrategy: normalizeRoutingStrategyInput(p.routingStrategy), providerPool: normalizeProviderPoolInput(p.providerPool) });
1725
+ }
1726
+ const merged = { providers: Array.from(providerMap.values()), proxies: Array.from(proxyMap.values()) };
1727
+ configStore.saveConfig(merged);
1728
+ return { success: true, mode: 'merge', providers: merged.providers.length, proxies: merged.proxies.length };
563
1729
  },
564
1730
 
565
- search_files: async (args) => {
566
- const root = path.resolve(args.path || '.');
567
- const pattern = args.pattern;
568
- try {
569
- const results = [];
570
- const globToRegex = (g) => {
571
- const r = g.replace(/\*\*/g, '§GLOBSTAR§')
572
- .replace(/\*/g, '[^/]*')
573
- .replace(/\?/g, '[^/]')
574
- .replace(/§GLOBSTAR§/g, '.*');
575
- return new RegExp('^' + r + '$');
576
- };
577
- const regex = globToRegex(pattern);
578
- const walk = async (dir, rel) => {
579
- const entries = await fs.promises.readdir(dir, { withFileTypes: true });
580
- for (const e of entries) {
581
- const fullPath = path.join(dir, e.name);
582
- const relPath = rel ? `${rel}/${e.name}` : e.name;
583
- if (e.isDirectory()) {
584
- if (e.name === 'node_modules' || e.name === '.git') continue;
585
- await walk(fullPath, relPath);
586
- } else if (regex.test(relPath)) {
587
- results.push(relPath);
588
- }
589
- }
590
- };
591
- await walk(root, '');
592
- return { pattern, root, matches: results.slice(0, 200), total: results.length };
593
- } catch (err) {
594
- return { error: err.message };
1731
+ rollback_config: async (args) => {
1732
+ if (!args.file) return { error: '需要指定快照文件' };
1733
+ const result = configStore.restoreSnapshot(args.file);
1734
+ if (result.error) return { error: result.error };
1735
+ return { success: true };
1736
+ },
1737
+
1738
+ // --- 系统操作 ---
1739
+ update_settings: async (args) => {
1740
+ if (!args.settings || typeof args.settings !== 'object') return { error: '需要 settings 对象' };
1741
+ for (const [key, value] of Object.entries(args.settings)) {
1742
+ configStore.setSetting(key, value);
595
1743
  }
1744
+ return { success: true, settings: configStore.getSettings() };
596
1745
  },
597
1746
 
598
- execute_command: async (args) => {
599
- const timeout = Math.min(Math.max(1000, parseInt(args.timeout) || 30000), 120000);
600
- return new Promise((resolve) => {
601
- exec(args.command, { cwd: args.cwd || process.cwd(), timeout, maxBuffer: 1024 * 1024 }, (err, stdout, stderr) => {
602
- if (err) {
603
- resolve({ exitCode: err.code || 1, stdout: stdout || '', stderr: stderr || err.message });
604
- } else {
605
- resolve({ exitCode: 0, stdout: stdout || '', stderr: stderr || '' });
606
- }
607
- });
608
- });
1747
+ trigger_key_health_check: async () => {
1748
+ await checkAllProviderKeys();
1749
+ return { success: true };
1750
+ },
1751
+
1752
+ check_health: async () => {
1753
+ return {
1754
+ status: 'ok',
1755
+ version: require('./package.json').version,
1756
+ uptime: Math.floor(process.uptime()),
1757
+ proxies: {
1758
+ total: configStore.getProxies().length,
1759
+ running: proxyManager.getRunningPorts().length,
1760
+ },
1761
+ };
609
1762
  },
610
1763
  };
611
1764
 
@@ -1392,48 +2545,393 @@ async function init() {
1392
2545
 
1393
2546
  // ==================== 智控助手 Tool Calling API ====================
1394
2547
 
2548
+ const conversationStore = require('./lib/conversation-store');
2549
+ conversationStore.init();
2550
+
2551
+ const skillStore = require('./lib/skill-store');
2552
+ skillStore.init();
2553
+
2554
+ const promptBuilder = require('./lib/prompt-builder');
2555
+
2556
+ // 会话并发锁:convId → true 表示正在 streaming
2557
+ const activeStreams = new Set();
2558
+
1395
2559
  function sendSSE(res, event, data) {
1396
2560
  res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
1397
2561
  }
1398
2562
 
2563
+ // 会话管理 API
2564
+ app.get('/api/assistant/conversations', (req, res) => {
2565
+ res.json({ conversations: conversationStore.list() });
2566
+ });
2567
+
2568
+ app.delete('/api/assistant/conversations/:id', (req, res) => {
2569
+ const conv = conversationStore.get(req.params.id);
2570
+ if (!conv) return res.status(404).json({ error: '会话不存在' });
2571
+ conversationStore.remove(req.params.id);
2572
+ res.json({ success: true });
2573
+ });
2574
+
2575
+ // 获取单个会话的消息历史(用于恢复会话显示)
2576
+ app.get('/api/assistant/conversations/:id/messages', (req, res) => {
2577
+ const conv = conversationStore.get(req.params.id);
2578
+ if (!conv) return res.status(404).json({ error: '会话不存在' });
2579
+ // 返回消息历史(过滤掉 system 消息,前端不需要显示)
2580
+ const messages = (conv.messages || []).filter(m => m.role !== 'system');
2581
+ const compressionSummary = conv.compressionSummary || null;
2582
+ res.json({ id: conv.id, proxyId: conv.proxyId, messages, compressionSummary });
2583
+ });
2584
+
2585
+ // 获取代理的候选供应商及其模型列表(供前端级联选择)
2586
+ app.get('/api/assistant/proxy-providers/:proxyId', (req, res) => {
2587
+ const proxy = configStore.getProxyById(req.params.proxyId);
2588
+ if (!proxy) return res.status(404).json({ error: '代理不存在' });
2589
+ const providers = configStore.getProviders().map(p => ({
2590
+ id: p.id,
2591
+ name: p.name,
2592
+ protocol: p.protocol,
2593
+ models: p.models || [],
2594
+ }));
2595
+ res.json({ providers, defaultModel: proxy.defaultModel || '' });
2596
+ });
2597
+
2598
+ // ========== Skill API ==========
2599
+ app.get('/api/skills', (req, res) => {
2600
+ res.json({ skills: skillStore.list() });
2601
+ });
2602
+
2603
+ app.get('/api/skills/:name', (req, res) => {
2604
+ const skill = skillStore.get(req.params.name);
2605
+ if (!skill) return res.status(404).json({ error: '技能不存在' });
2606
+ res.json(skill);
2607
+ });
2608
+
2609
+ app.post('/api/skills', (req, res) => {
2610
+ const { name, description, content } = req.body;
2611
+ if (!name || !content) return res.status(400).json({ error: '需要 name 和 content' });
2612
+ const skill = skillStore.create(name, description || '', content);
2613
+ if (!skill) return res.status(409).json({ error: '技能已存在' });
2614
+ res.json(skill);
2615
+ });
2616
+
2617
+ // 上传技能文件夹创建技能
2618
+ app.post('/api/skills/upload', (req, res) => {
2619
+ const { files } = req.body;
2620
+ if (!files || !files.length) return res.status(400).json({ error: '需要文件列表' });
2621
+ const skillMd = files.find(f => f.path === 'SKILL.md');
2622
+ if (!skillMd) return res.status(400).json({ error: '缺少 SKILL.md' });
2623
+ const MAX_BASE64 = 1024 * 1024;
2624
+ for (const f of files) {
2625
+ if (f.content.length > MAX_BASE64) return res.status(413).json({ error: `文件 ${f.path} 过大` });
2626
+ }
2627
+ const skill = skillStore.createFromUpload(files);
2628
+ if (!skill) return res.status(400).json({ error: 'SKILL.md 缺少 name 字段或技能已存在' });
2629
+ res.json(skill);
2630
+ });
2631
+
2632
+ app.put('/api/skills/:name', (req, res) => {
2633
+ const { description, content } = req.body;
2634
+ const skill = skillStore.update(req.params.name, description || '', content || '');
2635
+ if (!skill) return res.status(404).json({ error: '技能不存在或不可编辑' });
2636
+ res.json(skill);
2637
+ });
2638
+
2639
+ // 上传 skill 附属文件(scripts/reference)
2640
+ app.post('/api/skills/:name/upload', (req, res) => {
2641
+ const skill = skillStore.get(req.params.name);
2642
+ if (!skill) return res.status(404).json({ error: '技能不存在' });
2643
+ if (skill.category === 'system') return res.status(403).json({ error: '系统级技能不可修改' });
2644
+ const { filename, subDir, content } = req.body; // content: base64
2645
+ if (!filename || !content) return res.status(400).json({ error: '需要 filename 和 content' });
2646
+ const MAX_BASE64 = 1024 * 1024; // ~768KB decoded
2647
+ if (content.length > MAX_BASE64) return res.status(413).json({ error: '文件过大,最大 768KB' });
2648
+ const dir = subDir === 'reference' ? 'reference' : 'scripts';
2649
+ const targetDir = path.join(skill.dirPath, dir);
2650
+ fs.mkdirSync(targetDir, { recursive: true });
2651
+ const safeName = path.basename(filename).replace(/[^a-zA-Z0-9._-]/g, '_');
2652
+ fs.writeFileSync(path.join(targetDir, safeName), Buffer.from(content, 'base64'));
2653
+ skillStore.init(); // 重新加载
2654
+ res.json({ success: true, path: `${dir}/${safeName}` });
2655
+ });
2656
+
2657
+ // 删除 skill 附属文件
2658
+ app.delete('/api/skills/:name/file', (req, res) => {
2659
+ const skill = skillStore.get(req.params.name);
2660
+ if (!skill) return res.status(404).json({ error: '技能不存在' });
2661
+ if (skill.category === 'system') return res.status(403).json({ error: '系统级技能不可修改' });
2662
+ const { filePath } = req.body;
2663
+ if (!filePath) return res.status(400).json({ error: '需要 filePath' });
2664
+ const fullPath = path.join(skill.dirPath, filePath);
2665
+ if (!fullPath.startsWith(skill.dirPath)) return res.status(400).json({ error: '无效路径' });
2666
+ try { fs.unlinkSync(fullPath); } catch {}
2667
+ skillStore.init();
2668
+ res.json({ success: true });
2669
+ });
2670
+
2671
+ app.delete('/api/skills/:name', (req, res) => {
2672
+ const skill = skillStore.get(req.params.name);
2673
+ if (!skill) return res.status(404).json({ error: '技能不存在' });
2674
+ if (skill.category === 'system') return res.status(403).json({ error: '系统级技能不可删除' });
2675
+ if (!skillStore.remove(req.params.name)) {
2676
+ return res.status(500).json({ error: '删除失败,请检查文件权限' });
2677
+ }
2678
+ res.json({ success: true });
2679
+ });
2680
+
2681
+ // ==================== MCP 服务管理 API ====================
2682
+
2683
+ app.get('/api/mcp/servers', (req, res) => {
2684
+ const servers = configStore.getMcpServers();
2685
+ const status = mcpClient.getStatus();
2686
+ const statusMap = Object.fromEntries(status.map(s => [s.name, s]));
2687
+ const result = Object.entries(servers).map(([name, config]) => ({
2688
+ name,
2689
+ enabled: config.enabled !== false,
2690
+ transport: config.url ? 'http' : 'stdio',
2691
+ command: config.command,
2692
+ url: config.url,
2693
+ ...(statusMap[name] || { status: 'disconnected', tools: [], lastError: null }),
2694
+ }));
2695
+ res.json(result);
2696
+ });
2697
+
2698
+ app.get('/api/mcp/servers/:name', (req, res) => {
2699
+ const config = configStore.getMcpServer(req.params.name);
2700
+ if (!config) return res.status(404).json({ error: 'MCP 服务不存在' });
2701
+ const status = mcpClient.getStatus().find(s => s.name === req.params.name);
2702
+ res.json({ name: req.params.name, config, ...(status || { status: 'disconnected', tools: [] }) });
2703
+ });
2704
+
2705
+ app.post('/api/mcp/servers', async (req, res) => {
2706
+ const { name, command, args, env, url, headers, enabled } = req.body;
2707
+ if (!name) return res.status(400).json({ error: '需要服务名称' });
2708
+ if (!command && !url) return res.status(400).json({ error: '需要 command(本地)或 url(远程)' });
2709
+ const existing = configStore.getMcpServer(name);
2710
+ if (existing) return res.status(409).json({ error: '服务名已存在' });
2711
+ const serverConfig = {};
2712
+ if (url) {
2713
+ serverConfig.url = url;
2714
+ if (headers) serverConfig.headers = headers;
2715
+ } else {
2716
+ serverConfig.command = command;
2717
+ if (args) serverConfig.args = Array.isArray(args) ? args : args.split(/\s+/).filter(Boolean);
2718
+ if (env && Object.keys(env).length) serverConfig.env = env;
2719
+ }
2720
+ serverConfig.enabled = enabled !== false;
2721
+ configStore.addMcpServer(name, serverConfig);
2722
+ if (serverConfig.enabled) {
2723
+ mcpClient.connectServer(name, serverConfig).catch(err => {
2724
+ logger.error(`[MCP] 后台连接 ${name} 失败: ${err.message}`);
2725
+ });
2726
+ }
2727
+ res.json({ success: true, name });
2728
+ });
2729
+
2730
+ app.put('/api/mcp/servers/:name', async (req, res) => {
2731
+ const { command, args, env, url, headers, enabled } = req.body;
2732
+ const existing = configStore.getMcpServer(req.params.name);
2733
+ if (!existing) return res.status(404).json({ error: 'MCP 服务不存在' });
2734
+ const updates = {};
2735
+ if (url !== undefined) {
2736
+ updates.url = url;
2737
+ if (headers !== undefined) updates.headers = headers;
2738
+ delete updates.command;
2739
+ delete updates.args;
2740
+ delete updates.env;
2741
+ }
2742
+ if (command !== undefined) {
2743
+ updates.command = command;
2744
+ if (args !== undefined) updates.args = Array.isArray(args) ? args : args.split(/\s+/).filter(Boolean);
2745
+ if (env !== undefined) updates.env = env;
2746
+ delete updates.url;
2747
+ delete updates.headers;
2748
+ }
2749
+ if (enabled !== undefined) updates.enabled = enabled;
2750
+ configStore.updateMcpServer(req.params.name, updates);
2751
+ const newConfig = configStore.getMcpServer(req.params.name);
2752
+ if (newConfig.enabled) {
2753
+ mcpClient.reconnectIfChanged(req.params.name, newConfig).catch(() => {});
2754
+ } else {
2755
+ await mcpClient.disconnectServer(req.params.name);
2756
+ }
2757
+ res.json({ success: true });
2758
+ });
2759
+
2760
+ app.delete('/api/mcp/servers/:name', async (req, res) => {
2761
+ const existing = configStore.getMcpServer(req.params.name);
2762
+ if (!existing) return res.status(404).json({ error: 'MCP 服务不存在' });
2763
+ await mcpClient.disconnectServer(req.params.name);
2764
+ configStore.removeMcpServer(req.params.name);
2765
+ res.json({ success: true });
2766
+ });
2767
+
2768
+ app.post('/api/mcp/servers/:name/connect', async (req, res) => {
2769
+ const config = configStore.getMcpServer(req.params.name);
2770
+ if (!config) return res.status(404).json({ error: 'MCP 服务不存在' });
2771
+ try {
2772
+ await mcpClient.connectServer(req.params.name, config);
2773
+ const status = mcpClient.getStatus().find(s => s.name === req.params.name);
2774
+ res.json(status || { status: 'error', lastError: '连接失败' });
2775
+ } catch (err) {
2776
+ res.status(500).json({ error: err.message });
2777
+ }
2778
+ });
2779
+
2780
+ app.post('/api/mcp/servers/:name/disconnect', async (req, res) => {
2781
+ await mcpClient.disconnectServer(req.params.name);
2782
+ res.json({ success: true });
2783
+ });
2784
+
2785
+ app.get('/api/mcp/tools', (req, res) => {
2786
+ const status = mcpClient.getStatus();
2787
+ const allTools = status.filter(s => s.status === 'connected').flatMap(s =>
2788
+ s.tools.map(t => ({ ...t, server: s.name, transport: s.transport }))
2789
+ );
2790
+ res.json(allTools);
2791
+ });
2792
+
2793
+
1399
2794
  app.post('/api/assistant/chat', async (req, res) => {
1400
- const { proxyId, messages } = req.body;
1401
- if (!proxyId || !Array.isArray(messages)) {
1402
- return res.status(400).json({ error: '需要 proxyId 和 messages' });
2795
+ const { proxyId, conversationId, message, compress, providerId, model } = req.body;
2796
+ if (!proxyId || (!compress && !message)) {
2797
+ return res.status(400).json({ error: '需要 proxyId 和 message' });
1403
2798
  }
1404
2799
 
1405
2800
  const proxy = configStore.getProxyById(proxyId);
1406
2801
  if (!proxy) return res.status(404).json({ error: '代理不存在' });
1407
2802
  if (!resolveTarget(proxy)) return res.status(500).json({ error: '代理目标未配置' });
1408
2803
 
2804
+ // 查找或创建对话
2805
+ const settings = configStore.getSettings();
2806
+ let convId = conversationId;
2807
+ let conv;
2808
+ if (convId) {
2809
+ conv = conversationStore.get(convId);
2810
+ }
2811
+ if (!conv && compress) {
2812
+ return res.status(404).json({ error: '会话不存在,无法压缩' });
2813
+ }
2814
+ if (!conv) {
2815
+ const maxConvs = parseInt(settings.maxConversations) || 0;
2816
+ conv = conversationStore.create(proxyId, maxConvs);
2817
+ convId = conv.id;
2818
+ }
2819
+
2820
+ // 并发锁:同一会话正在 streaming 时拒绝新请求
2821
+ if (activeStreams.has(convId)) {
2822
+ return res.status(429).json({ error: '该会话正在处理中,请稍后再试' });
2823
+ }
2824
+ activeStreams.add(convId);
2825
+ conversationStore.touch(conv);
2826
+
2827
+ // 追加用户消息到对话历史(压缩请求不追加空消息)
2828
+ // 检测 /skillname 前缀触发技能
2829
+ let activeSkill = null;
2830
+ if (!compress && message) {
2831
+ const slashMatch = message.match(/^\/([a-zA-Z0-9_-]+)(?:\s+([\s\S]*))?$/);
2832
+ if (slashMatch) {
2833
+ const skillName = slashMatch[1];
2834
+ const skill = skillStore.get(skillName);
2835
+ if (skill) {
2836
+ activeSkill = skill;
2837
+ // 将用户消息中的参数部分也保留
2838
+ const args = slashMatch[2]?.trim();
2839
+ if (args) {
2840
+ conv.messages.push({ role: 'user', content: args });
2841
+ }
2842
+ } else {
2843
+ conv.messages.push({ role: 'user', content: message });
2844
+ }
2845
+ } else {
2846
+ conv.messages.push({ role: 'user', content: message });
2847
+ }
2848
+ conversationStore.touch(conv);
2849
+ }
2850
+
1409
2851
  const proxyUrl = `http://localhost:${proxy.port}/v1/chat/completions`;
1410
2852
  const proxyHeaders = { 'Content-Type': 'application/json' };
1411
2853
  if (proxy.requireAuth && proxy.authToken) {
1412
2854
  proxyHeaders['Authorization'] = `Bearer ${proxy.authToken}`;
1413
2855
  }
2856
+ if (providerId) proxyHeaders['x-pp-provider-id'] = providerId;
2857
+ if (model) proxyHeaders['x-pp-model'] = model;
2858
+ // 若供应商不在代理候选池中,传递完整供应商配置供代理动态构建临时候选
2859
+ if (providerId) {
2860
+ const target = resolveTarget(proxy);
2861
+ const inPool = target?.providerPool?.some(c => c.providerId === providerId);
2862
+ if (!inPool) {
2863
+ const provider = configStore.getProviderById(providerId);
2864
+ if (provider) {
2865
+ proxyHeaders['x-pp-provider-url'] = provider.url;
2866
+ proxyHeaders['x-pp-provider-protocol'] = provider.protocol;
2867
+ const enabledKeys = (provider.apiKeys || []).filter(k => k.enabled !== false).map(k => k.key);
2868
+ if (enabledKeys.length > 0) proxyHeaders['x-pp-provider-keys'] = JSON.stringify(enabledKeys);
2869
+ }
2870
+ }
2871
+ }
1414
2872
 
1415
2873
  // SSE 响应头
1416
2874
  res.setHeader('Content-Type', 'text/event-stream');
1417
2875
  res.setHeader('Cache-Control', 'no-cache');
1418
2876
  res.setHeader('Connection', 'keep-alive');
1419
2877
 
1420
- // 发送 SSE 的辅助函数,忽略写入错误
1421
2878
  function safeSSE(event, data) {
1422
2879
  try { sendSSE(res, event, data); } catch {}
1423
2880
  }
2881
+ const MAX_CONTEXT = Math.max(10000, parseInt(settings.maxContext) || 200000);
2882
+ const MAX_TOOL_ROUNDS = Math.max(1, Math.min(100, parseInt(settings.maxRounds) || 10));
2883
+
2884
+ // 手动压缩请求
2885
+ if (compress) {
2886
+ logger.log(`[assistant] 压缩请求 — ${conv.messages.length} messages`);
2887
+ safeSSE('compressing', {});
2888
+ const result = await compressConversation(conv, MAX_CONTEXT, proxyUrl, proxyHeaders, proxy.defaultModel);
2889
+ if (result) {
2890
+ conv.messages = result.messages;
2891
+ conv.compressionSummary = result.summary;
2892
+ conversationStore.touch(conv);
2893
+ safeSSE('compressed', { summary: result.summary, removedCount: result.removedCount, tokens: result.newTokens, maxTokens: MAX_CONTEXT, messages: conv.messages.length });
2894
+ logger.log(`[assistant] 压缩完成 — 移除 ${result.removedCount} 条`);
2895
+ } else {
2896
+ safeSSE('compressed', { summary: null, removedCount: 0, tokens: estimateConversationTokens(conv.messages), maxTokens: MAX_CONTEXT, messages: conv.messages.length });
2897
+ }
2898
+ safeSSE('done', {});
2899
+ res.end();
2900
+ return;
2901
+ }
1424
2902
 
1425
- const MAX_TOOL_ROUNDS = 10;
1426
- const conversationMessages = [...messages];
2903
+ // 发送 conversationId 给前端
2904
+ safeSSE('conversation', { id: convId });
1427
2905
 
1428
2906
  try {
1429
- for (let round = 0; round < MAX_TOOL_ROUNDS; round++) {
1430
- logger.log(`[assistant] round ${round} messages: ${conversationMessages.length}`);
1431
- for (let i = 0; i < conversationMessages.length; i++) {
1432
- const m = conversationMessages[i];
1433
- logger.log(`[assistant] msg[${i}]: role=${m.role}, hasReasoning=${!!m.reasoning_content}, hasToolCalls=${!!m.tool_calls}, contentLen=${(m.content || '').length}`);
2907
+ // 请求级别缓存 system prompt(避免每轮重建导致 prompt cache 失效)
2908
+ const systemPrompt = promptBuilder.buildSystemPrompt({ skillStore, mcpClient });
2909
+ const buildMessages = () => {
2910
+ const msgs = [{ role: 'system', content: systemPrompt }];
2911
+ if (activeSkill) {
2912
+ let skillInfo = `[技能指令: ${activeSkill.name}]\n${activeSkill.content}`;
2913
+ if (activeSkill.dirPath) skillInfo += `\n\n技能目录: ${activeSkill.dirPath}`;
2914
+ if (activeSkill.scripts?.length > 0) skillInfo += `\n可用脚本: ${activeSkill.scripts.map(f => 'scripts/' + f).join(', ')}`;
2915
+ msgs.push({ role: 'system', content: skillInfo });
2916
+ }
2917
+ if (conv.compressionSummary) {
2918
+ msgs.push({ role: 'system', content: `[压缩摘要]\n${conv.compressionSummary}\n\n---\n以上是之前对话的压缩摘要。最近的消息保留原文。请继续对话,不要复述摘要内容。` });
1434
2919
  }
2920
+ msgs.push(...conv.messages);
2921
+ return msgs;
2922
+ };
2923
+
2924
+ let currentTokens = estimateConversationTokens(buildMessages());
2925
+ const sendContext = () => {
2926
+ const pct = Math.round(currentTokens / MAX_CONTEXT * 1000) / 10;
2927
+ safeSSE('context', { tokens: currentTokens, maxTokens: MAX_CONTEXT, percent: pct, messages: conv.messages.length });
2928
+ };
2929
+ sendContext();
2930
+
2931
+ for (let round = 0; round < MAX_TOOL_ROUNDS; round++) {
2932
+ const messages = buildMessages();
2933
+ logger.log(`[assistant] round ${round} — ${messages.length} messages, ~${currentTokens} tokens`);
1435
2934
 
1436
- // 调用本地代理
1437
2935
  let fetchRes;
1438
2936
  try {
1439
2937
  fetchRes = await fetch(proxyUrl, {
@@ -1442,9 +2940,9 @@ async function init() {
1442
2940
  signal: AbortSignal.timeout(300000),
1443
2941
  body: JSON.stringify({
1444
2942
  model: proxy.defaultModel || 'gpt-4o',
1445
- messages: conversationMessages,
2943
+ messages,
1446
2944
  stream: true,
1447
- tools: TOOL_DEFINITIONS,
2945
+ tools: [...TOOL_DEFINITIONS, ...mcpClient.getToolDefinitions()],
1448
2946
  tool_choice: 'auto',
1449
2947
  }),
1450
2948
  });
@@ -1472,37 +2970,24 @@ async function init() {
1472
2970
  while (true) {
1473
2971
  const { done, value } = await reader.read();
1474
2972
  if (done) break;
1475
-
1476
2973
  buffer += decoder.decode(value, { stream: true });
1477
2974
  const lines = buffer.split('\n');
1478
2975
  buffer = lines.pop();
1479
-
1480
2976
  for (const line of lines) {
1481
2977
  const trimmed = line.trim();
1482
2978
  if (!trimmed || !trimmed.startsWith('data: ')) continue;
1483
2979
  const payload = trimmed.slice(6);
1484
2980
  if (payload === '[DONE]') continue;
1485
-
1486
2981
  try {
1487
2982
  const data = JSON.parse(payload);
1488
2983
  const delta = data.choices?.[0]?.delta;
1489
2984
  if (!delta) continue;
1490
-
1491
- if (delta.content) {
1492
- fullContent += delta.content;
1493
- safeSSE('content', { delta: delta.content });
1494
- }
1495
-
1496
- if (delta.reasoning_content) {
1497
- reasoningContent += delta.reasoning_content;
1498
- }
1499
-
2985
+ if (delta.content) { fullContent += delta.content; safeSSE('content', { delta: delta.content }); }
2986
+ if (delta.reasoning_content) reasoningContent += delta.reasoning_content;
1500
2987
  if (delta.tool_calls) {
1501
2988
  for (const tc of delta.tool_calls) {
1502
2989
  const idx = tc.index;
1503
- if (!toolCallAccumulator[idx]) {
1504
- toolCallAccumulator[idx] = { id: '', name: '', arguments: '' };
1505
- }
2990
+ if (!toolCallAccumulator[idx]) toolCallAccumulator[idx] = { id: '', name: '', arguments: '' };
1506
2991
  if (tc.id) toolCallAccumulator[idx].id = tc.id;
1507
2992
  if (tc.function?.name) toolCallAccumulator[idx].name = tc.function.name;
1508
2993
  if (tc.function?.arguments) toolCallAccumulator[idx].arguments += tc.function.arguments;
@@ -1513,9 +2998,17 @@ async function init() {
1513
2998
  }
1514
2999
 
1515
3000
  const toolCalls = Object.values(toolCallAccumulator).filter(tc => tc.id && tc.name);
1516
- logger.log(`[assistant] round ${round} done — content: ${fullContent.length} chars, tool_calls: ${toolCalls.length}`);
3001
+ logger.log(`[assistant] round ${round} done — ${fullContent.length} chars, ${toolCalls.length} tool calls`);
1517
3002
 
1518
3003
  if (toolCalls.length === 0) {
3004
+ // 最终回复,追加到对话历史(跳过空响应避免 null content 污染历史)
3005
+ if (fullContent || reasoningContent) {
3006
+ const assistantMsg = { role: 'assistant', content: fullContent || null };
3007
+ if (reasoningContent) assistantMsg.reasoning_content = reasoningContent;
3008
+ conv.messages.push(assistantMsg);
3009
+ }
3010
+ currentTokens = estimateConversationTokens(buildMessages());
3011
+ sendContext();
1519
3012
  safeSSE('done', { reasoning_content: reasoningContent || undefined });
1520
3013
  break;
1521
3014
  }
@@ -1525,52 +3018,123 @@ async function init() {
1525
3018
  reasoning_content: reasoningContent || undefined,
1526
3019
  calls: toolCalls.map(tc => {
1527
3020
  let args = {};
1528
- try { args = JSON.parse(tc.arguments); } catch {}
3021
+ try { args = JSON.parse(tc.arguments); } catch (e) {
3022
+ logger.log(`[assistant] tool_calls args parse error (${tc.name}): ${e.message}, raw: ${(tc.arguments || '').slice(0, 200)}`);
3023
+ args = { _raw: tc.arguments, _parseError: true };
3024
+ }
1529
3025
  return { id: tc.id, name: tc.name, arguments: args };
1530
3026
  }),
1531
3027
  });
1532
3028
 
1533
- // 追加 assistant 消息到对话历史
3029
+ // 追加 assistant(tool_calls) 到对话历史
1534
3030
  const assistantMsg = {
1535
3031
  role: 'assistant',
1536
3032
  content: fullContent || null,
1537
- tool_calls: toolCalls.map(tc => ({
1538
- id: tc.id,
1539
- type: 'function',
1540
- function: { name: tc.name, arguments: tc.arguments },
1541
- })),
3033
+ tool_calls: toolCalls.map(tc => ({ id: tc.id, type: 'function', function: { name: tc.name, arguments: tc.arguments } })),
1542
3034
  };
1543
3035
  if (reasoningContent) assistantMsg.reasoning_content = reasoningContent;
1544
- conversationMessages.push(assistantMsg);
3036
+ conv.messages.push(assistantMsg);
1545
3037
 
1546
3038
  // 执行工具
1547
3039
  for (const tc of toolCalls) {
1548
3040
  let args = {};
1549
- try { args = JSON.parse(tc.arguments); } catch {}
1550
- logger.log(`[assistant] EXEC tool: ${tc.name}(${JSON.stringify(args)})`);
3041
+ let argsParseError = false;
3042
+ try { args = JSON.parse(tc.arguments); } catch (e) {
3043
+ logger.log(`[assistant] tool args parse error (${tc.name}): ${e.message}`);
3044
+ argsParseError = true;
3045
+ }
3046
+ logger.log(`[assistant] EXEC tool: ${tc.name}`);
1551
3047
  let result;
1552
- try {
1553
- result = await TOOL_HANDLERS[tc.name]?.(args) || { error: `未知工具: ${tc.name}` };
3048
+ let isError = false;
3049
+ if (argsParseError) {
3050
+ result = { error: `工具 ${tc.name} 的参数 JSON 解析失败,原始内容: ${(tc.arguments || '').slice(0, 200)}` };
3051
+ isError = true;
3052
+ } else try {
3053
+ const mcpHandler = tc.name.startsWith('mcp__') ? mcpClient.getToolHandlerMap()[tc.name] : null;
3054
+ result = await (TOOL_HANDLERS[tc.name] || mcpHandler)?.(args) || { error: `未知工具: ${tc.name}` };
3055
+ if (result && result.error) isError = true;
1554
3056
  } catch (err) {
1555
3057
  logger.log(`[assistant] tool ${tc.name} error: ${err.message}`);
1556
3058
  result = { error: err.message };
3059
+ isError = true;
1557
3060
  }
1558
-
3061
+ result = truncateOutput(result);
1559
3062
  const resultStr = JSON.stringify(result);
1560
- logger.log(`[assistant] tool ${tc.name} done: ${resultStr.length} chars`);
1561
- safeSSE('tool_result', { tool_call_id: tc.id, name: tc.name, result });
3063
+ logger.log(`[assistant] tool ${tc.name} done: ${resultStr.length} chars${isError ? ' (error)' : ''}`);
3064
+ safeSSE('tool_result', { tool_call_id: tc.id, name: tc.name, result, is_error: isError });
3065
+ conv.messages.push({ role: 'tool', tool_call_id: tc.id, content: isError ? `[ERROR] ${resultStr}` : resultStr });
3066
+ }
1562
3067
 
1563
- conversationMessages.push({
1564
- role: 'tool',
1565
- tool_call_id: tc.id,
1566
- content: resultStr,
1567
- });
3068
+ // token 检查 + 压缩
3069
+ currentTokens = estimateConversationTokens(buildMessages());
3070
+ sendContext();
3071
+ if (currentTokens >= MAX_CONTEXT * 0.8) {
3072
+ logger.log(`[assistant] 上下文 ${Math.round(currentTokens / MAX_CONTEXT * 100)}%,自动压缩`);
3073
+ safeSSE('compressing', {});
3074
+ const compResult = await compressConversation(conv, MAX_CONTEXT, proxyUrl, proxyHeaders, proxy.defaultModel);
3075
+ if (compResult) {
3076
+ conv.messages = compResult.messages;
3077
+ conv.compressionSummary = compResult.summary;
3078
+ conversationStore.touch(conv);
3079
+ currentTokens = compResult.newTokens;
3080
+ safeSSE('compressed', { summary: compResult.summary, removedCount: compResult.removedCount, tokens: currentTokens, maxTokens: MAX_CONTEXT, messages: conv.messages.length });
3081
+ sendContext();
3082
+ logger.log(`[assistant] 压缩完成 — 移除 ${compResult.removedCount} 条`);
3083
+ }
1568
3084
  }
1569
- // 继续下一轮
1570
3085
  }
1571
3086
 
1572
- // 循环正常结束(达到最大轮次)
1573
- safeSSE('done', {});
3087
+ // 达到最大轮次 → 总结回复
3088
+ logger.log(`[assistant] max rounds reached, requesting summary`);
3089
+ try {
3090
+ const summaryRes = await fetch(proxyUrl, {
3091
+ method: 'POST',
3092
+ headers: proxyHeaders,
3093
+ signal: AbortSignal.timeout(120000),
3094
+ body: JSON.stringify({
3095
+ model: proxy.defaultModel || 'gpt-4o',
3096
+ messages: [
3097
+ ...buildMessages(),
3098
+ { role: 'system', content: '你已达到最大工具调用轮次限制(' + MAX_TOOL_ROUNDS + ' 轮),无法继续调用工具。请基于已获取的信息给出回复,并明确告知用户:由于达到工具调用轮次上限,信息获取可能不完整或操作被迫中断。如果还有未完成的工作,请说明并建议用户重新提问以继续。' },
3099
+ ],
3100
+ stream: true,
3101
+ }),
3102
+ });
3103
+ if (summaryRes.ok) {
3104
+ const sr = summaryRes.body.getReader();
3105
+ const sd = new TextDecoder();
3106
+ let sb = '';
3107
+ let summaryContent = '';
3108
+ let summaryReasoning = '';
3109
+ while (true) {
3110
+ const { done: finished, value: v } = await sr.read();
3111
+ if (finished) break;
3112
+ sb += sd.decode(v, { stream: true });
3113
+ const lines = sb.split('\n');
3114
+ sb = lines.pop();
3115
+ for (const line of lines) {
3116
+ const t = line.trim();
3117
+ if (!t || !t.startsWith('data: ') || t === 'data: [DONE]') continue;
3118
+ try {
3119
+ const chunk = JSON.parse(t.slice(6));
3120
+ const delta = chunk.choices?.[0]?.delta;
3121
+ if (!delta) continue;
3122
+ if (delta.content) { summaryContent += delta.content; safeSSE('content', { delta: delta.content }); }
3123
+ if (delta.reasoning_content) summaryReasoning += delta.reasoning_content;
3124
+ } catch {}
3125
+ }
3126
+ }
3127
+ // 追加总结到对话历史
3128
+ const summaryMsg = { role: 'assistant', content: summaryContent || null };
3129
+ if (summaryReasoning) summaryMsg.reasoning_content = summaryReasoning;
3130
+ conv.messages.push(summaryMsg);
3131
+ safeSSE('done', { reasoning_content: summaryReasoning || undefined });
3132
+ } else {
3133
+ safeSSE('done', {});
3134
+ }
3135
+ } catch {
3136
+ safeSSE('done', {});
3137
+ }
1574
3138
  } catch (err) {
1575
3139
  logger.log(`[assistant] error: ${err.message}`);
1576
3140
  if (!res.headersSent) {
@@ -1579,6 +3143,8 @@ async function init() {
1579
3143
  safeSSE('error', { message: err.message });
1580
3144
  }
1581
3145
  } finally {
3146
+ activeStreams.delete(convId);
3147
+ conversationStore.touch(conv); // 保存最终对话状态
1582
3148
  res.end();
1583
3149
  }
1584
3150
  });
@@ -1765,6 +3331,19 @@ async function init() {
1765
3331
  requestLog.onEntry((entry) => wsServer.broadcast(entry));
1766
3332
  logger.log(`[Admin] WebSocket 已附加 (ws://localhost:${PORT})`);
1767
3333
 
3334
+ // 初始化 MCP 客户端
3335
+ mcpClient.init({
3336
+ onUpdate: (serverName, status) => {
3337
+ wsServer.broadcast({ type: 'mcp_status', server: serverName, ...status });
3338
+ }
3339
+ }).then(() => {
3340
+ const status = mcpClient.getStatus();
3341
+ const connected = status.filter(s => s.status === 'connected').length;
3342
+ if (status.length > 0) logger.log(`[MCP] ${connected}/${status.length} 个 MCP 服务已连接`);
3343
+ }).catch(err => {
3344
+ logger.error('[MCP] 初始化失败:', err.message);
3345
+ });
3346
+
1768
3347
  openBrowser(adminUrl);
1769
3348
  });
1770
3349
  }
@@ -1776,6 +3355,8 @@ process.on('SIGINT', async () => {
1776
3355
  try {
1777
3356
  const wsServer = require('./lib/ws-server');
1778
3357
  wsServer.close();
3358
+ const mcpClient = require('./lib/mcp-client');
3359
+ await mcpClient.shutdown();
1779
3360
  const proxyManager = require('./lib/proxy-manager');
1780
3361
  const statsStore = require('./lib/stats-store');
1781
3362
  statsStore.flush();
@@ -1791,6 +3372,8 @@ process.on('SIGTERM', async () => {
1791
3372
  try {
1792
3373
  const wsServer = require('./lib/ws-server');
1793
3374
  wsServer.close();
3375
+ const mcpClient = require('./lib/mcp-client');
3376
+ await mcpClient.shutdown();
1794
3377
  const proxyManager = require('./lib/proxy-manager');
1795
3378
  const statsStore = require('./lib/stats-store');
1796
3379
  statsStore.flush();