protocol-proxy 2.4.0 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/config-store.js +295 -260
- package/lib/converters/gemini-to-anthropic.js +286 -277
- package/lib/converters/gemini-to-openai.js +255 -240
- package/lib/converters/openai-to-anthropic.js +368 -329
- package/lib/proxy-server.js +636 -491
- package/package.json +1 -1
- package/public/app.js +1296 -1222
- package/public/index.html +7 -2
- package/public/style.css +1448 -1344
- package/server.js +767 -747
package/server.js
CHANGED
|
@@ -1,747 +1,767 @@
|
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
if (!
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
models: models || [],
|
|
280
|
-
azureDeployment: azureDeployment || '',
|
|
281
|
-
azureApiVersion: azureApiVersion || '',
|
|
282
|
-
});
|
|
283
|
-
res.status(201).json(provider);
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
app.put('/api/providers/:id', async (req, res) => {
|
|
287
|
-
const existing = configStore.getProviderById(req.params.id);
|
|
288
|
-
if (!existing) return res.status(404).json({ error: 'Provider not found' });
|
|
289
|
-
|
|
290
|
-
const updates = {};
|
|
291
|
-
if (req.body.name !== undefined) updates.name = req.body.name;
|
|
292
|
-
if (req.body.url !== undefined) updates.url = req.body.url;
|
|
293
|
-
if (req.body.protocol !== undefined) updates.protocol = req.body.protocol;
|
|
294
|
-
if (req.body.apiKey !== undefined && req.body.apiKey !== '') updates.apiKey = req.body.apiKey;
|
|
295
|
-
if (req.body.
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
configStore.
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
const provider = configStore.getProviderById(providerId);
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
providerId
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
});
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
status: '
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
app.
|
|
547
|
-
const
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
return
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
})
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
});
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
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 ====================
|
|
253
|
+
|
|
254
|
+
app.get('/api/providers', (req, res) => {
|
|
255
|
+
const providers = configStore.getProviders().map(p => ({
|
|
256
|
+
...p,
|
|
257
|
+
apiKey: p.apiKey ? '***' : '',
|
|
258
|
+
apiKeys: (p.apiKeys || []).map((k, i) => ({ alias: k.alias || '', masked: true, index: i, enabled: k.enabled !== false })),
|
|
259
|
+
}));
|
|
260
|
+
res.json(providers);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
app.get('/api/providers/:id', (req, res) => {
|
|
264
|
+
const provider = configStore.getProviderById(req.params.id);
|
|
265
|
+
if (!provider) return res.status(404).json({ error: 'Provider not found' });
|
|
266
|
+
res.json({ ...provider, apiKey: provider.apiKey ? '***' : '', apiKeys: (provider.apiKeys || []).map((k, i) => ({ alias: k.alias || '', masked: true, index: i, enabled: k.enabled !== false })) });
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
app.post('/api/providers', (req, res) => {
|
|
270
|
+
const { name, url, protocol, apiKey, apiKeys, models, azureDeployment, azureApiVersion } = req.body;
|
|
271
|
+
if (!name || !url) {
|
|
272
|
+
return res.status(400).json({ error: 'name and url are required' });
|
|
273
|
+
}
|
|
274
|
+
const provider = configStore.addProvider({
|
|
275
|
+
name, url,
|
|
276
|
+
protocol: protocol || (/anthropic/i.test(url) ? 'anthropic' : 'openai'),
|
|
277
|
+
apiKey: apiKey || '',
|
|
278
|
+
apiKeys: Array.isArray(apiKeys) ? apiKeys.filter(k => k && typeof k === 'object' && k.key && k.key.trim()) : [],
|
|
279
|
+
models: models || [],
|
|
280
|
+
azureDeployment: azureDeployment || '',
|
|
281
|
+
azureApiVersion: azureApiVersion || '',
|
|
282
|
+
});
|
|
283
|
+
res.status(201).json(provider);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
app.put('/api/providers/:id', async (req, res) => {
|
|
287
|
+
const existing = configStore.getProviderById(req.params.id);
|
|
288
|
+
if (!existing) return res.status(404).json({ error: 'Provider not found' });
|
|
289
|
+
|
|
290
|
+
const updates = {};
|
|
291
|
+
if (req.body.name !== undefined) updates.name = req.body.name;
|
|
292
|
+
if (req.body.url !== undefined) updates.url = req.body.url;
|
|
293
|
+
if (req.body.protocol !== undefined) updates.protocol = req.body.protocol;
|
|
294
|
+
if (req.body.apiKey !== undefined && req.body.apiKey !== '') updates.apiKey = req.body.apiKey;
|
|
295
|
+
if (req.body.apiKeys !== undefined) {
|
|
296
|
+
// Map masked entries back to existing keys by index
|
|
297
|
+
const existingKeys = existing.apiKeys || [];
|
|
298
|
+
updates.apiKeys = req.body.apiKeys
|
|
299
|
+
.map(k => {
|
|
300
|
+
if (k && typeof k === 'object' && k.masked && typeof k.index === 'number') {
|
|
301
|
+
const existing = existingKeys[k.index];
|
|
302
|
+
if (!existing) return null;
|
|
303
|
+
return { ...existing, alias: typeof k.alias === 'string' ? k.alias.trim() : (existing.alias || ''), enabled: k.enabled !== false };
|
|
304
|
+
}
|
|
305
|
+
if (k && typeof k === 'object' && typeof k.key === 'string' && k.key.trim()) {
|
|
306
|
+
return { key: k.key.trim(), alias: typeof k.alias === 'string' ? k.alias.trim() : '', enabled: k.enabled !== false };
|
|
307
|
+
}
|
|
308
|
+
if (typeof k === 'string' && k.trim()) {
|
|
309
|
+
return { key: k.trim(), alias: '' };
|
|
310
|
+
}
|
|
311
|
+
return null;
|
|
312
|
+
})
|
|
313
|
+
.filter(Boolean);
|
|
314
|
+
}
|
|
315
|
+
if (req.body.models !== undefined) updates.models = req.body.models;
|
|
316
|
+
if (req.body.azureDeployment !== undefined) updates.azureDeployment = req.body.azureDeployment;
|
|
317
|
+
if (req.body.azureApiVersion !== undefined) updates.azureApiVersion = req.body.azureApiVersion;
|
|
318
|
+
|
|
319
|
+
const updated = configStore.updateProvider(req.params.id, updates);
|
|
320
|
+
|
|
321
|
+
// 同步更新引用此供应商的运行中代理
|
|
322
|
+
const affectedProxies = configStore.getProxies().filter(p => p.providerId === req.params.id);
|
|
323
|
+
for (const proxy of affectedProxies) {
|
|
324
|
+
if (!proxyManager.isRunning(proxy.id)) continue;
|
|
325
|
+
const target = resolveTarget(proxy);
|
|
326
|
+
if (target) proxyManager.updateProxyConfig({ ...proxy, target });
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
res.json(updated);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
app.delete('/api/providers/:id', (req, res) => {
|
|
333
|
+
const existing = configStore.getProviderById(req.params.id);
|
|
334
|
+
if (!existing) return res.status(404).json({ error: 'Provider not found' });
|
|
335
|
+
|
|
336
|
+
// 检查是否有代理在使用此供应商
|
|
337
|
+
const inUse = configStore.getProxies().some(p => p.providerId === req.params.id);
|
|
338
|
+
if (inUse) {
|
|
339
|
+
return res.status(409).json({ error: '该供应商正在被代理使用,无法删除' });
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
configStore.removeProvider(req.params.id);
|
|
343
|
+
res.json({ success: true });
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
// ==================== 代理 API ====================
|
|
347
|
+
|
|
348
|
+
// 获取所有代理配置
|
|
349
|
+
app.get('/api/proxies', (req, res) => {
|
|
350
|
+
const proxies = configStore.getProxies().map(p => {
|
|
351
|
+
const provider = configStore.getProviderById(p.providerId);
|
|
352
|
+
return {
|
|
353
|
+
id: p.id,
|
|
354
|
+
name: p.name,
|
|
355
|
+
port: p.port,
|
|
356
|
+
requireAuth: p.requireAuth,
|
|
357
|
+
authToken: p.authToken,
|
|
358
|
+
providerId: p.providerId,
|
|
359
|
+
providerName: provider?.name || '',
|
|
360
|
+
providerUrl: provider?.url || '',
|
|
361
|
+
protocol: provider?.protocol || '',
|
|
362
|
+
defaultModel: p.defaultModel || '',
|
|
363
|
+
providerWeight: Math.max(1, parseInt(p.providerWeight, 10) || 1),
|
|
364
|
+
routingStrategy: p.routingStrategy || 'primary_fallback',
|
|
365
|
+
providerPool: Array.isArray(p.providerPool) ? p.providerPool : [],
|
|
366
|
+
hasApiKey: !!(provider?.apiKey || (provider?.apiKeys && provider.apiKeys.length > 0)),
|
|
367
|
+
running: proxyManager.isRunning(p.id),
|
|
368
|
+
};
|
|
369
|
+
});
|
|
370
|
+
res.json(proxies);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
// 获取单个代理配置
|
|
374
|
+
app.get('/api/proxies/:id', (req, res) => {
|
|
375
|
+
const proxy = configStore.getProxyById(req.params.id);
|
|
376
|
+
if (!proxy) return res.status(404).json({ error: 'Proxy not found' });
|
|
377
|
+
const provider = configStore.getProviderById(proxy.providerId);
|
|
378
|
+
res.json({
|
|
379
|
+
...proxy,
|
|
380
|
+
providerName: provider?.name || '',
|
|
381
|
+
providerUrl: provider?.url || '',
|
|
382
|
+
protocol: provider?.protocol || '',
|
|
383
|
+
routingStrategy: proxy.routingStrategy || 'primary_fallback',
|
|
384
|
+
providerPool: Array.isArray(proxy.providerPool) ? proxy.providerPool : [],
|
|
385
|
+
hasApiKey: !!(provider?.apiKey || (provider?.apiKeys && provider.apiKeys.length > 0)),
|
|
386
|
+
});
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
// 创建代理
|
|
390
|
+
app.post('/api/proxies', async (req, res) => {
|
|
391
|
+
const { name, port, requireAuth, authToken, providerId, defaultModel, routingStrategy, providerPool, providerWeight } = req.body;
|
|
392
|
+
|
|
393
|
+
if (!name || !port || !providerId) {
|
|
394
|
+
return res.status(400).json({ error: 'name, port and providerId are required' });
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const provider = configStore.getProviderById(providerId);
|
|
398
|
+
if (!provider) return res.status(400).json({ error: '供应商不存在' });
|
|
399
|
+
|
|
400
|
+
const parsedPort = parseInt(port);
|
|
401
|
+
|
|
402
|
+
const existing = configStore.getProxies().find(p => p.port === parsedPort);
|
|
403
|
+
if (existing) {
|
|
404
|
+
return res.status(409).json({
|
|
405
|
+
error: `端口 ${parsedPort} 已被代理「${existing.name}」占用,请更换端口`,
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const proxy = configStore.addProxy({
|
|
410
|
+
name,
|
|
411
|
+
port: parsedPort,
|
|
412
|
+
requireAuth: !!requireAuth,
|
|
413
|
+
authToken: authToken || null,
|
|
414
|
+
providerId,
|
|
415
|
+
defaultModel: defaultModel || '',
|
|
416
|
+
providerWeight: Math.max(1, parseInt(providerWeight, 10) || 1),
|
|
417
|
+
routingStrategy: normalizeRoutingStrategyInput(routingStrategy),
|
|
418
|
+
providerPool: normalizeProviderPoolInput(providerPool),
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
try {
|
|
422
|
+
await startProxyWithProvider(proxy);
|
|
423
|
+
res.status(201).json({ ...proxy, running: true });
|
|
424
|
+
} catch (err) {
|
|
425
|
+
configStore.removeProxy(proxy.id);
|
|
426
|
+
res.status(500).json({ error: `代理启动失败: ${err.message}` });
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
// 更新代理
|
|
431
|
+
app.put('/api/proxies/:id', async (req, res) => {
|
|
432
|
+
const existing = configStore.getProxyById(req.params.id);
|
|
433
|
+
if (!existing) return res.status(404).json({ error: 'Proxy not found' });
|
|
434
|
+
|
|
435
|
+
const updates = {};
|
|
436
|
+
if (req.body.name !== undefined) updates.name = req.body.name;
|
|
437
|
+
if (req.body.port !== undefined) updates.port = parseInt(req.body.port);
|
|
438
|
+
if (req.body.requireAuth !== undefined) updates.requireAuth = !!req.body.requireAuth;
|
|
439
|
+
if (req.body.authToken !== undefined) updates.authToken = req.body.authToken || null;
|
|
440
|
+
if (req.body.providerId !== undefined) {
|
|
441
|
+
if (!configStore.getProviderById(req.body.providerId)) {
|
|
442
|
+
return res.status(400).json({ error: '供应商不存在' });
|
|
443
|
+
}
|
|
444
|
+
updates.providerId = req.body.providerId;
|
|
445
|
+
}
|
|
446
|
+
if (req.body.defaultModel !== undefined) updates.defaultModel = req.body.defaultModel;
|
|
447
|
+
if (req.body.providerWeight !== undefined) updates.providerWeight = Math.max(1, parseInt(req.body.providerWeight, 10) || 1);
|
|
448
|
+
if (req.body.routingStrategy !== undefined) updates.routingStrategy = normalizeRoutingStrategyInput(req.body.routingStrategy);
|
|
449
|
+
if (req.body.providerPool !== undefined) updates.providerPool = normalizeProviderPoolInput(req.body.providerPool);
|
|
450
|
+
|
|
451
|
+
const needRestart = updates.port !== undefined && updates.port !== existing.port;
|
|
452
|
+
if (needRestart) {
|
|
453
|
+
const conflict = configStore.getProxies().find(p => p.id !== req.params.id && p.port === updates.port);
|
|
454
|
+
if (conflict) {
|
|
455
|
+
return res.status(409).json({
|
|
456
|
+
error: `端口 ${updates.port} 已被代理「${conflict.name}」占用,请更换端口`,
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const updated = configStore.updateProxy(req.params.id, updates);
|
|
462
|
+
|
|
463
|
+
if (needRestart) {
|
|
464
|
+
try {
|
|
465
|
+
await startProxyWithProvider(updated);
|
|
466
|
+
} catch (err) {
|
|
467
|
+
return res.status(500).json({ error: `代理重启失败: ${err.message}` });
|
|
468
|
+
}
|
|
469
|
+
} else {
|
|
470
|
+
// 更新供应商配置引用
|
|
471
|
+
const target = resolveTarget(updated);
|
|
472
|
+
if (target) proxyManager.updateProxyConfig({ ...updated, target });
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
res.json({ ...updated, running: proxyManager.isRunning(updated.id) });
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
// 删除代理
|
|
479
|
+
app.delete('/api/proxies/:id', async (req, res) => {
|
|
480
|
+
const existing = configStore.getProxyById(req.params.id);
|
|
481
|
+
if (!existing) return res.status(404).json({ error: 'Proxy not found' });
|
|
482
|
+
|
|
483
|
+
await proxyManager.stopProxy(req.params.id);
|
|
484
|
+
configStore.removeProxy(req.params.id);
|
|
485
|
+
res.json({ success: true });
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
// 启动/停止代理
|
|
489
|
+
app.post('/api/proxies/:id/start', async (req, res) => {
|
|
490
|
+
const proxy = configStore.getProxyById(req.params.id);
|
|
491
|
+
if (!proxy) return res.status(404).json({ error: 'Proxy not found' });
|
|
492
|
+
|
|
493
|
+
try {
|
|
494
|
+
await startProxyWithProvider(proxy);
|
|
495
|
+
res.json({ success: true, running: true });
|
|
496
|
+
} catch (err) {
|
|
497
|
+
res.status(500).json({ error: 'Failed to start proxy', message: err.message });
|
|
498
|
+
}
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
app.post('/api/proxies/:id/stop', async (req, res) => {
|
|
502
|
+
await proxyManager.stopProxy(req.params.id);
|
|
503
|
+
res.json({ success: true, running: false });
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
// 获取运行状态
|
|
507
|
+
app.get('/api/status', (req, res) => {
|
|
508
|
+
res.json({
|
|
509
|
+
running: proxyManager.getRunningPorts(),
|
|
510
|
+
total: configStore.getProxies().length,
|
|
511
|
+
});
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
// 健康检查
|
|
515
|
+
app.get('/api/health', (req, res) => {
|
|
516
|
+
res.json({
|
|
517
|
+
status: 'ok',
|
|
518
|
+
version: pkg.version,
|
|
519
|
+
uptime: process.uptime(),
|
|
520
|
+
proxies: {
|
|
521
|
+
total: configStore.getProxies().length,
|
|
522
|
+
running: proxyManager.getRunningPorts().length,
|
|
523
|
+
},
|
|
524
|
+
});
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
// Token 用量统计
|
|
528
|
+
app.get('/api/stats', (req, res) => {
|
|
529
|
+
const { range, startDate, endDate, proxyId } = req.query;
|
|
530
|
+
const stats = statsStore.getStats({
|
|
531
|
+
range: range || 'daily',
|
|
532
|
+
startDate: startDate || undefined,
|
|
533
|
+
endDate: endDate || undefined,
|
|
534
|
+
proxyId: proxyId || undefined,
|
|
535
|
+
});
|
|
536
|
+
const proxies = configStore.getProxies().map(p => ({
|
|
537
|
+
id: p.id,
|
|
538
|
+
name: p.name,
|
|
539
|
+
providerName: configStore.getProviderById(p.providerId)?.name || '',
|
|
540
|
+
}));
|
|
541
|
+
res.json({ ...stats, proxies });
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
// ==================== 配置导入/导出 ====================
|
|
545
|
+
|
|
546
|
+
app.get('/api/config/export', (req, res) => {
|
|
547
|
+
const providers = configStore.getProviders();
|
|
548
|
+
const proxies = configStore.getProxies().map(p => {
|
|
549
|
+
const provider = configStore.getProviderById(p.providerId);
|
|
550
|
+
return {
|
|
551
|
+
id: p.id,
|
|
552
|
+
name: p.name,
|
|
553
|
+
port: p.port,
|
|
554
|
+
requireAuth: p.requireAuth,
|
|
555
|
+
authToken: p.authToken,
|
|
556
|
+
providerId: p.providerId,
|
|
557
|
+
defaultModel: p.defaultModel || '',
|
|
558
|
+
routingStrategy: p.routingStrategy || 'primary_fallback',
|
|
559
|
+
providerPool: Array.isArray(p.providerPool) ? p.providerPool : [],
|
|
560
|
+
providerName: provider?.name || '',
|
|
561
|
+
};
|
|
562
|
+
});
|
|
563
|
+
res.json({ providers, proxies, exportedAt: new Date().toISOString() });
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
app.post('/api/config/import', async (req, res) => {
|
|
567
|
+
const { config, mode } = req.body;
|
|
568
|
+
|
|
569
|
+
if (!config || !mode || !['overwrite', 'merge'].includes(mode)) {
|
|
570
|
+
return res.status(400).json({ error: '需要 config 和 mode(overwrite/merge)' });
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// 校验结构
|
|
574
|
+
if (!Array.isArray(config.providers) || !Array.isArray(config.proxies)) {
|
|
575
|
+
return res.status(400).json({ error: '配置格式错误:需要 providers 和 proxies 数组' });
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
for (const p of config.providers) {
|
|
579
|
+
if (!p.name || !p.url || !p.protocol) {
|
|
580
|
+
return res.status(400).json({ error: `供应商 "${p.name || '?'}" 缺少必要字段(name/url/protocol)` });
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
for (const p of config.proxies) {
|
|
585
|
+
if (!p.name || !p.port || !p.providerId) {
|
|
586
|
+
return res.status(400).json({ error: `代理 "${p.name || '?'}" 缺少必要字段(name/port/providerId)` });
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
if (mode === 'overwrite') {
|
|
591
|
+
// 覆盖模式:直接替换整个配置
|
|
592
|
+
const newConfig = {
|
|
593
|
+
providers: config.providers.map(p => ({
|
|
594
|
+
id: p.id,
|
|
595
|
+
name: p.name,
|
|
596
|
+
url: p.url,
|
|
597
|
+
protocol: p.protocol,
|
|
598
|
+
apiKey: p.apiKey || '',
|
|
599
|
+
models: Array.isArray(p.models) ? p.models : [],
|
|
600
|
+
})),
|
|
601
|
+
proxies: config.proxies.map(p => ({
|
|
602
|
+
id: p.id,
|
|
603
|
+
name: p.name,
|
|
604
|
+
port: p.port,
|
|
605
|
+
requireAuth: !!p.requireAuth,
|
|
606
|
+
authToken: p.authToken || null,
|
|
607
|
+
providerId: p.providerId,
|
|
608
|
+
defaultModel: p.defaultModel || '',
|
|
609
|
+
routingStrategy: normalizeRoutingStrategyInput(p.routingStrategy),
|
|
610
|
+
providerPool: normalizeProviderPoolInput(p.providerPool),
|
|
611
|
+
})),
|
|
612
|
+
};
|
|
613
|
+
configStore.saveConfig(newConfig);
|
|
614
|
+
return res.json({ success: true, mode, providers: newConfig.providers.length, proxies: newConfig.proxies.length });
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// 合并模式:按 ID 去重
|
|
618
|
+
const existingProviders = configStore.getProviders();
|
|
619
|
+
const existingProxies = configStore.getProxies();
|
|
620
|
+
|
|
621
|
+
const providerMap = new Map(existingProviders.map(p => [p.id, p]));
|
|
622
|
+
for (const p of config.providers) {
|
|
623
|
+
providerMap.set(p.id, {
|
|
624
|
+
id: p.id,
|
|
625
|
+
name: p.name,
|
|
626
|
+
url: p.url,
|
|
627
|
+
protocol: p.protocol,
|
|
628
|
+
apiKey: p.apiKey || '',
|
|
629
|
+
models: Array.isArray(p.models) ? p.models : [],
|
|
630
|
+
routingStrategy: normalizeRoutingStrategyInput(p.routingStrategy),
|
|
631
|
+
providerPool: normalizeProviderPoolInput(p.providerPool),
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
const proxyMap = new Map(existingProxies.map(p => [p.id, p]));
|
|
636
|
+
for (const p of config.proxies) {
|
|
637
|
+
// 检查端口冲突:导入的代理端口不能和现有代理或其他导入代理重复
|
|
638
|
+
const conflict = proxyMap.get(p.id)
|
|
639
|
+
? null // 同 ID 是覆盖,不算冲突
|
|
640
|
+
: Array.from(proxyMap.values()).find(ep => ep.port === p.port);
|
|
641
|
+
if (conflict) {
|
|
642
|
+
return res.status(409).json({
|
|
643
|
+
error: `端口 ${p.port} 已被代理「${conflict.name}」占用,无法导入代理「${p.name}」`,
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
proxyMap.set(p.id, {
|
|
647
|
+
id: p.id,
|
|
648
|
+
name: p.name,
|
|
649
|
+
port: p.port,
|
|
650
|
+
requireAuth: !!p.requireAuth,
|
|
651
|
+
authToken: p.authToken || null,
|
|
652
|
+
providerId: p.providerId,
|
|
653
|
+
defaultModel: p.defaultModel || '',
|
|
654
|
+
routingStrategy: normalizeRoutingStrategyInput(p.routingStrategy),
|
|
655
|
+
providerPool: normalizeProviderPoolInput(p.providerPool),
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
const merged = {
|
|
660
|
+
providers: Array.from(providerMap.values()),
|
|
661
|
+
proxies: Array.from(proxyMap.values()),
|
|
662
|
+
};
|
|
663
|
+
configStore.saveConfig(merged);
|
|
664
|
+
|
|
665
|
+
res.json({
|
|
666
|
+
success: true,
|
|
667
|
+
mode,
|
|
668
|
+
providers: merged.providers.length,
|
|
669
|
+
proxies: merged.proxies.length,
|
|
670
|
+
added: {
|
|
671
|
+
providers: merged.providers.length - existingProviders.length,
|
|
672
|
+
proxies: merged.proxies.length - existingProxies.length,
|
|
673
|
+
},
|
|
674
|
+
});
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
// 前端首页
|
|
678
|
+
app.get('/', (req, res) => {
|
|
679
|
+
res.sendFile(path.join(__dirname, 'public', 'index.html'));
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
// 启动
|
|
683
|
+
logger.init();
|
|
684
|
+
writePid();
|
|
685
|
+
|
|
686
|
+
// 启动所有已配置的代理
|
|
687
|
+
const proxies = configStore.getProxies();
|
|
688
|
+
await Promise.all(proxies.map(async (proxy) => {
|
|
689
|
+
try {
|
|
690
|
+
await startProxyWithProvider(proxy);
|
|
691
|
+
} catch (err) {
|
|
692
|
+
logger.error(`[Init] Failed to start proxy ${proxy.name}:`, err.message);
|
|
693
|
+
}
|
|
694
|
+
}));
|
|
695
|
+
|
|
696
|
+
app.listen(PORT, () => {
|
|
697
|
+
const adminUrl = `http://localhost:${PORT}`;
|
|
698
|
+
logger.log(`[Admin] Management server running on ${adminUrl}`);
|
|
699
|
+
logger.log(`[Admin] ${proxies.length} proxy config(s) loaded`);
|
|
700
|
+
logger.log(`[Admin] 日志文件: ${logger.LOG_FILE}`);
|
|
701
|
+
openBrowser(adminUrl);
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// 优雅关闭
|
|
706
|
+
process.on('SIGINT', async () => {
|
|
707
|
+
logger.log('[Shutdown] Shutting down...');
|
|
708
|
+
removePid();
|
|
709
|
+
try {
|
|
710
|
+
const proxyManager = require('./lib/proxy-manager');
|
|
711
|
+
const statsStore = require('./lib/stats-store');
|
|
712
|
+
statsStore.flush();
|
|
713
|
+
await proxyManager.stopAll();
|
|
714
|
+
} catch (err) {
|
|
715
|
+
logger.error('[Shutdown] stopAll error:', err.message);
|
|
716
|
+
}
|
|
717
|
+
process.exit(0);
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
process.on('SIGTERM', async () => {
|
|
721
|
+
removePid();
|
|
722
|
+
try {
|
|
723
|
+
const proxyManager = require('./lib/proxy-manager');
|
|
724
|
+
const statsStore = require('./lib/stats-store');
|
|
725
|
+
statsStore.flush();
|
|
726
|
+
await proxyManager.stopAll();
|
|
727
|
+
} catch (err) {
|
|
728
|
+
logger.error('[Shutdown] stopAll error:', err.message);
|
|
729
|
+
}
|
|
730
|
+
process.exit(0);
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
// ==================== CLI Dispatch ====================
|
|
734
|
+
|
|
735
|
+
const cmd = process.argv[2];
|
|
736
|
+
|
|
737
|
+
switch (cmd) {
|
|
738
|
+
case 'help':
|
|
739
|
+
showHelp();
|
|
740
|
+
break;
|
|
741
|
+
case '-v':
|
|
742
|
+
case '--version':
|
|
743
|
+
showVersion();
|
|
744
|
+
break;
|
|
745
|
+
case 'update':
|
|
746
|
+
updateService();
|
|
747
|
+
break;
|
|
748
|
+
case 'stop':
|
|
749
|
+
stopService();
|
|
750
|
+
break;
|
|
751
|
+
case 'status':
|
|
752
|
+
showStatus();
|
|
753
|
+
break;
|
|
754
|
+
case 'start':
|
|
755
|
+
startDaemon();
|
|
756
|
+
break;
|
|
757
|
+
case '--daemon':
|
|
758
|
+
init();
|
|
759
|
+
break;
|
|
760
|
+
case undefined:
|
|
761
|
+
init();
|
|
762
|
+
break;
|
|
763
|
+
default:
|
|
764
|
+
console.error(`未知命令: ${cmd}`);
|
|
765
|
+
showHelp();
|
|
766
|
+
process.exit(1);
|
|
767
|
+
}
|