protocol-proxy 2.1.6 → 2.3.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/converters/anthropic-to-gemini.js +253 -0
- package/lib/converters/gemini-to-anthropic.js +275 -0
- package/lib/converters/gemini-to-openai.js +238 -0
- package/lib/converters/openai-to-gemini.js +284 -0
- package/lib/detector.js +4 -0
- package/lib/proxy-server.js +140 -10
- package/lib/stats-store.js +285 -0
- package/package.json +2 -3
- package/public/app.js +273 -2
- package/public/index.html +110 -1
- package/public/style.css +324 -0
- package/server.js +154 -1
package/public/app.js
CHANGED
|
@@ -211,6 +211,12 @@ function selectProvider(id) {
|
|
|
211
211
|
const models = provider?.models || [];
|
|
212
212
|
selectModel(models[0] || '');
|
|
213
213
|
updateModelAddState();
|
|
214
|
+
// 同步 API Key placeholder
|
|
215
|
+
document.getElementById('target-key').placeholder = provider?.apiKey ? '已设置(留空则不修改)' : 'sk-...';
|
|
216
|
+
// 同步 Azure 字段
|
|
217
|
+
document.getElementById('target-azure-deployment').value = provider?.azureDeployment || '';
|
|
218
|
+
document.getElementById('target-azure-version').value = provider?.azureApiVersion || '';
|
|
219
|
+
document.getElementById('azure-fields').style.display = protocol === 'openai' ? '' : 'none';
|
|
214
220
|
}
|
|
215
221
|
|
|
216
222
|
// ==================== Model 下拉框 ====================
|
|
@@ -345,8 +351,254 @@ function updateModelAddState() {
|
|
|
345
351
|
}
|
|
346
352
|
}
|
|
347
353
|
|
|
354
|
+
// ==================== 配置导入/导出 ====================
|
|
355
|
+
|
|
356
|
+
let importData = null;
|
|
357
|
+
|
|
358
|
+
async function exportConfig() {
|
|
359
|
+
try {
|
|
360
|
+
const res = await fetch('/api/config/export');
|
|
361
|
+
const data = await res.json();
|
|
362
|
+
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
|
363
|
+
const url = URL.createObjectURL(blob);
|
|
364
|
+
const a = document.createElement('a');
|
|
365
|
+
const date = new Date().toISOString().slice(0, 10).replace(/-/g, '');
|
|
366
|
+
a.href = url;
|
|
367
|
+
a.download = `config-backup-${date}.json`;
|
|
368
|
+
a.click();
|
|
369
|
+
URL.revokeObjectURL(url);
|
|
370
|
+
showToast('配置已导出');
|
|
371
|
+
} catch (err) {
|
|
372
|
+
showToast('导出失败: ' + err.message, true);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function handleImportFile(e) {
|
|
377
|
+
const file = e.target.files[0];
|
|
378
|
+
if (!file) return;
|
|
379
|
+
e.target.value = '';
|
|
380
|
+
|
|
381
|
+
const reader = new FileReader();
|
|
382
|
+
reader.onload = () => {
|
|
383
|
+
try {
|
|
384
|
+
const data = JSON.parse(reader.result);
|
|
385
|
+
if (!Array.isArray(data.providers) || !Array.isArray(data.proxies)) {
|
|
386
|
+
showToast('配置格式错误:需要 providers 和 proxies 数组', true);
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
importData = data;
|
|
390
|
+
document.getElementById('import-providers-count').textContent = data.providers.length;
|
|
391
|
+
document.getElementById('import-proxies-count').textContent = data.proxies.length;
|
|
392
|
+
document.getElementById('import-modal').classList.add('active');
|
|
393
|
+
} catch (err) {
|
|
394
|
+
showToast('文件解析失败: ' + err.message, true);
|
|
395
|
+
}
|
|
396
|
+
};
|
|
397
|
+
reader.readAsText(file);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function closeImportModal() {
|
|
401
|
+
document.getElementById('import-modal').classList.remove('active');
|
|
402
|
+
importData = null;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
async function confirmImport() {
|
|
406
|
+
if (!importData) return;
|
|
407
|
+
const mode = document.querySelector('input[name="import-mode"]:checked')?.value || 'merge';
|
|
408
|
+
|
|
409
|
+
if (mode === 'overwrite') {
|
|
410
|
+
const ok = await showConfirm('确认<strong>覆盖</strong>现有配置?此操作不可撤销。');
|
|
411
|
+
if (!ok) return;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
try {
|
|
415
|
+
const res = await fetch('/api/config/import', {
|
|
416
|
+
method: 'POST',
|
|
417
|
+
headers: { 'Content-Type': 'application/json' },
|
|
418
|
+
body: JSON.stringify({ config: importData, mode }),
|
|
419
|
+
});
|
|
420
|
+
const result = await res.json();
|
|
421
|
+
|
|
422
|
+
if (!res.ok) {
|
|
423
|
+
showToast(result.error || '导入失败', true);
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
closeImportModal();
|
|
428
|
+
await Promise.all([loadProxies(), loadProviders()]);
|
|
429
|
+
|
|
430
|
+
const added = result.added;
|
|
431
|
+
let msg = `导入成功(${mode === 'overwrite' ? '覆盖' : '合并'})`;
|
|
432
|
+
if (added) msg += `:新增 ${added.providers} 供应商、${added.proxies} 代理`;
|
|
433
|
+
|
|
434
|
+
const restart = await showConfirm(`${msg}。<br><br>运行中的代理需要重启才能应用变更,新增的代理需要手动启动。<br><br>是否立即重启所有代理?`);
|
|
435
|
+
if (restart) {
|
|
436
|
+
await restartAllProxies();
|
|
437
|
+
}
|
|
438
|
+
} catch (err) {
|
|
439
|
+
showToast('导入失败: ' + err.message, true);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
async function restartAllProxies() {
|
|
444
|
+
try {
|
|
445
|
+
// 先停掉所有运行中的代理(不管 ID 是否匹配新配置)
|
|
446
|
+
const statusRes = await fetch('/api/status');
|
|
447
|
+
const status = await statusRes.json();
|
|
448
|
+
const runningIds = (status.running || []).map(r => r.id);
|
|
449
|
+
for (const id of runningIds) {
|
|
450
|
+
await fetch(`/api/proxies/${id}/stop`, { method: 'POST' });
|
|
451
|
+
}
|
|
452
|
+
// 重新加载配置
|
|
453
|
+
await loadProxies();
|
|
454
|
+
// 按新配置启动所有代理
|
|
455
|
+
for (const p of proxies) {
|
|
456
|
+
await fetch(`/api/proxies/${p.id}/start`, { method: 'POST' });
|
|
457
|
+
}
|
|
458
|
+
await loadProxies();
|
|
459
|
+
showToast('所有代理已重启');
|
|
460
|
+
} catch (err) {
|
|
461
|
+
showToast('重启失败: ' + err.message, true);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
348
465
|
// ==================== 初始化 ====================
|
|
349
466
|
|
|
467
|
+
// ==================== Token 用量统计 ====================
|
|
468
|
+
|
|
469
|
+
let statsRange = 'daily';
|
|
470
|
+
let statsProxyId = '';
|
|
471
|
+
|
|
472
|
+
async function loadStats() {
|
|
473
|
+
try {
|
|
474
|
+
const params = new URLSearchParams({ range: statsRange });
|
|
475
|
+
if (statsProxyId) params.set('proxyId', statsProxyId);
|
|
476
|
+
const startDate = document.getElementById('stats-start-date')?.value;
|
|
477
|
+
const endDate = document.getElementById('stats-end-date')?.value;
|
|
478
|
+
if (startDate) params.set('startDate', startDate);
|
|
479
|
+
if (endDate) params.set('endDate', endDate);
|
|
480
|
+
const res = await fetch('/api/stats?' + params);
|
|
481
|
+
const data = await res.json();
|
|
482
|
+
renderStatsSummary(data.summary);
|
|
483
|
+
renderStatsBreakdown(data);
|
|
484
|
+
renderStatsProxyOptions(data.proxies || []);
|
|
485
|
+
} catch (err) {
|
|
486
|
+
console.error('加载统计失败:', err);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function renderStatsSummary(summary) {
|
|
491
|
+
document.getElementById('stats-total-tokens').textContent = formatTokens(summary.total);
|
|
492
|
+
document.getElementById('stats-prompt-tokens').textContent = formatTokens(summary.prompt);
|
|
493
|
+
document.getElementById('stats-completion-tokens').textContent = formatTokens(summary.completion);
|
|
494
|
+
document.getElementById('stats-total-requests').textContent = summary.requests.toLocaleString();
|
|
495
|
+
const badge = document.getElementById('stats-estimated-badge');
|
|
496
|
+
if (badge) badge.style.display = summary.hasEstimated ? 'inline' : 'none';
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function formatTokens(n) {
|
|
500
|
+
if (!n || n === 0) return '0';
|
|
501
|
+
if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M';
|
|
502
|
+
if (n >= 1000) return (n / 1000).toFixed(1) + 'K';
|
|
503
|
+
return n.toLocaleString();
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function renderStatsBreakdown(data) {
|
|
507
|
+
const container = document.getElementById('stats-breakdown');
|
|
508
|
+
const { byProvider, byModel, summary } = data;
|
|
509
|
+
|
|
510
|
+
if (!byProvider || byProvider.length === 0) {
|
|
511
|
+
container.innerHTML = '<div class="empty">暂无数据</div>';
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
let html = '<table class="stats-table"><thead><tr>';
|
|
516
|
+
html += '<th>供应商</th><th>模型</th><th style="text-align:right">请求数</th>';
|
|
517
|
+
html += '<th style="text-align:right">输入 Token</th><th style="text-align:right">输出 Token</th>';
|
|
518
|
+
html += '<th style="text-align:right">合计</th>';
|
|
519
|
+
html += '</tr></thead><tbody>';
|
|
520
|
+
|
|
521
|
+
for (const item of byModel) {
|
|
522
|
+
const prefix = item.hasEstimated ? '~' : '';
|
|
523
|
+
html += '<tr>';
|
|
524
|
+
html += `<td class="provider-cell">${escapeHtml(item.provider)}</td>`;
|
|
525
|
+
html += `<td class="model-cell"><code>${escapeHtml(item.model)}</code></td>`;
|
|
526
|
+
html += `<td class="num">${item.requests.toLocaleString()}</td>`;
|
|
527
|
+
html += `<td class="num">${prefix ? `<span class="num-estimated" title="估算值">~</span>` : ''}${formatTokens(item.prompt)}</td>`;
|
|
528
|
+
html += `<td class="num">${prefix ? `<span class="num-estimated" title="估算值">~</span>` : ''}${formatTokens(item.completion)}</td>`;
|
|
529
|
+
html += `<td class="num">${prefix ? `<span class="num-estimated" title="估算值">~</span>` : ''}${formatTokens(item.total)}</td>`;
|
|
530
|
+
html += '</tr>';
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
html += '</tbody>';
|
|
534
|
+
html += '<tfoot><tr>';
|
|
535
|
+
html += '<td colspan="2">合计</td>';
|
|
536
|
+
html += `<td class="num">${summary.requests.toLocaleString()}</td>`;
|
|
537
|
+
html += `<td class="num">${formatTokens(summary.prompt)}</td>`;
|
|
538
|
+
html += `<td class="num">${formatTokens(summary.completion)}</td>`;
|
|
539
|
+
html += `<td class="num">${formatTokens(summary.total)}</td>`;
|
|
540
|
+
html += '</tr></tfoot></table>';
|
|
541
|
+
|
|
542
|
+
container.innerHTML = html;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function renderStatsProxyOptions(proxyList) {
|
|
546
|
+
const container = document.getElementById('stats-proxy-dropdown-options');
|
|
547
|
+
container.innerHTML = `<div class="model-option${!statsProxyId ? ' selected' : ''}" data-proxy-id="">
|
|
548
|
+
<span class="model-option-name">全部代理</span>
|
|
549
|
+
</div>` + proxyList.map(p => `
|
|
550
|
+
<div class="model-option${p.id === statsProxyId ? ' selected' : ''}" data-proxy-id="${escapeHtml(p.id)}">
|
|
551
|
+
<span class="model-option-name">${escapeHtml(p.name)}</span>
|
|
552
|
+
${p.providerName ? `<span style="color:#64748b;font-size:12px;margin-left:4px">${escapeHtml(p.providerName)}</span>` : ''}
|
|
553
|
+
</div>
|
|
554
|
+
`).join('');
|
|
555
|
+
|
|
556
|
+
container.querySelectorAll('.model-option').forEach(opt => {
|
|
557
|
+
opt.addEventListener('click', () => {
|
|
558
|
+
statsProxyId = opt.dataset.proxyId;
|
|
559
|
+
document.getElementById('stats-proxy-dropdown-value').textContent =
|
|
560
|
+
statsProxyId ? (proxyList.find(p => p.id === statsProxyId)?.name || '全部代理') : '全部代理';
|
|
561
|
+
document.getElementById('stats-proxy-dropdown').classList.remove('open');
|
|
562
|
+
container.querySelectorAll('.model-option').forEach(o => o.classList.remove('selected'));
|
|
563
|
+
opt.classList.add('selected');
|
|
564
|
+
loadStats();
|
|
565
|
+
});
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function initStatsDropdown() {
|
|
570
|
+
const trigger = document.getElementById('stats-proxy-dropdown-trigger');
|
|
571
|
+
const dropdown = document.getElementById('stats-proxy-dropdown');
|
|
572
|
+
|
|
573
|
+
trigger.addEventListener('click', (e) => {
|
|
574
|
+
e.stopPropagation();
|
|
575
|
+
dropdown.classList.toggle('open');
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
document.addEventListener('click', (e) => {
|
|
579
|
+
if (!dropdown.contains(e.target)) {
|
|
580
|
+
dropdown.classList.remove('open');
|
|
581
|
+
}
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function initStatsRangeBtns() {
|
|
586
|
+
document.querySelectorAll('.stats-range-btn').forEach(btn => {
|
|
587
|
+
btn.addEventListener('click', () => {
|
|
588
|
+
document.querySelectorAll('.stats-range-btn').forEach(b => b.classList.remove('active'));
|
|
589
|
+
btn.classList.add('active');
|
|
590
|
+
statsRange = btn.dataset.range;
|
|
591
|
+
// 清空日期选择器,显示全部数据
|
|
592
|
+
document.getElementById('stats-start-date').value = '';
|
|
593
|
+
document.getElementById('stats-end-date').value = '';
|
|
594
|
+
loadStats();
|
|
595
|
+
});
|
|
596
|
+
});
|
|
597
|
+
// 日期选择器变化时自动加载
|
|
598
|
+
document.getElementById('stats-start-date').addEventListener('change', loadStats);
|
|
599
|
+
document.getElementById('stats-end-date').addEventListener('change', loadStats);
|
|
600
|
+
}
|
|
601
|
+
|
|
350
602
|
function generateToken() {
|
|
351
603
|
const arr = new Uint8Array(24);
|
|
352
604
|
crypto.getRandomValues(arr);
|
|
@@ -385,9 +637,11 @@ function initSimpleDropdown(dropdownId, onChange) {
|
|
|
385
637
|
}
|
|
386
638
|
|
|
387
639
|
async function init() {
|
|
388
|
-
await Promise.all([loadProxies(), loadProviders()]);
|
|
640
|
+
await Promise.all([loadProxies(), loadProviders(), loadStats()]);
|
|
389
641
|
initProviderDropdown();
|
|
390
642
|
initModelDropdown();
|
|
643
|
+
initStatsDropdown();
|
|
644
|
+
initStatsRangeBtns();
|
|
391
645
|
initSimpleDropdown('auth-dropdown', (val) => {
|
|
392
646
|
const enabled = val === 'true';
|
|
393
647
|
document.getElementById('auth-token-group').style.display = enabled ? 'block' : 'none';
|
|
@@ -395,7 +649,12 @@ async function init() {
|
|
|
395
649
|
document.getElementById('proxy-auth-token').value = generateToken();
|
|
396
650
|
}
|
|
397
651
|
});
|
|
398
|
-
initSimpleDropdown('protocol-dropdown')
|
|
652
|
+
initSimpleDropdown('protocol-dropdown', (val) => {
|
|
653
|
+
document.getElementById('azure-fields').style.display = val === 'openai' ? '' : 'none';
|
|
654
|
+
});
|
|
655
|
+
// 初始状态:根据当前协议值决定 Azure 字段显示
|
|
656
|
+
const initProto = document.getElementById('target-protocol').value;
|
|
657
|
+
document.getElementById('azure-fields').style.display = initProto === 'openai' ? '' : 'none';
|
|
399
658
|
}
|
|
400
659
|
|
|
401
660
|
// ==================== 代理地址复制 ====================
|
|
@@ -541,6 +800,11 @@ function openModal(id = null) {
|
|
|
541
800
|
selectProvider(p.providerId || '');
|
|
542
801
|
selectModel(p.defaultModel || '');
|
|
543
802
|
document.getElementById('target-key').placeholder = p.hasApiKey ? '已设置(留空则不修改)' : 'sk-...';
|
|
803
|
+
// Azure 字段从供应商配置读取
|
|
804
|
+
const provider = providers.find(pr => pr.id === p.providerId);
|
|
805
|
+
document.getElementById('target-azure-deployment').value = provider?.azureDeployment || '';
|
|
806
|
+
document.getElementById('target-azure-version').value = provider?.azureApiVersion || '';
|
|
807
|
+
document.getElementById('azure-fields').style.display = p.protocol === 'openai' ? '' : 'none';
|
|
544
808
|
} else {
|
|
545
809
|
document.getElementById('proxy-id').value = '';
|
|
546
810
|
// 重置认证下拉框
|
|
@@ -552,6 +816,9 @@ function openModal(id = null) {
|
|
|
552
816
|
selectProvider('');
|
|
553
817
|
selectModel('');
|
|
554
818
|
document.getElementById('target-key').placeholder = 'sk-...';
|
|
819
|
+
document.getElementById('target-azure-deployment').value = '';
|
|
820
|
+
document.getElementById('target-azure-version').value = '';
|
|
821
|
+
document.getElementById('azure-fields').style.display = 'none';
|
|
555
822
|
}
|
|
556
823
|
|
|
557
824
|
updateModelAddState();
|
|
@@ -593,6 +860,10 @@ async function handleSubmit(e) {
|
|
|
593
860
|
const providerUpdates = {};
|
|
594
861
|
if (apiKey) providerUpdates.apiKey = apiKey;
|
|
595
862
|
if (protocol) providerUpdates.protocol = protocol;
|
|
863
|
+
const azureDeployment = document.getElementById('target-azure-deployment').value.trim();
|
|
864
|
+
const azureApiVersion = document.getElementById('target-azure-version').value.trim();
|
|
865
|
+
providerUpdates.azureDeployment = azureDeployment || '';
|
|
866
|
+
providerUpdates.azureApiVersion = azureApiVersion || '';
|
|
596
867
|
if (Object.keys(providerUpdates).length > 0) {
|
|
597
868
|
try {
|
|
598
869
|
const res = await fetch(`/api/providers/${providerId}`, {
|
package/public/index.html
CHANGED
|
@@ -24,10 +24,63 @@
|
|
|
24
24
|
</div>
|
|
25
25
|
</div>
|
|
26
26
|
|
|
27
|
+
<!-- Token 用量统计 -->
|
|
28
|
+
<section class="card stats-panel">
|
|
29
|
+
<div class="card-header">
|
|
30
|
+
<h2>Token 用量统计 <span class="stats-estimated-badge" id="stats-estimated-badge" style="display:none">含估算</span></h2>
|
|
31
|
+
<div class="stats-controls">
|
|
32
|
+
<div class="model-dropdown" id="stats-proxy-dropdown">
|
|
33
|
+
<div class="model-dropdown-trigger stats-filter-trigger" id="stats-proxy-dropdown-trigger">
|
|
34
|
+
<span id="stats-proxy-dropdown-value">全部代理</span>
|
|
35
|
+
<span class="model-dropdown-arrow">▾</span>
|
|
36
|
+
</div>
|
|
37
|
+
<div class="model-dropdown-menu" id="stats-proxy-dropdown-menu">
|
|
38
|
+
<div class="model-dropdown-options" id="stats-proxy-dropdown-options"></div>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
<div class="stats-range-btns">
|
|
42
|
+
<button class="btn btn-sm stats-range-btn active" data-range="daily">每日</button>
|
|
43
|
+
<button class="btn btn-sm stats-range-btn" data-range="monthly">每月</button>
|
|
44
|
+
<button class="btn btn-sm stats-range-btn" data-range="yearly">每年</button>
|
|
45
|
+
<span class="stats-date-sep">|</span>
|
|
46
|
+
<input type="date" id="stats-start-date" class="stats-date-input" placeholder="开始日期">
|
|
47
|
+
<span class="stats-date-sep">~</span>
|
|
48
|
+
<input type="date" id="stats-end-date" class="stats-date-input" placeholder="结束日期">
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
<div class="stats-summary" id="stats-summary">
|
|
53
|
+
<div class="stats-summary-item">
|
|
54
|
+
<span class="stats-summary-value" id="stats-total-tokens">-</span>
|
|
55
|
+
<span class="stats-summary-label">总 Token</span>
|
|
56
|
+
</div>
|
|
57
|
+
<div class="stats-summary-item">
|
|
58
|
+
<span class="stats-summary-value" id="stats-prompt-tokens">-</span>
|
|
59
|
+
<span class="stats-summary-label">输入 Token</span>
|
|
60
|
+
</div>
|
|
61
|
+
<div class="stats-summary-item">
|
|
62
|
+
<span class="stats-summary-value" id="stats-completion-tokens">-</span>
|
|
63
|
+
<span class="stats-summary-label">输出 Token</span>
|
|
64
|
+
</div>
|
|
65
|
+
<div class="stats-summary-item">
|
|
66
|
+
<span class="stats-summary-value" id="stats-total-requests">-</span>
|
|
67
|
+
<span class="stats-summary-label">请求数</span>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
<div id="stats-breakdown" class="stats-breakdown">
|
|
71
|
+
<div class="empty">暂无数据</div>
|
|
72
|
+
</div>
|
|
73
|
+
</section>
|
|
74
|
+
|
|
27
75
|
<section class="card">
|
|
28
76
|
<div class="card-header">
|
|
29
77
|
<h2>代理列表</h2>
|
|
30
|
-
<
|
|
78
|
+
<div class="card-header-actions">
|
|
79
|
+
<button class="btn" onclick="exportConfig()">导出配置</button>
|
|
80
|
+
<button class="btn" onclick="document.getElementById('import-file').click()">导入配置</button>
|
|
81
|
+
<input type="file" id="import-file" accept=".json" style="display:none" onchange="handleImportFile(event)">
|
|
82
|
+
<button class="btn btn-primary" onclick="openModal()">+ 新建代理</button>
|
|
83
|
+
</div>
|
|
31
84
|
</div>
|
|
32
85
|
<div id="proxy-list" class="proxy-list">
|
|
33
86
|
<div class="empty">加载中...</div>
|
|
@@ -111,6 +164,7 @@
|
|
|
111
164
|
<div class="model-dropdown-options" id="protocol-dropdown-options">
|
|
112
165
|
<div class="model-option selected" data-value="openai"><span class="model-option-name">OpenAI</span></div>
|
|
113
166
|
<div class="model-option" data-value="anthropic"><span class="model-option-name">Anthropic</span></div>
|
|
167
|
+
<div class="model-option" data-value="gemini"><span class="model-option-name">Gemini</span></div>
|
|
114
168
|
</div>
|
|
115
169
|
</div>
|
|
116
170
|
</div>
|
|
@@ -139,6 +193,16 @@
|
|
|
139
193
|
<input type="password" id="target-key" placeholder="sk-...">
|
|
140
194
|
</div>
|
|
141
195
|
</div>
|
|
196
|
+
<div class="form-row" id="azure-fields" style="display:none">
|
|
197
|
+
<div class="form-group">
|
|
198
|
+
<label>Azure Deployment <span style="color:#64748b;font-size:0.75rem">(仅 Azure 用户填写)</span></label>
|
|
199
|
+
<input type="text" id="target-azure-deployment" placeholder="仅 Azure 用户填写">
|
|
200
|
+
</div>
|
|
201
|
+
<div class="form-group">
|
|
202
|
+
<label>Azure API Version <span style="color:#64748b;font-size:0.75rem">(仅 Azure 用户填写)</span></label>
|
|
203
|
+
<input type="text" id="target-azure-version" placeholder="仅 Azure 用户填写">
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
142
206
|
</div>
|
|
143
207
|
</div>
|
|
144
208
|
|
|
@@ -163,6 +227,51 @@
|
|
|
163
227
|
</div>
|
|
164
228
|
</div>
|
|
165
229
|
|
|
230
|
+
<!-- 导入预览弹窗 -->
|
|
231
|
+
<div class="modal" id="import-modal">
|
|
232
|
+
<div class="modal-content" style="max-width:500px">
|
|
233
|
+
<div class="modal-header">
|
|
234
|
+
<h3>导入配置</h3>
|
|
235
|
+
<button class="btn-close" onclick="closeImportModal()">×</button>
|
|
236
|
+
</div>
|
|
237
|
+
<div class="import-preview" id="import-preview">
|
|
238
|
+
<div class="import-stats">
|
|
239
|
+
<div class="import-stat">
|
|
240
|
+
<span class="import-stat-value" id="import-providers-count">0</span>
|
|
241
|
+
<span class="import-stat-label">供应商</span>
|
|
242
|
+
</div>
|
|
243
|
+
<div class="import-stat">
|
|
244
|
+
<span class="import-stat-value" id="import-proxies-count">0</span>
|
|
245
|
+
<span class="import-stat-label">代理</span>
|
|
246
|
+
</div>
|
|
247
|
+
</div>
|
|
248
|
+
<div class="import-mode">
|
|
249
|
+
<label>导入模式</label>
|
|
250
|
+
<div class="import-mode-options">
|
|
251
|
+
<label class="import-mode-option">
|
|
252
|
+
<input type="radio" name="import-mode" value="merge" checked>
|
|
253
|
+
<span>
|
|
254
|
+
<strong>合并</strong>
|
|
255
|
+
<small>按 ID 去重:新增导入项,同 ID 覆盖</small>
|
|
256
|
+
</span>
|
|
257
|
+
</label>
|
|
258
|
+
<label class="import-mode-option">
|
|
259
|
+
<input type="radio" name="import-mode" value="overwrite">
|
|
260
|
+
<span>
|
|
261
|
+
<strong>覆盖</strong>
|
|
262
|
+
<small>完全替换现有配置</small>
|
|
263
|
+
</span>
|
|
264
|
+
</label>
|
|
265
|
+
</div>
|
|
266
|
+
</div>
|
|
267
|
+
</div>
|
|
268
|
+
<div class="modal-footer">
|
|
269
|
+
<button class="btn" onclick="closeImportModal()">取消</button>
|
|
270
|
+
<button class="btn btn-primary" onclick="confirmImport()">确认导入</button>
|
|
271
|
+
</div>
|
|
272
|
+
</div>
|
|
273
|
+
</div>
|
|
274
|
+
|
|
166
275
|
<script src="app.js"></script>
|
|
167
276
|
</body>
|
|
168
277
|
</html>
|