protocol-proxy 2.3.4 → 2.4.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/public/style.css CHANGED
@@ -249,7 +249,7 @@ header p {
249
249
  .proxy-header {
250
250
  display: flex;
251
251
  justify-content: space-between;
252
- align-items: flex-start;
252
+ align-items: center;
253
253
  margin-bottom: 14px;
254
254
  }
255
255
 
@@ -314,13 +314,16 @@ header p {
314
314
  }
315
315
 
316
316
  .target-table th:nth-child(1),
317
- .target-table td:nth-child(1) { width: 55%; }
317
+ .target-table td:nth-child(1) { width: 25%; }
318
318
 
319
319
  .target-table th:nth-child(2),
320
- .target-table td:nth-child(2) { width: 15%; }
320
+ .target-table td:nth-child(2) { width: 25%; }
321
321
 
322
322
  .target-table th:nth-child(3),
323
- .target-table td:nth-child(3) { width: 30%; }
323
+ .target-table td:nth-child(3) { width: 25%; }
324
+
325
+ .target-table th:nth-child(4),
326
+ .target-table td:nth-child(4) { width: 25%; text-align: center; }
324
327
 
325
328
  .target-table th,
326
329
  .target-table td {
@@ -1187,3 +1190,155 @@ form {
1187
1190
  font-size: 2rem;
1188
1191
  }
1189
1192
  }
1193
+
1194
+ .proxy-routing-badge {
1195
+ font-size: 0.75rem;
1196
+ padding: 3px 10px;
1197
+ border-radius: 6px;
1198
+ background: rgba(51, 65, 85, 0.5);
1199
+ color: #94a3b8;
1200
+ font-weight: 500;
1201
+ white-space: nowrap;
1202
+ }
1203
+
1204
+ .provider-tag {
1205
+ font-size: 0.7rem;
1206
+ padding: 1px 6px;
1207
+ border-radius: 4px;
1208
+ background: rgba(148, 163, 184, 0.15);
1209
+ color: #94a3b8;
1210
+ margin-left: 6px;
1211
+ vertical-align: middle;
1212
+ }
1213
+
1214
+ .provider-pool-group {
1215
+ grid-column: 1 / -1;
1216
+ }
1217
+
1218
+ .provider-pool-picker {
1219
+ margin-bottom: 12px;
1220
+ }
1221
+
1222
+ .provider-pool-list {
1223
+ display: flex;
1224
+ flex-direction: column;
1225
+ gap: 10px;
1226
+ }
1227
+
1228
+ .provider-pool-empty {
1229
+ padding: 12px 14px;
1230
+ border: 1px dashed rgba(71, 85, 105, 0.6);
1231
+ border-radius: 10px;
1232
+ color: #64748b;
1233
+ font-size: 0.9rem;
1234
+ background: rgba(6, 8, 15, 0.35);
1235
+ }
1236
+
1237
+ .provider-pool-item {
1238
+ display: grid;
1239
+ grid-template-columns: minmax(0, 1fr) 140px 120px auto;
1240
+ gap: 12px;
1241
+ align-items: center;
1242
+ padding: 12px 14px;
1243
+ border: 1px solid rgba(51, 65, 85, 0.5);
1244
+ border-radius: 12px;
1245
+ background: rgba(6, 8, 15, 0.45);
1246
+ }
1247
+
1248
+ .provider-pool-main {
1249
+ min-width: 0;
1250
+ }
1251
+
1252
+ .provider-pool-name {
1253
+ color: #e2e8f0;
1254
+ font-weight: 600;
1255
+ overflow: hidden;
1256
+ text-overflow: ellipsis;
1257
+ white-space: nowrap;
1258
+ }
1259
+
1260
+ .provider-pool-meta {
1261
+ color: #64748b;
1262
+ font-size: 0.82rem;
1263
+ overflow: hidden;
1264
+ text-overflow: ellipsis;
1265
+ white-space: nowrap;
1266
+ }
1267
+
1268
+ .provider-pool-model label {
1269
+ display: block;
1270
+ margin-bottom: 4px;
1271
+ font-size: 0.75rem;
1272
+ color: #94a3b8;
1273
+ }
1274
+
1275
+ .provider-pool-model-value {
1276
+ color: #e2e8f0;
1277
+ font-size: 0.85rem;
1278
+ overflow: hidden;
1279
+ text-overflow: ellipsis;
1280
+ white-space: nowrap;
1281
+ display: block;
1282
+ }
1283
+
1284
+ .pool-provider-group {
1285
+ position: relative;
1286
+ }
1287
+
1288
+ .pool-provider-trigger {
1289
+ display: flex;
1290
+ align-items: center;
1291
+ gap: 4px;
1292
+ }
1293
+
1294
+ .pool-provider-arrow {
1295
+ margin-left: auto;
1296
+ font-size: 12px;
1297
+ color: #64748b;
1298
+ transition: transform 0.15s;
1299
+ }
1300
+
1301
+ .pool-provider-group.open > .pool-provider-trigger .pool-provider-arrow {
1302
+ transform: rotate(90deg);
1303
+ }
1304
+
1305
+ .pool-model-sublist {
1306
+ display: none;
1307
+ padding-left: 16px;
1308
+ border-left: 2px solid rgba(71, 85, 105, 0.3);
1309
+ margin-left: 8px;
1310
+ }
1311
+
1312
+ .pool-provider-group.open > .pool-model-sublist {
1313
+ display: block;
1314
+ }
1315
+
1316
+ .pool-model-sublist .model-option {
1317
+ font-size: 13px;
1318
+ padding: 6px 12px;
1319
+ }
1320
+
1321
+ .provider-pool-weight label {
1322
+ display: block;
1323
+ margin-bottom: 4px;
1324
+ font-size: 0.75rem;
1325
+ color: #94a3b8;
1326
+ }
1327
+
1328
+ .provider-pool-weight input {
1329
+ width: 100%;
1330
+ }
1331
+
1332
+ .provider-pool-remove {
1333
+ padding: 8px 12px;
1334
+ border-radius: 10px;
1335
+ border: 1px solid rgba(127, 29, 29, 0.6);
1336
+ background: rgba(127, 29, 29, 0.2);
1337
+ color: #fca5a5;
1338
+ cursor: pointer;
1339
+ }
1340
+
1341
+ .provider-pool-remove:hover {
1342
+ background: rgba(127, 29, 29, 0.35);
1343
+ border-color: rgba(248, 113, 113, 0.8);
1344
+ }
package/server.js CHANGED
@@ -3,6 +3,7 @@ const path = require('path');
3
3
  const { exec, spawn } = require('child_process');
4
4
  const os = require('os');
5
5
  const fs = require('fs');
6
+ const logger = require('./lib/logger');
6
7
 
7
8
  // ==================== CLI ====================
8
9
 
@@ -137,7 +138,7 @@ async function init() {
137
138
  command = `xdg-open "${url}"`;
138
139
  }
139
140
  exec(command, (err) => {
140
- if (err) console.error('[Browser] 打开浏览器失败:', err.message);
141
+ if (err) logger.error('[Browser] 打开浏览器失败:', err.message);
141
142
  });
142
143
  }
