8e2576661a
Co-authored-by: Cursor <cursoragent@cursor.com>
156 lines
4.9 KiB
JavaScript
156 lines
4.9 KiB
JavaScript
(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();
|
|
}
|
|
})();
|