protocol-proxy 2.8.2 → 2.8.3

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
@@ -1,1226 +1,1838 @@
1
- #!/usr/bin/env node
2
- const path = require('path');
3
- const { exec, spawn } = require('child_process');
4
- const os = require('os');
5
- const fs = require('fs');
6
- const logger = require('./lib/logger');
7
-
8
- // ==================== CLI ====================
9
-
10
- const PID_FILE = path.join(os.tmpdir(), 'protocol-proxy.pid');
11
- const pkg = require('./package.json');
12
-
13
- function writePid() {
14
- try { fs.writeFileSync(PID_FILE, String(process.pid)); } catch (err) {
15
- console.error('[PID] 写入失败:', err.message);
16
- }
17
- }
18
-
19
- function readPid() {
20
- try { return parseInt(fs.readFileSync(PID_FILE, 'utf8').trim()); } catch { return null; }
21
- }
22
-
23
- function removePid() {
24
- try { fs.unlinkSync(PID_FILE); } catch (err) {
25
- if (err.code !== 'ENOENT') {
26
- console.error('[PID] 删除失败:', err.message);
27
- }
28
- }
29
- }
30
-
31
- function isProcessAlive(pid) {
32
- try { process.kill(pid, 0); return true; } catch { return false; }
33
- }
34
-
35
- function showHelp() {
36
- console.log(`
37
- protocol-proxy - OpenAI / Anthropic 协议转换透明代理
38
-
39
- 用法:
40
- protocol-proxy 前台启动服务(Ctrl+C 停止)
41
- protocol-proxy start 后台启动服务
42
- protocol-proxy stop 停止后台服务
43
- protocol-proxy status 查看运行状态
44
- protocol-proxy help 显示帮助信息
45
- protocol-proxy -v, --version 显示版本号
46
- protocol-proxy update 更新到最新版本
47
- `);
48
- }
49
-
50
- function startDaemon() {
51
- const pid = readPid();
52
- if (pid && isProcessAlive(pid)) {
53
- console.log(`服务已在运行 (PID: ${pid})`);
54
- return;
55
- }
56
-
57
- const child = spawn(process.execPath, [__filename, '--daemon'], {
58
- detached: true,
59
- stdio: 'ignore',
60
- });
61
- fs.writeFileSync(PID_FILE, String(child.pid));
62
- child.unref();
63
- console.log(`服务已在后台启动 (PID: ${child.pid})`);
64
- }
65
-
66
- function showVersion() {
67
- console.log(pkg.version);
68
- }
69
-
70
- function showStatus() {
71
- const pid = readPid();
72
- if (pid && isProcessAlive(pid)) {
73
- console.log(`服务正在运行 (PID: ${pid})`);
74
- const configStore = require('./lib/config-store');
75
- const proxies = configStore.getProxies();
76
- if (proxies.length > 0) {
77
- console.log(`\n已配置的代理 (${proxies.length} 个):`);
78
- for (const p of proxies) {
79
- console.log(` - ${p.name}: 端口 ${p.port} → ${p.target?.providerUrl || '未设置'}`);
80
- }
81
- }
82
- } else {
83
- removePid();
84
- console.log('服务未运行');
85
- }
86
- }
87
-
88
- function stopService() {
89
- const pid = readPid();
90
- if (!pid || !isProcessAlive(pid)) {
91
- removePid();
92
- console.log('服务未运行');
93
- return;
94
- }
95
- try {
96
- process.kill(pid, 'SIGTERM');
97
- removePid();
98
- console.log(`服务已停止 (PID: ${pid})`);
99
- } catch (err) {
100
- console.error('停止服务失败:', err.message);
101
- removePid();
102
- }
103
- }
104
-
105
- function updateService() {
106
- console.log('正在更新 protocol-proxy...');
107
- exec('npm install -g protocol-proxy@latest', (err, stdout, stderr) => {
108
- if (err) {
109
- console.error('更新失败:', err.message);
110
- process.exit(1);
111
- }
112
- if (stdout) console.log(stdout);
113
- if (stderr) console.error(stderr);
114
- console.log('更新完成');
115
- });
116
- }
117
-
118
- // ==================== 启动 ====================
119
-
120
- async function init() {
121
- const express = require('express');
122
- const cors = require('cors');
123
- const configStore = require('./lib/config-store');
124
- const proxyManager = require('./lib/proxy-manager');
125
- const statsStore = require('./lib/stats-store');
126
-
127
- const app = express();
128
- const PORT = process.env.ADMIN_PORT || 3000;
129
-
130
- function openBrowser(url) {
131
- const platform = os.platform();
132
- let command;
133
- if (platform === 'win32') {
134
- command = `start "" "${url}"`;
135
- } else if (platform === 'darwin') {
136
- command = `open "${url}"`;
137
- } else {
138
- command = `xdg-open "${url}"`;
139
- }
140
- exec(command, (err) => {
141
- if (err) logger.error('[Browser] 打开浏览器失败:', err.message);
142
- });
143
- }
144
-
145
- app.use(cors());
146
- app.use(express.json());
147
-
148
- // 访问日志
149
- app.use((req, res, next) => {
150
- const start = Date.now();
151
- res.on('finish', () => {
152
- const duration = Date.now() - start;
153
- logger.log(`[HTTP] ${req.method} ${req.path} - ${res.statusCode} (${duration}ms)`);
154
- });
155
- next();
156
- });
157
-
158
- app.use(express.static(path.join(__dirname, 'public')));
159
-
160
- // ==================== 辅助函数 ====================
161
-
162
- function resolveTarget(proxy) {
163
- const primaryProvider = configStore.getProviderById(proxy.providerId);
164
- if (!primaryProvider) return null;
165
-
166
- const pool = [];
167
- const seen = new Set();
168
-
169
- // Primary provider (no model override)
170
- const primaryKey = `${primaryProvider.id}\0`;
171
- seen.add(primaryKey);
172
- pool.push({
173
- providerId: primaryProvider.id,
174
- providerName: primaryProvider.name,
175
- providerUrl: primaryProvider.url,
176
- protocol: primaryProvider.protocol,
177
- apiKeys: primaryProvider.apiKeys || [],
178
- models: primaryProvider.models,
179
- azureDeployment: primaryProvider.azureDeployment || '',
180
- azureApiVersion: primaryProvider.azureApiVersion || '',
181
- model: '',
182
- weight: Math.max(1, parseInt(proxy.providerWeight, 10) || 1),
183
- });
184
-
185
- // Pool entries (may include model override)
186
- for (const entry of (proxy.providerPool || [])) {
187
- if (!entry || !entry.providerId) continue;
188
- const model = typeof entry.model === 'string' ? entry.model.trim() : '';
189
- const key = `${entry.providerId}\0${model}`;
190
- if (seen.has(key)) continue;
191
- seen.add(key);
192
- const provider = configStore.getProviderById(entry.providerId);
193
- if (!provider) continue;
194
- pool.push({
195
- providerId: provider.id,
196
- providerName: provider.name,
197
- providerUrl: provider.url,
198
- protocol: provider.protocol,
199
- apiKeys: provider.apiKeys || [],
200
- models: provider.models,
201
- azureDeployment: provider.azureDeployment || '',
202
- azureApiVersion: provider.azureApiVersion || '',
203
- model,
204
- weight: Math.max(1, parseInt(entry.weight, 10) || 1),
205
- });
206
- }
207
-
208
- if (pool.length === 0) return null;
209
-
210
- return {
211
- protocol: pool[0].protocol,
212
- routingStrategy: proxy.routingStrategy || 'primary_fallback',
213
- providerPool: pool,
214
- defaultModel: proxy.defaultModel,
215
- };
216
- }
217
-
218
- function normalizeProviderPoolInput(pool) {
219
- if (!Array.isArray(pool)) return [];
220
- const seen = new Set();
221
- const result = [];
222
- for (const item of pool) {
223
- if (!item || typeof item !== 'object') continue;
224
- const providerId = typeof item.providerId === 'string' ? item.providerId.trim() : '';
225
- if (!providerId) continue;
226
- const model = typeof item.model === 'string' ? item.model.trim() : '';
227
- const key = `${providerId}\0${model}`;
228
- if (seen.has(key)) continue;
229
- seen.add(key);
230
- result.push({
231
- providerId,
232
- model,
233
- weight: Math.max(1, parseInt(item.weight, 10) || 1),
234
- });
235
- }
236
- return result;
237
- }
238
-
239
- function normalizeRoutingStrategyInput(strategy) {
240
- return ['primary_fallback', 'round_robin', 'weighted', 'fastest'].includes(strategy)
241
- ? strategy
242
- : 'primary_fallback';
243
- }
244
-
245
- async function startProxyWithProvider(proxy) {
246
- const target = resolveTarget(proxy);
247
- if (!target) throw new Error(`供应商 ${proxy.providerId} 不存在`);
248
- const proxyConfig = { ...proxy, target };
249
- return proxyManager.startProxy(proxyConfig);
250
- }
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
-
335
- // ==================== 供应商 API ====================
336
-
337
- app.get('/api/providers', (req, res) => {
338
- const providers = configStore.getProviders().map(p => ({
339
- ...p,
340
- apiKey: p.apiKey ? '***' : '',
341
- apiKeys: (p.apiKeys || []).map((k, i) => ({ alias: k.alias || '', masked: true, index: i, enabled: k.enabled !== false })),
342
- }));
343
- res.json(providers);
344
- });
345
-
346
- app.get('/api/providers/:id', (req, res) => {
347
- const provider = configStore.getProviderById(req.params.id);
348
- if (!provider) return res.status(404).json({ error: 'Provider not found' });
349
- res.json({ ...provider, apiKey: provider.apiKey ? '***' : '', apiKeys: (provider.apiKeys || []).map((k, i) => ({ alias: k.alias || '', masked: true, index: i, enabled: k.enabled !== false })) });
350
- });
351
-
352
- app.post('/api/providers', (req, res) => {
353
- const { name, url, protocol, apiKey, apiKeys, models, azureDeployment, azureApiVersion } = req.body;
354
- if (!name || !url) {
355
- return res.status(400).json({ error: 'name and url are required' });
356
- }
357
- const provider = configStore.addProvider({
358
- name, url,
359
- protocol: protocol || (/anthropic/i.test(url) ? 'anthropic' : 'openai'),
360
- apiKey: apiKey || '',
361
- apiKeys: Array.isArray(apiKeys) ? apiKeys.filter(k => k && typeof k === 'object' && k.key && k.key.trim()) : [],
362
- models: models || [],
363
- azureDeployment: azureDeployment || '',
364
- azureApiVersion: azureApiVersion || '',
365
- });
366
- res.status(201).json(provider);
367
- });
368
-
369
- app.put('/api/providers/:id', async (req, res) => {
370
- const existing = configStore.getProviderById(req.params.id);
371
- if (!existing) return res.status(404).json({ error: 'Provider not found' });
372
-
373
- const updates = {};
374
- if (req.body.name !== undefined) updates.name = req.body.name;
375
- if (req.body.url !== undefined) updates.url = req.body.url;
376
- if (req.body.protocol !== undefined) updates.protocol = req.body.protocol;
377
- if (req.body.apiKey !== undefined && req.body.apiKey !== '') updates.apiKey = req.body.apiKey;
378
- if (req.body.apiKeys !== undefined) {
379
- // Map masked entries back to existing keys by index
380
- const existingKeys = existing.apiKeys || [];
381
- updates.apiKeys = req.body.apiKeys
382
- .map(k => {
383
- if (k && typeof k === 'object' && k.masked && typeof k.index === 'number') {
384
- const existing = existingKeys[k.index];
385
- if (!existing) return null;
386
- return { ...existing, alias: typeof k.alias === 'string' ? k.alias.trim() : (existing.alias || ''), enabled: k.enabled !== false };
387
- }
388
- if (k && typeof k === 'object' && typeof k.key === 'string' && k.key.trim()) {
389
- return { key: k.key.trim(), alias: typeof k.alias === 'string' ? k.alias.trim() : '', enabled: k.enabled !== false };
390
- }
391
- if (typeof k === 'string' && k.trim()) {
392
- return { key: k.trim(), alias: '' };
393
- }
394
- return null;
395
- })
396
- .filter(Boolean);
397
- }
398
- if (req.body.models !== undefined) updates.models = req.body.models;
399
- if (req.body.azureDeployment !== undefined) updates.azureDeployment = req.body.azureDeployment;
400
- if (req.body.azureApiVersion !== undefined) updates.azureApiVersion = req.body.azureApiVersion;
401
-
402
- const updated = configStore.updateProvider(req.params.id, updates);
403
-
404
- // 同步更新引用此供应商的运行中代理
405
- const affectedProxies = configStore.getProxies().filter(p => p.providerId === req.params.id);
406
- for (const proxy of affectedProxies) {
407
- if (!proxyManager.isRunning(proxy.id)) continue;
408
- const target = resolveTarget(proxy);
409
- if (target) proxyManager.updateProxyConfig({ ...proxy, target });
410
- }
411
-
412
- res.json(updated);
413
- });
414
-
415
- app.post('/api/providers/:id/test', async (req, res) => {
416
- const provider = configStore.getProviderById(req.params.id);
417
- if (!provider) return res.status(404).json({ error: 'Provider not found' });
418
-
419
- const existingKeys = provider.apiKeys || [];
420
- const reqKeys = Array.isArray(req.body.apiKeys) ? req.body.apiKeys : [];
421
- const resolved = reqKeys
422
- .map((k, i) => {
423
- if (k && typeof k === 'object' && k.masked && typeof k.index === 'number') {
424
- const ex = existingKeys[k.index];
425
- return ex ? { key: ex.key, alias: k.alias || ex.alias || '', domIndex: i } : null;
426
- }
427
- if (k && typeof k === 'object' && typeof k.key === 'string' && k.key.trim()) {
428
- return { key: k.key.trim(), alias: k.alias || '', domIndex: i };
429
- }
430
- if (typeof k === 'string' && k.trim()) return { key: k.trim(), alias: '', domIndex: i };
431
- return null;
432
- })
433
- .filter(Boolean);
434
-
435
- if (resolved.length === 0) {
436
- return res.json({ ok: false, message: '没有可用的 API Key', results: [] });
437
- }
438
-
439
- const protocol = req.body.protocol || provider.protocol || 'openai';
440
- const base = provider.url.replace(/\/$/, '');
441
- const hasV1Suffix = base.endsWith('/v1');
442
- const isAzure = protocol === 'openai' && !!provider.azureDeployment;
443
-
444
- function buildTestOpts(key) {
445
- if (protocol === 'openai') {
446
- if (isAzure) {
447
- const ver = provider.azureApiVersion || '2024-02-01';
448
- return {
449
- url: `${base}/openai/deployments/${provider.azureDeployment}/models?api-version=${ver}`,
450
- opts: { headers: { 'api-key': key } },
451
- };
452
- }
453
- return {
454
- url: hasV1Suffix ? `${base}/models` : `${base}/v1/models`,
455
- opts: { headers: { 'Authorization': `Bearer ${key}` } },
456
- };
457
- }
458
- if (protocol === 'anthropic') {
459
- const testModel = req.body.model || (provider.models && provider.models[0]) || 'claude-3-haiku-20240307';
460
- return {
461
- url: hasV1Suffix ? `${base}/messages` : `${base}/v1/messages`,
462
- opts: {
463
- method: 'POST',
464
- headers: { 'Content-Type': 'application/json', 'x-api-key': key, 'anthropic-version': '2023-06-01' },
465
- body: JSON.stringify({ model: testModel, max_tokens: 1, messages: [{ role: 'user', content: 'hi' }] }),
466
- },
467
- };
468
- }
469
- if (protocol === 'gemini') {
470
- return { url: `${base}/v1beta/models?key=${key}`, opts: {} };
471
- }
472
- return null;
473
- }
474
-
475
- if (protocol !== 'openai' && protocol !== 'anthropic' && protocol !== 'gemini') {
476
- return res.json({ ok: false, message: `不支持的协议: ${protocol}`, results: [] });
477
- }
478
-
479
- const results = await Promise.all(resolved.map(async entry => {
480
- const { url: testUrl, opts: fetchOpts } = buildTestOpts(entry.key);
481
- try {
482
- const startedAt = Date.now();
483
- const fetchRes = await fetch(testUrl, { ...fetchOpts, signal: AbortSignal.timeout(15000) });
484
- const latencyMs = Date.now() - startedAt;
485
- if (!fetchRes.ok) {
486
- const errText = await fetchRes.text().catch(() => '');
487
- const hint = fetchRes.status === 401 || fetchRes.status === 403
488
- ? 'API Key 无效或无权限'
489
- : `HTTP ${fetchRes.status}: ${errText.slice(0, 200) || '未知错误'}`;
490
- return { ok: false, alias: entry.alias, index: entry.domIndex, message: hint, latencyMs };
491
- }
492
- return { ok: true, alias: entry.alias, index: entry.domIndex, latencyMs };
493
- } catch (err) {
494
- const msg = err.name === 'TimeoutError' ? '连接超时 (15s)' : `连接失败: ${err.message}`;
495
- return { ok: false, alias: entry.alias, index: entry.domIndex, message: msg };
496
- }
497
- }));
498
-
499
- const passed = results.filter(r => r.ok).length;
500
- const failed = results.length - passed;
501
- res.json({ ok: failed === 0, passed, failed, total: results.length, results });
502
- });
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
-
601
- app.post('/api/providers/:id/available-models', async (req, res) => {
602
- const provider = configStore.getProviderById(req.params.id);
603
- if (!provider) return res.status(404).json({ error: 'Provider not found' });
604
-
605
- // Support unsaved API keys from form
606
- let keys;
607
- const reqKeys = Array.isArray(req.body?.apiKeys) ? req.body.apiKeys : [];
608
- if (reqKeys.length > 0) {
609
- const existingKeys = provider.apiKeys || [];
610
- keys = reqKeys
611
- .map(k => {
612
- if (k && typeof k === 'object' && k.masked && typeof k.index === 'number') {
613
- return existingKeys[k.index]?.key || null;
614
- }
615
- if (k && typeof k === 'object' && typeof k.key === 'string' && k.key.trim()) {
616
- return k.key.trim();
617
- }
618
- return null;
619
- })
620
- .filter(Boolean);
621
- } else {
622
- keys = (provider.apiKeys || []).map(k => k.key).filter(Boolean);
623
- }
624
- if (keys.length === 0) return res.json({ models: [], message: '没有可用的 API Key' });
625
-
626
- const protocol = provider.protocol || 'openai';
627
- const base = provider.url.replace(/\/$/, '');
628
- const hasV1Suffix = base.endsWith('/v1');
629
- const key = keys[0];
630
- const isAzure = protocol === 'openai' && !!provider.azureDeployment;
631
-
632
- try {
633
- let fetchUrl, fetchOpts;
634
- if (protocol === 'openai') {
635
- if (isAzure) {
636
- const ver = provider.azureApiVersion || '2024-02-01';
637
- fetchUrl = `${base}/openai/deployments/${provider.azureDeployment}/models?api-version=${ver}`;
638
- fetchOpts = { headers: { 'api-key': key } };
639
- } else {
640
- fetchUrl = hasV1Suffix ? `${base}/models` : `${base}/v1/models`;
641
- fetchOpts = { headers: { 'Authorization': `Bearer ${key}` } };
642
- }
643
- } else if (protocol === 'gemini') {
644
- fetchUrl = `${base}/v1beta/models?key=${key}`;
645
- fetchOpts = {};
646
- } else if (protocol === 'anthropic') {
647
- fetchUrl = hasV1Suffix ? `${base}/models` : `${base}/v1/models`;
648
- fetchOpts = { headers: { 'x-api-key': key, 'anthropic-version': '2023-06-01' } };
649
- } else {
650
- return res.json({ models: [], message: `不支持的协议: ${protocol}` });
651
- }
652
-
653
- const fetchRes = await fetch(fetchUrl, { ...fetchOpts, signal: AbortSignal.timeout(15000) });
654
- if (!fetchRes.ok) {
655
- const hint = fetchRes.status === 404 ? '该供应商不支持模型列表接口' : `获取失败: HTTP ${fetchRes.status}`;
656
- return res.json({ models: [], message: hint });
657
- }
658
-
659
- const data = await fetchRes.json().catch(() => null);
660
- let models = [];
661
- if (Array.isArray(data?.data)) {
662
- // OpenAI 格式(含第三方 Anthropic 兼容供应商)
663
- models = data.data.map(m => m.id || m.name).filter(Boolean).sort();
664
- } else if (Array.isArray(data?.models)) {
665
- // Gemini 格式
666
- models = data.models.map(m => (m.name || m.id)?.replace('models/', '')).filter(Boolean).sort();
667
- }
668
-
669
- res.json({ models });
670
- } catch (err) {
671
- res.json({ models: [], message: `获取失败: ${err.message}` });
672
- }
673
- });
674
-
675
- app.delete('/api/providers/:id', (req, res) => {
676
- const existing = configStore.getProviderById(req.params.id);
677
- if (!existing) return res.status(404).json({ error: 'Provider not found' });
678
-
679
- // 检查是否有代理在使用此供应商
680
- const inUse = configStore.getProxies().some(p => p.providerId === req.params.id);
681
- if (inUse) {
682
- return res.status(409).json({ error: '该供应商正在被代理使用,无法删除' });
683
- }
684
-
685
- configStore.removeProvider(req.params.id);
686
- res.json({ success: true });
687
- });
688
-
689
- // ==================== 代理 API ====================
690
-
691
- // 获取所有代理配置
692
- app.get('/api/proxies', (req, res) => {
693
- const proxies = configStore.getProxies().map(p => {
694
- const provider = configStore.getProviderById(p.providerId);
695
- return {
696
- id: p.id,
697
- name: p.name,
698
- port: p.port,
699
- requireAuth: p.requireAuth,
700
- authToken: p.authToken,
701
- providerId: p.providerId,
702
- providerName: provider?.name || '',
703
- providerUrl: provider?.url || '',
704
- protocol: provider?.protocol || '',
705
- defaultModel: p.defaultModel || '',
706
- providerWeight: Math.max(1, parseInt(p.providerWeight, 10) || 1),
707
- routingStrategy: p.routingStrategy || 'primary_fallback',
708
- providerPool: Array.isArray(p.providerPool) ? p.providerPool : [],
709
- hasApiKey: !!(provider?.apiKey || (provider?.apiKeys && provider.apiKeys.length > 0)),
710
- running: proxyManager.isRunning(p.id),
711
- };
712
- });
713
- res.json(proxies);
714
- });
715
-
716
- // 获取单个代理配置
717
- app.get('/api/proxies/:id', (req, res) => {
718
- const proxy = configStore.getProxyById(req.params.id);
719
- if (!proxy) return res.status(404).json({ error: 'Proxy not found' });
720
- const provider = configStore.getProviderById(proxy.providerId);
721
- res.json({
722
- ...proxy,
723
- providerName: provider?.name || '',
724
- providerUrl: provider?.url || '',
725
- protocol: provider?.protocol || '',
726
- routingStrategy: proxy.routingStrategy || 'primary_fallback',
727
- providerPool: Array.isArray(proxy.providerPool) ? proxy.providerPool : [],
728
- hasApiKey: !!(provider?.apiKey || (provider?.apiKeys && provider.apiKeys.length > 0)),
729
- });
730
- });
731
-
732
- // 创建代理
733
- app.post('/api/proxies', async (req, res) => {
734
- configStore.saveSnapshot('create-proxy');
735
- const { name, port, requireAuth, authToken, providerId, defaultModel, routingStrategy, providerPool, providerWeight } = req.body;
736
-
737
- if (!name || !port || !providerId) {
738
- return res.status(400).json({ error: 'name, port and providerId are required' });
739
- }
740
-
741
- const provider = configStore.getProviderById(providerId);
742
- if (!provider) return res.status(400).json({ error: '供应商不存在' });
743
-
744
- const parsedPort = parseInt(port);
745
-
746
- const existing = configStore.getProxies().find(p => p.port === parsedPort);
747
- if (existing) {
748
- return res.status(409).json({
749
- error: `端口 ${parsedPort} 已被代理「${existing.name}」占用,请更换端口`,
750
- });
751
- }
752
-
753
- const proxy = configStore.addProxy({
754
- name,
755
- port: parsedPort,
756
- requireAuth: !!requireAuth,
757
- authToken: authToken || null,
758
- providerId,
759
- defaultModel: defaultModel || '',
760
- providerWeight: Math.max(1, parseInt(providerWeight, 10) || 1),
761
- routingStrategy: normalizeRoutingStrategyInput(routingStrategy),
762
- providerPool: normalizeProviderPoolInput(providerPool),
763
- });
764
-
765
- try {
766
- await startProxyWithProvider(proxy);
767
- res.status(201).json({ ...proxy, running: true });
768
- } catch (err) {
769
- configStore.removeProxy(proxy.id);
770
- res.status(500).json({ error: `代理启动失败: ${err.message}` });
771
- }
772
- });
773
-
774
- // 更新代理
775
- app.put('/api/proxies/:id', async (req, res) => {
776
- configStore.saveSnapshot('update-proxy');
777
- const existing = configStore.getProxyById(req.params.id);
778
- if (!existing) return res.status(404).json({ error: 'Proxy not found' });
779
-
780
- const updates = {};
781
- if (req.body.name !== undefined) updates.name = req.body.name;
782
- if (req.body.port !== undefined) updates.port = parseInt(req.body.port);
783
- if (req.body.requireAuth !== undefined) updates.requireAuth = !!req.body.requireAuth;
784
- if (req.body.authToken !== undefined) updates.authToken = req.body.authToken || null;
785
- if (req.body.providerId !== undefined) {
786
- if (!configStore.getProviderById(req.body.providerId)) {
787
- return res.status(400).json({ error: '供应商不存在' });
788
- }
789
- updates.providerId = req.body.providerId;
790
- }
791
- if (req.body.defaultModel !== undefined) updates.defaultModel = req.body.defaultModel;
792
- if (req.body.providerWeight !== undefined) updates.providerWeight = Math.max(1, parseInt(req.body.providerWeight, 10) || 1);
793
- if (req.body.routingStrategy !== undefined) updates.routingStrategy = normalizeRoutingStrategyInput(req.body.routingStrategy);
794
- if (req.body.providerPool !== undefined) updates.providerPool = normalizeProviderPoolInput(req.body.providerPool);
795
-
796
- const needRestart = updates.port !== undefined && updates.port !== existing.port;
797
- if (needRestart) {
798
- const conflict = configStore.getProxies().find(p => p.id !== req.params.id && p.port === updates.port);
799
- if (conflict) {
800
- return res.status(409).json({
801
- error: `端口 ${updates.port} 已被代理「${conflict.name}」占用,请更换端口`,
802
- });
803
- }
804
- }
805
-
806
- const updated = configStore.updateProxy(req.params.id, updates);
807
-
808
- if (needRestart) {
809
- try {
810
- await startProxyWithProvider(updated);
811
- } catch (err) {
812
- return res.status(500).json({ error: `代理重启失败: ${err.message}` });
813
- }
814
- } else {
815
- // 更新供应商配置引用
816
- const target = resolveTarget(updated);
817
- if (target) proxyManager.updateProxyConfig({ ...updated, target });
818
- }
819
-
820
- res.json({ ...updated, running: proxyManager.isRunning(updated.id) });
821
- });
822
-
823
- // 删除代理
824
- app.delete('/api/proxies/:id', async (req, res) => {
825
- configStore.saveSnapshot('delete-proxy');
826
- const existing = configStore.getProxyById(req.params.id);
827
- if (!existing) return res.status(404).json({ error: 'Proxy not found' });
828
-
829
- await proxyManager.stopProxy(req.params.id);
830
- configStore.removeProxy(req.params.id);
831
- res.json({ success: true });
832
- });
833
-
834
- // 启动/停止代理
835
- app.post('/api/proxies/:id/start', async (req, res) => {
836
- const proxy = configStore.getProxyById(req.params.id);
837
- if (!proxy) return res.status(404).json({ error: 'Proxy not found' });
838
-
839
- try {
840
- await startProxyWithProvider(proxy);
841
- res.json({ success: true, running: true });
842
- } catch (err) {
843
- res.status(500).json({ error: 'Failed to start proxy', message: err.message });
844
- }
845
- });
846
-
847
- app.post('/api/proxies/:id/stop', async (req, res) => {
848
- await proxyManager.stopProxy(req.params.id);
849
- res.json({ success: true, running: false });
850
- });
851
-
852
- // 批量启动所有代理
853
- app.post('/api/proxies/start-all', async (req, res) => {
854
- const proxies = configStore.getProxies();
855
- const results = [];
856
- for (const proxy of proxies) {
857
- if (proxyManager.isRunning(proxy.id)) {
858
- results.push({ id: proxy.id, name: proxy.name, skipped: true });
859
- continue;
860
- }
861
- try {
862
- await startProxyWithProvider(proxy);
863
- results.push({ id: proxy.id, name: proxy.name, success: true });
864
- } catch (err) {
865
- results.push({ id: proxy.id, name: proxy.name, success: false, error: err.message });
866
- }
867
- }
868
- res.json({ results });
869
- });
870
-
871
- // 批量停止所有代理
872
- app.post('/api/proxies/stop-all', async (req, res) => {
873
- const running = proxyManager.getRunningPorts();
874
- const results = [];
875
- for (const r of running) {
876
- await proxyManager.stopProxy(r.id);
877
- results.push({ id: r.id, name: r.name, success: true });
878
- }
879
- res.json({ results });
880
- });
881
-
882
- // 获取运行状态
883
- app.get('/api/status', (req, res) => {
884
- res.json({
885
- running: proxyManager.getRunningPorts(),
886
- total: configStore.getProxies().length,
887
- });
888
- });
889
-
890
- // 健康检查
891
- app.get('/api/health', (req, res) => {
892
- res.json({
893
- status: 'ok',
894
- version: pkg.version,
895
- uptime: process.uptime(),
896
- proxies: {
897
- total: configStore.getProxies().length,
898
- running: proxyManager.getRunningPorts().length,
899
- },
900
- });
901
- });
902
-
903
- // API Key 健康状态
904
- app.get('/api/key-health', (req, res) => {
905
- const result = {};
906
- for (const [providerId, health] of keyHealth) {
907
- result[providerId] = health;
908
- }
909
- res.json(result);
910
- });
911
-
912
- // 手动触发健康检查
913
- app.post('/api/key-health/check', async (req, res) => {
914
- await checkAllProviderKeys();
915
- res.json({ success: true });
916
- });
917
-
918
- // 设置
919
- app.get('/api/settings', (req, res) => {
920
- res.json(configStore.getSettings());
921
- });
922
-
923
- app.put('/api/settings', (req, res) => {
924
- const settings = req.body;
925
- if (!settings || typeof settings !== 'object') {
926
- return res.status(400).json({ error: '需要 settings 对象' });
927
- }
928
- for (const [key, value] of Object.entries(settings)) {
929
- configStore.setSetting(key, value);
930
- }
931
- res.json(configStore.getSettings());
932
- });
933
-
934
- // Token 用量统计
935
- app.get('/api/stats', (req, res) => {
936
- const { range, startDate, endDate, proxyId } = req.query;
937
- const stats = statsStore.getStats({
938
- range: range || 'daily',
939
- startDate: startDate || undefined,
940
- endDate: endDate || undefined,
941
- proxyId: proxyId || undefined,
942
- });
943
- const proxies = configStore.getProxies().map(p => ({
944
- id: p.id,
945
- name: p.name,
946
- providerName: configStore.getProviderById(p.providerId)?.name || '',
947
- }));
948
- res.json({ ...stats, proxies });
949
- });
950
-
951
- // 日志查看
952
- app.get('/api/logs', (req, res) => {
953
- const lines = Math.min(parseInt(req.query.lines) || 200, 2000);
954
- try {
955
- if (!fs.existsSync(logger.LOG_FILE)) {
956
- return res.json({ lines: [] });
957
- }
958
- const content = fs.readFileSync(logger.LOG_FILE, 'utf8');
959
- const allLines = content.split('\n').filter(l => l.trim());
960
- const tail = allLines.slice(-lines);
961
- res.json({ lines: tail, total: allLines.length });
962
- } catch (err) {
963
- res.json({ lines: [], error: err.message });
964
- }
965
- });
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
-
974
- // ==================== 配置导入/导出 ====================
975
-
976
- app.get('/api/config/export', (req, res) => {
977
- const providers = configStore.getProviders();
978
- const proxies = configStore.getProxies().map(p => {
979
- const provider = configStore.getProviderById(p.providerId);
980
- return {
981
- id: p.id,
982
- name: p.name,
983
- port: p.port,
984
- requireAuth: p.requireAuth,
985
- authToken: p.authToken,
986
- providerId: p.providerId,
987
- defaultModel: p.defaultModel || '',
988
- routingStrategy: p.routingStrategy || 'primary_fallback',
989
- providerPool: Array.isArray(p.providerPool) ? p.providerPool : [],
990
- providerName: provider?.name || '',
991
- };
992
- });
993
- res.json({ providers, proxies, exportedAt: new Date().toISOString() });
994
- });
995
-
996
- app.post('/api/config/import', async (req, res) => {
997
- const { config, mode } = req.body;
998
-
999
- if (!config || !mode || !['overwrite', 'merge'].includes(mode)) {
1000
- return res.status(400).json({ error: '需要 config 和 mode(overwrite/merge)' });
1001
- }
1002
-
1003
- configStore.saveSnapshot('import-' + mode);
1004
-
1005
- // 校验结构
1006
- if (!Array.isArray(config.providers) || !Array.isArray(config.proxies)) {
1007
- return res.status(400).json({ error: '配置格式错误:需要 providers proxies 数组' });
1008
- }
1009
-
1010
- for (const p of config.providers) {
1011
- if (!p.name || !p.url || !p.protocol) {
1012
- return res.status(400).json({ error: `供应商 "${p.name || '?'}" 缺少必要字段(name/url/protocol)` });
1013
- }
1014
- }
1015
-
1016
- for (const p of config.proxies) {
1017
- if (!p.name || !p.port || !p.providerId) {
1018
- return res.status(400).json({ error: `代理 "${p.name || '?'}" 缺少必要字段(name/port/providerId)` });
1019
- }
1020
- }
1021
-
1022
- if (mode === 'overwrite') {
1023
- // 覆盖模式:直接替换整个配置
1024
- const newConfig = {
1025
- providers: config.providers.map(p => ({
1026
- id: p.id,
1027
- name: p.name,
1028
- url: p.url,
1029
- protocol: p.protocol,
1030
- apiKey: p.apiKey || '',
1031
- models: Array.isArray(p.models) ? p.models : [],
1032
- })),
1033
- proxies: config.proxies.map(p => ({
1034
- id: p.id,
1035
- name: p.name,
1036
- port: p.port,
1037
- requireAuth: !!p.requireAuth,
1038
- authToken: p.authToken || null,
1039
- providerId: p.providerId,
1040
- defaultModel: p.defaultModel || '',
1041
- routingStrategy: normalizeRoutingStrategyInput(p.routingStrategy),
1042
- providerPool: normalizeProviderPoolInput(p.providerPool),
1043
- })),
1044
- };
1045
- configStore.saveConfig(newConfig);
1046
- return res.json({ success: true, mode, providers: newConfig.providers.length, proxies: newConfig.proxies.length });
1047
- }
1048
-
1049
- // 合并模式:按 ID 去重
1050
- const existingProviders = configStore.getProviders();
1051
- const existingProxies = configStore.getProxies();
1052
-
1053
- const providerMap = new Map(existingProviders.map(p => [p.id, p]));
1054
- for (const p of config.providers) {
1055
- providerMap.set(p.id, {
1056
- id: p.id,
1057
- name: p.name,
1058
- url: p.url,
1059
- protocol: p.protocol,
1060
- apiKey: p.apiKey || '',
1061
- models: Array.isArray(p.models) ? p.models : [],
1062
- routingStrategy: normalizeRoutingStrategyInput(p.routingStrategy),
1063
- providerPool: normalizeProviderPoolInput(p.providerPool),
1064
- });
1065
- }
1066
-
1067
- const proxyMap = new Map(existingProxies.map(p => [p.id, p]));
1068
- for (const p of config.proxies) {
1069
- // 检查端口冲突:导入的代理端口不能和现有代理或其他导入代理重复
1070
- const conflict = proxyMap.get(p.id)
1071
- ? null // 同 ID 是覆盖,不算冲突
1072
- : Array.from(proxyMap.values()).find(ep => ep.port === p.port);
1073
- if (conflict) {
1074
- return res.status(409).json({
1075
- error: `端口 ${p.port} 已被代理「${conflict.name}」占用,无法导入代理「${p.name}」`,
1076
- });
1077
- }
1078
- proxyMap.set(p.id, {
1079
- id: p.id,
1080
- name: p.name,
1081
- port: p.port,
1082
- requireAuth: !!p.requireAuth,
1083
- authToken: p.authToken || null,
1084
- providerId: p.providerId,
1085
- defaultModel: p.defaultModel || '',
1086
- routingStrategy: normalizeRoutingStrategyInput(p.routingStrategy),
1087
- providerPool: normalizeProviderPoolInput(p.providerPool),
1088
- });
1089
- }
1090
-
1091
- const merged = {
1092
- providers: Array.from(providerMap.values()),
1093
- proxies: Array.from(proxyMap.values()),
1094
- };
1095
- configStore.saveConfig(merged);
1096
-
1097
- res.json({
1098
- success: true,
1099
- mode,
1100
- providers: merged.providers.length,
1101
- proxies: merged.proxies.length,
1102
- added: {
1103
- providers: merged.providers.length - existingProviders.length,
1104
- proxies: merged.proxies.length - existingProxies.length,
1105
- },
1106
- });
1107
- });
1108
-
1109
- // ==================== 配置版本历史 ====================
1110
-
1111
- app.get('/api/config/history', (req, res) => {
1112
- const snapshots = configStore.getSnapshots();
1113
- res.json({ snapshots });
1114
- });
1115
-
1116
- app.post('/api/config/rollback', async (req, res) => {
1117
- const { file } = req.body;
1118
- if (!file) return res.status(400).json({ error: '需要指定快照文件' });
1119
- const result = configStore.restoreSnapshot(file);
1120
- if (result.error) return res.status(400).json({ error: result.error });
1121
- res.json({ success: true });
1122
- });
1123
-
1124
- // 前端首页
1125
- app.get('/', (req, res) => {
1126
- res.sendFile(path.join(__dirname, 'public', 'index.html'));
1127
- });
1128
-
1129
- // 启动
1130
- logger.init();
1131
- writePid();
1132
-
1133
- // 启动所有已配置的代理
1134
- const proxies = configStore.getProxies();
1135
- await Promise.all(proxies.map(async (proxy) => {
1136
- try {
1137
- await startProxyWithProvider(proxy);
1138
- } catch (err) {
1139
- logger.error(`[Init] Failed to start proxy ${proxy.name}:`, err.message);
1140
- }
1141
- }));
1142
-
1143
- const http = require('http');
1144
- const server = app.listen(PORT, () => {
1145
- const adminUrl = `http://localhost:${PORT}`;
1146
- logger.log(`[Admin] Management server running on ${adminUrl}`);
1147
- logger.log(`[Admin] ${proxies.length} proxy config(s) loaded`);
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
-
1156
- openBrowser(adminUrl);
1157
- });
1158
- }
1159
-
1160
- // 优雅关闭
1161
- process.on('SIGINT', async () => {
1162
- logger.log('[Shutdown] Shutting down...');
1163
- removePid();
1164
- try {
1165
- const wsServer = require('./lib/ws-server');
1166
- wsServer.close();
1167
- const proxyManager = require('./lib/proxy-manager');
1168
- const statsStore = require('./lib/stats-store');
1169
- statsStore.flush();
1170
- await proxyManager.stopAll();
1171
- } catch (err) {
1172
- logger.error('[Shutdown] stopAll error:', err.message);
1173
- }
1174
- process.exit(0);
1175
- });
1176
-
1177
- process.on('SIGTERM', async () => {
1178
- removePid();
1179
- try {
1180
- const wsServer = require('./lib/ws-server');
1181
- wsServer.close();
1182
- const proxyManager = require('./lib/proxy-manager');
1183
- const statsStore = require('./lib/stats-store');
1184
- statsStore.flush();
1185
- await proxyManager.stopAll();
1186
- } catch (err) {
1187
- logger.error('[Shutdown] stopAll error:', err.message);
1188
- }
1189
- process.exit(0);
1190
- });
1191
-
1192
- // ==================== CLI Dispatch ====================
1193
-
1194
- const cmd = process.argv[2];
1195
-
1196
- switch (cmd) {
1197
- case 'help':
1198
- showHelp();
1199
- break;
1200
- case '-v':
1201
- case '--version':
1202
- showVersion();
1203
- break;
1204
- case 'update':
1205
- updateService();
1206
- break;
1207
- case 'stop':
1208
- stopService();
1209
- break;
1210
- case 'status':
1211
- showStatus();
1212
- break;
1213
- case 'start':
1214
- startDaemon();
1215
- break;
1216
- case '--daemon':
1217
- init();
1218
- break;
1219
- case undefined:
1220
- init();
1221
- break;
1222
- default:
1223
- console.error(`未知命令: ${cmd}`);
1224
- showHelp();
1225
- process.exit(1);
1226
- }
1
+ #!/usr/bin/env node
2
+ const path = require('path');
3
+ const { exec, spawn } = require('child_process');
4
+ const os = require('os');
5
+ const fs = require('fs');
6
+ const logger = require('./lib/logger');
7
+
8
+ // ==================== CLI ====================
9
+
10
+ const PID_FILE = path.join(os.tmpdir(), 'protocol-proxy.pid');
11
+ const pkg = require('./package.json');
12
+
13
+ function writePid() {
14
+ try { fs.writeFileSync(PID_FILE, String(process.pid)); } catch (err) {
15
+ console.error('[PID] 写入失败:', err.message);
16
+ }
17
+ }
18
+
19
+ function readPid() {
20
+ try { return parseInt(fs.readFileSync(PID_FILE, 'utf8').trim()); } catch { return null; }
21
+ }
22
+
23
+ function removePid() {
24
+ try { fs.unlinkSync(PID_FILE); } catch (err) {
25
+ if (err.code !== 'ENOENT') {
26
+ console.error('[PID] 删除失败:', err.message);
27
+ }
28
+ }
29
+ }
30
+
31
+ function isProcessAlive(pid) {
32
+ try { process.kill(pid, 0); return true; } catch { return false; }
33
+ }
34
+
35
+ function showHelp() {
36
+ console.log(`
37
+ protocol-proxy - OpenAI / Anthropic 协议转换透明代理
38
+
39
+ 用法:
40
+ protocol-proxy 前台启动服务(Ctrl+C 停止)
41
+ protocol-proxy start 后台启动服务
42
+ protocol-proxy stop 停止后台服务
43
+ protocol-proxy status 查看运行状态
44
+ protocol-proxy help 显示帮助信息
45
+ protocol-proxy -v, --version 显示版本号
46
+ protocol-proxy update 更新到最新版本
47
+ `);
48
+ }
49
+
50
+ function startDaemon() {
51
+ const pid = readPid();
52
+ if (pid && isProcessAlive(pid)) {
53
+ console.log(`服务已在运行 (PID: ${pid})`);
54
+ return;
55
+ }
56
+
57
+ const child = spawn(process.execPath, [__filename, '--daemon'], {
58
+ detached: true,
59
+ stdio: 'ignore',
60
+ });
61
+ fs.writeFileSync(PID_FILE, String(child.pid));
62
+ child.unref();
63
+ console.log(`服务已在后台启动 (PID: ${child.pid})`);
64
+ }
65
+
66
+ function showVersion() {
67
+ console.log(pkg.version);
68
+ }
69
+
70
+ function showStatus() {
71
+ const pid = readPid();
72
+ if (pid && isProcessAlive(pid)) {
73
+ console.log(`服务正在运行 (PID: ${pid})`);
74
+ const configStore = require('./lib/config-store');
75
+ const proxies = configStore.getProxies();
76
+ if (proxies.length > 0) {
77
+ console.log(`\n已配置的代理 (${proxies.length} 个):`);
78
+ for (const p of proxies) {
79
+ console.log(` - ${p.name}: 端口 ${p.port} → ${p.target?.providerUrl || '未设置'}`);
80
+ }
81
+ }
82
+ } else {
83
+ removePid();
84
+ console.log('服务未运行');
85
+ }
86
+ }
87
+
88
+ function stopService() {
89
+ const pid = readPid();
90
+ if (!pid || !isProcessAlive(pid)) {
91
+ removePid();
92
+ console.log('服务未运行');
93
+ return;
94
+ }
95
+ try {
96
+ process.kill(pid, 'SIGTERM');
97
+ removePid();
98
+ console.log(`服务已停止 (PID: ${pid})`);
99
+ } catch (err) {
100
+ console.error('停止服务失败:', err.message);
101
+ removePid();
102
+ }
103
+ }
104
+
105
+ function updateService() {
106
+ console.log('正在更新 protocol-proxy...');
107
+ exec('npm install -g protocol-proxy@latest', (err, stdout, stderr) => {
108
+ if (err) {
109
+ console.error('更新失败:', err.message);
110
+ process.exit(1);
111
+ }
112
+ if (stdout) console.log(stdout);
113
+ if (stderr) console.error(stderr);
114
+ console.log('更新完成');
115
+ });
116
+ }
117
+
118
+ // ==================== 启动 ====================
119
+
120
+ async function init() {
121
+ const express = require('express');
122
+ const cors = require('cors');
123
+ const configStore = require('./lib/config-store');
124
+ const proxyManager = require('./lib/proxy-manager');
125
+ const statsStore = require('./lib/stats-store');
126
+
127
+ const app = express();
128
+ const PORT = process.env.ADMIN_PORT || 3000;
129
+
130
+ function openBrowser(url) {
131
+ const platform = os.platform();
132
+ let command;
133
+ if (platform === 'win32') {
134
+ command = `start "" "${url}"`;
135
+ } else if (platform === 'darwin') {
136
+ command = `open "${url}"`;
137
+ } else {
138
+ command = `xdg-open "${url}"`;
139
+ }
140
+ exec(command, (err) => {
141
+ if (err) logger.error('[Browser] 打开浏览器失败:', err.message);
142
+ });
143
+ }
144
+
145
+ app.use(cors());
146
+ app.use(express.json());
147
+
148
+ // 访问日志
149
+ app.use((req, res, next) => {
150
+ const start = Date.now();
151
+ res.on('finish', () => {
152
+ const duration = Date.now() - start;
153
+ logger.log(`[HTTP] ${req.method} ${req.path} - ${res.statusCode} (${duration}ms)`);
154
+ });
155
+ next();
156
+ });
157
+
158
+ app.use(express.static(path.join(__dirname, 'public')));
159
+
160
+ // ==================== 辅助函数 ====================
161
+
162
+ function resolveTarget(proxy) {
163
+ const primaryProvider = configStore.getProviderById(proxy.providerId);
164
+ if (!primaryProvider) return null;
165
+
166
+ const pool = [];
167
+ const seen = new Set();
168
+
169
+ // Primary provider (no model override)
170
+ const primaryKey = `${primaryProvider.id}\0`;
171
+ seen.add(primaryKey);
172
+ pool.push({
173
+ providerId: primaryProvider.id,
174
+ providerName: primaryProvider.name,
175
+ providerUrl: primaryProvider.url,
176
+ protocol: primaryProvider.protocol,
177
+ apiKeys: primaryProvider.apiKeys || [],
178
+ models: primaryProvider.models,
179
+ azureDeployment: primaryProvider.azureDeployment || '',
180
+ azureApiVersion: primaryProvider.azureApiVersion || '',
181
+ model: '',
182
+ weight: Math.max(1, parseInt(proxy.providerWeight, 10) || 1),
183
+ });
184
+
185
+ // Pool entries (may include model override)
186
+ for (const entry of (proxy.providerPool || [])) {
187
+ if (!entry || !entry.providerId) continue;
188
+ const model = typeof entry.model === 'string' ? entry.model.trim() : '';
189
+ const key = `${entry.providerId}\0${model}`;
190
+ if (seen.has(key)) continue;
191
+ seen.add(key);
192
+ const provider = configStore.getProviderById(entry.providerId);
193
+ if (!provider) continue;
194
+ pool.push({
195
+ providerId: provider.id,
196
+ providerName: provider.name,
197
+ providerUrl: provider.url,
198
+ protocol: provider.protocol,
199
+ apiKeys: provider.apiKeys || [],
200
+ models: provider.models,
201
+ azureDeployment: provider.azureDeployment || '',
202
+ azureApiVersion: provider.azureApiVersion || '',
203
+ model,
204
+ weight: Math.max(1, parseInt(entry.weight, 10) || 1),
205
+ });
206
+ }
207
+
208
+ if (pool.length === 0) return null;
209
+
210
+ return {
211
+ protocol: pool[0].protocol,
212
+ routingStrategy: proxy.routingStrategy || 'primary_fallback',
213
+ providerPool: pool,
214
+ defaultModel: proxy.defaultModel,
215
+ };
216
+ }
217
+
218
+ function normalizeProviderPoolInput(pool) {
219
+ if (!Array.isArray(pool)) return [];
220
+ const seen = new Set();
221
+ const result = [];
222
+ for (const item of pool) {
223
+ if (!item || typeof item !== 'object') continue;
224
+ const providerId = typeof item.providerId === 'string' ? item.providerId.trim() : '';
225
+ if (!providerId) continue;
226
+ const model = typeof item.model === 'string' ? item.model.trim() : '';
227
+ const key = `${providerId}\0${model}`;
228
+ if (seen.has(key)) continue;
229
+ seen.add(key);
230
+ result.push({
231
+ providerId,
232
+ model,
233
+ weight: Math.max(1, parseInt(item.weight, 10) || 1),
234
+ });
235
+ }
236
+ return result;
237
+ }
238
+
239
+ function normalizeRoutingStrategyInput(strategy) {
240
+ return ['primary_fallback', 'round_robin', 'weighted', 'fastest'].includes(strategy)
241
+ ? strategy
242
+ : 'primary_fallback';
243
+ }
244
+
245
+ // ==================== 助手工具定义与执行器 ====================
246
+
247
+ const TOOL_DEFINITIONS = [
248
+ {
249
+ type: 'function',
250
+ function: {
251
+ name: 'get_system_status',
252
+ description: '获取系统概览:所有代理的运行状态、供应商数量、系统运行时长。',
253
+ parameters: { type: 'object', properties: {}, required: [] },
254
+ },
255
+ },
256
+ {
257
+ type: 'function',
258
+ function: {
259
+ name: 'get_providers',
260
+ description: '获取所有供应商列表,包含协议、Key 数量和健康状态。',
261
+ parameters: { type: 'object', properties: {}, required: [] },
262
+ },
263
+ },
264
+ {
265
+ type: 'function',
266
+ function: {
267
+ name: 'get_provider',
268
+ description: '根据 ID 获取单个供应商的详细信息。',
269
+ parameters: {
270
+ type: 'object',
271
+ properties: { providerId: { type: 'string', description: '供应商 ID' } },
272
+ required: ['providerId'],
273
+ },
274
+ },
275
+ },
276
+ {
277
+ type: 'function',
278
+ function: {
279
+ name: 'get_proxies',
280
+ description: '获取所有代理列表,包含端口、运行状态、关联供应商和路由策略。',
281
+ parameters: { type: 'object', properties: {}, required: [] },
282
+ },
283
+ },
284
+ {
285
+ type: 'function',
286
+ function: {
287
+ name: 'get_proxy',
288
+ description: '根据 ID 获取单个代理的详细信息。',
289
+ parameters: {
290
+ type: 'object',
291
+ properties: { proxyId: { type: 'string', description: '代理 ID' } },
292
+ required: ['proxyId'],
293
+ },
294
+ },
295
+ },
296
+ {
297
+ type: 'function',
298
+ function: {
299
+ name: 'get_usage_stats',
300
+ description: '查询用量统计,支持按时间范围、代理筛选。返回请求数和 Token 用量。',
301
+ parameters: {
302
+ type: 'object',
303
+ properties: {
304
+ range: { type: 'string', enum: ['hourly', 'daily', 'monthly', 'yearly'], description: '统计粒度,默认 daily' },
305
+ startDate: { type: 'string', description: '起始日期,格式 YYYY-MM-DD' },
306
+ endDate: { type: 'string', description: '结束日期,格式 YYYY-MM-DD' },
307
+ proxyId: { type: 'string', description: '按代理 ID 筛选' },
308
+ },
309
+ required: [],
310
+ },
311
+ },
312
+ },
313
+ {
314
+ type: 'function',
315
+ function: {
316
+ name: 'get_recent_requests',
317
+ description: '获取最近的请求日志,包含状态、延迟、模型、Token 用量等。',
318
+ parameters: {
319
+ type: 'object',
320
+ properties: {
321
+ limit: { type: 'number', description: '返回条数,默认 20,最大 100' },
322
+ },
323
+ required: [],
324
+ },
325
+ },
326
+ },
327
+ {
328
+ type: 'function',
329
+ function: {
330
+ name: 'get_system_logs',
331
+ description: '获取最近的系统日志(倒序),用于排查错误和异常。',
332
+ parameters: {
333
+ type: 'object',
334
+ properties: {
335
+ limit: { type: 'number', description: '返回行数,默认 30,最大 100' },
336
+ },
337
+ required: [],
338
+ },
339
+ },
340
+ },
341
+ {
342
+ type: 'function',
343
+ function: {
344
+ name: 'get_key_health',
345
+ description: '获取所有供应商的 API Key 健康检查结果,包含每个 Key 的状态和错误信息。',
346
+ parameters: { type: 'object', properties: {}, required: [] },
347
+ },
348
+ },
349
+ {
350
+ type: 'function',
351
+ function: {
352
+ name: 'get_settings',
353
+ description: '获取系统设置项。',
354
+ parameters: { type: 'object', properties: {}, required: [] },
355
+ },
356
+ },
357
+ {
358
+ type: 'function',
359
+ function: {
360
+ name: 'get_config_history',
361
+ description: '获取配置快照历史列表,可用于了解配置变更记录。',
362
+ parameters: { type: 'object', properties: {}, required: [] },
363
+ },
364
+ },
365
+ {
366
+ type: 'function',
367
+ function: {
368
+ name: 'read_file',
369
+ description: '读取文件内容。可以读取任意文件。',
370
+ parameters: {
371
+ type: 'object',
372
+ properties: {
373
+ path: { type: 'string', description: '文件的绝对路径或相对于工作目录的路径' },
374
+ offset: { type: 'number', description: '从第几行开始读(从 0 开始),默认 0' },
375
+ limit: { type: 'number', description: '最多读取多少行,默认 500' },
376
+ },
377
+ required: ['path'],
378
+ },
379
+ },
380
+ },
381
+ {
382
+ type: 'function',
383
+ function: {
384
+ name: 'write_file',
385
+ description: '写入文件内容。如果文件不存在会创建(含父目录)。会覆盖已有内容。',
386
+ parameters: {
387
+ type: 'object',
388
+ properties: {
389
+ path: { type: 'string', description: '文件的绝对路径或相对于工作目录的路径' },
390
+ content: { type: 'string', description: '要写入的内容' },
391
+ },
392
+ required: ['path', 'content'],
393
+ },
394
+ },
395
+ },
396
+ {
397
+ type: 'function',
398
+ function: {
399
+ name: 'list_directory',
400
+ description: '列出目录下的文件和子目录。',
401
+ parameters: {
402
+ type: 'object',
403
+ properties: {
404
+ path: { type: 'string', description: '目录路径,默认为当前工作目录' },
405
+ },
406
+ required: [],
407
+ },
408
+ },
409
+ },
410
+ {
411
+ type: 'function',
412
+ function: {
413
+ name: 'search_files',
414
+ description: '按文件名模式搜索文件,支持通配符(如 *.js、**/*.log)。',
415
+ parameters: {
416
+ type: 'object',
417
+ properties: {
418
+ pattern: { type: 'string', description: 'glob 模式,如 "**/*.js" 或 "src/**/*.ts"' },
419
+ path: { type: 'string', description: '搜索根目录,默认为当前工作目录' },
420
+ },
421
+ required: ['pattern'],
422
+ },
423
+ },
424
+ },
425
+ {
426
+ type: 'function',
427
+ function: {
428
+ name: 'execute_command',
429
+ description: '执行 shell 命令并返回输出。可以执行任意命令。',
430
+ parameters: {
431
+ type: 'object',
432
+ properties: {
433
+ command: { type: 'string', description: '要执行的 shell 命令' },
434
+ cwd: { type: 'string', description: '工作目录,默认为当前工作目录' },
435
+ timeout: { type: 'number', description: '超时时间(毫秒),默认 30000' },
436
+ },
437
+ required: ['command'],
438
+ },
439
+ },
440
+ },
441
+ ];
442
+
443
+ const TOOL_HANDLERS = {
444
+ get_system_status: async () => {
445
+ const proxies = configStore.getProxies().map(p => {
446
+ const provider = configStore.getProviderById(p.providerId);
447
+ return { name: p.name, port: p.port, running: proxyManager.isRunning(p.id), providerName: provider?.name || '' };
448
+ });
449
+ return { proxies, providerCount: configStore.getProviders().length, uptime: Math.floor(process.uptime()) };
450
+ },
451
+
452
+ get_providers: async () => {
453
+ return configStore.getProviders().map(p => {
454
+ const h = keyHealth.get(p.id);
455
+ let healthStatus = '未检测';
456
+ if (h) {
457
+ const ok = h.keys?.filter(k => k.ok).length || 0;
458
+ const total = h.keys?.length || 0;
459
+ healthStatus = h.status === 'healthy' ? `健康 (${ok}/${total})` :
460
+ h.status === 'partial' ? `部分异常 (${ok}/${total})` :
461
+ h.status === 'unhealthy' ? `异常 (${ok}/${total})` : '未检测';
462
+ }
463
+ return { id: p.id, name: p.name, url: p.url, protocol: p.protocol, keyCount: (p.apiKeys || []).length, health: healthStatus };
464
+ });
465
+ },
466
+
467
+ get_provider: async (args) => {
468
+ const p = configStore.getProviderById(args.providerId);
469
+ if (!p) return { error: `供应商 ${args.providerId} 不存在` };
470
+ const h = keyHealth.get(p.id);
471
+ return { id: p.id, name: p.name, url: p.url, protocol: p.protocol, apiKeys: (p.apiKeys || []).map((k, i) => ({ index: i, alias: k.alias || '', enabled: k.enabled !== false })), health: h || null };
472
+ },
473
+
474
+ get_proxies: async () => {
475
+ return configStore.getProxies().map(p => {
476
+ const provider = configStore.getProviderById(p.providerId);
477
+ return { id: p.id, name: p.name, port: p.port, running: proxyManager.isRunning(p.id), providerName: provider?.name || '', protocol: provider?.protocol || '', defaultModel: p.defaultModel || '', routingStrategy: p.routingStrategy || 'primary_fallback' };
478
+ });
479
+ },
480
+
481
+ get_proxy: async (args) => {
482
+ const p = configStore.getProxyById(args.proxyId);
483
+ if (!p) return { error: `代理 ${args.proxyId} 不存在` };
484
+ const provider = configStore.getProviderById(p.providerId);
485
+ return { id: p.id, name: p.name, port: p.port, running: proxyManager.isRunning(p.id), providerName: provider?.name || '', protocol: provider?.protocol || '', defaultModel: p.defaultModel || '', routingStrategy: p.routingStrategy || 'primary_fallback', requireAuth: !!p.requireAuth };
486
+ },
487
+
488
+ get_usage_stats: async (args) => {
489
+ return statsStore.getStats({ range: args.range || 'daily', startDate: args.startDate, endDate: args.endDate, proxyId: args.proxyId });
490
+ },
491
+
492
+ get_recent_requests: async (args) => {
493
+ const limit = Math.min(Math.max(1, parseInt(args.limit) || 20), 100);
494
+ return { entries: requestLog.getAll(limit) };
495
+ },
496
+
497
+ get_system_logs: async (args) => {
498
+ const limit = Math.min(Math.max(1, parseInt(args.limit) || 30), 100);
499
+ try {
500
+ const content = await fs.promises.readFile(logger.LOG_FILE, 'utf8');
501
+ const allLines = content.split('\n').filter(l => l.trim());
502
+ return { lines: allLines.slice(-limit) };
503
+ } catch {
504
+ return { lines: [] };
505
+ }
506
+ },
507
+
508
+ get_key_health: async () => {
509
+ const result = {};
510
+ for (const [providerId, health] of keyHealth) {
511
+ result[providerId] = health;
512
+ }
513
+ return result;
514
+ },
515
+
516
+ get_settings: async () => {
517
+ return configStore.getSettings();
518
+ },
519
+
520
+ get_config_history: async () => {
521
+ return { snapshots: configStore.getSnapshots() };
522
+ },
523
+
524
+ read_file: async (args) => {
525
+ const filePath = path.resolve(args.path);
526
+ try {
527
+ const content = await fs.promises.readFile(filePath, 'utf8');
528
+ const lines = content.split('\n');
529
+ const offset = Math.max(0, parseInt(args.offset) || 0);
530
+ const limit = Math.min(Math.max(1, parseInt(args.limit) || 500), 2000);
531
+ const sliced = lines.slice(offset, offset + limit);
532
+ return { content: sliced.join('\n'), totalLines: lines.length, offset, returnedLines: sliced.length };
533
+ } catch (err) {
534
+ return { error: err.message };
535
+ }
536
+ },
537
+
538
+ write_file: async (args) => {
539
+ const filePath = path.resolve(args.path);
540
+ try {
541
+ await fs.promises.mkdir(path.dirname(filePath), { recursive: true });
542
+ await fs.promises.writeFile(filePath, args.content, 'utf8');
543
+ return { success: true, path: filePath, bytes: Buffer.byteLength(args.content, 'utf8') };
544
+ } catch (err) {
545
+ return { error: err.message };
546
+ }
547
+ },
548
+
549
+ list_directory: async (args) => {
550
+ const dirPath = path.resolve(args.path || '.');
551
+ try {
552
+ const entries = await fs.promises.readdir(dirPath, { withFileTypes: true });
553
+ return {
554
+ path: dirPath,
555
+ entries: entries.map(e => ({
556
+ name: e.name,
557
+ type: e.isDirectory() ? 'directory' : 'file',
558
+ })),
559
+ };
560
+ } catch (err) {
561
+ return { error: err.message };
562
+ }
563
+ },
564
+
565
+ search_files: async (args) => {
566
+ const root = path.resolve(args.path || '.');
567
+ const pattern = args.pattern;
568
+ try {
569
+ const results = [];
570
+ const globToRegex = (g) => {
571
+ const r = g.replace(/\*\*/g, '§GLOBSTAR§')
572
+ .replace(/\*/g, '[^/]*')
573
+ .replace(/\?/g, '[^/]')
574
+ .replace(/§GLOBSTAR§/g, '.*');
575
+ return new RegExp('^' + r + '$');
576
+ };
577
+ const regex = globToRegex(pattern);
578
+ const walk = async (dir, rel) => {
579
+ const entries = await fs.promises.readdir(dir, { withFileTypes: true });
580
+ for (const e of entries) {
581
+ const fullPath = path.join(dir, e.name);
582
+ const relPath = rel ? `${rel}/${e.name}` : e.name;
583
+ if (e.isDirectory()) {
584
+ if (e.name === 'node_modules' || e.name === '.git') continue;
585
+ await walk(fullPath, relPath);
586
+ } else if (regex.test(relPath)) {
587
+ results.push(relPath);
588
+ }
589
+ }
590
+ };
591
+ await walk(root, '');
592
+ return { pattern, root, matches: results.slice(0, 200), total: results.length };
593
+ } catch (err) {
594
+ return { error: err.message };
595
+ }
596
+ },
597
+
598
+ execute_command: async (args) => {
599
+ const timeout = Math.min(Math.max(1000, parseInt(args.timeout) || 30000), 120000);
600
+ return new Promise((resolve) => {
601
+ exec(args.command, { cwd: args.cwd || process.cwd(), timeout, maxBuffer: 1024 * 1024 }, (err, stdout, stderr) => {
602
+ if (err) {
603
+ resolve({ exitCode: err.code || 1, stdout: stdout || '', stderr: stderr || err.message });
604
+ } else {
605
+ resolve({ exitCode: 0, stdout: stdout || '', stderr: stderr || '' });
606
+ }
607
+ });
608
+ });
609
+ },
610
+ };
611
+
612
+ async function startProxyWithProvider(proxy) {
613
+ const target = resolveTarget(proxy);
614
+ if (!target) throw new Error(`供应商 ${proxy.providerId} 不存在`);
615
+ const proxyConfig = { ...proxy, target };
616
+ return proxyManager.startProxy(proxyConfig);
617
+ }
618
+
619
+ // ==================== API Key 健康检查 ====================
620
+
621
+ const keyHealth = new Map(); // providerId -> { status, lastCheck, keys: [{index, ok, message}] }
622
+ let healthCheckRunning = false;
623
+
624
+ async function checkAllProviderKeys() {
625
+ if (healthCheckRunning) return;
626
+ healthCheckRunning = true;
627
+ try {
628
+ const providers = configStore.getProviders();
629
+ logger.log(`[Health] 开始检查 ${providers.length} 个供应商的 API Key...`);
630
+ for (const provider of providers) {
631
+ await checkProviderKeys(provider);
632
+ }
633
+ logger.log('[Health] API Key 健康检查完成');
634
+ } finally {
635
+ healthCheckRunning = false;
636
+ }
637
+ }
638
+
639
+ async function checkProviderKeys(provider) {
640
+ const keys = (provider.apiKeys || []).filter(k => k.enabled !== false);
641
+ if (keys.length === 0) {
642
+ keyHealth.set(provider.id, { status: 'unknown', lastCheck: Date.now(), keys: [] });
643
+ return;
644
+ }
645
+
646
+ const protocol = provider.protocol || 'openai';
647
+ const base = provider.url.replace(/\/$/, '');
648
+ const hasV1Suffix = base.endsWith('/v1');
649
+ const isAzure = protocol === 'openai' && !!provider.azureDeployment;
650
+
651
+ const results = await Promise.all(keys.map(async (k, i) => {
652
+ try {
653
+ let testUrl, fetchOpts;
654
+ if (protocol === 'openai') {
655
+ if (isAzure) {
656
+ const ver = provider.azureApiVersion || '2024-02-01';
657
+ testUrl = `${base}/openai/deployments/${provider.azureDeployment}/models?api-version=${ver}`;
658
+ fetchOpts = { headers: { 'api-key': k.key } };
659
+ } else {
660
+ testUrl = hasV1Suffix ? `${base}/models` : `${base}/v1/models`;
661
+ fetchOpts = { headers: { 'Authorization': `Bearer ${k.key}` } };
662
+ }
663
+ } else if (protocol === 'anthropic') {
664
+ const testModel = (provider.models && provider.models[0]) || 'claude-3-haiku-20240307';
665
+ testUrl = hasV1Suffix ? `${base}/messages` : `${base}/v1/messages`;
666
+ fetchOpts = {
667
+ method: 'POST',
668
+ headers: { 'Content-Type': 'application/json', 'x-api-key': k.key, 'anthropic-version': '2023-06-01' },
669
+ body: JSON.stringify({ model: testModel, max_tokens: 1, messages: [{ role: 'user', content: 'hi' }] }),
670
+ };
671
+ } else if (protocol === 'gemini') {
672
+ testUrl = `${base}/v1beta/models?key=${k.key}`;
673
+ fetchOpts = {};
674
+ } else {
675
+ return { index: i, ok: false, message: '不支持的协议' };
676
+ }
677
+ const res = await fetch(testUrl, { ...fetchOpts, signal: AbortSignal.timeout(15000) });
678
+ if (!res.ok) {
679
+ const hint = res.status === 401 || res.status === 403 ? 'Key 无效或无权限' : `HTTP ${res.status}`;
680
+ return { index: i, ok: false, message: hint };
681
+ }
682
+ return { index: i, ok: true };
683
+ } catch (err) {
684
+ return { index: i, ok: false, message: err.name === 'TimeoutError' ? '连接超时' : err.message };
685
+ }
686
+ }));
687
+
688
+ const allOk = results.every(r => r.ok);
689
+ const anyOk = results.some(r => r.ok);
690
+ keyHealth.set(provider.id, {
691
+ status: allOk ? 'healthy' : anyOk ? 'partial' : 'unhealthy',
692
+ lastCheck: Date.now(),
693
+ keys: results,
694
+ });
695
+ }
696
+
697
+ // 启动后延迟 5 秒执行首次检查
698
+ setTimeout(() => checkAllProviderKeys(), 5000);
699
+ // 每 24 小时检查一次
700
+ setInterval(() => checkAllProviderKeys(), 24 * 60 * 60 * 1000);
701
+
702
+ // ==================== 供应商 API ====================
703
+
704
+ app.get('/api/providers', (req, res) => {
705
+ const providers = configStore.getProviders().map(p => ({
706
+ ...p,
707
+ apiKey: p.apiKey ? '***' : '',
708
+ apiKeys: (p.apiKeys || []).map((k, i) => ({ alias: k.alias || '', masked: true, index: i, enabled: k.enabled !== false })),
709
+ }));
710
+ res.json(providers);
711
+ });
712
+
713
+ app.get('/api/providers/:id', (req, res) => {
714
+ const provider = configStore.getProviderById(req.params.id);
715
+ if (!provider) return res.status(404).json({ error: 'Provider not found' });
716
+ res.json({ ...provider, apiKey: provider.apiKey ? '***' : '', apiKeys: (provider.apiKeys || []).map((k, i) => ({ alias: k.alias || '', masked: true, index: i, enabled: k.enabled !== false })) });
717
+ });
718
+
719
+ app.post('/api/providers', (req, res) => {
720
+ const { name, url, protocol, apiKey, apiKeys, models, azureDeployment, azureApiVersion } = req.body;
721
+ if (!name || !url) {
722
+ return res.status(400).json({ error: 'name and url are required' });
723
+ }
724
+ const provider = configStore.addProvider({
725
+ name, url,
726
+ protocol: protocol || (/anthropic/i.test(url) ? 'anthropic' : 'openai'),
727
+ apiKey: apiKey || '',
728
+ apiKeys: Array.isArray(apiKeys) ? apiKeys.filter(k => k && typeof k === 'object' && k.key && k.key.trim()) : [],
729
+ models: models || [],
730
+ azureDeployment: azureDeployment || '',
731
+ azureApiVersion: azureApiVersion || '',
732
+ });
733
+ res.status(201).json(provider);
734
+ });
735
+
736
+ app.put('/api/providers/:id', async (req, res) => {
737
+ const existing = configStore.getProviderById(req.params.id);
738
+ if (!existing) return res.status(404).json({ error: 'Provider not found' });
739
+
740
+ const updates = {};
741
+ if (req.body.name !== undefined) updates.name = req.body.name;
742
+ if (req.body.url !== undefined) updates.url = req.body.url;
743
+ if (req.body.protocol !== undefined) updates.protocol = req.body.protocol;
744
+ if (req.body.apiKey !== undefined && req.body.apiKey !== '') updates.apiKey = req.body.apiKey;
745
+ if (req.body.apiKeys !== undefined) {
746
+ // Map masked entries back to existing keys by index
747
+ const existingKeys = existing.apiKeys || [];
748
+ updates.apiKeys = req.body.apiKeys
749
+ .map(k => {
750
+ if (k && typeof k === 'object' && k.masked && typeof k.index === 'number') {
751
+ const existing = existingKeys[k.index];
752
+ if (!existing) return null;
753
+ return { ...existing, alias: typeof k.alias === 'string' ? k.alias.trim() : (existing.alias || ''), enabled: k.enabled !== false };
754
+ }
755
+ if (k && typeof k === 'object' && typeof k.key === 'string' && k.key.trim()) {
756
+ return { key: k.key.trim(), alias: typeof k.alias === 'string' ? k.alias.trim() : '', enabled: k.enabled !== false };
757
+ }
758
+ if (typeof k === 'string' && k.trim()) {
759
+ return { key: k.trim(), alias: '' };
760
+ }
761
+ return null;
762
+ })
763
+ .filter(Boolean);
764
+ }
765
+ if (req.body.models !== undefined) updates.models = req.body.models;
766
+ if (req.body.azureDeployment !== undefined) updates.azureDeployment = req.body.azureDeployment;
767
+ if (req.body.azureApiVersion !== undefined) updates.azureApiVersion = req.body.azureApiVersion;
768
+
769
+ const updated = configStore.updateProvider(req.params.id, updates);
770
+
771
+ // 同步更新引用此供应商的运行中代理
772
+ const affectedProxies = configStore.getProxies().filter(p => p.providerId === req.params.id);
773
+ for (const proxy of affectedProxies) {
774
+ if (!proxyManager.isRunning(proxy.id)) continue;
775
+ const target = resolveTarget(proxy);
776
+ if (target) proxyManager.updateProxyConfig({ ...proxy, target });
777
+ }
778
+
779
+ res.json(updated);
780
+ });
781
+
782
+ app.post('/api/providers/:id/test', async (req, res) => {
783
+ const provider = configStore.getProviderById(req.params.id);
784
+ if (!provider) return res.status(404).json({ error: 'Provider not found' });
785
+
786
+ const existingKeys = provider.apiKeys || [];
787
+ const reqKeys = Array.isArray(req.body.apiKeys) ? req.body.apiKeys : [];
788
+ const resolved = reqKeys
789
+ .map((k, i) => {
790
+ if (k && typeof k === 'object' && k.masked && typeof k.index === 'number') {
791
+ const ex = existingKeys[k.index];
792
+ return ex ? { key: ex.key, alias: k.alias || ex.alias || '', domIndex: i } : null;
793
+ }
794
+ if (k && typeof k === 'object' && typeof k.key === 'string' && k.key.trim()) {
795
+ return { key: k.key.trim(), alias: k.alias || '', domIndex: i };
796
+ }
797
+ if (typeof k === 'string' && k.trim()) return { key: k.trim(), alias: '', domIndex: i };
798
+ return null;
799
+ })
800
+ .filter(Boolean);
801
+
802
+ if (resolved.length === 0) {
803
+ return res.json({ ok: false, message: '没有可用的 API Key', results: [] });
804
+ }
805
+
806
+ const protocol = req.body.protocol || provider.protocol || 'openai';
807
+ const base = provider.url.replace(/\/$/, '');
808
+ const hasV1Suffix = base.endsWith('/v1');
809
+ const isAzure = protocol === 'openai' && !!provider.azureDeployment;
810
+
811
+ function buildTestOpts(key) {
812
+ if (protocol === 'openai') {
813
+ if (isAzure) {
814
+ const ver = provider.azureApiVersion || '2024-02-01';
815
+ return {
816
+ url: `${base}/openai/deployments/${provider.azureDeployment}/models?api-version=${ver}`,
817
+ opts: { headers: { 'api-key': key } },
818
+ };
819
+ }
820
+ return {
821
+ url: hasV1Suffix ? `${base}/models` : `${base}/v1/models`,
822
+ opts: { headers: { 'Authorization': `Bearer ${key}` } },
823
+ };
824
+ }
825
+ if (protocol === 'anthropic') {
826
+ const testModel = req.body.model || (provider.models && provider.models[0]) || 'claude-3-haiku-20240307';
827
+ return {
828
+ url: hasV1Suffix ? `${base}/messages` : `${base}/v1/messages`,
829
+ opts: {
830
+ method: 'POST',
831
+ headers: { 'Content-Type': 'application/json', 'x-api-key': key, 'anthropic-version': '2023-06-01' },
832
+ body: JSON.stringify({ model: testModel, max_tokens: 1, messages: [{ role: 'user', content: 'hi' }] }),
833
+ },
834
+ };
835
+ }
836
+ if (protocol === 'gemini') {
837
+ return { url: `${base}/v1beta/models?key=${key}`, opts: {} };
838
+ }
839
+ return null;
840
+ }
841
+
842
+ if (protocol !== 'openai' && protocol !== 'anthropic' && protocol !== 'gemini') {
843
+ return res.json({ ok: false, message: `不支持的协议: ${protocol}`, results: [] });
844
+ }
845
+
846
+ const results = await Promise.all(resolved.map(async entry => {
847
+ const { url: testUrl, opts: fetchOpts } = buildTestOpts(entry.key);
848
+ try {
849
+ const startedAt = Date.now();
850
+ const fetchRes = await fetch(testUrl, { ...fetchOpts, signal: AbortSignal.timeout(15000) });
851
+ const latencyMs = Date.now() - startedAt;
852
+ if (!fetchRes.ok) {
853
+ const errText = await fetchRes.text().catch(() => '');
854
+ const hint = fetchRes.status === 401 || fetchRes.status === 403
855
+ ? 'API Key 无效或无权限'
856
+ : `HTTP ${fetchRes.status}: ${errText.slice(0, 200) || '未知错误'}`;
857
+ return { ok: false, alias: entry.alias, index: entry.domIndex, message: hint, latencyMs };
858
+ }
859
+ return { ok: true, alias: entry.alias, index: entry.domIndex, latencyMs };
860
+ } catch (err) {
861
+ const msg = err.name === 'TimeoutError' ? '连接超时 (15s)' : `连接失败: ${err.message}`;
862
+ return { ok: false, alias: entry.alias, index: entry.domIndex, message: msg };
863
+ }
864
+ }));
865
+
866
+ const passed = results.filter(r => r.ok).length;
867
+ const failed = results.length - passed;
868
+ res.json({ ok: failed === 0, passed, failed, total: results.length, results });
869
+ });
870
+
871
+ app.post('/api/test-connection', async (req, res) => {
872
+ const { url, protocol, apiKeys, models, azureDeployment, azureApiVersion } = req.body || {};
873
+ if (!url || !protocol) return res.json({ ok: false, message: '缺少 url 或 protocol', results: [] });
874
+ if (!Array.isArray(apiKeys) || apiKeys.length === 0) {
875
+ return res.json({ ok: false, message: '没有可用的 API Key', results: [] });
876
+ }
877
+ const keys = apiKeys.filter(k => k && k.key);
878
+ if (keys.length === 0) return res.json({ ok: false, message: '没有可用的 API Key', results: [] });
879
+ const base = url.replace(/\/$/, '');
880
+ const hasV1Suffix = base.endsWith('/v1');
881
+ const isAzure = protocol === 'openai' && !!azureDeployment;
882
+
883
+ function buildTestOpts(key) {
884
+ if (protocol === 'openai') {
885
+ if (isAzure) {
886
+ const ver = azureApiVersion || '2024-02-01';
887
+ return { url: `${base}/openai/deployments/${azureDeployment}/models?api-version=${ver}`, opts: { headers: { 'api-key': key } } };
888
+ }
889
+ return { url: hasV1Suffix ? `${base}/models` : `${base}/v1/models`, opts: { headers: { 'Authorization': `Bearer ${key}` } } };
890
+ }
891
+ if (protocol === 'anthropic') {
892
+ const testModel = (Array.isArray(models) && models[0]) || 'claude-3-haiku-20240307';
893
+ return {
894
+ url: hasV1Suffix ? `${base}/messages` : `${base}/v1/messages`,
895
+ 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' }] }) },
896
+ };
897
+ }
898
+ if (protocol === 'gemini') return { url: `${base}/v1beta/models?key=${key}`, opts: {} };
899
+ return null;
900
+ }
901
+
902
+ const results = await Promise.all(keys.map(async (k) => {
903
+ const built = buildTestOpts(k.key);
904
+ if (!built) return { ok: false, alias: k.alias || '', message: '不支持的协议' };
905
+ try {
906
+ const started = Date.now();
907
+ const fetchRes = await fetch(built.url, { ...built.opts, signal: AbortSignal.timeout(15000) });
908
+ const latency = Date.now() - started;
909
+ if (!fetchRes.ok) {
910
+ const hint = fetchRes.status === 401 || fetchRes.status === 403 ? 'API Key 无效或无权限' : `HTTP ${fetchRes.status}`;
911
+ return { ok: false, alias: k.alias || '', message: hint, latency };
912
+ }
913
+ return { ok: true, alias: k.alias || '', latency };
914
+ } catch (err) {
915
+ return { ok: false, alias: k.alias || '', message: err.name === 'TimeoutError' ? '连接超时' : err.message };
916
+ }
917
+ }));
918
+
919
+ const passed = results.filter(r => r.ok).length;
920
+ res.json({ ok: passed === keys.length, passed, failed: keys.length - passed, results });
921
+ });
922
+
923
+ app.post('/api/providers/available-models', async (req, res) => {
924
+ const { url, protocol, apiKey, azureDeployment, azureApiVersion } = req.body || {};
925
+ if (!url || !protocol) return res.json({ models: [], message: '缺少 url 或 protocol 参数' });
926
+ const key = apiKey || '';
927
+ const base = url.replace(/\/$/, '');
928
+ const hasV1Suffix = base.endsWith('/v1');
929
+ const isAzure = protocol === 'openai' && !!azureDeployment;
930
+ try {
931
+ let fetchUrl, fetchOpts;
932
+ if (protocol === 'openai') {
933
+ if (isAzure) {
934
+ const ver = azureApiVersion || '2024-02-01';
935
+ fetchUrl = `${base}/openai/deployments/${azureDeployment}/models?api-version=${ver}`;
936
+ fetchOpts = { headers: { 'api-key': key } };
937
+ } else {
938
+ fetchUrl = hasV1Suffix ? `${base}/models` : `${base}/v1/models`;
939
+ fetchOpts = key ? { headers: { 'Authorization': `Bearer ${key}` } } : {};
940
+ }
941
+ } else if (protocol === 'gemini') {
942
+ fetchUrl = `${base}/v1beta/models?key=${key}`;
943
+ fetchOpts = {};
944
+ } else if (protocol === 'anthropic') {
945
+ fetchUrl = hasV1Suffix ? `${base}/models` : `${base}/v1/models`;
946
+ fetchOpts = key ? { headers: { 'x-api-key': key, 'anthropic-version': '2023-06-01' } } : {};
947
+ } else {
948
+ return res.json({ models: [], message: `不支持的协议: ${protocol}` });
949
+ }
950
+ const fetchRes = await fetch(fetchUrl, { ...fetchOpts, signal: AbortSignal.timeout(15000) });
951
+ if (!fetchRes.ok) {
952
+ const hint = fetchRes.status === 404 ? '该供应商不支持模型列表接口' : `获取失败: HTTP ${fetchRes.status}`;
953
+ return res.json({ models: [], message: hint });
954
+ }
955
+ const data = await fetchRes.json().catch(() => null);
956
+ let models = [];
957
+ if (Array.isArray(data?.data)) {
958
+ models = data.data.map(m => m.id || m.name).filter(Boolean).sort();
959
+ } else if (Array.isArray(data?.models)) {
960
+ models = data.models.map(m => (m.name || m.id)?.replace('models/', '')).filter(Boolean).sort();
961
+ }
962
+ res.json({ models });
963
+ } catch (err) {
964
+ res.json({ models: [], message: `获取失败: ${err.message}` });
965
+ }
966
+ });
967
+
968
+ app.post('/api/providers/:id/available-models', async (req, res) => {
969
+ const provider = configStore.getProviderById(req.params.id);
970
+ if (!provider) return res.status(404).json({ error: 'Provider not found' });
971
+
972
+ // Support unsaved API keys from form
973
+ let keys;
974
+ const reqKeys = Array.isArray(req.body?.apiKeys) ? req.body.apiKeys : [];
975
+ if (reqKeys.length > 0) {
976
+ const existingKeys = provider.apiKeys || [];
977
+ keys = reqKeys
978
+ .map(k => {
979
+ if (k && typeof k === 'object' && k.masked && typeof k.index === 'number') {
980
+ return existingKeys[k.index]?.key || null;
981
+ }
982
+ if (k && typeof k === 'object' && typeof k.key === 'string' && k.key.trim()) {
983
+ return k.key.trim();
984
+ }
985
+ return null;
986
+ })
987
+ .filter(Boolean);
988
+ } else {
989
+ keys = (provider.apiKeys || []).map(k => k.key).filter(Boolean);
990
+ }
991
+ if (keys.length === 0) return res.json({ models: [], message: '没有可用的 API Key' });
992
+
993
+ const protocol = provider.protocol || 'openai';
994
+ const base = provider.url.replace(/\/$/, '');
995
+ const hasV1Suffix = base.endsWith('/v1');
996
+ const key = keys[0];
997
+ const isAzure = protocol === 'openai' && !!provider.azureDeployment;
998
+
999
+ try {
1000
+ let fetchUrl, fetchOpts;
1001
+ if (protocol === 'openai') {
1002
+ if (isAzure) {
1003
+ const ver = provider.azureApiVersion || '2024-02-01';
1004
+ fetchUrl = `${base}/openai/deployments/${provider.azureDeployment}/models?api-version=${ver}`;
1005
+ fetchOpts = { headers: { 'api-key': key } };
1006
+ } else {
1007
+ fetchUrl = hasV1Suffix ? `${base}/models` : `${base}/v1/models`;
1008
+ fetchOpts = { headers: { 'Authorization': `Bearer ${key}` } };
1009
+ }
1010
+ } else if (protocol === 'gemini') {
1011
+ fetchUrl = `${base}/v1beta/models?key=${key}`;
1012
+ fetchOpts = {};
1013
+ } else if (protocol === 'anthropic') {
1014
+ fetchUrl = hasV1Suffix ? `${base}/models` : `${base}/v1/models`;
1015
+ fetchOpts = { headers: { 'x-api-key': key, 'anthropic-version': '2023-06-01' } };
1016
+ } else {
1017
+ return res.json({ models: [], message: `不支持的协议: ${protocol}` });
1018
+ }
1019
+
1020
+ const fetchRes = await fetch(fetchUrl, { ...fetchOpts, signal: AbortSignal.timeout(15000) });
1021
+ if (!fetchRes.ok) {
1022
+ const hint = fetchRes.status === 404 ? '该供应商不支持模型列表接口' : `获取失败: HTTP ${fetchRes.status}`;
1023
+ return res.json({ models: [], message: hint });
1024
+ }
1025
+
1026
+ const data = await fetchRes.json().catch(() => null);
1027
+ let models = [];
1028
+ if (Array.isArray(data?.data)) {
1029
+ // OpenAI 格式(含第三方 Anthropic 兼容供应商)
1030
+ models = data.data.map(m => m.id || m.name).filter(Boolean).sort();
1031
+ } else if (Array.isArray(data?.models)) {
1032
+ // Gemini 格式
1033
+ models = data.models.map(m => (m.name || m.id)?.replace('models/', '')).filter(Boolean).sort();
1034
+ }
1035
+
1036
+ res.json({ models });
1037
+ } catch (err) {
1038
+ res.json({ models: [], message: `获取失败: ${err.message}` });
1039
+ }
1040
+ });
1041
+
1042
+ app.delete('/api/providers/:id', (req, res) => {
1043
+ const existing = configStore.getProviderById(req.params.id);
1044
+ if (!existing) return res.status(404).json({ error: 'Provider not found' });
1045
+
1046
+ // 检查是否有代理在使用此供应商
1047
+ const inUse = configStore.getProxies().some(p => p.providerId === req.params.id);
1048
+ if (inUse) {
1049
+ return res.status(409).json({ error: '该供应商正在被代理使用,无法删除' });
1050
+ }
1051
+
1052
+ configStore.removeProvider(req.params.id);
1053
+ res.json({ success: true });
1054
+ });
1055
+
1056
+ // ==================== 代理 API ====================
1057
+
1058
+ // 获取所有代理配置
1059
+ app.get('/api/proxies', (req, res) => {
1060
+ const proxies = configStore.getProxies().map(p => {
1061
+ const provider = configStore.getProviderById(p.providerId);
1062
+ return {
1063
+ id: p.id,
1064
+ name: p.name,
1065
+ port: p.port,
1066
+ requireAuth: p.requireAuth,
1067
+ authToken: p.authToken,
1068
+ providerId: p.providerId,
1069
+ providerName: provider?.name || '',
1070
+ providerUrl: provider?.url || '',
1071
+ protocol: provider?.protocol || '',
1072
+ defaultModel: p.defaultModel || '',
1073
+ providerWeight: Math.max(1, parseInt(p.providerWeight, 10) || 1),
1074
+ routingStrategy: p.routingStrategy || 'primary_fallback',
1075
+ providerPool: Array.isArray(p.providerPool) ? p.providerPool : [],
1076
+ hasApiKey: !!(provider?.apiKey || (provider?.apiKeys && provider.apiKeys.length > 0)),
1077
+ running: proxyManager.isRunning(p.id),
1078
+ };
1079
+ });
1080
+ res.json(proxies);
1081
+ });
1082
+
1083
+ // 获取单个代理配置
1084
+ app.get('/api/proxies/:id', (req, res) => {
1085
+ const proxy = configStore.getProxyById(req.params.id);
1086
+ if (!proxy) return res.status(404).json({ error: 'Proxy not found' });
1087
+ const provider = configStore.getProviderById(proxy.providerId);
1088
+ res.json({
1089
+ ...proxy,
1090
+ providerName: provider?.name || '',
1091
+ providerUrl: provider?.url || '',
1092
+ protocol: provider?.protocol || '',
1093
+ routingStrategy: proxy.routingStrategy || 'primary_fallback',
1094
+ providerPool: Array.isArray(proxy.providerPool) ? proxy.providerPool : [],
1095
+ hasApiKey: !!(provider?.apiKey || (provider?.apiKeys && provider.apiKeys.length > 0)),
1096
+ });
1097
+ });
1098
+
1099
+ // 创建代理
1100
+ app.post('/api/proxies', async (req, res) => {
1101
+ configStore.saveSnapshot('create-proxy');
1102
+ const { name, port, requireAuth, authToken, providerId, defaultModel, routingStrategy, providerPool, providerWeight } = req.body;
1103
+
1104
+ if (!name || !port || !providerId) {
1105
+ return res.status(400).json({ error: 'name, port and providerId are required' });
1106
+ }
1107
+
1108
+ const provider = configStore.getProviderById(providerId);
1109
+ if (!provider) return res.status(400).json({ error: '供应商不存在' });
1110
+
1111
+ const parsedPort = parseInt(port);
1112
+
1113
+ const existing = configStore.getProxies().find(p => p.port === parsedPort);
1114
+ if (existing) {
1115
+ return res.status(409).json({
1116
+ error: `端口 ${parsedPort} 已被代理「${existing.name}」占用,请更换端口`,
1117
+ });
1118
+ }
1119
+
1120
+ const proxy = configStore.addProxy({
1121
+ name,
1122
+ port: parsedPort,
1123
+ requireAuth: !!requireAuth,
1124
+ authToken: authToken || null,
1125
+ providerId,
1126
+ defaultModel: defaultModel || '',
1127
+ providerWeight: Math.max(1, parseInt(providerWeight, 10) || 1),
1128
+ routingStrategy: normalizeRoutingStrategyInput(routingStrategy),
1129
+ providerPool: normalizeProviderPoolInput(providerPool),
1130
+ });
1131
+
1132
+ try {
1133
+ await startProxyWithProvider(proxy);
1134
+ res.status(201).json({ ...proxy, running: true });
1135
+ } catch (err) {
1136
+ configStore.removeProxy(proxy.id);
1137
+ res.status(500).json({ error: `代理启动失败: ${err.message}` });
1138
+ }
1139
+ });
1140
+
1141
+ // 更新代理
1142
+ app.put('/api/proxies/:id', async (req, res) => {
1143
+ configStore.saveSnapshot('update-proxy');
1144
+ const existing = configStore.getProxyById(req.params.id);
1145
+ if (!existing) return res.status(404).json({ error: 'Proxy not found' });
1146
+
1147
+ const updates = {};
1148
+ if (req.body.name !== undefined) updates.name = req.body.name;
1149
+ if (req.body.port !== undefined) updates.port = parseInt(req.body.port);
1150
+ if (req.body.requireAuth !== undefined) updates.requireAuth = !!req.body.requireAuth;
1151
+ if (req.body.authToken !== undefined) updates.authToken = req.body.authToken || null;
1152
+ if (req.body.providerId !== undefined) {
1153
+ if (!configStore.getProviderById(req.body.providerId)) {
1154
+ return res.status(400).json({ error: '供应商不存在' });
1155
+ }
1156
+ updates.providerId = req.body.providerId;
1157
+ }
1158
+ if (req.body.defaultModel !== undefined) updates.defaultModel = req.body.defaultModel;
1159
+ if (req.body.providerWeight !== undefined) updates.providerWeight = Math.max(1, parseInt(req.body.providerWeight, 10) || 1);
1160
+ if (req.body.routingStrategy !== undefined) updates.routingStrategy = normalizeRoutingStrategyInput(req.body.routingStrategy);
1161
+ if (req.body.providerPool !== undefined) updates.providerPool = normalizeProviderPoolInput(req.body.providerPool);
1162
+
1163
+ const needRestart = updates.port !== undefined && updates.port !== existing.port;
1164
+ if (needRestart) {
1165
+ const conflict = configStore.getProxies().find(p => p.id !== req.params.id && p.port === updates.port);
1166
+ if (conflict) {
1167
+ return res.status(409).json({
1168
+ error: `端口 ${updates.port} 已被代理「${conflict.name}」占用,请更换端口`,
1169
+ });
1170
+ }
1171
+ }
1172
+
1173
+ const updated = configStore.updateProxy(req.params.id, updates);
1174
+
1175
+ if (needRestart) {
1176
+ try {
1177
+ await startProxyWithProvider(updated);
1178
+ } catch (err) {
1179
+ return res.status(500).json({ error: `代理重启失败: ${err.message}` });
1180
+ }
1181
+ } else {
1182
+ // 更新供应商配置引用
1183
+ const target = resolveTarget(updated);
1184
+ if (target) proxyManager.updateProxyConfig({ ...updated, target });
1185
+ }
1186
+
1187
+ res.json({ ...updated, running: proxyManager.isRunning(updated.id) });
1188
+ });
1189
+
1190
+ // 删除代理
1191
+ app.delete('/api/proxies/:id', async (req, res) => {
1192
+ configStore.saveSnapshot('delete-proxy');
1193
+ const existing = configStore.getProxyById(req.params.id);
1194
+ if (!existing) return res.status(404).json({ error: 'Proxy not found' });
1195
+
1196
+ await proxyManager.stopProxy(req.params.id);
1197
+ configStore.removeProxy(req.params.id);
1198
+ res.json({ success: true });
1199
+ });
1200
+
1201
+ // 启动/停止代理
1202
+ app.post('/api/proxies/:id/start', async (req, res) => {
1203
+ const proxy = configStore.getProxyById(req.params.id);
1204
+ if (!proxy) return res.status(404).json({ error: 'Proxy not found' });
1205
+
1206
+ try {
1207
+ await startProxyWithProvider(proxy);
1208
+ res.json({ success: true, running: true });
1209
+ } catch (err) {
1210
+ res.status(500).json({ error: 'Failed to start proxy', message: err.message });
1211
+ }
1212
+ });
1213
+
1214
+ app.post('/api/proxies/:id/stop', async (req, res) => {
1215
+ await proxyManager.stopProxy(req.params.id);
1216
+ res.json({ success: true, running: false });
1217
+ });
1218
+
1219
+ // 批量启动所有代理
1220
+ app.post('/api/proxies/start-all', async (req, res) => {
1221
+ const proxies = configStore.getProxies();
1222
+ const results = [];
1223
+ for (const proxy of proxies) {
1224
+ if (proxyManager.isRunning(proxy.id)) {
1225
+ results.push({ id: proxy.id, name: proxy.name, skipped: true });
1226
+ continue;
1227
+ }
1228
+ try {
1229
+ await startProxyWithProvider(proxy);
1230
+ results.push({ id: proxy.id, name: proxy.name, success: true });
1231
+ } catch (err) {
1232
+ results.push({ id: proxy.id, name: proxy.name, success: false, error: err.message });
1233
+ }
1234
+ }
1235
+ res.json({ results });
1236
+ });
1237
+
1238
+ // 批量停止所有代理
1239
+ app.post('/api/proxies/stop-all', async (req, res) => {
1240
+ const running = proxyManager.getRunningPorts();
1241
+ const results = [];
1242
+ for (const r of running) {
1243
+ await proxyManager.stopProxy(r.id);
1244
+ results.push({ id: r.id, name: r.name, success: true });
1245
+ }
1246
+ res.json({ results });
1247
+ });
1248
+
1249
+ // 获取运行状态
1250
+ app.get('/api/status', (req, res) => {
1251
+ res.json({
1252
+ running: proxyManager.getRunningPorts(),
1253
+ total: configStore.getProxies().length,
1254
+ });
1255
+ });
1256
+
1257
+ // 健康检查
1258
+ app.get('/api/health', (req, res) => {
1259
+ res.json({
1260
+ status: 'ok',
1261
+ version: pkg.version,
1262
+ uptime: process.uptime(),
1263
+ proxies: {
1264
+ total: configStore.getProxies().length,
1265
+ running: proxyManager.getRunningPorts().length,
1266
+ },
1267
+ });
1268
+ });
1269
+
1270
+ // API Key 健康状态
1271
+ app.get('/api/key-health', (req, res) => {
1272
+ const result = {};
1273
+ for (const [providerId, health] of keyHealth) {
1274
+ result[providerId] = health;
1275
+ }
1276
+ res.json(result);
1277
+ });
1278
+
1279
+ // 手动触发健康检查
1280
+ app.post('/api/key-health/check', async (req, res) => {
1281
+ await checkAllProviderKeys();
1282
+ res.json({ success: true });
1283
+ });
1284
+
1285
+ // 设置
1286
+ app.get('/api/settings', (req, res) => {
1287
+ res.json(configStore.getSettings());
1288
+ });
1289
+
1290
+ app.put('/api/settings', (req, res) => {
1291
+ const settings = req.body;
1292
+ if (!settings || typeof settings !== 'object') {
1293
+ return res.status(400).json({ error: '需要 settings 对象' });
1294
+ }
1295
+ for (const [key, value] of Object.entries(settings)) {
1296
+ configStore.setSetting(key, value);
1297
+ }
1298
+ res.json(configStore.getSettings());
1299
+ });
1300
+
1301
+ // Token 用量统计
1302
+ app.get('/api/stats', (req, res) => {
1303
+ const { range, startDate, endDate, proxyId } = req.query;
1304
+ const stats = statsStore.getStats({
1305
+ range: range || 'daily',
1306
+ startDate: startDate || undefined,
1307
+ endDate: endDate || undefined,
1308
+ proxyId: proxyId || undefined,
1309
+ });
1310
+ const proxies = configStore.getProxies().map(p => ({
1311
+ id: p.id,
1312
+ name: p.name,
1313
+ providerName: configStore.getProviderById(p.providerId)?.name || '',
1314
+ }));
1315
+ res.json({ ...stats, proxies });
1316
+ });
1317
+
1318
+ // 日志查看
1319
+ app.get('/api/logs', (req, res) => {
1320
+ const lines = Math.min(parseInt(req.query.lines) || 200, 2000);
1321
+ try {
1322
+ if (!fs.existsSync(logger.LOG_FILE)) {
1323
+ return res.json({ lines: [] });
1324
+ }
1325
+ const content = fs.readFileSync(logger.LOG_FILE, 'utf8');
1326
+ const allLines = content.split('\n').filter(l => l.trim());
1327
+ const tail = allLines.slice(-lines);
1328
+ res.json({ lines: tail, total: allLines.length });
1329
+ } catch (err) {
1330
+ res.json({ lines: [], error: err.message });
1331
+ }
1332
+ });
1333
+
1334
+ // 实时请求日志
1335
+ const requestLog = require('./lib/request-log');
1336
+ app.get('/api/request-logs', (req, res) => {
1337
+ const limit = Math.min(parseInt(req.query.limit) || 200, 2000);
1338
+ res.json({ entries: requestLog.getAll(limit), total: requestLog.getCount() });
1339
+ });
1340
+
1341
+ // ==================== 智控助手上下文 API ====================
1342
+
1343
+ app.get('/api/assistant/context', async (req, res) => {
1344
+ const proxyList = configStore.getProxies().map(p => {
1345
+ const provider = configStore.getProviderById(p.providerId);
1346
+ return {
1347
+ id: p.id,
1348
+ name: p.name,
1349
+ port: p.port,
1350
+ running: proxyManager.isRunning(p.id),
1351
+ providerId: p.providerId,
1352
+ providerName: provider?.name || '',
1353
+ protocol: provider?.protocol || '',
1354
+ defaultModel: p.defaultModel || '',
1355
+ routingStrategy: p.routingStrategy || 'primary_fallback',
1356
+ };
1357
+ });
1358
+
1359
+ const providerList = configStore.getProviders().map(p => ({
1360
+ id: p.id,
1361
+ name: p.name,
1362
+ url: p.url,
1363
+ protocol: p.protocol,
1364
+ apiKeys: (p.apiKeys || []).map((k, i) => ({ alias: k.alias || '', index: i, enabled: k.enabled !== false })),
1365
+ }));
1366
+
1367
+ const healthData = {};
1368
+ for (const [providerId, health] of keyHealth) {
1369
+ healthData[providerId] = health;
1370
+ }
1371
+
1372
+ const stats = statsStore.getStats({ range: 'daily' });
1373
+
1374
+ let recentLogs = [];
1375
+ try {
1376
+ const content = await fs.promises.readFile(logger.LOG_FILE, 'utf8');
1377
+ const allLines = content.split('\n').filter(l => l.trim());
1378
+ recentLogs = allLines.slice(-30);
1379
+ } catch {}
1380
+
1381
+ const recentRequests = requestLog.getAll(20);
1382
+
1383
+ res.json({
1384
+ proxies: proxyList,
1385
+ providers: providerList,
1386
+ health: healthData,
1387
+ stats,
1388
+ recentLogs,
1389
+ recentRequests,
1390
+ });
1391
+ });
1392
+
1393
+ // ==================== 智控助手 Tool Calling API ====================
1394
+
1395
+ function sendSSE(res, event, data) {
1396
+ res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
1397
+ }
1398
+
1399
+ app.post('/api/assistant/chat', async (req, res) => {
1400
+ const { proxyId, messages } = req.body;
1401
+ if (!proxyId || !Array.isArray(messages)) {
1402
+ return res.status(400).json({ error: '需要 proxyId 和 messages' });
1403
+ }
1404
+
1405
+ const proxy = configStore.getProxyById(proxyId);
1406
+ if (!proxy) return res.status(404).json({ error: '代理不存在' });
1407
+ if (!resolveTarget(proxy)) return res.status(500).json({ error: '代理目标未配置' });
1408
+
1409
+ const proxyUrl = `http://localhost:${proxy.port}/v1/chat/completions`;
1410
+ const proxyHeaders = { 'Content-Type': 'application/json' };
1411
+ if (proxy.requireAuth && proxy.authToken) {
1412
+ proxyHeaders['Authorization'] = `Bearer ${proxy.authToken}`;
1413
+ }
1414
+
1415
+ // SSE 响应头
1416
+ res.setHeader('Content-Type', 'text/event-stream');
1417
+ res.setHeader('Cache-Control', 'no-cache');
1418
+ res.setHeader('Connection', 'keep-alive');
1419
+
1420
+ // 发送 SSE 的辅助函数,忽略写入错误
1421
+ function safeSSE(event, data) {
1422
+ try { sendSSE(res, event, data); } catch {}
1423
+ }
1424
+
1425
+ const MAX_TOOL_ROUNDS = 10;
1426
+ const conversationMessages = [...messages];
1427
+
1428
+ try {
1429
+ for (let round = 0; round < MAX_TOOL_ROUNDS; round++) {
1430
+ logger.log(`[assistant] round ${round} — messages: ${conversationMessages.length}`);
1431
+ for (let i = 0; i < conversationMessages.length; i++) {
1432
+ const m = conversationMessages[i];
1433
+ logger.log(`[assistant] msg[${i}]: role=${m.role}, hasReasoning=${!!m.reasoning_content}, hasToolCalls=${!!m.tool_calls}, contentLen=${(m.content || '').length}`);
1434
+ }
1435
+
1436
+ // 调用本地代理
1437
+ let fetchRes;
1438
+ try {
1439
+ fetchRes = await fetch(proxyUrl, {
1440
+ method: 'POST',
1441
+ headers: proxyHeaders,
1442
+ signal: AbortSignal.timeout(300000),
1443
+ body: JSON.stringify({
1444
+ model: proxy.defaultModel || 'gpt-4o',
1445
+ messages: conversationMessages,
1446
+ stream: true,
1447
+ tools: TOOL_DEFINITIONS,
1448
+ tool_choice: 'auto',
1449
+ }),
1450
+ });
1451
+ } catch (fetchErr) {
1452
+ logger.log(`[assistant] round ${round} fetch error: ${fetchErr.message}`);
1453
+ safeSSE('error', { message: `代理请求失败: ${fetchErr.message}` });
1454
+ break;
1455
+ }
1456
+
1457
+ if (!fetchRes.ok) {
1458
+ const text = await fetchRes.text();
1459
+ logger.log(`[assistant] round ${round} HTTP ${fetchRes.status}: ${text.slice(0, 200)}`);
1460
+ safeSSE('error', { message: `代理请求失败: HTTP ${fetchRes.status} - ${text}` });
1461
+ break;
1462
+ }
1463
+
1464
+ // 解析 SSE 流
1465
+ const reader = fetchRes.body.getReader();
1466
+ const decoder = new TextDecoder();
1467
+ let buffer = '';
1468
+ let fullContent = '';
1469
+ let reasoningContent = '';
1470
+ const toolCallAccumulator = {};
1471
+
1472
+ while (true) {
1473
+ const { done, value } = await reader.read();
1474
+ if (done) break;
1475
+
1476
+ buffer += decoder.decode(value, { stream: true });
1477
+ const lines = buffer.split('\n');
1478
+ buffer = lines.pop();
1479
+
1480
+ for (const line of lines) {
1481
+ const trimmed = line.trim();
1482
+ if (!trimmed || !trimmed.startsWith('data: ')) continue;
1483
+ const payload = trimmed.slice(6);
1484
+ if (payload === '[DONE]') continue;
1485
+
1486
+ try {
1487
+ const data = JSON.parse(payload);
1488
+ const delta = data.choices?.[0]?.delta;
1489
+ if (!delta) continue;
1490
+
1491
+ if (delta.content) {
1492
+ fullContent += delta.content;
1493
+ safeSSE('content', { delta: delta.content });
1494
+ }
1495
+
1496
+ if (delta.reasoning_content) {
1497
+ reasoningContent += delta.reasoning_content;
1498
+ }
1499
+
1500
+ if (delta.tool_calls) {
1501
+ for (const tc of delta.tool_calls) {
1502
+ const idx = tc.index;
1503
+ if (!toolCallAccumulator[idx]) {
1504
+ toolCallAccumulator[idx] = { id: '', name: '', arguments: '' };
1505
+ }
1506
+ if (tc.id) toolCallAccumulator[idx].id = tc.id;
1507
+ if (tc.function?.name) toolCallAccumulator[idx].name = tc.function.name;
1508
+ if (tc.function?.arguments) toolCallAccumulator[idx].arguments += tc.function.arguments;
1509
+ }
1510
+ }
1511
+ } catch {}
1512
+ }
1513
+ }
1514
+
1515
+ const toolCalls = Object.values(toolCallAccumulator).filter(tc => tc.id && tc.name);
1516
+ logger.log(`[assistant] round ${round} done — content: ${fullContent.length} chars, tool_calls: ${toolCalls.length}`);
1517
+
1518
+ if (toolCalls.length === 0) {
1519
+ safeSSE('done', { reasoning_content: reasoningContent || undefined });
1520
+ break;
1521
+ }
1522
+
1523
+ // 通知前端
1524
+ safeSSE('tool_calls', {
1525
+ reasoning_content: reasoningContent || undefined,
1526
+ calls: toolCalls.map(tc => {
1527
+ let args = {};
1528
+ try { args = JSON.parse(tc.arguments); } catch {}
1529
+ return { id: tc.id, name: tc.name, arguments: args };
1530
+ }),
1531
+ });
1532
+
1533
+ // 追加 assistant 消息到对话历史
1534
+ const assistantMsg = {
1535
+ role: 'assistant',
1536
+ content: fullContent || null,
1537
+ tool_calls: toolCalls.map(tc => ({
1538
+ id: tc.id,
1539
+ type: 'function',
1540
+ function: { name: tc.name, arguments: tc.arguments },
1541
+ })),
1542
+ };
1543
+ if (reasoningContent) assistantMsg.reasoning_content = reasoningContent;
1544
+ conversationMessages.push(assistantMsg);
1545
+
1546
+ // 执行工具
1547
+ for (const tc of toolCalls) {
1548
+ let args = {};
1549
+ try { args = JSON.parse(tc.arguments); } catch {}
1550
+ logger.log(`[assistant] EXEC tool: ${tc.name}(${JSON.stringify(args)})`);
1551
+ let result;
1552
+ try {
1553
+ result = await TOOL_HANDLERS[tc.name]?.(args) || { error: `未知工具: ${tc.name}` };
1554
+ } catch (err) {
1555
+ logger.log(`[assistant] tool ${tc.name} error: ${err.message}`);
1556
+ result = { error: err.message };
1557
+ }
1558
+
1559
+ const resultStr = JSON.stringify(result);
1560
+ logger.log(`[assistant] tool ${tc.name} done: ${resultStr.length} chars`);
1561
+ safeSSE('tool_result', { tool_call_id: tc.id, name: tc.name, result });
1562
+
1563
+ conversationMessages.push({
1564
+ role: 'tool',
1565
+ tool_call_id: tc.id,
1566
+ content: resultStr,
1567
+ });
1568
+ }
1569
+ // 继续下一轮
1570
+ }
1571
+
1572
+ // 循环正常结束(达到最大轮次)
1573
+ safeSSE('done', {});
1574
+ } catch (err) {
1575
+ logger.log(`[assistant] error: ${err.message}`);
1576
+ if (!res.headersSent) {
1577
+ res.status(502).json({ error: `助手请求失败: ${err.message}` });
1578
+ } else {
1579
+ safeSSE('error', { message: err.message });
1580
+ }
1581
+ } finally {
1582
+ res.end();
1583
+ }
1584
+ });
1585
+
1586
+ // ==================== 配置导入/导出 ====================
1587
+
1588
+ app.get('/api/config/export', (req, res) => {
1589
+ const providers = configStore.getProviders();
1590
+ const proxies = configStore.getProxies().map(p => {
1591
+ const provider = configStore.getProviderById(p.providerId);
1592
+ return {
1593
+ id: p.id,
1594
+ name: p.name,
1595
+ port: p.port,
1596
+ requireAuth: p.requireAuth,
1597
+ authToken: p.authToken,
1598
+ providerId: p.providerId,
1599
+ defaultModel: p.defaultModel || '',
1600
+ routingStrategy: p.routingStrategy || 'primary_fallback',
1601
+ providerPool: Array.isArray(p.providerPool) ? p.providerPool : [],
1602
+ providerName: provider?.name || '',
1603
+ };
1604
+ });
1605
+ res.json({ providers, proxies, exportedAt: new Date().toISOString() });
1606
+ });
1607
+
1608
+ app.post('/api/config/import', async (req, res) => {
1609
+ const { config, mode } = req.body;
1610
+
1611
+ if (!config || !mode || !['overwrite', 'merge'].includes(mode)) {
1612
+ return res.status(400).json({ error: '需要 config 和 mode(overwrite/merge)' });
1613
+ }
1614
+
1615
+ configStore.saveSnapshot('import-' + mode);
1616
+
1617
+ // 校验结构
1618
+ if (!Array.isArray(config.providers) || !Array.isArray(config.proxies)) {
1619
+ return res.status(400).json({ error: '配置格式错误:需要 providers 和 proxies 数组' });
1620
+ }
1621
+
1622
+ for (const p of config.providers) {
1623
+ if (!p.name || !p.url || !p.protocol) {
1624
+ return res.status(400).json({ error: `供应商 "${p.name || '?'}" 缺少必要字段(name/url/protocol)` });
1625
+ }
1626
+ }
1627
+
1628
+ for (const p of config.proxies) {
1629
+ if (!p.name || !p.port || !p.providerId) {
1630
+ return res.status(400).json({ error: `代理 "${p.name || '?'}" 缺少必要字段(name/port/providerId)` });
1631
+ }
1632
+ }
1633
+
1634
+ if (mode === 'overwrite') {
1635
+ // 覆盖模式:直接替换整个配置
1636
+ const newConfig = {
1637
+ providers: config.providers.map(p => ({
1638
+ id: p.id,
1639
+ name: p.name,
1640
+ url: p.url,
1641
+ protocol: p.protocol,
1642
+ apiKey: p.apiKey || '',
1643
+ models: Array.isArray(p.models) ? p.models : [],
1644
+ })),
1645
+ proxies: config.proxies.map(p => ({
1646
+ id: p.id,
1647
+ name: p.name,
1648
+ port: p.port,
1649
+ requireAuth: !!p.requireAuth,
1650
+ authToken: p.authToken || null,
1651
+ providerId: p.providerId,
1652
+ defaultModel: p.defaultModel || '',
1653
+ routingStrategy: normalizeRoutingStrategyInput(p.routingStrategy),
1654
+ providerPool: normalizeProviderPoolInput(p.providerPool),
1655
+ })),
1656
+ };
1657
+ configStore.saveConfig(newConfig);
1658
+ return res.json({ success: true, mode, providers: newConfig.providers.length, proxies: newConfig.proxies.length });
1659
+ }
1660
+
1661
+ // 合并模式:按 ID 去重
1662
+ const existingProviders = configStore.getProviders();
1663
+ const existingProxies = configStore.getProxies();
1664
+
1665
+ const providerMap = new Map(existingProviders.map(p => [p.id, p]));
1666
+ for (const p of config.providers) {
1667
+ providerMap.set(p.id, {
1668
+ id: p.id,
1669
+ name: p.name,
1670
+ url: p.url,
1671
+ protocol: p.protocol,
1672
+ apiKey: p.apiKey || '',
1673
+ models: Array.isArray(p.models) ? p.models : [],
1674
+ routingStrategy: normalizeRoutingStrategyInput(p.routingStrategy),
1675
+ providerPool: normalizeProviderPoolInput(p.providerPool),
1676
+ });
1677
+ }
1678
+
1679
+ const proxyMap = new Map(existingProxies.map(p => [p.id, p]));
1680
+ for (const p of config.proxies) {
1681
+ // 检查端口冲突:导入的代理端口不能和现有代理或其他导入代理重复
1682
+ const conflict = proxyMap.get(p.id)
1683
+ ? null // 同 ID 是覆盖,不算冲突
1684
+ : Array.from(proxyMap.values()).find(ep => ep.port === p.port);
1685
+ if (conflict) {
1686
+ return res.status(409).json({
1687
+ error: `端口 ${p.port} 已被代理「${conflict.name}」占用,无法导入代理「${p.name}」`,
1688
+ });
1689
+ }
1690
+ proxyMap.set(p.id, {
1691
+ id: p.id,
1692
+ name: p.name,
1693
+ port: p.port,
1694
+ requireAuth: !!p.requireAuth,
1695
+ authToken: p.authToken || null,
1696
+ providerId: p.providerId,
1697
+ defaultModel: p.defaultModel || '',
1698
+ routingStrategy: normalizeRoutingStrategyInput(p.routingStrategy),
1699
+ providerPool: normalizeProviderPoolInput(p.providerPool),
1700
+ });
1701
+ }
1702
+
1703
+ const merged = {
1704
+ providers: Array.from(providerMap.values()),
1705
+ proxies: Array.from(proxyMap.values()),
1706
+ };
1707
+ configStore.saveConfig(merged);
1708
+
1709
+ res.json({
1710
+ success: true,
1711
+ mode,
1712
+ providers: merged.providers.length,
1713
+ proxies: merged.proxies.length,
1714
+ added: {
1715
+ providers: merged.providers.length - existingProviders.length,
1716
+ proxies: merged.proxies.length - existingProxies.length,
1717
+ },
1718
+ });
1719
+ });
1720
+
1721
+ // ==================== 配置版本历史 ====================
1722
+
1723
+ app.get('/api/config/history', (req, res) => {
1724
+ const snapshots = configStore.getSnapshots();
1725
+ res.json({ snapshots });
1726
+ });
1727
+
1728
+ app.post('/api/config/rollback', async (req, res) => {
1729
+ const { file } = req.body;
1730
+ if (!file) return res.status(400).json({ error: '需要指定快照文件' });
1731
+ const result = configStore.restoreSnapshot(file);
1732
+ if (result.error) return res.status(400).json({ error: result.error });
1733
+ res.json({ success: true });
1734
+ });
1735
+
1736
+ // 前端首页
1737
+ app.get('/', (req, res) => {
1738
+ res.sendFile(path.join(__dirname, 'public', 'index.html'));
1739
+ });
1740
+
1741
+ // 启动
1742
+ logger.init();
1743
+ writePid();
1744
+
1745
+ // 启动所有已配置的代理
1746
+ const proxies = configStore.getProxies();
1747
+ await Promise.all(proxies.map(async (proxy) => {
1748
+ try {
1749
+ await startProxyWithProvider(proxy);
1750
+ } catch (err) {
1751
+ logger.error(`[Init] Failed to start proxy ${proxy.name}:`, err.message);
1752
+ }
1753
+ }));
1754
+
1755
+ const http = require('http');
1756
+ const server = app.listen(PORT, () => {
1757
+ const adminUrl = `http://localhost:${PORT}`;
1758
+ logger.log(`[Admin] Management server running on ${adminUrl}`);
1759
+ logger.log(`[Admin] ${proxies.length} proxy config(s) loaded`);
1760
+ logger.log(`[Admin] 日志文件: ${logger.LOG_FILE}`);
1761
+
1762
+ // 初始化 WebSocket 实时日志
1763
+ const wsServer = require('./lib/ws-server');
1764
+ wsServer.init(server);
1765
+ requestLog.onEntry((entry) => wsServer.broadcast(entry));
1766
+ logger.log(`[Admin] WebSocket 已附加 (ws://localhost:${PORT})`);
1767
+
1768
+ openBrowser(adminUrl);
1769
+ });
1770
+ }
1771
+
1772
+ // 优雅关闭
1773
+ process.on('SIGINT', async () => {
1774
+ logger.log('[Shutdown] Shutting down...');
1775
+ removePid();
1776
+ try {
1777
+ const wsServer = require('./lib/ws-server');
1778
+ wsServer.close();
1779
+ const proxyManager = require('./lib/proxy-manager');
1780
+ const statsStore = require('./lib/stats-store');
1781
+ statsStore.flush();
1782
+ await proxyManager.stopAll();
1783
+ } catch (err) {
1784
+ logger.error('[Shutdown] stopAll error:', err.message);
1785
+ }
1786
+ process.exit(0);
1787
+ });
1788
+
1789
+ process.on('SIGTERM', async () => {
1790
+ removePid();
1791
+ try {
1792
+ const wsServer = require('./lib/ws-server');
1793
+ wsServer.close();
1794
+ const proxyManager = require('./lib/proxy-manager');
1795
+ const statsStore = require('./lib/stats-store');
1796
+ statsStore.flush();
1797
+ await proxyManager.stopAll();
1798
+ } catch (err) {
1799
+ logger.error('[Shutdown] stopAll error:', err.message);
1800
+ }
1801
+ process.exit(0);
1802
+ });
1803
+
1804
+ // ==================== CLI Dispatch ====================
1805
+
1806
+ const cmd = process.argv[2];
1807
+
1808
+ switch (cmd) {
1809
+ case 'help':
1810
+ showHelp();
1811
+ break;
1812
+ case '-v':
1813
+ case '--version':
1814
+ showVersion();
1815
+ break;
1816
+ case 'update':
1817
+ updateService();
1818
+ break;
1819
+ case 'stop':
1820
+ stopService();
1821
+ break;
1822
+ case 'status':
1823
+ showStatus();
1824
+ break;
1825
+ case 'start':
1826
+ startDaemon();
1827
+ break;
1828
+ case '--daemon':
1829
+ init();
1830
+ break;
1831
+ case undefined:
1832
+ init();
1833
+ break;
1834
+ default:
1835
+ console.error(`未知命令: ${cmd}`);
1836
+ showHelp();
1837
+ process.exit(1);
1838
+ }