protocol-proxy 2.9.0 → 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) => {
@@ -614,6 +615,425 @@ ${recentUserMsgs.map((m, i) => `${i + 1}. ${m}`).join('\n')}
614
615
  },
615
616
  },
616
617
  },
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
+ },
631
+ },
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
+ },
651
+ },
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
+ },
617
1037
  ];
618
1038
 
619
1039
  const TOOL_HANDLERS = {
@@ -866,6 +1286,480 @@ ${recentUserMsgs.map((m, i) => `${i + 1}. ${m}`).join('\n')}
866
1286
  return { error: err.message };
867
1287
  }
868
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 };
1591
+ },
1592
+
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 };
1620
+ },
1621
+
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 };
1628
+ },
1629
+
1630
+ connect_mcp_server: async (args) => {
1631
+ const config = configStore.getMcpServer(args.name);
1632
+ if (!config) return { error: `MCP 服务 "${args.name}" 不存在` };
1633
+ try {
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 };
1639
+ }
1640
+ },
1641
+
1642
+ disconnect_mcp_server: async (args) => {
1643
+ await mcpClient.disconnectServer(args.name);
1644
+ return { success: true };
1645
+ },
1646
+
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
+ );
1652
+ },
1653
+
1654
+ // --- 技能管理 ---
1655
+ get_skills: async () => {
1656
+ return { skills: skillStore.list() };
1657
+ },
1658
+
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 };
1664
+ },
1665
+
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 };
1670
+ },
1671
+
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),
1708
+ })),
1709
+ };
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 : [] });
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 };
1729
+ },
1730
+
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);
1743
+ }
1744
+ return { success: true, settings: configStore.getSettings() };
1745
+ },
1746
+
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
+ };
1762
+ },
869
1763
  };
870
1764
 
871
1765
  async function startProxyWithProvider(proxy) {
@@ -1654,6 +2548,11 @@ ${recentUserMsgs.map((m, i) => `${i + 1}. ${m}`).join('\n')}
1654
2548
  const conversationStore = require('./lib/conversation-store');
1655
2549
  conversationStore.init();
1656
2550
 
2551
+ const skillStore = require('./lib/skill-store');
2552
+ skillStore.init();
2553
+
2554
+ const promptBuilder = require('./lib/prompt-builder');
2555
+
1657
2556
  // 会话并发锁:convId → true 表示正在 streaming
1658
2557
  const activeStreams = new Set();
1659
2558
 
@@ -1696,48 +2595,201 @@ ${recentUserMsgs.map((m, i) => `${i + 1}. ${m}`).join('\n')}
1696
2595
  res.json({ providers, defaultModel: proxy.defaultModel || '' });
1697
2596
  });
1698
2597
 
1699
- function buildSystemPrompt() {
1700
- const now = new Date().toLocaleString('zh-CN', { hour12: false });
1701
- return `你是 Protocol Proxy 的智能助手,专门帮助管理员监控和排障。当前时间:${now}
1702
-
1703
- 你有以下工具可以调用:
1704
-
1705
- 系统查询:
1706
- - get_system_status: 获取系统概览(代理运行状态、供应商数量、运行时长)
1707
- - get_providers / get_provider: 获取供应商列表或详情
1708
- - get_proxies / get_proxy: 获取代理列表或详情
1709
- - get_usage_stats: 查询用量统计(支持按时间范围、代理筛选)
1710
- - get_recent_requests: 获取最近请求日志
1711
- - get_system_logs: 获取系统日志
1712
- - get_key_health: 获取 API Key 健康检查结果
1713
- - get_settings: 获取系统设置项
1714
- - get_config_history: 获取配置快照历史
1715
-
1716
- 文件与命令:
1717
- - read_file: 读取任意文件内容(支持指定行范围,自动检测二进制文件)
1718
- - write_file: 写入文件(会覆盖已有内容)
1719
- - edit_file: 精确替换文件中的字符串(比 write_file 更安全,只替换匹配内容)
1720
- - list_directory: 列出目录内容
1721
- - search_files: 按文件名 glob 模式搜索文件
1722
- - grep_search: 按正则表达式搜索文件内容(用于查找代码、日志关键字等)
1723
- - execute_command: 执行 shell 命令
1724
-
1725
- 规则:
1726
- - 当用户询问系统状态、代理、供应商、日志、用量等运维相关问题时,调用工具获取实时数据后再回答
1727
- - 当用户需要查看或修改文件、执行命令时,使用对应的文件和命令工具
1728
- - 当用户只是打招呼、闲聊、或询问与系统无关的问题时,直接回答,不要调用工具
1729
- - 不要凭空猜测系统状态,需要数据时必须调用工具
1730
- - 执行写操作或危险命令前,先告知用户将要做什么
1731
-
1732
- 你的职责:
1733
- 1. 回答关于代理配置和运行状态的问题
1734
- 2. 分析日志,指出异常和可能原因
1735
- 3. 根据数据给出优化建议(负载均衡、模型选择、故障切换策略)
1736
- 4. 用自然语言解释技术问题
1737
- 5. 如果发现问题,给出具体的修复步骤
1738
-
1739
- 请用中文回答,保持专业且易懂。`;
1740
- }
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
+
1741
2793
 
