${escapeHtml(message.content)}
const state = { sessions: [], messages: [], tasks: [], approvals: [], workers: [], events: [], selectedSessionId: null, }; 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"), }; function escapeHtml(input) { return String(input) .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'"); } 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) { throw new Error(payload.error || `${response.status} ${response.statusText}`); } return payload; } function selectedSession() { return state.sessions.find((session) => session.id === state.selectedSessionId) ?? 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 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 .map( (worker) => `当前没有消息。
`; } function renderTasks() { const latestPlan = [...state.events] .reverse() .find((event) => event.sessionId === state.selectedSessionId && event.type === "plan.created"); if (latestPlan) { const pausedCount = Array.isArray(latestPlan.payload.pausedTaskIds) ? latestPlan.payload.pausedTaskIds.length : 0; elements.planHint.textContent = `最新计划创建了 ${latestPlan.payload.taskIds.length} 个任务,自动暂停旧任务 ${pausedCount} 个。`; } else { elements.planHint.textContent = ""; } const tasks = tasksForSelectedSession(); elements.taskList.innerHTML = tasks.length ? tasks .map( (task) => `${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 request(`/api/tasks/${taskId}/${action}`, { method: "POST", body: "{}" }); await loadSession(state.selectedSessionId); await loadBootstrap(); render(); }); }); } function renderApprovals() { const approvals = approvalsForSelectedSession(); elements.approvalList.innerHTML = approvals.length ? approvals .map( (approval) => `当前没有待审批项。
`; elements.approvalList.querySelectorAll("[data-approval-id]").forEach((button) => { button.addEventListener("click", async () => { const approvalId = button.dataset.approvalId; const approved = button.dataset.approved === "true"; await request(`/api/approvals/${approvalId}/respond`, { method: "POST", body: JSON.stringify({ approved, responder: "web-user" }), }); await loadSession(state.selectedSessionId); await loadBootstrap(); render(); }); }); } function renderEvents() { const events = eventsForSelectedSession(); elements.eventList.innerHTML = events.length ? events .map( (event) => `${escapeHtml(JSON.stringify(event.payload, null, 2))}
当前没有事件。
`; } function render() { 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, ]; } elements.createSessionForm.addEventListener("submit", async (event) => { event.preventDefault(); const title = elements.sessionTitleInput.value.trim(); const details = await request("/api/sessions", { method: "POST", body: JSON.stringify({ title }), }); 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); await request("/api/workers/register", { method: "POST", body: JSON.stringify({ name, os, capabilities }), }); elements.workerName.value = ""; elements.workerCapabilities.value = "terminal"; await loadBootstrap(); render(); }); elements.messageForm.addEventListener("submit", async (event) => { event.preventDefault(); if (!state.selectedSessionId) return; const content = elements.messageInput.value.trim(); if (!content) return; await request(`/api/sessions/${state.selectedSessionId}/messages`, { method: "POST", body: JSON.stringify({ content, channel: "web" }), }); elements.messageInput.value = ""; await loadSession(state.selectedSessionId); await loadBootstrap(); render(); }); elements.archiveSession.addEventListener("click", async () => { if (!state.selectedSessionId) return; await request(`/api/sessions/${state.selectedSessionId}/archive`, { method: "POST", body: "{}", }); await loadSession(state.selectedSessionId); await loadBootstrap(); render(); }); elements.resetDemo.addEventListener("click", async () => { await request("/api/demo/reset", { method: "POST", body: "{}" }); state.sessions = []; state.messages = []; state.tasks = []; state.workers = []; state.approvals = []; state.events = []; state.selectedSessionId = null; render(); }); const stream = new EventSource("/api/events/stream"); stream.onmessage = async (event) => { const payload = JSON.parse(event.data); state.events.push(payload); if (payload.sessionId) { await loadSession(payload.sessionId); } await loadBootstrap(); render(); }; loadBootstrap().then(render).catch((error) => { console.error(error); elements.sessionSummary.textContent = error.message; });