protocol-proxy 1.1.5 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -21,36 +21,76 @@ 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);
24
+ // 迁移旧格式:target → providerId
25
+ function migrateTargetToProvider(config) {
26
+ if (!Array.isArray(config.proxies)) return config;
27
+ let changed = false;
28
+
29
+ for (const proxy of config.proxies) {
30
+ if (!proxy.target) continue;
31
+
32
+ // target 但没有 providerId → 从 target 创建供应商
33
+ if (!proxy.providerId) {
34
+ const t = proxy.target;
35
+ const provider = {
36
+ id: 'provider-' + Date.now(),
37
+ name: t.providerName || t.providerUrl,
38
+ url: t.providerUrl,
39
+ protocol: t.protocol || 'openai',
40
+ apiKey: t.apiKey || '',
41
+ models: Array.isArray(t.models) ? t.models : [],
42
+ };
43
+ config.providers = config.providers || [];
44
+ config.providers.push(provider);
45
+ proxy.providerId = provider.id;
46
+ proxy.defaultModel = t.defaultModel || '';
47
+ delete proxy.target;
48
+ changed = true;
49
+ } else {
50
+ // 有 target 也有 providerId → 迁移 apiKey 到供应商,删除 target
51
+ const provider = (config.providers || []).find(p => p.id === proxy.providerId);
52
+ if (provider && proxy.target.apiKey && !provider.apiKey) {
53
+ provider.apiKey = proxy.target.apiKey;
54
+ changed = true;
55
+ }
56
+ delete proxy.target;
57
+ changed = true;
58
+ }
59
+ }
60
+
61
+ if (changed) {
62
+ configCache = config;
63
+ saveConfig(config);
34
64
  }
65
+ return config;
66
+ }
35
67
 
68
+ function normalizeModels(models) {
69
+ if (!Array.isArray(models)) return [];
70
+ return Array.from(new Set(
71
+ models.filter(m => typeof m === 'string').map(m => m.trim()).filter(Boolean)
72
+ ));
73
+ }
74
+
75
+ function normalizeProvider(provider) {
76
+ if (!provider) return provider;
36
77
  return {
37
- ...target,
38
- models: Array.from(new Set(normalized)),
78
+ ...provider,
79
+ models: normalizeModels(provider.models),
39
80
  };
40
81
  }
41
82
 
42
83
  function normalizeProxy(proxy) {
43
84
  if (!proxy) return proxy;
44
- return {
45
- ...proxy,
46
- target: normalizeModels(proxy.target),
47
- };
85
+ return proxy;
48
86
  }
49
87
 
50
88
  function normalizeConfig(config) {
89
+ const providers = Array.isArray(config?.providers) ? config.providers : [];
51
90
  const proxies = Array.isArray(config?.proxies) ? config.proxies : [];
52
91
  return {
53
92
  ...config,
93
+ providers: providers.map(normalizeProvider),
54
94
  proxies: proxies.map(normalizeProxy),
55
95
  };
56
96
  }
@@ -58,15 +98,17 @@ function normalizeConfig(config) {
58
98
  function loadConfig() {
59
99
  try {
60
100
  if (!fs.existsSync(CONFIG_PATH)) {
61
- configCache = { proxies: [] };
101
+ configCache = { providers: [], proxies: [] };
62
102
  return configCache;
63
103
  }
64
104
  const raw = fs.readFileSync(CONFIG_PATH, 'utf-8');
65
- configCache = normalizeConfig(JSON.parse(raw));
105
+ let config = normalizeConfig(JSON.parse(raw));
106
+ config = migrateTargetToProvider(config);
107
+ configCache = config;
66
108
  return configCache;
67
109
  } catch (err) {
68
110
  console.error('加载配置失败:', err.message);
69
- return configCache || { proxies: [] };
111
+ return configCache || { providers: [], proxies: [] };
70
112
  }
71
113
  }
72
114
 
@@ -86,6 +128,49 @@ function saveConfig(config) {
86
128
  }
87
129
  }
88
130
 
