系统设置增加「系统更新」Tab:中控本机更新与客户端远程更新,实例页入口移除。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-10-04 07:24:36 +08:00
parent f2b29870ae
commit bce446178d
8 changed files with 497 additions and 199 deletions
+275
View File
@@ -0,0 +1,275 @@
(function (global) {
function sleep(ms) {
return new Promise(function (resolve) {
setTimeout(resolve, ms);
});
}
async function fetchJson(url, opts) {
const r = await fetch(url, Object || {});
let data = {};
try {
data = await r.json();
} catch (e) {
data = {};
}
return { httpOk: r.ok, status: r.status, data: data };
}
function escapeHtml(s) {
return String(s)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
function fmtVer(v) {
if (!v) return "—";
if (v.online === false) return v.msg || "离线";
const short = v.short_sha || (v.sha ? String(v.sha).slice(0, 7) : "");
const branch = v.branch ? v.branch + " · " : "";
if (!short) return v.msg || (v.ok ? "未知版本" : "不可用");
return branch + short + (v.dirty ? " (dirty)" : "");
}
function setStatus(el, text, cls) {
if (!el) return;
el.textContent = text || "";
el.className = "update-status" + (cls ? " " + cls : "");
}
async function waitRecover(versionUrl, beforeSha, statusEl, label) {
let sawDown = false;
const maxTries = 45;
for (let i = 0; i < maxTries; i++) {
await sleep(2000);
const res = await fetchJson(versionUrl);
const v = res.data || {};
const online = res.httpOk && (v.ok !== false) && (v.online !== false);
if (!online) {
sawDown = true;
setStatus(statusEl, "已受理 → 等待上线… (" + (i + 1) + "/" + maxTries + ")", "pending");
continue;
}
const afterSha = v.sha || "";
const afterShort = v.short_sha || (afterSha ? afterSha.slice(0, 7) : "");
const beforeShort = beforeSha ? String(beforeSha).slice(0, 7) : "";
if (afterSha && beforeSha && afterSha !== beforeSha) {
setStatus(statusEl, (label || "更新完成") + ": " + (beforeShort || "?") + " → " + (afterShort || "?"), "ok");
return v;
}
if (sawDown) {
setStatus(
statusEl,
afterSha && beforeSha && afterSha === beforeSha
? "已重启上线(已是最新 " + (afterShort || "") + ")"
: "已上线 " + (afterShort || ""),
"ok"
);
return v;
}
setStatus(statusEl, "更新中… (" + (i + 1) + "/" + maxTries + ")", "pending");
}
setStatus(statusEl, "超时未确认上线,请稍后刷新版本或查服务器日志", "err");
return null;
}
async function refreshHubVersion() {
const verEl = document.getElementById("hub-update-version");
if (!verEl) return null;
verEl.textContent = "探测中…";
verEl.className = "update-version";
const res = await fetchJson("/api/settings/update/hub/version");
const v = res.data || {};
if (res.httpOk && v.ok) {
verEl.textContent = fmtVer(v);
verEl.className = "update-version online";
} else {
verEl.textContent = v.msg || "读取失败";
verEl.className = "update-version offline";
}
return v;
}
async function runHubUpdate() {
const btn = document.getElementById("hub-update-btn");
const statusEl = document.getElementById("hub-update-status");
if (!btn) return;
if (
!window.confirm(
"确认更新中控?\n\n将执行 git pull + pm2 restart。\n页面会短暂不可用,完成后自动探测版本。"
)
) {
return;
}
btn.disabled = true;
setStatus(statusEl, "读取当前版本…", "pending");
const before = await refreshHubVersion();
const beforeSha = (before && before.sha) || "";
setStatus(statusEl, "下发更新指令…", "pending");
const res = await fetchJson("/api/settings/update/hub", {
method: "POST",
headers: { "Content-Type": "application/json", Accept: "application/json" },
body: "{}",
});
const data = res.data || {};
if (!data.ok) {
setStatus(statusEl, data.msg || "更新失败", "err");
btn.disabled = false;
return;
}
setStatus(statusEl, "已受理 → 等待上线…", "pending");
try {
await waitRecover(
"/api/settings/update/hub/version",
beforeSha || data.before_sha || "",
statusEl,
"中控更新完成"
);
await refreshHubVersion();
} finally {
btn.disabled = false;
}
}
async function refreshClientRow(row) {
const iid = row.getAttribute("data-id");
const verEl = row.querySelector("[data-client-version]");
if (!iid || !verEl) return null;
verEl.textContent = "探测中…";
verEl.className = "update-version";
const res = await fetchJson("/api/instances/" + iid + "/version");
const v = res.data || {};
if (v.online) {
verEl.textContent = fmtVer(Object.assign({ online: true }, v));
verEl.className = "update-version online";
} else {
verEl.textContent = v.msg || "离线/不可达";
verEl.className = "update-version offline";
}
return v;
}
async function runClientUpdate(row) {
const iid = row.getAttribute("data-id");
const name = row.getAttribute("data-name") || ("#" + iid);
const btn = row.querySelector("[data-client-update]");
const statusEl = row.querySelector("[data-client-status]");
if (!iid || !btn) return;
if (
!window.confirm(
"确认远程更新「" +
name +
"」?\n\n将执行 git pull + pm2 restart。\n有持仓时服务会短暂中断,请谨慎。"
)
) {
return;
}
btn.disabled = true;
setStatus(statusEl, "读取当前版本…", "pending");
const before = await refreshClientRow(row);
const beforeSha = (before && before.sha) || "";
setStatus(statusEl, "下发更新指令…", "pending");
const res = await fetchJson("/api/instances/" + iid + "/update", {
method: "POST",
headers: { "Content-Type": "application/json", Accept: "application/json" },
body: "{}",
});
const data = res.data || {};
if (!data.ok) {
setStatus(statusEl, data.msg || "更新失败", "err");
btn.disabled = false;
return;
}
setStatus(statusEl, "已受理 → 等待上线…", "pending");
try {
await waitRecover(
"/api/instances/" + iid + "/version",
beforeSha || data.before_sha || "",
statusEl,
"更新完成"
);
await refreshClientRow(row);
} finally {
btn.disabled = false;
}
}
async function loadClients() {
const body = document.getElementById("client-update-body");
if (!body) return;
body.innerHTML = '<tr><td colspan="4" class="muted">加载中…</td></tr>';
try {
const res = await fetchJson("/api/settings/update/clients");
const items = (res.data && res.data.items) || [];
if (!items.length) {
body.innerHTML = '<tr><td colspan="4" class="muted">暂无登记实例</td></tr>';
return;
}
body.innerHTML = items
.map(function (it) {
const id = it.id;
const name = it.name || "#" + id;
const enabled = !!it.enabled;
return (
'<tr class="client-update-row" data-id="' +
escapeHtml(String(id)) +
'" data-name="' +
escapeHtml(name) +
'">' +
"<td><strong>" +
escapeHtml(name) +
"</strong><div class=\"muted\" style=\"font-size:.75rem\">" +
escapeHtml(it.base_url || "") +
"</div></td>" +
'<td><span class="update-version muted" data-client-version>—</span></td>' +
"<td>" +
(enabled ? "启用" : '<span class="muted">停用</span>') +
"</td>" +
'<td style="white-space:nowrap">' +
'<button type="button" class="btn btn-sm" data-client-update' +
(enabled ? "" : " disabled title=\"实例未启用\"") +
">远程更新</button> " +
'<button type="button" class="btn btn-sm btn-secondary" data-client-refresh>刷新版本</button>' +
' <span class="update-status muted" data-client-status></span>' +
"</td>" +
"</tr>"
);
})
.join("");
body.querySelectorAll(".client-update-row").forEach(function (row) {
const upd = row.querySelector("[data-client-update]");
const ref = row.querySelector("[data-client-refresh]");
if (upd) {
upd.addEventListener("click", function () {
runClientUpdate(row);
});
}
if (ref) {
ref.addEventListener("click", function () {
refreshClientRow(row);
});
}
refreshClientRow(row);
});
} catch (e) {
body.innerHTML =
'<tr><td colspan="4" class="muted">' + escapeHtml((e && e.message) || "加载失败") + "</td></tr>";
}
}
function boot() {
const hubBtn = document.getElementById("hub-update-btn");
const hubRefresh = document.getElementById("hub-update-refresh");
const clientsRefresh = document.getElementById("client-update-refresh");
if (hubBtn) hubBtn.addEventListener("click", runHubUpdate);
if (hubRefresh) hubRefresh.addEventListener("click", refreshHubVersion);
if (clientsRefresh) clientsRefresh.addEventListener("click", loadClients);
refreshHubVersion();
loadClients();
}
global.HubSettingsUpdate = { boot: boot };
})(window);