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.
- package/lib/config-store.js +109 -19
- package/package.json +1 -1
- package/public/app.js +152 -232
- package/public/index.html +4 -3
- package/server.js +141 -29
package/lib/config-store.js
CHANGED
|
@@ -21,36 +21,76 @@ migrateOldConfig();
|
|
|
21
21
|
|
|
22
22
|
let configCache = null;
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
.
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
...
|
|
38
|
-
models:
|
|
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
|
-
|
|
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
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
|
|
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
|
-
}
|
|
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
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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');
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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="删除此供应商">×</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="删除此供应商">×</button>
|
|
148
110
|
</div>
|
|
149
|
-
`
|
|
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.
|
|
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
|
|
167
|
-
const p =
|
|
168
|
-
const ok = await showConfirm(`确定要删除供应商 <strong>${escapeHtml(p?.name ||
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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(
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
document.getElementById('target-protocol').value =
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
|
223
|
-
if (!
|
|
224
|
-
showToast('
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
-
|
|
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
|
|
250
|
-
const
|
|
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
|
-
|
|
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="删除此模型">×</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="删除此模型">×</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
|
-
|
|
281
|
-
if (
|
|
282
|
-
|
|
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
|
|
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 (
|
|
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
|
|
408
|
+
<td>${escapeHtml(p.providerName || p.providerUrl || '-')}</td>
|
|
454
409
|
<td>
|
|
455
|
-
<span class="badge" style="background:${
|
|
456
|
-
${
|
|
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(
|
|
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
|
-
|
|
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
|
|
543
|
-
if (!
|
|
544
|
-
showToast('
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
// ====================
|
|
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
|
+
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
|
-
|
|
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,
|
|
269
|
+
const { name, port, requireAuth, authToken, providerId, defaultModel } = req.body;
|
|
166
270
|
|
|
167
|
-
if (!name || !port || !
|
|
168
|
-
return res.status(400).json({ error: 'name, port and
|
|
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
|
-
|
|
292
|
+
providerId,
|
|
293
|
+
defaultModel: defaultModel || '',
|
|
187
294
|
});
|
|
188
295
|
|
|
189
296
|
try {
|
|
190
|
-
await
|
|
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 (
|
|
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
|
|
337
|
+
await startProxyWithProvider(updated);
|
|
228
338
|
} catch (err) {
|
|
229
339
|
return res.status(500).json({ error: `代理重启失败: ${err.message}` });
|
|
230
340
|
}
|
|
231
341
|
} else {
|
|
232
|
-
|
|
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
|
|
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
|
|
398
|
+
await startProxyWithProvider(proxy);
|
|
287
399
|
} catch (err) {
|
|
288
400
|
console.error(`[Init] Failed to start proxy ${proxy.name}:`, err.message);
|
|
289
401
|
}
|