${escapeHtml(step.detail)}
const state = { sessions: [], messages: [], tasks: [], approvals: [], workers: [], events: [], selectedSessionId: null, connection: "connecting", banner: null, }; const TASK_GROUPS = [ { title: "进行中", statuses: ["assigned", "running"] }, { title: "等待处理", statuses: ["planning", "queued"] }, { title: "等待审批或阻塞", statuses: ["waiting_approval", "blocked", "paused"] }, { title: "已完成", statuses: ["completed"] }, { title: "已结束", statuses: ["failed", "cancelled"] }, ]; const elements = { sessionList: document.querySelector("#session-list"), workerList: document.querySelector("#worker-list"), messageList: document.querySelector("#message-list"), taskList: document.querySelector("#task-list"), approvalList: document.querySelector("#approval-list"), eventList: document.querySelector("#event-list"), planHint: document.querySelector("#plan-hint"), sessionTitleDisplay: document.querySelector("#session-title-display"), sessionSummary: document.querySelector("#session-summary"), createSessionForm: document.querySelector("#create-session-form"), sessionTitleInput: document.querySelector("#session-title"), createWorkerForm: document.querySelector("#create-worker-form"), workerName: document.querySelector("#worker-name"), workerOs: document.querySelector("#worker-os"), workerCapabilities: document.querySelector("#worker-capabilities"), messageForm: document.querySelector("#message-form"), messageInput: document.querySelector("#message-input"), resetDemo: document.querySelector("#reset-demo"), archiveSession: document.querySelector("#archive-session"), streamStatus: document.querySelector("#stream-status"), appBanner: document.querySelector("#app-banner"), onboarding: document.querySelector("#onboarding"), }; let bannerTimer = null; let fallbackPoll = null; function escapeHtml(input) { return String(input) .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'"); } function setBanner(message, tone = "info", persistent = false) { state.banner = { message, tone }; renderBanner(); if (bannerTimer) { window.clearTimeout(bannerTimer); bannerTimer = null; } if (!persistent) { bannerTimer = window.setTimeout(() => { if (state.banner?.message === message) { clearBanner(); } }, 3200); } } function clearBanner() { state.banner = null; renderBanner(); } function renderBanner() { if (!state.banner) { elements.appBanner.className = "banner hidden"; elements.appBanner.textContent = ""; return; } elements.appBanner.className = `banner ${state.banner.tone}`; elements.appBanner.textContent = state.banner.message; } async function request(url, options = {}) { const response = await fetch(url, { headers: { "Content-Type": "application/json" }, ...options, }); let payload = {}; try { payload = await response.json(); } catch { payload = {}; } if (!response.ok) { const message = payload.message || payload.error || `${response.status} ${response.statusText}`; throw new Error(message); } return payload; } async function runAction(label, operation, successMessage, options = {}) { const { refreshBootstrap = true, refreshSession = Boolean(state.selectedSessionId), persistent = false, } = options; setBanner(`${label}中...`, "info", true); try { const result = await operation(); if (refreshBootstrap) { await loadBootstrap(); } if (refreshSession && state.selectedSessionId) { await loadSession(state.selectedSessionId); } render(); if (successMessage) { setBanner(successMessage, "success", persistent); } else { clearBanner(); } return result; } catch (error) { console.error(error); setBanner(`${label}失败:${error.message}`, "error", true); render(); return null; } } function selectedSession() { return state.sessions.find((session) => session.id === state.selectedSessionId) ?? null; } function findTask(taskId) { return state.tasks.find((task) => task.id === taskId) ?? null; } function findWorker(workerId) { return state.workers.find((worker) => worker.id === workerId) ?? null; } function tasksForSelectedSession() { return state.tasks.filter((task) => task.sessionId === state.selectedSessionId); } function messagesForSelectedSession() { return state.messages.filter((message) => message.sessionId === state.selectedSessionId); } function approvalsForSelectedSession() { return state.approvals.filter((approval) => approval.sessionId === state.selectedSessionId); } function eventsForSelectedSession() { return state.events .filter((event) => event.sessionId === null || event.sessionId === state.selectedSessionId) .slice(-50) .reverse(); } function formatClock(value) { return new Date(value).toLocaleTimeString(); } function formatAge(value) { const diffMs = Date.now() - new Date(value).getTime(); const seconds = Math.max(0, Math.round(diffMs / 1000)); if (seconds < 5) { return "刚刚"; } if (seconds < 60) { return `${seconds} 秒前`; } const minutes = Math.round(seconds / 60); if (minutes < 60) { return `${minutes} 分钟前`; } const hours = Math.round(minutes / 60); return `${hours} 小时前`; } function workerHealth(worker) { if (worker.status === "offline") { return { tone: "offline", label: "离线", description: "已手动下线或被系统判定为离线。", }; } const ageMs = Date.now() - new Date(worker.lastSeenAt).getTime(); if (ageMs > 30_000) { return { tone: "stale", label: "疑似掉线", description: `最近心跳 ${formatAge(worker.lastSeenAt)}`, }; } if (ageMs > 10_000) { return { tone: "lagging", label: "连接抖动", description: `最近心跳 ${formatAge(worker.lastSeenAt)}`, }; } return { tone: "live", label: "在线", description: `最近心跳 ${formatAge(worker.lastSeenAt)}`, }; } function renderSessions() { if (state.sessions.length === 0) { elements.sessionList.innerHTML = `
先创建一个项目会话。
`; return; } elements.sessionList.innerHTML = state.sessions .map((session) => { const active = session.id === state.selectedSessionId ? "active" : ""; const archived = session.status === "archived" ? "archived" : ""; return ` `; }) .join(""); elements.sessionList.querySelectorAll("[data-session-id]").forEach((button) => { button.addEventListener("click", async () => { state.selectedSessionId = button.dataset.sessionId; await loadSession(state.selectedSessionId); render(); }); }); } function renderWorkers() { if (state.workers.length === 0) { elements.workerList.innerHTML = `还没有 worker。可以手动注册,或直接运行 \`npm run demo\`。
`; return; } elements.workerList.innerHTML = [...state.workers] .sort((left, right) => left.name.localeCompare(right.name)) .map((worker) => { const currentTask = findTask(worker.currentTaskId); const health = workerHealth(worker); return `${escapeHtml(step.detail)}
当前没有消息。
`; } function taskGroupSections() { const tasks = tasksForSelectedSession(); return TASK_GROUPS.map((group) => ({ ...group, tasks: tasks.filter((task) => group.statuses.includes(task.status)), })).filter((group) => group.tasks.length > 0); } function renderTaskCard(task) { const worker = findWorker(task.assignedWorkerId); const dependencyTitles = task.dependencyIds .map((taskId) => findTask(taskId)?.title) .filter(Boolean); const pendingApproval = approvalsForSelectedSession().find( (approval) => approval.taskId === task.id && approval.status === "pending", ); return `${escapeHtml(task.description)}
当前没有任务。
`; elements.taskList.querySelectorAll("[data-action]").forEach((button) => { button.addEventListener("click", async () => { const taskId = button.dataset.taskId; const action = button.dataset.action; await runAction( "更新任务", () => request(`/api/tasks/${taskId}/${action}`, { method: "POST", body: "{}" }), "任务状态已更新。", ); }); }); } function renderApprovals() { const approvals = approvalsForSelectedSession(); elements.approvalList.innerHTML = approvals.length ? approvals .map((approval) => { const task = findTask(approval.taskId); const followUpText = approval.status === "pending" ? "批准后任务会重新进入队列;拒绝后任务会被取消。" : approval.status === "approved" ? "这条审批已经通过,对应任务可继续执行。" : "这条审批已被拒绝,对应任务已停止。"; return `${escapeHtml(approval.summary)}
当前没有待审批项。
`; elements.approvalList.querySelectorAll("[data-approval-id]").forEach((button) => { button.addEventListener("click", async () => { const approvalId = button.dataset.approvalId; const approved = button.dataset.approved === "true"; await runAction( approved ? "批准审批" : "拒绝审批", () => request(`/api/approvals/${approvalId}/respond`, { method: "POST", body: JSON.stringify({ approved, responder: "web-user" }), }), approved ? "审批已通过。" : "审批已拒绝。", ); }); }); } function renderEvents() { const events = eventsForSelectedSession(); elements.eventList.innerHTML = events.length ? events .map( (event) => `${escapeHtml(JSON.stringify(event.payload, null, 2))}
当前没有事件。
`; } function render() { renderBanner(); renderStreamStatus(); renderOnboarding(); renderSessions(); renderWorkers(); renderSessionHeader(); renderMessages(); renderTasks(); renderApprovals(); renderEvents(); } async function loadBootstrap() { const bootstrap = await request("/api/bootstrap"); state.sessions = bootstrap.sessions; state.messages = bootstrap.messages; state.tasks = bootstrap.tasks; state.workers = bootstrap.workers; state.approvals = bootstrap.approvals; state.events = bootstrap.events; if (!state.selectedSessionId && state.sessions[0]) { state.selectedSessionId = state.sessions[0].id; } } async function loadSession(sessionId) { if (!sessionId) return; const details = await request(`/api/sessions/${sessionId}`); state.sessions = state.sessions.map((session) => (session.id === sessionId ? details.session : session)); state.messages = [...state.messages.filter((message) => message.sessionId !== sessionId), ...details.messages]; state.tasks = [...state.tasks.filter((task) => task.sessionId !== sessionId), ...details.tasks]; state.approvals = [ ...state.approvals.filter((approval) => approval.sessionId !== sessionId), ...details.approvals, ]; } function startFallbackPolling() { if (fallbackPoll) { return; } fallbackPoll = window.setInterval(async () => { try { await loadBootstrap(); if (state.selectedSessionId) { await loadSession(state.selectedSessionId); } render(); } catch (error) { console.error(error); } }, 5000); } function stopFallbackPolling() { if (!fallbackPoll) { return; } window.clearInterval(fallbackPoll); fallbackPoll = null; } elements.createSessionForm.addEventListener("submit", async (event) => { event.preventDefault(); const title = elements.sessionTitleInput.value.trim(); const details = await runAction( "创建会话", () => request("/api/sessions", { method: "POST", body: JSON.stringify({ title }), }), "项目会话已创建。", { refreshBootstrap: false, refreshSession: false }, ); if (!details) { return; } state.sessions.unshift(details.session); state.selectedSessionId = details.session.id; await loadSession(details.session.id); elements.sessionTitleInput.value = ""; render(); }); elements.createWorkerForm.addEventListener("submit", async (event) => { event.preventDefault(); const name = elements.workerName.value.trim(); if (!name) return; const os = elements.workerOs.value; const capabilities = elements.workerCapabilities.value .split(",") .map((item) => item.trim()) .filter(Boolean); const result = await runAction( "注册 worker", () => request("/api/workers/register", { method: "POST", body: JSON.stringify({ name, os, capabilities }), }), "worker 已注册。", { refreshSession: false }, ); if (!result) { return; } elements.workerName.value = ""; elements.workerCapabilities.value = "terminal"; }); elements.messageForm.addEventListener("submit", async (event) => { event.preventDefault(); if (!state.selectedSessionId) return; const content = elements.messageInput.value.trim(); if (!content) return; const result = await runAction( "发送需求", () => request(`/api/sessions/${state.selectedSessionId}/messages`, { method: "POST", body: JSON.stringify({ content, channel: "web" }), }), "需求已发送给主控。", ); if (!result) { return; } elements.messageInput.value = ""; }); elements.archiveSession.addEventListener("click", async () => { const session = selectedSession(); if (!session) return; const archived = session.status === "archived"; await runAction( archived ? "恢复会话" : "归档会话", () => request(`/api/sessions/${state.selectedSessionId}/${archived ? "restore" : "archive"}`, { method: "POST", body: "{}", }), archived ? "会话已恢复。" : "会话已归档。", ); }); elements.resetDemo.addEventListener("click", async () => { const result = await runAction( "重置 Demo", () => request("/api/demo/reset", { method: "POST", body: JSON.stringify({ preserveWorkers: true }), }), "Demo 数据已重置。", { refreshSession: false }, ); if (!result) { return; } state.sessions = []; state.messages = []; state.tasks = []; state.approvals = []; state.events = []; state.selectedSessionId = null; await loadBootstrap(); render(); }); const stream = new EventSource("/api/events/stream"); stream.onopen = () => { const wasReconnecting = state.connection === "reconnecting"; state.connection = "live"; stopFallbackPolling(); render(); if (wasReconnecting) { setBanner("实时事件流已恢复。", "success"); } }; stream.onmessage = async (event) => { const payload = JSON.parse(event.data); state.events.push(payload); if (payload.sessionId) { await loadSession(payload.sessionId); } await loadBootstrap(); render(); }; stream.onerror = () => { if (state.connection !== "reconnecting") { state.connection = "reconnecting"; setBanner("实时事件流暂时断开,正在重连并降级轮询。", "warn", true); startFallbackPolling(); render(); } }; loadBootstrap() .then(async () => { if (state.selectedSessionId) { await loadSession(state.selectedSessionId); } render(); }) .catch((error) => { console.error(error); setBanner(`初始化失败:${error.message}`, "error", true); render(); });