protocol-proxy 1.1.4 → 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.
@@ -21,36 +21,32 @@ migrateOldConfig();
21
21
 
22
22
  let configCache = null;
23
23
 
24
- function normalizeModels(target) {
25
- if (!target) return target;
26
- const models = Array.isArray(target.models) ? target.models : [];
27
- const normalized = models
28
- .filter(model => typeof model === 'string')
29
- .map(model => model.trim())
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
- ...target,
38
- models: Array.from(new Set(normalized)),
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "protocol-proxy",
3
- "version": "1.1.4",
3
+ "version": "2.0.1",
4
4
  "description": "OpenAI / Anthropic 协议转换透明代理",
5
5
  "main": "server.js",
6
6
  "bin": {
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 loadProviders() {
7
+ async function loadProxies() {
8
8
  try {
9
- return JSON.parse(localStorage.getItem(PROVIDERS_KEY)) || [];
10
- } catch { return []; }
11
- }
12
-
13
- function saveProviders(providers) {
14
- localStorage.setItem(PROVIDERS_KEY, JSON.stringify(providers));
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
18
  }
50
19
 
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
- }
55
-
56
- function addModel(providerUrl, name) {
57
- if (!providerUrl) return;
58
- const models = loadModelsByProvider(providerUrl);
59
- if (!models.includes(name)) {
60
- models.push(name);
61
- saveModelsByProvider(providerUrl, models);
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 removeModel(providerUrl, name) {
66
- const models = loadModelsByProvider(providerUrl).filter(m => m !== name);
67
- saveModelsByProvider(providerUrl, models);
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,45 +59,51 @@ 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
- addProvider(name, url);
111
- selectProvider(url);
112
- dropdown.classList.remove('open');
113
- });
114
-
115
- addUrlInput.addEventListener('keydown', (e) => {
116
- if (e.key === 'Enter') {
117
- e.preventDefault();
118
- addBtn.click();
119
- }
120
- if (e.key === 'Escape') {
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 providers = loadProviders();
135
- const currentUrl = getSelectedProviderUrl();
100
+ const currentId = document.getElementById('provider-id').value;
136
101
 
137
102
  container.innerHTML = providers.map(p => `
138
- <div class="model-option${p.url === currentUrl ? ' selected' : ''}" data-url="${escapeHtml(p.url)}">
103
+ <div class="model-option${p.id === currentId ? ' selected' : ''}" data-id="${escapeHtml(p.id)}">
139
104
  <span class="model-option-name">${escapeHtml(p.name)}</span>
140
105
  ${p.name !== p.url ? `<span style="color:#64748b;font-size:12px;margin-left:4px">${escapeHtml(p.url)}</span>` : ''}
141
- <button type="button" class="model-option-delete" data-delete-url="${escapeHtml(p.url)}" title="删除此供应商">&times;</button>
106
+ <button type="button" class="model-option-delete" data-delete-id="${escapeHtml(p.id)}" title="删除此供应商">&times;</button>
142
107
  </div>
143
108
  `).join('');
144
109
 
@@ -149,7 +114,7 @@ function renderProviderOptions() {
149
114
  container.querySelectorAll('.model-option').forEach(opt => {
150
115
  opt.addEventListener('click', (e) => {
151
116
  if (e.target.closest('.model-option-delete')) return;
152
- selectProvider(opt.dataset.url);
117
+ selectProvider(opt.dataset.id);
153
118
  document.getElementById('provider-dropdown').classList.remove('open');
154
119
  });
155
120
  });
@@ -157,34 +122,42 @@ function renderProviderOptions() {
157
122
  container.querySelectorAll('.model-option-delete').forEach(btn => {
158
123
  btn.addEventListener('click', async (e) => {
159
124
  e.stopPropagation();
160
- const url = btn.dataset.deleteUrl;
161
- const p = findProviderByUrl(url);
162
- const ok = await showConfirm(`确定要删除供应商 <strong>${escapeHtml(p?.name || url)}</strong> 吗?`);
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> 吗?`);
163
128
  if (!ok) return;
164
- const providers = loadProviders().filter(pr => pr.url !== url);
165
- saveProviders(providers);
166
- localStorage.removeItem(getModelKey(url));
167
- if (getSelectedProviderUrl() === url) {
168
- selectProvider('');
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);
169
143
  }
170
- renderProviderOptions();
171
144
  });
172
145
  });
173
146
  }
174
147
 
175
- function selectProvider(url) {
176
- document.getElementById('target-url').value = url || '';
177
- const provider = findProviderByUrl(url);
178
- document.getElementById('target-protocol').value = url ? (provider?.protocol || detectProtocol(url)) : '';
179
- document.getElementById('provider-dropdown-value').textContent = url
180
- ? (provider ? `${provider.name} - ${url}` : url)
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)
181
154
  : '选择供应商...';
182
- // 切换供应商后刷新模型列表
183
155
  renderModelOptions();
184
156
  updateModelAddState();
185
157
  }
186
158
 
187
159
  // ==================== Model 下拉框 ====================
160
+
188
161
  function initModelDropdown() {
189
162
  const trigger = document.getElementById('model-dropdown-trigger');
190
163
  const dropdown = document.getElementById('model-dropdown');
@@ -206,49 +179,57 @@ function initModelDropdown() {
206
179
  }
207
180
  });
208
181
 
209
- addBtn.addEventListener('click', () => {
210
- const providerUrl = getSelectedProviderUrl();
211
- if (!providerUrl) {
212
- showToast('请先选择供应商地址', true);
182
+ addBtn.addEventListener('click', async () => {
183
+ const providerId = document.getElementById('provider-id').value;
184
+ if (!providerId) {
185
+ showToast('请先选择供应商', true);
213
186
  return;
214
187
  }
215
188
  const name = addInput.value.trim();
216
189
  if (!name) return;
217
- addModel(providerUrl, name);
218
- selectModel(name);
219
- renderModelOptions();
220
- addInput.value = '';
221
- addInput.focus();
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
+ }
222
207
  });
223
208
 
224
209
  addInput.addEventListener('keydown', (e) => {
225
- if (e.key === 'Enter') {
226
- e.preventDefault();
227
- addBtn.click();
228
- }
229
- if (e.key === 'Escape') {
230
- dropdown.classList.remove('open');
231
- }
210
+ if (e.key === 'Enter') { e.preventDefault(); addBtn.click(); }
211
+ if (e.key === 'Escape') dropdown.classList.remove('open');
232
212
  });
233
213
  }
234
214
 
235
215
  function renderModelOptions() {
236
216
  const container = document.getElementById('model-dropdown-options');
237
- const providerUrl = getSelectedProviderUrl();
238
- const models = providerUrl ? loadModelsByProvider(providerUrl) : [];
217
+ const providerId = document.getElementById('provider-id').value;
218
+ const provider = providers.find(p => p.id === providerId);
219
+ const models = provider?.models || [];
239
220
  const current = document.getElementById('target-model').value;
240
221
 
241
- container.innerHTML = models.map(m => `
242
- <div class="model-option${m === current ? ' selected' : ''}" data-model="${escapeHtml(m)}">
243
- <span class="model-option-name">${escapeHtml(m)}</span>
244
- <button type="button" class="model-option-delete" data-delete="${escapeHtml(m)}" title="删除此模型">&times;</button>
245
- </div>
246
- `).join('');
247
-
248
- if (!providerUrl) {
222
+ if (!providerId) {
249
223
  container.innerHTML = '<div style="padding:8px 12px;color:#64748b;font-size:13px">请先选择供应商</div>';
250
224
  } else if (models.length === 0) {
251
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="删除此模型">&times;</button>
231
+ </div>
232
+ `).join('');
252
233
  }
253
234
 
254
235
  container.querySelectorAll('.model-option').forEach(opt => {
@@ -265,11 +246,23 @@ function renderModelOptions() {
265
246
  const name = btn.dataset.delete;
266
247
  const ok = await showConfirm(`确定要删除模型 <strong>${escapeHtml(name)}</strong> 吗?`);
267
248
  if (!ok) return;
268
- removeModel(getSelectedProviderUrl(), name);
269
- if (document.getElementById('target-model').value === name) {
270
- selectModel('');
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);
271
265
  }
272
- renderModelOptions();
273
266
  });
274
267
  });
275
268
  }
@@ -281,11 +274,10 @@ function selectModel(value) {
281
274
  }
282
275
 
283
276
  function updateModelAddState() {
284
- const section = document.getElementById('model-add-section');
285
- const providerUrl = getSelectedProviderUrl();
277
+ const providerId = document.getElementById('provider-id').value;
286
278
  const addInput = document.getElementById('model-add-input');
287
279
  const addBtn = document.getElementById('model-add-btn');
288
- if (providerUrl) {
280
+ if (providerId) {
289
281
  addInput.disabled = false;
290
282
  addBtn.disabled = false;
291
283
  addInput.placeholder = '输入模型名称';
@@ -296,17 +288,8 @@ function updateModelAddState() {
296
288
  }
297
289
  }
298
290
 
299
- function getSelectedModels() {
300
- const providerUrl = getSelectedProviderUrl();
301
- const models = providerUrl ? [...loadModelsByProvider(providerUrl)] : [];
302
- const current = document.getElementById('target-model').value.trim();
303
- if (current && !models.includes(current)) {
304
- models.unshift(current);
305
- }
306
- return Array.from(new Set(models));
307
- }
308
-
309
291
  // ==================== 初始化 ====================
292
+
310
293
  function generateToken() {
311
294
  const arr = new Uint8Array(24);
312
295
  crypto.getRandomValues(arr);
@@ -314,7 +297,7 @@ function generateToken() {
314
297
  }
315
298
 
316
299
  async function init() {
317
- await loadProxies();
300
+ await Promise.all([loadProxies(), loadProviders()]);
318
301
  initProviderDropdown();
319
302
  initModelDropdown();
320
303
  document.getElementById('proxy-auth').addEventListener('change', (e) => {
@@ -326,26 +309,8 @@ async function init() {
326
309
  });
327
310
  }
328
311
 
329
- async function loadProxies() {
330
- try {
331
- const res = await fetch('/api/proxies');
332
- proxies = await res.json();
333
- renderProxies();
334
- updateStats();
335
- } catch (err) {
336
- console.error('加载代理失败:', err);
337
- document.getElementById('proxy-list').innerHTML =
338
- '<div class="empty">加载失败,请刷新重试</div>';
339
- }
340
- }
341
-
342
- function updateStats() {
343
- document.getElementById('stat-total').textContent = proxies.length;
344
- document.getElementById('stat-running').textContent =
345
- proxies.filter(p => p.running).length;
346
- }
347
-
348
312
  // ==================== 代理地址复制 ====================
313
+
349
314
  function getProxyUrl(port) {
350
315
  return `http://localhost:${port}`;
351
316
  }
@@ -397,6 +362,7 @@ function showToast(msg, isError) {
397
362
  }
398
363
 
399
364
  // ==================== 渲染代理列表 ====================
365
+
400
366
  function renderProxies() {
401
367
  const container = document.getElementById('proxy-list');
402
368
  if (proxies.length === 0) {
@@ -405,8 +371,6 @@ function renderProxies() {
405
371
  }
406
372
 
407
373
  container.innerHTML = proxies.map(p => {
408
- const t = p.target || {};
409
- const providerName = getProviderDisplayName(t.providerUrl || '', t.providerName);
410
374
  return `
411
375
  <div class="proxy-item">
412
376
  <div class="proxy-header">
@@ -438,13 +402,13 @@ function renderProxies() {
438
402
  </thead>
439
403
  <tbody>
440
404
  <tr>
441
- <td>${escapeHtml(providerName)}${providerName !== t.providerUrl ? ` <span style="color:#64748b;font-size:12px">(${escapeHtml(t.providerUrl)})</span>` : ''}</td>
405
+ <td>${escapeHtml(p.providerName || p.providerUrl || '-')}</td>
442
406
  <td>
443
- <span class="badge" style="background:${t.protocol==='openai'?'#0c4a6e':'#581c87'};color:${t.protocol==='openai'?'#7dd3fc':'#e9d5ff'}">
444
- ${t.protocol || '-'}
407
+ <span class="badge" style="background:${p.protocol==='openai'?'#0c4a6e':'#581c87'};color:${p.protocol==='openai'?'#7dd3fc':'#e9d5ff'}">
408
+ ${p.protocol || '-'}
445
409
  </span>
446
410
  </td>
447
- <td><code>${escapeHtml(t.defaultModel) || '-'}</code></td>
411
+ <td><code>${escapeHtml(p.defaultModel) || '-'}</code></td>
448
412
  </tr>
449
413
  </tbody>
450
414
  </table>
@@ -461,6 +425,7 @@ function renderProxies() {
461
425
  }
462
426
 
463
427
  // ==================== 弹窗操作 ====================
428
+
464
429
  function openModal(id = null) {
465
430
  editingId = id;
466
431
  document.getElementById('modal').dataset.proxyId = id || '';
@@ -470,42 +435,14 @@ function openModal(id = null) {
470
435
  if (id) {
471
436
  const p = proxies.find(x => x.id === id);
472
437
  if (!p) return;
473
- const t = p.target || {};
474
438
  document.getElementById('proxy-id').value = p.id;
475
439
  document.getElementById('proxy-name').value = p.name;
476
440
  document.getElementById('proxy-port').value = p.port;
477
441
  document.getElementById('proxy-auth').value = p.requireAuth ? 'true' : 'false';
478
442
  document.getElementById('proxy-auth-token').value = p.authToken || '';
479
443
  document.getElementById('auth-token-group').style.display = p.requireAuth ? 'block' : 'none';
480
- document.getElementById('target-key').value = t.apiKey || '';
481
-
482
- // 自动注册供应商到全局列表
483
- if (t.providerUrl && !findProviderByUrl(t.providerUrl)) {
484
- addProvider(getProviderDisplayName(t.providerUrl), t.providerUrl);
485
- }
486
-
487
- // 迁移旧模型数据:从按代理ID存储 → 按供应商URL存储
488
- if (t.providerUrl) {
489
- const existingByProvider = loadModelsByProvider(t.providerUrl);
490
- if (existingByProvider.length === 0) {
491
- // 优先用服务端的模型列表
492
- let modelsToMigrate = Array.isArray(t.models) ? t.models.filter(Boolean) : [];
493
- // 兼容旧版 localStorage(按代理ID存储)
494
- if (modelsToMigrate.length === 0) {
495
- const legacyKey = `protocol-proxy-models-${id}`;
496
- try {
497
- const legacy = JSON.parse(localStorage.getItem(legacyKey));
498
- if (Array.isArray(legacy)) modelsToMigrate = legacy;
499
- } catch {}
500
- }
501
- if (modelsToMigrate.length > 0) {
502
- saveModelsByProvider(t.providerUrl, modelsToMigrate);
503
- }
504
- }
505
- }
506
-
507
- selectProvider(t.providerUrl || '');
508
- selectModel(t.defaultModel || '');
444
+ selectProvider(p.providerId || '');
445
+ selectModel(p.defaultModel || '');
509
446
  } else {
510
447
  document.getElementById('proxy-id').value = '';
511
448
  document.getElementById('auth-token-group').style.display = 'none';
@@ -527,37 +464,27 @@ function closeModal() {
527
464
  async function handleSubmit(e) {
528
465
  e.preventDefault();
529
466
 
530
- const providerUrl = getSelectedProviderUrl();
531
- if (!providerUrl) {
532
- showToast('请选择供应商地址', true);
467
+ const providerId = document.getElementById('provider-id').value;
468
+ if (!providerId) {
469
+ showToast('请选择供应商', true);
533
470
  return;
534
471
  }
535
472
 
536
473
  const port = parseInt(document.getElementById('proxy-port').value);
537
474
 
538
- // 前端端口冲突校验
539
475
  const conflict = proxies.find(p => p.id !== editingId && p.port === port);
540
476
  if (conflict) {
541
477
  showToast(`端口 ${port} 已被代理「${conflict.name}」占用`, true);
542
478
  return;
543
479
  }
544
480
 
545
- const provider = findProviderByUrl(providerUrl);
546
- const target = {
547
- providerUrl,
548
- providerName: provider?.name || providerUrl,
549
- protocol: detectProtocol(providerUrl),
550
- defaultModel: document.getElementById('target-model').value.trim() || undefined,
551
- models: getSelectedModels(),
552
- apiKey: document.getElementById('target-key').value.trim(),
553
- };
554
-
555
481
  const payload = {
556
482
  name: document.getElementById('proxy-name').value.trim(),
557
483
  port,
558
484
  requireAuth: document.getElementById('proxy-auth').value === 'true',
559
485
  authToken: document.getElementById('proxy-auth-token').value.trim() || null,
560
- target,
486
+ providerId,
487
+ defaultModel: document.getElementById('target-model').value.trim() || '',
561
488
  };
562
489
 
563
490
  try {
@@ -586,6 +513,7 @@ async function handleSubmit(e) {
586
513
  }
587
514
 
588
515
  // ==================== 代理操作 ====================
516
+
589
517
  async function startProxy(id) {
590
518
  try {
591
519
  await fetch(`/api/proxies/${id}/start`, { method: 'POST' });
@@ -630,6 +558,7 @@ async function editProxy(id) {
630
558
  }
631
559
 
632
560
  // ==================== 工具函数 ====================
561
+
633
562
  function escapeHtml(text) {
634
563
  if (!text) return '';
635
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>供应商地址</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>目标协议</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
- // ==================== 管理 API ====================
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
- app.get('/api/proxies', (req, res) => {
145
- const proxies = configStore.getProxies().map(p => ({
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
- target: p.target ? {
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
- res.json(proxy);
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, target } = req.body;
264
+ const { name, port, requireAuth, authToken, providerId, defaultModel } = req.body;
166
265
 
167
- if (!name || !port || !target) {
168
- return res.status(400).json({ error: 'name, port and target are required' });
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
- target,
287
+ providerId,
288
+ defaultModel: defaultModel || '',
187
289
  });
188
290
 
189
291
  try {
190
- await proxyManager.startProxy(proxy);
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 (target !== undefined) updates.target = target;
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 proxyManager.restartProxy(updated);
332
+ await startProxyWithProvider(updated);
228
333
  } catch (err) {
229
334
  return res.status(500).json({ error: `代理重启失败: ${err.message}` });
230
335
  }
231
336
  } else {
232
- proxyManager.updateProxyConfig(updated);
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 proxyManager.startProxy(proxy);
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 proxyManager.startProxy(proxy);
393
+ await startProxyWithProvider(proxy);
287
394
  } catch (err) {
288
395
  console.error(`[Init] Failed to start proxy ${proxy.name}:`, err.message);
289
396
  }