protocol-proxy 2.6.0 → 2.8.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
@@ -249,6 +249,89 @@ async function init() {
249
249
  return proxyManager.startProxy(proxyConfig);
250
250
  }
251
251
 
252
+ // ==================== API Key 健康检查 ====================
253
+
254
+ const keyHealth = new Map(); // providerId -> { status, lastCheck, keys: [{index, ok, message}] }
255
+ let healthCheckRunning = false;
256
+
257
+ async function checkAllProviderKeys() {
258
+ if (healthCheckRunning) return;
259
+ healthCheckRunning = true;
260
+ try {
261
+ const providers = configStore.getProviders();
262
+ logger.log(`[Health] 开始检查 ${providers.length} 个供应商的 API Key...`);
263
+ for (const provider of providers) {
264
+ await checkProviderKeys(provider);
265
+ }
266
+ logger.log('[Health] API Key 健康检查完成');
267
+ } finally {
268
+ healthCheckRunning = false;
269
+ }
270
+ }
271
+
272
+ async function checkProviderKeys(provider) {
273
+ const keys = (provider.apiKeys || []).filter(k => k.enabled !== false);
274
+ if (keys.length === 0) {
275
+ keyHealth.set(provider.id, { status: 'unknown', lastCheck: Date.now(), keys: [] });
276
+ return;
277
+ }
278
+
279
+ const protocol = provider.protocol || 'openai';
280
+ const base = provider.url.replace(/\/$/, '');
281
+ const hasV1Suffix = base.endsWith('/v1');
282
+ const isAzure = protocol === 'openai' && !!provider.azureDeployment;
283
+
284
+ const results = await Promise.all(keys.map(async (k, i) => {
285
+ try {
286
+ let testUrl, fetchOpts;
287
+ if (protocol === 'openai') {
288
+ if (isAzure) {
289
+ const ver = provider.azureApiVersion || '2024-02-01';
290
+ testUrl = `${base}/openai/deployments/${provider.azureDeployment}/models?api-version=${ver}`;
291
+ fetchOpts = { headers: { 'api-key': k.key } };
292
+ } else {
293
+ testUrl = hasV1Suffix ? `${base}/models` : `${base}/v1/models`;
294
+ fetchOpts = { headers: { 'Authorization': `Bearer ${k.key}` } };
295
+ }
296
+ } else if (protocol === 'anthropic') {
297
+ const testModel = (provider.models && provider.models[0]) || 'claude-3-haiku-20240307';
298
+ testUrl = hasV1Suffix ? `${base}/messages` : `${base}/v1/messages`;
299
+ fetchOpts = {
300
+ method: 'POST',
301
+ headers: { 'Content-Type': 'application/json', 'x-api-key': k.key, 'anthropic-version': '2023-06-01' },
302
+ body: JSON.stringify({ model: testModel, max_tokens: 1, messages: [{ role: 'user', content: 'hi' }] }),
303
+ };
304
+ } else if (protocol === 'gemini') {
305
+ testUrl = `${base}/v1beta/models?key=${k.key}`;
306
+ fetchOpts = {};
307
+ } else {
308
+ return { index: i, ok: false, message: '不支持的协议' };
309
+ }
310
+ const res = await fetch(testUrl, { ...fetchOpts, signal: AbortSignal.timeout(15000) });
311
+ if (!res.ok) {
312
+ const hint = res.status === 401 || res.status === 403 ? 'Key 无效或无权限' : `HTTP ${res.status}`;
313
+ return { index: i, ok: false, message: hint };
314
+ }
315
+ return { index: i, ok: true };
316
+ } catch (err) {
317
+ return { index: i, ok: false, message: err.name === 'TimeoutError' ? '连接超时' : err.message };
318
+ }
319
+ }));
320
+
321
+ const allOk = results.every(r => r.ok);
322
+ const anyOk = results.some(r => r.ok);
323
+ keyHealth.set(provider.id, {
324
+ status: allOk ? 'healthy' : anyOk ? 'partial' : 'unhealthy',
325
+ lastCheck: Date.now(),
326
+ keys: results,
327
+ });
328
+ }
329
+
330
+ // 启动后延迟 5 秒执行首次检查
331
+ setTimeout(() => checkAllProviderKeys(), 5000);
332
+ // 每 24 小时检查一次
333
+ setInterval(() => checkAllProviderKeys(), 24 * 60 * 60 * 1000);
334
+
252
335
  // ==================== 供应商 API ====================
253
336
 