143
144
 
@@ -149,7 +150,7 @@ async function init() {
149
150
  const start = Date.now();
150
151
  res.on('finish', () => {
151
152
  const duration = Date.now() - start;
152
- console.log(`[HTTP] ${req.method} ${req.path} - ${res.statusCode} (${duration}ms)`);
153
+ logger.log(`[HTTP] ${req.method} ${req.path} - ${res.statusCode} (${duration}ms)`);
153
154
  });
154
155
  next();
155
156
  });
@@ -159,20 +160,90 @@ async function init() {
159
160
  // ==================== 辅助函数 ====================
160
161
 
161
162
  function resolveTarget(proxy) {
162
- const provider = configStore.getProviderById(proxy.providerId);
163
- if (!provider) return null;
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
+ apiKey: primaryProvider.apiKey,
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
+ apiKey: provider.apiKey,
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
+ const protocol = pool[0].protocol;
210
+ if (pool.some(p => p.protocol !== protocol)) return null;
211
+
164
212
  return {
165
- providerUrl: provider.url,
166
- providerName: provider.name,
167
- protocol: provider.protocol,
168
- apiKey: provider.apiKey,
213
+ protocol,
214
+ routingStrategy: proxy.routingStrategy || 'primary_fallback',
215
+ providerPool: pool,
169
216
  defaultModel: proxy.defaultModel,
170
- models: provider.models,
171
- azureDeployment: provider.azureDeployment || '',
172
- azureApiVersion: provider.azureApiVersion || '',
173
217
  };
174
218
  }
175
219
 
220
+ function normalizeProviderPoolInput(pool) {
221
+ if (!Array.isArray(pool)) return [];
222
+ const seen = new Set();
223
+ const result = [];
224
+ for (const item of pool) {
225
+ if (!item || typeof item !== 'object') continue;
226
+ const providerId = typeof item.providerId === 'string' ? item.providerId.trim() : '';
227
+ if (!providerId) continue;
228
+ const model = typeof item.model === 'string' ? item.model.trim() : '';
229
+ const key = `${providerId}\0${model}`;
230
+ if (seen.has(key)) continue;
231
+ seen.add(key);
232
+ result.push({
233
+ providerId,
234
+ model,
235
+ weight: Math.max(1, parseInt(item.weight, 10) || 1),
236
+ });
237
+ }
238
+ return result;
239
+ }
240
+
241
+ function normalizeRoutingStrategyInput(strategy) {
242
+ return ['primary_fallback', 'round_robin', 'weighted', 'fastest'].includes(strategy)
243
+ ? strategy
244
+ : 'primary_fallback';
245
+ }
246
+
176
247
  async function startProxyWithProvider(proxy) {
177
248
  const target = resolveTarget(proxy);
178
249
  if (!target) throw new Error(`供应商 ${proxy.providerId} 不存在`);
@@ -269,6 +340,9 @@ async function init() {
269
340
  providerUrl: provider?.url || '',
270
341
  protocol: provider?.protocol || '',
271
342
  defaultModel: p.defaultModel || '',
343
+ providerWeight: Math.max(1, parseInt(p.providerWeight, 10) || 1),
344
+ routingStrategy: p.routingStrategy || 'primary_fallback',
345
+ providerPool: Array.isArray(p.providerPool) ? p.providerPool : [],
272
346
  hasApiKey: !!provider?.apiKey,
273
347
  running: proxyManager.isRunning(p.id),
274
348
  };
@@ -286,13 +360,15 @@ async function init() {
286
360
  providerName: provider?.name || '',
287
361
  providerUrl: provider?.url || '',
288
362
  protocol: provider?.protocol || '',
363
+ routingStrategy: proxy.routingStrategy || 'primary_fallback',
364
+ providerPool: Array.isArray(proxy.providerPool) ? proxy.providerPool : [],
289
365
  hasApiKey: !!provider?.apiKey,
290
366
  });
291
367
  });
