protocol-proxy 2.8.0 → 2.8.2

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
@@ -456,7 +456,7 @@ async function init() {
456
456
  };
457
457
  }
458
458
  if (protocol === 'anthropic') {
459
- const testModel = req.body.model || 'claude-3-haiku-20240307';
459
+ const testModel = req.body.model || (provider.models && provider.models[0]) || 'claude-3-haiku-20240307';
460
460
  return {
461
461
  url: hasV1Suffix ? `${base}/messages` : `${base}/v1/messages`,
462
462
  opts: {
@@ -501,6 +501,103 @@ async function init() {
501
501
  res.json({ ok: failed === 0, passed, failed, total: results.length, results });
502
502
  });
503
503
 
504
+ app.post('/api/test-connection', async (req, res) => {
505
+ const { url, protocol, apiKeys, models, azureDeployment, azureApiVersion } = req.body || {};
506
+ if (!url || !protocol) return res.json({ ok: false, message: '缺少 url 或 protocol', results: [] });
507
+ if (!Array.isArray(apiKeys) || apiKeys.length === 0) {
508
+ return res.json({ ok: false, message: '没有可用的 API Key', results: [] });
509
+ }
510
+ const keys = apiKeys.filter(k => k && k.key);
511
+ if (keys.length === 0) return res.json({ ok: false, message: '没有可用的 API Key', results: [] });
512
+ const base = url.replace(/\/$/, '');
513
+ const hasV1Suffix = base.endsWith('/v1');
514
+ const isAzure = protocol === 'openai' && !!azureDeployment;
515
+
516
+ function buildTestOpts(key) {
517
+ if (protocol === 'openai') {
518
+ if (isAzure) {
519
+ const ver = azureApiVersion || '2024-02-01';
520
+ return { url: `${base}/openai/deployments/${azureDeployment}/models?api-version=${ver}`, opts: { headers: { 'api-key': key } } };
521
+ }
522
+ return { url: hasV1Suffix ? `${base}/models` : `${base}/v1/models`, opts: { headers: { 'Authorization': `Bearer ${key}` } } };
523
+ }
524
+ if (protocol === 'anthropic') {
525
+ const testModel = (Array.isArray(models) && models[0]) || 'claude-3-haiku-20240307';
526
+ return {
527
+ url: hasV1Suffix ? `${base}/messages` : `${base}/v1/messages`,
528
+ 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' }] }) },
529
+ };
530
+ }
531
+ if (protocol === 'gemini') return { url: `${base}/v1beta/models?key=${key}`, opts: {} };
532
+ return null;
533
+ }
534
+
535
+ const results = await Promise.all(keys.map(async (k) => {
536
+ const built = buildTestOpts(k.key);
537
+ if (!built) return { ok: false, alias: k.alias || '', message: '不支持的协议' };
538
+ try {
539
+ const started = Date.now();
540
+ const fetchRes = await fetch(built.url, { ...built.opts, signal: AbortSignal.timeout(15000) });
541
+ const latency = Date.now() - started;
542
+ if (!fetchRes.ok) {
543
+ const hint = fetchRes.status === 401 || fetchRes.status === 403 ? 'API Key 无效或无权限' : `HTTP ${fetchRes.status}`;
544
+ return { ok: false, alias: k.alias || '', message: hint, latency };
545
+ }
546
+ return { ok: true, alias: k.alias || '', latency };
547
+ } catch (err) {
548
+ return { ok: false, alias: k.alias || '', message: err.name === 'TimeoutError' ? '连接超时' : err.message };
549
+ }
550
+ }));
551
+
552
+ const passed = results.filter(r => r.ok).length;
553
+ res.json({ ok: passed === keys.length, passed, failed: keys.length - passed, results });
554
+ });
555
+
556
+ app.post('/api/providers/available-models', async (req, res) => {
557
+ const { url, protocol, apiKey, azureDeployment, azureApiVersion } = req.body || {};
558
+ if (!url || !protocol) return res.json({ models: [], message: '缺少 url 或 protocol 参数' });
559
+ const key = apiKey || '';
560
+ const base = url.replace(/\/$/, '');
561
+ const hasV1Suffix = base.endsWith('/v1');
562
+ const isAzure = protocol === 'openai' && !!azureDeployment;
563
+ try {
564
+ let fetchUrl, fetchOpts;
565
+ if (protocol === 'openai') {
566
+ if (isAzure) {
567
+ const ver = azureApiVersion || '2024-02-01';
568
+ fetchUrl = `${base}/openai/deployments/${azureDeployment}/models?api-version=${ver}`;
569
+ fetchOpts = { headers: { 'api-key': key } };
570
+ } else {
571
+ fetchUrl = hasV1Suffix ? `${base}/models` : `${base}/v1/models`;
572
+ fetchOpts = key ? { headers: { 'Authorization': `Bearer ${key}` } } : {};
573
+ }
574
+ } else if (protocol === 'gemini') {
575
+ fetchUrl = `${base}/v1beta/models?key=${key}`;
576
+ fetchOpts = {};
577
+ } else if (protocol === 'anthropic') {
578
+ fetchUrl = hasV1Suffix ? `${base}/models` : `${base}/v1/models`;
579
+ fetchOpts = key ? { headers: { 'x-api-key': key, 'anthropic-version': '2023-06-01' } } : {};
580
+ } else {
581
+ return res.json({ models: [], message: `不支持的协议: ${protocol}` });
582
+ }
583
+ const fetchRes = await fetch(fetchUrl, { ...fetchOpts, signal: AbortSignal.timeout(15000) });
584
+ if (!fetchRes.ok) {
585
+ const hint = fetchRes.status === 404 ? '该供应商不支持模型列表接口' : `获取失败: HTTP ${fetchRes.status}`;
586
+ return res.json({ models: [], message: hint });
587
+ }
588
+ const data = await fetchRes.json().catch(() => null);
589
+ let models = [];
590
+ if (Array.isArray(data?.data)) {
591
+ models = data.data.map(m => m.id || m.name).filter(Boolean).sort();
592
+ } else if (Array.isArray(data?.models)) {
593
+ models = data.models.map(m => (m.name || m.id)?.replace('models/', '')).filter(Boolean).sort();
594
+ }
595
+ res.json({ models });
596
+ } catch (err) {
597
+ res.json({ models: [], message: `获取失败: ${err.message}` });
598
+ }
599
+ });
600
+
504
601
  app.post('/api/providers/:id/available-models', async (req, res) => {
505
602
  const provider = configStore.getProviderById(req.params.id);
506
603
  if (!provider) return res.status(404).json({ error: 'Provider not found' });
@@ -867,6 +964,13 @@ async function init() {
867
964
  }
868
965
  });
869
966
 
967
+ // 实时请求日志
968
+ const requestLog = require('./lib/request-log');
969
+ app.get('/api/request-logs', (req, res) => {
970
+ const limit = Math.min(parseInt(req.query.limit) || 200, 2000);
971
+ res.json({ entries: requestLog.getAll(limit), total: requestLog.getCount() });
972
+ });
973
+
870
974
  // ==================== 配置导入/导出 ====================
871
975
 
872
976
  app.get('/api/config/export', (req, res) => {
@@ -1036,11 +1140,19 @@ async function init() {
1036
1140
  }
1037
1141
  }));
1038
1142
 
1039
- app.listen(PORT, () => {
1143
+ const http = require('http');
1144
+ const server = app.listen(PORT, () => {
1040
1145
  const adminUrl = `http://localhost:${PORT}`;
1041
1146
  logger.log(`[Admin] Management server running on ${adminUrl}`);
1042
1147
  logger.log(`[Admin] ${proxies.length} proxy config(s) loaded`);
1043
1148
  logger.log(`[Admin] 日志文件: ${logger.LOG_FILE}`);
1149
+
1150
+ // 初始化 WebSocket 实时日志
1151
+ const wsServer = require('./lib/ws-server');
1152
+ wsServer.init(server);
1153
+ requestLog.onEntry((entry) => wsServer.broadcast(entry));
1154
+ logger.log(`[Admin] WebSocket 已附加 (ws://localhost:${PORT})`);
1155
+
1044
1156
  openBrowser(adminUrl);
1045
1157
  });
1046
1158
  }
@@ -1050,6 +1162,8 @@ process.on('SIGINT', async () => {
1050
1162
  logger.log('[Shutdown] Shutting down...');
1051
1163
  removePid();
1052
1164
  try {
1165
+ const wsServer = require('./lib/ws-server');
1166
+ wsServer.close();
1053
1167
  const proxyManager = require('./lib/proxy-manager');
1054
1168
  const statsStore = require('./lib/stats-store');
1055
1169
  statsStore.flush();
@@ -1063,6 +1177,8 @@ process.on('SIGINT', async () => {
1063
1177
  process.on('SIGTERM', async () => {
1064
1178
  removePid();
1065
1179
  try {
1180
+ const wsServer = require('./lib/ws-server');
1181
+ wsServer.close();
1066
1182
  const proxyManager = require('./lib/proxy-manager');
1067
1183
  const statsStore = require('./lib/stats-store');
1068
1184
  statsStore.flush();