重构世界模拟器为模块化配方架构,完善创作编排、会话运行时与 Web UI,并清理过时技能。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-30 00:39:32 +08:00
parent 2b74c30d36
commit e670a5129c
167 changed files with 22955 additions and 5659 deletions

View File

@@ -8,11 +8,18 @@ import {
type LifecycleStage,
type SkillCatalogEntry,
} from "./skill-catalog.js";
import {
displayWorkerLabel,
formatAgentDisplayTitle,
formatWorkerDisplayTitle,
} from "./display-labels.js";
export type AgentMessageKind =
| "user_input"
| "orchestrator_decision"
| "orchestrator_thinking"
| "orchestrator_prompt"
| "orchestrator_assessment"
| "agent_tool"
| "worker_running"
| "worker_output"
@@ -77,18 +84,29 @@ export function classifyAgentMessage(text: string): EnrichedMessage {
return {
kind: "agent_tool",
actor: "orchestrator",
title: `Tool · ${name}`,
title: `工具 · ${name}`,
body: detail || "(无输出)",
text: trimmed,
};
}
const agentThink = trimmed.match(/^\[总管 思考\]\s*\n?\n?([\s\S]*)$/);
if (agentThink) {
return {
kind: "orchestrator_thinking",
actor: "orchestrator",
title: formatAgentDisplayTitle("思考"),
body: agentThink[1]?.trim() || "(无内容)",
text: trimmed,
};
}
const orchestrator = trimmed.match(/^\[总管\]\s*(\w+):\s*([\s\S]+)$/);
if (orchestrator) {
return {
kind: "orchestrator_decision",
actor: "orchestrator",
title: `Agent · ${orchestrator[1]}`,
title: formatAgentDisplayTitle(orchestrator[1]),
body: orchestrator[2].trim(),
text: trimmed,
};
@@ -99,8 +117,30 @@ export function classifyAgentMessage(text: string): EnrichedMessage {
return {
kind: "worker_running",
actor: workerRunning[1],
title: `Worker · ${workerRunning[1]}`,
body: "正在调用模型执行 SKILL…",
title: formatWorkerDisplayTitle(workerRunning[1], "running"),
body: "正在调用模型执行…",
text: trimmed,
};
}
const compressed = trimmed.match(/^\[上下文已压缩\]\s*([\s\S]+)$/);
if (compressed) {
return {
kind: "system_info",
actor: "system",
title: "上下文已压缩",
body: compressed[1].trim(),
text: trimmed,
};
}
const unitAccepted = trimmed.match(/^\[创作单位已验收\]\s*([\s\S]+)$/);
if (unitAccepted) {
return {
kind: "system_info",
actor: "system",
title: "创作单位已验收",
body: unitAccepted[1].trim(),
text: trimmed,
};
}
@@ -110,7 +150,7 @@ export function classifyAgentMessage(text: string): EnrichedMessage {
return {
kind: "worker_output",
actor: workerDone[1],
title: `Worker · ${workerDone[1]} 产出`,
title: formatWorkerDisplayTitle(workerDone[1], "output"),
body: workerDone[2]?.trim() || "(无正文)",
text: trimmed,
};
@@ -121,7 +161,7 @@ export function classifyAgentMessage(text: string): EnrichedMessage {
return {
kind: "worker_stub",
actor: stub?.[1],
title: `占位 Worker · ${stub?.[1] ?? "?"}`,
title: formatWorkerDisplayTitle(stub?.[1], "stub"),
body: trimmed,
text: trimmed,
};
@@ -135,7 +175,7 @@ export function classifyAgentMessage(text: string): EnrichedMessage {
return {
kind: "worker_questions",
actor: workerAskTagged[1],
title: `Worker · ${workerAskTagged[1]} 提问`,
title: formatWorkerDisplayTitle(workerAskTagged[1], "questions"),
body,
text: trimmed,
};
@@ -170,6 +210,44 @@ export function classifyAgentMessage(text: string): EnrichedMessage {
};
}
if (trimmed.startsWith("[Agent] 内容评价")) {
return {
kind: "orchestrator_assessment",
actor: "orchestrator",
title: "总管 · 内容评价",
body: trimmed.replace(/^\[Agent\]\s*内容评价[:]\s*/, "").trim() || trimmed,
text: trimmed,
};
}
if (trimmed.startsWith("[Agent] 提问") || trimmed.startsWith("[Agent] 可选追问")) {
return {
kind: "worker_questions",
actor: "orchestrator",
title: "总管 · 可选追问",
body: formatWorkerQuestionBody(
trimmed
.replace(/^\[Agent\]\s*可选追问(可跳过)[:]\s*/, "")
.replace(/^\[Agent\]\s*提问[:]\s*/, ""),
),
text: trimmed,
};
}
if (trimmed.match(/^\[Worker\]\s*可选追问/)) {
return {
kind: "worker_questions",
title: "可选追问",
body: formatWorkerQuestionBody(
trimmed.replace(
/^\[Worker\]\s*可选追问(可跳过,直接接受(?:目前)?产物)[:]\s*/,
"",
),
),
text: trimmed,
};
}
if (
trimmed.includes("请告诉我") ||
trimmed.includes("启动询问") ||
@@ -178,7 +256,7 @@ export function classifyAgentMessage(text: string): EnrichedMessage {
return {
kind: "orchestrator_prompt",
actor: "orchestrator",
title: "Agent · 启动询问",
title: "总管 · 启动询问",
body: trimmed,
text: trimmed,
};
@@ -268,16 +346,37 @@ export function buildFocus(
const detail =
intake && intake.requiredTotal > 0
? `必要项 ${intake.requiredFilled}/${intake.requiredTotal}`
: "完成必要项后可进入实例化";
: "总管将根据描述推理 Worker 集";
return {
actorType: "user",
actorLabel: "你",
action: "填写创作信息",
action: "描述创作需求",
detail,
};
}
if (reason?.kind === "input" && !session.slots.startupCompleted) {
return {
actorType: "user",
actorLabel: "你",
action: "描述创作需求",
detail: "发送后总管将开始:创作 · 核心",
};
}
if (reason?.kind === "input") {
if (reason.questions?.length) {
const q = reason.questions
.map((item) => item.prompt)
.filter((s) => s?.trim())
.join("");
return {
actorType: "user",
actorLabel: "你",
action: "回答追问",
detail: q.slice(0, 200) || reason.message,
};
}
return {
actorType: "user",
actorLabel: "你",
@@ -291,30 +390,37 @@ export function buildFocus(
return {
actorType: "orchestrator",
actorId: "orchestrator",
actorLabel: "Agent",
action: `建议 invoke ${worker}`,
actorLabel: "总管",
action: `建议调用 ${displayWorkerLabel(worker)}`,
detail: session.pendingDecision?.reason,
};
}
if (reason?.kind === "worker_questions") {
const q = reason.questions?.filter((s) => s?.trim()).join("") ?? "";
const q =
reason.questions
?.map((item) => item.prompt)
.filter((s) => s?.trim())
.join("") ?? "";
return {
actorType: "user",
actorId: reason.workerId,
actorLabel: "你",
action: `回答 · ${reason.workerId}`,
detail: q.slice(0, 200) || "请在下框补充",
action: `回答 · ${displayWorkerLabel(reason.workerId)}`,
detail: q.slice(0, 200) || "请在询问卡作答",
};
}
if (reason?.kind === "review_artifact") {
const art = session.artifacts.find((a) => a.id === session.pendingArtifactId);
const optionalQs = reason.questions?.length
? `;另有 ${reason.questions.length} 道可选追问`
: "";
return {
actorType: "user",
actorLabel: "你",
action: "验收产物",
detail: art?.summary ?? art?.workerId,
detail: `${art?.summary ?? displayWorkerLabel(art?.workerId) ?? ""}${optionalQs}`,
};
}
@@ -322,9 +428,9 @@ export function buildFocus(
return {
actorType: "worker",
actorId: session.currentWorkerId,
actorLabel: `Skill · ${session.currentWorkerId}`,
actorLabel: displayWorkerLabel(session.currentWorkerId),
action: "执行中",
detail: "模型按 SKILL 产出…",
detail: "模型正在产出…",
};
}
@@ -332,9 +438,9 @@ export function buildFocus(
return {
actorType: "orchestrator",
actorId: "orchestrator",
actorLabel: "Agent",
action: stage === "design" ? "设计 burst" : "游玩 burst",
detail: "tool loop读黑板 → 选 skill",
actorLabel: "总管",
action: stage === "design" ? "创作调度" : "游玩调度",
detail: "读黑板 → 选下一步",
};
}
@@ -342,7 +448,8 @@ export function buildFocus(
return {
actorType: "user",
actorLabel: "你",
action: "选择 Skill 包",
action: "恢复中的旧会话",
detail: "请发送任意消息继续,或联系维护者",
};
}

View File

@@ -1,190 +1,559 @@
import type { IncomingMessage, ServerResponse } from "node:http";
import { listSkills } from "../skills/loader.js";
import { createBook, deleteBook, 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));
}
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/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 };
const book = createBook({ title: body.title });
const session = await sessionManager.createForBook(book.id);
json(res, 201, { book, session });
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);
json(res, 200, { saves });
} 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;
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 = sessionManager.saveGameSnapshot(
sessionId,
body.label ?? "",
kind,
body.note,
);
json(res, 201, { save });
} catch (err) {
import type { IncomingMessage, ServerResponse } from "node:http";
import { listSkills } from "../skills/loader.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";
}
async function loadModulesPayload(skillId: string): Promise<{
skillPackId: string;
modules: Array<{
id: string;
name: string;
declaration: string;
artifact: string;
hasPrompt: boolean;
sectionsPresent: string[];
status: ModuleStatus;
}>;
}> {
const { loadSkill } = await import("../skills/loader.js");
const {
loadModuleCatalog,
loadModulePrompt,
parseModulePromptSections,
MODULE_SECTION_IDS,
} = await import("../skills/creation-flow.js");
const skill = await loadSkill(skillId);
if (!skill.skillPackRoot) {
return { skillPackId: skillId, modules: [] };
}
const catalog = await loadModuleCatalog(skill.skillPackRoot);
const modules = [];
for (const m of catalog?.modules ?? []) {
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),
});
}
return { skillPackId: skillId, modules };
}
async function loadModuleDetailPayload(
skillId: string,
moduleId: string,
): Promise<{
skillPackId: string;
id: string;
name: string;
declaration: string;
artifact: string;
hasPrompt: boolean;
sectionsPresent: string[];
status: ModuleStatus;
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 {
skillPackId: skillId,
id: entry.id,
name: entry.name,
declaration: entry.declaration,
artifact: entry.artifact,
hasPrompt,
sectionsPresent,
status: moduleStatusFromSections(
hasPrompt,
sectionsPresent,
sections.task,
),
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;
}
/** 新建作品:导演一层选型(内部 = recipes catalog */
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: [], skillPackId: skillId });
return true;
}
const catalog = await loadRecipeCatalog(skill.skillPackRoot);
json(res, 200, {
skillPackId: skillId,
directors: (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;
}
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;
/** 导演 Skill / 能力包 idregistry name */
orchestratorId?: string;
/** 用户手动选定的导演 id内部 recipe */
recipeId?: string;
};
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: "没有可用的导演 Skill能力包" });
return true;
}
const book = createBook({ title: body.title });
updateBook(book.id, {
activeSkillId: director.name,
activeSkillName: director.description?.split("\n")[0]?.slice(0, 80) || director.name,
orchestratorId: director.name,
orchestratorName: director.name,
});
const session = await sessionManager.createForBook(
book.id,
director.name,
body.recipeId?.trim(),
);
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 {
const session = await sessionManager.startNewPlayRun(active.id);
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 });
} 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 === "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() });
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;
}

