重构世界模拟器为模块化配方架构,完善创作编排、会话运行时与 Web UI,并清理过时技能。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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: "请发送任意消息继续,或联系维护者",
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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 / 能力包 id(registry 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;
|
||||
}
|
||||
|
||||
121
src/server/display-labels.ts
Normal file
121
src/server/display-labels.ts
Normal 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 "导演";
|
||||
}
|
||||
223
src/server/message-branch.ts
Normal file
223
src/server/message-branch.ts
Normal 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;
|
||||
}
|
||||
162
src/server/prune-creation-messages.ts
Normal file
162
src/server/prune-creation-messages.ts
Normal 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
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user