protocol-proxy 2.0.1 → 2.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/config-store.js +47 -1
- package/package.json +1 -1
- package/public/app.js +60 -10
- package/public/index.html +1 -0
- package/server.js +7 -2
package/lib/config-store.js
CHANGED
|
@@ -21,6 +21,50 @@ migrateOldConfig();
|
|
|
21
21
|
|
|
22
22
|
let configCache = null;
|
|
23
23
|
|
|
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);
|
|
64
|
+
}
|
|
65
|
+
return config;
|
|
66
|
+
}
|
|
67
|
+
|
|
24
68
|
function normalizeModels(models) {
|
|
25
69
|
if (!Array.isArray(models)) return [];
|
|
26
70
|
return Array.from(new Set(
|
|
@@ -58,7 +102,9 @@ function loadConfig() {
|
|
|
58
102
|
return configCache;
|
|
59
103
|
}
|
|
60
104
|
const raw = fs.readFileSync(CONFIG_PATH, 'utf-8');
|
|
61
|
-
|
|
105
|
+
let config = normalizeConfig(JSON.parse(raw));
|
|
106
|
+
config = migrateTargetToProvider(config);
|
|
107
|
+
configCache = config;
|
|
62
108
|
return configCache;
|
|
63
109
|
} catch (err) {
|
|
64
110
|
console.error('加载配置失败:', err.message);
|
package/package.json
CHANGED
package/public/app.js
CHANGED
|
@@ -40,14 +40,19 @@ function initProviderDropdown() {
|
|
|
40
40
|
const dropdown = document.getElementById('provider-dropdown');
|
|
41
41
|
const addNameInput = document.getElementById('provider-add-name');
|
|
42
42
|
const addUrlInput = document.getElementById('provider-add-url');
|
|
43
|
+
const addKeyInput = document.getElementById('provider-add-key');
|
|
43
44
|
const addBtn = document.getElementById('provider-add-btn');
|
|
44
45
|
|
|
45
46
|
trigger.addEventListener('click', (e) => {
|
|
46
47
|
e.stopPropagation();
|
|
47
48
|
dropdown.classList.toggle('open');
|
|
48
49
|
if (dropdown.classList.contains('open')) {
|
|
50
|
+
editingProviderId = null;
|
|
49
51
|
addNameInput.value = '';
|
|
50
52
|
addUrlInput.value = '';
|
|
53
|
+
addKeyInput.value = '';
|
|
54
|
+
addUrlInput.disabled = false;
|
|
55
|
+
addBtn.textContent = '添加';
|
|
51
56
|
renderProviderOptions();
|
|
52
57
|
addNameInput.focus();
|
|
53
58
|
}
|
|
@@ -62,27 +67,45 @@ function initProviderDropdown() {
|
|
|
62
67
|
addBtn.addEventListener('click', async () => {
|
|
63
68
|
const name = addNameInput.value.trim();
|
|
64
69
|
const url = addUrlInput.value.trim();
|
|
70
|
+
const apiKey = addKeyInput.value.trim();
|
|
65
71
|
if (!name || !url) {
|
|
66
72
|
showToast('请填写供应商名称和地址', true);
|
|
67
73
|
return;
|
|
68
74
|
}
|
|
69
75
|
try {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
76
|
+
let res;
|
|
77
|
+
if (editingProviderId) {
|
|
78
|
+
// 更新模式
|
|
79
|
+
res = await fetch(`/api/providers/${editingProviderId}`, {
|
|
80
|
+
method: 'PUT',
|
|
81
|
+
headers: { 'Content-Type': 'application/json' },
|
|
82
|
+
body: JSON.stringify({ name, apiKey }),
|
|
83
|
+
});
|
|
84
|
+
} else {
|
|
85
|
+
// 新增模式
|
|
86
|
+
res = await fetch('/api/providers', {
|
|
87
|
+
method: 'POST',
|
|
88
|
+
headers: { 'Content-Type': 'application/json' },
|
|
89
|
+
body: JSON.stringify({ name, url, apiKey }),
|
|
90
|
+
});
|
|
91
|
+
}
|
|
75
92
|
if (!res.ok) {
|
|
76
93
|
const err = await res.json();
|
|
77
|
-
showToast(err.error || '
|
|
94
|
+
showToast(err.error || '操作失败', true);
|
|
78
95
|
return;
|
|
79
96
|
}
|
|
80
97
|
const provider = await res.json();
|
|
98
|
+
editingProviderId = null;
|
|
99
|
+
addUrlInput.disabled = false;
|
|
100
|
+
addBtn.textContent = '添加';
|
|
101
|
+
addNameInput.value = '';
|
|
102
|
+
addUrlInput.value = '';
|
|
103
|
+
addKeyInput.value = '';
|
|
81
104
|
await loadProviders();
|
|
82
105
|
selectProvider(provider.id);
|
|
83
|
-
|
|
106
|
+
renderProviderOptions();
|
|
84
107
|
} catch (err) {
|
|
85
|
-
showToast('
|
|
108
|
+
showToast('操作失败: ' + err.message, true);
|
|
86
109
|
}
|
|
87
110
|
});
|
|
88
111
|
|
|
@@ -95,6 +118,8 @@ function initProviderDropdown() {
|
|
|
95
118
|
});
|
|
96
119
|
}
|
|
97
120
|
|
|
121
|
+
let editingProviderId = null;
|
|
122
|
+
|
|
98
123
|
function renderProviderOptions() {
|
|
99
124
|
const container = document.getElementById('provider-dropdown-options');
|
|
100
125
|
const currentId = document.getElementById('provider-id').value;
|
|
@@ -103,7 +128,10 @@ function renderProviderOptions() {
|
|
|
103
128
|
<div class="model-option${p.id === currentId ? ' selected' : ''}" data-id="${escapeHtml(p.id)}">
|
|
104
129
|
<span class="model-option-name">${escapeHtml(p.name)}</span>
|
|
105
130
|
${p.name !== p.url ? `<span style="color:#64748b;font-size:12px;margin-left:4px">${escapeHtml(p.url)}</span>` : ''}
|
|
106
|
-
<
|
|
131
|
+
<span style="margin-left:auto;display:flex;gap:4px">
|
|
132
|
+
<button type="button" class="model-option-delete" data-edit-id="${escapeHtml(p.id)}" title="编辑此供应商" style="color:#60a5fa">×</button>
|
|
133
|
+
<button type="button" class="model-option-delete" data-delete-id="${escapeHtml(p.id)}" title="删除此供应商">×</button>
|
|
134
|
+
</span>
|
|
107
135
|
</div>
|
|
108
136
|
`).join('');
|
|
109
137
|
|
|
@@ -119,7 +147,29 @@ function renderProviderOptions() {
|
|
|
119
147
|
});
|
|
120
148
|
});
|
|
121
149
|
|
|
122
|
-
|
|
150
|
+
// 编辑供应商
|
|
151
|
+
container.querySelectorAll('[data-edit-id]').forEach(btn => {
|
|
152
|
+
btn.addEventListener('click', async (e) => {
|
|
153
|
+
e.stopPropagation();
|
|
154
|
+
const id = btn.dataset.editId;
|
|
155
|
+
try {
|
|
156
|
+
const res = await fetch(`/api/providers/${id}`);
|
|
157
|
+
if (!res.ok) throw new Error('加载失败');
|
|
158
|
+
const p = await res.json();
|
|
159
|
+
editingProviderId = id;
|
|
160
|
+
document.getElementById('provider-add-name').value = p.name;
|
|
161
|
+
document.getElementById('provider-add-url').value = p.url;
|
|
162
|
+
document.getElementById('provider-add-key').value = p.apiKey || '';
|
|
163
|
+
document.getElementById('provider-add-url').disabled = true;
|
|
164
|
+
document.getElementById('provider-add-btn').textContent = '更新';
|
|
165
|
+
} catch (err) {
|
|
166
|
+
showToast('加载供应商失败: ' + err.message, true);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// 删除供应商
|
|
172
|
+
container.querySelectorAll('[data-delete-id]').forEach(btn => {
|
|
123
173
|
btn.addEventListener('click', async (e) => {
|
|
124
174
|
e.stopPropagation();
|
|
125
175
|
const id = btn.dataset.deleteId;
|
package/public/index.html
CHANGED
|
@@ -85,6 +85,7 @@
|
|
|
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>
|
package/server.js
CHANGED
|
@@ -198,7 +198,7 @@ async function init() {
|
|
|
198
198
|
if (req.body.name !== undefined) updates.name = req.body.name;
|
|
199
199
|
if (req.body.url !== undefined) updates.url = req.body.url;
|
|
200
200
|
if (req.body.protocol !== undefined) updates.protocol = req.body.protocol;
|
|
201
|
-
if (req.body.apiKey !== undefined) updates.apiKey = req.body.apiKey;
|
|
201
|
+
if (req.body.apiKey !== undefined && req.body.apiKey !== '') updates.apiKey = req.body.apiKey;
|
|
202
202
|
if (req.body.models !== undefined) updates.models = req.body.models;
|
|
203
203
|
|
|
204
204
|
const updated = configStore.updateProvider(req.params.id, updates);
|
|
@@ -235,7 +235,12 @@ async function init() {
|
|
|
235
235
|
const proxies = configStore.getProxies().map(p => {
|
|
236
236
|
const provider = configStore.getProviderById(p.providerId);
|
|
237
237
|
return {
|
|
238
|
-
|
|
238
|
+
id: p.id,
|
|
239
|
+
name: p.name,
|
|
240
|
+
port: p.port,
|
|
241
|
+
requireAuth: p.requireAuth,
|
|
242
|
+
authToken: p.authToken,
|
|
243
|
+
providerId: p.providerId,
|
|
239
244
|
providerName: provider?.name || '',
|
|
240
245
|
providerUrl: provider?.url || '',
|
|
241
246
|
protocol: provider?.protocol || '',
|