292
368
 
293
369
  // 创建代理
294
370
  app.post('/api/proxies', async (req, res) => {
295
- const { name, port, requireAuth, authToken, providerId, defaultModel } = req.body;
371
+ const { name, port, requireAuth, authToken, providerId, defaultModel, routingStrategy, providerPool, providerWeight } = req.body;
296
372
 
297
373
  if (!name || !port || !providerId) {
298
374
  return res.status(400).json({ error: 'name, port and providerId are required' });
@@ -317,6 +393,9 @@ async function init() {
317
393
  authToken: authToken || null,
318
394
  providerId,
319
395
  defaultModel: defaultModel || '',
396
+ providerWeight: Math.max(1, parseInt(providerWeight, 10) || 1),
397
+ routingStrategy: normalizeRoutingStrategyInput(routingStrategy),
398
+ providerPool: normalizeProviderPoolInput(providerPool),
320
399
  });
321
400
 
322
401
  try {
@@ -345,6 +424,9 @@ async function init() {
345
424
  updates.providerId = req.body.providerId;
346
425
  }
347
426
  if (req.body.defaultModel !== undefined) updates.defaultModel = req.body.defaultModel;
427
+ if (req.body.providerWeight !== undefined) updates.providerWeight = Math.max(1, parseInt(req.body.providerWeight, 10) || 1);
428
+ if (req.body.routingStrategy !== undefined) updates.routingStrategy = normalizeRoutingStrategyInput(req.body.routingStrategy);
429
+ if (req.body.providerPool !== undefined) updates.providerPool = normalizeProviderPoolInput(req.body.providerPool);
348
430
 
349
431
  const needRestart = updates.port !== undefined && updates.port !== existing.port;
350
432
  if (needRestart) {
@@ -453,6 +535,8 @@ async function init() {
453
535
  authToken: p.authToken,
454
536
  providerId: p.providerId,
455
537
  defaultModel: p.defaultModel || '',
538
+ routingStrategy: p.routingStrategy || 'primary_fallback',
539
+ providerPool: Array.isArray(p.providerPool) ? p.providerPool : [],
456
540
  providerName: provider?.name || '',
457
541
  };
458
542
  });
@@ -502,6 +586,8 @@ async function init() {
502
586
  authToken: p.authToken || null,
503
587
  providerId: p.providerId,
504
588
  defaultModel: p.defaultModel || '',
589
+ routingStrategy: normalizeRoutingStrategyInput(p.routingStrategy),
590
+ providerPool: normalizeProviderPoolInput(p.providerPool),
505
591
  })),
