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/lib/config-store.js +83 -2
- package/package.json +1 -1
- package/public/app.js +337 -4
- package/public/index.html +58 -0
- package/public/style.css +411 -65
- package/server.js +180 -0
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'));
|