131
+ // ==================== 供应商 CRUD ====================
132
+
133
+ function getProviders() {
134
+ return loadConfig().providers || [];
135
+ }
136
+
137
+ function getProviderById(id) {
138
+ return getProviders().find(p => p.id === id);
139
+ }
140
+
141
+ function addProvider(provider) {
142
+ const config = loadConfig();
143
+ config.providers = config.providers || [];
144
+ provider.id = provider.id || 'provider-' + Date.now();
145
+ provider.models = normalizeModels(provider.models);
146
+ config.providers.push(provider);
147
+ saveConfig(config);
148
+ return provider;
149
+ }
150
+
151
+ function updateProvider(id, updates) {
152
+ const config = loadConfig();
153
+ const idx = (config.providers || []).findIndex(p => p.id === id);
154
+ if (idx === -1) return null;
155
+ if (updates.models !== undefined) {
156
+ updates.models = normalizeModels(updates.models);
157
+ }
158
+ config.providers[idx] = { ...config.providers[idx], ...updates, id };
159
+ saveConfig(config);
160
+ return config.providers[idx];
161
+ }
162
+
163
+ function removeProvider(id) {
164
+ const config = loadConfig();
165
+ const idx = (config.providers || []).findIndex(p => p.id === id);
166
+ if (idx === -1) return null;
167
+ const removed = config.providers.splice(idx, 1)[0];
168
+ saveConfig(config);
169
+ return removed;
170
+ }
171
+
172
+ // ==================== 代理 CRUD ====================
173
+
89
174
  function getProxies() {
90
175
  return loadConfig().proxies || [];
91
176
  }