View File

@@ -0,0 +1,121 @@
/**
* 用户可见中文标签(与 docs/ui-glossary.md 同步)。
* 内部 id 不变;仅展示层映射。
*/
const STAGE_LABELS: Record<string, string> = {
design: "创作",
play: "游玩",
done: "已完成",
idle: "待命",
running: "执行中",
waiting_user: "等待你",
error: "出错",
};
const WORKER_LABELS: Record<string, string> = {
"design-core": "创作 · 核心",
"design-worker": "创作 · 演员规格",
"design-fixed": "创作 · 固定上下文",
"design-refine": "创作 · 细化与终稿",
"design-intake": "创作 · 综合收口",
"design-flow": "创作 · 流程编排",
"design-step": "创作 · 执行步骤",
"opening-generator": "开局 · 开场白",
orchestrator: "导演",
"agent-burst": "导演调度",
narrator: "叙事转述",
"role-decide": "角色决策",
"world-simulator": "世界推演",
"round-present": "回合呈现",
outline: "大纲 / 细纲",
"chapter-writer": "章节正文",
};
const FIXED_TOPIC_LABELS: Record<string, string> = {
"aesthetics-interaction": "美学纲领与交互范式",
interaction: "交互范式",
narrative_guide: "叙事指南",
input_protocol: "输入协议",
core_premise: "核心前提",
aesthetics: "美学纲领",
};
const PHASE_UNIT_LABELS: Record<string, string> = {
core: "核心",
refine: "细化",
};
const SKILL_PACK_LABELS: Record<string, string> = {
"world-simulator": "世界模拟器",
"expand-assistant": "扩写助手",
};
/** 生命周期 / 相位 */
export function displayStageLabel(id: string | undefined | null): string {
if (!id) return "";
return STAGE_LABELS[id] ?? id;
}
/** 导演选项 / skill pack 展示名 */
export function displaySkillPackLabel(id: string | undefined | null): string {
if (!id) return "";
return SKILL_PACK_LABELS[id] ?? id;
}
/**
* 演员 / 单位 / 能力 id → 用户可见名(不含动作后缀)。
*/
export function displayWorkerLabel(id: string | undefined | null): string {
if (!id) return "";
const trimmed = id.trim();
if (!trimmed) return "";
if (WORKER_LABELS[trimmed]) return WORKER_LABELS[trimmed]!;
if (trimmed.startsWith("phase:")) {
const key = trimmed.slice("phase:".length);
const name = PHASE_UNIT_LABELS[key] ?? key;
return `单位 · ${name}`;
}
if (trimmed.startsWith("worker:")) {
const ref = trimmed.slice("worker:".length);
return `演员 · ${displayWorkerLabel(ref)}`;
}
if (trimmed.startsWith("fixed:")) {
const topic = trimmed.slice("fixed:".length);
return `能力 · ${FIXED_TOPIC_LABELS[topic] ?? topic}`;
}
if (trimmed.startsWith("resident:")) {
return `常驻 · ${trimmed.slice("resident:".length)}`;
}
return trimmed;
}
export type WorkerTitleAction = "running" | "output" | "questions" | "stub" | null;
/** 气泡 / 流式标题:中文名 + 可选动作 */
export function formatWorkerDisplayTitle(
workerId: string | undefined | null,
action: WorkerTitleAction = null,
): string {
const base = displayWorkerLabel(workerId) || "演员";
switch (action) {
case "output":
return `${base} · 产出`;
case "questions":
return `${base} · 提问`;
case "stub":
return `${base} · 占位`;
case "running":
return `${base} · 执行中`;
default:
return base;
}
}
/** 导演(调度 Agent相关标题 */
export function formatAgentDisplayTitle(detail?: string): string {
if (detail?.trim()) return `导演 · ${detail.trim()}`;
return "导演";
}