1742
2794
  app.post('/api/assistant/chat', async (req, res) => {
1743
2795
  const { proxyId, conversationId, message, compress, providerId, model } = req.body;
@@ -1773,8 +2825,26 @@ ${recentUserMsgs.map((m, i) => `${i + 1}. ${m}`).join('\n')}
1773
2825
  conversationStore.touch(conv);
1774
2826
 
1775
2827
  // 追加用户消息到对话历史(压缩请求不追加空消息)
2828
+ // 检测 /skillname 前缀触发技能
2829
+ let activeSkill = null;
1776
2830
  if (!compress && message) {
1777
- conv.messages.push({ role: 'user', content: 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
+ }
1778
2848
  conversationStore.touch(conv);
1779
2849
  }
1780
2850
 
@@ -1835,9 +2905,15 @@ ${recentUserMsgs.map((m, i) => `${i + 1}. ${m}`).join('\n')}
1835
2905
 
1836
2906
  try {
1837
2907
  // 请求级别缓存 system prompt(避免每轮重建导致 prompt cache 失效)
1838
- const systemPrompt = buildSystemPrompt();
2908
+ const systemPrompt = promptBuilder.buildSystemPrompt({ skillStore, mcpClient });
1839
2909
  const buildMessages = () => {
1840
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
+ }
1841
2917
  if (conv.compressionSummary) {
1842
2918
  msgs.push({ role: 'system', content: `[压缩摘要]\n${conv.compressionSummary}\n\n---\n以上是之前对话的压缩摘要。最近的消息保留原文。请继续对话,不要复述摘要内容。` });
1843
2919
  }
@@ -1866,7 +2942,7 @@ ${recentUserMsgs.map((m, i) => `${i + 1}. ${m}`).join('\n')}
1866
2942
  model: proxy.defaultModel || 'gpt-4o',
1867
2943
  messages,
1868
2944
  stream: true,
1869
- tools: TOOL_DEFINITIONS,
2945
+ tools: [...TOOL_DEFINITIONS, ...mcpClient.getToolDefinitions()],
1870
2946
  tool_choice: 'auto',
1871
2947
  }),
1872
2948
  });
@@ -1974,7 +3050,8 @@ ${recentUserMsgs.map((m, i) => `${i + 1}. ${m}`).join('\n')}
1974
3050
  result = { error: `工具 ${tc.name} 的参数 JSON 解析失败,原始内容: ${(tc.arguments || '').slice(0, 200)}` };
1975
3051
  isError = true;
1976
3052
  } else try {
1977
- result = await TOOL_HANDLERS[tc.name]?.(args) || { error: `未知工具: ${tc.name}` };
3053
+ const mcpHandler = tc.name.startsWith('mcp__') ? mcpClient.getToolHandlerMap()[tc.name] : null;
3054
+ result = await (TOOL_HANDLERS[tc.name] || mcpHandler)?.(args) || { error: `未知工具: ${tc.name}` };
1978
3055
  if (result && result.error) isError = true;
1979
3056
  } catch (err) {
1980
3057
  logger.log(`[assistant] tool ${tc.name} error: ${err.message}`);
@@ -2254,6 +3331,19 @@ ${recentUserMsgs.map((m, i) => `${i + 1}. ${m}`).join('\n')}
2254
3331
  requestLog.onEntry((entry) => wsServer.broadcast(entry));
2255
3332
  logger.log(`[Admin] WebSocket 已附加 (ws://localhost:${PORT})`);
2256
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
+
2257
3347
  openBrowser(adminUrl);
2258
3348
  });
2259
3349
  }
@@ -2265,6 +3355,8 @@ process.on('SIGINT', async () => {
2265
3355
  try {
2266
3356
  const wsServer = require('./lib/ws-server');
2267
3357
  wsServer.close();
3358
+ const mcpClient = require('./lib/mcp-client');
3359
+ await mcpClient.shutdown();
2268
3360
  const proxyManager = require('./lib/proxy-manager');
2269
3361
  const statsStore = require('./lib/stats-store');
2270
3362
  statsStore.flush();
@@ -2280,6 +3372,8 @@ process.on('SIGTERM', async () => {
2280
3372
  try {
2281
3373
  const wsServer = require('./lib/ws-server');
2282
3374
  wsServer.close();
3375
+ const mcpClient = require('./lib/mcp-client');
3376
+ await mcpClient.shutdown();
2283
3377
  const proxyManager = require('./lib/proxy-manager');
2284
3378
  const statsStore = require('./lib/stats-store');
2285
3379
  statsStore.flush();