9a9a2975de
顶栏: - 版本号按钮, 有更新时变绿色并显示新版本号 - 每10分钟自动检查 设置页新增「版本与更新」区域: - 当前版本 + 更新状态徽章 - 检查更新 / 立即更新 按钮 - 备份列表 + 手动回滚 API: - GET /api/updates/backups — 备份列表 - POST /api/updates/rollback — 执行回滚 Co-Authored-By: Claude <noreply@anthropic.com>
244 lines
13 KiB
HTML
244 lines
13 KiB
HTML
<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">
|
||
<h3 style="margin:0">🔑 API Key 管理</h3>
|
||
<button class="btn btn-filled" onclick="showCreateKey()">+ 创建 Key</button>
|
||
</div>
|
||
<p style="color:var(--text-dim);font-size:.85rem;margin-bottom:12px">
|
||
API Key 用于服务器间安全通信。支持权限范围控制和过期时间。
|
||
</p>
|
||
<div id="apikey-list" style="overflow-x:auto">
|
||
<table style="width:100%;border-collapse:collapse">
|
||
<thead><tr style="text-align:left;border-bottom:1px solid var(--border)">
|
||
<th style="padding:8px">名称</th><th style="padding:8px">Key</th><th style="padding:8px">权限</th><th style="padding:8px">用量</th><th style="padding:8px">过期</th><th style="padding:8px">操作</th>
|
||
</tr></thead>
|
||
<tbody id="apikey-tbody"><tr><td colspan="6" style="padding:16px;text-align:center;color:var(--text-dim)">加载中...</td></tr></tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ── 创建对话框 (hidden) ── -->
|
||
<div id="create-dialog" style="display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.5);z-index:1000;display:none;align-items:center;justify-content:center">
|
||
<div class="card" style="min-width:420px;max-width:500px">
|
||
<h3 style="margin:0 0 16px 0">创建 API Key</h3>
|
||
<div class="form-group" style="margin-bottom:12px">
|
||
<label style="display:block;margin-bottom:4px;font-size:.85rem">名称</label>
|
||
<input id="new-key-name" class="input" placeholder="例如: 监控插件" style="width:100%;padding:8px;background:var(--bg);color:var(--text);border:1px solid var(--border);border-radius:var(--shape-xs)">
|
||
</div>
|
||
<div class="form-group" style="margin-bottom:12px">
|
||
<label style="display:block;margin-bottom:4px;font-size:.85rem">权限范围</label>
|
||
<select id="new-key-preset" class="input" style="width:100%;padding:8px;background:var(--bg);color:var(--text);border:1px solid var(--border);border-radius:var(--shape-xs)">
|
||
<option value="readonly">只读 — 仅查询系统状态</option>
|
||
<option value="monitor" selected>监控 — 查询 + 事件订阅</option>
|
||
<option value="full">管理 — 全部权限</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group" style="margin-bottom:16px">
|
||
<label style="display:block;margin-bottom:4px;font-size:.85rem">过期时间 (秒, 0=永不过期)</label>
|
||
<input id="new-key-ttl" class="input" type="number" value="0" min="0" style="width:100%;padding:8px;background:var(--bg);color:var(--text);border:1px solid var(--border);border-radius:var(--shape-xs)">
|
||
<span style="font-size:.75rem;color:var(--text-dim)">常用: 86400(1天) 604800(7天) 2592000(30天)</span>
|
||
</div>
|
||
<div style="display:flex;gap:8px;justify-content:flex-end">
|
||
<button class="btn btn-outlined" onclick="hideCreateKey()">取消</button>
|
||
<button class="btn btn-filled" onclick="doCreateKey()">创建</button>
|
||
</div>
|
||
<div id="create-result" style="margin-top:12px;display:none">
|
||
<div style="background:var(--md-sys-color-surface-container-lowest);padding:12px;border-radius:var(--shape-xs);word-break:break-all">
|
||
<strong style="color:var(--success)">✅ Key 已创建 (仅显示一次):</strong>
|
||
<code id="new-key-display" style="display:block;margin-top:4px;font-size:.9rem;color:var(--primary)"></code>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<style>
|
||
.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");
|
||
try {
|
||
var resp = await fetch("./api/apikeys");
|
||
var data = await resp.json();
|
||
if (!data.keys || !data.keys.length) {
|
||
tbody.innerHTML = '<tr><td colspan="6" style="padding:16px;text-align:center;color:var(--text-dim)">暂无 API Key</td></tr>';
|
||
return;
|
||
}
|
||
var rows = [];
|
||
data.keys.forEach(function(k) {
|
||
var expStr = k.expires_at > 0 ? new Date(k.expires_at*1000).toLocaleDateString() : "永不过期";
|
||
if (k.expired) expStr = '<span style="color:var(--error)">已过期</span>';
|
||
var permLabel = k.permissions && k.permissions[0] === "admin" ? "管理" : (k.permissions && k.permissions.length <= 2 ? "只读" : "监控");
|
||
rows.push(
|
||
'<tr>' +
|
||
'<td>' + esc(k.name) + '</td>' +
|
||
'<td><code>' + esc(k.key) + '</code></td>' +
|
||
'<td><span class="badge">' + permLabel + '</span></td>' +
|
||
'<td>' + (k.request_count || 0) + '</td>' +
|
||
'<td>' + expStr + '</td>' +
|
||
'<td><button class="btn btn-sm btn-outlined" style="color:var(--error);border-color:var(--error)" onclick="deleteKey(\'' + esc(k.full_key) + '\',\'' + esc(k.name) + '\')">删除</button></td>' +
|
||
'</tr>'
|
||
);
|
||
});
|
||
tbody.innerHTML = rows.join("");
|
||
} catch(e) {
|
||
tbody.innerHTML = '<tr><td colspan="6" style="padding:16px;text-align:center;color:var(--error)">加载失败: ' + esc(String(e)) + '</td></tr>';
|
||
}
|
||
}
|
||
|
||
function esc(s) { return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); }
|
||
|
||
// ── 创建对话框 ──
|
||
function showCreateKey() {
|
||
document.getElementById("create-dialog").style.display = "flex";
|
||
document.getElementById("create-result").style.display = "none";
|
||
document.getElementById("new-key-name").value = "";
|
||
document.getElementById("new-key-ttl").value = "0";
|
||
}
|
||
function hideCreateKey() {
|
||
document.getElementById("create-dialog").style.display = "none";
|
||
}
|
||
|
||
async function doCreateKey() {
|
||
var name = document.getElementById("new-key-name").value.trim();
|
||
if (!name) { alert("请输入名称"); return; }
|
||
var preset = document.getElementById("new-key-preset").value;
|
||
var ttl = parseInt(document.getElementById("new-key-ttl").value) || 0;
|
||
|
||
try {
|
||
var resp = await fetch("./api/apikeys", {
|
||
method: "POST",
|
||
headers: {"Content-Type": "application/json"},
|
||
body: JSON.stringify({name: name, preset: preset, ttl: ttl})
|
||
});
|
||
var data = await resp.json();
|
||
if (data.ok) {
|
||
document.getElementById("create-result").style.display = "block";
|
||
document.getElementById("new-key-display").textContent = data.key;
|
||
loadKeys();
|
||
} else {
|
||
alert("创建失败: " + (data.error || "未知错误"));
|
||
}
|
||
} catch(e) { alert("请求失败: " + e); }
|
||
}
|
||
|
||
// ── 删除 ──
|
||
async function deleteKey(fullKey, name) {
|
||
if (!confirm("确认删除 Key '" + name + "'? 此操作不可撤销。")) return;
|
||
try {
|
||
var resp = await fetch("./api/apikeys", {
|
||
method: "DELETE",
|
||
headers: {"Content-Type": "application/json"},
|
||
body: JSON.stringify({key: fullKey})
|
||
});
|
||
var data = await resp.json();
|
||
if (data.ok) loadKeys();
|
||
else alert("删除失败: " + (data.error || "未知错误"));
|
||
} catch(e) { alert("请求失败: " + e); }
|
||
}
|
||
|
||
// ── 初始加载 ──
|
||
loadKeys();
|
||
loadVersion();
|
||
loadBackups();
|
||
</script>
|
||
</div>
|