protocol-proxy 1.0.5 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "protocol-proxy",
3
- "version": "1.0.5",
3
+ "version": "1.1.0",
4
4
  "description": "OpenAI / Anthropic 协议转换透明代理",
5
5
  "main": "server.js",
6
6
  "bin": {
package/public/app.js CHANGED
@@ -1,63 +1,188 @@
1
1
  let proxies = [];
2
2
  let editingId = null;
3
3
 
4
- // ==================== Model 管理(按代理独立) ====================
5
- function getLegacyModelKey(proxyId) {
6
- return `protocol-proxy-models-${proxyId || '__new'}`;
4
+ // ==================== 供应商管理(全局共享) ====================
5
+ const PROVIDERS_KEY = 'protocol-proxy-providers';
6
+
7
+ function loadProviders() {
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));
7
15
  }
8
16
 
9
- function getModelKey(proxyId) {
10
- return getLegacyModelKey(proxyId);
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);
11
22
  }
12
23
 
13
- function loadLegacyModels(proxyId) {
14
- const saved = localStorage.getItem(getLegacyModelKey(proxyId));
24
+ function findProviderByUrl(url) {
25
+ return loadProviders().find(p => p.url === url);
26
+ }
27
+
28
+ function getProviderDisplayName(url) {
29
+ const p = findProviderByUrl(url);
30
+ return p ? p.name : url;
31
+ }
32
+
33
+ function detectProtocol(url) {
34
+ return /anthropic/i.test(url) ? 'anthropic' : 'openai';
35
+ }
36
+
37
+ // ==================== Model 管理(按供应商 URL) ====================
38
+ function getModelKey(providerUrl) {
39
+ return providerUrl ? `protocol-proxy-models-${providerUrl}` : 'protocol-proxy-models-__new';
40
+ }
41
+
42
+ function loadModelsByProvider(providerUrl) {
43
+ const saved = localStorage.getItem(getModelKey(providerUrl));
15
44
  if (saved) {
16
45
  try { return JSON.parse(saved); } catch { /* fall through */ }
17
46
  }
18
47
  return [];
19
48
  }
20
49
 
21
- function loadModels(proxyId) {
22
- const proxy = proxyId ? proxies.find(p => p.id === proxyId) : null;
23
- const serverModels = proxy?.target?.models;
24
- if (Array.isArray(serverModels)) {
25
- return serverModels.filter(Boolean);
26
- }
27
-
28
- return loadLegacyModels(proxyId);
29
- }
30
-
31
- function saveModels(proxyId, models) {
50
+ function saveModelsByProvider(providerUrl, models) {
32
51
  const normalized = Array.from(new Set((models || []).map(m => m.trim()).filter(Boolean)));
33
- if (proxyId) {
34
- const proxy = proxies.find(p => p.id === proxyId);
35
- if (proxy) {
36
- proxy.target = proxy.target || {};
37
- proxy.target.models = normalized;
38
- }
39
- return;
40
- }
41
- localStorage.setItem(getLegacyModelKey(proxyId), JSON.stringify(normalized));
52
+ localStorage.setItem(getModelKey(providerUrl), JSON.stringify(normalized));
42
53
  }
43
54
 
44
- function addModel(proxyId, name) {
45
- const models = loadModels(proxyId);
55
+ function addModel(providerUrl, name) {
56
+ if (!providerUrl) return;
57
+ const models = loadModelsByProvider(providerUrl);
46
58
  if (!models.includes(name)) {
47
59
  models.push(name);
48
- saveModels(proxyId, models);
60
+ saveModelsByProvider(providerUrl, models);
49
61
  }
50
62
  }
51
63
 
52
- function removeModel(proxyId, name) {
53
- const models = loadModels(proxyId).filter(m => m !== name);
54
- saveModels(proxyId, models);
64
+ function removeModel(providerUrl, name) {
65
+ const models = loadModelsByProvider(providerUrl).filter(m => m !== name);
66
+ saveModelsByProvider(providerUrl, models);
55
67
  }
56
68
 
57
69
  function getCurrentProxyId() {
58
70
  return document.getElementById('modal').dataset.proxyId || null;
59
71
  }
60
72
 
73
+ function getSelectedProviderUrl() {
74
+ return document.getElementById('target-url').value || '';
75
+ }
76
+
77
+ // ==================== 供应商下拉框 ====================
78
+ function initProviderDropdown() {
79
+ const trigger = document.getElementById('provider-dropdown-trigger');
80
+ const dropdown = document.getElementById('provider-dropdown');
81
+ const addNameInput = document.getElementById('provider-add-name');
82
+ const addUrlInput = document.getElementById('provider-add-url');
83
+ const addBtn = document.getElementById('provider-add-btn');
84
+
85
+ trigger.addEventListener('click', (e) => {
86
+ e.stopPropagation();
87
+ dropdown.classList.toggle('open');
88
+ if (dropdown.classList.contains('open')) {
89
+ addNameInput.value = '';
90
+ addUrlInput.value = '';
91
+ renderProviderOptions();
92
+ addNameInput.focus();
93
+ }
94
+ });
95
+
96
+ document.addEventListener('click', (e) => {
97
+ if (!dropdown.contains(e.target)) {
98
+ dropdown.classList.remove('open');
99
+ }
100
+ });
101
+
102
+ addBtn.addEventListener('click', () => {
103
+ const name = addNameInput.value.trim();
104
+ const url = addUrlInput.value.trim();
105
+ if (!name || !url) {
106
+ showToast('请填写供应商名称和地址', true);
107
+ return;
108
+ }
109
+ addProvider(name, url);
110
+ selectProvider(url);
111
+ dropdown.classList.remove('open');
112
+ });
113
+
114
+ addUrlInput.addEventListener('keydown', (e) => {
115
+ if (e.key === 'Enter') {
116
+ e.preventDefault();
117
+ addBtn.click();
118
+ }
119
+ if (e.key === 'Escape') {
120
+ dropdown.classList.remove('open');
121
+ }
122
+ });
123
+
124
+ addNameInput.addEventListener('keydown', (e) => {
125
+ if (e.key === 'Escape') {
126
+ dropdown.classList.remove('open');
127
+ }
128
+ });
129
+ }
130
+
131
+ function renderProviderOptions() {
132
+ const container = document.getElementById('provider-dropdown-options');
133
+ const providers = loadProviders();
134
+ const currentUrl = getSelectedProviderUrl();
135
+
136
+ container.innerHTML = providers.map(p => `
137
+ <div class="model-option${p.url === currentUrl ? ' selected' : ''}" data-url="${escapeHtml(p.url)}">
138
+ <span class="model-option-name">${escapeHtml(p.name)}</span>
139
+ <span style="color:#64748b;font-size:12px;margin-left:4px">${escapeHtml(p.url)}</span>
140
+ <button type="button" class="model-option-delete" data-delete-url="${escapeHtml(p.url)}" title="删除此供应商">&times;</button>
141
+ </div>
142
+ `).join('');
143
+
144
+ if (providers.length === 0) {
145
+ container.innerHTML = '<div style="padding:8px 12px;color:#64748b;font-size:13px">暂无供应商,请在下方添加</div>';
146
+ }
147
+
148
+ container.querySelectorAll('.model-option').forEach(opt => {
149
+ opt.addEventListener('click', (e) => {
150
+ if (e.target.closest('.model-option-delete')) return;
151
+ selectProvider(opt.dataset.url);
152
+ document.getElementById('provider-dropdown').classList.remove('open');
153
+ });
154
+ });
155
+
156
+ container.querySelectorAll('.model-option-delete').forEach(btn => {
157
+ btn.addEventListener('click', async (e) => {
158
+ e.stopPropagation();
159
+ const url = btn.dataset.deleteUrl;
160
+ const p = findProviderByUrl(url);
161
+ const ok = await showConfirm(`确定要删除供应商 <strong>${escapeHtml(p?.name || url)}</strong> 吗?`);
162
+ if (!ok) return;
163
+ const providers = loadProviders().filter(pr => pr.url !== url);
164
+ saveProviders(providers);
165
+ if (getSelectedProviderUrl() === url) {
166
+ selectProvider('');
167
+ }
168
+ renderProviderOptions();
169
+ });
170
+ });
171
+ }
172
+
173
+ function selectProvider(url) {
174
+ document.getElementById('target-url').value = url || '';
175
+ const provider = findProviderByUrl(url);
176
+ document.getElementById('target-protocol').value = url ? (provider?.protocol || detectProtocol(url)) : '';
177
+ document.getElementById('provider-dropdown-value').textContent = url
178
+ ? (provider ? `${provider.name} - ${url}` : url)
179
+ : '选择供应商...';
180
+ // 切换供应商后刷新模型列表
181
+ renderModelOptions();
182
+ updateModelAddState();
183
+ }
184
+
185
+ // ==================== Model 下拉框 ====================
61
186
  function initModelDropdown() {
62
187
  const trigger = document.getElementById('model-dropdown-trigger');
63
188
  const dropdown = document.getElementById('model-dropdown');
@@ -80,10 +205,14 @@ function initModelDropdown() {
80
205
  });
81
206
 
82
207
  addBtn.addEventListener('click', () => {
208
+ const providerUrl = getSelectedProviderUrl();
209
+ if (!providerUrl) {
210
+ showToast('请先选择供应商地址', true);
211
+ return;
212
+ }
83
213
  const name = addInput.value.trim();
84
214
  if (!name) return;
85
- const proxyId = getCurrentProxyId();
86
- addModel(proxyId, name);
215
+ addModel(providerUrl, name);
87
216
  selectModel(name);
88
217
  renderModelOptions();
89
218
  addInput.value = '';
@@ -103,8 +232,8 @@ function initModelDropdown() {
103
232
 
104
233
  function renderModelOptions() {
105
234
  const container = document.getElementById('model-dropdown-options');
106
- const proxyId = getCurrentProxyId();
107
- const models = loadModels(proxyId);
235
+ const providerUrl = getSelectedProviderUrl();
236
+ const models = providerUrl ? loadModelsByProvider(providerUrl) : [];
108
237
  const current = document.getElementById('target-model').value;
109
238
 
110
239
  container.innerHTML = models.map(m => `
@@ -114,6 +243,12 @@ function renderModelOptions() {
114
243
  </div>
115
244
  `).join('');
116
245
 
246
+ if (!providerUrl) {
247
+ container.innerHTML = '<div style="padding:8px 12px;color:#64748b;font-size:13px">请先选择供应商</div>';
248
+ } else if (models.length === 0) {
249
+ container.innerHTML = '<div style="padding:8px 12px;color:#64748b;font-size:13px">暂无模型,请在下方添加</div>';
250
+ }
251
+
117
252
  container.querySelectorAll('.model-option').forEach(opt => {
118
253
  opt.addEventListener('click', (e) => {
119
254
  if (e.target.closest('.model-option-delete')) return;
@@ -128,8 +263,7 @@ function renderModelOptions() {
128
263
  const name = btn.dataset.delete;
129
264
  const ok = await showConfirm(`确定要删除模型 <strong>${escapeHtml(name)}</strong> 吗?`);
130
265
  if (!ok) return;
131
- const pid = getCurrentProxyId();
132
- removeModel(pid, name);
266
+ removeModel(getSelectedProviderUrl(), name);
133
267
  if (document.getElementById('target-model').value === name) {
134
268
  selectModel('');
135
269
  }
@@ -144,9 +278,25 @@ function selectModel(value) {
144
278
  renderModelOptions();
145
279
  }
146
280
 
281
+ function updateModelAddState() {
282
+ const section = document.getElementById('model-add-section');
283
+ const providerUrl = getSelectedProviderUrl();
284
+ const addInput = document.getElementById('model-add-input');
285
+ const addBtn = document.getElementById('model-add-btn');
286
+ if (providerUrl) {
287
+ addInput.disabled = false;
288
+ addBtn.disabled = false;
289
+ addInput.placeholder = '输入模型名称';
290
+ } else {
291
+ addInput.disabled = true;
292
+ addBtn.disabled = true;
293
+ addInput.placeholder = '请先选择供应商';
294
+ }
295
+ }
296
+
147
297
  function getSelectedModels() {
148
- const proxyId = getCurrentProxyId();
149
- const models = [...loadModels(proxyId), ...loadLegacyModels(proxyId)];
298
+ const providerUrl = getSelectedProviderUrl();
299
+ const models = providerUrl ? [...loadModelsByProvider(providerUrl)] : [];
150
300
  const current = document.getElementById('target-model').value.trim();
151
301
  if (current && !models.includes(current)) {
152
302
  models.unshift(current);
@@ -163,6 +313,7 @@ function generateToken() {
163
313
 
164
314
  async function init() {
165
315
  await loadProxies();
316
+ initProviderDropdown();
166
317
  initModelDropdown();
167
318
  document.getElementById('proxy-auth').addEventListener('change', (e) => {
168
319
  const enabled = e.target.value === 'true';
@@ -253,6 +404,7 @@ function renderProxies() {
253
404
 
254
405
  container.innerHTML = proxies.map(p => {
255
406
  const t = p.target || {};
407
+ const providerName = getProviderDisplayName(t.providerUrl || '');
256
408
  return `
257
409
  <div class="proxy-item">
258
410
  <div class="proxy-header">
@@ -277,17 +429,17 @@ function renderProxies() {
277
429
  <table class="target-table">
278
430
  <thead>
279
431
  <tr>
280
- <th>供应商地址</th>
432
+ <th>供应商</th>
281
433
  <th>协议</th>
282
434
  <th>默认 Model</th>
283
435
  </tr>
284
436
  </thead>
285
437
  <tbody>
286
438
  <tr>
287
- <td>${escapeHtml(t.providerUrl)}</td>
439
+ <td>${escapeHtml(providerName)}${providerName !== t.providerUrl ? ` <span style="color:#64748b;font-size:12px">(${escapeHtml(t.providerUrl)})</span>` : ''}</td>
288
440
  <td>
289
441
  <span class="badge" style="background:${t.protocol==='openai'?'#0c4a6e':'#581c87'};color:${t.protocol==='openai'?'#7dd3fc':'#e9d5ff'}">
290
- ${t.protocol}
442
+ ${t.protocol || '-'}
291
443
  </span>
292
444
  </td>
293
445
  <td><code>${escapeHtml(t.defaultModel) || '-'}</code></td>
@@ -323,36 +475,41 @@ function openModal(id = null) {
323
475
  document.getElementById('proxy-auth').value = p.requireAuth ? 'true' : 'false';
324
476
  document.getElementById('proxy-auth-token').value = p.authToken || '';
325
477
  document.getElementById('auth-token-group').style.display = p.requireAuth ? 'block' : 'none';
326
- document.getElementById('target-url').value = t.providerUrl || '';
327
- document.getElementById('target-protocol').value = t.protocol || 'openai';
328
478
  document.getElementById('target-key').value = t.apiKey || '';
329
- if ((!Array.isArray(t.models) || t.models.length === 0)) {
330
- const legacyModels = loadModels(id);
331
- if (legacyModels.length > 0) {
332
- p.target = p.target || {};
333
- p.target.models = legacyModels;
334
- }
479
+
480
+ // 自动注册供应商到全局列表
481
+ if (t.providerUrl && !findProviderByUrl(t.providerUrl)) {
482
+ addProvider(getProviderDisplayName(t.providerUrl), t.providerUrl);
335
483
  }
484
+ selectProvider(t.providerUrl || '');
336
485
  selectModel(t.defaultModel || '');
337
486
  } else {
338
487
  document.getElementById('proxy-id').value = '';
339
488
  document.getElementById('auth-token-group').style.display = 'none';
340
- const models = loadModels(null);
341
- selectModel(models[0] || '');
489
+ selectProvider('');
490
+ selectModel('');
342
491
  }
343
492
 
493
+ updateModelAddState();
344
494
  document.getElementById('modal').classList.add('active');
345
495
  }
346
496
 
347
497
  function closeModal() {
348
498
  document.getElementById('modal').classList.remove('active');
349
499
  document.getElementById('model-dropdown').classList.remove('open');
500
+ document.getElementById('provider-dropdown').classList.remove('open');
350
501
  editingId = null;
351
502
  }
352
503
 
353
504
  async function handleSubmit(e) {
354
505
  e.preventDefault();
355
506
 
507
+ const providerUrl = getSelectedProviderUrl();
508
+ if (!providerUrl) {
509
+ showToast('请选择供应商地址', true);
510
+ return;
511
+ }
512
+
356
513
  const port = parseInt(document.getElementById('proxy-port').value);
357
514
 
358
515
  // 前端端口冲突校验
@@ -363,8 +520,8 @@ async function handleSubmit(e) {
363
520
  }
364
521
 
365
522
  const target = {
366
- providerUrl: document.getElementById('target-url').value.trim(),
367
- protocol: document.getElementById('target-protocol').value,
523
+ providerUrl,
524
+ protocol: detectProtocol(providerUrl),
368
525
  defaultModel: document.getElementById('target-model').value.trim() || undefined,
369
526
  models: getSelectedModels(),
370
527
  apiKey: document.getElementById('target-key').value.trim(),
@@ -395,15 +552,6 @@ async function handleSubmit(e) {
395
552
  return;
396
553
  }
397
554
 
398
- // 新建代理后,将临时模型列表迁移到真实 ID 下
399
- if (!editingId && result.id) {
400
- const tempModels = loadModels(null);
401
- if (tempModels.length > 0) {
402
- saveModels(result.id, tempModels);
403
- }
404
- localStorage.removeItem(getModelKey(null));
405
- }
406
-
407
555
  closeModal();
408
556
  await loadProxies();
409
557
  } catch (err) {
@@ -437,7 +585,6 @@ async function deleteProxy(id) {
437
585
  if (!ok) return;
438
586
  try {
439
587
  await fetch(`/api/proxies/${id}`, { method: 'DELETE' });
440
- localStorage.removeItem(getModelKey(id));
441
588
  await loadProxies();
442
589
  } catch (err) {
443
590
  showToast('删除失败: ' + err.message, true);
@@ -449,7 +596,6 @@ async function editProxy(id) {
449
596
  const res = await fetch(`/api/proxies/${id}`);
450
597
  if (!res.ok) throw new Error('加载失败');
451
598
  const full = await res.json();
452
- // 用完整数据替换列表中的掩码数据
453
599
  const idx = proxies.findIndex(p => p.id === id);
454
600
  if (idx !== -1) proxies[idx] = { ...proxies[idx], ...full };
455
601
  openModal(id);
package/public/index.html CHANGED
@@ -74,14 +74,25 @@
74
74
  <div class="form-row">
75
75
  <div class="form-group">
76
76
  <label>供应商地址</label>
77
- <input type="url" id="target-url" required placeholder="https://api.openai.com">
77
+ <input type="hidden" id="target-url">
78
+ <div class="model-dropdown" id="provider-dropdown">
79
+ <div class="model-dropdown-trigger" id="provider-dropdown-trigger">
80
+ <span id="provider-dropdown-value">选择供应商...</span>
81
+ <span class="model-dropdown-arrow">&#9662;</span>
82
+ </div>
83
+ <div class="model-dropdown-menu" id="provider-dropdown-menu">
84
+ <div class="model-dropdown-options" id="provider-dropdown-options"></div>
85
+ <div class="model-add-section">
86
+ <input type="text" class="model-add-input" id="provider-add-name" placeholder="供应商名称">
87
+ <input type="url" class="model-add-input" id="provider-add-url" placeholder="https://api.example.com">
88
+ <button type="button" class="btn btn-primary btn-sm" id="provider-add-btn">添加</button>
89
+ </div>
90
+ </div>
91
+ </div>
78
92
  </div>
79
93
  <div class="form-group">
80
94
  <label>目标协议</label>
81
- <select id="target-protocol">
82
- <option value="openai">OpenAI</option>
83
- <option value="anthropic">Anthropic</option>
84
- </select>
95
+ <input type="text" id="target-protocol" readonly placeholder="自动识别" style="background:#1e293b;color:#94a3b8;cursor:not-allowed">
85
96
  </div>
86
97
  </div>
87
98
  <div class="form-row">
@@ -95,7 +106,7 @@
95
106
  </div>
96
107
  <div class="model-dropdown-menu" id="model-dropdown-menu">
97
108
  <div class="model-dropdown-options" id="model-dropdown-options"></div>
98
- <div class="model-add-section">
109
+ <div class="model-add-section" id="model-add-section">
99
110
  <input type="text" class="model-add-input" id="model-add-input" placeholder="输入模型名称">
100
111
  <button type="button" class="btn btn-primary btn-sm" id="model-add-btn">添加</button>
101
112
  </div>