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/lib/config-store.js +36 -1
- package/lib/logger.js +58 -0
- package/lib/proxy-manager.js +4 -0
- package/lib/proxy-server.js +312 -178
- package/lib/stats-store.js +3 -5
- package/package.json +51 -51
- package/public/app.js +272 -22
- package/public/index.html +316 -277
- package/public/style.css +159 -4
- package/server.js +120 -28
package/package.json
CHANGED
|
@@ -1,51 +1,51 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "protocol-proxy",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "OpenAI / Anthropic 协议转换透明代理",
|
|
5
|
-
"main": "server.js",
|
|
6
|
-
"bin": {
|
|
7
|
-
"protocol-proxy": "server.js"
|
|
8
|
-
},
|
|
9
|
-
"files": [
|
|
10
|
-
"server.js",
|
|
11
|
-
"lib/",
|
|
12
|
-
"public/",
|
|
13
|
-
"config/",
|
|
14
|
-
"README.md"
|
|
15
|
-
],
|
|
16
|
-
"keywords": [
|
|
17
|
-
"proxy",
|
|
18
|
-
"openai",
|
|
19
|
-
"anthropic",
|
|
20
|
-
"api",
|
|
21
|
-
"protocol-converter"
|
|
22
|
-
],
|
|
23
|
-
"scripts": {
|
|
24
|
-
"start": "node server.js",
|
|
25
|
-
"dev": "node --watch server.js",
|
|
26
|
-
"build": "pkg . --out-path=dist"
|
|
27
|
-
},
|
|
28
|
-
"dependencies": {
|
|
29
|
-
"cors": "^2.8.5",
|
|
30
|
-
"express": "^4.19.2"
|
|
31
|
-
},
|
|
32
|
-
"devDependencies": {
|
|
33
|
-
"pkg": "^5.8.1"
|
|
34
|
-
},
|
|
35
|
-
"pkg": {
|
|
36
|
-
"scripts": [
|
|
37
|
-
"lib/**/*.js"
|
|
38
|
-
],
|
|
39
|
-
"assets": [
|
|
40
|
-
"public/**/*"
|
|
41
|
-
],
|
|
42
|
-
"targets": [
|
|
43
|
-
"node20-win-x64"
|
|
44
|
-
],
|
|
45
|
-
"outputPath": "dist",
|
|
46
|
-
"options": []
|
|
47
|
-
},
|
|
48
|
-
"engines": {
|
|
49
|
-
"node": ">=20.0.0"
|
|
50
|
-
}
|
|
51
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "protocol-proxy",
|
|
3
|
+
"version": "2.4.0",
|
|
4
|
+
"description": "OpenAI / Anthropic 协议转换透明代理",
|
|
5
|
+
"main": "server.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"protocol-proxy": "server.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"server.js",
|
|
11
|
+
"lib/",
|
|
12
|
+
"public/",
|
|
13
|
+
"config/",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
16
|
+
"keywords": [
|
|
17
|
+
"proxy",
|
|
18
|
+
"openai",
|
|
19
|
+
"anthropic",
|
|
20
|
+
"api",
|
|
21
|
+
"protocol-converter"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"start": "node server.js",
|
|
25
|
+
"dev": "node --watch server.js",
|
|
26
|
+
"build": "pkg . --out-path=dist"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"cors": "^2.8.5",
|
|
30
|
+
"express": "^4.19.2"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"pkg": "^5.8.1"
|
|
34
|
+
},
|
|
35
|
+
"pkg": {
|
|
36
|
+
"scripts": [
|
|
37
|
+
"lib/**/*.js"
|
|
38
|
+
],
|
|
39
|
+
"assets": [
|
|
40
|
+
"public/**/*"
|
|
41
|
+
],
|
|
42
|
+
"targets": [
|
|
43
|
+
"node20-win-x64"
|
|
44
|
+
],
|
|
45
|
+
"outputPath": "dist",
|
|
46
|
+
"options": []
|
|
47
|
+
},
|
|
48
|
+
"engines": {
|
|
49
|
+
"node": ">=20.0.0"
|
|
50
|
+
}
|
|
51
|
+
}
|
package/public/app.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
|
-
let proxies = [];
|
|
1
|
+
let proxies = [];
|
|
2
2
|
let providers = [];
|
|
3
3
|
let editingId = null;
|
|
4
|
+
let editingProviderId = null;
|
|
5
|
+
let importData = null;
|
|
6
|
+
let statsRange = 'daily';
|
|
7
|
+
let statsProxyId = '';
|
|
8
|
+
let providerPoolItems = [];
|
|
4
9
|
|
|
5
10
|
// ==================== 数据加载 ====================
|
|
6
11
|
|
|
@@ -33,6 +38,193 @@ function updateStats() {
|
|
|
33
38
|
proxies.filter(p => p.running).length;
|
|
34
39
|
}
|
|
35
40
|
|
|
41
|
+
function parseProviderPool(value) {
|
|
42
|
+
const text = (value || '').trim();
|
|
43
|
+
if (!text) return [];
|
|
44
|
+
const seen = new Set();
|
|
45
|
+
const items = [];
|
|
46
|
+
for (const part of text.split(/[\n,]/)) {
|
|
47
|
+
const token = part.trim();
|
|
48
|
+
if (!token) continue;
|
|
49
|
+
const [providerIdRaw, modelRaw, weightRaw] = token.split(':');
|
|
50
|
+
const providerId = (providerIdRaw || '').trim();
|
|
51
|
+
if (!providerId) continue;
|
|
52
|
+
const model = (modelRaw || '').trim();
|
|
53
|
+
const key = `${providerId}\0${model}`;
|
|
54
|
+
if (seen.has(key)) continue;
|
|
55
|
+
seen.add(key);
|
|
56
|
+
items.push({ providerId, model, weight: Math.max(1, parseInt((weightRaw || '1').trim(), 10) || 1) });
|
|
57
|
+
}
|
|
58
|
+
return items;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function formatProviderPool(pool) {
|
|
62
|
+
if (!Array.isArray(pool) || pool.length === 0) return '';
|
|
63
|
+
return pool.map(item => {
|
|
64
|
+
const w = Math.max(1, parseInt(item.weight, 10) || 1);
|
|
65
|
+
return item.model ? `${item.providerId}:${item.model}:${w}` : `${item.providerId}::${w}`;
|
|
66
|
+
}).join(', ');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function syncSimpleDropdown(dropdownId, value, hiddenInputId) {
|
|
70
|
+
const dropdown = document.getElementById(dropdownId);
|
|
71
|
+
if (!dropdown) return value;
|
|
72
|
+
const hiddenInput = document.getElementById(hiddenInputId || dropdownId.replace('-dropdown', ''));
|
|
73
|
+
const valueEl = dropdown.querySelector('[id$="-dropdown-value"]');
|
|
74
|
+
const options = Array.from(dropdown.querySelectorAll('.model-option'));
|
|
75
|
+
const nextValue = options.some(opt => opt.dataset.value === value)
|
|
76
|
+
? value
|
|
77
|
+
: (options[0]?.dataset.value || '');
|
|
78
|
+
options.forEach(opt => opt.classList.toggle('selected', opt.dataset.value === nextValue));
|
|
79
|
+
if (hiddenInput) hiddenInput.value = nextValue;
|
|
80
|
+
const selected = options.find(opt => opt.dataset.value === nextValue);
|
|
81
|
+
if (valueEl && selected) {
|
|
82
|
+
valueEl.textContent = selected.querySelector('.model-option-name')?.textContent || valueEl.textContent;
|
|
83
|
+
}
|
|
84
|
+
return nextValue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function syncProviderPoolState(items) {
|
|
88
|
+
providerPoolItems = Array.isArray(items)
|
|
89
|
+
? items
|
|
90
|
+
.filter(item => item && item.providerId)
|
|
91
|
+
.map(item => ({
|
|
92
|
+
providerId: item.providerId,
|
|
93
|
+
model: typeof item.model === 'string' ? item.model : '',
|
|
94
|
+
weight: Math.max(1, parseInt(item.weight, 10) || 1),
|
|
95
|
+
}))
|
|
96
|
+
: [];
|
|
97
|
+
renderProviderPoolEditor();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function addProviderToPool(providerId, model) {
|
|
101
|
+
if (!providerId) return;
|
|
102
|
+
const m = model || '';
|
|
103
|
+
if (providerPoolItems.some(item => item.providerId === providerId && (item.model || '') === m)) return;
|
|
104
|
+
providerPoolItems = [...providerPoolItems, { providerId, model: m, weight: 1 }];
|
|
105
|
+
renderProviderPoolEditor();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function removeProviderFromPool(providerId, model) {
|
|
109
|
+
const m = model || '';
|
|
110
|
+
providerPoolItems = providerPoolItems.filter(item => !(item.providerId === providerId && (item.model || '') === m));
|
|
111
|
+
renderProviderPoolEditor();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function updateProviderPoolWeight(providerId, model, weight) {
|
|
115
|
+
const m = model || '';
|
|
116
|
+
providerPoolItems = providerPoolItems.map(item => (
|
|
117
|
+
item.providerId === providerId && (item.model || '') === m
|
|
118
|
+
? { ...item, weight: Math.max(1, parseInt(weight, 10) || 1) }
|
|
119
|
+
: item
|
|
120
|
+
));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function renderProviderPoolEditor() {
|
|
124
|
+
const container = document.getElementById('provider-pool-list');
|
|
125
|
+
const select = document.getElementById('provider-pool-dropdown-options');
|
|
126
|
+
const valueEl = document.getElementById('provider-pool-dropdown-value');
|
|
127
|
+
const dropdown = document.getElementById('provider-pool-dropdown');
|
|
128
|
+
if (!container || !select || !valueEl || !dropdown) return;
|
|
129
|
+
|
|
130
|
+
const primaryId = document.getElementById('provider-id').value;
|
|
131
|
+
const defaultModel = document.getElementById('target-model').value;
|
|
132
|
+
// All providers available (including primary, for different models)
|
|
133
|
+
const available = providers.filter(p => p.id);
|
|
134
|
+
|
|
135
|
+
// Build dropdown: show providers, each expandable to models
|
|
136
|
+
select.innerHTML = available.length === 0
|
|
137
|
+
? '<div class="model-option"><span class="model-option-name">暂无可添加供应商</span></div>'
|
|
138
|
+
: available.map(p => {
|
|
139
|
+
const models = p.models || [];
|
|
140
|
+
const isPrimary = p.id === primaryId;
|
|
141
|
+
// Filter out already-added provider+model combos
|
|
142
|
+
const usedModels = new Set(
|
|
143
|
+
providerPoolItems
|
|
144
|
+
.filter(item => item.providerId === p.id)
|
|
145
|
+
.map(item => item.model || '')
|
|
146
|
+
);
|
|
147
|
+
// For primary provider, also exclude its default model (already in use)
|
|
148
|
+
if (isPrimary && defaultModel) usedModels.add(defaultModel);
|
|
149
|
+
const availModels = models.filter(m => !usedModels.has(m));
|
|
150
|
+
// "any model" not available for primary (already covered by defaultModel)
|
|
151
|
+
const anyModelUsed = usedModels.has('');
|
|
152
|
+
const showAnyModel = !isPrimary && !anyModelUsed;
|
|
153
|
+
return `
|
|
154
|
+
<div class="pool-provider-group" data-pool-provider="${escapeHtml(p.id)}">
|
|
155
|
+
<div class="model-option pool-provider-trigger" data-pool-provider-id="${escapeHtml(p.id)}">
|
|
156
|
+
<span class="model-option-name">${escapeHtml(p.name)}</span>
|
|
157
|
+
${p.url ? `<span style="color:#64748b;font-size:12px;margin-left:4px">${escapeHtml(p.url)}</span>` : ''}
|
|
158
|
+
<span class="pool-provider-arrow">▸</span>
|
|
159
|
+
</div>
|
|
160
|
+
<div class="pool-model-sublist" data-pool-models-for="${escapeHtml(p.id)}">
|
|
161
|
+
${showAnyModel ? `<div class="model-option pool-model-option" data-pool-provider-id="${escapeHtml(p.id)}" data-pool-model=""><span class="model-option-name">不指定模型(使用请求模型)</span></div>` : ''}
|
|
162
|
+
${availModels.map(m => `<div class="model-option pool-model-option" data-pool-provider-id="${escapeHtml(p.id)}" data-pool-model="${escapeHtml(m)}"><span class="model-option-name">${escapeHtml(m)}</span></div>`).join('')}
|
|
163
|
+
${availModels.length === 0 && !showAnyModel ? '<div class="model-option"><span class="model-option-name">该供应商所有模型已添加</span></div>' : ''}
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
`;
|
|
167
|
+
}).join('');
|
|
168
|
+
|
|
169
|
+
valueEl.textContent = available.length === 0 ? '暂无可添加供应商' : '从供应商列表添加';
|
|
170
|
+
|
|
171
|
+
// Provider click → toggle model sub-list
|
|
172
|
+
select.querySelectorAll('.pool-provider-trigger').forEach(trigger => {
|
|
173
|
+
trigger.addEventListener('click', (e) => {
|
|
174
|
+
e.stopPropagation();
|
|
175
|
+
const group = trigger.closest('.pool-provider-group');
|
|
176
|
+
const wasOpen = group.classList.contains('open');
|
|
177
|
+
// Close all other sub-lists
|
|
178
|
+
select.querySelectorAll('.pool-provider-group').forEach(g => g.classList.remove('open'));
|
|
179
|
+
if (!wasOpen) group.classList.add('open');
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// Model click → add to pool
|
|
184
|
+
select.querySelectorAll('.pool-model-option').forEach(opt => {
|
|
185
|
+
opt.addEventListener('click', () => {
|
|
186
|
+
addProviderToPool(opt.dataset.poolProviderId, opt.dataset.poolModel || '');
|
|
187
|
+
dropdown.classList.remove('open');
|
|
188
|
+
select.querySelectorAll('.pool-provider-group').forEach(g => g.classList.remove('open'));
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// Render pool items
|
|
193
|
+
container.innerHTML = providerPoolItems.length === 0
|
|
194
|
+
? '<div class="provider-pool-empty">暂无备选供应商,使用上方下拉框添加</div>'
|
|
195
|
+
: providerPoolItems.map(item => {
|
|
196
|
+
const provider = providers.find(p => p.id === item.providerId);
|
|
197
|
+
const modelLabel = item.model || '使用请求模型';
|
|
198
|
+
return `
|
|
199
|
+
<div class="provider-pool-item">
|
|
200
|
+
<div class="provider-pool-main">
|
|
201
|
+
<div class="provider-pool-name">${escapeHtml(provider?.name || item.providerId)}</div>
|
|
202
|
+
<div class="provider-pool-meta">${escapeHtml(provider?.url || '')}</div>
|
|
203
|
+
</div>
|
|
204
|
+
<div class="provider-pool-model">
|
|
205
|
+
<label>模型</label>
|
|
206
|
+
<span class="provider-pool-model-value">${escapeHtml(modelLabel)}</span>
|
|
207
|
+
</div>
|
|
208
|
+
<div class="provider-pool-weight">
|
|
209
|
+
<label>权重</label>
|
|
210
|
+
<input type="number" min="1" step="1" value="${Math.max(1, parseInt(item.weight, 10) || 1)}" data-weight-provider="${escapeHtml(item.providerId)}" data-weight-model="${escapeHtml(item.model || '')}">
|
|
211
|
+
</div>
|
|
212
|
+
<button type="button" class="provider-pool-remove" data-remove-provider="${escapeHtml(item.providerId)}" data-remove-model="${escapeHtml(item.model || '')}">移除</button>
|
|
213
|
+
</div>
|
|
214
|
+
`;
|
|
215
|
+
}).join('');
|
|
216
|
+
|
|
217
|
+
container.querySelectorAll('[data-weight-provider]').forEach(input => {
|
|
218
|
+
const handler = () => updateProviderPoolWeight(input.dataset.weightProvider, input.dataset.weightModel, input.value);
|
|
219
|
+
input.addEventListener('change', handler);
|
|
220
|
+
input.addEventListener('input', handler);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
container.querySelectorAll('[data-remove-provider]').forEach(btn => {
|
|
224
|
+
btn.addEventListener('click', () => removeProviderFromPool(btn.dataset.removeProvider, btn.dataset.removeModel));
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
36
228
|
// ==================== 供应商下拉框 ====================
|
|
37
229
|
|
|
38
230
|
function initProviderDropdown() {
|
|
@@ -114,8 +306,6 @@ function initProviderDropdown() {
|
|
|
114
306
|
});
|
|
115
307
|
}
|
|
116
308
|
|
|
117
|
-
let editingProviderId = null;
|
|
118
|
-
|
|
119
309
|
function renderProviderOptions() {
|
|
120
310
|
const container = document.getElementById('provider-dropdown-options');
|
|
121
311
|
const currentId = document.getElementById('provider-id').value;
|
|
@@ -217,6 +407,10 @@ function selectProvider(id) {
|
|
|
217
407
|
document.getElementById('target-azure-deployment').value = provider?.azureDeployment || '';
|
|
218
408
|
document.getElementById('target-azure-version').value = provider?.azureApiVersion || '';
|
|
219
409
|
document.getElementById('azure-fields').style.display = protocol === 'openai' ? '' : 'none';
|
|
410
|
+
// Only remove pool entries matching this provider's default model (allow other models)
|
|
411
|
+
const currentModel = models[0] || '';
|
|
412
|
+
providerPoolItems = providerPoolItems.filter(item => !(item.providerId === id && (!item.model || item.model === currentModel)));
|
|
413
|
+
renderProviderPoolEditor();
|
|
220
414
|
}
|
|
221
415
|
|
|
222
416
|
// ==================== Model 下拉框 ====================
|
|
@@ -353,8 +547,6 @@ function updateModelAddState() {
|
|
|
353
547
|
|
|
354
548
|
// ==================== 配置导入/导出 ====================
|
|
355
549
|
|
|
356
|
-
let importData = null;
|
|
357
|
-
|
|
358
550
|
async function exportConfig() {
|
|
359
551
|
try {
|
|
360
552
|
const res = await fetch('/api/config/export');
|
|
@@ -466,9 +658,6 @@ async function restartAllProxies() {
|
|
|
466
658
|
|
|
467
659
|
// ==================== Token 用量统计 ====================
|
|
468
660
|
|
|
469
|
-
let statsRange = 'daily';
|
|
470
|
-
let statsProxyId = '';
|
|
471
|
-
|
|
472
661
|
async function loadStats() {
|
|
473
662
|
try {
|
|
474
663
|
const params = new URLSearchParams({ range: statsRange });
|
|
@@ -605,11 +794,11 @@ function generateToken() {
|
|
|
605
794
|
return Array.from(arr, b => b.toString(16).padStart(2, '0')).join('');
|
|
606
795
|
}
|
|
607
796
|
|
|
608
|
-
function initSimpleDropdown(dropdownId, onChange) {
|
|
797
|
+
function initSimpleDropdown(dropdownId, onChange, hiddenInputId) {
|
|
609
798
|
const dropdown = document.getElementById(dropdownId);
|
|
610
799
|
const trigger = dropdown.querySelector('.model-dropdown-trigger');
|
|
611
800
|
const valueEl = dropdown.querySelector('[id$="-dropdown-value"]');
|
|
612
|
-
const hiddenInput = document.getElementById(dropdownId.replace('-dropdown', ''));
|
|
801
|
+
const hiddenInput = document.getElementById(hiddenInputId || dropdownId.replace('-dropdown', ''));
|
|
613
802
|
const opts = dropdown.querySelectorAll('.model-option');
|
|
614
803
|
|
|
615
804
|
trigger.addEventListener('click', (e) => {
|
|
@@ -625,23 +814,39 @@ function initSimpleDropdown(dropdownId, onChange) {
|
|
|
625
814
|
|
|
626
815
|
opts.forEach(opt => {
|
|
627
816
|
opt.addEventListener('click', () => {
|
|
628
|
-
|
|
629
|
-
opt.classList.add('selected');
|
|
630
|
-
const val = opt.dataset.value;
|
|
631
|
-
if (hiddenInput) hiddenInput.value = val;
|
|
632
|
-
valueEl.textContent = opt.querySelector('.model-option-name').textContent;
|
|
817
|
+
const val = syncSimpleDropdown(dropdownId, opt.dataset.value, hiddenInput?.id);
|
|
633
818
|
onChange?.(val);
|
|
634
819
|
dropdown.classList.remove('open');
|
|
635
820
|
});
|
|
636
821
|
});
|
|
822
|
+
|
|
823
|
+
syncSimpleDropdown(dropdownId, hiddenInput?.value || opts[0]?.dataset.value || '', hiddenInput?.id);
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
function initProviderPoolDropdown() {
|
|
827
|
+
const dropdown = document.getElementById('provider-pool-dropdown');
|
|
828
|
+
const trigger = document.getElementById('provider-pool-dropdown-trigger');
|
|
829
|
+
if (!dropdown || !trigger) return;
|
|
830
|
+
|
|
831
|
+
trigger.addEventListener('click', (e) => {
|
|
832
|
+
e.stopPropagation();
|
|
833
|
+
renderProviderPoolEditor();
|
|
834
|
+
dropdown.classList.toggle('open');
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
document.addEventListener('click', (e) => {
|
|
838
|
+
if (!dropdown.contains(e.target)) dropdown.classList.remove('open');
|
|
839
|
+
});
|
|
637
840
|
}
|
|
638
841
|
|
|
639
842
|
async function init() {
|
|
640
843
|
await Promise.all([loadProxies(), loadProviders(), loadStats()]);
|
|
844
|
+
renderProxies();
|
|
641
845
|
initProviderDropdown();
|
|
642
846
|
initModelDropdown();
|
|
643
847
|
initStatsDropdown();
|
|
644
848
|
initStatsRangeBtns();
|
|
849
|
+
initProviderPoolDropdown();
|
|
645
850
|
initSimpleDropdown('auth-dropdown', (val) => {
|
|
646
851
|
const enabled = val === 'true';
|
|
647
852
|
document.getElementById('auth-token-group').style.display = enabled ? 'block' : 'none';
|
|
@@ -651,7 +856,11 @@ async function init() {
|
|
|
651
856
|
});
|
|
652
857
|
initSimpleDropdown('protocol-dropdown', (val) => {
|
|
653
858
|
document.getElementById('azure-fields').style.display = val === 'openai' ? '' : 'none';
|
|
654
|
-
});
|
|
859
|
+
}, 'target-protocol');
|
|
860
|
+
initSimpleDropdown('routing-dropdown', (val) => {
|
|
861
|
+
document.getElementById('routing-strategy').value = val;
|
|
862
|
+
}, 'routing-strategy');
|
|
863
|
+
renderProviderPoolEditor();
|
|
655
864
|
// 初始状态:根据当前协议值决定 Azure 字段显示
|
|
656
865
|
const initProto = document.getElementById('target-protocol').value;
|
|
657
866
|
document.getElementById('azure-fields').style.display = initProto === 'openai' ? '' : 'none';
|
|
@@ -711,6 +920,13 @@ function showToast(msg, isError) {
|
|
|
711
920
|
|
|
712
921
|
// ==================== 渲染代理列表 ====================
|
|
713
922
|
|
|
923
|
+
const ROUTING_LABELS = {
|
|
924
|
+
primary_fallback: '主备切换',
|
|
925
|
+
round_robin: '轮询',
|
|
926
|
+
weighted: '加权',
|
|
927
|
+
fastest: '最快优先',
|
|
928
|
+
};
|
|
929
|
+
|
|
714
930
|
function renderProxies() {
|
|
715
931
|
const container = document.getElementById('proxy-list');
|
|
716
932
|
if (proxies.length === 0) {
|
|
@@ -719,6 +935,27 @@ function renderProxies() {
|
|
|
719
935
|
}
|
|
720
936
|
|
|
721
937
|
container.innerHTML = proxies.map(p => {
|
|
938
|
+
// Build unified provider rows: primary first, then pool entries
|
|
939
|
+
const primaryRow = {
|
|
940
|
+
name: p.providerName || p.providerUrl || '-',
|
|
941
|
+
tag: '',
|
|
942
|
+
protocol: p.protocol || '-',
|
|
943
|
+
model: p.defaultModel || '-',
|
|
944
|
+
weight: Math.max(1, parseInt(p.providerWeight, 10) || 1),
|
|
945
|
+
};
|
|
946
|
+
const poolRows = (p.providerPool || []).map(item => {
|
|
947
|
+
const prov = providers.find(pr => pr.id === item.providerId);
|
|
948
|
+
return {
|
|
949
|
+
name: prov?.name || item.providerId,
|
|
950
|
+
tag: '备选',
|
|
951
|
+
protocol: prov?.protocol || p.protocol || '-',
|
|
952
|
+
model: item.model || '-',
|
|
953
|
+
weight: Math.max(1, parseInt(item.weight, 10) || 1),
|
|
954
|
+
};
|
|
955
|
+
});
|
|
956
|
+
const allRows = [primaryRow, ...poolRows];
|
|
957
|
+
const strategy = ROUTING_LABELS[p.routingStrategy] || p.routingStrategy;
|
|
958
|
+
|
|
722
959
|
return `
|
|
723
960
|
<div class="proxy-item">
|
|
724
961
|
<div class="proxy-header">
|
|
@@ -728,6 +965,7 @@ function renderProxies() {
|
|
|
728
965
|
${p.running ? '运行中' : '已停止'}
|
|
729
966
|
</span>
|
|
730
967
|
</div>
|
|
968
|
+
<span class="proxy-routing-badge">${escapeHtml(strategy)}</span>
|
|
731
969
|
</div>
|
|
732
970
|
<div class="proxy-meta">
|
|
733
971
|
<span>端口: <strong>${p.port}</strong></span>
|
|
@@ -745,19 +983,22 @@ function renderProxies() {
|
|
|
745
983
|
<tr>
|
|
746
984
|
<th>供应商</th>
|
|
747
985
|
<th>协议</th>
|
|
748
|
-
<th
|
|
986
|
+
<th>模型</th>
|
|
987
|
+
<th>权重</th>
|
|
749
988
|
</tr>
|
|
750
989
|
</thead>
|
|
751
990
|
<tbody>
|
|
991
|
+
${allRows.map(r => `
|
|
752
992
|
<tr>
|
|
753
|
-
<td>${escapeHtml(
|
|
993
|
+
<td>${escapeHtml(r.name)}${r.tag ? `<span class="provider-tag">${r.tag}</span>` : ''}</td>
|
|
754
994
|
<td>
|
|
755
|
-
<span class="badge" style="background:${
|
|
756
|
-
${
|
|
995
|
+
<span class="badge" style="background:${r.protocol==='openai'?'#0c4a6e':r.protocol==='anthropic'?'#581c87':'#064e3b'};color:${r.protocol==='openai'?'#7dd3fc':r.protocol==='anthropic'?'#e9d5ff':'#6ee7b7'}">
|
|
996
|
+
${r.protocol}
|
|
757
997
|
</span>
|
|
758
998
|
</td>
|
|
759
|
-
<td><code>${escapeHtml(
|
|
760
|
-
|
|
999
|
+
<td><code>${escapeHtml(r.model)}</code></td>
|
|
1000
|
+
<td>${r.weight}</td>
|
|
1001
|
+
</tr>`).join('')}
|
|
761
1002
|
</tbody>
|
|
762
1003
|
</table>
|
|
763
1004
|
<div class="proxy-actions">
|
|
@@ -805,6 +1046,9 @@ function openModal(id = null) {
|
|
|
805
1046
|
document.getElementById('target-azure-deployment').value = provider?.azureDeployment || '';
|
|
806
1047
|
document.getElementById('target-azure-version').value = provider?.azureApiVersion || '';
|
|
807
1048
|
document.getElementById('azure-fields').style.display = p.protocol === 'openai' ? '' : 'none';
|
|
1049
|
+
document.getElementById('provider-weight').value = Math.max(1, parseInt(p.providerWeight, 10) || 1);
|
|
1050
|
+
syncSimpleDropdown('routing-dropdown', p.routingStrategy || 'primary_fallback', 'routing-strategy');
|
|
1051
|
+
syncProviderPoolState(p.providerPool || []);
|
|
808
1052
|
} else {
|
|
809
1053
|
document.getElementById('proxy-id').value = '';
|
|
810
1054
|
// 重置认证下拉框
|
|
@@ -819,6 +1063,9 @@ function openModal(id = null) {
|
|
|
819
1063
|
document.getElementById('target-azure-deployment').value = '';
|
|
820
1064
|
document.getElementById('target-azure-version').value = '';
|
|
821
1065
|
document.getElementById('azure-fields').style.display = 'none';
|
|
1066
|
+
document.getElementById('provider-weight').value = 1;
|
|
1067
|
+
syncSimpleDropdown('routing-dropdown', 'primary_fallback', 'routing-strategy');
|
|
1068
|
+
syncProviderPoolState([]);
|
|
822
1069
|
}
|
|
823
1070
|
|
|
824
1071
|
updateModelAddState();
|
|
@@ -888,6 +1135,9 @@ async function handleSubmit(e) {
|
|
|
888
1135
|
authToken: document.getElementById('proxy-auth-token').value.trim() || null,
|
|
889
1136
|
providerId,
|
|
890
1137
|
defaultModel,
|
|
1138
|
+
providerWeight: Math.max(1, parseInt(document.getElementById('provider-weight').value, 10) || 1),
|
|
1139
|
+
routingStrategy: document.getElementById('routing-strategy').value || 'primary_fallback',
|
|
1140
|
+
providerPool: providerPoolItems,
|
|
891
1141
|
};
|
|
892
1142
|
|
|
893
1143
|
try {
|