254
337
  app.get('/api/providers', (req, res) => {
@@ -551,6 +634,7 @@ async function init() {
551
634
 
552
635
  // 创建代理
553
636
  app.post('/api/proxies', async (req, res) => {
637
+ configStore.saveSnapshot('create-proxy');
554
638
  const { name, port, requireAuth, authToken, providerId, defaultModel, routingStrategy, providerPool, providerWeight } = req.body;
555
639
 
556
640
  if (!name || !port || !providerId) {
@@ -592,6 +676,7 @@ async function init() {
592
676
 
593
677
  // 更新代理
594
678
  app.put('/api/proxies/:id', async (req, res) => {
679
+ configStore.saveSnapshot('update-proxy');
595
680
  const existing = configStore.getProxyById(req.params.id);
596
681
  if (!existing) return res.status(404).json({ error: 'Proxy not found' });
597
682
 
@@ -640,6 +725,7 @@ async function init() {
640
725
 
641
726
  // 删除代理
642
727
  app.delete('/api/proxies/:id', async (req, res) => {
728
+ configStore.saveSnapshot('delete-proxy');
643
729
  const existing = configStore.getProxyById(req.params.id);
644
730
  if (!existing) return res.status(404).json({ error: 'Proxy not found' });
645
731
 
@@ -666,6 +752,36 @@ async function init() {
666
752
  res.json({ success: true, running: false });
667
753
  });
668
754
 
755
+ // 批量启动所有代理
756
+ app.post('/api/proxies/start-all', async (req, res) => {
757
+ const proxies = configStore.getProxies();
758
+ const results = [];
759
+ for (const proxy of proxies) {
760
+ if (proxyManager.isRunning(proxy.id)) {
761
+ results.push({ id: proxy.id, name: proxy.name, skipped: true });
762
+ continue;
763
+ }
764
+ try {
765
+ await startProxyWithProvider(proxy);
766
+ results.push({ id: proxy.id, name: proxy.name, success: true });
767
+ } catch (err) {
768
+ results.push({ id: proxy.id, name: proxy.name, success: false, error: err.message });
769
+ }
770
+ }
771
+ res.json({ results });
772
+ });
773
+
774
+ // 批量停止所有代理
775
+ app.post('/api/proxies/stop-all', async (req, res) => {
776
+ const running = proxyManager.getRunningPorts();
777
+ const results = [];
778
+ for (const r of running) {
779
+ await proxyManager.stopProxy(r.id);
780
+ results.push({ id: r.id, name: r.name, success: true });
781
+ }
782
+ res.json({ results });
783
+ });
784
+
669
785
  // 获取运行状态
670
786
  app.get('/api/status', (req, res) => {
671
787
  res.json({
@@ -687,6 +803,37 @@ async function init() {
687
803
  });
688
804
  });
689
805
 
806
+ // API Key 健康状态
807
+ app.get('/api/key-health', (req, res) => {
808
+ const result = {};
809
+ for (const [providerId, health] of keyHealth) {
810
+ result[providerId] = health;
811
+ }
812
+ res.json(result);
813
+ });
814
+
815
+ // 手动触发健康检查
816
+ app.post('/api/key-health/check', async (req, res) => {
817
+ await checkAllProviderKeys();
818
+ res.json({ success: true });
819
+ });
820
+
821
+ // 设置
822
+ app.get('/api/settings', (req, res) => {
823
+ res.json(configStore.getSettings());
824
+ });
825
+
826
+ app.put('/api/settings', (req, res) => {
827
+ const settings = req.body;
828
+ if (!settings || typeof settings !== 'object') {
829
+ return res.status(400).json({ error: '需要 settings 对象' });
830
+ }
831
+ for (const [key, value] of Object.entries(settings)) {
832
+ configStore.setSetting(key, value);
833
+ }
834
+ res.json(configStore.getSettings());
835
+ });
836
+
690
837
  // Token 用量统计
691
838
  app.get('/api/stats', (req, res) => {
692
839
  const { range, startDate, endDate, proxyId } = req.query;
@@ -704,6 +851,22 @@ async function init() {
704
851
  res.json({ ...stats, proxies });
705
852
  });
706
853
 
854
+ // 日志查看
855
+ app.get('/api/logs', (req, res) => {
856
+ const lines = Math.min(parseInt(req.query.lines) || 200, 2000);
857
+ try {
858
+ if (!fs.existsSync(logger.LOG_FILE)) {
859
+ return res.json({ lines: [] });
860
+ }
861
+ const content = fs.readFileSync(logger.LOG_FILE, 'utf8');
862
+ const allLines = content.split('\n').filter(l => l.trim());
863
+ const tail = allLines.slice(-lines);
864
+ res.json({ lines: tail, total: allLines.length });
865
+ } catch (err) {
866
+ res.json({ lines: [], error: err.message });
867
+ }
868
+ });
869
+
707
870
  // ==================== 配置导入/导出 ====================
708
871
 
709
872
  app.get('/api/config/export', (req, res) => {
@@ -733,6 +896,8 @@ async function init() {
733
896
  return res.status(400).json({ error: '需要 config 和 mode(overwrite/merge)' });
734
897
  }
735
898
 
899
+ configStore.saveSnapshot('import-' + mode);
900
+
736
901
  // 校验结构
737
902
  if (!Array.isArray(config.providers) || !Array.isArray(config.proxies)) {
738
903
  return res.status(400).json({ error: '配置格式错误:需要 providers 和 proxies 数组' });
@@ -837,6 +1002,21 @@ async function init() {
837
1002
  });
838
1003
  });
839
1004
 
1005
+ // ==================== 配置版本历史 ====================
1006
+
1007
+ app.get('/api/config/history', (req, res) => {
1008
+ const snapshots = configStore.getSnapshots();
1009
+ res.json({ snapshots });
1010
+ });
1011
+
1012
+ app.post('/api/config/rollback', async (req, res) => {
1013
+ const { file } = req.body;
1014
+ if (!file) return res.status(400).json({ error: '需要指定快照文件' });
1015
+ const result = configStore.restoreSnapshot(file);
1016
+ if (result.error) return res.status(400).json({ error: result.error });
1017
+ res.json({ success: true });
1018
+ });
1019
+
840
1020
  // 前端首页
841
1021
  app.get('/', (req, res) => {
842
1022
  res.sendFile(path.join(__dirname, 'public', 'index.html'));