- 装配只认 prompt_order.enabled;squash_system_messages 只合并相邻 system(新增 system 档,连接符按酒馆用单换行)
- 包装层:new_chat_prompt、示例对话(new_example_chat_prompt + <START> 切段 + name)、角色卡三块与 personality_format/scenario_format、{{user}}/{{char}} 取值、任务契约并进预设条目
- 工具协议:按 ToolBindings / 伪工具协议数据声明真 tools(presetToolPolicy=preset),工具描述用到的条目不再重复作为消息发出
- 正则:promptOnly 作用于对话历史(含 depth 过滤)、写入时脚本改写回复;{{//}} 注释按配平扫描
- tools/st-request-diff:真假上游抓包对比工具、三份预设的机制结论与实测报告
721 lines
22 KiB
TypeScript
721 lines
22 KiB
TypeScript
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<string> {
|
||
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<string, string>;
|
||
meta: Record<string, unknown> | 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<string, string> = {};
|
||
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<string, unknown> | 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<string, unknown>;
|
||
}
|
||
} 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<boolean> {
|
||
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;
|
||
}
|