View File

@@ -0,0 +1,223 @@
import { randomUUID } from "node:crypto";
import type { BlackboardItem } from "../types/blackboard.js";
import type { RuntimeSession } from "../types/runtime.js";
export type BranchableMessage = {
id: string;
role: "system" | "user";
text: string;
createdAt: string;
kind?: string;
actor?: string;
title?: string;
body?: string;
branchGroupId?: string;
branchIndex?: number;
branchTotal?: number;
};
export type SessionCheckpoint = {
runtimeSession: RuntimeSession;
blackboardItems: BlackboardItem[];
};
export type MessageBranchVariant = {
/** 从分支点起的消息链(含分支点消息本身) */
messages: BranchableMessage[];
checkpoint: SessionCheckpoint;
};
export type MessageBranch = {
anchorIndex: number;
groupId: string;
variants: MessageBranchVariant[];
activeIndex: number;
};
export type MessageBranchState = {
branches: Record<string, MessageBranch>;
/** 下标 i = 追加 messages[i] 之前的 runtime 快照 */
preMessageCheckpoints: Record<number, SessionCheckpoint>;
};
export function createMessageBranchState(): MessageBranchState {
return { branches: {}, preMessageCheckpoints: {} };
}
export function cloneCheckpoint(cp: SessionCheckpoint): SessionCheckpoint {
return {
runtimeSession: structuredClone(cp.runtimeSession),
blackboardItems: structuredClone(cp.blackboardItems),
};
}
export function recordPreMessageCheckpoint(
state: MessageBranchState,
index: number,
checkpoint: SessionCheckpoint,
): void {
state.preMessageCheckpoints[index] = cloneCheckpoint(checkpoint);
}
export function attachBranchMeta(messages: BranchableMessage[], groupId: string, activeIndex: number): void {
const count = messages.filter((m) => m.branchGroupId === groupId).length || 1;
const total = Math.max(count, activeIndex + 1);
for (const m of messages) {
if (m.branchGroupId === groupId && m.branchIndex === activeIndex) {
m.branchTotal = total;
}
}
}
export function syncBranchTotals(
messages: BranchableMessage[],
branch: MessageBranch,
): void {
const total = branch.variants.length;
const active = branch.activeIndex;
const head = branch.variants[active]?.messages[0];
if (!head) return;
for (const m of messages) {
if (m.id === head.id || (m.branchGroupId === branch.groupId && m.branchIndex === active)) {
m.branchGroupId = branch.groupId;
m.branchIndex = active;
m.branchTotal = total;
}
}
}
function cloneMessages(msgs: BranchableMessage[]): BranchableMessage[] {
return msgs.map((m) => ({ ...m }));
}
export function ensureBranchForEdit(
state: MessageBranchState,
messages: BranchableMessage[],
messageIndex: number,
checkpoint: SessionCheckpoint,
): MessageBranch {
const msg = messages[messageIndex];
const groupId = msg.branchGroupId ?? msg.id;
let branch = state.branches[groupId];
if (!branch) {
branch = {
anchorIndex: messageIndex,
groupId,
activeIndex: 0,
variants: [
{
messages: cloneMessages(messages.slice(messageIndex)),
checkpoint: cloneCheckpoint(checkpoint),
},
],
};
state.branches[groupId] = branch;
for (const m of messages.slice(messageIndex)) {
m.branchGroupId = groupId;
m.branchIndex = 0;
m.branchTotal = 1;
}
}
return branch;
}
export function ensureBranchForRefresh(
state: MessageBranchState,
messages: BranchableMessage[],
messageIndex: number,
checkpoint: SessionCheckpoint,
): MessageBranch {
const msg = messages[messageIndex];
const groupId = msg.branchGroupId ?? msg.id;
let branch = state.branches[groupId];
if (!branch) {
branch = {
anchorIndex: messageIndex,
groupId,
activeIndex: 0,
variants: [
{
messages: cloneMessages(messages.slice(messageIndex)),
checkpoint: cloneCheckpoint(checkpoint),
},
],
};
state.branches[groupId] = branch;
for (const m of messages.slice(messageIndex)) {
m.branchGroupId = groupId;
m.branchIndex = 0;
m.branchTotal = 1;
}
}
return branch;
}
export function appendBranchVariant(
branch: MessageBranch,
headMessage: BranchableMessage,
checkpoint: SessionCheckpoint,
): number {
const index = branch.variants.length;
branch.variants.push({
messages: [{ ...headMessage, branchGroupId: branch.groupId, branchIndex: index }],
checkpoint: cloneCheckpoint(checkpoint),
});
branch.activeIndex = index;
return index;
}
export function updateActiveBranchVariant(
branch: MessageBranch,
tailMessages: BranchableMessage[],
checkpoint: SessionCheckpoint,
): void {
const variant = branch.variants[branch.activeIndex];
if (!variant) return;
variant.messages = cloneMessages(tailMessages);
variant.checkpoint = cloneCheckpoint(checkpoint);
}
export function switchBranchVariant(
state: MessageBranchState,
messages: BranchableMessage[],
groupId: string,
delta: -1 | 1,
): { messages: BranchableMessage[]; checkpoint: SessionCheckpoint } | null {
const branch = state.branches[groupId];
if (!branch) return null;
const next = branch.activeIndex + delta;
if (next < 0 || next >= branch.variants.length) return null;
branch.activeIndex = next;
const variant = branch.variants[next];
const prefix = messages.slice(0, branch.anchorIndex);
const merged = [...prefix, ...cloneMessages(variant.messages)];
syncBranchTotals(merged, branch);
return { messages: merged, checkpoint: cloneCheckpoint(variant.checkpoint) };
}
export function createUserVariantMessage(text: string, groupId: string, branchIndex: number): BranchableMessage {
return {
id: randomUUID(),
role: "user",
text,
createdAt: new Date().toISOString(),
kind: "user_input",
title: "你的输入",
body: text,
branchGroupId: groupId,
branchIndex,
};
}
export function isRefreshableMessage(msg: BranchableMessage): boolean {
if (msg.role === "user") return false;
const kind = msg.kind ?? "system_info";
return kind === "worker_questions" || kind === "worker_output";
}
export function findPrecedingUserIndex(messages: BranchableMessage[], fromIndex: number): number {
for (let i = fromIndex - 1; i >= 0; i--) {
if (messages[i].role === "user") return i;
}
return -1;
}

