diff --git a/skills/dialogue/world-simulator/recipes/catalog.yaml b/skills/dialogue/world-simulator/recipes/catalog.yaml index 5fd6ed7..c6b4481 100644 --- a/skills/dialogue/world-simulator/recipes/catalog.yaml +++ b/skills/dialogue/world-simulator/recipes/catalog.yaml @@ -1,8 +1,7 @@ -# 导演选项目录(用户在新建作品时手动选择;对用户称「导演」/「配方」) +# 节点流程 · 配方目录(与转述进料配方不共享) # id = recipes/{id}/ 文件夹 # name = 固定中文名(下拉展示) # declaration = 给人看的短说明 -# 内部仍叫 recipe;勿对用户再说「配方」作第二层选项 # recipe.yaml 写方法论:when / core / process / principles + 近期 steps # 步骤 name 必须 ∈ modules/catalog.yaml(【能力】) # 选定后写入黑板 tag 创作.选用配方 diff --git a/skills/dialogue/world-simulator/recipes/转述/catalog.yaml b/skills/dialogue/world-simulator/recipes/转述/catalog.yaml new file mode 100644 index 0000000..ed59348 --- /dev/null +++ b/skills/dialogue/world-simulator/recipes/转述/catalog.yaml @@ -0,0 +1,11 @@ +# 转述进料 · 配方目录(与节点流程配方不共享) +# id = recipes/转述/{id}/ 文件夹;对用户展示用 name(中文) +# 选定后写入黑板 tag 创作.选用配方;转述模式不种节点 DAG + +recipes: + - id: 文本生成器 + name: 文本生成器 + declaration: 钉需求与文风;无世界推演。正文格式与开场白另步。 + - id: 正文组成 + name: 正文组成 + declaration: 钉呈现格式(壳与分区);开场白按该格式生成 diff --git a/skills/dialogue/world-simulator/recipes/转述/文本生成器/recipe.yaml b/skills/dialogue/world-simulator/recipes/转述/文本生成器/recipe.yaml new file mode 100644 index 0000000..3b91184 --- /dev/null +++ b/skills/dialogue/world-simulator/recipes/转述/文本生成器/recipe.yaml @@ -0,0 +1,25 @@ +# 文本生成器 · 转述进料配方 +# 钉体验与设定;不写开场叙事本身。正文组成(格式)与开场白(按格式生成)见另配方或同提示分阶段。 + +when: >- + 按用户需求钉清要写什么、给谁看、气质与禁忌; + 不要世界推演、不要跨轮状态机、不要把聊天确认段当成开场白 + +core: >- + 产物落需求与文风类上下文;默认无变量、无旁观、无主世界层。 + 「正文」指展示格式(设计.正文组成),不是开场白;开场白按格式另生成。 + +process: + - 听用户说清要写什么与交付形态;写入需求/文风等设定产物。 + - 缺关键信息就问;不要问卷式盘问。 + - 不要去搭世界模拟、变量、舞台骨架。 + - 不要把整章叙事只留在聊天确认段里。 + +principles: + - 正推文本交付规格,勿套世界模拟全套。 + - 保留用户原意,少二次创作。 + - 格式与开场白分清:正文组成≠开场白。 + +brief: 文本生成:钉需求与文风(正文格式 / 开场白另步) + +steps: [] diff --git a/skills/dialogue/world-simulator/recipes/转述/正文组成/recipe.yaml b/skills/dialogue/world-simulator/recipes/转述/正文组成/recipe.yaml new file mode 100644 index 0000000..4f7d0d8 --- /dev/null +++ b/skills/dialogue/world-simulator/recipes/转述/正文组成/recipe.yaml @@ -0,0 +1,25 @@ +# 正文组成 · 转述进料配方 +# 只钉「怎么给用户看」的格式;开场白按此格式另生成。 + +when: >- + 已大致知道要什么体验,需要定版式:选壳、分区、各区职责; + 或用户在谈呈现/版式/要不要选项坞/监控栏 + +core: >- + 写入设计.正文组成(格式契约)。 + 这不是开场白、不是故事正文;开场白必须按本格式的可见块生成。 + +process: + - 对照设定产物,选 shell_id(文本沉浸默认 prose)。 + - 写可见块挂到壳区域(至少 body)。 + - 需要时写微调(tone_chrome、block_labels、是否显示建议行动)。 + - 用户要第一幕时:再按本格式生成设计.开场白(内容),勿把格式 JSON 当开场白展示。 + +principles: + - 格式权威在设计.正文组成;开场白服从格式。 + - 不重议文风遣词(归叙事/需求);本步只交展示契约。 + - 产物自洽:只读设计.正文组成就能知道开场/每轮终稿怎么灌数。 + +brief: 正文组成:钉呈现壳与可见块(格式)→ 开场白按格式生成 + +steps: [] diff --git a/src/server/display-labels.ts b/src/server/display-labels.ts index 7ef0dc2..d096753 100644 --- a/src/server/display-labels.ts +++ b/src/server/display-labels.ts @@ -52,6 +52,8 @@ const PHASE_UNIT_LABELS: Record = { const SKILL_PACK_LABELS: Record = { "world-simulator": "世界模拟器", "expand-assistant": "扩写助手", + 文本生成器: "文本生成器", + "text-generator": "文本生成器", }; /** 生命周期 / 相位 */ diff --git a/src/skills/creation-flow.ts b/src/skills/creation-flow.ts index 827a3e2..6b26603 100644 --- a/src/skills/creation-flow.ts +++ b/src/skills/creation-flow.ts @@ -49,9 +49,15 @@ export function isRuntimePinnedTag(tag: string): boolean { } export const MODULE_CATALOG_FILENAME = "modules/catalog.yaml"; export const RECIPE_CATALOG_FILENAME = "recipes/catalog.yaml"; +/** 转述进料专用配方目录(与节点流程 catalog 不共享) */ +export const DICTATE_RECIPE_CATALOG_FILENAME = "recipes/转述/catalog.yaml"; +export const DICTATE_RECIPE_DIR = "recipes/转述"; export const DESIGN_STEP_WORKER_ID = "design-step"; export const DESIGN_FLOW_WORKER_ID = "design-flow"; +/** 配方所属进料族:节点流程 vs 转述 */ +export type RecipeIntakeFamily = "recipe" | "dictate"; + const DEFAULT_SKILLS_ROOT = path.resolve( path.dirname(fileURLToPath(import.meta.url)), "../../skills", @@ -225,6 +231,8 @@ export type RecipeCatalogEntry = { id: string; name: string; declaration: string; + /** 条目来自哪本目录;缺省视为节点流程 */ + family?: RecipeIntakeFamily; }; export type RecipeCatalog = { @@ -1329,7 +1337,10 @@ function indentMultiline(text: string, indent: string): string { return [lines[0], ...lines.slice(1).map((l) => `${indent}${l}`)].join("\n"); } -export function parseRecipeCatalog(raw: string): RecipeCatalog | null { +export function parseRecipeCatalog( + raw: string, + family: RecipeIntakeFamily = "recipe", +): RecipeCatalog | null { let doc: unknown; try { doc = parseYaml(raw); @@ -1354,7 +1365,7 @@ export function parseRecipeCatalog(raw: string): RecipeCatalog | null { ? slugFromName(name) : ""; if (!name || !declaration || !id) continue; - recipes.push({ id, name, declaration }); + recipes.push({ id, name, declaration, family }); } if (recipes.length === 0) return null; return { recipes }; @@ -1363,11 +1374,16 @@ export function parseRecipeCatalog(raw: string): RecipeCatalog | null { export async function loadRecipeCatalog( skillPackRoot: string, skillsRoot = DEFAULT_SKILLS_ROOT, + family: RecipeIntakeFamily = "recipe", ): Promise { - const fullPath = path.join(skillsRoot, skillPackRoot, RECIPE_CATALOG_FILENAME); + const filename = + family === "dictate" + ? DICTATE_RECIPE_CATALOG_FILENAME + : RECIPE_CATALOG_FILENAME; + const fullPath = path.join(skillsRoot, skillPackRoot, filename); try { const raw = await readFile(fullPath, "utf8"); - return parseRecipeCatalog(raw); + return parseRecipeCatalog(raw, family); } catch { return null; } @@ -1456,13 +1472,17 @@ export async function loadRecipeDetail( entry: RecipeCatalogEntry, skillsRoot = DEFAULT_SKILLS_ROOT, ): Promise { - const fullPath = path.join( - skillsRoot, - skillPackRoot, - "recipes", - entry.id, - "recipe.yaml", - ); + const family = entry.family ?? "recipe"; + const fullPath = + family === "dictate" + ? path.join( + skillsRoot, + skillPackRoot, + DICTATE_RECIPE_DIR, + entry.id, + "recipe.yaml", + ) + : path.join(skillsRoot, skillPackRoot, "recipes", entry.id, "recipe.yaml"); try { const raw = await readFile(fullPath, "utf8"); return parseRecipeYaml(raw, entry); @@ -1801,10 +1821,18 @@ export async function resolveSelectedRecipeDetail(params: { const ref = parseSelectedRecipeRef(params.selectedRecipeRef); if (!ref) return null; const skillsRoot = params.skillsRoot ?? DEFAULT_SKILLS_ROOT; - const catalog = await loadRecipeCatalog(params.skillPackRoot, skillsRoot); - const entry = findRecipeCatalogEntry(catalog, ref); - if (!entry) return null; - return loadRecipeDetail(params.skillPackRoot, entry, skillsRoot); + for (const family of ["recipe", "dictate"] as const) { + const catalog = await loadRecipeCatalog( + params.skillPackRoot, + skillsRoot, + family, + ); + const entry = findRecipeCatalogEntry(catalog, ref); + if (entry) { + return loadRecipeDetail(params.skillPackRoot, entry, skillsRoot); + } + } + return null; } export function validateCreationFlow( diff --git a/tests/creation-flow.test.ts b/tests/creation-flow.test.ts index 54964b9..2d3ab1b 100644 --- a/tests/creation-flow.test.ts +++ b/tests/creation-flow.test.ts @@ -18,6 +18,7 @@ import { loadModuleCatalog, loadModulePrompt, loadRecipeCatalog, + loadRecipeDetail, mergeCreationFlowPreservingAccepted, needsFlowExpansion, nextPendingStep, @@ -484,6 +485,7 @@ describe("creation-flow", () => { const recipes = await loadRecipeCatalog("dialogue/world-simulator"); expect(recipes?.recipes.some((r) => r.name === "世界模拟器")).toBe(true); expect(recipes?.recipes.some((r) => r.name === "扩写助手")).toBe(true); + expect(recipes?.recipes.some((r) => r.name === "文本生成器")).toBe(false); const block = formatRecipeCatalogForAgent(recipes!); expect(block).toContain("须由用户手动选择"); expect(block).toContain("世界模拟器"); @@ -506,6 +508,22 @@ describe("creation-flow", () => { expect(formatted).not.toContain("调味提示"); }); + it("loads dictate recipe catalog separately with Chinese ids", async () => { + const dictate = await loadRecipeCatalog( + "dialogue/world-simulator", + undefined, + "dictate", + ); + expect(dictate?.recipes.some((r) => r.id === "文本生成器")).toBe(true); + expect(dictate?.recipes.some((r) => r.name === "文本生成器")).toBe(true); + expect(dictate?.recipes.some((r) => r.name === "世界模拟器")).toBe(false); + const entry = dictate!.recipes.find((r) => r.id === "文本生成器")!; + expect(entry.family).toBe("dictate"); + const detail = await loadRecipeDetail("dialogue/world-simulator", entry); + expect(detail.core).toBeTruthy(); + expect(detail.seed).toBeNull(); + }); + it("parses selected recipe ref", () => { expect(parseSelectedRecipeRef("world-simulator")).toBe("world-simulator"); expect(parseSelectedRecipeRef('{"id":"expand-assistant","name":"扩写助手"}')).toBe( diff --git a/web/display-labels.js b/web/display-labels.js index 285fc71..3328497 100644 --- a/web/display-labels.js +++ b/web/display-labels.js @@ -57,6 +57,8 @@ export function displaySkillPackLabel(id) { const map = { "world-simulator": "世界模拟器", "expand-assistant": "扩写助手", + 文本生成器: "文本生成器", + "text-generator": "文本生成器", }; return map[id] ?? id; }