protocol-proxy 2.5.1 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/server.js CHANGED
@@ -329,6 +329,169 @@ async function init() {
329
329
  res.json(updated);
330
330
  });
331
331
 
332
+ app.post('/api/providers/:id/test', async (req, res) => {
333
+ const provider = configStore.getProviderById(req.params.id);
334
+ if (!provider) return res.status(404).json({ error: 'Provider not found' });
335
+
336
+ const existingKeys = provider.apiKeys || [];
337
+ const reqKeys = Array.isArray(req.body.apiKeys) ? req.body.apiKeys : [];
338
+ const resolved = reqKeys
339
+ .map((k, i) => {
340
+ if (k && typeof k === 'object' && k.masked && typeof k.index === 'number') {
341
+ const ex = existingKeys[k.index];
342
+ return ex ? { key: ex.key, alias: k.alias || ex.alias || '', domIndex: i } : null;
343
+ }
344
+ if (k && typeof k === 'object' && typeof k.key === 'string' && k.key.trim()) {
345
+ return { key: k.key.trim(), alias: k.alias || '', domIndex: i };
346
+ }
347
+ if (typeof k === 'string' && k.trim()) return { key: k.trim(), alias: '', domIndex: i };
348
+ return null;
349
+ })
350
+ .filter(Boolean);
351
+
352
+ if (resolved.length === 0) {
353
+ return res.json({ ok: false, message: '没有可用的 API Key', results: [] });
354
+ }
355
+
356
+ const protocol = req.body.protocol || provider.protocol || 'openai';
357
+ const base = provider.url.replace(/\/$/, '');
358
+ const hasV1Suffix = base.endsWith('/v1');
359
+ const isAzure = protocol === 'openai' && !!provider.azureDeployment;
360
+
361
+ function buildTestOpts(key) {
362
+ if (protocol === 'openai') {
363
+ if (isAzure) {
364
+ const ver = provider.azureApiVersion || '2024-02-01';
365
+ return {
366
+ url: `${base}/openai/deployments/${provider.azureDeployment}/models?api-version=${ver}`,
367
+ opts: { headers: { 'api-key': key } },
368
+ };
369
+ }
370
+ return {
371
+ url: hasV1Suffix ? `${base}/models` : `${base}/v1/models`,
372
+ opts: { headers: { 'Authorization': `Bearer ${key}` } },
373
+ };
374
+ }
375
+ if (protocol === 'anthropic') {
376
+ const testModel = req.body.model || 'claude-3-haiku-20240307';
377
+ return {
378
+ url: hasV1Suffix ? `${base}/messages` : `${base}/v1/messages`,
379
+ opts: {
380
+ method: 'POST',
381
+ headers: { 'Content-Type': 'application/json', 'x-api-key': key, 'anthropic-version': '2023-06-01' },
382
+ body: JSON.stringify({ model: testModel, max_tokens: 1, messages: [{ role: 'user', content: 'hi' }] }),
383
+ },
384
+ };
385
+ }
386
+ if (protocol === 'gemini') {
387
+ return { url: `${base}/v1beta/models?key=${key}`, opts: {} };
388
+ }
389
+ return null;
390
+ }
391
+
392
+ if (protocol !== 'openai' && protocol !== 'anthropic' && protocol !== 'gemini') {
393
+ return res.json({ ok: false, message: `不支持的协议: ${protocol}`, results: [] });
394
+ }
395
+
396
+ const results = await Promise.all(resolved.map(async entry => {
397
+ const { url: testUrl, opts: fetchOpts } = buildTestOpts(entry.key);
398
+ try {
399
+ const startedAt = Date.now();
400
+ const fetchRes = await fetch(testUrl, { ...fetchOpts, signal: AbortSignal.timeout(15000) });
401
+ const latencyMs = Date.now() - startedAt;
402
+ if (!fetchRes.ok) {
403
+ const errText = await fetchRes.text().catch(() => '');
404
+ const hint = fetchRes.status === 401 || fetchRes.status === 403
405
+ ? 'API Key 无效或无权限'
406
+ : `HTTP ${fetchRes.status}: ${errText.slice(0, 200) || '未知错误'}`;
407
+ return { ok: false, alias: entry.alias, index: entry.domIndex, message: hint, latencyMs };
408
+ }
409
+ return { ok: true, alias: entry.alias, index: entry.domIndex, latencyMs };
410
+ } catch (err) {
411
+ const msg = err.name === 'TimeoutError' ? '连接超时 (15s)' : `连接失败: ${err.message}`;
412
+ return { ok: false, alias: entry.alias, index: entry.domIndex, message: msg };
413
+ }
414
+ }));
415
+
416
+ const passed = results.filter(r => r.ok).length;
417
+ const failed = results.length - passed;
418
+ res.json({ ok: failed === 0, passed, failed, total: results.length, results });
419
+ });
420
+
421
+ app.post('/api/providers/:id/available-models', async (req, res) => {
422
+ const provider = configStore.getProviderById(req.params.id);
423
+ if (!provider) return res.status(404).json({ error: 'Provider not found' });
424
+
425
+ // Support unsaved API keys from form
426
+ let keys;
427
+ const reqKeys = Array.isArray(req.body?.apiKeys) ? req.body.apiKeys : [];
428
+ if (reqKeys.length > 0) {
429
+ const existingKeys = provider.apiKeys || [];
430
+ keys = reqKeys
431
+ .map(k => {
432
+ if (k && typeof k === 'object' && k.masked && typeof k.index === 'number') {
433
+ return existingKeys[k.index]?.key || null;
434
+ }
435
+ if (k && typeof k === 'object' && typeof k.key === 'string' && k.key.trim()) {
436
+ return k.key.trim();
437
+ }
438
+ return null;
439
+ })
440
+ .filter(Boolean);
441
+ } else {
442
+ keys = (provider.apiKeys || []).map(k => k.key).filter(Boolean);
443
+ }
444
+ if (keys.length === 0) return res.json({ models: [], message: '没有可用的 API Key' });
445
+
446
+ const protocol = provider.protocol || 'openai';
447
+ const base = provider.url.replace(/\/$/, '');
448
+ const hasV1Suffix = base.endsWith('/v1');
449
+ const key = keys[0];
450
+ const isAzure = protocol === 'openai' && !!provider.azureDeployment;
451
+
452
+ try {
453
+ let fetchUrl, fetchOpts;
454
+ if (protocol === 'openai') {
455
+ if (isAzure) {
456
+ const ver = provider.azureApiVersion || '2024-02-01';
457
+ fetchUrl = `${base}/openai/deployments/${provider.azureDeployment}/models?api-version=${ver}`;
458
+ fetchOpts = { headers: { 'api-key': key } };
459
+ } else {
460
+ fetchUrl = hasV1Suffix ? `${base}/models` : `${base}/v1/models`;
461
+ fetchOpts = { headers: { 'Authorization': `Bearer ${key}` } };
462
+ }
463
+ } else if (protocol === 'gemini') {
464
+ fetchUrl = `${base}/v1beta/models?key=${key}`;
465
+ fetchOpts = {};
466
+ } else if (protocol === 'anthropic') {
467
+ fetchUrl = hasV1Suffix ? `${base}/models` : `${base}/v1/models`;
468
+ fetchOpts = { headers: { 'x-api-key': key, 'anthropic-version': '2023-06-01' } };
469
+ } else {
470
+ return res.json({ models: [], message: `不支持的协议: ${protocol}` });
471
+ }
472
+
473
+ const fetchRes = await fetch(fetchUrl, { ...fetchOpts, signal: AbortSignal.timeout(15000) });
474
+ if (!fetchRes.ok) {
475
+ const hint = fetchRes.status === 404 ? '该供应商不支持模型列表接口' : `获取失败: HTTP ${fetchRes.status}`;
476
+ return res.json({ models: [], message: hint });
477
+ }
478
+
479
+ const data = await fetchRes.json().catch(() => null);
480
+ let models = [];
481
+ if (Array.isArray(data?.data)) {
482
+ // OpenAI 格式(含第三方 Anthropic 兼容供应商)
483
+ models = data.data.map(m => m.id || m.name).filter(Boolean).sort();
484
+ } else if (Array.isArray(data?.models)) {
485
+ // Gemini 格式
486
+ models = data.models.map(m => (m.name || m.id)?.replace('models/', '')).filter(Boolean).sort();
487
+ }
488
+
489
+ res.json({ models });
490
+ } catch (err) {
491
+ res.json({ models: [], message: `获取失败: ${err.message}` });
492
+ }
493
+ });
494
+
332
495
  app.delete('/api/providers/:id', (req, res) => {
333
496
  const existing = configStore.getProviderById(req.params.id);
334
497
  if (!existing) return res.status(404).json({ error: 'Provider not found' });
@@ -388,6 +551,7 @@ async function init() {
388
551
 
389
552
  // 创建代理
390
553
  app.post('/api/proxies', async (req, res) => {
554
+ configStore.saveSnapshot('create-proxy');
391
555
  const { name, port, requireAuth, authToken, providerId, defaultModel, routingStrategy, providerPool, providerWeight } = req.body;
392
556
 
393
557
  if (!name || !port || !providerId) {
@@ -429,6 +593,7 @@ async function init() {
429
593
 
430
594
  // 更新代理
431
595
  app.put('/api/proxies/:id', async (req, res) => {
596
+ configStore.saveSnapshot('update-proxy');
432
597
  const existing = configStore.getProxyById(req.params.id);
433
598
  if (!existing) return res.status(404).json({ error: 'Proxy not found' });
434
599
 
@@ -477,6 +642,7 @@ async function init() {
477
642
 
478
643
  // 删除代理
479
644
  app.delete('/api/proxies/:id', async (req, res) => {
645
+ configStore.saveSnapshot('delete-proxy');
480
646
  const existing = configStore.getProxyById(req.params.id);
481
647
  if (!existing) return res.status(404).json({ error: 'Proxy not found' });
482
648
 
@@ -503,6 +669,36 @@ async function init() {
503
669
  res.json({ success: true, running: false });
504
670
  });
505
671
 
672
+ // 批量启动所有代理
673
+ app.post('/api/proxies/start-all', async (req, res) => {
674
+ const proxies = configStore.getProxies();
675
+ const results = [];
676
+ for (const proxy of proxies) {
677
+ if (proxyManager.isRunning(proxy.id)) {
678
+ results.push({ id: proxy.id, name: proxy.name, skipped: true });
679
+ continue;
680
+ }
681
+ try {
682
+ await startProxyWithProvider(proxy);
683
+ results.push({ id: proxy.id, name: proxy.name, success: true });
684
+ } catch (err) {
685
+ results.push({ id: proxy.id, name: proxy.name, success: false, error: err.message });
686
+ }
687
+ }
688
+ res.json({ results });
689
+ });
690
+
691
+ // 批量停止所有代理
692
+ app.post('/api/proxies/stop-all', async (req, res) => {
693
+ const running = proxyManager.getRunningPorts();
694
+ const results = [];
695
+ for (const r of running) {
696
+ await proxyManager.stopProxy(r.id);
697
+ results.push({ id: r.id, name: r.name, success: true });
698
+ }
699
+ res.json({ results });
700
+ });
701
+
506
702
  // 获取运行状态
507
703
  app.get('/api/status', (req, res) => {
508
704
  res.json({
@@ -524,6 +720,22 @@ async function init() {
524
720
  });
525
721
  });
526
722
 
723
+ // 设置
724
+ app.get('/api/settings', (req, res) => {
725
+ res.json(configStore.getSettings());
726
+ });
727
+
728
+ app.put('/api/settings', (req, res) => {
729
+ const settings = req.body;
730
+ if (!settings || typeof settings !== 'object') {
731
+ return res.status(400).json({ error: '需要 settings 对象' });
732
+ }
733
+ for (const [key, value] of Object.entries(settings)) {
734
+ configStore.setSetting(key, value);
735
+ }
736
+ res.json(configStore.getSettings());
737
+ });
738
+
527
739
  // Token 用量统计
528
740
  app.get('/api/stats', (req, res) => {
529
741
  const { range, startDate, endDate, proxyId } = req.query;
@@ -541,6 +753,22 @@ async function init() {
541
753
  res.json({ ...stats, proxies });
542
754
  });
543
755
 
756
+ // 日志查看
757
+ app.get('/api/logs', (req, res) => {
758
+ const lines = Math.min(parseInt(req.query.lines) || 200, 2000);
759
+ try {
760
+ if (!fs.existsSync(logger.LOG_FILE)) {
761
+ return res.json({ lines: [] });
762
+ }
763
+ const content = fs.readFileSync(logger.LOG_FILE, 'utf8');
764
+ const allLines = content.split('\n').filter(l => l.trim());
765
+ const tail = allLines.slice(-lines);
766
+ res.json({ lines: tail, total: allLines.length });
767
+ } catch (err) {
768
+ res.json({ lines: [], error: err.message });
769
+ }
770
+ });
771
+
544
772
  // ==================== 配置导入/导出 ====================
545
773
 
546
774
  app.get('/api/config/export', (req, res) => {
@@ -570,6 +798,8 @@ async function init() {
570
798
  return res.status(400).json({ error: '需要 config 和 mode(overwrite/merge)' });
571
799
  }
572
800
 
801
+ configStore.saveSnapshot('import-' + mode);
802
+
573
803
  // 校验结构
574
804
  if (!Array.isArray(config.providers) || !Array.isArray(config.proxies)) {
575
805
  return res.status(400).json({ error: '配置格式错误:需要 providers 和 proxies 数组' });
@@ -674,6 +904,21 @@ async function init() {
674
904
  });
675
905
  });
676
906
 
907
+ // ==================== 配置版本历史 ====================
908
+
909
+ app.get('/api/config/history', (req, res) => {
910
+ const snapshots = configStore.getSnapshots();
911
+ res.json({ snapshots });
912
+ });
913
+
914
+ app.post('/api/config/rollback', async (req, res) => {
915
+ const { file } = req.body;
916
+ if (!file) return res.status(400).json({ error: '需要指定快照文件' });
917
+ const result = configStore.restoreSnapshot(file);
918
+ if (result.error) return res.status(400).json({ error: result.error });
919
+ res.json({ success: true });
920
+ });
921
+
677
922
  // 前端首页
678
923
  app.get('/', (req, res) => {
679
924
  res.sendFile(path.join(__dirname, 'public', 'index.html'));