View File

@@ -0,0 +1,162 @@
/**
* 创作过程对话策略(与黑板固定上下文正交):
* - session.messages全量保留供用户浏览编辑分支只暴露当前认可版本
* - 拼给 AI 的「创作.对话」:用户全量 + AI 永远只留最后一次(含未完成提问)
*
* 「隐藏 AI 历史」只作用于拼接,不删、不藏用户可见消息。
*/
export type DialogueChatMessage = {
id: string;
role: "system" | "user";
text: string;
createdAt?: string;
kind?: string;
actor?: string;
title?: string;
body?: string;
compressed?: boolean;
};
/** @deprecated 用 DialogueChatMessage */
export type PrunableChatMessage = DialogueChatMessage;
/** 写入黑板、注入 design worker 的对话 transcript tag */
export const CREATION_DIALOGUE_TAG = "创作.对话";
const AI_CONTENT_KINDS = new Set([
"worker_output",
"worker_questions",
"orchestrator_thinking",
"orchestrator_decision",
]);
const AI_EPHEMERAL_KINDS = new Set(["worker_running", "worker_stub", "agent_tool"]);
function isAiContent(m: DialogueChatMessage): boolean {
const kind = m.kind ?? "";
if (AI_CONTENT_KINDS.has(kind)) return true;
if (m.role === "system" && kind.startsWith("worker_")) return true;
return false;
}
function isAiEphemeral(m: DialogueChatMessage): boolean {
return AI_EPHEMERAL_KINDS.has(m.kind ?? "");
}
function isUserMessage(m: DialogueChatMessage): boolean {
return m.role === "user" || m.kind === "user_input";
}
/**
* 从全量 messages 选出「拼给 AI」的视图全部用户 + 最后一次 AI 内容。
* 不修改原数组。
*/
export function selectCreationDialogueForAi<T extends DialogueChatMessage>(
messages: readonly T[],
): T[] {
let lastAiIdx = -1;
for (let i = messages.length - 1; i >= 0; i--) {
if (isAiContent(messages[i]!)) {
lastAiIdx = i;
break;
}
}
const out: T[] = [];
for (let i = 0; i < messages.length; i++) {
const m = messages[i]!;
if (m.compressed) continue;
if (isUserMessage(m)) {
out.push(m);
continue;
}
if (isAiEphemeral(m)) continue;
if (isAiContent(m) && i === lastAiIdx) out.push(m);
}
return out;
}
/**
* @deprecated 勿再改写 session.messages改用 selectCreationDialogueForAi / buildCreationDialogueTranscript。
* 保留空操作兼容旧调用点。
*/
export function trimCreationDialogueMessages<T extends DialogueChatMessage>(
_messages: T[],
): { removedCount: number } {
return { removedCount: 0 };
}
function displayText(m: DialogueChatMessage): string {
const body = (m.body ?? m.text ?? "").trim();
return body || "(空)";
}
/** 拼给 design worker 的对话前情(用户全量 + 最后一次 AI不改 messages */
export function buildCreationDialogueTranscript(
messages: readonly DialogueChatMessage[],
): string {
const selected = selectCreationDialogueForAi(messages);
const lines: string[] = [
"以下为创作过程对话用户发言全部保留AI 仅保留最后一次输出,含未完成提问)。",
"",
];
for (const m of selected) {
if (isUserMessage(m)) {
lines.push(`### 用户`);
lines.push(displayText(m));
lines.push("");
continue;
}
if (isAiContent(m)) {
const who =
m.kind === "worker_questions"
? `AI · ${m.actor ?? "worker"} 提问`
: m.kind === "worker_output"
? `AI · ${m.actor ?? "worker"} 产出`
: m.kind === "orchestrator_thinking"
? "AI · 总管思考"
: `AI · ${m.title ?? m.kind ?? "系统"}`;
lines.push(`### ${who}`);
lines.push(displayText(m));
lines.push("");
}
}
const text = lines.join("\n").trim();
return text || "(尚无创作对话)";
}
/**
* @deprecated 创作验收后不再删 messages用户可继续浏览。保留空操作兼容。
*/
export function pruneCreationUnitMessages<T extends DialogueChatMessage>(
_messages: T[],
_workerId: string,
_options: { afterCreatedAt?: string | null } = {},
): { removedCount: number; productKept: boolean } {
return { removedCount: 0, productKept: false };
}
/** Run 验收:过程消息标 compressed主 feed 隐藏),不删除。 */
export function foldRunProcessMessages<T extends DialogueChatMessage>(
messages: T[],
workerId: string,
): void {
for (const m of messages) {
if (m.compressed) continue;
if (
(m.kind === "worker_questions" || m.kind === "worker_running") &&
(m.actor === workerId || (m.text ?? "").includes(workerId))
) {
m.compressed = true;
continue;
}
if (
m.kind === "worker_output" &&
m.actor === workerId &&
!String(m.text ?? "").includes("[上下文已压缩]")
) {
m.compressed = true;
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -10,20 +10,24 @@ import {
import {
ensureActiveProfileDefault,
loadAppSettings,
normalizeContextTraceKeepLatest,
saveAppSettings,
setActivePresetId,
setActiveProfileId,
} from "../config/settings.js";
import {
countEnabledEntries,
countInjectingEntries,
listAllPresetEntries,
patchPresetEntries,
type PresetEntryPatch,
} from "../preset/entries.js";
import {
deletePreset,
getPreset,
importAndSavePreset,
listPresets,
} from "../preset/store.js";
import {
countInjectingEntries,
listEnabledPresetEntries,
} from "../preset/entries.js";
import { sessionManager } from "./session-manager.js";
async function readBody(req: IncomingMessage): Promise<string> {
@@ -49,13 +53,14 @@ export async function handleSettingsApi(
const settings = loadAppSettings();
const profiles = listApiProfiles();
const presets = listPresets().map((p) => {
const entries = listEnabledPresetEntries(p);
const all = listAllPresetEntries(p);
return {
id: p.id,
name: p.name,
source: p.source,
enabledCount: p.promptOrder.filter((o) => o.enabled).length,
injectingCount: countInjectingEntries(entries),
enabledCount: countEnabledEntries(all),
injectingCount: countInjectingEntries(all),
entryCount: all.length,
importedAt: p.importedAt,
};
});
@@ -67,6 +72,7 @@ export async function handleSettingsApi(
const body = JSON.parse(await readBody(req)) as {
activeProfileId?: string | null;
activePresetId?: string | null;
contextTraceKeepLatest?: number;
};
const settings = loadAppSettings();
if (body.activeProfileId !== undefined) {
@@ -75,11 +81,28 @@ export async function handleSettingsApi(
if (body.activePresetId !== undefined) {
settings.activePresetId = body.activePresetId;
}
if (body.contextTraceKeepLatest !== undefined) {
settings.contextTraceKeepLatest = normalizeContextTraceKeepLatest(
body.contextTraceKeepLatest,
);
}
saveAppSettings(settings);
json(res, 200, { settings });
return true;
}
if (pathname === "/api/settings/context-traces/prune" && req.method === "POST") {
const result = sessionManager.pruneAllOpenContextTraces();
json(res, 200, result);
return true;
}
if (pathname === "/api/settings/context-traces/clear" && req.method === "POST") {
const result = sessionManager.clearAllOpenContextTraces();
json(res, 200, result);
return true;
}
if (pathname === "/api/profiles" && req.method === "GET") {
ensureActiveProfileDefault();
json(res, 200, { profiles: listApiProfiles() });
@@ -206,22 +229,65 @@ export async function handleSettingsApi(
const presetEntriesMatch = pathname.match(
/^\/api\/presets\/([^/]+)\/entries$/,
);
if (presetEntriesMatch && req.method === "GET") {
if (presetEntriesMatch) {
const id = decodeURIComponent(presetEntriesMatch[1]);
const preset = getPreset(id);
if (!preset) {
json(res, 404, { error: "预设不存在" });
return true;
}
const entries = listEnabledPresetEntries(preset);
json(res, 200, {
presetId: preset.id,
presetName: preset.name,
generation: preset.generation,
entries,
injectingCount: countInjectingEntries(entries),
});
return true;
if (req.method === "GET") {
const entries = listAllPresetEntries(preset);
json(res, 200, {
presetId: preset.id,
presetName: preset.name,
generation: preset.generation,
entries,
enabledCount: countEnabledEntries(entries),
injectingCount: countInjectingEntries(entries),
});
return true;
}
if (req.method === "PATCH" || req.method === "PUT") {
const body = JSON.parse(await readBody(req)) as {
entries?: PresetEntryPatch[];
entry?: PresetEntryPatch;
};
const patches = body.entries?.length
? body.entries
: body.entry
? [body.entry]
: [];
if (!patches.length) {
json(res, 400, { error: "缺少 entries 或 entry" });
return true;
}
try {
const next = patchPresetEntries(preset, patches);
const entries = listAllPresetEntries(next);
const settings = loadAppSettings();
let reloadedSessions = 0;
if (settings.activePresetId === id) {
reloadedSessions = sessionManager.reloadAllLlms();
}
json(res, 200, {
presetId: next.id,
presetName: next.name,
generation: next.generation,
entries,
enabledCount: countEnabledEntries(entries),
injectingCount: countInjectingEntries(entries),
reloadedSessions,
});
} catch (err) {
json(res, 400, {
error: err instanceof Error ? err.message : String(err),
});
}
return true;
}
}
const presetMatch = pathname.match(/^\/api\/presets\/([^/]+)(\/activate)?$/);
@@ -246,8 +312,13 @@ export async function handleSettingsApi(
json(res, 404, { error: "预设不存在" });
return true;
}
const entries = listEnabledPresetEntries(preset);
json(res, 200, { preset, entries, injectingCount: countInjectingEntries(entries) });
const entries = listAllPresetEntries(preset);
json(res, 200, {
preset,
entries,
enabledCount: countEnabledEntries(entries),
injectingCount: countInjectingEntries(entries),
});
return true;
}

View File

@@ -1,6 +1,20 @@
import type { RuntimeSession } from "../types/runtime.js";
import {
deriveDesignStageScope,
deriveRunWorkerScope,
instantiateMeta,
parseWorkerSetYaml,
runWorkerMeta,
} from "../skills/worker-set-parse.js";
import {
canEnterPlay,
hasAcceptedWorkerSet,
inferLifecycleStage,
type LifecycleStage,
} from "../skills/worker-declaration.js";
export type LifecycleStage = "design" | "play";
export type { LifecycleStage };
export { canEnterPlay, hasAcceptedWorkerSet, inferLifecycleStage };
export type SkillCatalogEntry = {
id: string;
@@ -9,6 +23,8 @@ export type SkillCatalogEntry = {
/** 这一步要干嘛(占位说明,详细设计后续补充) */
purpose: string;
status: "pending" | "active" | "done" | "skipped";
/** 同一 skill 被 invoke 的次数design 阶段 instantiate 可多次) */
runCount?: number;
};
type CatalogTemplate = {
@@ -18,242 +34,172 @@ type CatalogTemplate = {
purpose: string;
};
const GENERIC_DESIGN: CatalogTemplate[] = [
{
id: "interaction-paradigm",
stage: "design",
label: "交互范式",
purpose: "弄清用户要什么体验,产出 run skill 清单(要哪些能力)。",
},
{
id: "intake",
stage: "design",
label: "启动收集",
purpose: "收集最小需求,写入用户.需求 / book.brief。",
},
{
id: "world-blueprint",
stage: "design",
label: "世界蓝图",
purpose: "定背景板与核心冲突,供后续 skill 引用。",
},
{
id: "narrative-guide",
stage: "design",
label: "叙事指南",
purpose: "定 POV、时态、文风static 上下文上半)。",
},
{
export type BuildSkillCatalogOptions = {
/** `设计.worker集` 或 `.草稿` 的 YAML 正文 */
workerSetYaml?: string;
};
function templatesFor(_skillPackId?: string): CatalogTemplate[] {
return [];
}
type ArtifactRunStats = {
total: number;
accepted: number;
pending: number;
rejected: number;
};
function collectArtifactRuns(session: RuntimeSession): Map<string, ArtifactRunStats> {
const map = new Map<string, ArtifactRunStats>();
for (const art of session.artifacts) {
if (!art.workerId) continue;
const prev = map.get(art.workerId) ?? {
total: 0,
accepted: 0,
pending: 0,
rejected: 0,
};
prev.total += 1;
if (art.status === "accepted") prev.accepted += 1;
else if (art.status === "rejected") prev.rejected += 1;
else prev.pending += 1;
map.set(art.workerId, prev);
}
return map;
}
function statusForWorkerId(
id: string,
session: RuntimeSession,
runs: Map<string, ArtifactRunStats>,
skipped: Set<string>,
): SkillCatalogEntry["status"] {
if (skipped.has(id)) return "skipped";
const stats = runs.get(id);
if (stats?.accepted) return "done";
if (session.currentWorkerId === id) return "active";
if (stats?.pending) return "active";
return "pending";
}
function buildWorldSimulatorCatalog(
session: RuntimeSession,
lifecycle: LifecycleStage,
workerSetYaml?: string,
): SkillCatalogEntry[] {
const workerSet = parseWorkerSetYaml(workerSetYaml);
const runs = collectArtifactRuns(session);
const skipped = new Set(workerSet?.instantiate_hints?.skip ?? []);
if (lifecycle === "play") {
const runIds = deriveRunWorkerScope(workerSet);
const entries: SkillCatalogEntry[] = [
{
id: "agent-burst",
stage: "run",
label: "总管调度",
purpose: "总管 tool loop读黑板 → 选择下一步 Worker。",
status:
session.phase === "running" && !session.currentWorkerId
? "active"
: "pending",
},
];
for (const id of runIds) {
const meta = runWorkerMeta(id);
const stats = runs.get(id);
entries.push({
id,
stage: "run",
label: meta.label,
purpose: meta.purpose,
status: statusForWorkerId(id, session, runs, new Set()),
runCount: stats?.total || undefined,
});
}
return entries;
}
const designSteps: Array<{ id: string; label: string; purpose: string }> = [
{
id: "design-flow",
label: "创作 · 流程编排",
purpose: "编排/增量修订可变 DAG → 设计.创作流程(可反复编入同能力)。",
},
{
id: "design-step",
label: "创作 · 执行步骤",
purpose: "按已认可流程执行当前一步模块。",
},
];
const entries: SkillCatalogEntry[] = designSteps.map((step) => ({
id: step.id,
stage: "design" as const,
label: step.label,
purpose: step.purpose,
status: statusForWorkerId(step.id, session, runs, skipped),
runCount: runs.get(step.id)?.total || undefined,
}));
const planned = deriveDesignStageScope(workerSet);
for (const id of planned) {
const meta = instantiateMeta(id);
const stats = runs.get(id);
entries.push({
id,
stage: "design",
label: meta.label,
purpose: meta.purpose,
status: statusForWorkerId(id, session, runs, skipped),
runCount: stats?.total || undefined,
});
}
const designIds = new Set(designSteps.map((s) => s.id));
for (const [id, stats] of runs) {
if (designIds.has(id) || id === "design-intake") continue;
if (planned.includes(id) || skipped.has(id)) continue;
const meta = instantiateMeta(id);
entries.push({
id,
stage: "design",
label: meta.label,
purpose: meta.purpose,
status: statusForWorkerId(id, session, runs, skipped),
runCount: stats.total || undefined,
});
}
entries.push({
id: "declare-ready",
stage: "design",
label: "实例就绪",
purpose: "agent 确认设计够开跑,进入游玩阶段。",
},
];
purpose: "Worker 集已 accept 即可进游玩;若声明了开局,建议先验收开场白(非强制锁定)。",
status: hasAcceptedWorkerSet(session) ? "done" : "pending",
});
const GENERIC_RUN: CatalogTemplate[] = [
{
id: "agent-burst",
stage: "run",
label: "Agent 调度",
purpose: "总管 tool loop读黑板 → 选择 invoke 哪个 run skill。",
},
{
id: "narrator",
stage: "run",
label: "转述 / 展示",
purpose: "把世界状态编排成给用户看的叙事回复。",
},
{
id: "world-simulator",
stage: "run",
label: "世界模拟",
purpose: "裁决规则、更新事件流与可见信息。",
},
];
const BY_SKILL_PACK: Record<string, CatalogTemplate[]> = {
basic: [
{
id: "intake",
stage: "design",
label: "创作简报",
purpose: "收集题材、篇幅、风格 → book.brief。",
},
{
id: "declare-ready",
stage: "design",
label: "进入运行",
purpose: "简报确认后 declare ready。",
},
{
id: "outline",
stage: "run",
label: "生成大纲",
purpose: "根据 brief 生成 outline 产物。",
},
],
"weird-rules-short": [
{
id: "intake",
stage: "design",
label: "创作简报",
purpose: "收集规则怪谈情境与条数。",
},
{
id: "write-rules",
stage: "run",
label: "写规则",
purpose: "产出规则草稿与隐藏 core。",
},
{
id: "review-infer",
stage: "run",
label: "读者验收",
purpose: "盲读规则,不写 core。",
},
{
id: "review-author",
stage: "run",
label: "作者验收",
purpose: "对照 core 查一致性。",
},
],
"roleplay-game-theory": [
{
id: "intake",
stage: "design",
label: "博弈需求",
purpose: "收集情境、角色、轮次 → 用户.博弈需求。",
},
{
id: "setup-scenario",
stage: "design",
label: "结构化设定",
purpose: "整理为情境、规则、角色设定 tag。",
},
{
id: "declare-ready",
stage: "design",
label: "开始模拟",
purpose: "setup 验收后进入 run。",
},
{
id: "world-engine",
stage: "run",
label: "世界机",
purpose: "发可见信息、收行动、裁决回合。",
},
{
id: "role-decide",
stage: "run",
label: "角色决策",
purpose: "各角色独立产出思考与行动。",
},
{
id: "present-round",
stage: "run",
label: "回合展示",
purpose: "编排给用户看的本轮摘要。",
},
],
"world-simulator": [
{
id: "interaction-paradigm",
stage: "design",
label: "交互范式",
purpose: "定体验与 run skill 清单。",
},
{
id: "world-blueprint",
stage: "design",
label: "世界蓝图",
purpose: "背景板与核心设定。",
},
{
id: "topology",
stage: "design",
label: "拓扑 / 关系",
purpose: "地图、关系网或进阶路径(按需)。",
},
{
id: "generation-rules",
stage: "design",
label: "生成规则",
purpose: "元规则:如何生成实例内容。",
},
{
id: "narrative-guide",
stage: "design",
label: "叙事指南",
purpose: "正文气质与禁忌static 上)。",
},
{
id: "variable-catalog",
stage: "design",
label: "变量目录",
purpose: "要跟踪的状态与更新格式。",
},
{
id: "declare-ready",
stage: "design",
label: "实例就绪",
purpose: "agent 声明可开跑。",
},
{
id: "world-simulator",
stage: "run",
label: "世界模拟器",
purpose: "每轮推进世界状态与事件流。",
},
{
id: "narrator",
stage: "run",
label: "转述者",
purpose: "把状态写成用户可见叙事。",
},
],
};
function templatesFor(skillPackId?: string): CatalogTemplate[] {
if (skillPackId && BY_SKILL_PACK[skillPackId]) {
return BY_SKILL_PACK[skillPackId];
}
return [...GENERIC_DESIGN, ...GENERIC_RUN];
}
export function inferLifecycleStage(session: RuntimeSession): LifecycleStage {
const override = session.slots.uiLifecycleStage;
if (override === "design" || override === "play") {
return override;
}
if (!session.slots.startupCompleted) return "design";
if (session.phase === "done") return "play";
return "play";
}
export function canEnterPlay(session: RuntimeSession): boolean {
return Boolean(session.slots.startupCompleted);
return entries;
}
export function buildSkillCatalog(
session: RuntimeSession,
skillPackId?: string,
lifecycle: LifecycleStage = inferLifecycleStage(session),
options?: BuildSkillCatalogOptions,
): SkillCatalogEntry[] {
if (skillPackId === "world-simulator") {
return buildWorldSimulatorCatalog(session, lifecycle, options?.workerSetYaml);
}
const templates = templatesFor(skillPackId);
const filtered = templates.filter((t) =>
lifecycle === "design" ? t.stage === "design" : t.stage === "run",
);
const workerIds = new Set(
session.artifacts.map((a) => a.workerId).filter(Boolean),
);
const acceptedWorkers = new Set(
session.artifacts
.filter((a) => a.status === "accepted")
.map((a) => a.workerId),
);
const runs = collectArtifactRuns(session);
return filtered.map((t) => {
let status: SkillCatalogEntry["status"] = "pending";
@@ -267,18 +213,25 @@ export function buildSkillCatalog(
status = "active";
}
} else if (t.id === "declare-ready") {
if (session.slots.startupCompleted) status = "done";
if (session.slots.startupCompleted || hasAcceptedWorkerSet(session)) {
status = "done";
}
} else if (t.id === "agent-burst") {
if (session.phase === "running" && !session.currentWorkerId) {
status = "active";
}
} else if (workerIds.has(t.id)) {
status = acceptedWorkers.has(t.id) ? "done" : "active";
} else if (runs.has(t.id)) {
status = statusForWorkerId(t.id, session, runs, new Set());
} else if (session.currentWorkerId === t.id) {
status = "active";
}
return { ...t, status };
const runCount = runs.get(t.id)?.total;
return {
...t,
status,
runCount: runCount || undefined,
};
});
}

View File

@@ -65,6 +65,8 @@ const server = createServer(async (req, res) => {
"GET /api/presets",
"GET /api/books",
"GET /api/skills",
"GET /api/directors",
"GET /api/modules",
"GET /api/stats/tokens",
],
});
@@ -130,6 +132,104 @@ const server = createServer(async (req, res) => {
return;
}
if (req.method === "POST" && sub === "/answers") {
const body = JSON.parse(await readBody(req)) as {
answers?: Array<{
questionId?: string;
optionId?: string;
text?: string;
}>;
note?: string;
};
const answers = (body.answers ?? [])
.filter(
(a) =>
typeof a?.questionId === "string" &&
a.questionId.trim() &&
typeof a?.text === "string" &&
a.text.trim(),
)
.map((a) => ({
questionId: a.questionId!.trim(),
optionId:
typeof a.optionId === "string" && a.optionId.trim()
? a.optionId.trim()
: undefined,
text: a.text!.trim(),
}));
if (!answers.length) {
json(res, 400, { error: "answers 不能为空" });
return;
}
const note =
typeof body.note === "string" && body.note.trim()
? body.note.trim()
: undefined;
try {
const view = await sessionManager.answerQuestions(
sessionId,
answers,
note,
);
json(res, 200, view);
} catch (err) {
json(res, 400, {
error: err instanceof Error ? err.message : "提交失败",
});
}
return;
}
const messageActionMatch = sub.match(
/^\/messages\/([^/]+)\/(edit|refresh|variant|delete)$/,
);
if (req.method === "POST" && messageActionMatch) {
const messageId = decodeURIComponent(messageActionMatch[1]);
const action = messageActionMatch[2];
const body = JSON.parse(await readBody(req).catch(() => "{}")) as {
text?: string;
direction?: string;
};
let view;
try {
if (action === "edit") {
if (!body.text?.trim()) {
json(res, 400, { error: "text 不能为空" });
return;
}
view = await sessionManager.editMessage(
sessionId,
messageId,
body.text.trim(),
);
} else if (action === "refresh") {
view = await sessionManager.refreshMessage(sessionId, messageId);
} else if (action === "variant") {
if (body.direction !== "prev" && body.direction !== "next") {
json(res, 400, { error: "direction 须为 prev 或 next" });
return;
}
view = await sessionManager.switchMessageVariant(
sessionId,
messageId,
body.direction,
);
} else if (action === "delete") {
view = await sessionManager.deleteMessage(sessionId, messageId);
} else {
json(res, 400, { error: "未知 action" });
return;
}
} catch (err) {
json(res, 400, {
error: err instanceof Error ? err.message : "操作失败",
});
return;
}
json(res, 200, view);
return;
}
if (req.method === "POST" && sub === "/lifecycle") {
const body = JSON.parse(await readBody(req)) as { stage?: string };
if (body.stage !== "design" && body.stage !== "play") {
@@ -147,6 +247,74 @@ const server = createServer(async (req, res) => {
return;
}
if (req.method === "POST" && sub === "/recipe") {
const body = JSON.parse(await readBody(req)) as { recipeId?: string };
if (!body.recipeId?.trim()) {
json(res, 400, { error: "缺少 recipeId" });
return;
}
try {
const view = await sessionManager.setSelectedRecipe(
sessionId,
body.recipeId.trim(),
);
json(res, 200, view);
} catch (err) {
json(res, 400, {
error: err instanceof Error ? err.message : "选定配方失败",
});
}
return;
}
if (req.method === "POST" && sub === "/board") {
const body = JSON.parse(await readBody(req)) as {
tag?: string;
content?: string;
};
if (!body.tag?.trim()) {
json(res, 400, { error: "缺少 tag" });
return;
}
try {
const view = sessionManager.writeUserBoardTag(
sessionId,
body.tag,
body.content ?? "",
);
json(res, 200, view);
} catch (err) {
json(res, 400, {
error: err instanceof Error ? err.message : "写入失败",
});
}
return;
}
if (req.method === "POST" && sub === "/context-traces/prune") {
try {
const result = sessionManager.pruneSessionContextTraces(sessionId);
json(res, 200, { ...result, session: sessionManager.get(sessionId) });
} catch (err) {
json(res, 400, {
error: err instanceof Error ? err.message : "修剪失败",
});
}
return;
}
if (req.method === "POST" && sub === "/context-traces/clear") {
try {
const result = sessionManager.clearSessionContextTraces(sessionId);
json(res, 200, { ...result, session: sessionManager.get(sessionId) });
} catch (err) {
json(res, 400, {
error: err instanceof Error ? err.message : "清除失败",
});
}
return;
}
if (req.method === "POST" && sub === "/actions") {
const body = JSON.parse(await readBody(req)) as { action?: string };
let view;
@@ -160,6 +328,9 @@ const server = createServer(async (req, res) => {
case "accept":
view = await sessionManager.accept(sessionId);
break;
case "skip_questions":
view = await sessionManager.skipQuestions(sessionId);
break;
case "reject":
view = await sessionManager.reject(sessionId);
break;