新建作品改为直接进入对话落盘,按本局创作方案写能力,并把楼层呈现和零层卡分开。

输入栏同时可以按分组切换模型。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-30 12:00:46 +08:00
co-authored by Cursor
parent 39fe2732fb
commit c2c9203fb0
87 changed files with 8413 additions and 3180 deletions
+524 -26
View File
@@ -15,6 +15,7 @@ import {
catalogModulesForIntake,
formatRecipeCatalogForAgent,
formatDictateRecipeBrief,
formatRecipeExamplesForCreationPlan,
formatSelectedRecipeForAgent,
isCreationFlowComplete,
loadAllRecipeDetails,
@@ -28,6 +29,7 @@ import {
listReadySteps,
computeStepLayers,
spawnRepeatableCreationStep,
repeatableInstanceProductTag,
spawnInstanceFromPrototype,
hasSelectableCreationWork,
hasAcceptedCloserStep,
@@ -466,7 +468,7 @@ describe("creation-flow", () => {
catalog?.modules.find((m) => m.name === "具体实例")?.params?.[0]?.key,
).toBe("rule_id");
expect(
catalog?.modules.find((m) => m.name === "上下文投影排序")?.auto,
catalog?.modules.find((m) => m.name === "上下文排序")?.auto,
).toBe(true);
const gen = catalog?.modules.find((m) => m.name === "生成规则");
expect(gen?.when).toBeTruthy();
@@ -476,8 +478,7 @@ describe("creation-flow", () => {
expect(block).toContain("【能力");
expect(block).toContain("美学纲领与交互范式:");
expect(block).toContain("生成规则〔可反复〕〔先验产物〕");
expect(block).toContain("开场白与开场变量〔收口〕");
expect(block).toContain("上下文投影排序〔程序步〕");
expect(block).toContain("上下文排序〔程序步〕");
expect(block).toContain("何时用");
expect(block).toContain("何时不用");
expect(block).toContain("role=prototype");
@@ -489,6 +490,8 @@ describe("creation-flow", () => {
const dictateBlock = formatModuleCatalogForDictate(catalog!);
expect(dictateBlock).toMatch(/^- 叙事指南:/m);
expect(dictateBlock).toMatch(/^- 故事推进:/m);
expect(dictateBlock).toContain("开场白与开场变量〔可反复〕〔收口〕");
expect(dictateBlock).toContain("信息可见范围〔落档程序〕");
expect(dictateBlock).not.toMatch(/^- 叙事指南与故事推进/m);
expect(
catalog?.modules.find((m) => m.name === "叙事指南与故事推进")?.intake,
@@ -499,16 +502,17 @@ describe("creation-flow", () => {
expect(catalog?.modules.find((m) => m.name === "故事推进")?.intake).toBe(
"dictate",
);
expect(catalog?.modules.find((m) => m.name === "用户需求")?.libraries).toEqual([
expect(catalog?.modules.find((m) => m.name === "故事推进")?.libraries).toEqual([
"preferences",
]);
expect(catalog?.modules.some((m) => m.id === "user-requirements")).toBe(false);
expect(
catalog?.modules.find((m) => m.name === "叙事指南")?.libraries,
).toEqual(["style-packs"]);
expect(
catalog?.modules.find((m) => m.name === "叙事指南与故事推进")?.libraries,
).toEqual(["style-packs"]);
expect(dictateBlock).toContain("【偏好库 · 可选用】");
expect(dictateBlock).not.toContain("【偏好库 · 可选用】");
expect(
catalogModulesForIntake(catalog!, "recipe").some(
(m) => m.name === "叙事指南",
@@ -570,36 +574,44 @@ describe("creation-flow", () => {
expect(dictate?.recipes.some((r) => r.name === "数据化跑团体验")).toBe(true);
expect(dictate?.recipes.some((r) => r.id === "快穿短局")).toBe(true);
expect(dictate?.recipes.some((r) => r.name === "快穿短局")).toBe(true);
expect(dictate?.recipes.some((r) => r.id === "正文组成")).toBe(false);
expect(dictate?.recipes).toHaveLength(4);
expect(dictate?.recipes[0]?.id).toBe("快穿短局");
const entry = dictate!.recipes.find((r) => r.id === "文本生成器")!;
expect(entry.family).toBe("dictate");
const detail = await loadRecipeDetail("dialogue/world-simulator", entry);
expect(detail.core).toMatch(/范例|模仿|转述|游玩/);
expect(detail.method?.join("")).toMatch(/范例|写法|输入/);
expect(detail.tierTendencies?.some((item) => item.module_id === "narrative-guide")).toBe(true);
expect(detail.seed).toBeNull();
expect(formatDictateRecipeBrief(detail)).toMatch(/模仿|范例|转述|开玩/);
expect(formatDictateRecipeBrief(detail)).toMatch(/模仿要点|学哪些面|要点|文风/);
const longform = dictate!.recipes.find((r) => r.id === "交互式长文生成器")!;
const longDetail = await loadRecipeDetail("dialogue/world-simulator", longform);
expect(longDetail.brief).toMatch(/长文/);
expect(longDetail.core).toMatch(/长文|分段/);
expect(longDetail.brief).toMatch(/长连续/);
expect(longDetail.method?.join("")).toMatch(/变量|动态表/);
expect(longDetail.method?.join("")).toMatch(/硬机制|数据化跑团/);
const rpg = dictate!.recipes.find((r) => r.id === "数据化跑团体验")!;
const rpgDetail = await loadRecipeDetail("dialogue/world-simulator", rpg);
expect(rpgDetail.brief).toMatch(/美学|真值|映射/);
expect(rpgDetail.core).toMatch(/美学|真值|declare_variable|何时落盘/);
expect(formatDictateRecipeBrief(rpgDetail)).toContain("declare_variable");
expect(formatDictateRecipeBrief(rpgDetail)).toContain("何时落盘");
expect(formatDictateRecipeBrief(rpgDetail)).toMatch(/美学纲领|体验核心/);
expect(rpgDetail.brief).toMatch(/硬判断|结构化状态/);
expect(rpgDetail.method?.join("")).toMatch(/长短|连续性/);
expect(rpgDetail.tierTendencies?.some((item) => item.module_id === "mechanism")).toBe(true);
const skip = dictate!.recipes.find((r) => r.id === "快穿短局")!;
const skipDetail = await loadRecipeDetail("dialogue/world-simulator", skip);
expect(skipDetail.seed).toBeNull();
expect(skipDetail.brief).toMatch(/一核|短局|主角/);
expect(skipDetail.core).toMatch(/一核|开场白|黑板/);
expect(skipDetail.when).toMatch(/快穿|一次性|短任务/);
expect(formatDictateRecipeBrief(skipDetail)).toMatch(/舞台骨架|生成规则|拓扑/);
expect(formatDictateRecipeBrief(skipDetail)).toMatch(/回合推演|数据化跑团体验/);
expect(skipDetail.brief).toMatch(/体验核|短反馈/);
expect(skipDetail.signals?.join("")).toMatch(/快穿|一次性|短任务/);
const examples = formatRecipeExamplesForCreationPlan([
detail,
longDetail,
rpgDetail,
skipDetail,
]);
expect(examples).toContain("适用信号");
expect(examples).toContain("档位倾向");
expect(examples).toContain("variable-design=有必要");
expect(examples).not.toContain("固定路径");
});
it("parses selected recipe ref", () => {
@@ -844,10 +856,8 @@ modules:
);
expect(prompt).toBeTruthy();
const opening = extractModuleOpening(prompt!);
expect(opening).toContain("原型世界");
expect(opening).toContain("变在哪里");
expect(opening).toContain("代入");
expect(opening).toContain("最想反复感受到");
expect(opening).toContain("反复获得什么体验");
expect(opening).toContain("站在什么位置");
expect(opening).not.toContain("nail清站位");
const {
@@ -884,11 +894,400 @@ modules:
}),
currentStepName: "美学纲领与交互范式",
});
expect(binding?.opening).toContain("原型世界");
expect(binding?.opening).toContain("反复获得什么体验");
expect(binding?.modulePrompt).toContain("```task");
expect(binding?.modulePrompt).not.toContain("最想反复感受到的是什么");
});
it("loads the migrated world-blueprint contract", async () => {
const catalog = await loadModuleCatalog("dialogue/world-simulator");
const module = catalog?.modules.find((item) => item.id === "world-blueprint");
expect(module?.layer).toBe("final");
expect(module?.mount).toEqual(["world-simulator"]);
const prompt = await loadModulePrompt(
"dialogue/world-simulator",
"world-blueprint",
);
const { parseModulePromptSections } = await import(
"../src/skills/creation-flow.js"
);
const sections = parseModulePromptSections(prompt!);
expect(sections.blocks.task).toContain("稳定反馈");
expect(sections.blocks.output).toContain('"核心运转"');
expect(sections.blocks.output).not.toContain('"自评"');
expect(sections.blocks.output).not.toContain('"追问"');
expect(sections.blocks.score).toContain("支撑度");
expect(sections.blocks.score).toContain("克制度");
expect(sections.blocks.score).not.toContain("0–10");
});
it("loads the migrated generation-rules contract", async () => {
const catalog = await loadModuleCatalog("dialogue/world-simulator");
const module = catalog?.modules.find((item) => item.id === "generation-rules");
expect(module?.layer).toBe("final");
expect(module?.mount).toEqual(["world-simulator", "auditor"]);
expect(module?.repeatable).toBe(true);
const prompt = await loadModulePrompt(
"dialogue/world-simulator",
"generation-rules",
);
const { parseModulePromptSections } = await import(
"../src/skills/creation-flow.js"
);
const sections = parseModulePromptSections(prompt!);
expect(sections.blocks.meta).toContain("两份或以上");
expect(sections.blocks.task).toContain("填写工作流");
expect(sections.blocks.output).toContain('"字段"');
expect(sections.blocks.output).toContain('"填写工作流"');
expect(sections.blocks.output).toContain('"依赖关系"');
expect(sections.blocks.output).not.toContain('"rules"');
expect(sections.blocks.output).not.toContain('"自评"');
expect(sections.blocks.score).toContain("字段妥当");
expect(sections.blocks.score).toContain("池与提示");
expect(sections.blocks.score).toContain("填写工作流");
});
it("loads the migrated concrete-instances contract", async () => {
const catalog = await loadModuleCatalog("dialogue/world-simulator");
const module = catalog?.modules.find(
(item) => item.id === "concrete-instances",
);
expect(module?.layer).toBe("final");
expect(module?.mount).toEqual(["world-simulator"]);
expect(module?.repeatable).toBe(true);
const prompt = await loadModulePrompt(
"dialogue/world-simulator",
"concrete-instances",
);
const { parseModulePromptSections } = await import(
"../src/skills/creation-flow.js"
);
const sections = parseModulePromptSections(prompt!);
expect(sections.blocks.meta).toContain("字段很少");
expect(sections.blocks.task).toContain("一份产物只含一条记录");
expect(sections.blocks.output).toContain('"名称"');
expect(sections.blocks.output).toContain('"记录"');
expect(sections.blocks.output).not.toContain('"自评"');
expect(sections.blocks.score).toContain("工作流符合度");
expect(sections.blocks.score).not.toContain("充实度");
});
it("loads the migrated narrative-guide contract", async () => {
const catalog = await loadModuleCatalog("dialogue/world-simulator");
const module = catalog?.modules.find((item) => item.id === "narrative-guide");
expect(module?.layer).toBe("final");
expect(module?.mount).toEqual(["narrator", "world-simulator"]);
expect(module?.libraries).toEqual(["style-packs"]);
const prompt = await loadModulePrompt(
"dialogue/world-simulator",
"narrative-guide",
);
const { parseModulePromptSections } = await import(
"../src/skills/creation-flow.js"
);
const sections = parseModulePromptSections(prompt!);
expect(sections.blocks.task).toContain("read_library_entry");
expect(sections.blocks.task).toContain("风格推荐");
expect(sections.blocks.output).toContain('"选用文风"');
expect(sections.blocks.output).toContain('"写法要求"');
expect(sections.blocks.output).not.toContain('"示范"');
expect(sections.blocks.output).not.toContain('"自评"');
expect(sections.blocks.score).toContain("遣词可执行");
expect(sections.blocks.score).toContain("文风选用");
expect(sections.blocks.score).toContain("本局化");
});
it("loads the migrated story-progression contract", async () => {
const catalog = await loadModuleCatalog("dialogue/world-simulator");
const module = catalog?.modules.find(
(item) => item.id === "story-progression",
);
expect(module?.layer).toBe("final");
expect(module?.mount).toEqual(["world-simulator", "narrator"]);
expect(module?.libraries).toEqual(["preferences"]);
const prompt = await loadModulePrompt(
"dialogue/world-simulator",
"story-progression",
);
const { parseModulePromptSections } = await import(
"../src/skills/creation-flow.js"
);
const sections = parseModulePromptSections(prompt!);
expect(sections.blocks.task).toContain("只选定一种");
expect(sections.blocks.task).toContain("read_library_entry");
expect(sections.blocks.output).toContain('"用户输入用法"');
expect(sections.blocks.output).toContain('"扩写要求"');
expect(sections.blocks.output).toContain('"续写要求"');
expect(sections.blocks.output).toContain('"单轮篇幅"');
expect(sections.blocks.output).toContain('"目标字数"');
expect(sections.blocks.output).toContain('"扩写占比"');
expect(sections.blocks.output).toContain('"续写占比"');
expect(sections.blocks.output).toContain('"OOC处理"');
expect(sections.blocks.output).not.toContain('"自评"');
expect(sections.blocks.score).toContain("唯一性与切分");
expect(sections.blocks.score).toContain("可执行性");
expect(sections.blocks.score).toContain("偏好本局化");
});
it("loads the migrated mechanism contract", async () => {
const catalog = await loadModuleCatalog("dialogue/world-simulator");
const module = catalog?.modules.find((item) => item.id === "mechanism");
expect(module?.layer).toBe("final");
expect(module?.mount).toEqual(["world-simulator"]);
const prompt = await loadModulePrompt(
"dialogue/world-simulator",
"mechanism",
);
const { parseModulePromptSections } = await import(
"../src/skills/creation-flow.js"
);
const sections = parseModulePromptSections(prompt!);
expect(sections.blocks.task).toContain("多条共同作用");
expect(sections.blocks.task).toContain("缺失后果");
expect(sections.blocks.output).toContain('"既成机制"');
expect(sections.blocks.output).toContain('"因果影响"');
expect(sections.blocks.output).toContain('"推演口径"');
expect(sections.blocks.output).toContain('"共同作用"');
expect(sections.blocks.output).not.toContain('"缺失后果"');
expect(sections.blocks.output).not.toContain('"自评"');
expect(sections.blocks.score).toContain("骨架必要度");
expect(sections.blocks.score).toContain("因果清晰度");
expect(sections.blocks.score).toContain("推演开放度");
expect(sections.blocks.examples).toContain("灵气复苏");
expect(sections.blocks.examples).toContain("全民直播求生");
});
it("loads the migrated topology contract", async () => {
const catalog = await loadModuleCatalog("dialogue/world-simulator");
const module = catalog?.modules.find((item) => item.id === "topology");
expect(module?.layer).toBe("final");
expect(module?.mount).toEqual(["world-simulator", "auditor"]);
expect(module?.repeatable).toBe(true);
const prompt = await loadModulePrompt(
"dialogue/world-simulator",
"topology",
);
const { parseModulePromptSections } = await import(
"../src/skills/creation-flow.js"
);
const sections = parseModulePromptSections(prompt!);
expect(sections.blocks.task).toContain("一张图");
expect(sections.blocks.task).toContain("static");
expect(sections.blocks.task).toContain("record");
expect(sections.blocks.task).toContain("expand");
expect(sections.blocks.output).toContain('"节点字段"');
expect(sections.blocks.output).toContain('"连接字段"');
expect(sections.blocks.output).toContain('"初始图"');
expect(sections.blocks.output).toContain('"维护规则"');
expect(sections.blocks.output).not.toContain('"自评"');
expect(sections.blocks.output).not.toContain("SVG");
expect(sections.blocks.score).toContain("对象适配");
expect(sections.blocks.score).toContain("字段合同");
expect(sections.blocks.score).toContain("初始图有效性");
expect(sections.blocks.score).toContain("维护可执行性");
expect(sections.blocks.score).toContain("长期一致性");
});
it("loads the migrated variable-design contract", async () => {
const catalog = await loadModuleCatalog("dialogue/world-simulator");
const module = catalog?.modules.find((item) => item.id === "variable-design");
expect(module?.layer).toBe("final");
expect(module?.mount).toEqual(["world-simulator", "auditor"]);
const prompt = await loadModulePrompt(
"dialogue/world-simulator",
"variable-design",
);
const { parseModulePromptSections } = await import(
"../src/skills/creation-flow.js"
);
const sections = parseModulePromptSections(prompt!);
expect(sections.blocks.task).toContain("deterministic");
expect(sections.blocks.task).toContain("narrative_inference");
expect(sections.blocks.task).toContain("inline_tool");
expect(sections.blocks.task).toContain("auditor_bundle");
expect(sections.blocks.output).toContain('"允许写入者"');
expect(sections.blocks.output).toContain('"更新规则"');
expect(sections.blocks.output).toContain('"派生映射"');
expect(sections.blocks.output).toContain('"触发规则"');
expect(sections.blocks.output).toContain('"维护复杂度"');
expect(sections.blocks.output).not.toContain('"自评"');
expect(sections.blocks.output).not.toContain("maintain.v1");
expect(sections.blocks.output).not.toContain("隐藏段");
expect(sections.blocks.score).toContain("真值必要性");
expect(sections.blocks.score).toContain("字段妥当性");
expect(sections.blocks.score).toContain("更新可执行性");
expect(sections.blocks.score).toContain("权限与公开性");
expect(sections.blocks.score).toContain("映射与触发一致性");
});
it("loads the migrated status-bar contract", async () => {
const catalog = await loadModuleCatalog("dialogue/world-simulator");
const module = catalog?.modules.find((item) => item.id === "status-bar");
expect(module?.layer).toBe("final");
expect(module?.mount).toEqual(["world-simulator", "narrator"]);
const prompt = await loadModulePrompt(
"dialogue/world-simulator",
"status-bar",
);
const { parseModulePromptSections } = await import(
"../src/skills/creation-flow.js"
);
const sections = parseModulePromptSections(prompt!);
expect(sections.blocks.task).toContain("语义区域");
expect(sections.blocks.task).toContain("inline");
expect(sections.blocks.output).toContain('"语义区域"');
expect(sections.blocks.output).toContain("program_projection");
expect(sections.blocks.output).toContain("maintenance_channel");
expect(sections.blocks.output).toContain("state_ops");
expect(sections.blocks.output).not.toContain('"启用"');
expect(sections.blocks.output).not.toContain("1000~2000");
expect(sections.blocks.output).not.toContain('"自评"');
expect(sections.blocks.score).toContain("区域必要性");
expect(sections.blocks.score).toContain("来源一致性");
expect(sections.blocks.score).toContain("内容可执行性");
expect(sections.blocks.score).toContain("信息负担");
expect(sections.blocks.score).toContain("职责边界");
});
it("loads the migrated random-range contract", async () => {
const catalog = await loadModuleCatalog("dialogue/world-simulator");
const module = catalog?.modules.find((item) => item.id === "random-range");
expect(module?.layer).toBe("final");
expect(module?.mount).toEqual(["world-simulator"]);
const prompt = await loadModulePrompt(
"dialogue/world-simulator",
"random-range",
);
const { parseModulePromptSections } = await import(
"../src/skills/creation-flow.js"
);
const sections = parseModulePromptSections(prompt!);
expect(sections.blocks.task).toContain("名称.随机数");
expect(sections.blocks.task).toContain("只剩 1 个");
expect(sections.blocks.output).toContain("敏捷.随机数");
expect(sections.blocks.output).not.toContain("票 id");
expect(sections.blocks.output).not.toContain('"自评"');
expect(sections.blocks.score).toContain("覆盖度");
expect(sections.blocks.score).toContain("克制");
});
it("loads the migrated reply-format contract", async () => {
const catalog = await loadModuleCatalog("dialogue/world-simulator");
const module = catalog?.modules.find((item) => item.id === "reply-format");
expect(module?.layer).toBe("final");
expect(module?.mount).toEqual(["world-simulator", "narrator"]);
const prompt = await loadModulePrompt(
"dialogue/world-simulator",
"reply-format",
);
const { parseModulePromptSections } = await import(
"../src/skills/creation-flow.js"
);
const sections = parseModulePromptSections(prompt!);
expect(sections.blocks.task).toContain("普通楼层");
expect(sections.blocks.task).toContain("present.onData");
expect(sections.blocks.output).toContain('"数据契约"');
expect(sections.blocks.output).toContain('"区域落点"');
expect(sections.blocks.output).toContain('"frontend"');
expect(sections.blocks.output).toContain('"html"');
expect(sections.blocks.output).toContain('"css"');
expect(sections.blocks.output).toContain('"js"');
expect(sections.blocks.output).toContain('"示例灌数"');
expect(sections.blocks.output).not.toContain('"自评"');
expect(sections.blocks.score).toContain("区域落实");
expect(sections.blocks.score).toContain("权威绑定");
expect(sections.blocks.score).toContain("实现完整");
expect(sections.blocks.score).toContain("本局化");
});
it("loads the migrated opening-setup contract", async () => {
const catalog = await loadModuleCatalog("dialogue/world-simulator");
const module = catalog?.modules.find((item) => item.id === "opening-setup");
expect(module?.layer).toBe("final");
expect(module?.mount).toEqual(["world-simulator", "narrator"]);
expect(module?.closer).toBe(true);
expect(module?.repeatable).toBe(true);
const prompt = await loadModulePrompt(
"dialogue/world-simulator",
"opening-setup",
);
const { parseModulePromptSections } = await import(
"../src/skills/creation-flow.js"
);
const sections = parseModulePromptSections(prompt!);
expect(sections.blocks.task).toContain("默认只生成 1 条");
expect(sections.blocks.task).toContain("@玩家");
expect(sections.blocks.task).toContain("随机");
expect(sections.blocks.output).toContain('"默认候选id"');
expect(sections.blocks.output).toContain('"开场白候选"');
expect(sections.blocks.output).not.toContain('"开场白全文"');
expect(sections.blocks.output).toContain("present.v1");
expect(sections.blocks.output).not.toContain('"自评"');
expect(sections.blocks.score).toContain("开场必要");
expect(sections.blocks.score).toContain("前端符合");
expect(sections.blocks.score).toContain("状态自洽");
expect(sections.blocks.score).toContain("角色占位");
expect(sections.blocks.score).toContain("收口可切换");
});
it("loads the three zero-layer presentation contracts", async () => {
const catalog = await loadModuleCatalog("dialogue/world-simulator");
const ids = [
"zero-layer-status",
"zero-layer-reply-format",
"zero-layer-opening-setup",
];
for (const id of ids) {
const module = catalog?.modules.find((item) => item.id === id);
expect(module?.layer).toBe("final");
expect(module?.mount).toEqual(["world-simulator", "narrator"]);
const prompt = await loadModulePrompt("dialogue/world-simulator", id);
const { parseModulePromptSections } = await import(
"../src/skills/creation-flow.js"
);
const sections = parseModulePromptSections(prompt!);
expect(sections.blocks.task).toBeTruthy();
expect(sections.blocks.output).not.toContain('"自评"');
expect(sections.blocks.score).toBeTruthy();
}
expect(
catalog?.modules.find((item) => item.id === "zero-layer-opening-setup")
?.closer,
).toBe(true);
expect(
catalog?.modules.find((item) => item.id === "zero-layer-opening-setup")
?.repeatable,
).toBe(true);
const formatPrompt = await loadModulePrompt(
"dialogue/world-simulator",
"zero-layer-reply-format",
);
expect(formatPrompt).toContain("zero-layer.update.v1");
expect(formatPrompt).toContain("slot_updates");
expect(formatPrompt).toContain("base_revision");
const openingPrompt = await loadModulePrompt(
"dialogue/world-simulator",
"zero-layer-opening-setup",
);
expect(openingPrompt).toContain("zero-layer.card.v1");
expect(openingPrompt).toContain("@玩家");
expect(openingPrompt).toContain("不输出任何 `名称.随机数`");
});
it("parses and validates step params from catalog", async () => {
const flow = parseCreationFlow(`{
"status": "open",
@@ -954,7 +1353,67 @@ modules:
expect(loaded.promptBody).toContain("【本步对象】");
expect(loaded.promptBody).toContain("target: 怪物");
expect(loaded.promptBody).toContain("rule_id: monsters");
expect(loaded.worker.outputTags).toContain("设计.生成规则");
expect(loaded.worker.outputTags).toEqual(["设计.生成规则#monsters"]);
});
it("lands each rule, instance, and opening on its own tag", () => {
const catalog = parseModuleCatalog(`
modules:
- id: generation-rules
name: 生成规则
declaration: 一条规则一份产物
artifact: 设计.生成规则
repeatable: true
- id: concrete-instances
name: 具体实例
declaration: 一条记录一份产物
artifact: 设计.具体实例
repeatable: true
- id: opening-setup
name: 开场白与开场变量
declaration: 一条开场一份产物
artifact: 设计.开场白与开场变量
repeatable: true
closer: true
- id: aesthetics-interaction
name: 美学纲领与交互范式
declaration: 一次写完
artifact: 设计.美学纲领与交互范式
`)!;
const rule = catalog.modules.find((m) => m.id === "generation-rules")!;
const row = catalog.modules.find((m) => m.id === "concrete-instances")!;
const opening = catalog.modules.find((m) => m.id === "opening-setup")!;
const once = catalog.modules.find((m) => m.id === "aesthetics-interaction")!;
expect(
repeatableInstanceProductTag({
module: rule,
step: { id: "生成规则·怪物", role: "instance", params: { rule_id: "女租客" } },
}),
).toBe("设计.生成规则#女租客");
expect(
repeatableInstanceProductTag({
module: row,
step: { id: "具体实例#1", role: "instance", params: { batch_goal: "雷樱" } },
}),
).toBe("设计.具体实例#1");
expect(
repeatableInstanceProductTag({
module: row,
step: { id: "具体实例·雷樱", role: "instance", params: { batch_goal: "雷樱" } },
}),
).toBe("设计.具体实例#雷樱");
expect(
repeatableInstanceProductTag({
module: opening,
step: { id: "开场白与开场变量#dorm", role: "instance" },
}),
).toBe("设计.开场白#dorm");
expect(
repeatableInstanceProductTag({
module: once,
step: { id: "美学纲领与交互范式", role: "instance" },
}),
).toBeNull();
});
it("injects empty prior-artifact plan and still lets the step run", async () => {
@@ -1300,6 +1759,45 @@ modules:
expect(second.step.id).toBe("生成规则#2");
});
it("spawns an opening instance from its own prototype without depending on itself", () => {
const catalog = parseModuleCatalog(`
modules:
- name: 开场白与开场变量
declaration: 可增殖收口
artifact: 设计.开场白与开场变量
closer: true
repeatable: true
`)!;
const flow = parseCreationFlow(`{
"status": "open",
"steps": [
{
"id": "开场白与开场变量",
"name": "开场白与开场变量",
"role": "prototype",
"depends_on": []
}
]
}`)!;
const spawned = spawnInstanceFromPrototype({
flow,
catalog,
prototypeId: "开场白与开场变量",
acceptedStepIds: [],
});
expect("error" in spawned).toBe(false);
if ("error" in spawned) return;
expect(spawned.step.role).toBe("instance");
expect(spawned.step.from).toBe("开场白与开场变量");
expect(spawned.step.depends_on).toEqual([]);
const proto = spawned.flow.steps.find((s) => s.role === "prototype");
expect(proto?.depends_on).toEqual([]);
expect(spawned.flow.steps.map((s) => s.id)).toEqual([
"开场白与开场变量",
"开场白与开场变量#1",
]);
});
it("removeUnstartedInstance drops empty spawn and closer dep", () => {
const flow = parseCreationFlow(`{
"status": "open",
+274
View File
@@ -0,0 +1,274 @@
import { describe, expect, it } from "vitest";
import {
CREATION_PLAN_TAG,
buildDictateSystemPrompt,
creationPlanModules,
formatCreationPlanCapabilityCatalog,
formatCreationPlanProgress,
runDictateTurn,
validateCreationPlan,
type CreationPlan,
} from "../src/dictate/index.js";
import {
formatModuleCatalogForDictate,
loadModuleCatalog,
} from "../src/skills/creation-flow.js";
import type { AgentDriver } from "../src/runtime/driver.js";
async function fixture() {
const catalog = await loadModuleCatalog("dialogue/world-simulator");
expect(catalog).toBeTruthy();
const modules = creationPlanModules(catalog!);
const plan: CreationPlan = {
schema: "creation-plan.v1",
技能: "本局创作方案",
brief: "以封闭雨夜中的身份猜疑兑现持续压迫感",
recipe: {
primary: "封闭空间悬疑",
secondary_traits: ["关系拉扯"],
method: ["让线索公开程度与人物关系共同改变每轮选择"],
references: [],
},
items: modules.map((module, index) => ({
module_id: module.id,
tier: index === 0 ? "必须" : "有一定效果",
why: `${module.name}对本局的作用`,
missing: "",
instances: [],
})),
};
return { catalog: catalog!, modules, plan };
}
describe("creation plan", () => {
it("covers all current content modules and excludes approved mechanical or legacy nodes", async () => {
const { modules } = await fixture();
expect(modules).toHaveLength(14);
expect(modules.map((module) => module.id)).not.toEqual(
expect.arrayContaining([
"creation-plan",
"narrative",
"variable-context",
"context-order",
"worker-spec",
"refine",
"user-requirements",
]),
);
expect(modules.map((module) => module.id)).toEqual(
expect.arrayContaining([
"aesthetics-interaction",
"generation-rules",
"reply-format",
"opening-setup",
]),
);
});
it("selects exactly one presentation chain", async () => {
const catalog = await loadModuleCatalog("dialogue/world-simulator");
expect(catalog).toBeTruthy();
const zeroModules = creationPlanModules(catalog!, "zero_layer");
const ids = zeroModules.map((module) => module.id);
expect(zeroModules).toHaveLength(14);
expect(ids).toEqual(
expect.arrayContaining([
"zero-layer-status",
"zero-layer-reply-format",
"zero-layer-opening-setup",
]),
);
expect(ids).not.toEqual(
expect.arrayContaining(["status-bar", "reply-format", "opening-setup"]),
);
const plan: CreationPlan = {
schema: "creation-plan.v1",
技能: "本局创作方案",
brief: "持久终端体验",
presentation_mode: "zero_layer",
recipe: {
primary: "终端操作",
secondary_traits: [],
method: ["原地更新持久卡"],
references: [],
},
items: zeroModules.map((module) => ({
module_id: module.id,
tier: "必须",
why: module.name,
missing: "",
instances: [],
})),
};
expect(validateCreationPlan(JSON.stringify(plan), catalog).ok).toBe(true);
plan.items[plan.items.findIndex((item) => item.module_id === "zero-layer-status")] = {
module_id: "status-bar",
tier: "必须",
why: "错误混入普通楼层",
missing: "",
instances: [],
};
const mixed = validateCreationPlan(JSON.stringify(plan), catalog);
expect(mixed.ok).toBe(false);
expect(mixed.missingIds).toContain("zero-layer-status");
expect(mixed.unknownIds).toContain("status-bar");
});
it("rejects missing module ids and reports them", async () => {
const { catalog, plan } = await fixture();
const omitted = plan.items.at(-1)!.module_id;
plan.items.pop();
const result = validateCreationPlan(JSON.stringify(plan), catalog);
expect(result.ok).toBe(false);
expect(result.missingIds).toEqual([omitted]);
expect(result.error).toContain(omitted);
});
it("uses a short catalog before the plan and grouped state after it", async () => {
const { catalog, plan } = await fixture();
const shortCatalog = formatCreationPlanCapabilityCatalog(catalog);
expect(shortCatalog).toContain("items 必须覆盖");
expect(shortCatalog).toContain("presentation_mode=`message|zero_layer`");
expect(shortCatalog).toContain("0层回复呈现");
expect(shortCatalog).not.toContain("creation-plan(本局创作方案)");
const rules = plan.items.find((item) => item.module_id === "generation-rules");
expect(rules).toBeTruthy();
rules!.tier = "必须";
const unused = plan.items.find((item) => item.module_id === "topology");
expect(unused).toBeTruthy();
unused!.tier = "没有意义";
const progress = formatCreationPlanProgress({
plan,
catalog,
products: [
{ tag: catalog.modules.find((m) => m.id === "aesthetics-interaction")!.artifact, content: "{}" },
{ tag: "设计.生成规则#tenants", content: "{}" },
],
});
expect(progress).toContain("【本局创作方案 · 当前决策】");
expect(progress).toContain("不是本轮待办");
expect(progress).toContain("何时用 / 何时不用");
expect(progress).toContain("### 必须");
expect(progress).toContain("〔已落〕");
expect(progress).toContain("要落两份或以上同类对象");
expect(progress).toContain("字段很少、也不承载玩法");
const unusedSection = progress.split("### 没有意义")[1] ?? "";
expect(unusedSection).toContain("topology");
expect(unusedSection).not.toContain("何时用:");
const oldFullGuide = formatModuleCatalogForDictate(catalog);
const before = buildDictateSystemPrompt({
capabilityCatalog: shortCatalog,
recipeExamples: "- text-generator:文本生成案例",
});
const after = buildDictateSystemPrompt({ creationPlanState: progress });
expect(before).toContain("尚无本局创作方案");
expect(after).toContain("当前决策");
expect(after).toContain("收口前的核对清单");
expect(after).not.toContain("旧配方案例目录");
expect(after.length).toBeLessThan(
buildDictateSystemPrompt({ moduleGuide: oldFullGuide }).length,
);
});
it("blocks an incomplete plan insert before writing", async () => {
const { catalog, plan } = await fixture();
const missingId = plan.items.at(-1)!.module_id;
plan.items.pop();
const products: Array<{ tag: string; content: string }> = [];
const driver: AgentDriver = {
async run(input) {
const outcome = await input.handleStep([
{
id: "p1",
name: "insert",
arguments: JSON.stringify({
position: CREATION_PLAN_TAG,
content: JSON.stringify(plan),
}),
},
]);
expect(outcome.kind).toBe("continue");
if (outcome.kind === "continue") {
const payload = JSON.parse(outcome.results[0]!.content) as {
error?: string;
missing_ids?: string[];
};
expect(payload.error).toContain("能力覆盖校验失败");
expect(payload.missing_ids).toContain(missingId);
}
return { iterations: 1, stop: { kind: "text", content: "方案未写入。" } };
},
};
const result = await runDictateTurn({
llm: {} as never,
dialogue: [{ role: "user", text: "做一个封闭空间悬疑" }],
moduleCatalog: catalog,
driver,
handlers: {
listProducts: () => products,
writeProduct: (tag, content) => products.push({ tag, content }),
deleteProduct: () => false,
readTag: () => undefined,
writeTag: () => undefined,
deleteTag: () => false,
clearDialogue: () => undefined,
},
});
expect(result.wroteTags).toEqual([]);
expect(products).toEqual([]);
});
it("warns but does not reject an opening with required gaps", async () => {
const { catalog, plan } = await fixture();
const requiredId = plan.items[0]!.module_id;
const products = [{ tag: CREATION_PLAN_TAG, content: JSON.stringify(plan) }];
const driver: AgentDriver = {
async run(input) {
const outcome = await input.handleStep([
{
id: "o1",
name: "insert",
arguments: JSON.stringify({
position: "设计.开场白",
content: "@玩家在雨夜醒来。",
}),
},
]);
expect(outcome.kind).toBe("continue");
if (outcome.kind === "continue") {
const payload = JSON.parse(outcome.results[0]!.content) as {
ok?: boolean;
missing_required_module_ids?: string[];
};
expect(payload.ok).toBe(true);
expect(payload.missing_required_module_ids).toContain(requiredId);
}
return { iterations: 1, stop: { kind: "text", content: "开场已写入,并提示缺项。" } };
},
};
await runDictateTurn({
llm: {} as never,
dialogue: [{ role: "user", text: "先写开场" }],
moduleCatalog: catalog,
driver,
handlers: {
listProducts: () => products,
writeProduct: (tag, content) => products.push({ tag, content }),
deleteProduct: () => false,
readTag: () => undefined,
writeTag: () => undefined,
deleteTag: () => false,
clearDialogue: () => undefined,
},
});
expect(products.some((product) => product.tag === "设计.开场白")).toBe(true);
});
});
+29 -2
View File
@@ -46,6 +46,33 @@ describe("dictate play-bind", () => {
expect(refs.indexOf("设计.正文组成")).toBeLessThan(histIdx);
});
it("groups context by change frequency before applying importance order", () => {
const fragment = (skill: string, stability: "stable" | "semi") =>
JSON.stringify({
schema: "context-fragment.v1",
技能: skill,
brief: skill,
mount: ["world-simulator"],
稳变: stability,
正文: { value: skill },
开放问题: [],
});
const order = buildDictateContextOrder({
products: [
{ tag: "设计.稳定约束", content: fragment("稳定约束", "stable"), order: 10 },
{ tag: "设计.阶段备忘", content: fragment("阶段备忘", "semi"), order: -50 },
],
playSlots: defaultPlaySlots(),
});
const refs = order.slots[0]!.inserts.map((i) => i.ref);
const history = refs.indexOf(DIALOGUE_HISTORY_TAG);
expect(refs.indexOf("设计.稳定约束")).toBeLessThan(history);
expect(refs.indexOf("设计.阶段备忘")).toBeGreaterThan(history);
expect(refs.indexOf("设计.阶段备忘")).toBeLessThan(
refs.indexOf("用户.最新输入"),
);
});
it("skips opening and worker-set tags; narrator only gets style", () => {
const board = new Blackboard();
board.write({
@@ -118,7 +145,7 @@ describe("dictate play-bind", () => {
});
board.write({
tag: "设计.故事推进",
content: "用户输入用法:大纲扩写。禁止扮演停笔。",
content: "用户输入用法:大纲扩写。禁止续写与自动写入。",
source: "dictate",
metadata: { [DICTATE_ORDER_META_KEY]: -22 },
});
@@ -147,7 +174,7 @@ describe("dictate play-bind", () => {
expect(narrRefs).not.toContain("用户.需求");
expect(JSON.parse(result.workerSetJson).play_slots.narrator).toBe(true);
expect(board.getContentByTag("设计.故事推进")).toBe(
"用户输入用法:大纲扩写。禁止扮演停笔。",
"用户输入用法:大纲扩写。禁止续写与自动写入。",
);
});
+86
View File
@@ -0,0 +1,86 @@
import { describe, expect, it } from "vitest";
import {
buildDictateModuleReadIndex,
runDictateTurn,
} from "../src/dictate/index.js";
import {
formatModuleCatalogForDictate,
loadModuleCatalog,
} from "../src/skills/creation-flow.js";
import type { AgentDriver } from "../src/runtime/driver.js";
describe("dictate read_library_entry", () => {
it("lists short entries on read_module and returns selected full entries only", async () => {
const catalog = await loadModuleCatalog("dialogue/world-simulator");
const moduleReadIndex = await buildDictateModuleReadIndex({
skillPackRoot: "dialogue/world-simulator",
catalog: catalog!,
});
const preferenceModule = moduleReadIndex.get("story-progression")!;
const entryId = preferenceModule.libraries[0]!.entries[0]!.id;
const tags = new Map<string, string>();
const driver: AgentDriver = {
async run(input) {
const outcome = await input.handleStep([
{
id: "m1",
name: "read_module",
arguments: JSON.stringify({ module_id: "story-progression" }),
},
{
id: "l1",
name: "read_library_entry",
arguments: JSON.stringify({
library_id: "preferences",
entry_ids: [entryId],
}),
},
{
id: "l2",
name: "read_library_entry",
arguments: JSON.stringify({
library_id: "style-packs",
entry_ids: ["missing"],
}),
},
]);
if (outcome.kind === "continue") {
const module = JSON.parse(outcome.results[0]!.content);
const selected = JSON.parse(outcome.results[1]!.content);
const rejected = JSON.parse(outcome.results[2]!.content);
expect(module.libraries[0].entries[0].summary).toBeTruthy();
expect(selected.entries).toHaveLength(1);
expect(selected.entries[0].content).toBeTruthy();
expect(rejected.error).toContain("未绑定库");
}
return { iterations: 1, stop: { kind: "text", content: "已读取" } };
},
};
await runDictateTurn({
llm: {} as never,
dialogue: [],
moduleCatalog: catalog,
moduleReadIndex,
driver,
handlers: {
listProducts: () => [],
writeProduct: () => undefined,
deleteProduct: () => false,
readTag: (tag) => tags.get(tag),
writeTag: (tag, content) => {
tags.set(tag, content);
},
deleteTag: (tag) => tags.delete(tag),
clearDialogue: () => undefined,
},
});
});
it("does not inject full bound libraries into the default dictate catalog", async () => {
const catalog = await loadModuleCatalog("dialogue/world-simulator");
const block = formatModuleCatalogForDictate(catalog!);
expect(block).not.toContain("starter-no-user-ntr-victim");
expect(block).not.toContain("用户代入角色不做被 NTR");
});
});
+279
View File
@@ -0,0 +1,279 @@
import { describe, expect, it } from "vitest";
import {
DICTATE_MODULE_READ_STATE_TAG,
buildDictateModuleReadIndex,
runDictateTurn,
} from "../src/dictate/index.js";
import { loadModuleCatalog } from "../src/skills/creation-flow.js";
import type { AgentDriver } from "../src/runtime/driver.js";
async function setup() {
const catalog = await loadModuleCatalog("dialogue/world-simulator");
expect(catalog).toBeTruthy();
const moduleReadIndex = await buildDictateModuleReadIndex({
skillPackRoot: "dialogue/world-simulator",
catalog: catalog!,
});
return { catalog: catalog!, moduleReadIndex };
}
describe("dictate read_module", () => {
it("rejects insert before reading the current prompt version", async () => {
const { catalog, moduleReadIndex } = await setup();
const tags = new Map<string, string>();
const driver: AgentDriver = {
async run(input) {
const outcome = await input.handleStep([
{
id: "i1",
name: "insert",
arguments: JSON.stringify({
position: "设计.美学纲领与交互范式",
content: "{}",
self_score: {
dims: [
{ name: "核心体验", score: 5 },
{ name: "背景与规则", score: 5 },
],
},
}),
},
]);
if (outcome.kind === "continue") {
const payload = JSON.parse(outcome.results[0]!.content);
expect(payload.error).toContain('read_module("aesthetics-interaction")');
}
return { iterations: 1, stop: { kind: "text", content: "未写入" } };
},
};
const result = await runDictateTurn({
llm: {} as never,
dialogue: [],
moduleCatalog: catalog,
moduleReadIndex,
driver,
handlers: handlers(tags),
});
expect(result.wroteTags).toEqual([]);
});
it("allows reading multiple modules then inserting both in the same step", async () => {
const { catalog, moduleReadIndex } = await setup();
const tags = new Map<string, string>();
const driver: AgentDriver = {
async run(input) {
const outcome = await input.handleStep([
{
id: "r1",
name: "read_module",
arguments: JSON.stringify({ module_id: "美学纲领与交互范式" }),
},
{
id: "r2",
name: "read_module",
arguments: JSON.stringify({ module_id: "protagonist" }),
},
{
id: "i1",
name: "insert",
arguments: JSON.stringify({
position: "设计.美学纲领与交互范式",
content: "{}",
self_score: {
dims: [
{ name: "核心体验", score: 5 },
{ name: "背景与规则", score: 3, gap: "规则因果仍需确认" },
],
},
}),
},
{
id: "i2",
name: "insert",
arguments: JSON.stringify({
position: "设计.主角设定",
content: "{}",
self_score: {
dims: [
{ name: "宽松度", score: 5 },
{ name: "必要度", score: 5 },
],
},
}),
},
]);
if (outcome.kind === "continue") {
const read = JSON.parse(outcome.results[0]!.content);
expect(read.task).toBeTruthy();
expect(read.version).toMatch(/^[a-f0-9]{16}$/);
expect(JSON.parse(outcome.results[2]!.content).ok).toBe(true);
expect(JSON.parse(outcome.results[3]!.content).ok).toBe(true);
}
return { iterations: 1, stop: { kind: "text", content: "已写入" } };
},
};
const result = await runDictateTurn({
llm: {} as never,
dialogue: [],
moduleCatalog: catalog,
moduleReadIndex,
driver,
handlers: handlers(tags),
});
expect(result.wroteTags).toEqual([
"设计.美学纲领与交互范式",
"设计.主角设定",
]);
expect(tags.get(DICTATE_MODULE_READ_STATE_TAG)).toContain("protagonist");
});
it("returns presentation references for reply-format", async () => {
const { moduleReadIndex } = await setup();
const module = moduleReadIndex.get("reply-format");
expect(module?.presentation_references?.shells.length).toBeGreaterThan(0);
expect(module?.presentation_references?.markdown_safe_subset).toContain(
"正文安全子集",
);
expect(module?.presentation_references?.iframe_contract).toContain(
"present.onData",
);
});
it("requires another read when the prompt version changes", async () => {
const { catalog, moduleReadIndex } = await setup();
const tags = new Map<string, string>();
const driver: AgentDriver = {
async run(input) {
await input.handleStep([
{
id: "r1",
name: "read_module",
arguments: JSON.stringify({ module_id: "aesthetics-interaction" }),
},
]);
moduleReadIndex.get("aesthetics-interaction")!.version = "new-version";
const outcome = await input.handleStep([
{
id: "i1",
name: "insert",
arguments: JSON.stringify({
position: "设计.美学纲领与交互范式",
content: "{}",
}),
},
]);
if (outcome.kind === "continue") {
expect(JSON.parse(outcome.results[0]!.content).error).toContain(
"当前提示版本",
);
}
return { iterations: 2, stop: { kind: "text", content: "需重读" } };
},
};
const result = await runDictateTurn({
llm: {} as never,
dialogue: [],
moduleCatalog: catalog,
moduleReadIndex,
driver,
handlers: handlers(tags),
});
expect(result.wroteTags).toEqual([]);
});
it("validates score dimensions and stores low-score gaps as metadata", async () => {
const { catalog, moduleReadIndex } = await setup();
const tags = new Map<string, string>();
let metadata: unknown;
const driver: AgentDriver = {
async run(input) {
await input.handleStep([
{
id: "r1",
name: "read_module",
arguments: JSON.stringify({ module_id: "aesthetics-interaction" }),
},
]);
const rejected = await input.handleStep([
{
id: "i1",
name: "insert",
arguments: JSON.stringify({
position: "设计.美学纲领与交互范式",
content: "{}",
self_score: {
dims: [
{ name: "核心体验", score: 3 },
{ name: "背景与规则", score: 5 },
],
},
}),
},
]);
if (rejected.kind === "continue") {
expect(JSON.parse(rejected.results[0]!.content).error).toContain(
"必须写 gap",
);
}
await input.handleStep([
{
id: "i2",
name: "insert",
arguments: JSON.stringify({
position: "设计.美学纲领与交互范式",
content: "{}",
self_score: {
dims: [
{ name: "核心体验", score: 3, gap: "发生方式仍偏宽" },
{ name: "背景与规则", score: 5 },
],
},
}),
},
]);
return { iterations: 3, stop: { kind: "text", content: "已写入" } };
},
};
await runDictateTurn({
llm: {} as never,
dialogue: [],
moduleCatalog: catalog,
moduleReadIndex,
driver,
handlers: {
...handlers(tags),
writeProduct: (tag, content, order, meta) => {
tags.set(tag, content);
metadata = { order, ...meta };
},
},
});
expect(metadata).toMatchObject({
layer: "final",
selfScore: {
dims: expect.arrayContaining([
expect.objectContaining({ name: "核心体验", score: 3 }),
]),
},
});
});
});
function handlers(tags: Map<string, string>) {
return {
listProducts: () =>
[...tags.entries()]
.filter(([tag]) => tag.startsWith("设计.") || tag.startsWith("用户."))
.map(([tag, content]) => ({ tag, content })),
writeProduct: (tag: string, content: string) => tags.set(tag, content),
deleteProduct: (tag: string) => tags.delete(tag),
readTag: (tag: string) => tags.get(tag),
writeTag: (tag: string, content: string) => {
tags.set(tag, content);
},
deleteTag: (tag: string) => tags.delete(tag),
clearDialogue: () => undefined,
};
}
+155 -32
View File
@@ -73,7 +73,7 @@ describe("dictate context", () => {
expect(isDictateModeValue("recipe")).toBe(false);
});
it("system prompt requires insert toolcall and relative order", () => {
it("system prompt starts from a plan and keeps generic write tools", () => {
const prompt = buildDictateSystemPrompt({
recipeName: "数据化跑团体验",
recipeBrief:
@@ -81,9 +81,8 @@ describe("dictate context", () => {
moduleGuide:
"【能力 · 何时落盘】\n- 美学纲领与交互范式:…\n 落盘:insert 「设计.美学纲领与交互范式」\n 何时用:还不能回答站位",
});
expect(prompt).toContain("设计.正文组成");
expect(prompt).toContain("设计.开场白");
expect(prompt).toContain("输出.开场白");
expect(prompt).toContain("creation-plan.v1");
expect(prompt).toContain("设计.本局创作方案");
expect(prompt).toContain("insert");
expect(prompt).toContain("delete");
expect(prompt).toContain("declare_variable");
@@ -93,29 +92,15 @@ describe("dictate context", () => {
expect(prompt).toContain("同 position 再 insert");
expect(prompt).toContain("数据化跑团体验");
expect(prompt).toMatch(/order/);
expect(prompt).toContain("仅允许 toolcall");
expect(prompt).toContain("设计.模仿范例");
expect(prompt).toContain("设计.模仿要点");
expect(prompt).toContain("设计.故事推进");
expect(prompt).toContain("设计.叙事指南");
expect(prompt).toContain("大纲扩写");
expect(prompt).toContain("扮演停笔关掉");
expect(prompt).toContain("只挂载这些产物");
expect(prompt).toContain("【能力 · 何时落盘】");
expect(prompt).toContain("设计.美学纲领与交互范式");
expect(prompt).toContain("同轮可多次 insert");
expect(prompt).toContain("不抢跑未齐条件的下游");
expect(prompt).toContain("也不自动开跑 D");
expect(prompt).toContain("快穿短局");
expect(prompt).toContain("一核短局");
expect(prompt).toMatch(/舞台骨架/);
expect(prompt).toMatch(/覆盖 C/);
expect(prompt).toContain("设计.主角设定");
expect(prompt).toContain("reply_module");
expect(prompt).toContain("对用户可见回复");
expect(prompt).toContain("有 N 个 reply_module");
expect(prompt).toContain("〔可反复〕须拆分");
expect(prompt).toContain("设计.具体实例#");
expect(prompt).toContain("通常 2~3 个");
expect(prompt).toContain("给 2~3 个选项来问");
expect(prompt).toContain("不是 DAG");
expect(prompt).toContain("不是用户已选路径");
expect(prompt).toContain("自然回应");
expect(prompt).not.toContain("体验锚定(程序打分)");
expect(prompt).not.toContain("## 本轮落什么");
});
it("formats module catalog for dictate with land tags and when", async () => {
@@ -209,13 +194,12 @@ describe("dictate context", () => {
});
const brief = index.get("设计.美学纲领与交互范式");
expect(brief).toBeTruthy();
expect(brief!).toContain("# 模块 · 美学纲领与交互范式");
expect(brief!).toContain("回答时请附带本模块");
expect(brief!).toContain("# 本轮关注 · 美学纲领与交互范式");
expect(brief!).toContain("不要求固定标题");
expect(brief!).toContain("本模块内容包括");
expect(brief!).toContain("## 写入用户回复时做什么");
expect(brief!).toContain("## 探测与追问(本模块口径)");
expect(brief!).toContain("展示优于提问");
expect(brief!).toMatch(/自评关注|交互范式|美学纲领/);
expect(brief!).toContain("同轮多个产物时合并回应");
expect(brief!).toMatch(/自评关注|核心体验|背景与规则/);
const alias = index.get("设计.开场白");
expect(alias).toContain("开场白与开场变量");
@@ -357,6 +341,145 @@ describe("dictate context", () => {
expect(products.map((p) => p.tag)).toEqual(["设计.具体实例#lei-ying"]);
});
it("splits a concrete-instance records array into one product per record", async () => {
const products: { tag: string; content: string }[] = [];
const body = JSON.stringify({
schema: "context-fragment.v1",
技能: "具体实例",
brief: "十位目标",
正文: {
rule_id: "目标女配",
batch_id: "target-npcs-roster",
本批数量: 3,
批次条件: ["待清算的恶女名录"],
records: [
{ 姓名: "苏晚", 身份: "同学" },
{ 姓名: "林乔", 身份: "上司" },
{ 姓名: "苏晚", 身份: "同名另一人" },
],
},
});
const driver: AgentDriver = {
async run(input) {
const outcome = await input.handleStep([
{
id: "c1",
name: "insert",
arguments: JSON.stringify({
position: "设计.具体实例#target-npcs-roster",
content: body,
}),
},
]);
expect(outcome.kind).toBe("continue");
if (outcome.kind === "continue") {
const payload = JSON.parse(outcome.results[0]!.content) as {
ok?: boolean;
split?: number;
positions?: string[];
};
expect(payload.ok).toBe(true);
expect(payload.split).toBe(3);
expect(payload.positions).toEqual([
"设计.具体实例#苏晚",
"设计.具体实例#林乔",
"设计.具体实例#苏晚-2",
]);
}
return { iterations: 1, stop: { kind: "text", content: "已拆开" } };
},
};
const result = await runDictateTurn({
llm: {} as never,
dialogue: [{ role: "user", text: "把这三位分开落盘" }],
driver,
repeatableFamilies: ["设计.具体实例"],
handlers: {
listProducts: () => products,
writeProduct: (tag, content) => {
products.push({ tag, content });
},
deleteProduct: () => false,
readTag: () => undefined,
writeTag: () => undefined,
deleteTag: () => false,
clearDialogue: () => undefined,
},
});
expect(result.wroteTags).toEqual([
"设计.具体实例#苏晚",
"设计.具体实例#林乔",
"设计.具体实例#苏晚-2",
]);
const first = JSON.parse(products[0]!.content) as {
brief: string;
正文: { 规则?: string; 名称?: string; 记录?: { 姓名?: string }; batch_id?: string };
};
expect(first.brief).toBe("苏晚");
expect(first.正文.规则).toBe("目标女配");
expect(first.正文.名称).toBe("苏晚");
expect(first.正文.记录?.姓名).toBe("苏晚");
expect(first.正文.batch_id).toBeUndefined();
});
it("prepares runtime tools before persisting an opening", async () => {
const events: string[] = [];
const driver: AgentDriver = {
async run(input) {
const prepared = await input.handleStep([
{
id: "prepare",
name: "prepare_opening",
arguments: "{}",
},
]);
expect(prepared.kind).toBe("continue");
const outcome = await input.handleStep([
{
id: "opening",
name: "insert",
arguments: JSON.stringify({
position: "设计.开场白",
content: "@玩家推开门。",
}),
},
]);
expect(outcome.kind).toBe("continue");
if (outcome.kind === "continue") {
const payload = JSON.parse(outcome.results[0]!.content) as {
runtime_prepared_before_opening?: boolean;
preparation_mode?: string;
};
expect(payload.runtime_prepared_before_opening).toBe(true);
expect(payload.preparation_mode).toBe("prepare_opening");
}
return {
text: "开场已写入",
iterations: 1,
stop: { kind: "text", content: "开场已写入" },
};
},
};
await runDictateTurn({
llm: {} as never,
dialogue: [{ role: "user", text: "生成开场" }],
driver,
handlers: {
listProducts: () => [],
prepareOpening: () => events.push("prepare"),
writeProduct: () => events.push("write"),
deleteProduct: () => false,
readTag: () => undefined,
writeTag: () => undefined,
deleteTag: () => false,
clearDialogue: () => undefined,
},
});
expect(events).toEqual(["prepare", "write"]);
});
it("insert tool result includes reply_module from lookup", async () => {
let capturedSpec: string | undefined;
const driver: AgentDriver = {
@@ -378,7 +501,7 @@ describe("dictate context", () => {
reply_module?: string;
};
expect(payload.ok).toBe(true);
expect(payload.reply_module).toContain("回答时请附带本模块");
expect(payload.reply_module).toContain("自然回复");
capturedSpec = payload.reply_module;
}
return {
@@ -601,7 +724,7 @@ describe("dictate context", () => {
};
expect(a.reply_module).toContain("美学纲领");
expect(b.reply_module).toContain("主角设定");
expect(a.reply_hint).toMatch(/多个 reply_module|本步已带回模块/);
expect(a.reply_hint).toMatch(/多个产物|本步已带回模块/);
expect(b.reply_hint).toContain("本步已带回模块");
}
return {
+203
View File
@@ -0,0 +1,203 @@
import { describe, expect, it } from "vitest";
import type { AgentDriver } from "../src/runtime/driver.js";
import { buildDictateMessages, runDictateTurn } from "../src/dictate/index.js";
import { scoreExperienceAnchor } from "../src/dictate/experience-anchor.js";
const anchored = {
体验锚定: {
站位: "完全代入",
核心感觉: "垄断生存资源的掌控感",
轮转停点: "用户要做决定时停笔等用户",
},
};
describe("experience anchor score", () => {
it("scores the three fields and ignores self-score", () => {
const onlySelf = scoreExperienceAnchor(
JSON.stringify({
自评: { 维度: [{ 名: "美学纲领", 分数: 10 }] },
正文: { 设定逻辑: { 完备度判断: { 核心状态: "核心明确", 判断依据: "我觉得够了" } } },
}),
);
expect(onlySelf.anchored).toBe(false);
expect(onlySelf.passed).toBe(0);
const full = scoreExperienceAnchor(JSON.stringify(anchored));
expect(full.anchored).toBe(true);
expect(full.passed).toBe(3);
expect(full.items.map((item) => item.id)).toEqual(["站位", "核心感觉", "轮转停点"]);
});
it("rejects placeholders and unanswered stance", () => {
const score = scoreExperienceAnchor(
JSON.stringify({
体验锚定: {
站位: "代入还是旁观",
核心感觉: "未定",
轮转停点: "等用户",
},
}),
);
expect(score.anchored).toBe(false);
expect(score.items.every((item) => !item.ok)).toBe(true);
});
it("falls back to aesthetics body fields and prose labels", () => {
const fromBody = scoreExperienceAnchor(
JSON.stringify({
正文: {
设定逻辑: {
参与方式: { 用户与user关系: { 结论: "完全代入" } },
内容维度: { 核心感觉: { 已知: "求生时的紧张感" } },
},
交互范式: {
描写权限: { 思考与抉择: "要替用户做决定时必须停笔等用户" },
},
},
}),
);
expect(fromBody.anchored).toBe(true);
const prose = scoreExperienceAnchor(
["站位:旁观", "核心感觉:看棋子互相算计", "轮转停点:场面收束前停笔等用户下令"].join("\n"),
);
expect(prose.anchored).toBe(true);
expect(prose.items.find((item) => item.id === "站位")?.value).toBe("旁观");
});
it("does not inject a fixed anchor gate into dictate context", () => {
const body =
buildDictateMessages({
systemPrompt: "SYS",
products: [
{
tag: "设计.美学纲领与交互范式",
content: JSON.stringify({
体验锚定: { 站位: "完全代入", 核心感觉: "未定", 轮转停点: "未定" },
}),
},
],
dialogue: [],
})[1]?.content ?? "";
expect(body).toContain("当前产物");
expect(body).toContain("设计.美学纲领与交互范式");
expect(body).not.toContain("体验锚定(程序打分");
expect(body).not.toContain("不得 insert 开场白");
});
it("does not block opening on a fixed 3/3 anchor count", async () => {
const products = new Map<string, string>();
const driver: AgentDriver = {
async run(input) {
const first = await input.handleStep([
{
id: "o1",
name: "insert",
arguments: JSON.stringify({ position: "设计.开场白", content: "@玩家睁眼。" }),
},
]);
expect(first.kind).toBe("continue");
if (first.kind === "continue") {
const payload = JSON.parse(first.results[0]!.content) as {
ok?: boolean;
error?: string;
};
expect(payload.ok).toBe(true);
expect(payload.error).toBeUndefined();
}
const sameStep = await input.handleStep([
{
id: "a1",
name: "insert",
arguments: JSON.stringify({
position: "设计.美学纲领与交互范式",
content: JSON.stringify(anchored),
}),
},
{
id: "o2",
name: "insert",
arguments: JSON.stringify({ position: "设计.开场白", content: "@玩家睁眼。" }),
},
]);
expect(sameStep.kind).toBe("continue");
if (sameStep.kind === "continue") {
const aesthetics = JSON.parse(sameStep.results[0]!.content) as {
ok?: boolean;
experience_anchor?: unknown;
};
const opening = JSON.parse(sameStep.results[1]!.content) as { ok?: boolean };
expect(aesthetics.ok).toBe(true);
expect(aesthetics.experience_anchor).toBeUndefined();
expect(opening.ok).toBe(true);
}
return { iterations: 1, stop: { kind: "text", content: "已补三件并写开场。" } };
},
};
const result = await runDictateTurn({
llm: {} as never,
recipeName: "快穿短局",
dialogue: [{ role: "user", text: "先开场" }],
driver,
handlers: {
listProducts: () => [...products.entries()].map(([tag, content]) => ({ tag, content })),
writeProduct: (tag, content) => {
products.set(tag, content);
},
deleteProduct: (tag) => products.delete(tag),
readTag: () => undefined,
writeTag: () => undefined,
deleteTag: () => false,
clearDialogue: () => undefined,
},
});
expect(products.has("设计.开场白")).toBe(true);
expect(result.wroteTags).toContain("设计.美学纲领与交互范式");
expect(result.wroteTags).toContain("设计.开场白");
});
it("does not block opening for recipes that skip the aesthetics gate", async () => {
const products = new Map<string, string>();
const driver: AgentDriver = {
async run(input) {
const outcome = await input.handleStep([
{
id: "o1",
name: "insert",
arguments: JSON.stringify({ position: "设计.开场白", content: "@玩家睁眼。" }),
},
]);
expect(outcome.kind).toBe("continue");
if (outcome.kind === "continue") {
const payload = JSON.parse(outcome.results[0]!.content) as { ok?: boolean; error?: string };
expect(payload.ok).toBe(true);
expect(payload.error).toBeUndefined();
}
return { iterations: 1, stop: { kind: "text", content: "开场已写。" } };
},
};
await runDictateTurn({
llm: {} as never,
recipeName: "文本生成器",
dialogue: [{ role: "user", text: "来一段开篇" }],
driver,
handlers: {
listProducts: () => [...products.entries()].map(([tag, content]) => ({ tag, content })),
writeProduct: (tag, content) => {
products.set(tag, content);
},
deleteProduct: () => false,
readTag: () => undefined,
writeTag: () => undefined,
deleteTag: () => false,
clearDialogue: () => undefined,
},
});
expect(products.get("设计.开场白")).toContain("@玩家");
});
});
+17
View File
@@ -5,6 +5,23 @@ import {
} from "../web/json-doc.js";
describe("tryParseJsonDoc", () => {
it("joins a fragment whose root object closed before later keys", () => {
const raw = `{
"schema":"context-fragment-v1",
"技能":"主角设定",
"brief":"点题",
"brief_ok":true,
"mount":"world-simulator"
},
"稳变":"stable",
"正文": { "称呼": "李宗" }
}`;
const parsed = tryParseJsonDoc(raw);
expect(parsed?.schema).toBe("context-fragment-v1");
expect(parsed?.稳变).toBe("stable");
expect(parsed?.正文?.称呼).toBe("李宗");
});
it("keeps interior Chinese quotes in an otherwise valid fragment", () => {
const raw = JSON.stringify({
schema: "context-fragment.v1",
+22
View File
@@ -98,4 +98,26 @@ describe("maintain need_generate sampling", () => {
});
expect(buildChanceBatchFromGenerationRule(rules, "nickname")).toBeNull();
});
it("reads flat single-rule body and string pool entries", () => {
const rules = JSON.stringify({
正文: {
rule_id: "wasteland-npc",
池: [
{
名称: "公开职业池",
条目: ["厨师", "医生", "掮客"],
},
],
},
});
const batch = buildChanceBatchFromGenerationRule(rules, "wasteland-npc");
expect(batch).toHaveLength(1);
expect(batch![0]!.id).toBe("pool:公开职业池");
expect(batch![0]!.items?.map((item) => item.id)).toEqual([
"厨师",
"医生",
"掮客",
]);
});
});
+33
View File
@@ -53,6 +53,39 @@ describe("opening-seal", () => {
).toBe(false);
});
it("reads the default candidate body without a copied 开场白全文", () => {
const raw = JSON.stringify({
schema: "context-fragment.v1",
技能: "开场白与开场变量",
正文: {
默认候选id: "gate",
开场白候选: [
{
id: "dorm",
正文: "@玩家推开宿舍门。",
meta: { 开场变量: [{ 名: "口粮", 值: 1 }] },
},
{
id: "gate",
正文: "@玩家停在闸机前。",
meta: { 开场变量: [{ 名: "债额", 值: 3, 依据: "闸机票" }] },
},
],
},
});
const first = parseOpeningSealPayload(raw, 0);
expect(first?.candidates).toEqual([
"@玩家停在闸机前。",
"@玩家推开宿舍门。",
]);
expect(first?.selectedText).toBe("@玩家停在闸机前。");
expect(first?.variables).toEqual([
{ name: "债额", value: 3, note: "闸机票" },
]);
const second = parseOpeningSealPayload(raw, 1);
expect(second?.selectedText).toBe("@玩家推开宿舍门。");
});
it("parses 1+ opening candidates and selects by index", () => {
const first = parseOpeningSealPayload(fragment, 0);
expect(first?.candidates).toHaveLength(2);
+21
View File
@@ -1,10 +1,31 @@
import { describe, expect, it } from "vitest";
import {
canonicalizeInsertedContent,
extractLastProductJson,
selectJsonPayload,
tryParseJsonDoc,
} from "../src/parse/json-doc.js";
describe("canonicalizeInsertedContent", () => {
it("extracts a broken fragment string into canonical JSON", () => {
const raw = `{
"schema":"context-fragment-v1",
"技能":"主角设定",
"brief":"点题",
"mount":"world-simulator"
},
"正文": { "称呼": "@玩家" }
}`;
const stored = canonicalizeInsertedContent(raw);
expect(JSON.parse(stored)).toMatchObject({
schema: "context-fragment.v1",
技能: "主角设定",
正文: { 称呼: "@玩家" },
});
expect(canonicalizeInsertedContent("开场是一段散文。")).toBe("开场是一段散文。");
});
});
describe("selectJsonPayload", () => {
it("keeps content when it is already the product", () => {
const json = '{"schema":"context-fragment.v1","brief":"a","正文":{}}';
+1 -1
View File
@@ -236,7 +236,7 @@ describe("phase runtime", () => {
if (session.waitingReason?.kind === "worker_questions") {
expect(session.waitingReason.questions[0]?.id).toBe("module-opening");
expect(session.waitingReason.questions[0]?.prompt).toContain(
"玩家实际会待着玩的地方",
"会反复影响你的外部舞台",
);
}
expect(llmCalls.calls()).toBe(0);
+17
View File
@@ -108,4 +108,21 @@ describe("play-aux view", () => {
expect(dorm?.persona?.name).toBe("李宗");
expect(gate?.persona?.name).toBe("阿泽");
});
it("lists proliferated zero-layer openings for play swipe", () => {
const board = new Blackboard();
board.write({
tag: "设计.0层开场白与初态#dorm",
content: "@玩家推开宿舍门。",
source: "agent",
});
board.write({
tag: "设计.0层开场白与初态#gate",
content: "@玩家停在闸机前。",
source: "agent",
});
const view = buildPlayAuxView(board, { [SLOT_OPENING_SELECTED_INDEX]: 1 });
expect(view.openings.map((o) => o.slot).sort()).toEqual(["dorm", "gate"]);
expect(view.selectedOpeningIndex).toBe(1);
});
});
+40
View File
@@ -0,0 +1,40 @@
import { describe, expect, it } from "vitest";
import {
formatShellRefsForPrompt,
loadShellRefCatalog,
} from "../src/skills/shell-ref-catalog.js";
const PACK = "dialogue/world-simulator";
describe("shell ref catalog", () => {
it("loads the five reference shells with layout and sample", async () => {
const shells = await loadShellRefCatalog(PACK);
expect(shells.map((shell) => shell.id)).toEqual([
"reading",
"dual-text",
"data-cards",
"alongside",
"paged",
"piece",
]);
for (const shell of shells) {
expect(shell.intro).toBeTruthy();
expect(shell.note).toContain("不替换");
expect(shell.note).toContain("可替换");
expect(shell.layout).toContain(`id: ${shell.id}`);
expect(shell.sample).toContain("present.onData");
}
});
it("tells creation to drop unused regions", async () => {
const shells = await loadShellRefCatalog(PACK);
const block = formatShellRefsForPrompt(shells);
expect(block).toContain("【壳参考库】");
expect(block).toContain("不留空位");
expect(block).toContain("可按本局用途替换");
expect(block).not.toContain("斗鱼弹幕");
expect(block).not.toContain("规则怪谈");
expect(block).toContain("`dual-text`");
expect(block).not.toContain("chat_monitor");
});
});