@@ -124,6 +209,11 @@ function removeProxy(id) {
124
209
  module.exports = {
125
210
  loadConfig,
126
211
  saveConfig,
212
+ getProviders,
213
+ getProviderById,
214
+ addProvider,
215
+ updateProvider,
216
+ removeProvider,
127
217
  getProxies,
128
218
  getProxyById,
129
219
  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.2",
4
4
  "description": "OpenAI / Anthropic 协议转换透明代理",
5
5
  "main": "server.js",
6
6
  "bin": {
package/public/app.js CHANGED
@@ -1,86 +1,46 @@
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');
82
41
  const addNameInput = document.getElementById('provider-add-name');
83
42
  const addUrlInput = document.getElementById('provider-add-url');
43
+ const addKeyInput = document.getElementById('provider-add-key');
84
44
  const addBtn = document.getElementById('provider-add-btn');
85
45
 
86
46
  trigger.addEventListener('click', (e) => {
@@ -89,6 +49,7 @@ function initProviderDropdown() {
89
49
  if (dropdown.classList.contains('open')) {
90
50
  addNameInput.value = '';
91
51
  addUrlInput.value = '';
52
+ addKeyInput.value = '';
92
53
  renderProviderOptions();
93
54
  addNameInput.focus();
94
55
  }
@@ -100,53 +61,54 @@ function initProviderDropdown() {
100
61
  }
101
62
  });
102
63
 
103
- addBtn.addEventListener('click', () => {
64
+ addBtn.addEventListener('click', async () => {
104
65
  const name = addNameInput.value.trim();
105
66
  const url = addUrlInput.value.trim();
67
+ const apiKey = addKeyInput.value.trim();
106
68
  if (!name || !url) {
107
69
  showToast('请填写供应商名称和地址', true);
108
70
  return;
109
71
  }
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') {
72
+ try {
73
+ const res = await fetch('/api/providers', {
74
+ method: 'POST',
75
+ headers: { 'Content-Type': 'application/json' },
76
+ body: JSON.stringify({ name, url, apiKey }),
77
+ });
78
+ if (!res.ok) {
79
+ const err = await res.json();
80
+ showToast(err.error || '添加失败', true);
81
+ return;
82
+ }
83
+ const provider = await res.json();
84
+ await loadProviders();
85
+ selectProvider(provider.id);
121
86
  dropdown.classList.remove('open');
87
+ } catch (err) {
88
+ showToast('添加失败: ' + err.message, true);
122
89
  }
123
90
  });
124
91
 
92
+ addUrlInput.addEventListener('keydown', (e) => {
93
+ if (e.key === 'Enter') { e.preventDefault(); addBtn.click(); }
94
+ if (e.key === 'Escape') dropdown.classList.remove('open');
95
+ });
125
96
  addNameInput.addEventListener('keydown', (e) => {
126
- if (e.key === 'Escape') {
127
- dropdown.classList.remove('open');
128
- }
97
+ if (e.key === 'Escape') dropdown.classList.remove('open');
129
98
  });
130
99
  }
131
100
 
132
101
  function renderProviderOptions() {
133
102
  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>
103
+ const currentId = document.getElementById('provider-id').value;
104
+
105
+ container.innerHTML = providers.map(p => `
106
+ <div class="model-option${p.id === currentId ? ' selected' : ''}" data-id="${escapeHtml(p.id)}">
107
+ <span class="model-option-name">${escapeHtml(p.name)}</span>
108
+ ${p.name !== p.url ? `<span style="color:#64748b;font-size:12px;margin-left:4px">${escapeHtml(p.url)}</span>` : ''}
109
+ <button type="button" class="model-option-delete" data-delete-id="${escapeHtml(p.id)}" title="删除此供应商">&times;</button>
148
110
  </div>
149
- `}).join('');
111
+ `).join('');
150
112
 
151
113
  if (providers.length === 0) {
152
114
  container.innerHTML = '<div style="padding:8px 12px;color:#64748b;font-size:13px">暂无供应商,请在下方添加</div>';
@@ -155,7 +117,7 @@ function renderProviderOptions() {
155
117
  container.querySelectorAll('.model-option').forEach(opt => {
156
118
  opt.addEventListener('click', (e) => {
157
119
  if (e.target.closest('.model-option-delete')) return;
158
- selectProvider(opt.dataset.url);
120
+ selectProvider(opt.dataset.id);
159
121
  document.getElementById('provider-dropdown').classList.remove('open');
160
122
  });
161
123
  });
@@ -163,40 +125,42 @@ function renderProviderOptions() {
163
125
  container.querySelectorAll('.model-option-delete').forEach(btn => {
164
126
  btn.addEventListener('click', async (e) => {
165
127
  e.stopPropagation();
166
- const url = btn.dataset.deleteUrl;
167
- const p = findProviderByUrl(url);
168
- const ok = await showConfirm(`确定要删除供应商 <strong>${escapeHtml(p?.name || url)}</strong> 吗?`);
128
+ const id = btn.dataset.deleteId;
129
+ const p = providers.find(pr => pr.id === id);
130
+ const ok = await showConfirm(`确定要删除供应商 <strong>${escapeHtml(p?.name || '')}</strong> 吗?`);
169
131
  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('');
132
+ try {
133
+ const res = await fetch(`/api/providers/${id}`, { method: 'DELETE' });
134
+ if (!res.ok) {
135
+ const err = await res.json();
136
+ showToast(err.error || '删除失败', true);
137
+ return;
138
+ }
139
+ await loadProviders();
140
+ if (document.getElementById('provider-id').value === id) {
141
+ selectProvider('');
142
+ }
143
+ renderProviderOptions();
144
+ } catch (err) {
145
+ showToast('删除失败: ' + err.message, true);
175
146
  }
176
- renderProviderOptions();
177
147
  });
178
148
  });
179
149
  }
180
150
 
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
- // 切换供应商后刷新模型列表
151
+ function selectProvider(id) {
152
+ const provider = providers.find(p => p.id === id);
153
+ document.getElementById('provider-id').value = id || '';
154
+ document.getElementById('target-protocol').value = provider ? provider.protocol : '';
155
+ document.getElementById('provider-dropdown-value').textContent = provider
156
+ ? (provider.name !== provider.url ? `${provider.name} - ${provider.url}` : provider.url)
157
+ : '选择供应商...';
195
158
  renderModelOptions();
196
159
  updateModelAddState();
197
160
  }
198
161
 
199
162
  // ==================== Model 下拉框 ====================
163
+
200
164
  function initModelDropdown() {
201
165
  const trigger = document.getElementById('model-dropdown-trigger');
202
166
  const dropdown = document.getElementById('model-dropdown');
@@ -218,49 +182,57 @@ function initModelDropdown() {
218
182
  }
219
183
  });
220
184
 
221
- addBtn.addEventListener('click', () => {
222
- const providerUrl = getSelectedProviderUrl();
223
- if (!providerUrl) {
224
- showToast('请先选择供应商地址', true);
185
+ addBtn.addEventListener('click', async () => {
186
+ const providerId = document.getElementById('provider-id').value;
187
+ if (!providerId) {
188
+ showToast('请先选择供应商', true);
225
189
  return;
226
190
  }
227
191
  const name = addInput.value.trim();
228
192
  if (!name) return;
229
- addModel(providerUrl, name);
230
- selectModel(name);
231
- renderModelOptions();
232
- addInput.value = '';
233
- addInput.focus();
193
+ const provider = providers.find(p => p.id === providerId);
194
+ if (!provider) return;
195
+ const models = [...(provider.models || []), name];
196
+ try {
197
+ await fetch(`/api/providers/${providerId}`, {
198
+ method: 'PUT',
199
+ headers: { 'Content-Type': 'application/json' },
200
+ body: JSON.stringify({ models }),
201
+ });
202
+ await loadProviders();
203
+ selectModel(name);
204
+ renderModelOptions();
205
+ addInput.value = '';
206
+ addInput.focus();
207
+ } catch (err) {
208
+ showToast('添加模型失败: ' + err.message, true);
209
+ }
234
210
  });
235
211
 
236
212
  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
- }
213
+ if (e.key === 'Enter') { e.preventDefault(); addBtn.click(); }
214
+ if (e.key === 'Escape') dropdown.classList.remove('open');
244
215
  });
245
216
  }
246
217
 
247
218
  function renderModelOptions() {
248
219
  const container = document.getElementById('model-dropdown-options');
249
- const providerUrl = getSelectedProviderUrl();
250
- const models = providerUrl ? loadModelsByProvider(providerUrl) : [];
220
+ const providerId = document.getElementById('provider-id').value;
221
+ const provider = providers.find(p => p.id === providerId);
222
+ const models = provider?.models || [];
251
223
  const current = document.getElementById('target-model').value;
252
224
 
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) {
225
+ if (!providerId) {
261
226
  container.innerHTML = '<div style="padding:8px 12px;color:#64748b;font-size:13px">请先选择供应商</div>';
262
227
  } else if (models.length === 0) {
263
228
  container.innerHTML = '<div style="padding:8px 12px;color:#64748b;font-size:13px">暂无模型,请在下方添加</div>';
229
+ } else {
230
+ container.innerHTML = models.map(m => `
231
+ <div class="model-option${m === current ? ' selected' : ''}" data-model="${escapeHtml(m)}">
232
+ <span class="model-option-name">${escapeHtml(m)}</span>
233
+ <button type="button" class="model-option-delete" data-delete="${escapeHtml(m)}" title="删除此模型">&times;</button>
234
+ </div>
235
+ `).join('');
264
236
  }
265
237
 
266
238
  container.querySelectorAll('.model-option').forEach(opt => {
@@ -277,11 +249,23 @@ function renderModelOptions() {
277
249
  const name = btn.dataset.delete;
278
250
  const ok = await showConfirm(`确定要删除模型 <strong>${escapeHtml(name)}</strong> 吗?`);
279
251
  if (!ok) return;
280
- removeModel(getSelectedProviderUrl(), name);
281
- if (document.getElementById('target-model').value === name) {
282
- selectModel('');
252
+ const provider = providers.find(p => p.id === providerId);
253
+ if (!provider) return;
254
+ const models = (provider.models || []).filter(m => m !== name);
255
+ try {
256
+ await fetch(`/api/providers/${providerId}`, {
257
+ method: 'PUT',
258
+ headers: { 'Content-Type': 'application/json' },
259
+ body: JSON.stringify({ models }),
260
+ });
261
+ await loadProviders();
262
+ if (document.getElementById('target-model').value === name) {
263
+ selectModel('');
264
+ }
265
+ renderModelOptions();
266
+ } catch (err) {
267
+ showToast('删除模型失败: ' + err.message, true);
283
268
  }
284
- renderModelOptions();
285
269
  });
286
270
  });
287
271
  }
@@ -293,11 +277,10 @@ function selectModel(value) {
293
277
  }
294
278
 
295
279
  function updateModelAddState() {
296
- const section = document.getElementById('model-add-section');
297
- const providerUrl = getSelectedProviderUrl();
280
+ const providerId = document.getElementById('provider-id').value;
298
281
  const addInput = document.getElementById('model-add-input');
299
282
  const addBtn = document.getElementById('model-add-btn');
300
- if (providerUrl) {
283
+ if (providerId) {
301
284
  addInput.disabled = false;
302
285
  addBtn.disabled = false;
303
286
  addInput.placeholder = '输入模型名称';
@@ -308,17 +291,8 @@ function updateModelAddState() {
308
291
  }
309
292
  }
310
293
 
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
294
  // ==================== 初始化 ====================
295
+
322
296
  function generateToken() {
323
297
  const arr = new Uint8Array(24);
324
298
  crypto.getRandomValues(arr);
@@ -326,7 +300,7 @@ function generateToken() {
326
300
  }
327
301
 
328
302
  async function init() {
329
- await loadProxies();
303
+ await Promise.all([loadProxies(), loadProviders()]);
330
304
  initProviderDropdown();
331
305
  initModelDropdown();
332
306
  document.getElementById('proxy-auth').addEventListener('change', (e) => {
@@ -338,26 +312,8 @@ async function init() {
338
312
  });
339
313
  }
340
314
 
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
315
  // ==================== 代理地址复制 ====================
316
+
361
317
  function getProxyUrl(port) {
362
318
  return `http://localhost:${port}`;
363
319
  }
@@ -409,6 +365,7 @@ function showToast(msg, isError) {
409
365
  }
410
366
 
411
367
  // ==================== 渲染代理列表 ====================
368
+
412
369
  function renderProxies() {
413
370
  const container = document.getElementById('proxy-list');
414
371
  if (proxies.length === 0) {
@@ -417,8 +374,6 @@ function renderProxies() {
417
374
  }
418
375
 
419
376
  container.innerHTML = proxies.map(p => {
420
- const t = p.target || {};
421
- const providerName = getProviderDisplayName(t.providerUrl || '', t.providerName);
422
377
  return `
423
378
  <div class="proxy-item">
424
379
  <div class="proxy-header">
@@ -450,13 +405,13 @@ function renderProxies() {
450
405
  </thead>
451
406
  <tbody>
452
407
  <tr>
453
- <td>${escapeHtml(providerName)}${providerName !== t.providerUrl ? ` <span style="color:#64748b;font-size:12px">(${escapeHtml(t.providerUrl)})</span>` : ''}</td>
408
+ <td>${escapeHtml(p.providerName || p.providerUrl || '-')}</td>
454
409
  <td>
455
- <span class="badge" style="background:${t.protocol==='openai'?'#0c4a6e':'#581c87'};color:${t.protocol==='openai'?'#7dd3fc':'#e9d5ff'}">
456
- ${t.protocol || '-'}
410
+ <span class="badge" style="background:${p.protocol==='openai'?'#0c4a6e':'#581c87'};color:${p.protocol==='openai'?'#7dd3fc':'#e9d5ff'}">
411
+ ${p.protocol || '-'}
457
412
  </span>
458
413
  </td>
459
- <td><code>${escapeHtml(t.defaultModel) || '-'}</code></td>
414
+ <td><code>${escapeHtml(p.defaultModel) || '-'}</code></td>
460
415
  </tr>
461
416
  </tbody>
462
417
  </table>
@@ -473,6 +428,7 @@ function renderProxies() {
473
428
  }
474
429
 
475
430
  // ==================== 弹窗操作 ====================
431
+
476
432
  function openModal(id = null) {
477
433
  editingId = id;
478
434
  document.getElementById('modal').dataset.proxyId = id || '';
@@ -482,42 +438,14 @@ function openModal(id = null) {
482
438
  if (id) {
483
439
  const p = proxies.find(x => x.id === id);
484
440
  if (!p) return;
485
- const t = p.target || {};
486
441
  document.getElementById('proxy-id').value = p.id;
487
442
  document.getElementById('proxy-name').value = p.name;
488
443
  document.getElementById('proxy-port').value = p.port;
489
444
  document.getElementById('proxy-auth').value = p.requireAuth ? 'true' : 'false';
490
445
  document.getElementById('proxy-auth-token').value = p.authToken || '';
491
446
  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 || '');
447
+ selectProvider(p.providerId || '');
448
+ selectModel(p.defaultModel || '');
521
449
  } else {
522
450
  document.getElementById('proxy-id').value = '';
523
451
  document.getElementById('auth-token-group').style.display = 'none';
@@ -539,37 +467,27 @@ function closeModal() {
539
467
  async function handleSubmit(e) {
540
468
  e.preventDefault();
541
469
 
542
- const providerUrl = getSelectedProviderUrl();
543
- if (!providerUrl) {
544
- showToast('请选择供应商地址', true);
470
+ const providerId = document.getElementById('provider-id').value;
471
+ if (!providerId) {
472
+ showToast('请选择供应商', true);
545
473
  return;
546
474
  }
547
475
 
548
476
  const port = parseInt(document.getElementById('proxy-port').value);
549
477
 
550
- // 前端端口冲突校验
551
478
  const conflict = proxies.find(p => p.id !== editingId && p.port === port);
552
479
  if (conflict) {
553
480
  showToast(`端口 ${port} 已被代理「${conflict.name}」占用`, true);
554
481
  return;
555
482
  }
556
483
 
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
484
  const payload = {
568
485
  name: document.getElementById('proxy-name').value.trim(),
569
486
  port,
570
487
  requireAuth: document.getElementById('proxy-auth').value === 'true',
571
488
  authToken: document.getElementById('proxy-auth-token').value.trim() || null,
572
- target,
489
+ providerId,
490
+ defaultModel: document.getElementById('target-model').value.trim() || '',
573
491
  };
574
492
 
575
493
  try {
@@ -598,6 +516,7 @@ async function handleSubmit(e) {
598
516
  }
599
517
 
600
518
  // ==================== 代理操作 ====================
519
+
601
520
  async function startProxy(id) {
602
521
  try {
603
522
  await fetch(`/api/proxies/${id}/start`, { method: 'POST' });
@@ -642,6 +561,7 @@ async function editProxy(id) {
642
561
  }
643
562
 
644
563
  // ==================== 工具函数 ====================
564
+
645
565
  function escapeHtml(text) {
646
566
  if (!text) return '';
647
567
  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>
@@ -85,13 +85,14 @@
85
85
  <div class="model-add-section">
86
86
  <input type="text" class="model-add-input" id="provider-add-name" placeholder="供应商名称">
87
87
  <input type="url" class="model-add-input" id="provider-add-url" placeholder="https://api.example.com">
88
+ <input type="password" class="model-add-input" id="provider-add-key" placeholder="API Key">
88
89
  <button type="button" class="btn btn-primary btn-sm" id="provider-add-btn">添加</button>
89
90
  </div>
90
91
  </div>
91
92
  </div>
92
93
  </div>
93
94
  <div class="form-group">
94
- <label>目标协议</label>
95
+ <label>协议</label>
95
96
  <input type="text" id="target-protocol" readonly placeholder="自动识别" style="background:#1e293b;color:#94a3b8;cursor:not-allowed">
96
97
  </div>
97
98
  </div>
package/server.js CHANGED
@@ -138,39 +138,145 @@ 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
+ id: p.id,
239
+ name: p.name,
240
+ port: p.port,
241
+ requireAuth: p.requireAuth,
242
+ authToken: p.authToken,
243
+ providerId: p.providerId,
244
+ providerName: provider?.name || '',
245
+ providerUrl: provider?.url || '',
246
+ protocol: provider?.protocol || '',
247
+ defaultModel: p.defaultModel || '',
248
+ running: proxyManager.isRunning(p.id),
249
+ };
250
+ });
153
251
  res.json(proxies);
154
252
  });
155
253
 
156
- // 获取单个代理配置(含敏感信息)
254
+ // 获取单个代理配置
157
255
  app.get('/api/proxies/:id', (req, res) => {
158
256
  const proxy = configStore.getProxyById(req.params.id);
159
257
  if (!proxy) return res.status(404).json({ error: 'Proxy not found' });
160
- res.json(proxy);
258
+ const provider = configStore.getProviderById(proxy.providerId);
259
+ res.json({
260
+ ...proxy,
261
+ providerName: provider?.name || '',
262
+ providerUrl: provider?.url || '',
263
+ protocol: provider?.protocol || '',
264
+ });
161
265
  });
162
266
 
163
267
  // 创建代理
164
268
  app.post('/api/proxies', async (req, res) => {
165
- const { name, port, requireAuth, authToken, target } = req.body;
269
+ const { name, port, requireAuth, authToken, providerId, defaultModel } = req.body;
166
270
 
167
- if (!name || !port || !target) {
168
- return res.status(400).json({ error: 'name, port and target are required' });
271
+ if (!name || !port || !providerId) {
272
+ return res.status(400).json({ error: 'name, port and providerId are required' });
169
273
  }
170
274
 
275
+ const provider = configStore.getProviderById(providerId);
276
+ if (!provider) return res.status(400).json({ error: '供应商不存在' });
277
+
171
278
  const parsedPort = parseInt(port);
172
279
 
173
- // 端口冲突校验
174
280
  const existing = configStore.getProxies().find(p => p.port === parsedPort);
175
281
  if (existing) {
176
282
  return res.status(409).json({
@@ -183,14 +289,14 @@ async function init() {
183
289
  port: parsedPort,
184
290
  requireAuth: !!requireAuth,
185
291
  authToken: authToken || null,
186
- target,
292
+ providerId,
293
+ defaultModel: defaultModel || '',
187
294
  });
188
295
 
189
296
  try {
190
- await proxyManager.startProxy(proxy);
297
+ await startProxyWithProvider(proxy);
191
298
  res.status(201).json({ ...proxy, running: true });
192
299
  } catch (err) {
193
- // 启动失败,回滚已保存的配置
194
300
  configStore.removeProxy(proxy.id);
195
301
  res.status(500).json({ error: `代理启动失败: ${err.message}` });
196
302
  }
@@ -198,18 +304,22 @@ async function init() {
198
304
 
199
305
  // 更新代理
200
306
  app.put('/api/proxies/:id', async (req, res) => {
201
- const { name, port, requireAuth, authToken, target } = req.body;
202
307
  const existing = configStore.getProxyById(req.params.id);
203
308
  if (!existing) return res.status(404).json({ error: 'Proxy not found' });
204
309
 
205
310
  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;
311
+ if (req.body.name !== undefined) updates.name = req.body.name;
312
+ if (req.body.port !== undefined) updates.port = parseInt(req.body.port);
313
+ if (req.body.requireAuth !== undefined) updates.requireAuth = !!req.body.requireAuth;
314
+ if (req.body.authToken !== undefined) updates.authToken = req.body.authToken || null;
315
+ if (req.body.providerId !== undefined) {
316
+ if (!configStore.getProviderById(req.body.providerId)) {
317
+ return res.status(400).json({ error: '供应商不存在' });
318
+ }
319
+ updates.providerId = req.body.providerId;
320
+ }
321
+ if (req.body.defaultModel !== undefined) updates.defaultModel = req.body.defaultModel;
211
322
 
212
- // 端口变更时校验冲突
213
323
  const needRestart = updates.port !== undefined && updates.port !== existing.port;
214
324
  if (needRestart) {
215
325
  const conflict = configStore.getProxies().find(p => p.id !== req.params.id && p.port === updates.port);
@@ -224,12 +334,14 @@ async function init() {
224
334
 
225
335
  if (needRestart) {
226
336
  try {
227
- await proxyManager.restartProxy(updated);
337
+ await startProxyWithProvider(updated);
228
338
  } catch (err) {
229
339
  return res.status(500).json({ error: `代理重启失败: ${err.message}` });
230
340
  }
231
341
  } else {
232
- proxyManager.updateProxyConfig(updated);
342
+ // 更新供应商配置引用
343
+ const target = resolveTarget(updated);
344
+ if (target) proxyManager.updateProxyConfig({ ...updated, target });
233
345
  }
234
346
 
235
347
  res.json({ ...updated, running: proxyManager.isRunning(updated.id) });
@@ -251,7 +363,7 @@ async function init() {
251
363
  if (!proxy) return res.status(404).json({ error: 'Proxy not found' });
252
364
 
253
365
  try {
254
- await proxyManager.startProxy(proxy);
366
+ await startProxyWithProvider(proxy);
255
367
  res.json({ success: true, running: true });
256
368
  } catch (err) {
257
369
  res.status(500).json({ error: 'Failed to start proxy', message: err.message });
@@ -283,7 +395,7 @@ async function init() {
283
395
  const proxies = configStore.getProxies();
284
396
  for (const proxy of proxies) {
285
397
  try {
286
- await proxyManager.startProxy(proxy);
398
+ await startProxyWithProvider(proxy);
287
399
  } catch (err) {
288
400
  console.error(`[Init] Failed to start proxy ${proxy.name}:`, err.message);
289
401
  }