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.
@@ -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.5",
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
- }
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 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,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
- 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();
136
-
137
- container.innerHTML = providers.map(p => {
138
- // 优先从服务端配置取供应商名称
139
- const serverProxy = proxies.find(pr => pr.target?.providerUrl === p.url);
140
- const displayName = (p.name && p.name !== p.url) ? p.name
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="删除此供应商">&times;</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="删除此供应商">&times;</button>
148
107
  </div>
149
- `}).join('');
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.url);
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 url = btn.dataset.deleteUrl;
167
- const p = findProviderByUrl(url);
168
- 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> 吗?`);
169
128
  if (!ok) return;
170
- const providers = loadProviders().filter(pr => pr.url !== url);
171
- saveProviders(providers);
172
- localStorage.removeItem(getModelKey(url));
173
- if (getSelectedProviderUrl() === url) {
174
- 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);
175
143
  }
176
- renderProviderOptions();
177
144
  });
178
145
  });
179
146
  }
180
147
 
181
- function selectProvider(url) {
182
- document.getElementById('target-url').value = url || '';
183
- const provider = findProviderByUrl(url);
184
- document.getElementById('target-protocol').value = url ? (provider?.protocol || detectProtocol(url)) : '';
185
- if (url) {
186
- const serverProxy = proxies.find(pr => pr.target?.providerUrl === url);
187
- const name = (provider?.name && provider.name !== url) ? provider.name
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 providerUrl = getSelectedProviderUrl();
223
- if (!providerUrl) {
224
- showToast('请先选择供应商地址', true);
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
- addModel(providerUrl, name);
230
- selectModel(name);
231
- renderModelOptions();
232
- addInput.value = '';
233
- 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
+ }
234
207
  });
235
208
 
236
209
  addInput.addEventListener('keydown', (e) => {
237
- if (e.key === 'Enter') {
238
- e.preventDefault();
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 providerUrl = getSelectedProviderUrl();
250
- 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 || [];
251
220
  const current = document.getElementById('target-model').value;
252
221
 
253
- container.innerHTML = models.map(m => `
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="删除此模型">&times;</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="删除此模型">&times;</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
- removeModel(getSelectedProviderUrl(), name);
281
- if (document.getElementById('target-model').value === name) {
282
- 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);
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 section = document.getElementById('model-add-section');
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 (providerUrl) {
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)}${providerName !== t.providerUrl ? ` <span style="color:#64748b;font-size:12px">(${escapeHtml(t.providerUrl)})</span>` : ''}</td>
405
+ <td>${escapeHtml(p.providerName || p.providerUrl || '-')}</td>
454
406
  <td>
455
- <span class="badge" style="background:${t.protocol==='openai'?'#0c4a6e':'#581c87'};color:${t.protocol==='openai'?'#7dd3fc':'#e9d5ff'}">
456
- ${t.protocol || '-'}
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(t.defaultModel) || '-'}</code></td>
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
- document.getElementById('target-key').value = t.apiKey || '';
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 providerUrl = getSelectedProviderUrl();
543
- if (!providerUrl) {
544
- showToast('请选择供应商地址', true);
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
- target,
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>供应商地址</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
  }