import type { IncomingMessage, ServerResponse } from "node:http"; import { listSkills } from "../skills/loader.js"; import type { ModuleCatalogEntry } from "../skills/creation-flow.js"; import { createBook, deleteBook, duplicateBook, getBook, listBooks, updateBook } from "../book/store.js"; import { sessionManager } from "./session-manager.js"; async function readBody(req: IncomingMessage): Promise { const chunks: Buffer[] = []; for await (const chunk of req) { chunks.push(chunk as Buffer); } return Buffer.concat(chunks).toString("utf8"); } function json(res: ServerResponse, status: number, data: unknown): void { res.writeHead(status, { "Content-Type": "application/json; charset=utf-8" }); res.end(JSON.stringify(data)); } type ModuleStatus = "ready" | "partial" | "skeleton" | "missing"; function moduleStatusFromSections( hasPrompt: boolean, present: string[], taskBody?: string, ): ModuleStatus { if (!hasPrompt) return "missing"; const hasTask = present.includes("task"); const hasOutput = present.includes("output"); if (!hasTask || !hasOutput) return "skeleton"; const task = taskBody?.trim() ?? ""; const stub = !task || /待作者细写|待完善|(待/.test(task) || task.length < 120; if (stub) return "skeleton"; const depth = ["principles", "probe", "checklist", "examples"].filter((id) => present.includes(id), ).length; return depth >= 2 ? "ready" : "partial"; } type ModuleBrowseRow = { id: string; name: string; declaration: string; artifact: string; hasPrompt: boolean; sectionsPresent: string[]; status: ModuleStatus; group: string; presentMode: "message" | "zero_layer" | null; layer: "intermediate" | "final" | null; repeatable: boolean; closer: boolean; priorArtifact: boolean; programSide: boolean; libraries: string[]; land: string; }; async function moduleBrowseFields( entry: ModuleCatalogEntry, ): Promise< Omit< ModuleBrowseRow, | "id" | "name" | "declaration" | "artifact" | "hasPrompt" | "sectionsPresent" | "status" > > { const { dictateModuleLandHint, isDictateProgramModule } = await import( "../skills/creation-flow.js" ); const { moduleBrowseGroup, modulePresentationMode } = await import( "../dictate/creation-plan.js" ); const { getLibrary } = await import("../libraries/registry.js"); return { group: moduleBrowseGroup(entry), presentMode: modulePresentationMode(entry.id), layer: entry.layer ?? null, repeatable: Boolean(entry.repeatable), closer: Boolean(entry.closer), priorArtifact: entry.kind === "prior-artifact", programSide: isDictateProgramModule(entry), libraries: (entry.libraries ?? []).map( (id) => getLibrary(id)?.label ?? id, ), land: dictateModuleLandHint(entry), }; } async function loadModulesPayload(skillId: string): Promise<{ groups: Array<{ id: string; label: string; hint: string }>; modules: ModuleBrowseRow[]; }> { const { loadSkill } = await import("../skills/loader.js"); const { loadModuleCatalog, loadModulePrompt, parseModulePromptSections, catalogModulesForIntake, MODULE_SECTION_IDS, } = await import("../skills/creation-flow.js"); const { MODULE_BROWSE_GROUPS } = await import("../dictate/creation-plan.js"); const skill = await loadSkill(skillId); if (!skill.skillPackRoot) { return { groups: [...MODULE_BROWSE_GROUPS], modules: [] }; } const catalog = await loadModuleCatalog(skill.skillPackRoot); const modules: ModuleBrowseRow[] = []; for (const m of catalogModulesForIntake(catalog ?? { modules: [] }, "dictate")) { const prompt = await loadModulePrompt(skill.skillPackRoot, m.id); const sections = prompt ? parseModulePromptSections(prompt) : { raw: "", blocks: {} }; const sectionsPresent = MODULE_SECTION_IDS.filter((id) => Boolean(sections.blocks[id]?.trim()), ); const hasPrompt = Boolean(prompt?.trim()); const taskBody = sections.blocks.task?.trim(); modules.push({ id: m.id, name: m.name, declaration: m.declaration, artifact: m.artifact, hasPrompt, sectionsPresent: [...sectionsPresent], status: moduleStatusFromSections(hasPrompt, sectionsPresent, taskBody), ...(await moduleBrowseFields(m)), }); } return { groups: [...MODULE_BROWSE_GROUPS], modules }; } async function loadModuleDetailPayload( skillId: string, moduleId: string, ): Promise< | (ModuleBrowseRow & { sections: Record; meta: Record | null; raw: string; }) | null > { const { loadSkill } = await import("../skills/loader.js"); const { loadModuleCatalog, loadModulePrompt, parseModulePromptSections, MODULE_SECTION_IDS, } = await import("../skills/creation-flow.js"); const { parse: parseYaml } = await import("yaml"); const skill = await loadSkill(skillId); if (!skill.skillPackRoot) return null; const catalog = await loadModuleCatalog(skill.skillPackRoot); const entry = catalog?.modules.find((m) => m.id === moduleId) ?? null; if (!entry) return null; const prompt = await loadModulePrompt(skill.skillPackRoot, moduleId); const parsed = prompt ? parseModulePromptSections(prompt) : { raw: "", blocks: {} }; const sections: Record = {}; for (const id of MODULE_SECTION_IDS) { const body = parsed.blocks[id]?.trim(); if (body) sections[id] = body; } const sectionsPresent = Object.keys(sections); const hasPrompt = Boolean(prompt?.trim()); let meta: Record | null = null; const metaRaw = sections.meta; if (metaRaw) { try { const doc = parseYaml(metaRaw); if (doc && typeof doc === "object" && !Array.isArray(doc)) { meta = doc as Record; } } catch { meta = null; } } return { id: entry.id, name: entry.name, declaration: entry.declaration, artifact: entry.artifact, hasPrompt, sectionsPresent, status: moduleStatusFromSections( hasPrompt, sectionsPresent, sections.task, ), ...(await moduleBrowseFields(entry)), sections, meta, raw: prompt ?? "", }; } export async function handleBooksApi( req: IncomingMessage, res: ServerResponse, pathname: string, ): Promise { if (pathname === "/api/skills" && req.method === "GET") { const skills = await listSkills(); json(res, 200, { skills: skills.map((s) => ({ id: s.name, name: s.name, description: s.description, category: s.category, bookKind: s.bookKind, })), }); return true; } /** 新建作品:配方一层选型(节点流程 / 转述各一本目录,不共享) */ if (pathname === "/api/directors" && req.method === "GET") { const { DEFAULT_ORCHESTRATOR_ID } = await import( "../config/default-orchestrator.js" ); const skillId = DEFAULT_ORCHESTRATOR_ID; try { const { loadSkill } = await import("../skills/loader.js"); const { loadRecipeCatalog } = await import("../skills/creation-flow.js"); const skill = await loadSkill(skillId); if (!skill.skillPackRoot) { json(res, 200, { directors: [], recipeDirectors: [], dictateDirectors: [], skillPackId: skillId, }); return true; } const [recipeCat, dictateCat] = await Promise.all([ loadRecipeCatalog(skill.skillPackRoot, undefined, "recipe"), loadRecipeCatalog(skill.skillPackRoot, undefined, "dictate"), ]); const mapRow = (r: { id: string; name: string; declaration: string; }) => ({ id: r.id, name: r.name, declaration: r.declaration, }); const recipeDirectors = (recipeCat?.recipes ?? []).map(mapRow); const dictateDirectors = (dictateCat?.recipes ?? []).map(mapRow); json(res, 200, { skillPackId: skillId, /** @deprecated 兼容旧前端:默认节点流程列表 */ directors: recipeDirectors, recipeDirectors, dictateDirectors, }); } catch (err) { json(res, 404, { error: err instanceof Error ? err.message : "未找到配方列表", }); } return true; } const recipesMatch = pathname.match(/^\/api\/skills\/([^/]+)\/recipes$/); if (recipesMatch && req.method === "GET") { const skillId = decodeURIComponent(recipesMatch[1]); try { const { loadSkill } = await import("../skills/loader.js"); const { loadRecipeCatalog } = await import("../skills/creation-flow.js"); const skill = await loadSkill(skillId); if (!skill.skillPackRoot) { json(res, 200, { recipes: [] }); return true; } const catalog = await loadRecipeCatalog(skill.skillPackRoot); json(res, 200, { recipes: (catalog?.recipes ?? []).map((r) => ({ id: r.id, name: r.name, declaration: r.declaration, })), }); } catch (err) { json(res, 404, { error: err instanceof Error ? err.message : "未找到技能包", }); } return true; } /** 对话落盘节点:按角色分组,不含旧配方包 */ if (pathname === "/api/modules" && req.method === "GET") { const { DEFAULT_ORCHESTRATOR_ID } = await import( "../config/default-orchestrator.js" ); try { const payload = await loadModulesPayload(DEFAULT_ORCHESTRATOR_ID); json(res, 200, payload); } catch (err) { json(res, 404, { error: err instanceof Error ? err.message : "未找到能力目录", }); } return true; } const moduleDetailMatch = pathname.match(/^\/api\/modules\/([^/]+)$/); if (moduleDetailMatch && req.method === "GET") { const { DEFAULT_ORCHESTRATOR_ID } = await import( "../config/default-orchestrator.js" ); const moduleId = decodeURIComponent(moduleDetailMatch[1]); try { const detail = await loadModuleDetailPayload( DEFAULT_ORCHESTRATOR_ID, moduleId, ); if (!detail) { json(res, 404, { error: `未找到能力:${moduleId}` }); return true; } json(res, 200, detail); } catch (err) { json(res, 404, { error: err instanceof Error ? err.message : "未找到能力", }); } return true; } const skillModulesMatch = pathname.match( /^\/api\/skills\/([^/]+)\/modules$/, ); if (skillModulesMatch && req.method === "GET") { const skillId = decodeURIComponent(skillModulesMatch[1]); try { const payload = await loadModulesPayload(skillId); json(res, 200, payload); } catch (err) { json(res, 404, { error: err instanceof Error ? err.message : "未找到能力目录", }); } return true; } const skillModuleDetailMatch = pathname.match( /^\/api\/skills\/([^/]+)\/modules\/([^/]+)$/, ); if (skillModuleDetailMatch && req.method === "GET") { const skillId = decodeURIComponent(skillModuleDetailMatch[1]); const moduleId = decodeURIComponent(skillModuleDetailMatch[2]); try { const detail = await loadModuleDetailPayload(skillId, moduleId); if (!detail) { json(res, 404, { error: `未找到能力:${moduleId}` }); return true; } json(res, 200, detail); } catch (err) { json(res, 404, { error: err instanceof Error ? err.message : "未找到能力", }); } return true; } if (pathname === "/api/books" && req.method === "GET") { json(res, 200, { books: listBooks() }); return true; } if (pathname === "/api/books" && req.method === "POST") { const body = JSON.parse(await readBody(req)) as { title?: string; /** 编排器包 id(registry name) */ orchestratorId?: string; /** 旧客户端可传;新建作品改由创作对话判断路径 */ recipeId?: string; /** 进料方式:对话落盘(默认)| 听书连载 | 旧工序编排 */ creationMode?: "recipe" | "dictate" | "listen_serial"; }; const skills = await listSkills(); const requested = body.orchestratorId?.trim(); const director = (requested && skills.find((s) => s.name === requested)) || skills.find((s) => s.name === "world-simulator") || skills[0]; if (!director) { json(res, 400, { error: "没有可用的编排器技能包" }); return true; } const rawMode = body.creationMode; const creationMode = rawMode === "listen_serial" || rawMode === "recipe" || rawMode === "dictate" ? rawMode : "dictate"; const book = createBook({ title: body.title, creationMode }); updateBook(book.id, { activeSkillId: director.name, activeSkillName: director.description?.split("\n")[0]?.slice(0, 80) || director.name, orchestratorId: director.name, orchestratorName: director.name, creationMode, }); const session = await sessionManager.createForBook( book.id, director.name, body.recipeId?.trim() || undefined, { creationMode }, ); json(res, 201, { book: getBook(book.id) ?? book, session }); return true; } if (pathname === "/api/books/batch" && req.method === "DELETE") { const body = JSON.parse(await readBody(req)) as { ids?: string[] }; const ids = (body.ids ?? []).filter(Boolean); const deleted: string[] = []; for (const id of ids) { const book = getBook(id); if (!book) continue; sessionManager.dropBookSessions(id); deleteBook(id); deleted.push(id); } json(res, 200, { deleted }); return true; } const duplicateMatch = pathname.match(/^\/api\/books\/([^/]+)\/duplicate$/); if (duplicateMatch && req.method === "POST") { const bookId = decodeURIComponent(duplicateMatch[1]); const body = JSON.parse(await readBody(req)) as { title?: string }; try { const book = duplicateBook(bookId, body.title); json(res, 201, { book }); } catch (err) { json(res, 400, { error: err instanceof Error ? err.message : "复制失败", }); } return true; } const playNewMatch = pathname.match(/^\/api\/books\/([^/]+)\/play\/new$/); if (playNewMatch && req.method === "POST") { const bookId = decodeURIComponent(playNewMatch[1]); const book = getBook(bookId); if (!book) { json(res, 404, { error: "Book 不存在" }); return true; } const active = sessionManager.getActiveSessionForBook(bookId); if (!active?.id) { json(res, 400, { error: "请先打开该作品" }); return true; } try { let instanceId: string | undefined; let useOpeningPersona: boolean | undefined; try { const raw = await readBody(req); if (raw.trim()) { const body = JSON.parse(raw) as { instanceId?: string; useOpeningPersona?: boolean; }; instanceId = body.instanceId?.trim() || undefined; useOpeningPersona = typeof body.useOpeningPersona === "boolean" ? body.useOpeningPersona : undefined; } } catch { instanceId = undefined; useOpeningPersona = undefined; } const session = await sessionManager.startNewPlayRun(active.id, instanceId, { useOpeningPersona, }); json(res, 200, { session }); } catch (err) { json(res, 400, { error: err instanceof Error ? err.message : "无法新建游玩", }); } return true; } const savesListMatch = pathname.match(/^\/api\/books\/([^/]+)\/saves$/); if (savesListMatch) { const bookId = decodeURIComponent(savesListMatch[1]); const book = getBook(bookId); if (!book) { json(res, 404, { error: "Book 不存在" }); return true; } if (req.method === "GET") { try { const saves = sessionManager.listGameSnapshots(bookId).map((s) => ({ ...s, kindLabel: s.kind === "instance" ? "创作定稿" : "游玩进度", })); json(res, 200, { saves, playWorkingInstanceId: sessionManager.playWorkingInstanceId(bookId) ?? null, }); } catch (err) { json(res, 400, { error: err instanceof Error ? err.message : "读取存档失败", }); } return true; } if (req.method === "POST") { const body = JSON.parse(await readBody(req)) as { label?: string; note?: string; sessionId?: string; /** instance=创作定稿截面;run=游玩进度(默认) */ kind?: "instance" | "run"; }; const sessionId = body.sessionId?.trim() || sessionManager.getActiveSessionForBook(bookId)?.id; if (!sessionId) { json(res, 400, { error: "当前作品没有活跃会话,无法存档" }); return true; } const kind = body.kind === "instance" ? "instance" : "run"; try { const save = kind === "run" ? sessionManager.savePlaySnapshot(sessionId, body.label ?? "", body.note) : sessionManager.saveGameSnapshot( sessionId, body.label ?? "", "instance", body.note, ); json(res, 201, { save: { ...save, kindLabel: save.kind === "instance" ? "创作定稿" : "游玩进度", }, }); } catch (err) { json(res, 400, { error: err instanceof Error ? err.message : "存档失败", }); } return true; } } const saveItemMatch = pathname.match( /^\/api\/books\/([^/]+)\/saves\/([^/]+)(\/load)?$/, ); if (saveItemMatch) { const bookId = decodeURIComponent(saveItemMatch[1]); const saveId = decodeURIComponent(saveItemMatch[2]); const isLoad = saveItemMatch[3] === "/load"; if (isLoad && req.method === "POST") { try { const session = await sessionManager.loadGameSnapshot(bookId, saveId); json(res, 200, { session }); } catch (err) { json(res, 400, { error: err instanceof Error ? err.message : "读档失败", }); } return true; } if (req.method === "GET" && !isLoad) { try { const data = sessionManager.getGameSnapshotProducts(bookId, saveId); json(res, 200, { save: { ...data.save, kindLabel: data.save.kind === "instance" ? "创作定稿" : "游玩进度", }, products: data.products, }); } catch (err) { json(res, 404, { error: err instanceof Error ? err.message : "读取存档产物失败", }); } return true; } if ((req.method === "PATCH" || req.method === "PUT") && !isLoad) { const body = JSON.parse(await readBody(req)) as { label?: string; tag?: string; content?: string; }; try { if (typeof body.tag === "string") { const data = sessionManager.patchGameSnapshotProduct( bookId, saveId, body.tag, typeof body.content === "string" ? body.content : "", ); json(res, 200, { save: { ...data.save, kindLabel: data.save.kind === "instance" ? "创作定稿" : "游玩进度", }, products: data.products, }); return true; } const save = sessionManager.renameGameSnapshot( bookId, saveId, body.label ?? "", ); json(res, 200, { save: { ...save, kindLabel: save.kind === "instance" ? "创作定稿" : "游玩进度", }, }); } catch (err) { json(res, 400, { error: err instanceof Error ? err.message : "更新失败", }); } return true; } if (req.method === "DELETE") { try { sessionManager.deleteGameSnapshot(bookId, saveId); json(res, 200, { ok: true }); } catch (err) { json(res, 404, { error: err instanceof Error ? err.message : "删除失败", }); } return true; } } const bookMatch = pathname.match(/^\/api\/books\/([^/]+)(\/open)?$/); if (bookMatch) { const bookId = decodeURIComponent(bookMatch[1]); const isOpen = bookMatch[2] === "/open"; if (isOpen && req.method === "POST") { const book = getBook(bookId); if (!book) { json(res, 404, { error: "Book 不存在" }); return true; } const session = await sessionManager.openBook(book.id); json(res, 200, { book, session }); return true; } if (req.method === "GET") { const book = getBook(bookId); if (!book) { json(res, 404, { error: "Book 不存在" }); return true; } json(res, 200, { book }); return true; } if (req.method === "PUT") { const body = JSON.parse(await readBody(req)) as { title?: string }; try { const book = updateBook(bookId, { title: body.title?.trim() }); sessionManager.syncBookTitle(bookId, book.title); json(res, 200, { book }); } catch (err) { json(res, 404, { error: err instanceof Error ? err.message : "更新失败", }); } return true; } if (req.method === "DELETE") { const book = getBook(bookId); if (!book) { json(res, 404, { error: "Book 不存在" }); return true; } sessionManager.dropBookSessions(bookId); deleteBook(bookId); json(res, 200, { ok: true }); return true; } } return false; }