506
592
  };
507
593
  configStore.saveConfig(newConfig);
@@ -521,6 +607,8 @@ async function init() {
521
607
  protocol: p.protocol,
522
608
  apiKey: p.apiKey || '',
523
609
  models: Array.isArray(p.models) ? p.models : [],
610
+ routingStrategy: normalizeRoutingStrategyInput(p.routingStrategy),
611
+ providerPool: normalizeProviderPoolInput(p.providerPool),
524
612
  });
525
613
  }
526
614
 
@@ -535,16 +623,18 @@ async function init() {
535
623
  error: `端口 ${p.port} 已被代理「${conflict.name}」占用,无法导入代理「${p.name}」`,
536
624
  });
537
625
  }
538
- proxyMap.set(p.id, {
539
- id: p.id,
540
- name: p.name,
541
- port: p.port,
542
- requireAuth: !!p.requireAuth,
543
- authToken: p.authToken || null,
544
- providerId: p.providerId,
545
- defaultModel: p.defaultModel || '',
546
- });
547
- }
626
+ proxyMap.set(p.id, {
627
+ id: p.id,
628
+ name: p.name,
629
+ port: p.port,
630
+ requireAuth: !!p.requireAuth,
631
+ authToken: p.authToken || null,
632
+ providerId: p.providerId,
633
+ defaultModel: p.defaultModel || '',
634
+ routingStrategy: normalizeRoutingStrategyInput(p.routingStrategy),
635
+ providerPool: normalizeProviderPoolInput(p.providerPool),
636
+ });
637
+ }
548
638
 
549
639
  const merged = {
550
640
  providers: Array.from(providerMap.values()),
@@ -570,6 +660,7 @@ async function init() {
570
660
  });
571
661
 
572
662
  // 启动
663
+ logger.init();
573
664
  writePid();
574
665
 
575
666
  // 启动所有已配置的代理
@@ -578,21 +669,22 @@ async function init() {
578
669
  try {
579
670
  await startProxyWithProvider(proxy);
580
671
  } catch (err) {
581
- console.error(`[Init] Failed to start proxy ${proxy.name}:`, err.message);
672
+ logger.error(`[Init] Failed to start proxy ${proxy.name}:`, err.message);
582
673
  }
583
674
  }));
584
675
 
585
676
  app.listen(PORT, () => {
586
677
  const adminUrl = `http://localhost:${PORT}`;
587
- console.log(`[Admin] Management server running on ${adminUrl}`);
588
- console.log(`[Admin] ${proxies.length} proxy config(s) loaded`);
678
+ logger.log(`[Admin] Management server running on ${adminUrl}`);
679
+ logger.log(`[Admin] ${proxies.length} proxy config(s) loaded`);
680
+ logger.log(`[Admin] 日志文件: ${logger.LOG_FILE}`);
589
681
  openBrowser(adminUrl);
590
682
  });
591
683
  }
592
684
 
593
685
  // 优雅关闭
594
686
  process.on('SIGINT', async () => {
595
- console.log('\nShutting down...');
687
+ logger.log('[Shutdown] Shutting down...');
596
688
  removePid();
597
689
  try {
598
690
  const proxyManager = require('./lib/proxy-manager');
@@ -600,7 +692,7 @@ process.on('SIGINT', async () => {
600
692
  statsStore.flush();
601
693
  await proxyManager.stopAll();
602
694
  } catch (err) {
603
- console.error('[Shutdown] stopAll error:', err.message);
695
+ logger.error('[Shutdown] stopAll error:', err.message);
604
696
  }
605
697
  process.exit(0);
606
698
  });
@@ -613,7 +705,7 @@ process.on('SIGTERM', async () => {
613
705
  statsStore.flush();
614
706
  await proxyManager.stopAll();
615
707
  } catch (err) {
616
- console.error('[Shutdown] stopAll error:', err.message);
708
+ logger.error('[Shutdown] stopAll error:', err.message);
617
709
  }
618
710
  process.exit(0);
619
711
  });