protocol-proxy 1.1.5 → 2.0.1
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 +63 -19
- package/package.json +1 -1
- package/public/app.js +149 -232
- package/public/index.html +3 -3
- package/server.js +136 -29
package/lib/config-store.js
CHANGED
|
@@ -21,36 +21,32 @@ migrateOldConfig();
|
|
|
21
21
|
|
|
22
22
|
let configCache = null;
|
|
23
23
|
|
|
24
|
-
function normalizeModels(
|
|
25
|
-
if (!
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
.filter(Boolean);
|
|
31
|
-
|
|
32
|
-
if (target.defaultModel && !normalized.includes(target.defaultModel)) {
|
|
33
|
-
normalized.unshift(target.defaultModel);
|
|
34
|
-
}
|
|
24
|
+
function normalizeModels(models) {
|
|
25
|
+
if (!Array.isArray(models)) return [];
|
|
26
|
+
return Array.from(new Set(
|
|
27
|
+
models.filter(m => typeof m === 'string').map(m => m.trim()).filter(Boolean)
|
|
28
|
+
));
|
|
29
|
+
}
|
|
35
30
|
|
|
31
|
+
function normalizeProvider(provider) {
|
|
32
|
+
if (!provider) return provider;
|
|
36
33
|
return {
|
|
37
|
-
...
|
|
38
|
-
models:
|
|
34
|
+
...provider,
|
|
35
|
+
models: normalizeModels(provider.models),
|
|
39
36
|
};
|
|
40
37
|
}
|
|
41
38
|
|
|
42
39
|
function normalizeProxy(proxy) {
|
|
43
40
|
if (!proxy) return proxy;
|
|
44
|
-
return
|
|
45
|
-
...proxy,
|
|
46
|
-
target: normalizeModels(proxy.target),
|
|
47
|
-
};
|
|
41
|
+
return proxy;
|
|
48
42
|
}
|
|
49
43
|
|
|
50
44
|
function normalizeConfig(config) {
|
|
45
|
+
const providers = Array.isArray(config?.providers) ? config.providers : [];
|
|
51
46
|
const proxies = Array.isArray(config?.proxies) ? config.proxies : [];
|
|
52
47
|
return {
|
|
53
48
|
...config,
|
|
49
|
+
providers: providers.map(normalizeProvider),
|
|
54
50
|
proxies: proxies.map(normalizeProxy),
|
|
55
51
|
};
|
|
56
52
|
}
|
|
@@ -58,7 +54,7 @@ function normalizeConfig(config) {
|
|
|
58
54
|
function loadConfig() {
|
|
59
55
|
try {
|
|
60
56
|
if (!fs.existsSync(CONFIG_PATH)) {
|
|
61
|
-
configCache = { proxies: [] };
|
|
57
|
+
configCache = { providers: [], proxies: [] };
|
|
62
58
|
return configCache;
|
|
63
59
|
}
|
|
64
60
|
const raw = fs.readFileSync(CONFIG_PATH, 'utf-8');
|
|
@@ -66,7 +62,7 @@ function loadConfig() {
|
|
|
66
62
|
return configCache;
|
|
67
63
|
} catch (err) {
|
|
68
64
|
console.error('加载配置失败:', err.message);
|
|
69
|
-
return configCache || { proxies: [] };
|
|
65
|
+
return configCache || { providers: [], proxies: [] };
|
|
70
66
|
}
|
|
71
67
|
}
|
|
72
68
|
|
|
@@ -86,6 +82,49 @@ function saveConfig(config) {
|
|
|
86
82
|
}
|
|
87
83
|
}
|
|
88
84
|
|
|
85
|
+
// ==================== 供应商 CRUD ====================
|
|
86
|
+
|
|
87
|
+
function getProviders() {
|
|
88
|
+
return loadConfig().providers || [];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function getProviderById(id) {
|
|
92
|
+
return getProviders().find(p => p.id === id);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function addProvider(provider) {
|
|
96
|
+
const config = loadConfig();
|
|
97
|
+
config.providers = config.providers || [];
|
|
98
|
+
provider.id = provider.id || 'provider-' + Date.now();
|
|
99
|
+
provider.models = normalizeModels(provider.models);
|
|
100
|
+
config.providers.push(provider);
|
|
101
|
+
saveConfig(config);
|
|
102
|
+
return provider;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function updateProvider(id, updates) {
|
|
106
|
+
const config = loadConfig();
|
|
107
|
+
const idx = (config.providers || []).findIndex(p => p.id === id);
|
|
108
|
+
if (idx === -1) return null;
|
|
109
|
+
if (updates.models !== undefined) {
|
|
110
|
+
updates.models = normalizeModels(updates.models);
|
|
111
|
+
}
|
|
112
|
+
config.providers[idx] = { ...config.providers[idx], ...updates, id };
|
|
113
|
+
saveConfig(config);
|
|
114
|
+
return config.providers[idx];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function removeProvider(id) {
|
|
118
|
+
const config = loadConfig();
|
|
119
|
+
const idx = (config.providers || []).findIndex(p => p.id === id);
|
|
120
|
+
if (idx === -1) return null;
|
|
121
|
+
const removed = config.providers.splice(idx, 1)[0];
|
|
122
|
+
saveConfig(config);
|
|
123
|
+
return removed;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ==================== 代理 CRUD ====================
|
|
127
|
+
|
|
89
128
|
function getProxies() {
|
|
90
129
|
return loadConfig().proxies || [];
|
|
91
130
|
}
|
|
@@ -124,6 +163,11 @@ function removeProxy(id) {
|
|
|
124
163
|
module.exports = {
|
|
125
164
|
loadConfig,
|
|
126
165
|
saveConfig,
|
|
166
|
+
getProviders,
|
|
167
|
+
getProviderById,
|
|
168
|
+
addProvider,
|
|
169
|
+
updateProvider,
|
|
170
|
+
removeProvider,
|
|
127
171
|
getProxies,
|
|
128
172
|
getProxyById,
|
|
129
173
|
addProxy,
|
package/package.json
CHANGED
package/public/app.js
CHANGED
|
@@ -1,81 +1,40 @@
|
|
|
1
1
|
let proxies = [];
|
|
2
|
+
let providers = [];
|
|
2
3
|
let editingId = null;
|
|
3
4
|
|
|
4
|
-
// ====================
|
|
5
|
-
const PROVIDERS_KEY = 'protocol-proxy-providers';
|
|
5
|
+
// ==================== 数据加载 ====================
|
|
6
6
|
|
|
7
|
-
function
|
|
7
|
+
async function loadProxies() {
|
|
8
8
|
try {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
function addProvider(name, url) {
|
|
18
|
-
const providers = loadProviders();
|
|
19
|
-
if (providers.some(p => p.url === url)) return;
|
|
20
|
-
providers.push({ id: 'p-' + Date.now(), name, url, protocol: detectProtocol(url) });
|
|
21
|
-
saveProviders(providers);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function findProviderByUrl(url) {
|
|
25
|
-
return loadProviders().find(p => p.url === url);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function getProviderDisplayName(url, serverName) {
|
|
29
|
-
if (serverName && serverName !== url) return serverName;
|
|
30
|
-
const p = findProviderByUrl(url);
|
|
31
|
-
return p ? p.name : url;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function detectProtocol(url) {
|
|
35
|
-
return /anthropic/i.test(url) ? 'anthropic' : 'openai';
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// ==================== Model 管理(按供应商 URL) ====================
|
|
39
|
-
function getModelKey(providerUrl) {
|
|
40
|
-
return providerUrl ? `protocol-proxy-models-${providerUrl}` : 'protocol-proxy-models-__new';
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function loadModelsByProvider(providerUrl) {
|
|
44
|
-
const saved = localStorage.getItem(getModelKey(providerUrl));
|
|
45
|
-
if (saved) {
|
|
46
|
-
try { return JSON.parse(saved); } catch { /* fall through */ }
|
|
9
|
+
const res = await fetch('/api/proxies');
|
|
10
|
+
proxies = await res.json();
|
|
11
|
+
renderProxies();
|
|
12
|
+
updateStats();
|
|
13
|
+
} catch (err) {
|
|
14
|
+
console.error('加载代理失败:', err);
|
|
15
|
+
document.getElementById('proxy-list').innerHTML =
|
|
16
|
+
'<div class="empty">加载失败,请刷新重试</div>';
|
|
47
17
|
}
|
|
48
|
-
return [];
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function saveModelsByProvider(providerUrl, models) {
|
|
52
|
-
const normalized = Array.from(new Set((models || []).map(m => m.trim()).filter(Boolean)));
|
|
53
|
-
localStorage.setItem(getModelKey(providerUrl), JSON.stringify(normalized));
|
|
54
18
|
}
|
|
55
19
|
|
|
56
|
-
function
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
20
|
+
async function loadProviders() {
|
|
21
|
+
try {
|
|
22
|
+
const res = await fetch('/api/providers');
|
|
23
|
+
providers = await res.json();
|
|
24
|
+
} catch (err) {
|
|
25
|
+
console.error('加载供应商失败:', err);
|
|
26
|
+
providers = [];
|
|
62
27
|
}
|
|
63
28
|
}
|
|
64
29
|
|
|
65
|
-
function
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
function getCurrentProxyId() {
|
|
71
|
-
return document.getElementById('modal').dataset.proxyId || null;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function getSelectedProviderUrl() {
|
|
75
|
-
return document.getElementById('target-url').value || '';
|
|
30
|
+
function updateStats() {
|
|
31
|
+
document.getElementById('stat-total').textContent = proxies.length;
|
|
32
|
+
document.getElementById('stat-running').textContent =
|
|
33
|
+
proxies.filter(p => p.running).length;
|
|
76
34
|
}
|
|
77
35
|
|
|
78
36
|
// ==================== 供应商下拉框 ====================
|
|
37
|
+
|
|
79
38
|
function initProviderDropdown() {
|
|
80
39
|
const trigger = document.getElementById('provider-dropdown-trigger');
|
|
81
40
|
const dropdown = document.getElementById('provider-dropdown');
|
|
@@ -100,53 +59,53 @@ function initProviderDropdown() {
|
|
|
100
59
|
}
|
|
101
60
|
});
|
|
102
61
|
|
|
103
|
-
addBtn.addEventListener('click', () => {
|
|
62
|
+
addBtn.addEventListener('click', async () => {
|
|
104
63
|
const name = addNameInput.value.trim();
|
|
105
64
|
const url = addUrlInput.value.trim();
|
|
106
65
|
if (!name || !url) {
|
|
107
66
|
showToast('请填写供应商名称和地址', true);
|
|
108
67
|
return;
|
|
109
68
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
69
|
+
try {
|
|
70
|
+
const res = await fetch('/api/providers', {
|
|
71
|
+
method: 'POST',
|
|
72
|
+
headers: { 'Content-Type': 'application/json' },
|
|
73
|
+
body: JSON.stringify({ name, url }),
|
|
74
|
+
});
|
|
75
|
+
if (!res.ok) {
|
|
76
|
+
const err = await res.json();
|
|
77
|
+
showToast(err.error || '添加失败', true);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const provider = await res.json();
|
|
81
|
+
await loadProviders();
|
|
82
|
+
selectProvider(provider.id);
|
|
121
83
|
dropdown.classList.remove('open');
|
|
84
|
+
} catch (err) {
|
|
85
|
+
showToast('添加失败: ' + err.message, true);
|
|
122
86
|
}
|
|
123
87
|
});
|
|
124
88
|
|
|
89
|
+
addUrlInput.addEventListener('keydown', (e) => {
|
|
90
|
+
if (e.key === 'Enter') { e.preventDefault(); addBtn.click(); }
|
|
91
|
+
if (e.key === 'Escape') dropdown.classList.remove('open');
|
|
92
|
+
});
|
|
125
93
|
addNameInput.addEventListener('keydown', (e) => {
|
|
126
|
-
if (e.key === 'Escape')
|
|
127
|
-
dropdown.classList.remove('open');
|
|
128
|
-
}
|
|
94
|
+
if (e.key === 'Escape') dropdown.classList.remove('open');
|
|
129
95
|
});
|
|
130
96
|
}
|
|
131
97
|
|
|
132
98
|
function renderProviderOptions() {
|
|
133
99
|
const container = document.getElementById('provider-dropdown-options');
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
: (serverProxy?.target?.providerName && serverProxy.target.providerName !== p.url) ? serverProxy.target.providerName
|
|
142
|
-
: p.url;
|
|
143
|
-
return `
|
|
144
|
-
<div class="model-option${p.url === currentUrl ? ' selected' : ''}" data-url="${escapeHtml(p.url)}">
|
|
145
|
-
<span class="model-option-name">${escapeHtml(displayName)}</span>
|
|
146
|
-
${displayName !== p.url ? `<span style="color:#64748b;font-size:12px;margin-left:4px">${escapeHtml(p.url)}</span>` : ''}
|
|
147
|
-
<button type="button" class="model-option-delete" data-delete-url="${escapeHtml(p.url)}" title="删除此供应商">×</button>
|
|
100
|
+
const currentId = document.getElementById('provider-id').value;
|
|
101
|
+
|
|
102
|
+
container.innerHTML = providers.map(p => `
|
|
103
|
+
<div class="model-option${p.id === currentId ? ' selected' : ''}" data-id="${escapeHtml(p.id)}">
|
|
104
|
+
<span class="model-option-name">${escapeHtml(p.name)}</span>
|
|
105
|
+
${p.name !== p.url ? `<span style="color:#64748b;font-size:12px;margin-left:4px">${escapeHtml(p.url)}</span>` : ''}
|
|
106
|
+
<button type="button" class="model-option-delete" data-delete-id="${escapeHtml(p.id)}" title="删除此供应商">×</button>
|
|
148
107
|
</div>
|
|
149
|
-
`
|
|
108
|
+
`).join('');
|
|
150
109
|
|
|
151
110
|
if (providers.length === 0) {
|
|
152
111
|
container.innerHTML = '<div style="padding:8px 12px;color:#64748b;font-size:13px">暂无供应商,请在下方添加</div>';
|
|
@@ -155,7 +114,7 @@ function renderProviderOptions() {
|
|
|
155
114
|
container.querySelectorAll('.model-option').forEach(opt => {
|
|
156
115
|
opt.addEventListener('click', (e) => {
|
|
157
116
|
if (e.target.closest('.model-option-delete')) return;
|
|
158
|
-
selectProvider(opt.dataset.
|
|
117
|
+
selectProvider(opt.dataset.id);
|
|
159
118
|
document.getElementById('provider-dropdown').classList.remove('open');
|
|
160
119
|
});
|
|
161
120
|
});
|
|
@@ -163,40 +122,42 @@ function renderProviderOptions() {
|
|
|
163
122
|
container.querySelectorAll('.model-option-delete').forEach(btn => {
|
|
164
123
|
btn.addEventListener('click', async (e) => {
|
|
165
124
|
e.stopPropagation();
|
|
166
|
-
const
|
|
167
|
-
const p =
|
|
168
|
-
const ok = await showConfirm(`确定要删除供应商 <strong>${escapeHtml(p?.name ||
|
|
125
|
+
const id = btn.dataset.deleteId;
|
|
126
|
+
const p = providers.find(pr => pr.id === id);
|
|
127
|
+
const ok = await showConfirm(`确定要删除供应商 <strong>${escapeHtml(p?.name || '')}</strong> 吗?`);
|
|
169
128
|
if (!ok) return;
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
129
|
+
try {
|
|
130
|
+
const res = await fetch(`/api/providers/${id}`, { method: 'DELETE' });
|
|
131
|
+
if (!res.ok) {
|
|
132
|
+
const err = await res.json();
|
|
133
|
+
showToast(err.error || '删除失败', true);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
await loadProviders();
|
|
137
|
+
if (document.getElementById('provider-id').value === id) {
|
|
138
|
+
selectProvider('');
|
|
139
|
+
}
|
|
140
|
+
renderProviderOptions();
|
|
141
|
+
} catch (err) {
|
|
142
|
+
showToast('删除失败: ' + err.message, true);
|
|
175
143
|
}
|
|
176
|
-
renderProviderOptions();
|
|
177
144
|
});
|
|
178
145
|
});
|
|
179
146
|
}
|
|
180
147
|
|
|
181
|
-
function selectProvider(
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
document.getElementById('target-protocol').value =
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
: (serverProxy?.target?.providerName && serverProxy.target.providerName !== url) ? serverProxy.target.providerName
|
|
189
|
-
: null;
|
|
190
|
-
document.getElementById('provider-dropdown-value').textContent = name ? `${name} - ${url}` : url;
|
|
191
|
-
} else {
|
|
192
|
-
document.getElementById('provider-dropdown-value').textContent = '选择供应商...';
|
|
193
|
-
}
|
|
194
|
-
// 切换供应商后刷新模型列表
|
|
148
|
+
function selectProvider(id) {
|
|
149
|
+
const provider = providers.find(p => p.id === id);
|
|
150
|
+
document.getElementById('provider-id').value = id || '';
|
|
151
|
+
document.getElementById('target-protocol').value = provider ? provider.protocol : '';
|
|
152
|
+
document.getElementById('provider-dropdown-value').textContent = provider
|
|
153
|
+
? (provider.name !== provider.url ? `${provider.name} - ${provider.url}` : provider.url)
|
|
154
|
+
: '选择供应商...';
|
|
195
155
|
renderModelOptions();
|
|
196
156
|
updateModelAddState();
|
|
197
157
|
}
|
|
198
158
|
|
|
199
159
|
// ==================== Model 下拉框 ====================
|
|
160
|
+
|
|
200
161
|
function initModelDropdown() {
|
|
201
162
|
const trigger = document.getElementById('model-dropdown-trigger');
|
|
202
163
|
const dropdown = document.getElementById('model-dropdown');
|
|
@@ -218,49 +179,57 @@ function initModelDropdown() {
|
|
|
218
179
|
}
|
|
219
180
|
});
|
|
220
181
|
|
|
221
|
-
addBtn.addEventListener('click', () => {
|
|
222
|
-
const
|
|
223
|
-
if (!
|
|
224
|
-
showToast('
|
|
182
|
+
addBtn.addEventListener('click', async () => {
|
|
183
|
+
const providerId = document.getElementById('provider-id').value;
|
|
184
|
+
if (!providerId) {
|
|
185
|
+
showToast('请先选择供应商', true);
|
|
225
186
|
return;
|
|
226
187
|
}
|
|
227
188
|
const name = addInput.value.trim();
|
|
228
189
|
if (!name) return;
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
190
|
+
const provider = providers.find(p => p.id === providerId);
|
|
191
|
+
if (!provider) return;
|
|
192
|
+
const models = [...(provider.models || []), name];
|
|
193
|
+
try {
|
|
194
|
+
await fetch(`/api/providers/${providerId}`, {
|
|
195
|
+
method: 'PUT',
|
|
196
|
+
headers: { 'Content-Type': 'application/json' },
|
|
197
|
+
body: JSON.stringify({ models }),
|
|
198
|
+
});
|
|
199
|
+
await loadProviders();
|
|
200
|
+
selectModel(name);
|
|
201
|
+
renderModelOptions();
|
|
202
|
+
addInput.value = '';
|
|
203
|
+
addInput.focus();
|
|
204
|
+
} catch (err) {
|
|
205
|
+
showToast('添加模型失败: ' + err.message, true);
|
|
206
|
+
}
|
|
234
207
|
});
|
|
235
208
|
|
|
236
209
|
addInput.addEventListener('keydown', (e) => {
|
|
237
|
-
if (e.key === 'Enter') {
|
|
238
|
-
|
|
239
|
-
addBtn.click();
|
|
240
|
-
}
|
|
241
|
-
if (e.key === 'Escape') {
|
|
242
|
-
dropdown.classList.remove('open');
|
|
243
|
-
}
|
|
210
|
+
if (e.key === 'Enter') { e.preventDefault(); addBtn.click(); }
|
|
211
|
+
if (e.key === 'Escape') dropdown.classList.remove('open');
|
|
244
212
|
});
|
|
245
213
|
}
|
|
246
214
|
|
|
247
215
|
function renderModelOptions() {
|
|
248
216
|
const container = document.getElementById('model-dropdown-options');
|
|
249
|
-
const
|
|
250
|
-
const
|
|
217
|
+
const providerId = document.getElementById('provider-id').value;
|
|
218
|
+
const provider = providers.find(p => p.id === providerId);
|
|
219
|
+
const models = provider?.models || [];
|
|
251
220
|
const current = document.getElementById('target-model').value;
|
|
252
221
|
|
|
253
|
-
|
|
254
|
-
<div class="model-option${m === current ? ' selected' : ''}" data-model="${escapeHtml(m)}">
|
|
255
|
-
<span class="model-option-name">${escapeHtml(m)}</span>
|
|
256
|
-
<button type="button" class="model-option-delete" data-delete="${escapeHtml(m)}" title="删除此模型">×</button>
|
|
257
|
-
</div>
|
|
258
|
-
`).join('');
|
|
259
|
-
|
|
260
|
-
if (!providerUrl) {
|
|
222
|
+
if (!providerId) {
|
|
261
223
|
container.innerHTML = '<div style="padding:8px 12px;color:#64748b;font-size:13px">请先选择供应商</div>';
|
|
262
224
|
} else if (models.length === 0) {
|
|
263
225
|
container.innerHTML = '<div style="padding:8px 12px;color:#64748b;font-size:13px">暂无模型,请在下方添加</div>';
|
|
226
|
+
} else {
|
|
227
|
+
container.innerHTML = models.map(m => `
|
|
228
|
+
<div class="model-option${m === current ? ' selected' : ''}" data-model="${escapeHtml(m)}">
|
|
229
|
+
<span class="model-option-name">${escapeHtml(m)}</span>
|
|
230
|
+
<button type="button" class="model-option-delete" data-delete="${escapeHtml(m)}" title="删除此模型">×</button>
|
|
231
|
+
</div>
|
|
232
|
+
`).join('');
|
|
264
233
|
}
|
|
265
234
|
|
|
266
235
|
container.querySelectorAll('.model-option').forEach(opt => {
|
|
@@ -277,11 +246,23 @@ function renderModelOptions() {
|
|
|
277
246
|
const name = btn.dataset.delete;
|
|
278
247
|
const ok = await showConfirm(`确定要删除模型 <strong>${escapeHtml(name)}</strong> 吗?`);
|
|
279
248
|
if (!ok) return;
|
|
280
|
-
|
|
281
|
-
if (
|
|
282
|
-
|
|
249
|
+
const provider = providers.find(p => p.id === providerId);
|
|
250
|
+
if (!provider) return;
|
|
251
|
+
const models = (provider.models || []).filter(m => m !== name);
|
|
252
|
+
try {
|
|
253
|
+
await fetch(`/api/providers/${providerId}`, {
|
|
254
|
+
method: 'PUT',
|
|
255
|
+
headers: { 'Content-Type': 'application/json' },
|
|
256
|
+
body: JSON.stringify({ models }),
|
|
257
|
+
});
|
|
258
|
+
await loadProviders();
|
|
259
|
+
if (document.getElementById('target-model').value === name) {
|
|
260
|
+
selectModel('');
|
|
261
|
+
}
|
|
262
|
+
renderModelOptions();
|
|
263
|
+
} catch (err) {
|
|
264
|
+
showToast('删除模型失败: ' + err.message, true);
|
|
283
265
|
}
|
|
284
|
-
renderModelOptions();
|
|
285
266
|
});
|
|
286
267
|
});
|
|
287
268
|
}
|
|
@@ -293,11 +274,10 @@ function selectModel(value) {
|
|
|
293
274
|
}
|
|
294
275
|
|
|
295
276
|
function updateModelAddState() {
|
|
296
|
-
const
|
|
297
|
-
const providerUrl = getSelectedProviderUrl();
|
|
277
|
+
const providerId = document.getElementById('provider-id').value;
|
|
298
278
|
const addInput = document.getElementById('model-add-input');
|
|
299
279
|
const addBtn = document.getElementById('model-add-btn');
|
|
300
|
-
if (
|
|
280
|
+
if (providerId) {
|
|
301
281
|
addInput.disabled = false;
|
|
302
282
|
addBtn.disabled = false;
|
|
303
283
|
addInput.placeholder = '输入模型名称';
|
|
@@ -308,17 +288,8 @@ function updateModelAddState() {
|
|
|
308
288
|
}
|
|
309
289
|
}
|
|
310
290
|
|
|
311
|
-
function getSelectedModels() {
|
|
312
|
-
const providerUrl = getSelectedProviderUrl();
|
|
313
|
-
const models = providerUrl ? [...loadModelsByProvider(providerUrl)] : [];
|
|
314
|
-
const current = document.getElementById('target-model').value.trim();
|
|
315
|
-
if (current && !models.includes(current)) {
|
|
316
|
-
models.unshift(current);
|
|
317
|
-
}
|
|
318
|
-
return Array.from(new Set(models));
|
|
319
|
-
}
|
|
320
|
-
|
|
321
291
|
// ==================== 初始化 ====================
|
|
292
|
+
|
|
322
293
|
function generateToken() {
|
|
323
294
|
const arr = new Uint8Array(24);
|
|
324
295
|
crypto.getRandomValues(arr);
|
|
@@ -326,7 +297,7 @@ function generateToken() {
|
|
|
326
297
|
}
|
|
327
298
|
|
|
328
299
|
async function init() {
|
|
329
|
-
await loadProxies();
|
|
300
|
+
await Promise.all([loadProxies(), loadProviders()]);
|
|
330
301
|
initProviderDropdown();
|
|
331
302
|
initModelDropdown();
|
|
332
303
|
document.getElementById('proxy-auth').addEventListener('change', (e) => {
|
|
@@ -338,26 +309,8 @@ async function init() {
|
|
|
338
309
|
});
|
|
339
310
|
}
|
|
340
311
|
|
|
341
|
-
async function loadProxies() {
|
|
342
|
-
try {
|
|
343
|
-
const res = await fetch('/api/proxies');
|
|
344
|
-
proxies = await res.json();
|
|
345
|
-
renderProxies();
|
|
346
|
-
updateStats();
|
|
347
|
-
} catch (err) {
|
|
348
|
-
console.error('加载代理失败:', err);
|
|
349
|
-
document.getElementById('proxy-list').innerHTML =
|
|
350
|
-
'<div class="empty">加载失败,请刷新重试</div>';
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
function updateStats() {
|
|
355
|
-
document.getElementById('stat-total').textContent = proxies.length;
|
|
356
|
-
document.getElementById('stat-running').textContent =
|
|
357
|
-
proxies.filter(p => p.running).length;
|
|
358
|
-
}
|
|
359
|
-
|
|
360
312
|
// ==================== 代理地址复制 ====================
|
|
313
|
+
|
|
361
314
|
function getProxyUrl(port) {
|
|
362
315
|
return `http://localhost:${port}`;
|
|
363
316
|
}
|
|
@@ -409,6 +362,7 @@ function showToast(msg, isError) {
|
|
|
409
362
|
}
|
|
410
363
|
|
|
411
364
|
// ==================== 渲染代理列表 ====================
|
|
365
|
+
|
|
412
366
|
function renderProxies() {
|
|
413
367
|
const container = document.getElementById('proxy-list');
|
|
414
368
|
if (proxies.length === 0) {
|
|
@@ -417,8 +371,6 @@ function renderProxies() {
|
|
|
417
371
|
}
|
|
418
372
|
|
|
419
373
|
container.innerHTML = proxies.map(p => {
|
|
420
|
-
const t = p.target || {};
|
|
421
|
-
const providerName = getProviderDisplayName(t.providerUrl || '', t.providerName);
|
|
422
374
|
return `
|
|
423
375
|
<div class="proxy-item">
|
|
424
376
|
<div class="proxy-header">
|
|
@@ -450,13 +402,13 @@ function renderProxies() {
|
|
|
450
402
|
</thead>
|
|
451
403
|
<tbody>
|
|
452
404
|
<tr>
|
|
453
|
-
<td>${escapeHtml(providerName
|
|
405
|
+
<td>${escapeHtml(p.providerName || p.providerUrl || '-')}</td>
|
|
454
406
|
<td>
|
|
455
|
-
<span class="badge" style="background:${
|
|
456
|
-
${
|
|
407
|
+
<span class="badge" style="background:${p.protocol==='openai'?'#0c4a6e':'#581c87'};color:${p.protocol==='openai'?'#7dd3fc':'#e9d5ff'}">
|
|
408
|
+
${p.protocol || '-'}
|
|
457
409
|
</span>
|
|
458
410
|
</td>
|
|
459
|
-
<td><code>${escapeHtml(
|
|
411
|
+
<td><code>${escapeHtml(p.defaultModel) || '-'}</code></td>
|
|
460
412
|
</tr>
|
|
461
413
|
</tbody>
|
|
462
414
|
</table>
|
|
@@ -473,6 +425,7 @@ function renderProxies() {
|
|
|
473
425
|
}
|
|
474
426
|
|
|
475
427
|
// ==================== 弹窗操作 ====================
|
|
428
|
+
|
|
476
429
|
function openModal(id = null) {
|
|
477
430
|
editingId = id;
|
|
478
431
|
document.getElementById('modal').dataset.proxyId = id || '';
|
|
@@ -482,42 +435,14 @@ function openModal(id = null) {
|
|
|
482
435
|
if (id) {
|
|
483
436
|
const p = proxies.find(x => x.id === id);
|
|
484
437
|
if (!p) return;
|
|
485
|
-
const t = p.target || {};
|
|
486
438
|
document.getElementById('proxy-id').value = p.id;
|
|
487
439
|
document.getElementById('proxy-name').value = p.name;
|
|
488
440
|
document.getElementById('proxy-port').value = p.port;
|
|
489
441
|
document.getElementById('proxy-auth').value = p.requireAuth ? 'true' : 'false';
|
|
490
442
|
document.getElementById('proxy-auth-token').value = p.authToken || '';
|
|
491
443
|
document.getElementById('auth-token-group').style.display = p.requireAuth ? 'block' : 'none';
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
// 自动注册供应商到全局列表
|
|
495
|
-
if (t.providerUrl && !findProviderByUrl(t.providerUrl)) {
|
|
496
|
-
addProvider(getProviderDisplayName(t.providerUrl), t.providerUrl);
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
// 迁移旧模型数据:从按代理ID存储 → 按供应商URL存储
|
|
500
|
-
if (t.providerUrl) {
|
|
501
|
-
const existingByProvider = loadModelsByProvider(t.providerUrl);
|
|
502
|
-
if (existingByProvider.length === 0) {
|
|
503
|
-
// 优先用服务端的模型列表
|
|
504
|
-
let modelsToMigrate = Array.isArray(t.models) ? t.models.filter(Boolean) : [];
|
|
505
|
-
// 兼容旧版 localStorage(按代理ID存储)
|
|
506
|
-
if (modelsToMigrate.length === 0) {
|
|
507
|
-
const legacyKey = `protocol-proxy-models-${id}`;
|
|
508
|
-
try {
|
|
509
|
-
const legacy = JSON.parse(localStorage.getItem(legacyKey));
|
|
510
|
-
if (Array.isArray(legacy)) modelsToMigrate = legacy;
|
|
511
|
-
} catch {}
|
|
512
|
-
}
|
|
513
|
-
if (modelsToMigrate.length > 0) {
|
|
514
|
-
saveModelsByProvider(t.providerUrl, modelsToMigrate);
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
selectProvider(t.providerUrl || '');
|
|
520
|
-
selectModel(t.defaultModel || '');
|
|
444
|
+
selectProvider(p.providerId || '');
|
|
445
|
+
selectModel(p.defaultModel || '');
|
|
521
446
|
} else {
|
|
522
447
|
document.getElementById('proxy-id').value = '';
|
|
523
448
|
document.getElementById('auth-token-group').style.display = 'none';
|
|
@@ -539,37 +464,27 @@ function closeModal() {
|
|
|
539
464
|
async function handleSubmit(e) {
|
|
540
465
|
e.preventDefault();
|
|
541
466
|
|
|
542
|
-
const
|
|
543
|
-
if (!
|
|
544
|
-
showToast('
|
|
467
|
+
const providerId = document.getElementById('provider-id').value;
|
|
468
|
+
if (!providerId) {
|
|
469
|
+
showToast('请选择供应商', true);
|
|
545
470
|
return;
|
|
546
471
|
}
|
|
547
472
|
|
|
548
473
|
const port = parseInt(document.getElementById('proxy-port').value);
|
|
549
474
|
|
|
550
|
-
// 前端端口冲突校验
|
|
551
475
|
const conflict = proxies.find(p => p.id !== editingId && p.port === port);
|
|
552
476
|
if (conflict) {
|
|
553
477
|
showToast(`端口 ${port} 已被代理「${conflict.name}」占用`, true);
|
|
554
478
|
return;
|
|
555
479
|
}
|
|
556
480
|
|
|
557
|
-
const provider = findProviderByUrl(providerUrl);
|
|
558
|
-
const target = {
|
|
559
|
-
providerUrl,
|
|
560
|
-
providerName: provider?.name || providerUrl,
|
|
561
|
-
protocol: detectProtocol(providerUrl),
|
|
562
|
-
defaultModel: document.getElementById('target-model').value.trim() || undefined,
|
|
563
|
-
models: getSelectedModels(),
|
|
564
|
-
apiKey: document.getElementById('target-key').value.trim(),
|
|
565
|
-
};
|
|
566
|
-
|
|
567
481
|
const payload = {
|
|
568
482
|
name: document.getElementById('proxy-name').value.trim(),
|
|
569
483
|
port,
|
|
570
484
|
requireAuth: document.getElementById('proxy-auth').value === 'true',
|
|
571
485
|
authToken: document.getElementById('proxy-auth-token').value.trim() || null,
|
|
572
|
-
|
|
486
|
+
providerId,
|
|
487
|
+
defaultModel: document.getElementById('target-model').value.trim() || '',
|
|
573
488
|
};
|
|
574
489
|
|
|
575
490
|
try {
|
|
@@ -598,6 +513,7 @@ async function handleSubmit(e) {
|
|
|
598
513
|
}
|
|
599
514
|
|
|
600
515
|
// ==================== 代理操作 ====================
|
|
516
|
+
|
|
601
517
|
async function startProxy(id) {
|
|
602
518
|
try {
|
|
603
519
|
await fetch(`/api/proxies/${id}/start`, { method: 'POST' });
|
|
@@ -642,6 +558,7 @@ async function editProxy(id) {
|
|
|
642
558
|
}
|
|
643
559
|
|
|
644
560
|
// ==================== 工具函数 ====================
|
|
561
|
+
|
|
645
562
|
function escapeHtml(text) {
|
|
646
563
|
if (!text) return '';
|
|
647
564
|
const div = document.createElement('div');
|
package/public/index.html
CHANGED
|
@@ -71,10 +71,10 @@
|
|
|
71
71
|
<div class="target-section">
|
|
72
72
|
<h4>目标供应商配置</h4>
|
|
73
73
|
<div class="target-item">
|
|
74
|
+
<input type="hidden" id="provider-id">
|
|
74
75
|
<div class="form-row">
|
|
75
76
|
<div class="form-group">
|
|
76
|
-
<label
|
|
77
|
-
<input type="hidden" id="target-url">
|
|
77
|
+
<label>供应商</label>
|
|
78
78
|
<div class="model-dropdown" id="provider-dropdown">
|
|
79
79
|
<div class="model-dropdown-trigger" id="provider-dropdown-trigger">
|
|
80
80
|
<span id="provider-dropdown-value">选择供应商...</span>
|
|
@@ -91,7 +91,7 @@
|
|
|
91
91
|
</div>
|
|
92
92
|
</div>
|
|
93
93
|
<div class="form-group">
|
|
94
|
-
<label
|
|
94
|
+
<label>协议</label>
|
|
95
95
|
<input type="text" id="target-protocol" readonly placeholder="自动识别" style="background:#1e293b;color:#94a3b8;cursor:not-allowed">
|
|
96
96
|
</div>
|
|
97
97
|
</div>
|
package/server.js
CHANGED
|
@@ -138,39 +138,140 @@ async function init() {
|
|
|
138
138
|
app.use(express.json());
|
|
139
139
|
app.use(express.static(path.join(__dirname, 'public')));
|
|
140
140
|
|
|
141
|
-
// ====================
|
|
141
|
+
// ==================== 辅助函数 ====================
|
|
142
|
+
|
|
143
|
+
function resolveTarget(proxy) {
|
|
144
|
+
const provider = configStore.getProviderById(proxy.providerId);
|
|
145
|
+
if (!provider) return null;
|
|
146
|
+
return {
|
|
147
|
+
providerUrl: provider.url,
|
|
148
|
+
providerName: provider.name,
|
|
149
|
+
protocol: provider.protocol,
|
|
150
|
+
apiKey: provider.apiKey,
|
|
151
|
+
defaultModel: proxy.defaultModel,
|
|
152
|
+
models: provider.models,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
142
155
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
156
|
+
async function startProxyWithProvider(proxy) {
|
|
157
|
+
const target = resolveTarget(proxy);
|
|
158
|
+
if (!target) throw new Error(`供应商 ${proxy.providerId} 不存在`);
|
|
159
|
+
const proxyConfig = { ...proxy, target };
|
|
160
|
+
return proxyManager.startProxy(proxyConfig);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ==================== 供应商 API ====================
|
|
164
|
+
|
|
165
|
+
app.get('/api/providers', (req, res) => {
|
|
166
|
+
const providers = configStore.getProviders().map(p => ({
|
|
146
167
|
...p,
|
|
147
|
-
|
|
148
|
-
...p.target,
|
|
149
|
-
apiKey: p.target.apiKey ? '***' : '',
|
|
150
|
-
} : null,
|
|
151
|
-
running: proxyManager.isRunning(p.id),
|
|
168
|
+
apiKey: p.apiKey ? '***' : '',
|
|
152
169
|
}));
|
|
170
|
+
res.json(providers);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
app.get('/api/providers/:id', (req, res) => {
|
|
174
|
+
const provider = configStore.getProviderById(req.params.id);
|
|
175
|
+
if (!provider) return res.status(404).json({ error: 'Provider not found' });
|
|
176
|
+
res.json(provider);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
app.post('/api/providers', (req, res) => {
|
|
180
|
+
const { name, url, protocol, apiKey, models } = req.body;
|
|
181
|
+
if (!name || !url) {
|
|
182
|
+
return res.status(400).json({ error: 'name and url are required' });
|
|
183
|
+
}
|
|
184
|
+
const provider = configStore.addProvider({
|
|
185
|
+
name, url,
|
|
186
|
+
protocol: protocol || (/anthropic/i.test(url) ? 'anthropic' : 'openai'),
|
|
187
|
+
apiKey: apiKey || '',
|
|
188
|
+
models: models || [],
|
|
189
|
+
});
|
|
190
|
+
res.status(201).json(provider);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
app.put('/api/providers/:id', async (req, res) => {
|
|
194
|
+
const existing = configStore.getProviderById(req.params.id);
|
|
195
|
+
if (!existing) return res.status(404).json({ error: 'Provider not found' });
|
|
196
|
+
|
|
197
|
+
const updates = {};
|
|
198
|
+
if (req.body.name !== undefined) updates.name = req.body.name;
|
|
199
|
+
if (req.body.url !== undefined) updates.url = req.body.url;
|
|
200
|
+
if (req.body.protocol !== undefined) updates.protocol = req.body.protocol;
|
|
201
|
+
if (req.body.apiKey !== undefined) updates.apiKey = req.body.apiKey;
|
|
202
|
+
if (req.body.models !== undefined) updates.models = req.body.models;
|
|
203
|
+
|
|
204
|
+
const updated = configStore.updateProvider(req.params.id, updates);
|
|
205
|
+
|
|
206
|
+
// 同步更新引用此供应商的运行中代理
|
|
207
|
+
const affectedProxies = configStore.getProxies().filter(p => p.providerId === req.params.id);
|
|
208
|
+
for (const proxy of affectedProxies) {
|
|
209
|
+
if (!proxyManager.isRunning(proxy.id)) continue;
|
|
210
|
+
const target = resolveTarget(proxy);
|
|
211
|
+
if (target) proxyManager.updateProxyConfig({ ...proxy, target });
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
res.json(updated);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
app.delete('/api/providers/:id', (req, res) => {
|
|
218
|
+
const existing = configStore.getProviderById(req.params.id);
|
|
219
|
+
if (!existing) return res.status(404).json({ error: 'Provider not found' });
|
|
220
|
+
|
|
221
|
+
// 检查是否有代理在使用此供应商
|
|
222
|
+
const inUse = configStore.getProxies().some(p => p.providerId === req.params.id);
|
|
223
|
+
if (inUse) {
|
|
224
|
+
return res.status(409).json({ error: '该供应商正在被代理使用,无法删除' });
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
configStore.removeProvider(req.params.id);
|
|
228
|
+
res.json({ success: true });
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// ==================== 代理 API ====================
|
|
232
|
+
|
|
233
|
+
// 获取所有代理配置
|
|
234
|
+
app.get('/api/proxies', (req, res) => {
|
|
235
|
+
const proxies = configStore.getProxies().map(p => {
|
|
236
|
+
const provider = configStore.getProviderById(p.providerId);
|
|
237
|
+
return {
|
|
238
|
+
...p,
|
|
239
|
+
providerName: provider?.name || '',
|
|
240
|
+
providerUrl: provider?.url || '',
|
|
241
|
+
protocol: provider?.protocol || '',
|
|
242
|
+
defaultModel: p.defaultModel || '',
|
|
243
|
+
running: proxyManager.isRunning(p.id),
|
|
244
|
+
};
|
|
245
|
+
});
|
|
153
246
|
res.json(proxies);
|
|
154
247
|
});
|
|
155
248
|
|
|
156
|
-
//
|
|
249
|
+
// 获取单个代理配置
|
|
157
250
|
app.get('/api/proxies/:id', (req, res) => {
|
|
158
251
|
const proxy = configStore.getProxyById(req.params.id);
|
|
159
252
|
if (!proxy) return res.status(404).json({ error: 'Proxy not found' });
|
|
160
|
-
|
|
253
|
+
const provider = configStore.getProviderById(proxy.providerId);
|
|
254
|
+
res.json({
|
|
255
|
+
...proxy,
|
|
256
|
+
providerName: provider?.name || '',
|
|
257
|
+
providerUrl: provider?.url || '',
|
|
258
|
+
protocol: provider?.protocol || '',
|
|
259
|
+
});
|
|
161
260
|
});
|
|
162
261
|
|
|
163
262
|
// 创建代理
|
|
164
263
|
app.post('/api/proxies', async (req, res) => {
|
|
165
|
-
const { name, port, requireAuth, authToken,
|
|
264
|
+
const { name, port, requireAuth, authToken, providerId, defaultModel } = req.body;
|
|
166
265
|
|
|
167
|
-
if (!name || !port || !
|
|
168
|
-
return res.status(400).json({ error: 'name, port and
|
|
266
|
+
if (!name || !port || !providerId) {
|
|
267
|
+
return res.status(400).json({ error: 'name, port and providerId are required' });
|
|
169
268
|
}
|
|
170
269
|
|
|
270
|
+
const provider = configStore.getProviderById(providerId);
|
|
271
|
+
if (!provider) return res.status(400).json({ error: '供应商不存在' });
|
|
272
|
+
|
|
171
273
|
const parsedPort = parseInt(port);
|
|
172
274
|
|
|
173
|
-
// 端口冲突校验
|
|
174
275
|
const existing = configStore.getProxies().find(p => p.port === parsedPort);
|
|
175
276
|
if (existing) {
|
|
176
277
|
return res.status(409).json({
|
|
@@ -183,14 +284,14 @@ async function init() {
|
|
|
183
284
|
port: parsedPort,
|
|
184
285
|
requireAuth: !!requireAuth,
|
|
185
286
|
authToken: authToken || null,
|
|
186
|
-
|
|
287
|
+
providerId,
|
|
288
|
+
defaultModel: defaultModel || '',
|
|
187
289
|
});
|
|
188
290
|
|
|
189
291
|
try {
|
|
190
|
-
await
|
|
292
|
+
await startProxyWithProvider(proxy);
|
|
191
293
|
res.status(201).json({ ...proxy, running: true });
|
|
192
294
|
} catch (err) {
|
|
193
|
-
// 启动失败,回滚已保存的配置
|
|
194
295
|
configStore.removeProxy(proxy.id);
|
|
195
296
|
res.status(500).json({ error: `代理启动失败: ${err.message}` });
|
|
196
297
|
}
|
|
@@ -198,18 +299,22 @@ async function init() {
|
|
|
198
299
|
|
|
199
300
|
// 更新代理
|
|
200
301
|
app.put('/api/proxies/:id', async (req, res) => {
|
|
201
|
-
const { name, port, requireAuth, authToken, target } = req.body;
|
|
202
302
|
const existing = configStore.getProxyById(req.params.id);
|
|
203
303
|
if (!existing) return res.status(404).json({ error: 'Proxy not found' });
|
|
204
304
|
|
|
205
305
|
const updates = {};
|
|
206
|
-
if (name !== undefined) updates.name = name;
|
|
207
|
-
if (port !== undefined) updates.port = parseInt(port);
|
|
208
|
-
if (requireAuth !== undefined) updates.requireAuth = !!requireAuth;
|
|
209
|
-
if (authToken !== undefined) updates.authToken = authToken || null;
|
|
210
|
-
if (
|
|
306
|
+
if (req.body.name !== undefined) updates.name = req.body.name;
|
|
307
|
+
if (req.body.port !== undefined) updates.port = parseInt(req.body.port);
|
|
308
|
+
if (req.body.requireAuth !== undefined) updates.requireAuth = !!req.body.requireAuth;
|
|
309
|
+
if (req.body.authToken !== undefined) updates.authToken = req.body.authToken || null;
|
|
310
|
+
if (req.body.providerId !== undefined) {
|
|
311
|
+
if (!configStore.getProviderById(req.body.providerId)) {
|
|
312
|
+
return res.status(400).json({ error: '供应商不存在' });
|
|
313
|
+
}
|
|
314
|
+
updates.providerId = req.body.providerId;
|
|
315
|
+
}
|
|
316
|
+
if (req.body.defaultModel !== undefined) updates.defaultModel = req.body.defaultModel;
|
|
211
317
|
|
|
212
|
-
// 端口变更时校验冲突
|
|
213
318
|
const needRestart = updates.port !== undefined && updates.port !== existing.port;
|
|
214
319
|
if (needRestart) {
|
|
215
320
|
const conflict = configStore.getProxies().find(p => p.id !== req.params.id && p.port === updates.port);
|
|
@@ -224,12 +329,14 @@ async function init() {
|
|
|
224
329
|
|
|
225
330
|
if (needRestart) {
|
|
226
331
|
try {
|
|
227
|
-
await
|
|
332
|
+
await startProxyWithProvider(updated);
|
|
228
333
|
} catch (err) {
|
|
229
334
|
return res.status(500).json({ error: `代理重启失败: ${err.message}` });
|
|
230
335
|
}
|
|
231
336
|
} else {
|
|
232
|
-
|
|
337
|
+
// 更新供应商配置引用
|
|
338
|
+
const target = resolveTarget(updated);
|
|
339
|
+
if (target) proxyManager.updateProxyConfig({ ...updated, target });
|
|
233
340
|
}
|
|
234
341
|
|
|
235
342
|
res.json({ ...updated, running: proxyManager.isRunning(updated.id) });
|
|
@@ -251,7 +358,7 @@ async function init() {
|
|
|
251
358
|
if (!proxy) return res.status(404).json({ error: 'Proxy not found' });
|
|
252
359
|
|
|
253
360
|
try {
|
|
254
|
-
await
|
|
361
|
+
await startProxyWithProvider(proxy);
|
|
255
362
|
res.json({ success: true, running: true });
|
|
256
363
|
} catch (err) {
|
|
257
364
|
res.status(500).json({ error: 'Failed to start proxy', message: err.message });
|
|
@@ -283,7 +390,7 @@ async function init() {
|
|
|
283
390
|
const proxies = configStore.getProxies();
|
|
284
391
|
for (const proxy of proxies) {
|
|
285
392
|
try {
|
|
286
|
-
await
|
|
393
|
+
await startProxyWithProvider(proxy);
|
|
287
394
|
} catch (err) {
|
|
288
395
|
console.error(`[Init] Failed to start proxy ${proxy.name}:`, err.message);
|
|
289
396
|
}
|