diff --git a/skills/dialogue/world-simulator/recipes/转述/catalog.yaml b/skills/dialogue/world-simulator/recipes/转述/catalog.yaml index ed59348..d7c6656 100644 --- a/skills/dialogue/world-simulator/recipes/转述/catalog.yaml +++ b/skills/dialogue/world-simulator/recipes/转述/catalog.yaml @@ -3,9 +3,15 @@ # 选定后写入黑板 tag 创作.选用配方;转述模式不种节点 DAG recipes: + - id: 数据化跑团体验 + name: 数据化跑团体验 + declaration: 回合互动跑团;跨轮数值真值 + 分档映射投影(带数值) - id: 文本生成器 name: 文本生成器 - declaration: 钉需求与文风;无世界推演。正文格式与开场白另步。 + declaration: 创作收范例与模仿面;开玩后主世界出正文、转述出文风/壳 + - 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..3bef329 --- /dev/null +++ b/skills/dialogue/world-simulator/recipes/转述/交互式长文生成器/recipe.yaml @@ -0,0 +1,25 @@ +# 交互式长文生成器 · Boss 直聘进料配方 +# 原「文本生成器」长文路径;多轮互动钉规格,再生成连贯长文。 + +when: >- + 用户要写较长、可分段推进的正文(章段、连载向、润色扩写); + 需要多轮确认需求/文风/结构;不要世界推演、不要跨轮数值状态机 + +core: >- + 把用户原话收成可生成长文的规格:需求、文风、结构与禁忌用 insert。 + 默认无变量、无旁观、无主世界层。正文格式(设计.正文组成)与开场白按需另钉。 + +process: + - 听清写什么、给谁看、气质与禁忌 → insert 用户.需求 / 设计.文风 等(order 偏小)。缺关键信息就问一句。 + - 需要版式 → insert 设计.正文组成;要开篇样例 → 按格式 insert 设计.开场白。 + - 不要搭跑团数值、舞台骨架、declare_variable。 + - 长章叙事落产物,不要只留在聊天确认段。 + +principles: + - 正推长文交付规格,勿套数据化跑团。 + - 保留用户原意;规格只走 toolcall。 + - 格式与开场白分清:正文组成 ≠ 开场白。 + +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..018c243 --- /dev/null +++ b/skills/dialogue/world-simulator/recipes/转述/数据化跑团体验/recipe.yaml @@ -0,0 +1,28 @@ +# 数据化跑团体验 · Boss 直聘进料配方 +# 注入转述总助手「本局配方」;不种节点 DAG。 + +when: >- + 用户要回合制角色扮演 / 沉浸推演,且体验靠跨轮数值或阶段推进 + (好感、资源、章节、地点、任务阶段等);按值换态度、大纲或场景文案 + +core: >- + 把用户原话收成可开玩规格:设定类 insert;跨轮状态 declare_variable; + 阈值→文案 declare_map。作用对象是本局黑板;开玩后由主世界层读投影与真值写正文。 + 默认只开主世界层,原文即终稿。 + +process: + - 先钉站位、每轮看什么、体验核与禁忌 → insert 用户.需求 / 设计.美学 / 设计.世界设定 等(order 偏小)。缺一句关键承诺就问一句;勿问卷。 + - 必须跨轮记住的状态 → declare_variable(key、type、initial、user_visible)。只立承重字段;性格/态度长文不立成可写变量。 + - 有「到某值换一套文案」→ declare_map(field 对准真值;bands 写区间或精确值 + content;target_tag 用 上下文.* 或 大纲.*)。缺分档细节就问档位边界与每档要点,再落盘。 + - 需要版式 → insert 设计.正文组成;要第一幕 → 按该格式 insert 设计.开场白。用户本轮只谈数值则只做 D,不硬塞开场白。 + - 材料只够本配方就只做本配方;聊完一轮用自然语言短确认 + 建议下一刀补什么。 + +principles: + - 承重才立真值;派生文案 = f(真值),只走映射投影。 + - 分档正文写在 declare_map 的 bands.content;缺则问清后再 declare,不要用 insert 把整张分档表塞进设定散文。 + - 默认无独立叙事转述、无旁观;用户点名要文风层或程序盯表时再议。 + - 保留用户原意;规格只走 toolcall,聊天只确认与建议。 + +brief: 数据化跑团:设定 + 真值 + 分档映射 → 可开玩 + +steps: [] diff --git a/skills/dialogue/world-simulator/recipes/转述/文本生成器/recipe.yaml b/skills/dialogue/world-simulator/recipes/转述/文本生成器/recipe.yaml index 3b91184..f01615a 100644 --- a/skills/dialogue/world-simulator/recipes/转述/文本生成器/recipe.yaml +++ b/skills/dialogue/world-simulator/recipes/转述/文本生成器/recipe.yaml @@ -1,25 +1,31 @@ -# 文本生成器 · 转述进料配方 -# 钉体验与设定;不写开场叙事本身。正文组成(格式)与开场白(按格式生成)见另配方或同提示分阶段。 +# 文本生成器 · Boss 直聘进料配方 +# 创作期写出游玩要用的固定上下文;成文在游玩:主世界出正文,转述出文风/壳。 when: >- - 按用户需求钉清要写什么、给谁看、气质与禁忌; - 不要世界推演、不要跨轮状态机、不要把聊天确认段当成开场白 + 用户要短篇/短文批量,且要强模仿给定范例; + 篇与篇不必剧情承接;成文走游玩管线(主世界 + 叙事转述) core: >- - 产物落需求与文风类上下文;默认无变量、无旁观、无主世界层。 - 「正文」指展示格式(设计.正文组成),不是开场白;开场白按格式另生成。 + 创作进料必须 insert 齐游玩上下文:模仿范例、模仿要点、叙事指南(大纲扩写)、可选轻量需求。 + 落档只挂载这些创作期产物,不另造用法。开玩后主世界写故事正文(Markdown),转述按范例改写/镶壳。 + 刚需开转述;用户输入用法以创作期叙事指南为准。 process: - - 听用户说清要写什么与交付形态;写入需求/文风等设定产物。 - - 缺关键信息就问;不要问卷式盘问。 - - 不要去搭世界模拟、变量、舞台骨架。 - - 不要把整章叙事只留在聊天确认段里。 + - 要齐:① insert 设计.模仿范例;② insert 设计.模仿要点(用户点名要学的面)。缺则问这两项。 + - 必 insert 设计.叙事指南与故事推进(创作期产物,开玩直接挂载)。正文至少写清: + 「用户输入用法:大纲扩写。本轮输入=短文大纲/场面要点;一次成完整篇并收束。禁止扮演停笔、禁止等用户行动。主世界交故事正文;有转述时转述负责文风与呈现,可补残稿但不改剧情。」 + - 可选 insert 用户.需求(题材/禁忌/篇幅)。 + - 齐料清单缺叙事指南 / 范例 / 要点任一 → 先补 insert,不要说可以开玩。 + - 材料齐了:告知「可以落档/开玩」;禁止在创作期写成稿短文。 + - 开玩:用户本轮=本篇大纲;读创作期已挂上下文推进。对话一长 clear_dialogue,留产物。 principles: - - 正推文本交付规格,勿套世界模拟全套。 - - 保留用户原意,少二次创作。 - - 格式与开场白分清:正文组成≠开场白。 + - 游玩只消费创作期产物;用法/文风/模仿面都在 insert 里,不靠落档程序补种。 + - 创作≠生产:创作收上下文;生产在游玩。 + - 分工:主世界 = 故事正文;转述 = 文风/壳(对照范例+要点;可补残稿)。 + - 输入用法由创作期叙事指南钉死为大纲扩写。 + - 一篇一世界;少问约束;无 declare_variable。 -brief: 文本生成:钉需求与文风(正文格式 / 开场白另步) +brief: 文本生成:创作期钉范例+要点+大纲扩写指南 → 开玩挂载成文 steps: [] diff --git a/src/dictate/play-bind.ts b/src/dictate/play-bind.ts index 1cc24d7..20a45b2 100644 --- a/src/dictate/play-bind.ts +++ b/src/dictate/play-bind.ts @@ -27,6 +27,10 @@ import { type PlaySlotsConfig, } from "../skills/play-slots.js"; import { parseWorkerSetYaml } from "../skills/worker-set-parse.js"; +import { + CREATION_SELECTED_RECIPE_TAG, + parseSelectedRecipeRef, +} from "../skills/creation-flow.js"; import { VARIABLE_CATALOG_TAG, } from "../skills/variable-catalog.js"; @@ -48,12 +52,17 @@ const SKIP_BIND_TAGS = new Set([ "设计.worker集.草稿", CONTEXT_ORDER_TAG, "创作.进料模式", + CREATION_SELECTED_RECIPE_TAG, VARIABLE_CATALOG_TAG, VALUE_MAP_TAG, ]); /** 偏文风/呈现:有叙事转述时挂转述;否则仍挂主世界层 */ -const STYLE_TAG_RE = /叙事指南|文风|美学|纲领|禁忌|示例|正文组成|回复格式|监控栏|篇幅|结构/; +const STYLE_TAG_RE = + /叙事指南|文风|美学|纲领|禁忌|示例|模仿|正文组成|回复格式|监控栏|篇幅|结构/; + +/** 这些配方开玩时刚需叙事转述(主世界出内容、转述出文风) */ +const RECIPES_REQUIRE_NARRATOR = new Set(["文本生成器"]); export type DictatePlayBindResult = { workerSetJson: string; @@ -185,7 +194,7 @@ function buildNarratorInserts(products: DictateProduct[]): ContextOrderInsert[] if (!isStyleProductTag(p.tag)) continue; inserts.push(insert(o++, p.tag, "full", "文风/呈现参考")); } - inserts.push(insert(o++, "运行.本轮.裁决", "full")); + inserts.push(insert(o++, "输出.用户展示", "full")); return inserts; } @@ -204,7 +213,7 @@ function buildAuditorInserts(params: { for (const tag of params.runtimeTags) { inserts.push(insert(o++, tag, tag === "变量.当前" ? "fields" : "full")); } - inserts.push(insert(o++, "运行.本轮.裁决", "full")); + inserts.push(insert(o++, "输出.用户展示", "full")); inserts.push(insert(o++, "用户.最新输入", "full")); return inserts; } @@ -261,7 +270,7 @@ export function buildDictateContextOrder(params: { inserts: [ insert(0, WORKER_PERSONA_REF, "fixed", "槽位人设"), insert(1, "用户.最新输入", "full"), - insert(2, "运行.本轮.裁决", "full"), + insert(2, "输出.用户展示", "full"), ], }); } @@ -275,15 +284,27 @@ export function buildDictateContextOrder(params: { }); } -function resolvePlaySlots(existingRaw: string | undefined): PlaySlotsConfig { +function resolvePlaySlots( + existingRaw: string | undefined, + board: Blackboard, +): PlaySlotsConfig { const parsed = existingRaw?.trim() ? parseWorkerSetYaml(existingRaw) : null; - return parsed?.play_slots ?? defaultPlaySlots(); + const slots = { ...(parsed?.play_slots ?? defaultPlaySlots()) }; + const recipeRef = parseSelectedRecipeRef( + board.getContentByTag(CREATION_SELECTED_RECIPE_TAG), + ); + if (recipeRef && RECIPES_REQUIRE_NARRATOR.has(recipeRef)) { + slots.gm = true; + slots.narrator = true; + } + return slots; } /** * 生成/更新 设计.worker集:保留已有 play_slots,重写 context_order 为产物挂载表。 + * 文本生成器等配方会强制打开叙事转述。 */ export function bindDictateProductsToPlaySpec(board: Blackboard): DictatePlayBindResult { const products = collectDictateBindProducts(board); @@ -291,7 +312,7 @@ export function bindDictateProductsToPlaySpec(board: Blackboard): DictatePlayBin board.getContentByTag(WORKER_SET_FINAL_TAG)?.trim() || board.getContentByTag(WORKER_SET_DRAFT_TAG)?.trim() || ""; - const playSlots = resolvePlaySlots(existing || undefined); + const playSlots = resolvePlaySlots(existing || undefined, board); const runtimeTags = collectRuntimeStateTags(board); const contextOrder = buildDictateContextOrder({ products, @@ -320,7 +341,13 @@ export function bindDictateProductsToPlaySpec(board: Blackboard): DictatePlayBin } catch { row = { play_slots: playSlots, brief: "Boss直聘落档自动生成" }; } - if (!row.play_slots) row.play_slots = playSlots; + row.play_slots = playSlots; + if (playSlots.narrator) { + row.brief = + typeof row.brief === "string" && String(row.brief).includes("转述") + ? row.brief + : "Boss直聘:主世界出内容,叙事转述出文风"; + } const withSlots = JSON.stringify(row, null, 2); const workerSetJson = mergeContextOrderIntoWorkerSetJson(withSlots, contextOrder); diff --git a/src/dictate/prompt.ts b/src/dictate/prompt.ts index 041d176..688e151 100644 --- a/src/dictate/prompt.ts +++ b/src/dictate/prompt.ts @@ -14,8 +14,9 @@ export function buildDictateSystemPrompt(opts?: { ? ` ## 本局配方 名称:${opts.recipeName ?? "(未命名)"} -${opts.recipeBrief ? `要点:${opts.recipeBrief}` : ""} +${opts.recipeBrief ? `要点:\n${opts.recipeBrief}` : ""} 按上述适用与原则整理产物;材料只够做本配方时,就只做本配方范围内的事。 +本配方若含跨轮数值:优先走 D(declare_variable / declare_map),不要只 insert 散文表。 ` : ""; @@ -45,6 +46,7 @@ ${opts.recipeBrief ? `要点:${opts.recipeBrief}` : ""} **分清:** - 正文组成(格式)→ insert \`设计.正文组成\` - 开场白(内容)→ insert \`设计.开场白\`;落档/开玩同步 \`输出.开场白\` +- 多篇无关联短文(生产在游玩)→ 创作期钉 \`设计.模仿范例\` / \`设计.模仿要点\` / \`设计.叙事指南与故事推进\`(大纲扩写) - 好感分档性格等 → declare_variable + declare_map,不要 insert 一整张表进设定散文 ${recipeBlock} ## 按用户本轮意图择一为主 @@ -60,6 +62,9 @@ insert 设定类;order 偏小。回复:确认体验 + 建议补充。 ### D · 钉变量与映射 用户提到跨轮状态、分档态度、章大纲切换时:declare_variable(含是否对用户可见)→ declare_map(分档正文)。可同轮多次调用。 +### E · 文本生成器(创作收料 → 开玩挂载) +必须用 insert 写齐游玩上下文:\`设计.模仿范例\`、\`设计.模仿要点\`、\`设计.叙事指南与故事推进\`(用户输入用法:大纲扩写;禁止扮演停笔)。缺任一就先补产物,不要说可以开玩。禁止在创作期写完整短文。开玩后程序只挂载这些产物:主世界出内容,转述出文风。 + ## 常用序感 - 需求/美学等不变设定:靠前(负) - 正文组成:中间(0) diff --git a/src/dictate/turn.ts b/src/dictate/turn.ts index 5990e8c..e758fda 100644 --- a/src/dictate/turn.ts +++ b/src/dictate/turn.ts @@ -48,7 +48,7 @@ export type DictateTurnResult = { }; /** - * 跑一轮 Boss 直聘整理:上下文 = 预设提示 + 按相对序的产物 + 全量对话。 + * 跑一轮 Boss 直聘整理:任务 messages 由本模块拼;preset 夹心由 LLM 包装层统一装配。 */ export async function runDictateTurn(params: { llm: LlmProvider; diff --git a/src/dictate/types.ts b/src/dictate/types.ts index f3de02b..0ef93d7 100644 --- a/src/dictate/types.ts +++ b/src/dictate/types.ts @@ -38,8 +38,10 @@ export function defaultDictateOrder(tag: string): number { const t = tag.trim(); if (t === "用户.需求") return -40; if (t === "设计.变量目录" || t === "设计.变量映射") return -25; - if (/美学|纲领|禁忌|示例/.test(t)) return -30; + if (t === "设计.模仿范例" || t === "设计.模仿要点") return -28; + if (/美学|纲领|禁忌|示例|模仿/.test(t)) return -30; if (/文风|叙事指南|篇幅|结构/.test(t)) return -20; + if (/短文集|短文\./.test(t)) return 10; if (/正文组成|回复格式/.test(t)) return 0; if (/开场白/.test(t)) return 20; return 0; diff --git a/src/server/display-labels.ts b/src/server/display-labels.ts index d096753..964707c 100644 --- a/src/server/display-labels.ts +++ b/src/server/display-labels.ts @@ -54,6 +54,8 @@ const SKILL_PACK_LABELS: Record = { "expand-assistant": "扩写助手", 文本生成器: "文本生成器", "text-generator": "文本生成器", + 交互式长文生成器: "交互式长文生成器", + 数据化跑团体验: "数据化跑团体验", }; /** 生命周期 / 相位 */ diff --git a/src/server/session-manager.ts b/src/server/session-manager.ts index c8800e7..46fa1d4 100644 --- a/src/server/session-manager.ts +++ b/src/server/session-manager.ts @@ -109,6 +109,7 @@ import { creationFlowFromRecipeSeed, findRecipeCatalogEntry, formatCreationFlowForUser, + formatDictateRecipeBrief, findModuleByName, findStepByRef, type FlowStepTitleHint, @@ -682,14 +683,17 @@ export class SessionManager { s.runtime.getBlackboard(), s.recipeOptions, ); + const recipeBrief = await resolveDictateRecipeBriefForSession({ + skillName: s.runtime.getActiveSkill()?.name, + recipeView, + recipeOptions: s.recipeOptions, + }); const result = await runDictateTurn({ llm, dialogue, recipeName: recipeView?.name, - recipeBrief: recipeView - ? [recipeView.declaration].filter(Boolean).join(";") - : undefined, + recipeBrief, handlers: { listProducts: () => this.listDictateProducts(s), writeProduct: (tag, content, order) => { @@ -3586,6 +3590,35 @@ function resolveSelectedRecipeView( return { id: ref, name: ref, declaration: "" }; } +/** Boss 直聘:优先注入 recipe.yaml 方法论,缺则退回 catalog declaration */ +async function resolveDictateRecipeBriefForSession(params: { + skillName?: string | null; + recipeView: { id: string; name: string; declaration: string } | null; + recipeOptions?: RecipeCatalogEntry[]; +}): Promise { + const view = params.recipeView; + if (!view) return undefined; + const entry = findRecipeCatalogEntry( + params.recipeOptions?.length ? { recipes: params.recipeOptions } : null, + view.id, + ); + const skillName = params.skillName?.trim(); + if (entry && skillName) { + try { + const skill = await loadSkill(skillName); + const packRoot = skill.skillPackRoot?.trim(); + if (packRoot) { + const detail = await loadRecipeDetail(packRoot, entry); + const brief = formatDictateRecipeBrief(detail); + if (brief) return brief; + } + } catch (err) { + console.error("[会话] 加载转述配方要点失败", err); + } + } + return view.declaration?.trim() || undefined; +} + function buildWorkerSetUserView( blackboard: Blackboard, session?: RuntimeSession, diff --git a/src/skills/creation-flow.ts b/src/skills/creation-flow.ts index 6b26603..e424a3f 100644 --- a/src/skills/creation-flow.ts +++ b/src/skills/creation-flow.ts @@ -247,6 +247,8 @@ export type RecipeDetail = { id: string; name: string; declaration: string; + /** 一句话方法摘要(recipe.yaml brief;转述进料优先注入) */ + brief?: string; /** 适用什么体验/任务 */ when?: string; /** 整套设计方法的核心思路与最终目标 */ @@ -1433,8 +1435,12 @@ export function parseRecipeYaml( ? row.name.trim() : meta.name; + const brief = + typeof row.brief === "string" && row.brief.trim() + ? row.brief.trim() + : undefined; const seedParsed = parseCreationFlow(JSON.stringify({ - brief: typeof row.brief === "string" ? row.brief : undefined, + brief, status: "open", steps: Array.isArray(row.steps) ? row.steps : [], })); @@ -1444,6 +1450,7 @@ export function parseRecipeYaml( id: meta.id, name, declaration: meta.declaration, + ...(brief ? { brief } : {}), when, ...(core ? { core } : {}), ...(process ? { process } : {}), @@ -1551,6 +1558,38 @@ export function formatRecipeCatalogForAgent(catalog: RecipeCatalog): string { return `【可选配方】(须由用户手动选择)\n${lines.join("\n")}`; } +/** + * Boss 直聘 / 转述进料:把已选配方方法论收成短要点(进系统提示「本局配方」)。 + * 不含 DAG steps / design-flow 选型话术。 + */ +export function formatDictateRecipeBrief(detail: RecipeDetail): string { + const lines: string[] = []; + if (detail.brief?.trim()) lines.push(detail.brief.trim()); + else if (detail.declaration?.trim()) lines.push(detail.declaration.trim()); + if (detail.when?.trim()) lines.push(`适用:${detail.when.trim()}`); + if (detail.core?.trim()) { + lines.push("核心:"); + lines.push(detail.core.trim()); + } + if (detail.process) { + lines.push("怎么做:"); + lines.push(formatRecipeFieldBlock(detail.process)); + } + if (detail.principles) { + lines.push("原则:"); + lines.push(formatRecipeFieldBlock(detail.principles)); + } + if ( + !detail.core && + !detail.process && + !detail.principles && + detail.hint?.trim() + ) { + lines.push(detail.hint.trim()); + } + return lines.join("\n").trim(); +} + /** 注入 design-flow:用户已选配方(方法论 + 开局起点) */ export function formatSelectedRecipeForAgent(detail: RecipeDetail): string { const lines: string[] = [ diff --git a/tests/creation-flow.test.ts b/tests/creation-flow.test.ts index 2d3ab1b..cb0fb4e 100644 --- a/tests/creation-flow.test.ts +++ b/tests/creation-flow.test.ts @@ -12,6 +12,7 @@ import { listCallableCatalogModules, formatModuleCatalogForAgent, formatRecipeCatalogForAgent, + formatDictateRecipeBrief, formatSelectedRecipeForAgent, isCreationFlowComplete, loadAllRecipeDetails, @@ -516,12 +517,28 @@ describe("creation-flow", () => { ); 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); + expect(dictate?.recipes.some((r) => r.name === "交互式长文生成器")).toBe(true); + expect(dictate?.recipes.some((r) => r.name === "数据化跑团体验")).toBe(true); + 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).toBeTruthy(); + expect(detail.core).toMatch(/范例|模仿|转述|游玩/); 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(/长文|分段/); + + 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("declare_map"); }); it("parses selected recipe ref", () => { @@ -619,7 +636,7 @@ recipes: name: 缺声明 `); expect(cat?.recipes).toEqual([ - { id: "ok", name: "好配方", declaration: "有声明" }, + { id: "ok", name: "好配方", declaration: "有声明", family: "recipe" }, ]); }); diff --git a/tests/dictate-play-bind.test.ts b/tests/dictate-play-bind.test.ts index 5604fd4..f359523 100644 --- a/tests/dictate-play-bind.test.ts +++ b/tests/dictate-play-bind.test.ts @@ -14,6 +14,8 @@ describe("dictate play-bind", () => { it("classifies style tags", () => { expect(isDictateStyleBindTag("设计.美学纲领")).toBe(true); expect(isDictateStyleBindTag("设计.正文组成")).toBe(true); + expect(isDictateStyleBindTag("设计.模仿范例")).toBe(true); + expect(isDictateStyleBindTag("设计.模仿要点")).toBe(true); expect(isDictateStyleBindTag("用户.需求")).toBe(false); }); @@ -77,13 +79,68 @@ describe("dictate play-bind", () => { const narrRefs = narr!.inserts.map((i) => i.ref); expect(narrRefs).toContain("设计.美学纲领"); expect(narrRefs).not.toContain("用户.需求"); - expect(narrRefs).toContain("运行.本轮.裁决"); + expect(narrRefs).toContain("输出.用户展示"); + expect(narrRefs).not.toContain("运行.本轮.裁决"); const gm = result.contextOrder.slots.find((s) => s.ref === "world-simulator"); expect(gm!.inserts.map((i) => i.ref)).toContain("用户.需求"); expect(result.workerSetJson).toContain("context_order"); }); + it("forces narrator on for 文本生成器 and mounts creation-period style context", () => { + const board = new Blackboard(); + board.write({ + tag: "创作.选用配方", + content: JSON.stringify({ id: "文本生成器", name: "文本生成器" }), + source: "user", + }); + board.write({ + tag: "设计.模仿范例", + content: "范例正文", + source: "dictate", + metadata: { [DICTATE_ORDER_META_KEY]: -28 }, + }); + board.write({ + tag: "设计.模仿要点", + content: "- 句式节奏\n- 感官密度", + source: "dictate", + metadata: { [DICTATE_ORDER_META_KEY]: -28 }, + }); + board.write({ + tag: "设计.叙事指南与故事推进", + content: "用户输入用法:大纲扩写。禁止扮演停笔。", + source: "dictate", + metadata: { [DICTATE_ORDER_META_KEY]: -22 }, + }); + board.write({ + tag: "用户.需求", + content: "短篇", + source: "dictate", + }); + board.write({ + tag: "设计.worker集", + content: JSON.stringify({ + play_slots: { gm: true, narrator: false, auditor: false, perspective: false }, + }), + source: "test", + }); + + const result = bindDictateProductsToPlaySpec(board); + expect(result.contextOrder.play_slots?.narrator).toBe(true); + const narr = result.contextOrder.slots.find((s) => s.ref === "narrator"); + expect(narr).toBeTruthy(); + const narrRefs = narr!.inserts.map((i) => i.ref); + expect(narrRefs).toContain("设计.模仿范例"); + expect(narrRefs).toContain("设计.模仿要点"); + expect(narrRefs).toContain("设计.叙事指南与故事推进"); + expect(narrRefs).not.toContain("用户.需求"); + expect(JSON.parse(result.workerSetJson).play_slots.narrator).toBe(true); + // 落档不改写创作期叙事指南 + expect(board.getContentByTag("设计.叙事指南与故事推进")).toBe( + "用户输入用法:大纲扩写。禁止扮演停笔。", + ); + }); + it("does not invent variable mappings; binds declared map slots and catalog", () => { const board = new Blackboard(); board.write({ tag: "用户.需求", content: "x", source: "dictate" }); diff --git a/tests/dictate.test.ts b/tests/dictate.test.ts index 2ff42fe..5a44dd0 100644 --- a/tests/dictate.test.ts +++ b/tests/dictate.test.ts @@ -64,8 +64,8 @@ describe("dictate context", () => { it("system prompt requires insert toolcall and relative order", () => { const prompt = buildDictateSystemPrompt({ - recipeName: "正文组成", - recipeBrief: "钉呈现壳与可见块", + recipeName: "数据化跑团体验", + recipeBrief: "设定 + 真值 + 分档映射\n怎么做:\n- declare_variable\n- declare_map", }); expect(prompt).toContain("设计.正文组成"); expect(prompt).toContain("设计.开场白"); @@ -73,8 +73,15 @@ describe("dictate context", () => { expect(prompt).toContain("insert"); expect(prompt).toContain("declare_variable"); expect(prompt).toContain("declare_map"); + 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("只挂载这些产物"); }); it("sorts products by relative order: neg then 0 then pos", () => { diff --git a/web/display-labels.js b/web/display-labels.js index 3328497..470e1d5 100644 --- a/web/display-labels.js +++ b/web/display-labels.js @@ -59,6 +59,8 @@ export function displaySkillPackLabel(id) { "expand-assistant": "扩写助手", 文本生成器: "文本生成器", "text-generator": "文本生成器", + 交互式长文生成器: "交互式长文生成器", + 数据化跑团体验: "数据化跑团体验", }; return map[id] ?? id; }