protocol-proxy 2.6.0 → 2.7.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/lib/config-store.js +83 -2
- package/package.json +1 -1
- package/public/app.js +234 -1
- package/public/index.html +46 -0
- package/public/style.css +378 -65
- package/server.js +82 -0
package/lib/config-store.js
CHANGED
|
@@ -154,20 +154,35 @@ function normalizeConfig(config) {
|
|
|
154
154
|
function loadConfig() {
|
|
155
155
|
try {
|
|
156
156
|
if (!fs.existsSync(CONFIG_PATH)) {
|
|
157
|
-
configCache = { providers: [], proxies: [] };
|
|
157
|
+
configCache = { providers: [], proxies: [], settings: {} };
|
|
158
158
|
return configCache;
|
|
159
159
|
}
|
|
160
160
|
const raw = fs.readFileSync(CONFIG_PATH, 'utf-8');
|
|
161
161
|
let config = normalizeConfig(JSON.parse(raw));
|
|
162
162
|
config = migrateTargetToProvider(config);
|
|
163
|
+
if (!config.settings) config.settings = {};
|
|
163
164
|
configCache = config;
|
|
164
165
|
return configCache;
|
|
165
166
|
} catch (err) {
|
|
166
167
|
console.error('加载配置失败:', err.message);
|
|
167
|
-
return configCache || { providers: [], proxies: [] };
|
|
168
|
+
return configCache || { providers: [], proxies: [], settings: {} };
|
|
168
169
|
}
|
|
169
170
|
}
|
|
170
171
|
|
|
172
|
+
function getSettings() {
|
|
173
|
+
return loadConfig().settings || {};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function setSetting(key, value) {
|
|
177
|
+
const config = loadConfig();
|
|
178
|
+
if (!config.settings) config.settings = {};
|
|
179
|
+
config.settings[key] = value;
|
|
180
|
+
saveConfig(config);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const SNAPSHOT_DIR = path.join(os.homedir(), '.protocol-proxy', 'snapshots');
|
|
184
|
+
const MAX_SNAPSHOTS = 30;
|
|
185
|
+
|
|
171
186
|
function saveConfig(config) {
|
|
172
187
|
try {
|
|
173
188
|
const normalizedConfig = normalizeConfig(config);
|
|
@@ -186,6 +201,67 @@ function saveConfig(config) {
|
|
|
186
201
|
}
|
|
187
202
|
}
|
|
188
203
|
|
|
204
|
+
function saveSnapshot(reason) {
|
|
205
|
+
try {
|
|
206
|
+
if (!fs.existsSync(CONFIG_PATH)) return;
|
|
207
|
+
if (!fs.existsSync(SNAPSHOT_DIR)) fs.mkdirSync(SNAPSHOT_DIR, { recursive: true });
|
|
208
|
+
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
|
209
|
+
const name = `${ts}_${reason || 'save'}.json`;
|
|
210
|
+
fs.copyFileSync(CONFIG_PATH, path.join(SNAPSHOT_DIR, name));
|
|
211
|
+
// 清理旧快照
|
|
212
|
+
const files = fs.readdirSync(SNAPSHOT_DIR)
|
|
213
|
+
.filter(f => f.endsWith('.json'))
|
|
214
|
+
.sort();
|
|
215
|
+
while (files.length > MAX_SNAPSHOTS) {
|
|
216
|
+
const oldest = files.shift();
|
|
217
|
+
fs.unlinkSync(path.join(SNAPSHOT_DIR, oldest));
|
|
218
|
+
}
|
|
219
|
+
} catch (err) {
|
|
220
|
+
console.error('[Snapshot] 保存快照失败:', err.message);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function getSnapshots() {
|
|
225
|
+
try {
|
|
226
|
+
if (!fs.existsSync(SNAPSHOT_DIR)) return [];
|
|
227
|
+
return fs.readdirSync(SNAPSHOT_DIR)
|
|
228
|
+
.filter(f => f.endsWith('.json'))
|
|
229
|
+
.sort().reverse()
|
|
230
|
+
.map(f => {
|
|
231
|
+
const fullPath = path.join(SNAPSHOT_DIR, f);
|
|
232
|
+
const stat = fs.statSync(fullPath);
|
|
233
|
+
const name = f.replace('.json', '');
|
|
234
|
+
const [ts, ...reasonParts] = name.split('_');
|
|
235
|
+
return {
|
|
236
|
+
file: f,
|
|
237
|
+
timestamp: stat.mtime.toISOString(),
|
|
238
|
+
reason: reasonParts.join('_') || 'save',
|
|
239
|
+
size: stat.size,
|
|
240
|
+
};
|
|
241
|
+
});
|
|
242
|
+
} catch (err) {
|
|
243
|
+
console.error('[Snapshot] 读取快照列表失败:', err.message);
|
|
244
|
+
return [];
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function restoreSnapshot(file) {
|
|
249
|
+
try {
|
|
250
|
+
if (!/^[\w\-]+\.json$/.test(file)) return { error: '非法文件名' };
|
|
251
|
+
const snapshotPath = path.join(SNAPSHOT_DIR, file);
|
|
252
|
+
if (!fs.existsSync(snapshotPath)) return { error: '快照不存在' };
|
|
253
|
+
const content = fs.readFileSync(snapshotPath, 'utf-8');
|
|
254
|
+
const config = JSON.parse(content);
|
|
255
|
+
// 先对当前配置做快照,以便回滚本次操作
|
|
256
|
+
saveSnapshot('before-rollback');
|
|
257
|
+
saveConfig(config);
|
|
258
|
+
return { success: true };
|
|
259
|
+
} catch (err) {
|
|
260
|
+
console.error('[Snapshot] 恢复快照失败:', err.message);
|
|
261
|
+
return { error: err.message };
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
189
265
|
// ==================== 供应商 CRUD ====================
|
|
190
266
|
|
|
191
267
|
function getProviders() {
|
|
@@ -280,6 +356,11 @@ function removeProxy(id) {
|
|
|
280
356
|
module.exports = {
|
|
281
357
|
loadConfig,
|
|
282
358
|
saveConfig,
|
|
359
|
+
saveSnapshot,
|
|
360
|
+
getSnapshots,
|
|
361
|
+
restoreSnapshot,
|
|
362
|
+
getSettings,
|
|
363
|
+
setSetting,
|
|
283
364
|
getProviders,
|
|
284
365
|
getProviderById,
|
|
285
366
|
addProvider,
|
package/package.json
CHANGED
package/public/app.js
CHANGED
|
@@ -7,6 +7,49 @@ let statsRange = 'daily';
|
|
|
7
7
|
let statsProxyId = '';
|
|
8
8
|
let providerPoolItems = [];
|
|
9
9
|
|
|
10
|
+
// ==================== 主题切换 ====================
|
|
11
|
+
|
|
12
|
+
const THEMES = [
|
|
13
|
+
{ id: 'dark', icon: '☾', label: '深色' },
|
|
14
|
+
{ id: 'light', icon: '☀', label: '浅色' },
|
|
15
|
+
{ id: 'pure-black', icon: '●', label: '纯黑' },
|
|
16
|
+
{ id: 'neon', icon: '⚡', label: '霓虹' },
|
|
17
|
+
{ id: 'amber', icon: '◈', label: '琥珀' },
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
function applyTheme(theme) {
|
|
21
|
+
const t = THEMES.find(t => t.id === theme) || THEMES[0];
|
|
22
|
+
document.documentElement.setAttribute('data-theme', t.id);
|
|
23
|
+
const icon = document.getElementById('theme-icon');
|
|
24
|
+
const label = document.getElementById('theme-label');
|
|
25
|
+
if (icon) icon.textContent = t.icon;
|
|
26
|
+
if (label) label.textContent = t.label;
|
|
27
|
+
localStorage.setItem('theme', t.id);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function toggleTheme() {
|
|
31
|
+
const current = document.documentElement.getAttribute('data-theme') || 'dark';
|
|
32
|
+
const idx = THEMES.findIndex(t => t.id === current);
|
|
33
|
+
const next = THEMES[(idx + 1) % THEMES.length];
|
|
34
|
+
applyTheme(next.id);
|
|
35
|
+
fetch('/api/settings', {
|
|
36
|
+
method: 'PUT',
|
|
37
|
+
headers: { 'Content-Type': 'application/json' },
|
|
38
|
+
body: JSON.stringify({ theme: next.id }),
|
|
39
|
+
}).catch(() => {});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// 初始化主题:优先服务端,fallback 到 localStorage
|
|
43
|
+
(async () => {
|
|
44
|
+
try {
|
|
45
|
+
const res = await fetch('/api/settings');
|
|
46
|
+
const settings = await res.json();
|
|
47
|
+
applyTheme(settings.theme || localStorage.getItem('theme') || 'dark');
|
|
48
|
+
} catch {
|
|
49
|
+
applyTheme(localStorage.getItem('theme') || 'dark');
|
|
50
|
+
}
|
|
51
|
+
})();
|
|
52
|
+
|
|
10
53
|
// ==================== 数据加载 ====================
|
|
11
54
|
|
|
12
55
|
async function loadProxies() {
|
|
@@ -1036,6 +1079,33 @@ async function init() {
|
|
|
1036
1079
|
// 初始状态:根据当前协议值决定 Azure 字段显示
|
|
1037
1080
|
const initProto = document.getElementById('target-protocol').value;
|
|
1038
1081
|
document.getElementById('azure-fields').style.display = initProto === 'openai' ? '' : 'none';
|
|
1082
|
+
|
|
1083
|
+
// 快捷键
|
|
1084
|
+
document.addEventListener('keydown', (e) => {
|
|
1085
|
+
// Esc 关闭最上层弹窗
|
|
1086
|
+
if (e.key === 'Escape') {
|
|
1087
|
+
if (document.getElementById('confirm-modal').classList.contains('active')) return;
|
|
1088
|
+
if (document.getElementById('log-modal').classList.contains('active')) { closeLogViewer(); return; }
|
|
1089
|
+
if (document.getElementById('history-modal').classList.contains('active')) { closeHistoryViewer(); return; }
|
|
1090
|
+
if (document.getElementById('test-result-modal').classList.contains('active')) { document.getElementById('test-result-modal').classList.remove('active'); return; }
|
|
1091
|
+
if (document.getElementById('import-modal').classList.contains('active')) { closeImportModal(); return; }
|
|
1092
|
+
if (document.getElementById('modal').classList.contains('active')) { closeModal(); return; }
|
|
1093
|
+
}
|
|
1094
|
+
// Ctrl+S 保存表单
|
|
1095
|
+
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
|
|
1096
|
+
if (document.getElementById('modal').classList.contains('active')) {
|
|
1097
|
+
e.preventDefault();
|
|
1098
|
+
document.getElementById('proxy-form').requestSubmit();
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
// Ctrl+N 新建代理
|
|
1102
|
+
if ((e.ctrlKey || e.metaKey) && e.key === 'n') {
|
|
1103
|
+
if (!document.getElementById('modal').classList.contains('active')) {
|
|
1104
|
+
e.preventDefault();
|
|
1105
|
+
openModal();
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
});
|
|
1039
1109
|
}
|
|
1040
1110
|
|
|
1041
1111
|
// ==================== 代理地址复制 ====================
|
|
@@ -1090,6 +1160,149 @@ function showToast(msg, isError) {
|
|
|
1090
1160
|
setTimeout(() => toast.remove(), 2000);
|
|
1091
1161
|
}
|
|
1092
1162
|
|
|
1163
|
+
// ==================== 批量操作 ====================
|
|
1164
|
+
|
|
1165
|
+
async function startAllProxies() {
|
|
1166
|
+
try {
|
|
1167
|
+
const res = await fetch('/api/proxies/start-all', { method: 'POST' });
|
|
1168
|
+
const data = await res.json();
|
|
1169
|
+
await loadProxies();
|
|
1170
|
+
const started = data.results.filter(r => r.success).length;
|
|
1171
|
+
const skipped = data.results.filter(r => r.skipped).length;
|
|
1172
|
+
const failed = data.results.filter(r => !r.success && !r.skipped).length;
|
|
1173
|
+
let msg = `启动完成:${started} 个启动`;
|
|
1174
|
+
if (skipped > 0) msg += `,${skipped} 个已在运行`;
|
|
1175
|
+
if (failed > 0) msg += `,${failed} 个失败`;
|
|
1176
|
+
showToast(msg, failed > 0);
|
|
1177
|
+
} catch (err) {
|
|
1178
|
+
showToast('批量启动失败: ' + err.message, true);
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
async function stopAllProxies() {
|
|
1183
|
+
const ok = await showConfirm('确定要停止所有运行中的代理吗?', '全部停止');
|
|
1184
|
+
if (!ok) return;
|
|
1185
|
+
try {
|
|
1186
|
+
const res = await fetch('/api/proxies/stop-all', { method: 'POST' });
|
|
1187
|
+
const data = await res.json();
|
|
1188
|
+
await loadProxies();
|
|
1189
|
+
showToast(`已停止 ${data.results.length} 个代理`);
|
|
1190
|
+
} catch (err) {
|
|
1191
|
+
showToast('批量停止失败: ' + err.message, true);
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
// ==================== 日志查看 ====================
|
|
1196
|
+
|
|
1197
|
+
async function openLogViewer() {
|
|
1198
|
+
document.getElementById('log-modal').classList.add('active');
|
|
1199
|
+
await loadLogs();
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
function closeLogViewer() {
|
|
1203
|
+
document.getElementById('log-modal').classList.remove('active');
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
async function loadLogs() {
|
|
1207
|
+
const container = document.getElementById('log-content');
|
|
1208
|
+
const lines = document.getElementById('log-lines-select').value;
|
|
1209
|
+
container.textContent = '加载中...';
|
|
1210
|
+
try {
|
|
1211
|
+
const res = await fetch(`/api/logs?lines=${lines}`);
|
|
1212
|
+
const data = await res.json();
|
|
1213
|
+
document.getElementById('log-total').textContent = data.total ? `(共 ${data.total} 行)` : '';
|
|
1214
|
+
if (!data.lines || data.lines.length === 0) {
|
|
1215
|
+
container.textContent = '暂无日志';
|
|
1216
|
+
return;
|
|
1217
|
+
}
|
|
1218
|
+
container.innerHTML = data.lines.map(line => {
|
|
1219
|
+
let cls = 'log-line';
|
|
1220
|
+
if (/error|fail|失败/i.test(line)) cls += ' log-error';
|
|
1221
|
+
else if (/warn|警告/i.test(line)) cls += ' log-warn';
|
|
1222
|
+
return `<div class="${cls}">${escapeHtml(line)}</div>`;
|
|
1223
|
+
}).join('');
|
|
1224
|
+
container.scrollTop = container.scrollHeight;
|
|
1225
|
+
} catch (err) {
|
|
1226
|
+
container.textContent = '加载失败: ' + err.message;
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
// ==================== 版本历史 ====================
|
|
1231
|
+
|
|
1232
|
+
async function openHistoryViewer() {
|
|
1233
|
+
document.getElementById('history-modal').classList.add('active');
|
|
1234
|
+
await loadHistory();
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
function closeHistoryViewer() {
|
|
1238
|
+
document.getElementById('history-modal').classList.remove('active');
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
async function loadHistory() {
|
|
1242
|
+
const container = document.getElementById('history-content');
|
|
1243
|
+
container.textContent = '加载中...';
|
|
1244
|
+
try {
|
|
1245
|
+
const res = await fetch('/api/config/history');
|
|
1246
|
+
const data = await res.json();
|
|
1247
|
+
if (!data.snapshots || data.snapshots.length === 0) {
|
|
1248
|
+
container.innerHTML = '<div class="empty">暂无历史版本</div>';
|
|
1249
|
+
return;
|
|
1250
|
+
}
|
|
1251
|
+
const REASON_LABELS = {
|
|
1252
|
+
'create-proxy': '创建代理',
|
|
1253
|
+
'update-proxy': '更新代理',
|
|
1254
|
+
'delete-proxy': '删除代理',
|
|
1255
|
+
'import-merge': '导入配置(合并)',
|
|
1256
|
+
'import-overwrite': '导入配置(覆盖)',
|
|
1257
|
+
'before-rollback': '回滚前备份',
|
|
1258
|
+
'save': '保存',
|
|
1259
|
+
};
|
|
1260
|
+
container.innerHTML = `<div class="history-list">` + data.snapshots.map(s => {
|
|
1261
|
+
const date = new Date(s.timestamp);
|
|
1262
|
+
const timeStr = date.toLocaleString('zh-CN', { hour12: false });
|
|
1263
|
+
const label = REASON_LABELS[s.reason] || s.reason;
|
|
1264
|
+
const sizeStr = s.size > 1024 ? `${(s.size / 1024).toFixed(1)} KB` : `${s.size} B`;
|
|
1265
|
+
return `
|
|
1266
|
+
<div class="history-item">
|
|
1267
|
+
<div class="history-info">
|
|
1268
|
+
<span class="history-time">${timeStr}</span>
|
|
1269
|
+
<span class="history-reason">${escapeHtml(label)}</span>
|
|
1270
|
+
<span class="history-size">${sizeStr}</span>
|
|
1271
|
+
</div>
|
|
1272
|
+
<button class="btn btn-sm history-rollback-btn" data-file="${escapeHtml(s.file)}">恢复</button>
|
|
1273
|
+
</div>
|
|
1274
|
+
`;
|
|
1275
|
+
}).join('') + '</div>';
|
|
1276
|
+
container.querySelectorAll('.history-rollback-btn').forEach(btn => {
|
|
1277
|
+
btn.addEventListener('click', () => rollbackToSnapshot(btn.dataset.file));
|
|
1278
|
+
});
|
|
1279
|
+
} catch (err) {
|
|
1280
|
+
container.textContent = '加载失败: ' + err.message;
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
async function rollbackToSnapshot(file) {
|
|
1285
|
+
const ok = await showConfirm('确认恢复到此版本?<br>当前配置会先自动备份。', '确认恢复');
|
|
1286
|
+
if (!ok) return;
|
|
1287
|
+
try {
|
|
1288
|
+
const res = await fetch('/api/config/rollback', {
|
|
1289
|
+
method: 'POST',
|
|
1290
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1291
|
+
body: JSON.stringify({ file }),
|
|
1292
|
+
});
|
|
1293
|
+
const data = await res.json();
|
|
1294
|
+
if (!res.ok) {
|
|
1295
|
+
showToast(data.error || '恢复失败', true);
|
|
1296
|
+
return;
|
|
1297
|
+
}
|
|
1298
|
+
closeHistoryViewer();
|
|
1299
|
+
await Promise.all([loadProxies(), loadProviders()]);
|
|
1300
|
+
showToast('已恢复到历史版本');
|
|
1301
|
+
} catch (err) {
|
|
1302
|
+
showToast('恢复失败: ' + err.message, true);
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1093
1306
|
// ==================== 渲染代理列表 ====================
|
|
1094
1307
|
|
|
1095
1308
|
const ROUTING_LABELS = {
|
|
@@ -1099,14 +1312,34 @@ const ROUTING_LABELS = {
|
|
|
1099
1312
|
fastest: '最快优先',
|
|
1100
1313
|
};
|
|
1101
1314
|
|
|
1315
|
+
function getFilteredProxies() {
|
|
1316
|
+
const q = (document.getElementById('proxy-search-input')?.value || '').trim().toLowerCase();
|
|
1317
|
+
if (!q) return proxies;
|
|
1318
|
+
return proxies.filter(p => {
|
|
1319
|
+
const name = (p.name || '').toLowerCase();
|
|
1320
|
+
const port = String(p.port || '');
|
|
1321
|
+
const provider = (p.providerName || '').toLowerCase();
|
|
1322
|
+
return name.includes(q) || port.includes(q) || provider.includes(q);
|
|
1323
|
+
});
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
function filterProxies() {
|
|
1327
|
+
renderProxies();
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1102
1330
|
function renderProxies() {
|
|
1103
1331
|
const container = document.getElementById('proxy-list');
|
|
1332
|
+
const list = getFilteredProxies();
|
|
1104
1333
|
if (proxies.length === 0) {
|
|
1105
1334
|
container.innerHTML = '<div class="empty">暂无代理配置,点击右上角创建</div>';
|
|
1106
1335
|
return;
|
|
1107
1336
|
}
|
|
1337
|
+
if (list.length === 0) {
|
|
1338
|
+
container.innerHTML = '<div class="empty">没有匹配的代理</div>';
|
|
1339
|
+
return;
|
|
1340
|
+
}
|
|
1108
1341
|
|
|
1109
|
-
container.innerHTML =
|
|
1342
|
+
container.innerHTML = list.map(p => {
|
|
1110
1343
|
// Build unified provider rows: primary first, then pool entries
|
|
1111
1344
|
const primaryRow = {
|
|
1112
1345
|
name: p.providerName || p.providerUrl || '-',
|
package/public/index.html
CHANGED
|
@@ -8,6 +8,10 @@
|
|
|
8
8
|
</head>
|
|
9
9
|
<body>
|
|
10
10
|
<div class="container">
|
|
11
|
+
<button class="theme-toggle" id="theme-toggle" onclick="toggleTheme()" title="切换主题">
|
|
12
|
+
<span id="theme-icon">☾</span>
|
|
13
|
+
<span id="theme-label" class="theme-label">深色</span>
|
|
14
|
+
</button>
|
|
11
15
|
<header>
|
|
12
16
|
<h1>Protocol Proxy</h1>
|
|
13
17
|
<p>OpenAI / Anthropic 协议转换透明代理</p>
|
|
@@ -79,9 +83,20 @@
|
|
|
79
83
|
<button class="btn" onclick="exportConfig()">导出配置</button>
|
|
80
84
|
<button class="btn" onclick="document.getElementById('import-file').click()">导入配置</button>
|
|
81
85
|
<input type="file" id="import-file" accept=".json" style="display:none" onchange="handleImportFile(event)">
|
|
86
|
+
<button class="btn" onclick="openHistoryViewer()">版本历史</button>
|
|
82
87
|
<button class="btn btn-primary" onclick="openModal()">+ 新建代理</button>
|
|
83
88
|
</div>
|
|
84
89
|
</div>
|
|
90
|
+
<div class="proxy-toolbar">
|
|
91
|
+
<div class="proxy-search">
|
|
92
|
+
<input type="text" id="proxy-search-input" placeholder="搜索代理名称、端口、供应商..." oninput="filterProxies()">
|
|
93
|
+
</div>
|
|
94
|
+
<div class="proxy-toolbar-actions">
|
|
95
|
+
<button class="btn btn-sm" onclick="startAllProxies()">全部启动</button>
|
|
96
|
+
<button class="btn btn-sm" onclick="stopAllProxies()">全部停止</button>
|
|
97
|
+
<button class="btn btn-sm" onclick="openLogViewer()">日志</button>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
85
100
|
<div id="proxy-list" class="proxy-list">
|
|
86
101
|
<div class="empty">加载中...</div>
|
|
87
102
|
</div>
|
|
@@ -285,6 +300,37 @@
|
|
|
285
300
|
</div>
|
|
286
301
|
</div>
|
|
287
302
|
|
|
303
|
+
<!-- 日志查看弹窗 -->
|
|
304
|
+
<div class="modal" id="log-modal">
|
|
305
|
+
<div class="modal-content" style="max-width:800px">
|
|
306
|
+
<div class="modal-header">
|
|
307
|
+
<h3>运行日志 <span id="log-total" style="color:#64748b;font-size:0.8rem;font-weight:400"></span></h3>
|
|
308
|
+
<button class="btn-close" onclick="closeLogViewer()">×</button>
|
|
309
|
+
</div>
|
|
310
|
+
<div class="log-toolbar">
|
|
311
|
+
<select id="log-lines-select" onchange="loadLogs()">
|
|
312
|
+
<option value="100">最近 100 行</option>
|
|
313
|
+
<option value="200" selected>最近 200 行</option>
|
|
314
|
+
<option value="500">最近 500 行</option>
|
|
315
|
+
<option value="1000">最近 1000 行</option>
|
|
316
|
+
</select>
|
|
317
|
+
<button class="btn btn-sm" onclick="loadLogs()">刷新</button>
|
|
318
|
+
</div>
|
|
319
|
+
<div class="log-content" id="log-content">加载中...</div>
|
|
320
|
+
</div>
|
|
321
|
+
</div>
|
|
322
|
+
|
|
323
|
+
<!-- 版本历史弹窗 -->
|
|
324
|
+
<div class="modal" id="history-modal">
|
|
325
|
+
<div class="modal-content" style="max-width:600px">
|
|
326
|
+
<div class="modal-header">
|
|
327
|
+
<h3>配置版本历史</h3>
|
|
328
|
+
<button class="btn-close" onclick="closeHistoryViewer()">×</button>
|
|
329
|
+
</div>
|
|
330
|
+
<div class="history-content" id="history-content">加载中...</div>
|
|
331
|
+
</div>
|
|
332
|
+
</div>
|
|
333
|
+
|
|
288
334
|
<!-- 导入预览弹窗 -->
|
|
289
335
|
<div class="modal" id="import-modal">
|
|
290
336
|
<div class="modal-content" style="max-width:500px">
|
package/public/style.css
CHANGED
|
@@ -1,9 +1,114 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
--bg-body: #030712;
|
|
3
|
+
--bg-card: rgba(15, 23, 42, 0.5);
|
|
4
|
+
--bg-surface: rgba(6, 8, 15, 0.6);
|
|
5
|
+
--bg-surface-alt: rgba(15, 23, 42, 0.4);
|
|
6
|
+
--bg-elevated: rgba(15, 23, 42, 0.7);
|
|
7
|
+
--bg-dropdown: rgba(15, 23, 42, 0.95);
|
|
8
|
+
--text-primary: #f1f5f9;
|
|
9
|
+
--text-secondary: #e2e8f0;
|
|
10
|
+
--text-muted: #94a3b8;
|
|
11
|
+
--text-dim: #64748b;
|
|
12
|
+
--text-faint: #475569;
|
|
13
|
+
--border-main: rgba(51, 65, 85, 0.4);
|
|
14
|
+
--border-light: rgba(51, 65, 85, 0.35);
|
|
15
|
+
--border-input: rgba(51, 65, 85, 0.5);
|
|
16
|
+
--grid-opacity: 0.03;
|
|
17
|
+
--card-shadow: 0 4px 24px rgba(0, 0, 0, 0.3);
|
|
18
|
+
--accent: #60a5fa;
|
|
19
|
+
--accent-dim: #3b82f6;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
[data-theme="light"] {
|
|
23
|
+
--bg-body: #f1f5f9;
|
|
24
|
+
--bg-card: rgba(255, 255, 255, 0.85);
|
|
25
|
+
--bg-surface: rgba(241, 245, 249, 0.8);
|
|
26
|
+
--bg-surface-alt: rgba(241, 245, 249, 0.6);
|
|
27
|
+
--bg-elevated: rgba(255, 255, 255, 0.92);
|
|
28
|
+
--bg-dropdown: rgba(255, 255, 255, 0.97);
|
|
29
|
+
--text-primary: #0f172a;
|
|
30
|
+
--text-secondary: #1e293b;
|
|
31
|
+
--text-muted: #475569;
|
|
32
|
+
--text-dim: #64748b;
|
|
33
|
+
--text-faint: #94a3b8;
|
|
34
|
+
--border-main: rgba(203, 213, 225, 0.6);
|
|
35
|
+
--border-light: rgba(203, 213, 225, 0.5);
|
|
36
|
+
--border-input: rgba(203, 213, 225, 0.7);
|
|
37
|
+
--grid-opacity: 0.4;
|
|
38
|
+
--card-shadow: 0 4px 24px rgba(0, 0, 0, 0.08);
|
|
39
|
+
--accent: #2563eb;
|
|
40
|
+
--accent-dim: #1d4ed8;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
[data-theme="pure-black"] {
|
|
44
|
+
--bg-body: #000000;
|
|
45
|
+
--bg-card: rgba(10, 10, 10, 0.7);
|
|
46
|
+
--bg-surface: rgba(5, 5, 5, 0.8);
|
|
47
|
+
--bg-surface-alt: rgba(10, 10, 10, 0.5);
|
|
48
|
+
--bg-elevated: rgba(10, 10, 10, 0.85);
|
|
49
|
+
--bg-dropdown: rgba(10, 10, 10, 0.97);
|
|
50
|
+
--text-primary: #e5e5e5;
|
|
51
|
+
--text-secondary: #d4d4d4;
|
|
52
|
+
--text-muted: #a3a3a3;
|
|
53
|
+
--text-dim: #737373;
|
|
54
|
+
--text-faint: #525252;
|
|
55
|
+
--border-main: rgba(64, 64, 64, 0.4);
|
|
56
|
+
--border-light: rgba(64, 64, 64, 0.3);
|
|
57
|
+
--border-input: rgba(64, 64, 64, 0.5);
|
|
58
|
+
--grid-opacity: 0.02;
|
|
59
|
+
--card-shadow: 0 4px 24px rgba(0, 0, 0, 0.5);
|
|
60
|
+
--accent: #a3a3a3;
|
|
61
|
+
--accent-dim: #737373;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
[data-theme="neon"] {
|
|
65
|
+
--bg-body: #0a0a1a;
|
|
66
|
+
--bg-card: rgba(10, 10, 30, 0.6);
|
|
67
|
+
--bg-surface: rgba(10, 10, 30, 0.7);
|
|
68
|
+
--bg-surface-alt: rgba(15, 15, 40, 0.4);
|
|
69
|
+
--bg-elevated: rgba(10, 10, 30, 0.8);
|
|
70
|
+
--bg-dropdown: rgba(10, 10, 30, 0.95);
|
|
71
|
+
--text-primary: #e0f7ff;
|
|
72
|
+
--text-secondary: #c5e8f5;
|
|
73
|
+
--text-muted: #7ec8e8;
|
|
74
|
+
--text-dim: #4a9ab5;
|
|
75
|
+
--text-faint: #2d6a80;
|
|
76
|
+
--border-main: rgba(0, 200, 255, 0.2);
|
|
77
|
+
--border-light: rgba(0, 200, 255, 0.15);
|
|
78
|
+
--border-input: rgba(0, 200, 255, 0.25);
|
|
79
|
+
--grid-opacity: 0.06;
|
|
80
|
+
--card-shadow: 0 4px 24px rgba(0, 200, 255, 0.08);
|
|
81
|
+
--accent: #00d4ff;
|
|
82
|
+
--accent-dim: #0099cc;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
[data-theme="amber"] {
|
|
86
|
+
--bg-body: #0f0a05;
|
|
87
|
+
--bg-card: rgba(20, 15, 8, 0.6);
|
|
88
|
+
--bg-surface: rgba(15, 10, 5, 0.7);
|
|
89
|
+
--bg-surface-alt: rgba(20, 15, 8, 0.45);
|
|
90
|
+
--bg-elevated: rgba(20, 15, 8, 0.8);
|
|
91
|
+
--bg-dropdown: rgba(20, 15, 8, 0.95);
|
|
92
|
+
--text-primary: #fef3c7;
|
|
93
|
+
--text-secondary: #fde68a;
|
|
94
|
+
--text-muted: #c8a55a;
|
|
95
|
+
--text-dim: #92703a;
|
|
96
|
+
--text-faint: #6b5025;
|
|
97
|
+
--border-main: rgba(180, 130, 50, 0.25);
|
|
98
|
+
--border-light: rgba(180, 130, 50, 0.18);
|
|
99
|
+
--border-input: rgba(180, 130, 50, 0.3);
|
|
100
|
+
--grid-opacity: 0.04;
|
|
101
|
+
--card-shadow: 0 4px 24px rgba(180, 130, 50, 0.06);
|
|
102
|
+
--accent: #f59e0b;
|
|
103
|
+
--accent-dim: #d97706;
|
|
104
|
+
}
|
|
105
|
+
|
|
1
106
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
2
107
|
|
|
3
108
|
body {
|
|
4
109
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif;
|
|
5
|
-
background:
|
|
6
|
-
color:
|
|
110
|
+
background: var(--bg-body);
|
|
111
|
+
color: var(--text-secondary);
|
|
7
112
|
line-height: 1.6;
|
|
8
113
|
min-height: 100vh;
|
|
9
114
|
}
|
|
@@ -14,8 +119,8 @@ body::before {
|
|
|
14
119
|
position: fixed;
|
|
15
120
|
top: 0; left: 0; right: 0; bottom: 0;
|
|
16
121
|
background-image:
|
|
17
|
-
linear-gradient(rgba(59, 130, 246,
|
|
18
|
-
linear-gradient(90deg, rgba(59, 130, 246,
|
|
122
|
+
linear-gradient(rgba(59, 130, 246, var(--grid-opacity)) 1px, transparent 1px),
|
|
123
|
+
linear-gradient(90deg, rgba(59, 130, 246, var(--grid-opacity)) 1px, transparent 1px);
|
|
19
124
|
background-size: 60px 60px;
|
|
20
125
|
pointer-events: none;
|
|
21
126
|
z-index: 0;
|
|
@@ -29,6 +134,39 @@ body::before {
|
|
|
29
134
|
padding: 40px 20px;
|
|
30
135
|
}
|
|
31
136
|
|
|
137
|
+
/* Theme toggle */
|
|
138
|
+
.theme-toggle {
|
|
139
|
+
position: fixed;
|
|
140
|
+
top: 20px;
|
|
141
|
+
right: 20px;
|
|
142
|
+
z-index: 100;
|
|
143
|
+
height: 36px;
|
|
144
|
+
padding: 0 14px 0 10px;
|
|
145
|
+
border-radius: 10px;
|
|
146
|
+
border: 1px solid var(--border-input);
|
|
147
|
+
background: var(--bg-card);
|
|
148
|
+
backdrop-filter: blur(12px);
|
|
149
|
+
color: var(--text-muted);
|
|
150
|
+
font-size: 1rem;
|
|
151
|
+
cursor: pointer;
|
|
152
|
+
display: flex;
|
|
153
|
+
align-items: center;
|
|
154
|
+
gap: 6px;
|
|
155
|
+
transition: all 0.25s;
|
|
156
|
+
white-space: nowrap;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.theme-label {
|
|
160
|
+
font-size: 0.78rem;
|
|
161
|
+
font-weight: 500;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.theme-toggle:hover {
|
|
165
|
+
border-color: var(--accent);
|
|
166
|
+
color: var(--accent);
|
|
167
|
+
box-shadow: 0 0 12px rgba(59, 130, 246, 0.15);
|
|
168
|
+
}
|
|
169
|
+
|
|
32
170
|
/* Header */
|
|
33
171
|
header {
|
|
34
172
|
text-align: center;
|
|
@@ -62,9 +200,9 @@ header p {
|
|
|
62
200
|
}
|
|
63
201
|
|
|
64
202
|
.stat-item {
|
|
65
|
-
background:
|
|
203
|
+
background: var(--bg-surface);
|
|
66
204
|
backdrop-filter: blur(12px);
|
|
67
|
-
border: 1px solid
|
|
205
|
+
border: 1px solid var(--border-input);
|
|
68
206
|
border-radius: 16px;
|
|
69
207
|
padding: 24px 48px;
|
|
70
208
|
text-align: center;
|
|
@@ -93,7 +231,7 @@ header p {
|
|
|
93
231
|
display: block;
|
|
94
232
|
font-size: 2.2rem;
|
|
95
233
|
font-weight: 700;
|
|
96
|
-
color:
|
|
234
|
+
color: var(--accent);
|
|
97
235
|
font-family: 'Cascadia Code', 'Fira Code', 'Consolas', monospace;
|
|
98
236
|
}
|
|
99
237
|
|
|
@@ -107,12 +245,12 @@ header p {
|
|
|
107
245
|
|
|
108
246
|
/* Card */
|
|
109
247
|
.card {
|
|
110
|
-
background:
|
|
248
|
+
background: var(--bg-card);
|
|
111
249
|
backdrop-filter: blur(16px);
|
|
112
|
-
border: 1px solid
|
|
250
|
+
border: 1px solid var(--border-main);
|
|
113
251
|
border-radius: 20px;
|
|
114
252
|
overflow: hidden;
|
|
115
|
-
box-shadow:
|
|
253
|
+
box-shadow: var(--card-shadow);
|
|
116
254
|
}
|
|
117
255
|
|
|
118
256
|
.card-header {
|
|
@@ -120,7 +258,7 @@ header p {
|
|
|
120
258
|
justify-content: space-between;
|
|
121
259
|
align-items: center;
|
|
122
260
|
padding: 24px 28px;
|
|
123
|
-
border-bottom: 1px solid
|
|
261
|
+
border-bottom: 1px solid var(--border-main);
|
|
124
262
|
}
|
|
125
263
|
|
|
126
264
|
.card-header h2 {
|
|
@@ -152,14 +290,13 @@ header p {
|
|
|
152
290
|
}
|
|
153
291
|
|
|
154
292
|
.btn-primary {
|
|
155
|
-
background: linear-gradient(135deg,
|
|
293
|
+
background: linear-gradient(135deg, var(--accent), var(--accent-dim));
|
|
156
294
|
border-color: transparent;
|
|
157
295
|
color: white;
|
|
158
296
|
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.25);
|
|
159
297
|
}
|
|
160
298
|
|
|
161
299
|
.btn-primary:hover {
|
|
162
|
-
background: linear-gradient(135deg, #4b92ff, #3573fb);
|
|
163
300
|
box-shadow: 0 4px 16px rgba(59, 130, 246, 0.4);
|
|
164
301
|
transform: translateY(-1px);
|
|
165
302
|
}
|
|
@@ -193,6 +330,47 @@ header p {
|
|
|
193
330
|
box-shadow: 0 4px 16px rgba(34, 197, 94, 0.35);
|
|
194
331
|
}
|
|
195
332
|
|
|
333
|
+
/* Proxy toolbar */
|
|
334
|
+
.proxy-toolbar {
|
|
335
|
+
display: flex;
|
|
336
|
+
align-items: center;
|
|
337
|
+
gap: 12px;
|
|
338
|
+
padding: 14px 28px;
|
|
339
|
+
border-bottom: 1px solid var(--border-light);
|
|
340
|
+
background: var(--bg-surface-alt);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
.proxy-search {
|
|
344
|
+
flex: 1;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
.proxy-search input {
|
|
348
|
+
width: 100%;
|
|
349
|
+
padding: 8px 14px;
|
|
350
|
+
background: var(--bg-surface);
|
|
351
|
+
border: 1px solid var(--border-input);
|
|
352
|
+
border-radius: 8px;
|
|
353
|
+
color: var(--text-secondary);
|
|
354
|
+
font-size: 0.85rem;
|
|
355
|
+
transition: all 0.2s;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
.proxy-search input::placeholder {
|
|
359
|
+
color: #475569;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
.proxy-search input:focus {
|
|
363
|
+
outline: none;
|
|
364
|
+
border-color: rgba(59, 130, 246, 0.5);
|
|
365
|
+
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.1);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
.proxy-toolbar-actions {
|
|
369
|
+
display: flex;
|
|
370
|
+
gap: 6px;
|
|
371
|
+
flex-shrink: 0;
|
|
372
|
+
}
|
|
373
|
+
|
|
196
374
|
/* Proxy list */
|
|
197
375
|
.proxy-list {
|
|
198
376
|
padding: 20px;
|
|
@@ -201,7 +379,7 @@ header p {
|
|
|
201
379
|
.empty {
|
|
202
380
|
text-align: center;
|
|
203
381
|
padding: 80px 20px;
|
|
204
|
-
color:
|
|
382
|
+
color: var(--text-faint);
|
|
205
383
|
font-size: 0.95rem;
|
|
206
384
|
}
|
|
207
385
|
|
|
@@ -209,15 +387,15 @@ header p {
|
|
|
209
387
|
content: '◈';
|
|
210
388
|
display: block;
|
|
211
389
|
font-size: 3rem;
|
|
212
|
-
color:
|
|
390
|
+
color: var(--text-faint);
|
|
213
391
|
margin-bottom: 16px;
|
|
214
392
|
}
|
|
215
393
|
|
|
216
394
|
/* Proxy Item */
|
|
217
395
|
.proxy-item {
|
|
218
|
-
background:
|
|
396
|
+
background: var(--bg-surface);
|
|
219
397
|
backdrop-filter: blur(12px);
|
|
220
|
-
border: 1px solid
|
|
398
|
+
border: 1px solid var(--border-light);
|
|
221
399
|
border-radius: 16px;
|
|
222
400
|
padding: 24px;
|
|
223
401
|
margin-bottom: 16px;
|
|
@@ -355,7 +533,7 @@ header p {
|
|
|
355
533
|
gap: 10px;
|
|
356
534
|
margin-top: 18px;
|
|
357
535
|
padding-top: 18px;
|
|
358
|
-
border-top: 1px solid
|
|
536
|
+
border-top: 1px solid var(--border-light);
|
|
359
537
|
}
|
|
360
538
|
|
|
361
539
|
/* Modal */
|
|
@@ -380,9 +558,9 @@ header p {
|
|
|
380
558
|
}
|
|
381
559
|
|
|
382
560
|
.modal-content {
|
|
383
|
-
background:
|
|
561
|
+
background: var(--bg-elevated);
|
|
384
562
|
backdrop-filter: blur(24px);
|
|
385
|
-
border: 1px solid
|
|
563
|
+
border: 1px solid var(--border-main);
|
|
386
564
|
border-radius: 20px;
|
|
387
565
|
width: 100%;
|
|
388
566
|
max-width: 700px;
|
|
@@ -401,7 +579,7 @@ header p {
|
|
|
401
579
|
justify-content: space-between;
|
|
402
580
|
align-items: center;
|
|
403
581
|
padding: 22px 28px;
|
|
404
|
-
border-bottom: 1px solid
|
|
582
|
+
border-bottom: 1px solid var(--border-main);
|
|
405
583
|
}
|
|
406
584
|
|
|
407
585
|
.modal-header h3 {
|
|
@@ -454,10 +632,10 @@ form {
|
|
|
454
632
|
.form-group textarea {
|
|
455
633
|
width: 100%;
|
|
456
634
|
padding: 11px 16px;
|
|
457
|
-
background:
|
|
458
|
-
border: 1px solid
|
|
635
|
+
background: var(--bg-surface);
|
|
636
|
+
border: 1px solid var(--border-input);
|
|
459
637
|
border-radius: 10px;
|
|
460
|
-
color:
|
|
638
|
+
color: var(--text-primary);
|
|
461
639
|
font-size: 0.95rem;
|
|
462
640
|
transition: all 0.25s ease;
|
|
463
641
|
appearance: none;
|
|
@@ -507,7 +685,7 @@ form {
|
|
|
507
685
|
.target-section {
|
|
508
686
|
margin-top: 24px;
|
|
509
687
|
padding-top: 24px;
|
|
510
|
-
border-top: 1px solid
|
|
688
|
+
border-top: 1px solid var(--border-main);
|
|
511
689
|
}
|
|
512
690
|
|
|
513
691
|
.target-section h4 {
|
|
@@ -520,8 +698,8 @@ form {
|
|
|
520
698
|
}
|
|
521
699
|
|
|
522
700
|
.target-item {
|
|
523
|
-
background:
|
|
524
|
-
border: 1px solid
|
|
701
|
+
background: var(--bg-surface-alt);
|
|
702
|
+
border: 1px solid var(--border-light);
|
|
525
703
|
border-radius: 14px;
|
|
526
704
|
padding: 20px;
|
|
527
705
|
margin-bottom: 12px;
|
|
@@ -551,7 +729,7 @@ form {
|
|
|
551
729
|
gap: 12px;
|
|
552
730
|
padding-top: 20px;
|
|
553
731
|
margin-top: 20px;
|
|
554
|
-
border-top: 1px solid
|
|
732
|
+
border-top: 1px solid var(--border-main);
|
|
555
733
|
}
|
|
556
734
|
|
|
557
735
|
/* Proxy address */
|
|
@@ -561,8 +739,8 @@ form {
|
|
|
561
739
|
gap: 10px;
|
|
562
740
|
margin-top: 14px;
|
|
563
741
|
padding: 12px 16px;
|
|
564
|
-
background:
|
|
565
|
-
border: 1px solid
|
|
742
|
+
background: var(--bg-surface-alt);
|
|
743
|
+
border: 1px solid var(--border-light);
|
|
566
744
|
border-radius: 12px;
|
|
567
745
|
font-family: 'Cascadia Code', 'Fira Code', 'Consolas', monospace;
|
|
568
746
|
transition: all 0.25s;
|
|
@@ -576,7 +754,7 @@ form {
|
|
|
576
754
|
.proxy-address code {
|
|
577
755
|
flex: 1;
|
|
578
756
|
font-size: 0.85rem;
|
|
579
|
-
color:
|
|
757
|
+
color: var(--accent);
|
|
580
758
|
word-break: break-all;
|
|
581
759
|
}
|
|
582
760
|
|
|
@@ -648,10 +826,10 @@ form {
|
|
|
648
826
|
justify-content: space-between;
|
|
649
827
|
width: 100%;
|
|
650
828
|
padding: 11px 16px;
|
|
651
|
-
background:
|
|
652
|
-
border: 1px solid
|
|
829
|
+
background: var(--bg-surface);
|
|
830
|
+
border: 1px solid var(--border-input);
|
|
653
831
|
border-radius: 10px;
|
|
654
|
-
color:
|
|
832
|
+
color: var(--text-primary);
|
|
655
833
|
font-size: 0.95rem;
|
|
656
834
|
cursor: pointer;
|
|
657
835
|
transition: all 0.25s ease;
|
|
@@ -683,9 +861,9 @@ form {
|
|
|
683
861
|
top: calc(100% + 6px);
|
|
684
862
|
left: 0;
|
|
685
863
|
right: 0;
|
|
686
|
-
background:
|
|
864
|
+
background: var(--bg-dropdown);
|
|
687
865
|
backdrop-filter: blur(24px);
|
|
688
|
-
border: 1px solid
|
|
866
|
+
border: 1px solid var(--border-input);
|
|
689
867
|
border-radius: 12px;
|
|
690
868
|
z-index: 100;
|
|
691
869
|
overflow: hidden;
|
|
@@ -772,17 +950,17 @@ form {
|
|
|
772
950
|
display: flex;
|
|
773
951
|
gap: 8px;
|
|
774
952
|
padding: 10px 16px;
|
|
775
|
-
border-top: 1px solid
|
|
776
|
-
background:
|
|
953
|
+
border-top: 1px solid var(--border-main);
|
|
954
|
+
background: var(--bg-surface-alt);
|
|
777
955
|
}
|
|
778
956
|
|
|
779
957
|
.model-add-input {
|
|
780
958
|
flex: 1;
|
|
781
959
|
padding: 7px 12px;
|
|
782
|
-
background:
|
|
783
|
-
border: 1px solid
|
|
960
|
+
background: var(--bg-surface);
|
|
961
|
+
border: 1px solid var(--border-input);
|
|
784
962
|
border-radius: 8px;
|
|
785
|
-
color:
|
|
963
|
+
color: var(--text-secondary);
|
|
786
964
|
font-size: 0.85rem;
|
|
787
965
|
transition: all 0.2s;
|
|
788
966
|
}
|
|
@@ -799,9 +977,9 @@ form {
|
|
|
799
977
|
}
|
|
800
978
|
|
|
801
979
|
.confirm-modal .confirm-box {
|
|
802
|
-
background:
|
|
980
|
+
background: var(--bg-elevated);
|
|
803
981
|
backdrop-filter: blur(24px);
|
|
804
|
-
border: 1px solid
|
|
982
|
+
border: 1px solid var(--border-main);
|
|
805
983
|
border-radius: 20px;
|
|
806
984
|
padding: 32px 36px;
|
|
807
985
|
max-width: 380px;
|
|
@@ -872,8 +1050,8 @@ form {
|
|
|
872
1050
|
|
|
873
1051
|
.import-stat {
|
|
874
1052
|
flex: 1;
|
|
875
|
-
background:
|
|
876
|
-
border: 1px solid
|
|
1053
|
+
background: var(--bg-surface-alt);
|
|
1054
|
+
border: 1px solid var(--border-light);
|
|
877
1055
|
border-radius: 12px;
|
|
878
1056
|
padding: 16px;
|
|
879
1057
|
text-align: center;
|
|
@@ -883,7 +1061,7 @@ form {
|
|
|
883
1061
|
display: block;
|
|
884
1062
|
font-size: 1.8rem;
|
|
885
1063
|
font-weight: 700;
|
|
886
|
-
color:
|
|
1064
|
+
color: var(--accent);
|
|
887
1065
|
font-family: 'Cascadia Code', 'Fira Code', 'Consolas', monospace;
|
|
888
1066
|
}
|
|
889
1067
|
|
|
@@ -913,8 +1091,8 @@ form {
|
|
|
913
1091
|
align-items: flex-start;
|
|
914
1092
|
gap: 12px;
|
|
915
1093
|
padding: 14px 16px;
|
|
916
|
-
background:
|
|
917
|
-
border: 1px solid
|
|
1094
|
+
background: var(--bg-surface-alt);
|
|
1095
|
+
border: 1px solid var(--border-light);
|
|
918
1096
|
border-radius: 10px;
|
|
919
1097
|
cursor: pointer;
|
|
920
1098
|
transition: all 0.2s;
|
|
@@ -974,17 +1152,17 @@ form {
|
|
|
974
1152
|
.stats-filter-trigger {
|
|
975
1153
|
padding: 7px 14px !important;
|
|
976
1154
|
font-size: 0.85rem !important;
|
|
977
|
-
background:
|
|
1155
|
+
background: var(--bg-surface-alt) !important;
|
|
978
1156
|
}
|
|
979
1157
|
|
|
980
1158
|
.stats-range-btns {
|
|
981
1159
|
display: flex;
|
|
982
1160
|
gap: 4px;
|
|
983
1161
|
align-items: center;
|
|
984
|
-
background:
|
|
1162
|
+
background: var(--bg-surface-alt);
|
|
985
1163
|
border-radius: 10px;
|
|
986
1164
|
padding: 3px;
|
|
987
|
-
border: 1px solid
|
|
1165
|
+
border: 1px solid var(--border-light);
|
|
988
1166
|
}
|
|
989
1167
|
|
|
990
1168
|
.stats-range-btn {
|
|
@@ -1015,8 +1193,8 @@ form {
|
|
|
1015
1193
|
}
|
|
1016
1194
|
|
|
1017
1195
|
.stats-date-input {
|
|
1018
|
-
background:
|
|
1019
|
-
border: 1px solid
|
|
1196
|
+
background: var(--bg-surface);
|
|
1197
|
+
border: 1px solid var(--border-input);
|
|
1020
1198
|
border-radius: 6px;
|
|
1021
1199
|
color: #e2e8f0;
|
|
1022
1200
|
padding: 6px 8px;
|
|
@@ -1049,8 +1227,8 @@ form {
|
|
|
1049
1227
|
}
|
|
1050
1228
|
|
|
1051
1229
|
.stats-summary-item {
|
|
1052
|
-
background:
|
|
1053
|
-
border: 1px solid
|
|
1230
|
+
background: var(--bg-surface-alt);
|
|
1231
|
+
border: 1px solid var(--border-light);
|
|
1054
1232
|
border-radius: 12px;
|
|
1055
1233
|
padding: 18px 16px;
|
|
1056
1234
|
text-align: center;
|
|
@@ -1066,7 +1244,7 @@ form {
|
|
|
1066
1244
|
display: block;
|
|
1067
1245
|
font-size: 1.5rem;
|
|
1068
1246
|
font-weight: 700;
|
|
1069
|
-
color:
|
|
1247
|
+
color: var(--accent);
|
|
1070
1248
|
font-family: 'Cascadia Code', 'Fira Code', 'Consolas', monospace;
|
|
1071
1249
|
margin-bottom: 4px;
|
|
1072
1250
|
}
|
|
@@ -1128,7 +1306,7 @@ form {
|
|
|
1128
1306
|
text-align: right;
|
|
1129
1307
|
font-family: 'Cascadia Code', 'Fira Code', 'Consolas', monospace;
|
|
1130
1308
|
font-size: 0.82rem;
|
|
1131
|
-
color:
|
|
1309
|
+
color: var(--accent);
|
|
1132
1310
|
}
|
|
1133
1311
|
|
|
1134
1312
|
.stats-table tfoot td {
|
|
@@ -1276,10 +1454,10 @@ form {
|
|
|
1276
1454
|
.api-key-display {
|
|
1277
1455
|
display: block;
|
|
1278
1456
|
padding: 11px 16px;
|
|
1279
|
-
background:
|
|
1280
|
-
border: 1px solid
|
|
1457
|
+
background: var(--bg-surface);
|
|
1458
|
+
border: 1px solid var(--border-input);
|
|
1281
1459
|
border-radius: 10px;
|
|
1282
|
-
color:
|
|
1460
|
+
color: var(--text-dim);
|
|
1283
1461
|
font-size: 0.9rem;
|
|
1284
1462
|
letter-spacing: 2px;
|
|
1285
1463
|
cursor: pointer;
|
|
@@ -1343,11 +1521,11 @@ form {
|
|
|
1343
1521
|
|
|
1344
1522
|
.provider-pool-empty {
|
|
1345
1523
|
padding: 12px 14px;
|
|
1346
|
-
border: 1px dashed
|
|
1524
|
+
border: 1px dashed var(--border-input);
|
|
1347
1525
|
border-radius: 10px;
|
|
1348
|
-
color:
|
|
1526
|
+
color: var(--text-dim);
|
|
1349
1527
|
font-size: 0.9rem;
|
|
1350
|
-
background:
|
|
1528
|
+
background: var(--bg-surface-alt);
|
|
1351
1529
|
}
|
|
1352
1530
|
|
|
1353
1531
|
.provider-pool-item {
|
|
@@ -1356,9 +1534,9 @@ form {
|
|
|
1356
1534
|
gap: 12px;
|
|
1357
1535
|
align-items: center;
|
|
1358
1536
|
padding: 12px 14px;
|
|
1359
|
-
border: 1px solid
|
|
1537
|
+
border: 1px solid var(--border-input);
|
|
1360
1538
|
border-radius: 12px;
|
|
1361
|
-
background:
|
|
1539
|
+
background: var(--bg-surface);
|
|
1362
1540
|
}
|
|
1363
1541
|
|
|
1364
1542
|
.provider-pool-main {
|
|
@@ -1558,3 +1736,138 @@ form {
|
|
|
1558
1736
|
background: rgba(71, 85, 105, 0.5);
|
|
1559
1737
|
border-radius: 3px;
|
|
1560
1738
|
}
|
|
1739
|
+
|
|
1740
|
+
/* Log viewer */
|
|
1741
|
+
.log-toolbar {
|
|
1742
|
+
display: flex;
|
|
1743
|
+
gap: 10px;
|
|
1744
|
+
align-items: center;
|
|
1745
|
+
padding: 14px 28px;
|
|
1746
|
+
border-bottom: 1px solid var(--border-light);
|
|
1747
|
+
background: var(--bg-surface-alt);
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
.log-toolbar select {
|
|
1751
|
+
padding: 6px 10px;
|
|
1752
|
+
background: var(--bg-surface);
|
|
1753
|
+
border: 1px solid var(--border-input);
|
|
1754
|
+
border-radius: 8px;
|
|
1755
|
+
color: #e2e8f0;
|
|
1756
|
+
font-size: 0.85rem;
|
|
1757
|
+
cursor: pointer;
|
|
1758
|
+
appearance: auto;
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
.log-toolbar select option {
|
|
1762
|
+
background: #0f172a;
|
|
1763
|
+
color: #e2e8f0;
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
.log-content {
|
|
1767
|
+
padding: 16px 28px 28px;
|
|
1768
|
+
max-height: 500px;
|
|
1769
|
+
overflow-y: auto;
|
|
1770
|
+
font-family: 'Cascadia Code', 'Fira Code', 'Consolas', monospace;
|
|
1771
|
+
font-size: 0.78rem;
|
|
1772
|
+
line-height: 1.7;
|
|
1773
|
+
color: var(--text-muted);
|
|
1774
|
+
background: var(--bg-surface-alt);
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
.log-content::-webkit-scrollbar {
|
|
1778
|
+
width: 6px;
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
.log-content::-webkit-scrollbar-track {
|
|
1782
|
+
background: transparent;
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
.log-content::-webkit-scrollbar-thumb {
|
|
1786
|
+
background: rgba(71, 85, 105, 0.5);
|
|
1787
|
+
border-radius: 3px;
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
.log-line {
|
|
1791
|
+
padding: 1px 0;
|
|
1792
|
+
white-space: pre-wrap;
|
|
1793
|
+
word-break: break-all;
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
.log-line.log-error {
|
|
1797
|
+
color: #fca5a5;
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
.log-line.log-warn {
|
|
1801
|
+
color: #fbbf24;
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
/* History viewer */
|
|
1805
|
+
.history-content {
|
|
1806
|
+
padding: 20px 28px 28px;
|
|
1807
|
+
max-height: 500px;
|
|
1808
|
+
overflow-y: auto;
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
.history-content::-webkit-scrollbar {
|
|
1812
|
+
width: 6px;
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
.history-content::-webkit-scrollbar-track {
|
|
1816
|
+
background: transparent;
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1819
|
+
.history-content::-webkit-scrollbar-thumb {
|
|
1820
|
+
background: rgba(71, 85, 105, 0.5);
|
|
1821
|
+
border-radius: 3px;
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
.history-list {
|
|
1825
|
+
display: flex;
|
|
1826
|
+
flex-direction: column;
|
|
1827
|
+
gap: 8px;
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
.history-item {
|
|
1831
|
+
display: flex;
|
|
1832
|
+
align-items: center;
|
|
1833
|
+
justify-content: space-between;
|
|
1834
|
+
padding: 12px 16px;
|
|
1835
|
+
background: var(--bg-surface-alt);
|
|
1836
|
+
border: 1px solid var(--border-light);
|
|
1837
|
+
border-radius: 10px;
|
|
1838
|
+
transition: border-color 0.2s;
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
.history-item:hover {
|
|
1842
|
+
border-color: rgba(59, 130, 246, 0.3);
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
.history-info {
|
|
1846
|
+
display: flex;
|
|
1847
|
+
align-items: center;
|
|
1848
|
+
gap: 14px;
|
|
1849
|
+
flex: 1;
|
|
1850
|
+
min-width: 0;
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
.history-time {
|
|
1854
|
+
color: #e2e8f0;
|
|
1855
|
+
font-size: 0.85rem;
|
|
1856
|
+
font-family: 'Cascadia Code', 'Fira Code', 'Consolas', monospace;
|
|
1857
|
+
white-space: nowrap;
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1860
|
+
.history-reason {
|
|
1861
|
+
color: #94a3b8;
|
|
1862
|
+
font-size: 0.82rem;
|
|
1863
|
+
padding: 2px 8px;
|
|
1864
|
+
background: rgba(51, 65, 85, 0.3);
|
|
1865
|
+
border-radius: 6px;
|
|
1866
|
+
white-space: nowrap;
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
.history-size {
|
|
1870
|
+
color: #64748b;
|
|
1871
|
+
font-size: 0.78rem;
|
|
1872
|
+
white-space: nowrap;
|
|
1873
|
+
}
|
package/server.js
CHANGED
|
@@ -551,6 +551,7 @@ async function init() {
|
|
|
551
551
|
|
|
552
552
|
// 创建代理
|
|
553
553
|
app.post('/api/proxies', async (req, res) => {
|
|
554
|
+
configStore.saveSnapshot('create-proxy');
|
|
554
555
|
const { name, port, requireAuth, authToken, providerId, defaultModel, routingStrategy, providerPool, providerWeight } = req.body;
|
|
555
556
|
|
|
556
557
|
if (!name || !port || !providerId) {
|
|
@@ -592,6 +593,7 @@ async function init() {
|
|
|
592
593
|
|
|
593
594
|
// 更新代理
|
|
594
595
|
app.put('/api/proxies/:id', async (req, res) => {
|
|
596
|
+
configStore.saveSnapshot('update-proxy');
|
|
595
597
|
const existing = configStore.getProxyById(req.params.id);
|
|
596
598
|
if (!existing) return res.status(404).json({ error: 'Proxy not found' });
|
|
597
599
|
|
|
@@ -640,6 +642,7 @@ async function init() {
|
|
|
640
642
|
|
|
641
643
|
// 删除代理
|
|
642
644
|
app.delete('/api/proxies/:id', async (req, res) => {
|
|
645
|
+
configStore.saveSnapshot('delete-proxy');
|
|
643
646
|
const existing = configStore.getProxyById(req.params.id);
|
|
644
647
|
if (!existing) return res.status(404).json({ error: 'Proxy not found' });
|
|
645
648
|
|
|
@@ -666,6 +669,36 @@ async function init() {
|
|
|
666
669
|
res.json({ success: true, running: false });
|
|
667
670
|
});
|
|
668
671
|
|
|
672
|
+
// 批量启动所有代理
|
|
673
|
+
app.post('/api/proxies/start-all', async (req, res) => {
|
|
674
|
+
const proxies = configStore.getProxies();
|
|
675
|
+
const results = [];
|
|
676
|
+
for (const proxy of proxies) {
|
|
677
|
+
if (proxyManager.isRunning(proxy.id)) {
|
|
678
|
+
results.push({ id: proxy.id, name: proxy.name, skipped: true });
|
|
679
|
+
continue;
|
|
680
|
+
}
|
|
681
|
+
try {
|
|
682
|
+
await startProxyWithProvider(proxy);
|
|
683
|
+
results.push({ id: proxy.id, name: proxy.name, success: true });
|
|
684
|
+
} catch (err) {
|
|
685
|
+
results.push({ id: proxy.id, name: proxy.name, success: false, error: err.message });
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
res.json({ results });
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
// 批量停止所有代理
|
|
692
|
+
app.post('/api/proxies/stop-all', async (req, res) => {
|
|
693
|
+
const running = proxyManager.getRunningPorts();
|
|
694
|
+
const results = [];
|
|
695
|
+
for (const r of running) {
|
|
696
|
+
await proxyManager.stopProxy(r.id);
|
|
697
|
+
results.push({ id: r.id, name: r.name, success: true });
|
|
698
|
+
}
|
|
699
|
+
res.json({ results });
|
|
700
|
+
});
|
|
701
|
+
|
|
669
702
|
// 获取运行状态
|
|
670
703
|
app.get('/api/status', (req, res) => {
|
|
671
704
|
res.json({
|
|
@@ -687,6 +720,22 @@ async function init() {
|
|
|
687
720
|
});
|
|
688
721
|
});
|
|
689
722
|
|
|
723
|
+
// 设置
|
|
724
|
+
app.get('/api/settings', (req, res) => {
|
|
725
|
+
res.json(configStore.getSettings());
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
app.put('/api/settings', (req, res) => {
|
|
729
|
+
const settings = req.body;
|
|
730
|
+
if (!settings || typeof settings !== 'object') {
|
|
731
|
+
return res.status(400).json({ error: '需要 settings 对象' });
|
|
732
|
+
}
|
|
733
|
+
for (const [key, value] of Object.entries(settings)) {
|
|
734
|
+
configStore.setSetting(key, value);
|
|
735
|
+
}
|
|
736
|
+
res.json(configStore.getSettings());
|
|
737
|
+
});
|
|
738
|
+
|
|
690
739
|
// Token 用量统计
|
|
691
740
|
app.get('/api/stats', (req, res) => {
|
|
692
741
|
const { range, startDate, endDate, proxyId } = req.query;
|
|
@@ -704,6 +753,22 @@ async function init() {
|
|
|
704
753
|
res.json({ ...stats, proxies });
|
|
705
754
|
});
|
|
706
755
|
|
|
756
|
+
// 日志查看
|
|
757
|
+
app.get('/api/logs', (req, res) => {
|
|
758
|
+
const lines = Math.min(parseInt(req.query.lines) || 200, 2000);
|
|
759
|
+
try {
|
|
760
|
+
if (!fs.existsSync(logger.LOG_FILE)) {
|
|
761
|
+
return res.json({ lines: [] });
|
|
762
|
+
}
|
|
763
|
+
const content = fs.readFileSync(logger.LOG_FILE, 'utf8');
|
|
764
|
+
const allLines = content.split('\n').filter(l => l.trim());
|
|
765
|
+
const tail = allLines.slice(-lines);
|
|
766
|
+
res.json({ lines: tail, total: allLines.length });
|
|
767
|
+
} catch (err) {
|
|
768
|
+
res.json({ lines: [], error: err.message });
|
|
769
|
+
}
|
|
770
|
+
});
|
|
771
|
+
|
|
707
772
|
// ==================== 配置导入/导出 ====================
|
|
708
773
|
|
|
709
774
|
app.get('/api/config/export', (req, res) => {
|
|
@@ -733,6 +798,8 @@ async function init() {
|
|
|
733
798
|
return res.status(400).json({ error: '需要 config 和 mode(overwrite/merge)' });
|
|
734
799
|
}
|
|
735
800
|
|
|
801
|
+
configStore.saveSnapshot('import-' + mode);
|
|
802
|
+
|
|
736
803
|
// 校验结构
|
|
737
804
|
if (!Array.isArray(config.providers) || !Array.isArray(config.proxies)) {
|
|
738
805
|
return res.status(400).json({ error: '配置格式错误:需要 providers 和 proxies 数组' });
|
|
@@ -837,6 +904,21 @@ async function init() {
|
|
|
837
904
|
});
|
|
838
905
|
});
|
|
839
906
|
|
|
907
|
+
// ==================== 配置版本历史 ====================
|
|
908
|
+
|
|
909
|
+
app.get('/api/config/history', (req, res) => {
|
|
910
|
+
const snapshots = configStore.getSnapshots();
|
|
911
|
+
res.json({ snapshots });
|
|
912
|
+
});
|
|
913
|
+
|
|
914
|
+
app.post('/api/config/rollback', async (req, res) => {
|
|
915
|
+
const { file } = req.body;
|
|
916
|
+
if (!file) return res.status(400).json({ error: '需要指定快照文件' });
|
|
917
|
+
const result = configStore.restoreSnapshot(file);
|
|
918
|
+
if (result.error) return res.status(400).json({ error: result.error });
|
|
919
|
+
res.json({ success: true });
|
|
920
|
+
});
|
|
921
|
+
|
|
840
922
|
// 前端首页
|
|
841
923
|
app.get('/', (req, res) => {
|
|
842
924
|
res.sendFile(path.join(__dirname, 'public', 'index.html'));
|