实例页增加远程更新客户端:下发 git pull + pm2 restart,并轮询版本确认上线。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-10-04 07:11:15 +08:00
parent f7ffd08274
commit 8e2576661a
6 changed files with 370 additions and 55 deletions
+30
View File
@@ -330,6 +330,36 @@ button, .btn {
}
.check { display: flex !important; align-items: center; gap: 8px; }
.inst-row { margin: 10px 0; border: 1px solid var(--line); border-radius: 10px; padding: 8px 12px; }
.inst-row > summary {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px 12px;
cursor: pointer;
}
.inst-version {
font-size: 0.78rem;
font-variant-numeric: tabular-nums;
margin-left: auto;
}
.inst-version.online { color: var(--ok); }
.inst-version.offline { color: var(--bad); }
.inst-remote-actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
margin: 10px 0 4px;
}
.inst-remote-actions .btn { margin-top: 0; }
.inst-update-hint { margin: 0 0 12px; font-size: 0.78rem; }
.inst-update-status {
font-size: 0.8rem;
min-height: 1.2em;
}
.inst-update-status.pending { color: var(--accent-2); }
.inst-update-status.ok { color: var(--ok); }
.inst-update-status.err { color: var(--bad); }
.overview-table-wrap {
overflow-x: auto;
margin-top: 12px;
+155
View File
@@ -0,0 +1,155 @@
(function () {
function sleep(ms) {
return new Promise(function (resolve) {
setTimeout(resolve, ms);
});
}
async function fetchJson(url, opts) {
const r = await fetch(url, opts || {});
let data = {};
try {
data = await r.json();
} catch (e) {
data = {};
}
return { httpOk: r.ok, status: r.status, data: data };
}
function setStatus(el, text, cls) {
if (!el) return;
el.textContent = text || "";
el.className = "inst-update-status" + (cls ? " " + cls : "");
}
function fmtVer(v) {
if (!v || !v.online) return "离线";
const short = v.short_sha || (v.sha ? String(v.sha).slice(0, 7) : "");
const branch = v.branch ? v.branch + " · " : "";
if (!short) return v.msg || "未知版本";
return branch + short + (v.dirty ? " (dirty)" : "");
}
async function refreshVersion(row) {
const iid = row.getAttribute("data-id");
const verEl = row.querySelector("[data-inst-version]");
if (!iid || !verEl) return null;
verEl.textContent = "探测中…";
const res = await fetchJson("/api/instances/" + iid + "/version");
const v = res.data || {};
if (v.online) {
verEl.textContent = fmtVer(v);
verEl.className = "inst-version online";
} else {
verEl.textContent = (v.msg || "离线/不可达");
verEl.className = "inst-version offline";
}
return v;
}
async function waitForUpdate(row, beforeSha) {
const statusEl = row.querySelector("[data-inst-update-status]");
const iid = row.getAttribute("data-id");
let sawDown = false;
const maxTries = 45;
for (let i = 0; i < maxTries; i++) {
await sleep(2000);
const res = await fetchJson("/api/instances/" + iid + "/version");
const v = res.data || {};
if (!v.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,
"更新完成: " + (beforeShort || "?") + " → " + (afterShort || "?"),
"ok"
);
await refreshVersion(row);
return true;
}
if (sawDown) {
setStatus(
statusEl,
afterSha && beforeSha && afterSha === beforeSha
? "已重启上线(已是最新 " + (afterShort || "") + ")"
: "已上线 " + (afterShort || ""),
"ok"
);
await refreshVersion(row);
return true;
}
setStatus(statusEl, "更新中… (" + (i + 1) + "/" + maxTries + ")", "pending");
}
setStatus(statusEl, "超时未确认上线,请稍后刷新版本或查客户端日志", "err");
await refreshVersion(row);
return false;
}
async function runUpdate(row) {
const iid = row.getAttribute("data-id");
const name = row.getAttribute("data-name") || ("#" + iid);
const btn = row.querySelector("[data-inst-update]");
const statusEl = row.querySelector("[data-inst-update-status]");
if (!iid || !btn) return;
const ok = window.confirm(
"确认远程更新「" +
name +
"」?\n\n将执行 git pull + pm2 restart。\n有持仓时服务会短暂中断,请谨慎。"
);
if (!ok) return;
btn.disabled = true;
setStatus(statusEl, "读取当前版本…", "pending");
const before = await refreshVersion(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 waitForUpdate(row, beforeSha || data.before_sha || "");
} finally {
btn.disabled = false;
}
}
function bind() {
document.querySelectorAll(".inst-row[data-id]").forEach(function (row) {
const updateBtn = row.querySelector("[data-inst-update]");
const refreshBtn = row.querySelector("[data-inst-refresh-ver]");
if (updateBtn) {
updateBtn.addEventListener("click", function () {
runUpdate(row);
});
}
if (refreshBtn) {
refreshBtn.addEventListener("click", function () {
refreshVersion(row);
});
}
refreshVersion(row);
});
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", bind);
} else {
bind();
}
})();