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