import { renderWorkspace, updateLiveStreamPanel } from "./agent-ui.js"; import { downloadMarkdown, sessionToMarkdown } from "./export.js"; import { renderIntakePanel } from "./intake-ui.js"; import { displaySkillPackLabel } from "./display-labels.js"; import { clearQuestionCardState, collectQuestionAnswers, getActiveQuestions, renderQuestionsCard, } from "./questions-ui.js"; let sessionId = null; let activeBookId = null; let lastView = null; let books = []; let composerForceInput = false; let sidebarNav = { level: "root" }; let bookSelectMode = false; let selectedBookIds = new Set(); const playSavesByBook = new Map(); const playSavesLoading = new Set(); let bookMenuBookId = null; let livePollTimer = null; const LIVE_POLL_MS = 280; const $ = (id) => document.getElementById(id); const PHASE = { idle: "待命", running: "执行中", waiting_user: "等待你", done: "已完成", error: "出错" }; const REASON = { skill_selection: "选择能力包", intake: "补充信息", input: "等待输入", approve_step: "确认执行", review_artifact: "验收产物", worker_questions: "回答提问", revision: "按反馈修改", }; /** @type {{ id: string, name: string, description: string }[]} */ let directors = []; async function api(path, options = {}) { const res = await fetch(path, { headers: { "Content-Type": "application/json" }, ...options, }); const data = await res.json(); if (!res.ok) throw new Error(data.error || "请求失败"); return data; } function esc(s) { return String(s).replace(/&/g, "&").replace(//g, ">"); } function statusFor(view, loading) { if (loading) return { cls: "running", text: "处理中…" }; if (view.phase === "done") return { cls: "done", text: "已完成" }; if (view.phase === "running" && !view.waitingReason) { return { cls: "running", text: view.focus?.action ?? "总管运行中" }; } if (view.waitingReason) { return { cls: "waiting", text: REASON[view.waitingReason.kind] ?? view.waitingReason.kind }; } return { cls: "", text: PHASE[view.phase] ?? view.phase }; } function isAgentBusy(view, loading) { if (loading) return true; return view.phase === "running" && !view.waitingReason; } function resolveComposer(view, loading) { if (view.phase === "done") return { mode: "idle", text: "会话已结束" }; if (view.phase === "error") { const send = view.actions?.find((a) => a.type === "send_message"); return { mode: "input", placeholder: send?.placeholder ?? "说明后重试,或直接发送继续…", hint: view.hints?.[0] ?? "执行出错,可在下方重试", }; } if (isAgentBusy(view, loading)) { return { mode: "waiting", text: view.focus?.action ?? "总管或 Worker 执行中…" }; } const confirmIntake = view.actions?.find((a) => a.type === "confirm_intake"); if (view.waitingReason?.kind === "intake" && view.intake) { const hasUser = (view.messages ?? []).some((m) => m.role === "user"); const send = view.actions?.find((a) => a.type === "send_message"); return { mode: view.intake.ready && confirmIntake ? "intake_ready" : "intake", intake: view.intake, intakePrompt: hasUser ? view.intakePrompt : null, showIntakePanel: hasUser, confirmIntake, placeholder: send?.placeholder ?? "描述你想创作什么…", }; } const approve = view.actions?.find((a) => a.type === "approve"); const accept = view.actions?.find((a) => a.type === "accept"); if (approve && !composerForceInput) { return { mode: "action", primary: approve, primaryType: "approve", hint: view.focus?.detail, showReject: true, }; } if (accept && view.waitingReason?.kind !== "review_artifact" && !composerForceInput) { return { mode: "action", primary: accept, primaryType: "accept", hint: "验收产物", showReject: true, }; } const send = view.actions?.find((a) => a.type === "send_message"); const hasQuestionsCard = (view.waitingReason?.kind === "worker_questions" && view.waitingReason?.questions?.length) || (view.waitingReason?.kind === "input" && view.waitingReason?.questions?.length) || (view.waitingReason?.kind === "review_artifact" && view.waitingReason?.questions?.length); if (view.waitingReason?.kind === "review_artifact") { const parseBroken = Boolean(view.reviewArtifact?.workerSetView?.parseError); return { mode: "input", placeholder: send?.placeholder ?? "输入修改意见后发送…", hint: hasQuestionsCard ? "接受产物即收起追问(表示无需再完善)" : null, acceptAction: accept ? { label: accept.label || "接受目前产物", disabled: parseBroken, disabledTitle: parseBroken ? "规格解析失败,请先发送修改意见让 Worker 重写" : undefined, } : null, }; } if (hasQuestionsCard && !composerForceInput) { return { mode: "input", placeholder: send?.placeholder ?? "也可在此自由补充…", hint: null, }; } if ( send || view.waitingReason?.kind === "input" || view.waitingReason?.kind === "worker_questions" || composerForceInput ) { const wr = view.waitingReason; if (wr?.kind === "worker_questions" && !hasQuestionsCard) { return { mode: "input", placeholder: "在此直接回答上面的问题…", hint: null, }; } const hint = view.hints?.[0] ?? null; return { mode: "input", placeholder: send?.placeholder ?? "输入消息…", hint }; } const finish = view.actions?.find((a) => a.type === "finish"); if (finish) { return { mode: "action", primary: finish, primaryType: "finish", hint: null, showReject: false }; } return { mode: "idle", text: "暂无可用操作" }; } function renderComposer(view, loading) { const root = $("composer"); if (!root) return; const spec = resolveComposer(view, loading); if (spec.mode === "waiting") { root.innerHTML = `
${esc(spec.text)}
`; return; } if (spec.mode === "idle") { root.innerHTML = `
${esc(spec.text)}
`; return; } if (spec.mode === "intake" || spec.mode === "intake_ready") { const intakePanel = spec.showIntakePanel ? `
${renderIntakePanel(spec.intake, { variant: "composer" })}
` : ""; const confirm = spec.mode === "intake_ready" && spec.confirmIntake ? `
` : ""; root.innerHTML = ` ${spec.intakePrompt ? `

${esc(spec.intakePrompt)}

` : ""} ${intakePanel} ${confirm}
`; wireComposerForm(); root.querySelector("[data-act=confirm_intake]")?.addEventListener("click", () => runAction("confirm_intake")); return; } if (spec.mode === "action") { const reject = spec.showReject ? ` ` : ""; root.innerHTML = ` ${spec.hint ? `

${esc(spec.hint)}

` : ""}
${reject}
`; root.querySelector(`[data-act="${spec.primaryType}"]`)?.addEventListener("click", () => runAction(spec.primaryType)); root.querySelector("[data-act=reject]")?.addEventListener("click", () => runAction("reject")); root.querySelector("[data-act=force-input]")?.addEventListener("click", () => { composerForceInput = true; renderComposer(view, loading); }); return; } const acceptBtn = spec.acceptAction ? `` : ""; root.innerHTML = ` ${spec.askBanner ? `
${esc(spec.askBanner.title)}${esc(spec.askBanner.body)}
` : ""} ${spec.hint ? `

${esc(spec.hint)}

` : ""}
${acceptBtn}
`; wireComposerForm(); root.querySelector("[data-act=accept]")?.addEventListener("click", () => runAction("accept")); } function wireComposerForm() { const form = $("composer-form"); const input = $("composer-input"); form?.addEventListener("submit", async (e) => { e.preventDefault(); await sendText(input?.value ?? ""); }); input?.addEventListener("keydown", (e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); form?.requestSubmit(); } }); input?.focus(); } function bookPlayReady(bookId) { return lastView?.bookId === bookId && Boolean(lastView?.playReady); } async function fetchPlaySavesForBook(bookId, force = false) { if (!force && (playSavesByBook.has(bookId) || playSavesLoading.has(bookId))) return; playSavesLoading.add(bookId); renderBookList(); try { const data = await api(`/api/books/${encodeURIComponent(bookId)}/saves`); playSavesByBook.set(bookId, data.saves ?? []); } catch { playSavesByBook.set(bookId, []); } finally { playSavesLoading.delete(bookId); renderBookList(); } } function setBookSelectMode(on, { keepSelection = false } = {}) { bookSelectMode = on; if (!on) selectedBookIds = new Set(); else if (!keepSelection) selectedBookIds = new Set(); renderBookList(); } function toggleBookSelection(bookId) { if (!bookSelectMode) setBookSelectMode(true, { keepSelection: true }); if (selectedBookIds.has(bookId)) selectedBookIds.delete(bookId); else selectedBookIds.add(bookId); renderBookList(); } function hideBookMenu() { const menu = $("book-action-menu"); if (menu) menu.hidden = true; bookMenuBookId = null; } function refreshBookMenuLabels() { const n = selectedBookIds.size; const multi = bookSelectMode && n > 0; $("book-menu-open")?.toggleAttribute("hidden", multi); $("book-menu-rename")?.toggleAttribute("hidden", n !== 1); const dup = $("book-menu-duplicate"); if (dup) dup.textContent = n > 1 ? `复制 ${n} 项备份` : "复制备份"; const del = $("book-menu-delete"); if (del) del.textContent = n > 1 ? `删除 ${n} 项` : "删除"; const sel = $("book-menu-select"); if (sel) sel.textContent = bookSelectMode ? "取消多选" : "多选"; } function showBookMenu(bookId, x, y) { const menu = $("book-action-menu"); if (!menu) return; bookMenuBookId = bookId; refreshBookMenuLabels(); menu.hidden = false; menu.style.left = `${x}px`; menu.style.top = `${y}px`; const rect = menu.getBoundingClientRect(); if (rect.right > window.innerWidth) { menu.style.left = `${Math.max(4, window.innerWidth - rect.width - 4)}px`; } if (rect.bottom > window.innerHeight) { menu.style.top = `${Math.max(4, window.innerHeight - rect.height - 4)}px`; } } function getNavBook() { if (sidebarNav.level !== "book") return null; return books.find((b) => b.id === sidebarNav.bookId) ?? null; } function navigateToRoot() { sidebarNav = { level: "root" }; renderPathBar(); renderBookList(); } function navigateToBook(bookId) { sidebarNav = { level: "book", bookId }; renderPathBar(); renderBookList(); void fetchPlaySavesForBook(bookId); } async function renameBookById(bookId) { const book = books.find((b) => b.id === bookId); if (!book) return; const title = prompt("作品名称", book.title); if (title == null) return; const trimmed = title.trim(); if (!trimmed) return alert("名称不能为空"); try { const data = await api(`/api/books/${encodeURIComponent(bookId)}`, { method: "PUT", body: JSON.stringify({ title: trimmed }), }); const i = books.findIndex((b) => b.id === bookId); if (i >= 0 && data.book) books[i] = { ...books[i], ...data.book }; if (activeBookId === bookId && lastView) { renderSession({ ...lastView, bookTitle: trimmed }, false); } else { renderBookList(); } } catch (err) { alert(err.message); } } async function duplicateBookById(bookId, title) { try { const data = await api(`/api/books/${encodeURIComponent(bookId)}/duplicate`, { method: "POST", body: JSON.stringify(title ? { title } : {}), }); if (data.book) { books.unshift({ id: data.book.id, title: data.book.title, activeSkillId: data.book.activeSkillId ?? data.book.orchestratorId, activeSkillName: data.book.activeSkillName ?? data.book.orchestratorName, preview: data.book.preview, updatedAt: data.book.updatedAt, orchestratorId: data.book.orchestratorId, orchestratorName: data.book.orchestratorName, }); renderBookList(); } return data.book; } catch (err) { alert(err.message); return null; } } async function duplicateSelectedBooks() { const ids = bookSelectMode || selectedBookIds.size ? [...selectedBookIds] : []; if (!ids.length) return; for (const id of ids) { await duplicateBookById(id); } setBookSelectMode(false); } async function deleteSelectedBooks() { const ids = [...selectedBookIds]; if (!ids.length) return; const label = ids.length === 1 ? `「${books.find((b) => b.id === ids[0])?.title ?? "该作品"}」` : `${ids.length} 个作品`; if (!confirm(`删除 ${label}?不可恢复。`)) return; try { const data = await api("/api/books/batch", { method: "DELETE", body: JSON.stringify({ ids }), }); const deleted = new Set(data.deleted ?? ids); books = books.filter((b) => !deleted.has(b.id)); setBookSelectMode(false); if (activeBookId && deleted.has(activeBookId)) { if (books.length) await openBook(books[0].id); else renderEmpty(); } else { renderBookList(); } } catch (err) { alert(err.message); } } function rectsIntersect(a, b) { return a.left < b.right && a.right > b.left && a.top < b.bottom && a.bottom > b.top; } function setupBookBoxSelect() { const list = $("book-list"); const overlay = $("book-box-overlay"); if (!list || !overlay) return; let dragging = false; let start = null; const clearDrag = () => { dragging = false; start = null; overlay.hidden = true; }; list.addEventListener("mousedown", (e) => { if (sidebarNav.level !== "root" || e.button !== 0) return; if (e.target.closest("#book-action-menu")) return; dragging = true; start = { x: e.clientX, y: e.clientY }; overlay.hidden = true; const onMove = (ev) => { if (!dragging || !start) return; const dx = Math.abs(ev.clientX - start.x); const dy = Math.abs(ev.clientY - start.y); if (dx < 6 && dy < 6) return; if (!bookSelectMode) setBookSelectMode(true, { keepSelection: true }); const left = Math.min(start.x, ev.clientX); const top = Math.min(start.y, ev.clientY); const width = Math.abs(ev.clientX - start.x); const height = Math.abs(ev.clientY - start.y); overlay.hidden = false; overlay.style.left = `${left}px`; overlay.style.top = `${top}px`; overlay.style.width = `${width}px`; overlay.style.height = `${height}px`; }; const onUp = (ev) => { if (!dragging || !start) return; const dx = Math.abs(ev.clientX - start.x); const dy = Math.abs(ev.clientY - start.y); if (dx >= 6 || dy >= 6) { const box = { left: Math.min(start.x, ev.clientX), right: Math.max(start.x, ev.clientX), top: Math.min(start.y, ev.clientY), bottom: Math.max(start.y, ev.clientY), }; for (const row of list.querySelectorAll("[data-book-id]")) { const rect = row.getBoundingClientRect(); if (rectsIntersect(box, rect)) selectedBookIds.add(row.dataset.bookId); } renderBookList(); } document.removeEventListener("mousemove", onMove); document.removeEventListener("mouseup", onUp); clearDrag(); }; document.addEventListener("mousemove", onMove); document.addEventListener("mouseup", onUp); }); } function renderPathBar() { const bar = $("book-path-bar"); if (!bar) return; bar.innerHTML = ""; bar.hidden = !books.length; const rootBtn = document.createElement("button"); rootBtn.type = "button"; rootBtn.className = "path-seg"; rootBtn.dataset.nav = "root"; rootBtn.innerHTML = `作品`; if (sidebarNav.level === "root") rootBtn.classList.add("current"); rootBtn.addEventListener("click", () => { if (sidebarNav.level !== "root") navigateToRoot(); }); bar.appendChild(rootBtn); if (sidebarNav.level === "book") { const book = getNavBook(); if (!book) return; const sep = document.createElement("span"); sep.className = "path-sep"; sep.setAttribute("aria-hidden", "true"); sep.textContent = "›"; bar.appendChild(sep); const bookSeg = document.createElement("span"); bookSeg.className = "path-seg current"; bookSeg.textContent = book.title; bar.appendChild(bookSeg); } } function renderExplorerRow({ name, meta, active, selected, bookId, actionClass, onClick, onDelete }) { const row = document.createElement("button"); row.type = "button"; row.className = ["explorer-row", actionClass, active ? "active" : "", selected ? "selected" : ""] .filter(Boolean) .join(" "); if (bookId) row.dataset.bookId = bookId; row.innerHTML = ` ${esc(name)} ${meta ? `${esc(meta)}` : ""} ${onDelete ? `×` : ""}`; row.addEventListener("click", (e) => { if (e.target.closest(".explorer-del")) return; onClick?.(e); }); row.querySelector(".explorer-del")?.addEventListener("click", (e) => { e.stopPropagation(); onDelete?.(); }); if (bookId) { row.addEventListener("contextmenu", (e) => { e.preventDefault(); if (bookSelectMode && !selectedBookIds.has(bookId)) { selectedBookIds.add(bookId); renderBookList(); } showBookMenu(bookId, e.clientX, e.clientY); }); } return row; } function renderBookContents(book, list) { const loading = playSavesLoading.has(book.id); const saves = playSavesByBook.get(book.id); const ready = bookPlayReady(book.id); list.appendChild( renderExplorerRow({ name: "继续创作", meta: "自动保存", active: activeBookId === book.id && lastView?.lifecycleStage !== "play", onClick: () => { void openBook(book.id).then(() => navigateToRoot()); }, }), ); if (loading && saves === undefined) { const hint = document.createElement("p"); hint.className = "explorer-hint"; hint.textContent = "加载存档…"; list.appendChild(hint); return; } const saveList = saves ?? []; if (!saveList.length && !ready) { const hint = document.createElement("p"); hint.className = "explorer-hint"; hint.textContent = "验收 Worker 集后可保存定稿 / 游玩存档"; list.appendChild(hint); return; } if (saveList.length) { const label = document.createElement("div"); label.className = "explorer-section-label"; label.textContent = "存档"; list.appendChild(label); } for (const s of saveList) { const kindLabel = s.kindLabel || (s.kind === "instance" ? "创作定稿" : s.kind === "opening" ? "开局" : "游玩进度"); list.appendChild( renderExplorerRow({ name: s.label, meta: `${kindLabel} · ${new Date(s.createdAt).toLocaleString("zh-CN")}`, onClick: () => void loadPlaySave(book.id, s.id), onDelete: () => void deletePlaySave(book.id, s.id, s.label), actionClass: "explorer-file", }), ); } if (ready) { list.appendChild( renderExplorerRow({ name: "+ 新建游玩", actionClass: "explorer-action", onClick: () => void startNewPlayForBook(book.id), }), ); } else if (!saveList.length) { const hint = document.createElement("p"); hint.className = "explorer-hint"; hint.textContent = "暂无游玩存档"; list.appendChild(hint); } } function renderBookList() { const list = $("book-list"); if (!list) return; renderPathBar(); if (!books.length) { list.innerHTML = ``; $("btn-new-inline")?.addEventListener("click", openNewBookDialog); return; } list.innerHTML = ""; if (sidebarNav.level === "book") { const book = getNavBook(); if (!book) { navigateToRoot(); return; } renderBookContents(book, list); return; } for (const book of books) { const skill = book.activeSkillName ?? book.activeSkillId ?? book.orchestratorName ?? "实例设计"; const selected = selectedBookIds.has(book.id); list.appendChild( renderExplorerRow({ name: book.title, meta: skill, active: book.id === activeBookId && !selected, selected, bookId: book.id, actionClass: "explorer-folder", onClick: () => { if (bookSelectMode) { toggleBookSelection(book.id); return; } navigateToBook(book.id); void openBook(book.id); }, }), ); } } async function loadPlaySave(bookId, saveId) { try { if (lastView) renderSession(lastView, true); const data = await api( `/api/books/${encodeURIComponent(bookId)}/saves/${encodeURIComponent(saveId)}/load`, { method: "POST" }, ); sidebarNav = { level: "root" }; renderSession(data.session, false); } catch (err) { if (lastView) renderSession(lastView, false); alert(err.message); } } async function deletePlaySave(bookId, saveId, label) { if (!confirm(`删除游玩存档「${label}」?`)) return; try { await api( `/api/books/${encodeURIComponent(bookId)}/saves/${encodeURIComponent(saveId)}`, { method: "DELETE" }, ); await fetchPlaySavesForBook(bookId, true); } catch (err) { alert(err.message); } } async function startNewPlayForBook(bookId) { if (!bookPlayReady(bookId)) { alert("须先验收 Worker 集"); return; } try { if (lastView) renderSession(lastView, true); const data = await api(`/api/books/${encodeURIComponent(bookId)}/play/new`, { method: "POST" }); sidebarNav = { level: "root" }; renderSession(data.session, false); } catch (err) { if (lastView) renderSession(lastView, false); alert(err.message); } } function renderHeader(view, loading) { const st = statusFor(view, loading); $("work-title").textContent = view.bookTitle ?? "未命名作品"; const skill = displaySkillPackLabel(view.activeSkill) || view.selectedRecipe?.name || "导演"; const phase = view.waitingReason ? REASON[view.waitingReason.kind] ?? view.phase : PHASE[view.phase] ?? view.phase; $("work-meta").textContent = `${skill} · ${phase}`; $("status-dot").className = `status-dot ${st.cls}`; $("status-text").textContent = st.text; const showSavePlay = Boolean(view.bookId && view.playReady && view.lifecycleStage === "play"); const showSaveInstance = Boolean(view.bookId && view.playReady); $("btn-save-play").hidden = !showSavePlay; const btnInst = $("btn-save-instance"); if (btnInst) btnInst.hidden = !showSaveInstance; $("btn-export").disabled = !(view.messages?.length); } function renderEmpty() { sessionId = null; lastView = null; activeBookId = null; sidebarNav = { level: "root" }; setBookSelectMode(false); composerForceInput = false; $("work-title").textContent = "未打开作品"; $("work-meta").textContent = ""; $("status-dot").className = "status-dot"; $("status-text").textContent = "—"; $("btn-save-play").hidden = true; const btnInst = $("btn-save-instance"); if (btnInst) btnInst.hidden = true; $("btn-export").disabled = true; $("message-feed").innerHTML = `

点击左侧 + 新建作品

`; $("skill-picker").hidden = true; $("agent-focus").innerHTML = ""; $("agent-timeline").innerHTML = ""; $("skill-guide-list").innerHTML = ""; const wsp = $("worker-set-user-panel"); if (wsp) wsp.innerHTML = ""; $("tool-trace").hidden = true; hideBookMenu(); document.body.dataset.lifecycle = "design"; $("composer").innerHTML = `
暂无打开的作品
`; renderBookList(); } function stopLivePoll() { if (livePollTimer != null) { clearInterval(livePollTimer); livePollTimer = null; } } function startLivePoll() { stopLivePoll(); if (!sessionId) return; livePollTimer = setInterval(async () => { if (!sessionId) return; try { const view = await api(`/api/sessions/${encodeURIComponent(sessionId)}`); lastView = { ...lastView, liveStream: view.liveStream, agentThinking: view.agentThinking }; updateLiveStreamPanel(view); } catch { /* ignore transient poll errors */ } }, LIVE_POLL_MS); } function renderSession(view, loading = false) { lastView = view; sessionId = view.id; activeBookId = view.bookId ?? activeBookId; if ( !loading && view.waitingReason?.kind !== "approve_step" && view.waitingReason?.kind !== "review_artifact" && view.waitingReason?.kind !== "worker_questions" && !(view.waitingReason?.kind === "input" && view.waitingReason?.questions?.length) ) { composerForceInput = false; } renderHeader(view, loading); renderBookList(); renderWorkspace(view, loading, (skillId) => sendText(skillId), { onSkipQuestions: () => runAction("skip_questions"), onEditMessage: (messageId, text) => messageAction("edit", messageId, { text }), onRefreshMessage: (messageId) => messageAction("refresh", messageId), onDeleteMessage: (messageId) => messageAction("delete", messageId), onViewContext: (messageId) => showContextTraceDialog(messageId), onSwitchVariant: (messageId, direction) => messageAction("variant", messageId, { direction }), }); renderComposer(view, loading); if (loading) startLivePoll(); else stopLivePoll(); } async function messageAction(kind, messageId, body = {}) { if (!sessionId) return; try { renderSession(lastView, true); const view = await api( `/api/sessions/${encodeURIComponent(sessionId)}/messages/${encodeURIComponent(messageId)}/${kind}`, { method: "POST", body: JSON.stringify(body), }, ); renderSession(view, false); } catch (err) { if (lastView) renderSession({ ...lastView, hints: [err.message] }, false); } } async function sendText(text) { if (!sessionId) return; const trimmed = text?.trim() ?? ""; const qHost = $("questions-card-host"); const activeQs = getActiveQuestions(lastView); const reviewing = lastView?.waitingReason?.kind === "review_artifact"; // 产物验收:底栏有字 ⇒ 按修改意见重做;接受走「接受目前产物」 if (reviewing && trimmed) { try { renderSession(lastView, true); const view = await api(`/api/sessions/${encodeURIComponent(sessionId)}/messages`, { method: "POST", body: JSON.stringify({ text: trimmed }), }); clearQuestionCardState(qHost); renderSession(view, false); } catch (err) { if (lastView) renderSession({ ...lastView, hints: [err.message] }, false); } return; } if (activeQs?.questions?.length && qHost?.classList.contains("is-open")) { const collected = collectQuestionAnswers(qHost, lastView); const answered = collected.ok ? collected.answers.filter((a) => a.text && a.text !== "(未答)") : []; // 可选追问:未选任何选项时,允许直接发自由文本(或空操作交给下方逻辑) if (!collected.ok && !activeQs.optional) { if (typeof collected.page === "number" && qHost._qState) { qHost._qState.page = collected.page; renderQuestionsCard(qHost, lastView, { onSkipQuestions: () => runAction("skip_questions"), }); } alert(collected.error); return; } if (collected.ok && answered.length) { try { renderSession(lastView, true); const body = { answers: collected.answers }; if (trimmed) body.note = trimmed; const view = await api(`/api/sessions/${encodeURIComponent(sessionId)}/answers`, { method: "POST", body: JSON.stringify(body), }); clearQuestionCardState(qHost); renderSession(view, false); } catch (err) { if (lastView) renderSession({ ...lastView, hints: [err.message] }, false); } return; } } if (!trimmed) return; try { renderSession(lastView, true); const view = await api(`/api/sessions/${encodeURIComponent(sessionId)}/messages`, { method: "POST", body: JSON.stringify({ text: trimmed }), }); renderSession(view, false); } catch (err) { if (lastView) renderSession({ ...lastView, hints: [err.message] }, false); } } async function runAction(action) { if (!sessionId) return; try { renderSession(lastView, true); const view = await api(`/api/sessions/${encodeURIComponent(sessionId)}/actions`, { method: "POST", body: JSON.stringify({ action }), }); // 接受产物 = 不再完善 → 收起询问卡;跳过追问同理 if (action === "accept" || action === "skip_questions") { clearQuestionCardState($("questions-card-host")); } renderSession(view, false); } catch (err) { if (lastView) renderSession({ ...lastView, hints: [err.message] }, false); } } async function setLifecycle(stage) { if (!sessionId || stage === lastView?.lifecycleStage) return; if (stage === "play" && !lastView?.playReady) { alert("请先验收 Worker 集(创作定稿)后再进入游玩"); return; } try { const view = await api(`/api/sessions/${encodeURIComponent(sessionId)}/lifecycle`, { method: "POST", body: JSON.stringify({ stage }), }); renderSession(view, false); } catch (err) { alert(err.message || "无法切换阶段"); if (lastView) renderSession(lastView, false); } } async function loadBooks() { const data = await api("/api/books"); books = data.books ?? []; renderBookList(); } function openNewBookDialog() { $("input-book-title").value = ""; void populateDirectorSelect(); $("dialog-new-book").showModal(); $("input-book-title").focus(); } async function populateDirectorSelect() { const sel = $("select-director"); const desc = $("director-desc"); if (!sel) return; try { const data = await api("/api/directors"); const directors = data.directors ?? []; sel.innerHTML = ""; if (!directors.length) { sel.innerHTML = ``; if (desc) { desc.hidden = false; desc.textContent = "尚未配置导演选项(recipes/catalog.yaml)。"; } return; } for (const d of directors) { const opt = document.createElement("option"); opt.value = d.id; opt.textContent = displaySkillPackLabel(d.id) || d.name || d.id; sel.appendChild(opt); } const preferred = directors.find((d) => d.id === "world-simulator") ?? directors[0]; sel.value = preferred.id; const syncDesc = () => { const cur = directors.find((d) => d.id === sel.value); if (desc) { const text = (cur?.declaration ?? "").trim(); desc.hidden = !text; desc.textContent = text; } }; sel.onchange = syncDesc; syncDesc(); } catch (err) { sel.innerHTML = ``; if (desc) { desc.hidden = false; desc.textContent = err.message; } } } async function createBook() { const title = $("input-book-title").value.trim() || "未命名作品"; const recipeId = $("select-director")?.value?.trim(); if (!recipeId) { alert("请选择导演"); return; } $("btn-create-book").disabled = true; try { const directorsMeta = await api("/api/directors").catch(() => null); const orchestratorId = directorsMeta?.skillPackId || "world-simulator"; const data = await api("/api/books", { method: "POST", body: JSON.stringify({ title, orchestratorId, recipeId, }), }); $("dialog-new-book").close(); if (data.book) books.unshift(data.book); activeBookId = data.book?.id; navigateToBook(data.book.id); renderSession(data.session, false); } catch (err) { alert(err.message); } finally { $("btn-create-book").disabled = false; } } async function openBook(bookId) { activeBookId = bookId; renderBookList(); if (lastView) renderSession(lastView, true); try { const data = await api(`/api/books/${encodeURIComponent(bookId)}/open`, { method: "POST" }); if (data.book) { const i = books.findIndex((b) => b.id === bookId); if (i >= 0) books[i] = { ...books[i], ...data.book }; } renderSession(data.session, false); } catch (err) { if (lastView) renderSession({ ...lastView, hints: [err.message] }, false); } } async function deleteBookById(bookId) { const book = books.find((b) => b.id === bookId); if (!book) return; if (!confirm(`删除「${book.title}」?不可恢复。`)) return; try { await api(`/api/books/${encodeURIComponent(bookId)}`, { method: "DELETE" }); books = books.filter((b) => b.id !== bookId); playSavesByBook.delete(bookId); selectedBookIds.delete(bookId); if (sidebarNav.level === "book" && sidebarNav.bookId === bookId) sidebarNav = { level: "root" }; hideBookMenu(); if (activeBookId === bookId) { if (books.length) { await openBook(books[0].id); } else { renderEmpty(); } } else { renderBookList(); } } catch (err) { alert(err.message); } } async function saveCurrentPlay() { if (!activeBookId || !lastView?.playReady) return alert("须先验收 Worker 集"); if (lastView.lifecycleStage !== "play") return alert("请先切换到「游玩」"); const label = prompt("游玩进度存档名称"); if (!label?.trim()) return; try { await api(`/api/books/${encodeURIComponent(activeBookId)}/saves`, { method: "POST", body: JSON.stringify({ label: label.trim(), sessionId, kind: "run" }), }); playSavesByBook.delete(activeBookId); if (sidebarNav.level === "book" && sidebarNav.bookId === activeBookId) { void fetchPlaySavesForBook(activeBookId, true); } } catch (err) { alert(err.message); } } async function saveCurrentInstance() { if (!activeBookId || !lastView?.playReady) return alert("须先验收 Worker 集"); const label = prompt("创作定稿存档名称"); if (!label?.trim()) return; try { await api(`/api/books/${encodeURIComponent(activeBookId)}/saves`, { method: "POST", body: JSON.stringify({ label: label.trim(), sessionId, kind: "instance" }), }); playSavesByBook.delete(activeBookId); if (sidebarNav.level === "book" && sidebarNav.bookId === activeBookId) { void fetchPlaySavesForBook(activeBookId, true); } } catch (err) { alert(err.message); } } function showContextTraceDialog(messageId) { const msg = (lastView?.messages ?? []).find((m) => m.id === messageId); const review = lastView?.reviewArtifact; const trace = msg?.contextTrace || (review && (review.sourceMessageId === messageId || review.id === messageId) ? review.contextTrace : undefined); const dlg = $("dialog-context-trace"); const meta = $("context-trace-meta"); const body = $("context-trace-body"); if (!dlg || !meta || !body) return; if (!trace?.messages?.length) { alert("这条消息没有保存请求上下文(可能已被修剪,或当时未开启保存)"); return; } const kb = ((trace.charCount || 0) / 1024).toFixed(1); meta.textContent = `${trace.caller || "unknown"} · ${trace.model || "model?"} · ${kb} KB · ${trace.createdAt || ""}`; body.textContent = trace.messages .map((m) => `======== ${m.role} ========\n${m.content}`) .join("\n\n"); dlg.showModal(); } $("btn-copy-context")?.addEventListener("click", async () => { const body = $("context-trace-body")?.textContent ?? ""; try { await navigator.clipboard.writeText(body); } catch { alert("复制失败"); } }); function exportSession() { if (!lastView) return; const name = (lastView.bookTitle ?? "session").replace(/[\\/:*?"<>|]/g, "_"); downloadMarkdown(`${name}.md`, sessionToMarkdown(lastView)); } $("btn-new-book")?.addEventListener("click", openNewBookDialog); $("btn-cancel-new")?.addEventListener("click", () => $("dialog-new-book").close()); $("book-action-menu")?.addEventListener("click", (e) => { const action = e.target.closest("[data-action]")?.getAttribute("data-action"); const bookId = bookMenuBookId; hideBookMenu(); if (!bookId || !action) return; if (action === "open") { setBookSelectMode(false); navigateToBook(bookId); void openBook(bookId); } else if (action === "rename") { const id = selectedBookIds.size === 1 ? [...selectedBookIds][0] : bookId; void renameBookById(id); } else if (action === "duplicate") { if (selectedBookIds.size > 1) void duplicateSelectedBooks(); else void duplicateBookById(bookId).then(() => setBookSelectMode(false)); } else if (action === "select-mode") { if (bookSelectMode) { setBookSelectMode(false); } else { setBookSelectMode(true, { keepSelection: true }); selectedBookIds.add(bookId); renderBookList(); } } else if (action === "delete") { if (selectedBookIds.size > 1) void deleteSelectedBooks(); else void deleteBookById(bookId); } }); document.addEventListener("click", (e) => { if (!e.target.closest("#book-action-menu")) hideBookMenu(); }); document.addEventListener("keydown", (e) => { if (e.key === "Escape") { hideBookMenu(); if (bookSelectMode || selectedBookIds.size) setBookSelectMode(false); } }); document.addEventListener("scroll", hideBookMenu, true); $("form-new-book")?.addEventListener("submit", (e) => { e.preventDefault(); createBook(); }); $("lifecycle-toggle")?.addEventListener("click", (e) => { const btn = e.target.closest("[data-stage]"); if (!btn?.disabled) setLifecycle(btn.getAttribute("data-stage")); }); $("btn-export")?.addEventListener("click", exportSession); $("btn-save-play")?.addEventListener("click", () => void saveCurrentPlay()); $("btn-save-instance")?.addEventListener("click", () => void saveCurrentInstance()); window.addEventListener("wa:session-updated", (e) => { const view = e.detail; if (view?.id) renderSession(view, false); }); async function init() { try { void populateDirectorSelect(); await loadBooks(); if (books.length) { sidebarNav = { level: "root" }; await openBook(books[0].id); } else { renderEmpty(); } } catch { $("status-text").textContent = "加载失败"; } } init(); setupBookBoxSelect();