const $ = (id) => document.getElementById(id); const state = { csrf: "", requests: [], selected: null }; const eventNames = { request_created: "铸澜提交申请", human_approved: "主人批准边界", human_rejected: "主人拒绝申请", human_revoked: "主人撤销能力", capability_picked_up: "铸澜领取临时能力", capability_verified: "运行层校验能力", capability_expired: "临时能力到期", request_expired: "申请单到期" }; async function api(path, options = {}) { const headers = { "Content-Type": "application/json", ...(options.headers || {}) }; const response = await fetch(path, { credentials: "same-origin", ...options, headers }); let data = {}; try { data = await response.json(); } catch (_) {} if (!response.ok) throw new Error(data.error || `HTTP_${response.status}`); return data; } function toast(message) { const node = $("toast"); node.textContent = message; node.classList.add("show"); window.setTimeout(() => node.classList.remove("show"), 2600); } function humanAction(value) { return ({ read: "读取", edit: "编辑", test: "运行测试", commit: "提交", push_candidate: "推送候选分支" })[value] || value; } function stateLabel(value) { return ({ PENDING: "等待批准", APPROVED: "已批准", REJECTED: "已拒绝", REVOKED: "已撤销", EXPIRED: "已过期" })[value] || value; } function renderRequest(request) { state.selected = request; $("empty-state").classList.add("hidden"); $("request-card").classList.remove("hidden"); $("request-id").textContent = request.id; $("request-description").textContent = request.description; $("persona").textContent = request.persona_id; $("development-id").textContent = request.development_id; $("repository").textContent = request.repository; $("branch").textContent = request.branch; $("base-sha").textContent = request.base_sha; $("actions").replaceChildren(...request.actions.map((action) => { const node = document.createElement("span"); node.className = "chip"; node.textContent = humanAction(action); return node; })); $("paths").replaceChildren(...request.paths.map((path) => { const node = document.createElement("span"); node.className = "path"; node.textContent = path; return node; })); const label = stateLabel(request.state); for (const id of ["request-state", "card-state"]) { const node = $(id); node.textContent = label; node.className = `state ${request.state === "APPROVED" ? "approved" : request.state === "REJECTED" ? "rejected" : "waiting"}`; } $("decision-area").classList.toggle("hidden", request.state !== "PENDING"); $("revoke-area").classList.toggle("hidden", request.state !== "APPROVED"); loadReceipts(request.id); } function renderEmpty(message, detail) { state.selected = null; $("request-card").classList.add("hidden"); const empty = $("empty-state"); empty.classList.remove("hidden"); empty.querySelector("h3").textContent = message; empty.querySelector("p").textContent = detail; $("request-state").textContent = "湖面安静"; } async function loadReceipts(requestId) { try { const data = await api(`api/v1/owner/requests/${encodeURIComponent(requestId)}/receipts`); const nodes = data.receipts.map((receipt) => { const li = document.createElement("li"); li.className = receipt.result === "PASS" || receipt.result === "APPROVED" || receipt.result === "PENDING" ? "pass" : "fail"; const dot = document.createElement("i"); const body = document.createElement("div"); const title = document.createElement("strong"); title.textContent = eventNames[receipt.event] || receipt.event; const detail = document.createElement("span"); detail.textContent = `${receipt.created_at} · ${receipt.result} · ${receipt.receipt_hash.slice(0, 12)}`; body.append(title, detail); li.append(dot, body); return li; }); $("timeline").replaceChildren(...nodes); } catch (_) { $("timeline").innerHTML = "
  • 回执读取失败运行层没有返回可验证结果。
  • "; } } async function loadRequests(preferredId = "") { const data = await api("api/v1/owner/requests"); state.requests = data.requests; const preferred = preferredId && data.requests.find((item) => item.id === preferredId); const pending = data.requests.find((item) => item.state === "PENDING"); const selected = preferred || pending || data.requests[0]; if (selected) renderRequest(selected); else renderEmpty("暂无真实申请", "铸澜提交结构化申请后会出现在这里;公开建单本身没有执行权。"); } async function restoreSession() { try { const session = await api("api/v1/owner/session"); state.csrf = session.csrf; $("auth-title").textContent = `主人 ${session.owner_login} · 已验证`; $("auth-detail").textContent = "审批会话有效。任何扩大仓库、路径、动作或期限的请求都必须重新批准。"; $("login-open").classList.add("hidden"); $("logout").classList.remove("hidden"); const preferred = new URLSearchParams(location.search).get("request") || ""; await loadRequests(preferred); const oauthReturn = new URLSearchParams(location.search).get("oauth_return"); if (oauthReturn) { try { const normalized = oauthReturn.replace(/-/g, "+").replace(/_/g, "/"); const decoded = atob(normalized + "=".repeat((4 - normalized.length % 4) % 4)); if (decoded.startsWith("/oauth/authorize?")) location.assign(`/zhulan${decoded}`); } catch (_) {} } } catch (_) { state.csrf = ""; $("login-open").classList.remove("hidden"); $("logout").classList.add("hidden"); } } $("login-open").addEventListener("click", () => $("login-dialog").showModal()); $("login-form").addEventListener("submit", async (event) => { event.preventDefault(); $("login-error").textContent = ""; const button = event.currentTarget.querySelector("button[type=submit]"); button.disabled = true; try { const result = await api("api/v1/owner/login", { method: "POST", body: JSON.stringify({ username: $("username").value, password: $("password").value }) }); state.csrf = result.csrf; $("password").value = ""; $("login-dialog").close(); toast("代码频道身份验证通过"); await restoreSession(); } catch (_) { $("login-error").textContent = "账号验证失败。没有创建审批会话。"; } finally { button.disabled = false; } }); $("approve").addEventListener("click", async () => { if (!state.selected || state.selected.state !== "PENDING") return; const exact = `${state.selected.repository}\n${state.selected.branch}\n${state.selected.paths.join(",")}`; if (!window.confirm(`批准这一段开发?\n\n${exact}\n\n批准不会开放 root 或任意 shell。`)) return; $("approve").disabled = true; try { await api(`api/v1/owner/requests/${state.selected.id}/approve`, { method: "POST", headers: { "X-CSRF-Token": state.csrf }, body: JSON.stringify({ ttl_seconds: Number($("ttl").value) }) }); toast("已批准。铸澜可领取一次临时能力。"); await restoreSession(); } catch (error) { toast(`批准失败:${error.message}`); } finally { $("approve").disabled = false; } }); $("reject").addEventListener("click", async () => { if (!state.selected || state.selected.state !== "PENDING") return; const reason = window.prompt("写一句拒绝原因,铸澜会据此修正申请:", "范围需要重新确认"); if (reason === null) return; try { await api(`api/v1/owner/requests/${state.selected.id}/reject`, { method: "POST", headers: { "X-CSRF-Token": state.csrf }, body: JSON.stringify({ reason }) }); toast("已拒绝并留下回执。"); await restoreSession(); } catch (error) { toast(`拒绝失败:${error.message}`); } }); $("revoke").addEventListener("click", async () => { if (!state.selected || state.selected.state !== "APPROVED") return; if (!window.confirm("立即撤销这段开发能力?已连接会话会在下一次调用时被服务器驳回。")) return; try { await api(`api/v1/owner/requests/${state.selected.id}/revoke`, { method: "POST", headers: { "X-CSRF-Token": state.csrf }, body: JSON.stringify({ reason: "主人在审批端主动撤销" }) }); toast("能力已撤销,原连接不再有效。"); await restoreSession(); } catch (error) { toast(`撤销失败:${error.message}`); } }); $("logout").addEventListener("click", async () => { try { await api("api/v1/owner/logout", { method: "POST", headers: { "X-CSRF-Token": state.csrf }, body: "{}" }); } catch (_) {} location.reload(); }); restoreSession();