protocol-proxy 2.1.6 → 2.3.2
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/converters/anthropic-to-gemini.js +253 -0
- package/lib/converters/gemini-to-anthropic.js +275 -0
- package/lib/converters/gemini-to-openai.js +238 -0
- package/lib/converters/openai-to-gemini.js +284 -0
- package/lib/detector.js +4 -0
- package/lib/proxy-server.js +140 -10
- package/lib/stats-store.js +285 -0
- package/package.json +2 -3
- package/public/app.js +273 -2
- package/public/index.html +110 -1
- package/public/style.css +324 -0
- package/server.js +154 -1
package/public/style.css
CHANGED
|
@@ -843,6 +843,330 @@ form {
|
|
|
843
843
|
min-width: 90px;
|
|
844
844
|
}
|
|
845
845
|
|
|
846
|
+
/* Card header actions */
|
|
847
|
+
.card-header-actions {
|
|
848
|
+
display: flex;
|
|
849
|
+
gap: 8px;
|
|
850
|
+
align-items: center;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
/* Import modal */
|
|
854
|
+
.import-preview {
|
|
855
|
+
padding: 24px 28px;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
.import-stats {
|
|
859
|
+
display: flex;
|
|
860
|
+
gap: 16px;
|
|
861
|
+
margin-bottom: 24px;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
.import-stat {
|
|
865
|
+
flex: 1;
|
|
866
|
+
background: rgba(6, 8, 15, 0.4);
|
|
867
|
+
border: 1px solid rgba(51, 65, 85, 0.3);
|
|
868
|
+
border-radius: 12px;
|
|
869
|
+
padding: 16px;
|
|
870
|
+
text-align: center;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
.import-stat-value {
|
|
874
|
+
display: block;
|
|
875
|
+
font-size: 1.8rem;
|
|
876
|
+
font-weight: 700;
|
|
877
|
+
color: #60a5fa;
|
|
878
|
+
font-family: 'Cascadia Code', 'Fira Code', 'Consolas', monospace;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
.import-stat-label {
|
|
882
|
+
color: #64748b;
|
|
883
|
+
font-size: 0.8rem;
|
|
884
|
+
text-transform: uppercase;
|
|
885
|
+
letter-spacing: 0.06em;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
.import-mode label {
|
|
889
|
+
display: block;
|
|
890
|
+
margin-bottom: 10px;
|
|
891
|
+
color: #94a3b8;
|
|
892
|
+
font-size: 0.85rem;
|
|
893
|
+
font-weight: 500;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
.import-mode-options {
|
|
897
|
+
display: flex;
|
|
898
|
+
flex-direction: column;
|
|
899
|
+
gap: 10px;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
.import-mode-option {
|
|
903
|
+
display: flex;
|
|
904
|
+
align-items: flex-start;
|
|
905
|
+
gap: 12px;
|
|
906
|
+
padding: 14px 16px;
|
|
907
|
+
background: rgba(6, 8, 15, 0.4);
|
|
908
|
+
border: 1px solid rgba(51, 65, 85, 0.3);
|
|
909
|
+
border-radius: 10px;
|
|
910
|
+
cursor: pointer;
|
|
911
|
+
transition: all 0.2s;
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
.import-mode-option:hover {
|
|
915
|
+
border-color: rgba(59, 130, 246, 0.3);
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
.import-mode-option input[type="radio"] {
|
|
919
|
+
margin-top: 3px;
|
|
920
|
+
accent-color: #3b82f6;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
.import-mode-option strong {
|
|
924
|
+
display: block;
|
|
925
|
+
color: #e2e8f0;
|
|
926
|
+
font-size: 0.9rem;
|
|
927
|
+
margin-bottom: 2px;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
.import-mode-option small {
|
|
931
|
+
color: #64748b;
|
|
932
|
+
font-size: 0.8rem;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
/* Stats Panel */
|
|
936
|
+
.stats-panel .card-header {
|
|
937
|
+
flex-wrap: wrap;
|
|
938
|
+
gap: 12px;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
.stats-estimated-badge {
|
|
942
|
+
display: inline-block;
|
|
943
|
+
padding: 2px 10px;
|
|
944
|
+
border-radius: 12px;
|
|
945
|
+
font-size: 0.7rem;
|
|
946
|
+
font-weight: 500;
|
|
947
|
+
background: rgba(251, 191, 36, 0.15);
|
|
948
|
+
color: #fbbf24;
|
|
949
|
+
border: 1px solid rgba(251, 191, 36, 0.2);
|
|
950
|
+
vertical-align: middle;
|
|
951
|
+
letter-spacing: 0.02em;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
.stats-controls {
|
|
955
|
+
display: flex;
|
|
956
|
+
gap: 12px;
|
|
957
|
+
flex-wrap: wrap;
|
|
958
|
+
justify-content: flex-end; align-items: center;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
.stats-controls .model-dropdown {
|
|
962
|
+
width: 180px;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
.stats-filter-trigger {
|
|
966
|
+
padding: 7px 14px !important;
|
|
967
|
+
font-size: 0.85rem !important;
|
|
968
|
+
background: rgba(51, 65, 85, 0.3) !important;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
.stats-range-btns {
|
|
972
|
+
display: flex;
|
|
973
|
+
gap: 4px;
|
|
974
|
+
align-items: center;
|
|
975
|
+
background: rgba(6, 8, 15, 0.4);
|
|
976
|
+
border-radius: 10px;
|
|
977
|
+
padding: 3px;
|
|
978
|
+
border: 1px solid rgba(51, 65, 85, 0.3);
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
.stats-range-btn {
|
|
982
|
+
border: none !important;
|
|
983
|
+
background: transparent !important;
|
|
984
|
+
box-shadow: none !important;
|
|
985
|
+
padding: 6px 16px !important;
|
|
986
|
+
border-radius: 8px !important;
|
|
987
|
+
color: #64748b !important;
|
|
988
|
+
font-size: 0.82rem !important;
|
|
989
|
+
transition: all 0.2s !important;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
.stats-range-btn:hover {
|
|
993
|
+
color: #94a3b8 !important;
|
|
994
|
+
background: rgba(51, 65, 85, 0.3) !important;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
.stats-range-btn.active {
|
|
998
|
+
background: rgba(59, 130, 246, 0.2) !important;
|
|
999
|
+
color: #60a5fa !important;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
.stats-date-range {
|
|
1003
|
+
display: flex;
|
|
1004
|
+
align-items: center;
|
|
1005
|
+
gap: 4px;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
.stats-date-input {
|
|
1009
|
+
background: rgba(15, 23, 42, 0.6);
|
|
1010
|
+
border: 1px solid rgba(51, 65, 85, 0.5);
|
|
1011
|
+
border-radius: 6px;
|
|
1012
|
+
color: #e2e8f0;
|
|
1013
|
+
padding: 6px 8px;
|
|
1014
|
+
font-size: 0.78rem;
|
|
1015
|
+
outline: none;
|
|
1016
|
+
transition: border-color 0.2s;
|
|
1017
|
+
width: 125px;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
.stats-date-input:focus {
|
|
1021
|
+
border-color: #3b82f6;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
.stats-date-input::-webkit-calendar-picker-indicator {
|
|
1025
|
+
filter: invert(0.7);
|
|
1026
|
+
cursor: pointer;
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
.stats-date-sep {
|
|
1030
|
+
color: #475569;
|
|
1031
|
+
font-size: 0.85rem;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
.stats-summary {
|
|
1035
|
+
display: grid;
|
|
1036
|
+
grid-template-columns: repeat(4, 1fr);
|
|
1037
|
+
gap: 16px;
|
|
1038
|
+
padding: 0 28px;
|
|
1039
|
+
margin-top: 8px;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
.stats-summary-item {
|
|
1043
|
+
background: rgba(6, 8, 15, 0.4);
|
|
1044
|
+
border: 1px solid rgba(51, 65, 85, 0.3);
|
|
1045
|
+
border-radius: 12px;
|
|
1046
|
+
padding: 18px 16px;
|
|
1047
|
+
text-align: center;
|
|
1048
|
+
transition: all 0.25s;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
.stats-summary-item:hover {
|
|
1052
|
+
border-color: rgba(59, 130, 246, 0.25);
|
|
1053
|
+
box-shadow: 0 0 16px rgba(59, 130, 246, 0.06);
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
.stats-summary-value {
|
|
1057
|
+
display: block;
|
|
1058
|
+
font-size: 1.5rem;
|
|
1059
|
+
font-weight: 700;
|
|
1060
|
+
color: #60a5fa;
|
|
1061
|
+
font-family: 'Cascadia Code', 'Fira Code', 'Consolas', monospace;
|
|
1062
|
+
margin-bottom: 4px;
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
.stats-summary-label {
|
|
1066
|
+
color: #64748b;
|
|
1067
|
+
font-size: 0.78rem;
|
|
1068
|
+
text-transform: uppercase;
|
|
1069
|
+
letter-spacing: 0.06em;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
.stats-breakdown {
|
|
1073
|
+
padding: 20px 28px 28px;
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
.stats-table {
|
|
1077
|
+
width: 100%;
|
|
1078
|
+
border-collapse: separate;
|
|
1079
|
+
border-spacing: 0;
|
|
1080
|
+
font-size: 0.85rem;
|
|
1081
|
+
border-radius: 10px;
|
|
1082
|
+
overflow: hidden;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
.stats-table th,
|
|
1086
|
+
.stats-table td {
|
|
1087
|
+
text-align: left;
|
|
1088
|
+
padding: 10px 14px;
|
|
1089
|
+
border-bottom: 1px solid rgba(30, 41, 59, 0.6);
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
.stats-table th {
|
|
1093
|
+
color: #475569;
|
|
1094
|
+
font-weight: 600;
|
|
1095
|
+
text-transform: uppercase;
|
|
1096
|
+
font-size: 0.7rem;
|
|
1097
|
+
letter-spacing: 0.08em;
|
|
1098
|
+
background: rgba(15, 23, 42, 0.4);
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
.stats-table td {
|
|
1102
|
+
color: #94a3b8;
|
|
1103
|
+
word-break: break-all;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
.stats-table tbody tr {
|
|
1107
|
+
transition: background 0.15s;
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
.stats-table tbody tr:hover {
|
|
1111
|
+
background: rgba(59, 130, 246, 0.04);
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
.stats-table tbody tr:last-child td {
|
|
1115
|
+
border-bottom: none;
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
.stats-table td.num {
|
|
1119
|
+
text-align: right;
|
|
1120
|
+
font-family: 'Cascadia Code', 'Fira Code', 'Consolas', monospace;
|
|
1121
|
+
font-size: 0.82rem;
|
|
1122
|
+
color: #7dd3fc;
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
.stats-table tfoot td {
|
|
1126
|
+
font-weight: 600;
|
|
1127
|
+
color: #e2e8f0;
|
|
1128
|
+
background: rgba(15, 23, 42, 0.3);
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
.stats-table .provider-cell {
|
|
1132
|
+
color: #a78bfa;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
.stats-table .model-cell {
|
|
1136
|
+
color: #94a3b8;
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
.num-estimated {
|
|
1140
|
+
color: #fbbf24;
|
|
1141
|
+
font-size: 0.75rem;
|
|
1142
|
+
margin-right: 2px;
|
|
1143
|
+
cursor: help;
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
@media (max-width: 640px) {
|
|
1147
|
+
.stats-summary {
|
|
1148
|
+
grid-template-columns: repeat(2, 1fr);
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
.stats-controls {
|
|
1152
|
+
flex-direction: column;
|
|
1153
|
+
align-items: stretch;
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
.stats-controls .model-dropdown {
|
|
1157
|
+
width: 100%;
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
.stats-date-range {
|
|
1161
|
+
width: 100%;
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
.stats-date-input {
|
|
1165
|
+
flex: 1;
|
|
1166
|
+
min-width: 0;
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
|
|
846
1170
|
/* Responsive */
|
|
847
1171
|
@media (max-width: 640px) {
|
|
848
1172
|
.form-row {
|
package/server.js
CHANGED
|
@@ -121,6 +121,7 @@ async function init() {
|
|
|
121
121
|
const cors = require('cors');
|
|
122
122
|
const configStore = require('./lib/config-store');
|
|
123
123
|
const proxyManager = require('./lib/proxy-manager');
|
|
124
|
+
const statsStore = require('./lib/stats-store');
|
|
124
125
|
|
|
125
126
|
const app = express();
|
|
126
127
|
const PORT = process.env.ADMIN_PORT || 3000;
|
|
@@ -167,6 +168,8 @@ async function init() {
|
|
|
167
168
|
apiKey: provider.apiKey,
|
|
168
169
|
defaultModel: proxy.defaultModel,
|
|
169
170
|
models: provider.models,
|
|
171
|
+
azureDeployment: provider.azureDeployment || '',
|
|
172
|
+
azureApiVersion: provider.azureApiVersion || '',
|
|
170
173
|
};
|
|
171
174
|
}
|
|
172
175
|
|
|
@@ -194,7 +197,7 @@ async function init() {
|
|
|
194
197
|
});
|
|
195
198
|
|
|
196
199
|
app.post('/api/providers', (req, res) => {
|
|
197
|
-
const { name, url, protocol, apiKey, models } = req.body;
|
|
200
|
+
const { name, url, protocol, apiKey, models, azureDeployment, azureApiVersion } = req.body;
|
|
198
201
|
if (!name || !url) {
|
|
199
202
|
return res.status(400).json({ error: 'name and url are required' });
|
|
200
203
|
}
|
|
@@ -203,6 +206,8 @@ async function init() {
|
|
|
203
206
|
protocol: protocol || (/anthropic/i.test(url) ? 'anthropic' : 'openai'),
|
|
204
207
|
apiKey: apiKey || '',
|
|
205
208
|
models: models || [],
|
|
209
|
+
azureDeployment: azureDeployment || '',
|
|
210
|
+
azureApiVersion: azureApiVersion || '',
|
|
206
211
|
});
|
|
207
212
|
res.status(201).json(provider);
|
|
208
213
|
});
|
|
@@ -217,6 +222,8 @@ async function init() {
|
|
|
217
222
|
if (req.body.protocol !== undefined) updates.protocol = req.body.protocol;
|
|
218
223
|
if (req.body.apiKey !== undefined && req.body.apiKey !== '') updates.apiKey = req.body.apiKey;
|
|
219
224
|
if (req.body.models !== undefined) updates.models = req.body.models;
|
|
225
|
+
if (req.body.azureDeployment !== undefined) updates.azureDeployment = req.body.azureDeployment;
|
|
226
|
+
if (req.body.azureApiVersion !== undefined) updates.azureApiVersion = req.body.azureApiVersion;
|
|
220
227
|
|
|
221
228
|
const updated = configStore.updateProvider(req.params.id, updates);
|
|
222
229
|
|
|
@@ -415,6 +422,148 @@ async function init() {
|
|
|
415
422
|
});
|
|
416
423
|
});
|
|
417
424
|
|
|
425
|
+
// Token 用量统计
|
|
426
|
+
app.get('/api/stats', (req, res) => {
|
|
427
|
+
const { range, startDate, endDate, proxyId } = req.query;
|
|
428
|
+
const stats = statsStore.getStats({
|
|
429
|
+
range: range || 'daily',
|
|
430
|
+
startDate: startDate || undefined,
|
|
431
|
+
endDate: endDate || undefined,
|
|
432
|
+
proxyId: proxyId || undefined,
|
|
433
|
+
});
|
|
434
|
+
const proxies = configStore.getProxies().map(p => ({
|
|
435
|
+
id: p.id,
|
|
436
|
+
name: p.name,
|
|
437
|
+
providerName: configStore.getProviderById(p.providerId)?.name || '',
|
|
438
|
+
}));
|
|
439
|
+
res.json({ ...stats, proxies });
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
// ==================== 配置导入/导出 ====================
|
|
443
|
+
|
|
444
|
+
app.get('/api/config/export', (req, res) => {
|
|
445
|
+
const providers = configStore.getProviders();
|
|
446
|
+
const proxies = configStore.getProxies().map(p => {
|
|
447
|
+
const provider = configStore.getProviderById(p.providerId);
|
|
448
|
+
return {
|
|
449
|
+
id: p.id,
|
|
450
|
+
name: p.name,
|
|
451
|
+
port: p.port,
|
|
452
|
+
requireAuth: p.requireAuth,
|
|
453
|
+
authToken: p.authToken,
|
|
454
|
+
providerId: p.providerId,
|
|
455
|
+
defaultModel: p.defaultModel || '',
|
|
456
|
+
providerName: provider?.name || '',
|
|
457
|
+
};
|
|
458
|
+
});
|
|
459
|
+
res.json({ providers, proxies, exportedAt: new Date().toISOString() });
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
app.post('/api/config/import', async (req, res) => {
|
|
463
|
+
const { config, mode } = req.body;
|
|
464
|
+
|
|
465
|
+
if (!config || !mode || !['overwrite', 'merge'].includes(mode)) {
|
|
466
|
+
return res.status(400).json({ error: '需要 config 和 mode(overwrite/merge)' });
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// 校验结构
|
|
470
|
+
if (!Array.isArray(config.providers) || !Array.isArray(config.proxies)) {
|
|
471
|
+
return res.status(400).json({ error: '配置格式错误:需要 providers 和 proxies 数组' });
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
for (const p of config.providers) {
|
|
475
|
+
if (!p.name || !p.url || !p.protocol) {
|
|
476
|
+
return res.status(400).json({ error: `供应商 "${p.name || '?'}" 缺少必要字段(name/url/protocol)` });
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
for (const p of config.proxies) {
|
|
481
|
+
if (!p.name || !p.port || !p.providerId) {
|
|
482
|
+
return res.status(400).json({ error: `代理 "${p.name || '?'}" 缺少必要字段(name/port/providerId)` });
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
if (mode === 'overwrite') {
|
|
487
|
+
// 覆盖模式:直接替换整个配置
|
|
488
|
+
const newConfig = {
|
|
489
|
+
providers: config.providers.map(p => ({
|
|
490
|
+
id: p.id,
|
|
491
|
+
name: p.name,
|
|
492
|
+
url: p.url,
|
|
493
|
+
protocol: p.protocol,
|
|
494
|
+
apiKey: p.apiKey || '',
|
|
495
|
+
models: Array.isArray(p.models) ? p.models : [],
|
|
496
|
+
})),
|
|
497
|
+
proxies: config.proxies.map(p => ({
|
|
498
|
+
id: p.id,
|
|
499
|
+
name: p.name,
|
|
500
|
+
port: p.port,
|
|
501
|
+
requireAuth: !!p.requireAuth,
|
|
502
|
+
authToken: p.authToken || null,
|
|
503
|
+
providerId: p.providerId,
|
|
504
|
+
defaultModel: p.defaultModel || '',
|
|
505
|
+
})),
|
|
506
|
+
};
|
|
507
|
+
configStore.saveConfig(newConfig);
|
|
508
|
+
return res.json({ success: true, mode, providers: newConfig.providers.length, proxies: newConfig.proxies.length });
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// 合并模式:按 ID 去重
|
|
512
|
+
const existingProviders = configStore.getProviders();
|
|
513
|
+
const existingProxies = configStore.getProxies();
|
|
514
|
+
|
|
515
|
+
const providerMap = new Map(existingProviders.map(p => [p.id, p]));
|
|
516
|
+
for (const p of config.providers) {
|
|
517
|
+
providerMap.set(p.id, {
|
|
518
|
+
id: p.id,
|
|
519
|
+
name: p.name,
|
|
520
|
+
url: p.url,
|
|
521
|
+
protocol: p.protocol,
|
|
522
|
+
apiKey: p.apiKey || '',
|
|
523
|
+
models: Array.isArray(p.models) ? p.models : [],
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const proxyMap = new Map(existingProxies.map(p => [p.id, p]));
|
|
528
|
+
for (const p of config.proxies) {
|
|
529
|
+
// 检查端口冲突:导入的代理端口不能和现有代理或其他导入代理重复
|
|
530
|
+
const conflict = proxyMap.get(p.id)
|
|
531
|
+
? null // 同 ID 是覆盖,不算冲突
|
|
532
|
+
: Array.from(proxyMap.values()).find(ep => ep.port === p.port);
|
|
533
|
+
if (conflict) {
|
|
534
|
+
return res.status(409).json({
|
|
535
|
+
error: `端口 ${p.port} 已被代理「${conflict.name}」占用,无法导入代理「${p.name}」`,
|
|
536
|
+
});
|
|
537
|
+
}
|
|
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
|
+
}
|
|
548
|
+
|
|
549
|
+
const merged = {
|
|
550
|
+
providers: Array.from(providerMap.values()),
|
|
551
|
+
proxies: Array.from(proxyMap.values()),
|
|
552
|
+
};
|
|
553
|
+
configStore.saveConfig(merged);
|
|
554
|
+
|
|
555
|
+
res.json({
|
|
556
|
+
success: true,
|
|
557
|
+
mode,
|
|
558
|
+
providers: merged.providers.length,
|
|
559
|
+
proxies: merged.proxies.length,
|
|
560
|
+
added: {
|
|
561
|
+
providers: merged.providers.length - existingProviders.length,
|
|
562
|
+
proxies: merged.proxies.length - existingProxies.length,
|
|
563
|
+
},
|
|
564
|
+
});
|
|
565
|
+
});
|
|
566
|
+
|
|
418
567
|
// 前端首页
|
|
419
568
|
app.get('/', (req, res) => {
|
|
420
569
|
res.sendFile(path.join(__dirname, 'public', 'index.html'));
|
|
@@ -447,6 +596,8 @@ process.on('SIGINT', async () => {
|
|
|
447
596
|
removePid();
|
|
448
597
|
try {
|
|
449
598
|
const proxyManager = require('./lib/proxy-manager');
|
|
599
|
+
const statsStore = require('./lib/stats-store');
|
|
600
|
+
statsStore.flush();
|
|
450
601
|
await proxyManager.stopAll();
|
|
451
602
|
} catch (err) {
|
|
452
603
|
console.error('[Shutdown] stopAll error:', err.message);
|
|
@@ -458,6 +609,8 @@ process.on('SIGTERM', async () => {
|
|
|
458
609
|
removePid();
|
|
459
610
|
try {
|
|
460
611
|
const proxyManager = require('./lib/proxy-manager');
|
|
612
|
+
const statsStore = require('./lib/stats-store');
|
|
613
|
+
statsStore.flush();
|
|
461
614
|
await proxyManager.stopAll();
|
|
462
615
|
} catch (err) {
|
|
463
616
|
console.error('[Shutdown] stopAll error:', err.message);
|