feat: WebUI 版本检测 — 顶栏按钮 + 设置页备份/回滚/更新管理

顶栏:
- 版本号按钮, 有更新时变绿色并显示新版本号
- 每10分钟自动检查

设置页新增「版本与更新」区域:
- 当前版本 + 更新状态徽章
- 检查更新 / 立即更新 按钮
- 备份列表 + 手动回滚

API:
- GET /api/updates/backups — 备份列表
- POST /api/updates/rollback — 执行回滚

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
qinglong
2026-06-13 20:39:28 +08:00
parent c20417ff93
commit 9a9a2975de
4 changed files with 173 additions and 1 deletions
+93
View File
@@ -1,6 +1,29 @@
<div class="plugin-page-root">
<h2>⚙ 框架设置</h2>
<!-- ── 版本与更新 ── -->
<div class="card" style="margin-bottom:24px">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
<h3 style="margin:0">🔄 版本与更新</h3>
</div>
<div id="update-area">
<div style="display:flex;gap:12px;align-items:center;flex-wrap:wrap">
<span style="font-size:1.1rem;font-weight:600">SenSu <span id="ver-current">v?</span></span>
<span id="ver-badge" style="font-size:.85rem;padding:2px 8px;border-radius:12px;background:var(--md-sys-color-surface-container-lowest)">检查中...</span>
<button class="btn btn-sm btn-filled" onclick="checkUpdate()">检查更新</button>
<button id="btn-apply" class="btn btn-sm btn-filled" style="display:none;background:var(--success,#4caf50)" onclick="applyUpdate()">⬆ 立即更新</button>
</div>
<div id="update-notes" style="margin-top:8px;font-size:.85rem;color:var(--text-dim)"></div>
</div>
<div style="margin-top:16px;border-top:1px solid var(--border);padding-top:12px">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
<h4 style="margin:0">📦 备份管理</h4>
<button class="btn btn-sm btn-outlined" onclick="loadBackups()">刷新列表</button>
</div>
<div id="backup-list"><span style="color:var(--text-dim);font-size:.85rem">加载中...</span></div>
</div>
</div>
<!-- ── API Key 管理 ── -->
<div class="card" style="margin-bottom:24px">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px">
@@ -58,9 +81,77 @@
.input:focus { outline:1px solid var(--primary) }
#apikey-tbody td { padding:8px; border-bottom:1px solid var(--md-sys-color-surface-container-lowest) }
#apikey-tbody tr:hover { background:var(--md-sys-color-surface-container-lowest) }
.backup-row { display:flex;align-items:center;justify-content:space-between;padding:6px 0;border-bottom:1px solid var(--md-sys-color-surface-container-lowest);gap:8px }
.backup-name { font-size:.85rem;word-break:break-all;flex:1;min-width:0 }
</style>
<script>
// ── 版本与更新 ──
async function loadVersion() {
try {
var r = await fetch("./api/updates"), d = await r.json();
document.getElementById("ver-current").textContent = d.current_version || d.latest_version || "?";
var badge = document.getElementById("ver-badge");
if (d.update_available) {
badge.textContent = "🔄 " + d.latest_version + " 可用";
badge.style.background = "var(--success,#4caf50)";
badge.style.color = "#fff";
document.getElementById("btn-apply").style.display = "inline-block";
document.getElementById("update-notes").innerHTML = d.latest_version ? "最新版本: <b>"+esc(d.latest_version)+"</b>" : "";
} else if (d.checked) {
badge.textContent = "✅ 已是最新";
badge.style.background = "var(--md-sys-color-surface-container-lowest)";
badge.style.color = "var(--text)";
document.getElementById("btn-apply").style.display = "none";
} else {
badge.textContent = "⏳ 等待检查...";
}
if (d.last_check) {
document.getElementById("update-notes").innerHTML += " <span style='font-size:.75rem;color:var(--text-dim)'>上次检查: "+new Date(d.last_check*1000).toLocaleString()+"</span>";
}
} catch(e) {}
}
async function checkUpdate() {
document.getElementById("ver-badge").textContent = "⏳ 检查中...";
await fetch("./api/updates/check", {method:"POST"});
setTimeout(loadVersion, 1500);
}
async function applyUpdate() {
if (!confirm("确认更新 SenSu?更新前会自动备份当前版本。")) return;
document.getElementById("btn-apply").disabled = true;
document.getElementById("btn-apply").textContent = "⏳ 更新中...";
try {
var r = await fetch("./api/updates/apply", {method:"POST"}), d = await r.json();
if (d.ok) alert("✅ " + d.msg);
else alert("❌ " + (d.error||"失败"));
} catch(e) { alert("❌ " + e); }
loadVersion();
}
// ── 备份管理 ──
async function loadBackups() {
var el = document.getElementById("backup-list");
try {
var r = await fetch("./api/updates/backups"), d = await r.json();
if (!d.backups.length) { el.innerHTML = '<span style="color:var(--text-dim);font-size:.85rem">暂无备份</span>'; return; }
el.innerHTML = d.backups.map(function(b,i){
return '<div class="backup-row"><span class="backup-name">'+esc(b.name)+'</span><span style="font-size:.8rem;color:var(--text-dim)">'+b.size_mb+'MB | '+b.time+'</span>'+
'<button class="btn btn-sm btn-outlined" onclick="doRollback(\''+esc(b.name)+'\')">回滚</button></div>';
}).join("");
} catch(e) { el.innerHTML = '<span style="color:var(--error)">加载失败</span>'; }
}
async function doRollback(name) {
if (!confirm("确认回滚到 "+name+"? 当前版本将被覆盖。")) return;
try {
var r = await fetch("./api/updates/rollback", {method:"POST", headers:{"Content-Type":"application/json"}, body:JSON.stringify({name:name})}), d = await r.json();
if (d.ok) alert("✅ " + d.msg);
else alert("❌ " + (d.error||"失败"));
} catch(e) { alert("❌ " + e); }
}
// ── 加载 API Keys ──
async function loadKeys() {
var tbody = document.getElementById("apikey-tbody");
@@ -146,5 +237,7 @@
// ── 初始加载 ──
loadKeys();
loadVersion();
loadBackups();
</script>
</div>