diff --git a/docs/ui-glossary.md b/docs/ui-glossary.md index 59113f5..e8c5bd9 100644 --- a/docs/ui-glossary.md +++ b/docs/ui-glossary.md @@ -135,8 +135,8 @@ | `design-flow` | **创作 · 流程编排** | 以已选【配方】为起点,编排/增量修订工作流计划(可变 DAG) | | `design-step` | **创作 · 执行步骤** | 按工作流计划执行当前【技能】 | | `opening-generator` | **开局 · 开场白** | 无 `opening-setup` 时的兜底落库;有则选定开场白节点即收口 | -| `opening-setup`(技能) | **开场白与开场变量** | 每条开场 = meta(用户角色+初值)+ 正文(present.v1,壳跟正文组成)。游玩辅助 Tab「开场」点选;没有或不采用则用当前用户角色卡 | -| `protagonist`(技能) | **主角设定** | 旧路径兜底;新开场把用户角色写在开场白 meta 里 | +| `opening-setup`(技能) | **开场白与开场变量** | 工序路径:1~多条候选 swipe。对话落盘用 `设计.开场白[#短码]` 增殖;游玩辅助 Tab「开场」点选 | +| `protagonist`(技能) | **主角设定** | `设计.主角设定[#短码]`;与开场白同短码即绑定该开局。挂在当前选用的用户角色卡下 | | `worker-spec`(技能) | **游玩拓扑** | 勾选固定槽;旧称「Worker 规格」,勿再当自由发明演员 | | `design-core` 等 | (已废弃) | 旧分步 skill;勿再调度 | @@ -207,6 +207,7 @@ | `narrative_guide` | 叙事指南与故事推进 | | `narrative-guide` | 叙事指南(对话落盘) | | `story-progression` | 故事推进(对话落盘) | +| `user-requirements` | 用户需求(偏好库选用) | | `input_protocol` | 输入协议 | | `core_premise` | 核心前提 | | `aesthetics` | 美学纲领(旧;已并入美学纲领与交互范式) | diff --git a/skills/dialogue/world-simulator/modules/catalog.yaml b/skills/dialogue/world-simulator/modules/catalog.yaml index bbf3fa5..dd6e492 100644 --- a/skills/dialogue/world-simulator/modules/catalog.yaml +++ b/skills/dialogue/world-simulator/modules/catalog.yaml @@ -10,6 +10,7 @@ # auto = true 时为程序步:不抛默认问题、不经「同意并开始」,提案后直接执行(产物仍验收) # kind = 节点特性。缺省=普通执行步。prior-artifact=先验产物(须先定写什么;params 是规划产物,不拦执行) # intake = 可选;recipe=仅工序编排 DAG;dictate=仅对话落盘何时落盘。缺省=两族都可见 +# libraries = 可选;本步绑定的全局库 id 列表(如 style-packs);design-step 注入,库空跳过 # params = 可选;普通节点=编排进 DAG 时 steps[].params 的声明(required 须在执行前钉齐) # 〔先验产物〕=规划产物字段(可提前填,空则步内钉) # opening = 可选;覆盖 prompt.md 里 ```opening 默认问题(一般只写在 prompt.md) @@ -38,11 +39,20 @@ modules: - id: protagonist name: 主角设定 + repeatable: true declaration: >- 钉清 @玩家 扮演的那个人:名字、背景、特殊设定(身份/金手指等)。 - 仅当开场白 meta 不够用时才排;用户角色优先写在开场白里。 + 代入有名片时排本步;可 设计.主角设定#短码 与开场白同槽绑定;禁止把名字/简介写进开场白正文。 artifact: 设计.主角设定 + - id: user-requirements + name: 用户需求 + libraries: [preferences] + declaration: >- + 把跨局偏好库里对本局生效的硬约束(禁区/纠偏/默认取向)落成可执行条目; + 不重做文风问卷,也不钉体验站位 + artifact: 设计.用户需求 + # —— 世界与内容 —— - id: world-blueprint name: 舞台骨架 @@ -91,6 +101,7 @@ modules: - id: narrative name: 叙事指南与故事推进 intake: recipe + libraries: [style-packs] declaration: >- 美学已有方向时:把感觉落成可执行的遣词、笔墨落点、禁忌与推进方式; 不重做体验问卷。工序编排用本步;对话落盘拆成「叙事指南」「故事推进」 @@ -99,6 +110,7 @@ modules: - id: narrative-guide name: 叙事指南 intake: dictate + libraries: [style-packs] declaration: >- 美学已有方向时:把感觉落成可执行的遣词、笔墨落点、禁忌与写法档; 不重做体验问卷,也不钉故事怎么往前 diff --git a/skills/dialogue/world-simulator/modules/narrative-guide/prompt.md b/skills/dialogue/world-simulator/modules/narrative-guide/prompt.md index b067283..db03d2a 100644 --- a/skills/dialogue/world-simulator/modules/narrative-guide/prompt.md +++ b/skills/dialogue/world-simulator/modules/narrative-guide/prompt.md @@ -46,11 +46,12 @@ feeds: narrator,gm 把美学里的感觉落成可执行的写法。作用对象是本局游玩正文(转述与主世界层都读这份)。 执行顺序: -1. 继承美学体验与禁忌,改写成做法向句子。 -2. 写纲领、遣词(含示范)、笔墨焦点、禁忌/不偏好。 +1. 若提示中有【文风库 · 可选用】且用户点名/对上某包:优先按该包落成「怎么写」,再按本局美学微调;不要重做文风问卷。 +2. 否则继承美学体验与禁忌,改写成做法向句子。 +3. 写纲领、遣词(含示范)、笔墨焦点、禁忌/不偏好。 遣词用户未提段落形态时,默认写入:完整长自然段叙述,不要单句成段(篇幅数字交给「回复呈现」)。 -3. 情境备用 = 写法档(体裁/调性容器),不是具体桥段。有切换则每条写:名字=档名(如「官能小说」「网文爽文」「展示恐怖」「展示情色」),用法=该档怎么遣词、笔墨落哪、何时切进来。无切换则 `[]`。用户若报出「当众折辱」「独处一室」这类场合,升一档写进对应写法档,场合本身不单列。 -4. 自评 + 追问。故事怎么往前、用户输入用法交给「故事推进」,本步不写。 +4. 情境备用 = 写法档(体裁/调性容器),不是具体桥段。有切换则每条写:名字=档名(如「官能小说」「网文爽文」「展示恐怖」「展示情色」),用法=该档怎么遣词、笔墨落哪、何时切进来。无切换则 `[]`。用户若报出「当众折辱」「独处一室」这类场合,升一档写进对应写法档,场合本身不单列。 +5. 自评 + 追问。故事怎么往前、用户输入用法交给「故事推进」,本步不写。 summary:`叙事指南 · …` ``` diff --git a/skills/dialogue/world-simulator/modules/narrative/prompt.md b/skills/dialogue/world-simulator/modules/narrative/prompt.md index bada053..30f1525 100644 --- a/skills/dialogue/world-simulator/modules/narrative/prompt.md +++ b/skills/dialogue/world-simulator/modules/narrative/prompt.md @@ -50,16 +50,17 @@ feeds: narrator,gm mount:`narrator` 与 `world-simulator` 都挂同一份全文——不要为省一点点 token 拆两套裁剪方案。 执行顺序: -1. 继承美学体验与禁忌,改写成做法向句子。 -2. 写纲领、遣词(含示范)、笔墨焦点、禁忌/不偏好。 +1. 若提示中有【文风库 · 可选用】且用户点名/对上某包:优先按该包落成「怎么写」部分,再按本局美学微调;不要重做文风问卷。推进仍按本步规则钉。 +2. 否则继承美学体验与禁忌,改写成做法向句子。 +3. 写纲领、遣词(含示范)、笔墨焦点、禁忌/不偏好。 遣词用户未提段落形态时,默认写入:完整长自然段叙述,不要单句成段(篇幅数字交给「回复呈现」)。 -3. 情境备用 = 写法档(体裁/调性容器),不是具体桥段。有切换则每条写:名字=档名(如「官能小说」「网文爽文」「展示恐怖」「展示情色」),用法=该档怎么遣词、笔墨落哪、何时切进来。无切换则 `[]`。用户若报出「当众折辱」「独处一室」这类场合,升一档写进对应写法档,场合本身不单列。 -4. 写推进与决策(可写「无专门推进」);变量只写钩子。 +4. 情境备用 = 写法档(体裁/调性容器),不是具体桥段。有切换则每条写:名字=档名(如「官能小说」「网文爽文」「展示恐怖」「展示情色」),用法=该档怎么遣词、笔墨落哪、何时切进来。无切换则 `[]`。用户若报出「当众折辱」「独处一室」这类场合,升一档写进对应写法档,场合本身不单列。 +5. 写推进与决策(可写「无专门推进」);变量只写钩子。 「用户输入用法」未提及时:按美学/交互范式选一种写入产物—— - 写手/大纲成稿 → **大纲扩写**(输入=大纲→细化成正文;不越界、完整演绎); - 代入扮演、轮转等用户决策 → **扮演停笔**(输入的行为与对话作正文开头并扩写填满;不写用户角色未给出的行为/对话;不替用户角色做决定;停在其他角色等回应之刻)。 两种都像则写进追问让用户点一次;不要混成半套规则。 -5. 自评 + 追问。 +6. 自评 + 追问。 summary:`叙事指南与故事推进 · …` ``` diff --git a/skills/dialogue/world-simulator/modules/user-requirements/prompt.md b/skills/dialogue/world-simulator/modules/user-requirements/prompt.md new file mode 100644 index 0000000..58aaf23 --- /dev/null +++ b/skills/dialogue/world-simulator/modules/user-requirements/prompt.md @@ -0,0 +1,118 @@ +# 用户需求 + +> 对话落盘与工序编排共用。程序只切割下方 **fence 块**;`##` 标题仅供人读。 +> 产物外壳:`docs/context-fragment-design.md`。 +> 从全局【偏好库】选用硬约束写入本局;不管文风遣词(那是叙事指南)。 + +## meta + +```meta +name: 用户需求 +id: user-requirements +artifact: 设计.用户需求 +declaration: > + 把跨局偏好库里对本局生效的硬约束(禁区/纠偏/默认取向)落成可执行条目; + 不重做文风问卷,也不钉体验站位 +when: | + 提示中有【偏好库 · 可选用】且库非空,或用户主动谈跨局禁区、绝对不要写什么、默认取向要进本局。 + 开局常见:库里已有起步条目且本局尚无「设计.用户需求」。 +when_not: | + 本局已有等价「设计.用户需求」且用户未要求改偏好 → 不重复排。 + 只谈文风遣词/示范 → 「叙事指南」(工序编排:「叙事指南与故事推进」)。 + 只谈体验站位/核心感觉 → 「美学纲领与交互范式」。 + 库空且用户未提硬约束 → 不排。 +boundary: | + 本技能:一份 context-fragment,写清本局必须遵守的硬约束列表。 + 文风:叙事指南。体验:美学。推进:故事推进。 +feeds: narrator,gm +``` + +## opening + +```opening +偏好库里已有跨局硬约束时,请确认本局要带哪些(想到什么写什么): + +1. 哪些条目本局生效?有没有要改写、收紧或放宽的? +2. 有没有库外、但本局一定要遵守的禁区/纠偏?(一句话即可) +``` + +## task + +```task +你正在执行「用户需求」。产物必须是 **context-fragment.v1** JSON,写入「设计.用户需求」。 + +作用对象是本局游玩固定上下文(主世界层与转述都读)。只写硬约束,不写遣词示范。 + +执行顺序: +1. 若提示中有【偏好库 · 可选用】:默认把 active 条目纳入,再按用户点名增删/改写;勿整库无脑粘贴原文若用户只要子集。 +2. 用户本局补充的禁区/纠偏:写成可执行短句,与库条并列。 +3. 自评 + 追问。文风交给叙事指南;体验站位交给美学。 + +summary:`用户需求 · …` +``` + +## principles + +```principles +1. 硬约束 ≠ 文风:禁区/取向/纠偏归本步;遣词与示范归叙事指南。 +2. 条目要短、可执行;写「不要 X,改做 Y」优于空泛「注意分寸」。 +3. 库空且用户无补充 → 可不排本步;有库则优先落盘,勿装作没有。 +4. 与本局世界观许可冲突时,以本步产物为准(游玩侧已约定)。 +``` + +## probe + +```probe +本步只钉:本局生效的硬约束列表是否够用、有无漏掉用户点名的禁区。 + +库条目用户未否决的默认纳入;用户只要子集则只写子集。 +缺关键禁区且用户话里有信号:在「追问」写 1 点。够用:`追问.题目` 为 `[]`。 + +用户谈到文风遣词:写入「开放问题」,交给叙事指南。 +用户谈到站位/核心感觉:写入「开放问题」,交给美学。 +``` + +## output + +```output +{ + "schema": "context-fragment.v1", + "技能": "用户需求", + "brief": "一句话:本局硬约束重心", + "mount": ["world-simulator", "narrator"], + "稳变": "stable", + "正文": { + "来源说明": "偏好库选用 + 本局补充", + "约束": [ + "短、可执行的一条", + "…" + ] + }, + "自评": { + "维度": [ + { "名": "可执行", "分数": 8, "说明": "…" }, + { "名": "覆盖用户点名", "分数": 8, "说明": "…" } + ], + "薄弱点": "" + }, + "追问": { "导语": "", "题目": [] }, + "开放问题": [] +} +``` + +硬规则:合法 JSON;`约束` 至少 1 条(若用户明确「本局不要任何跨局偏好」则可写一条「本局不套用偏好库」);禁止 order。 + +## checklist + +```checklist +- [ ] 约束短且可执行? +- [ ] 未把文风遣词/体验站位写进本步? +- [ ] 用户点名的禁区已覆盖或已追问? +``` + +## examples + +```examples +好:纳入偏好库 NTR/取向两条,并加一条「不要反复报精确数字」。 +坏:写成叙事遣词示范;或整库粘贴却无视用户「只要取向那条」。 +``` diff --git a/src/skills/creation-flow.ts b/src/skills/creation-flow.ts index 007e76a..7764a52 100644 --- a/src/skills/creation-flow.ts +++ b/src/skills/creation-flow.ts @@ -10,6 +10,10 @@ import { readFile } from "node:fs/promises"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { parse as parseYaml } from "yaml"; +import { + collectLibraryIdsFromModules, + formatBoundLibrariesForPrompt, +} from "../libraries/index.js"; import { tryParseJsonDoc } from "../parse/json-doc.js"; export const CREATION_FLOW_TAG = "设计.创作流程"; @@ -211,6 +215,11 @@ export type ModuleCatalogEntry = { * recipe = 只进工序编排 DAG;dictate = 只进对话落盘何时落盘。 */ intake?: RecipeIntakeFamily; + /** + * 可选:本步绑定的全局库 id(如 style-packs)。 + * design-step 执行时按注册表注入对应块;库空则跳过。 + */ + libraries?: string[]; /** 来自 prompt.md ```meta:何时该选用(编排选型) */ when?: string; /** 来自 prompt.md ```meta:何时不该选用 */ @@ -951,6 +960,7 @@ export function parseModuleCatalog(raw: string): ModuleCatalog | null { const kind = parseModuleNodeKind(m.kind); const params = parseModuleParamSpecs(m.params); const intake = parseModuleIntake(m.intake); + const libraries = parseModuleLibraries(m.libraries); modules.push({ id, name, @@ -963,6 +973,7 @@ export function parseModuleCatalog(raw: string): ModuleCatalog | null { ...(opening ? { opening } : {}), ...(params ? { params } : {}), ...(intake ? { intake } : {}), + ...(libraries ? { libraries } : {}), }); } if (modules.length === 0) return null; @@ -992,6 +1003,19 @@ function parseModuleParamSpecs(raw: unknown): ModuleParamSpec[] | undefined { return out.length > 0 ? out : undefined; } +function parseModuleLibraries(raw: unknown): string[] | undefined { + if (!Array.isArray(raw) || raw.length === 0) return undefined; + const ids: string[] = []; + const seen = new Set(); + for (const item of raw) { + const id = typeof item === "string" ? item.trim() : ""; + if (!id || seen.has(id)) continue; + seen.add(id); + ids.push(id); + } + return ids.length ? ids : undefined; +} + /** * 能力 prompt.md 可切割块(fence 语言标签 = 块 id)。 * 标准块见 MODULE_SECTION_IDS;程序只认 ```id … ```,不认散文标题 alone。 @@ -1373,7 +1397,8 @@ const DICTATE_PROGRAM_SIDE_MODULE_IDS = new Set([ * 不写 DAG / design-step 话术;不塞执行全文。 */ export function formatModuleCatalogForDictate(catalog: ModuleCatalog): string { - const lines = catalogModulesForIntake(catalog, "dictate").map((m) => { + const dictateModules = catalogModulesForIntake(catalog, "dictate"); + const lines = dictateModules.map((m) => { const programSide = DICTATE_PROGRAM_SIDE_MODULE_IDS.has(m.id) || m.auto; const flags = [ m.repeatable ? "〔可反复〕" : "", @@ -1393,7 +1418,7 @@ export function formatModuleCatalogForDictate(catalog: ModuleCatalog): string { if (m.boundary) parts.push(` 边界:${indentMultiline(m.boundary, " ")}`); return parts.join("\n"); }); - return [ + const head = [ "【能力 · 何时落盘】", "对照下方「何时用 / 何时不用 / 边界」与已有产物判定:条件成立才 insert(或 declare_*);含糊或未到时机 → 本轮不落该能力,改为短确认或问挡住的 1 点。", "同轮可多次落盘:凡本轮材料已够且「何时用」成立的能力,都可 insert / declare_*(可并行);每个 insert 各回 reply_module,可见回复分段附带。", @@ -1404,6 +1429,10 @@ export function formatModuleCatalogForDictate(catalog: ModuleCatalog): string { "能力执行全文不进聊天;规格只走 toolcall。insert 成功后按返回的 reply_module 写可见回复(见系统提示「对用户可见回复」)。", lines.join("\n"), ].join("\n"); + + const libIds = collectLibraryIdsFromModules(dictateModules); + const libBlock = formatBoundLibrariesForPrompt(libIds).trim(); + return libBlock ? `${head}\n\n${libBlock}` : head; } function dictateLandHint(m: ModuleCatalogEntry): string { diff --git a/src/skills/loader.ts b/src/skills/loader.ts index 7fca51a..05685bc 100644 --- a/src/skills/loader.ts +++ b/src/skills/loader.ts @@ -559,11 +559,36 @@ export async function loadWorkerSkillWithContext( skill.skillPackRoot, skillsRoot, ); + const parts: string[] = []; if (shells.length) { - rosterBlock = `${formatPresentShellsForPrompt(shells)}\n\n`; + parts.push(formatPresentShellsForPrompt(shells)); + } + const { + loadMarkdownSafeSubsetCatalog, + formatMarkdownSafeSubsetForPrompt, + } = await import("./markdown-safe-subset.js"); + const safeSubset = await loadMarkdownSafeSubsetCatalog( + skill.skillPackRoot, + skillsRoot, + ); + if (safeSubset) { + parts.push(formatMarkdownSafeSubsetForPrompt(safeSubset)); + } + if (parts.length) { + rosterBlock = `${parts.join("\n\n")}\n\n`; } } - modulePromptBlock = `${rosterBlock}${paramsBlock}\n\n## 【本步方法 · ${binding.module.name}】\n\n${binding.modulePrompt.trim()}${openingNote}${inheritNote}`; + let libraryBlock = ""; + if (binding.module.libraries?.length) { + const { formatBoundLibrariesForPrompt } = await import( + "../libraries/index.js" + ); + const libText = formatBoundLibrariesForPrompt( + binding.module.libraries, + ).trim(); + if (libText) libraryBlock = `${libText}\n\n`; + } + modulePromptBlock = `${rosterBlock}${libraryBlock}${paramsBlock}\n\n## 【本步方法 · ${binding.module.name}】\n\n${binding.modulePrompt.trim()}${openingNote}${inheritNote}`; const depTags = inheritTag ? binding.depTags.filter((tag) => tag !== inheritTag) : binding.depTags; diff --git a/tests/creation-flow.test.ts b/tests/creation-flow.test.ts index 10c1517..6ba05f5 100644 --- a/tests/creation-flow.test.ts +++ b/tests/creation-flow.test.ts @@ -499,6 +499,16 @@ describe("creation-flow", () => { expect(catalog?.modules.find((m) => m.name === "故事推进")?.intake).toBe( "dictate", ); + expect(catalog?.modules.find((m) => m.name === "用户需求")?.libraries).toEqual([ + "preferences", + ]); + 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( catalogModulesForIntake(catalog!, "recipe").some( (m) => m.name === "叙事指南",