diff --git a/docs/design-orchestrator-guide.md b/docs/design-orchestrator-guide.md index ac9de98..1c40c0b 100644 --- a/docs/design-orchestrator-guide.md +++ b/docs/design-orchestrator-guide.md @@ -234,7 +234,7 @@ value, rev, updatedAt, source # source: user | worker: | system ### 5.4 维护 worker -读:正文/裁决、维护规则、当前表 → 写:字段更新。 +读:本轮正文、维护规则、当前表 → 写:字段更新。 可不写用户终稿;可与展示链解耦、**延后**跑。 ### 5.5 副作用(含大纲、人设、事件) @@ -479,7 +479,7 @@ phase:core(核心体验 + 交互) | 意图 | 典型停点 | |------|----------| | 自主长篇 | 大纲 / 主题 worker → `review`;正文链多段 → `continue`,直到用户叫停或大纲段落再 `review` | -| 交互 / RP | 产出用户可见终稿的转述(如 narrator)→ `review`;世界裁决等中间层 → `continue` | +| 交互 / RP | 产出用户可见终稿的槽(无转述=主世界层;有转述=narrator)→ `review`;有转述时主世界草稿 → `continue` | 创作期按 **§7.2 创作单位** 分块验收(worker 与固定上下文同级),与上表正交。 design-flow / design-step / 创作调度必须为 **每个** run worker 写出 `acceptance`;缺省时不得假设「全都不用验收」——面向用户的可读输出默认倾向 `review`,纯中间层倾向 `continue`,吃不准就 ask_user。 diff --git a/docs/play-dm-auditor.md b/docs/play-dm-auditor.md index 62b11e6..0ac0333 100644 --- a/docs/play-dm-auditor.md +++ b/docs/play-dm-auditor.md @@ -7,69 +7,44 @@ | 称呼 | 槽 | 干什么 | 不干什么 | |------|-----|--------|----------| -| **主世界层(DM)** | gm | 读设定与当前状态,对本轮玩家行动做**裁决**;可提议改变量 | 不负责长文文风;不靠「再想一遍」修表 | -| **旁观维护(副 LLM)** | auditor | **查漏补缺**:表缺字段、该不该按生成规则补一条实例;多数轮**空操作** | 不当作者写正文;不替代裁决 | +| **主世界层(DM)** | gm | 读设定与当前状态,推进场面,直接写**故事正文(Markdown)** | 不交 JSON 裁决包;不靠结构化包改表 | +| **旁观维护(副 LLM)** | auditor | **查漏补缺**:读本轮正文推断表变更、该不该按生成规则补实例;多数轮**空操作** | 不当作者写正文;不替代主世界叙事 | -转述只吃裁决包写用户可见文字——与「俩人怎么配合」无关。 +转述(若启用)读主世界正文:提取场面、按文风改写、按壳镶前端;可补残稿,不改剧情。 ## 2. 你提到的两种做法 **做法甲:DM 先提要求 → 副 LLM 交作业 → DM 再过一遍** ```text -DM:「按怪物规则补 1 个」→ 旁观生成 → DM 再裁决一次 +DM:「按怪物规则补 1 个」→ 旁观生成 → DM 再写一遍 ``` - 优点:DM 始终握笔。 - 缺点:同一回合容易 **3 次模型调用**,贵、慢;旁观一不小心变成「小作者」。 -**做法乙:DM 先交齐裁决 → 把结果和要求交给副 LLM → 副 LLM 用工具改表** +**做法乙:DM 先交齐正文 → 副 LLM 根据正文改表** ```text -DM 出裁决包(含变量变更)→ 旁观只动表/触发生成 → 程序合并,不再开一轮 DM +DM 写故事正文 →(可选转述润色)→ 旁观只动表/触发生成 → 程序合并,不再开一轮 DM ``` -- 优点:叙事权威仍在 DM;旁观像**库管/校对**,适合工具改表。 -- 缺点:若裁决里已经写错事实,旁观「修表」救不了叙事,只能修状态。 +- 优点:叙事权威仍在 DM;旁观像**库管/校对**。 +- 缺点:若正文写错事实,旁观「修表」救不了叙事,只能修状态。 ## 3. 本仓库采用的(推荐) -**采用做法乙:主世界层先拍板 → 转述 → 旁观在回合末改表(合并结果供下一轮主世界层读取)。** +**采用做法乙:主世界层先写正文 →(可选)转述 → 旁观在回合末改表(合并结果供下一轮主世界层读取)。** ```text 每轮: 1. 主世界层:读上一轮合并后的 变量.当前 + 设定 + 历史 + 用户输入 - → 出 settlement 裁决包(可含 variable_changes;可经 harness 调机遇工具) - 2. 程序:合并裁决中的 variable_changes → 变量.当前(边沿副作用照常) - 3. 叙事转述(若启用):只读裁决包写用户可见正文 - 4. 旁观维护:读本轮裁决 + 合同 + 表 + → 直接写 Markdown 故事正文(输出.用户展示;可经 harness 调机遇工具) + 2. 叙事转述(若启用):读主世界正文 → 提取/改写/镶壳 → 覆盖 输出.用户展示 + 3. 旁观维护:读本轮正文 + 合同 + 表 → 多数轮:空 maintain → 少数轮:table_ops / need_generate - 5. 程序:合并旁观 table_ops → 变量.当前(**下一轮**主世界层才读到) + 4. 程序:合并旁观 table_ops → 变量.当前(**下一轮**主世界层才读到) ``` -要点: - -1. **不要**默认「旁观先扫再让 DM 裁决」(旧顺序已废弃)。 -2. **不要**默认「DM 写完再让旁观润色全文」(贵,且旁观会抢笔)。 -3. **不要**默认「旁观先写一大段设定再交给 DM」(旁观变作者)。 -4. 生成规则给旁观:只要**合同**(有哪些 rule、格式、何时该生成),不要整本描写范文。 -5. 真要「按规则生成实例」:旁观点 `need_generate.rule_id`,由**程序/既定生成通路**落表,而不是旁观写小说。 - -## 4. 和「生成规则要不要给副 LLM」的关系 - -问题本质不是「塞不塞那一份文档」,而是: - -- 旁观要不要能说「该按某条规则补东西」?→ **要**,就得看见规则目录/格式。 -- 旁观要不要会写角色描写?→ **不要**,所以描写长文不必进旁观上下文。 - -## 5. 上下文与思考流(UI 对接) - -主世界层游玩期走 **gm harness**(tool loop):与普通 worker 相同,经 `onThinkingDelta` / `onOutputDelta` 把推理与正文增量推到前端;机遇工具往返也在同一气泡时间线内展示,不另开隐藏通道。 - -转述、旁观仍各是一次 worker 调用,各自独立流式输出;**不要**把旁观维护当成 gm 的 tool。 - -## 6. 一句话 - -**主世界层断事;转述说话;旁观在回合末对表,改表给下一轮。** -交互上优先「一次裁决 + 延后轻维护」,不优先「来回多轮 LLM 传话」。 +无转述时:主世界正文即用户终稿,往往已够用。 diff --git a/docs/play-presentation-shells.md b/docs/play-presentation-shells.md index 71fafe6..9abe82b 100644 --- a/docs/play-presentation-shells.md +++ b/docs/play-presentation-shells.md @@ -93,8 +93,8 @@ 设计.监控栏 → 回复呈现清单(看什么) 设计.正文组成 → 壳 + 美化(怎么展示) -运行.本轮.裁决 → body 事实;suggested_actions → 行动区 -输出.用户展示 → present.v1 或纯 Markdown(→ body) +输出.用户展示(主世界正文)→ 转述提取/改写后的 body;suggested_actions → 行动区 +输出.用户展示(终稿)→ present.v1 或纯 Markdown(→ body) ``` ## 5. 游玩灌数契约 diff --git a/docs/progressive-data-design.md b/docs/progressive-data-design.md index 7640341..1b35360 100644 --- a/docs/progressive-data-design.md +++ b/docs/progressive-data-design.md @@ -94,10 +94,10 @@ Progressive 投影 本轮主世界层只看见「当前档」;未 ```text 用户输入 - → 主世界层(世界推演):读变量 + 已投影 data,交事件/改真值(按规则);可经 harness 批量调 chance 工具 + → 主世界层(世界推演):读变量 + 已投影 data,写本轮故事正文(Markdown);可经 harness 批量调 chance 工具 →(可选)角色视角:强信息隔离时出反应建议(默认关,插在主世界层之后) - →(可选)叙事转述:把裁决写成用户可见正文 - → 旁观维护(副 LLM):合同+表+本轮裁决;默认空操作(maintain.v1);改表供下一轮主世界层读取 + →(可选)叙事转述:读主世界正文,提取/改写/镶壳为用户终稿(可补残稿,不改剧情) + → 旁观维护(副 LLM):合同+表+本轮正文;默认空操作(maintain.v1);改表供下一轮主世界层读取 ``` | 内容 | 给谁 | 不要做成 | @@ -111,13 +111,13 @@ Progressive 投影 本轮主世界层只看见「当前档」;未 用户可见名:`ui-glossary.md`(主世界层、旁观维护、叙事转述…)。作者文档可称 **主世界层 / 旁观维护**,与酒馆「主 GM + auditor」同构;**不要**对用户改称整套系统为「GM 框架」而废弃「执行单元」。 -**维护路径(不是 gm 的 chance toolcall)**:主世界层输出 settlement.v1 → Runtime 合并 `variable_changes` 进 `变量.当前` → `side_effects` 换投影;旁观维护输出 maintain.v1 → Runtime 按需合并 `table_ops` / `need_generate` 程序抽样。gm harness 的 `chance` 只负责本回合裁决前的骰子/抽签。 +**维护路径(不是 gm 的 chance toolcall)**:主世界层写正文 → 旁观维护输出 maintain.v1 → Runtime 按需合并 `table_ops` / `need_generate` 程序抽样 → `side_effects` 换投影。可选正文隐藏段维护语句。遗留 settlement.v1 仍可合并(旧存档)。gm harness 的 `chance` 只负责本回合的骰子/抽签。 ## 6. 世界模拟器技能落点 | 技能 | 钉什么 | |------|--------| -| **变量设计与更新规则** | 真值;**维护语句**形状(settlement / 隐藏段);**Data 映射索引**(指向生成规则+具体实例);`side_effects` | +| **变量设计与更新规则** | 真值;**维护语句**形状(旁观 maintain / 隐藏段);**Data 映射索引**(指向生成规则+具体实例);`side_effects` | | **变量控制上下文** | 各槽视野;**旁观汇总**(当前档摘要,非整表);剧透边界 | | **生成规则 / 具体实例** | 分档表 schema 与行数据(如好感→性格);生成后由变量设计索引 | | **正文组成** | 隐藏段定界(供解析维护语句);用户可见块序 | diff --git a/skills/dialogue/world-simulator/modules/context-order/prompt.md b/skills/dialogue/world-simulator/modules/context-order/prompt.md index a280ef0..6c0ff2e 100644 --- a/skills/dialogue/world-simulator/modules/context-order/prompt.md +++ b/skills/dialogue/world-simulator/modules/context-order/prompt.md @@ -56,9 +56,9 @@ feeds: design_only 2. 禁止重写上游长文;只写顺序与投影。 3. 每轮槽:order 0=该槽固定人设;其后按「稳→对话.历史→易变」软排。 4. 主世界层必须含 ref=`对话.历史`(默认 projection=summary);转述与旁观维护默认不含历史。 -5. 主世界层应双挂「设计.叙事指南与故事推进」等 mount 到 gm 的块;有「设计.随机范围整理」则挂 gm(summary 即可);转述只读裁决+文风/壳。 -6. 旁观维护在回合末:须含「运行.本轮.裁决」与变量合同,不含历史。 -7. 越稳越靠前;本轮输入/真值/裁决通常在历史之后。 +5. 主世界层应双挂「设计.叙事指南与故事推进」等 mount 到 gm 的块;有「设计.随机范围整理」则挂 gm(summary 即可);转述只读本轮正文(输出.用户展示)+ 文风/壳。 +6. 旁观维护在回合末:须含「输出.用户展示」与变量合同,不含历史。 +7. 越稳越靠前;本轮输入/真值/正文通常在历史之后。 8. 同一内容可挂多槽。 9. summary:如 `投影排序 · 旁观+主世界层+转述`。 diff --git a/skills/dialogue/world-simulator/modules/refine/prompt.md b/skills/dialogue/world-simulator/modules/refine/prompt.md index f2dd5c2..94a69ad 100644 --- a/skills/dialogue/world-simulator/modules/refine/prompt.md +++ b/skills/dialogue/world-simulator/modules/refine/prompt.md @@ -76,8 +76,8 @@ boundary: | ```principles 1. 合并优于重写;槽位优于发明演员。 2. 面向用户的终稿点 acceptance=review(默认是 world-simulator;开了转述则是 narrator;写手是 chapter-writer);旁观/outline 常用 continue;有转述时 gm 用 continue。 -3. 真值变更写在 gm 的 outputs(运行.本轮.变量变更 / 裁决包内 variable_changes),旁观维护只出 maintain.v1;不靠第三变量 Worker。 -4. 裁决包约定:启用转述时,运行.本轮.裁决 使用 settlement.v1(见 progressive-data-design / 模板);Runtime 合并 variable_changes。无转述时主世界层直接写 输出.用户展示。 +3. 真值变更主路径:旁观维护 maintain.v1(读本轮正文改表);可选正文隐藏段维护语句。主世界层只写故事正文,不交 settlement 裁决包。 +4. 正文约定:主世界层始终写「输出.用户展示」(Markdown)。无转述时原文即终稿;有转述时转述读该正文再提取/改写/镶壳并覆盖终稿。 5. 键名稳定;未决进 open_questions。 ``` diff --git a/skills/dialogue/world-simulator/modules/reply-format/prompt.md b/skills/dialogue/world-simulator/modules/reply-format/prompt.md index 3fe6b58..f86e9a2 100644 --- a/skills/dialogue/world-simulator/modules/reply-format/prompt.md +++ b/skills/dialogue/world-simulator/modules/reply-format/prompt.md @@ -174,7 +174,7 @@ summary:`正文组成 · 适配 {shell_id} · …`。 "拼装方": "叙事转述一次写出各块|程序按块拼接|混合", "推荐灌数": "分块壳优先 present.v1;prose 可为纯 Markdown", "单轮正文篇幅": "以回复呈现的字数为准,本步不重钉", - "与裁决包关系": "转述读 settlement 再填各块;隐藏段可含 variable_changes 镜像" + "与正文关系": "转述读主世界「输出.用户展示」再填各块;可补残稿与版式,不改剧情;隐藏段可供旁观解析维护语句" }, "示例": { "性质": "验收预览|非正史|仅看壳与美化", diff --git a/skills/dialogue/world-simulator/modules/variable-design/prompt.md b/skills/dialogue/world-simulator/modules/variable-design/prompt.md index f4bb463..e652c19 100644 --- a/skills/dialogue/world-simulator/modules/variable-design/prompt.md +++ b/skills/dialogue/world-simulator/modules/variable-design/prompt.md @@ -37,7 +37,7 @@ feeds: gm 1. 数字/阶段?(好感、章节、资源…)一次性门? 2. 有没有「到了某值换一套态度/大纲」的表?(表正文可已在具体实例里) -3. 每轮模型用什么方式声明变量变了?(裁决包字段 / 隐藏段键值 / 两者) +3. 每轮如何声明变量变了?(旁观维护 maintain.v1 / 正文隐藏段键值 / 两者) 4. 旁观或主世界层是否需要一份「当前映射摘要」(如好感档→性格要点)? ``` @@ -51,14 +51,14 @@ feeds: gm - **Data 映射**:阈值→切片大表(性格档、章纲、事件池)——格式归生成规则,行归具体实例;本步建索引并指向实例/规则 id - **Progressive**:side_effects 按真值边沿投影当前切片;旁观/主世界层读投影或汇总,不整表常驻 -**维护语句**:规定模型每轮如何声明变更(供程序拆进 `变量.当前` / 裁决包 `variable_changes`)。 +**维护语句**:规定如何声明变更(供程序拆进 `变量.当前`)。主路径是旁观维护 `maintain.v1`(读本轮正文推断 table_ops);可选正文隐藏段键值。主世界层不交 settlement 裁决包。 这不是掷骰;随机数走「随机范围整理」+ 程序插入的备用数。隐藏段定界与「正文组成」对齐。 执行顺序: 1. 读美学/机制/生成规则/具体实例/正文组成;不重开体验。 2. 立真值(承重才建);写写入源与更新规则。 3. 建 Data 映射索引:每张表写清键依哪个真值、材料来自哪条 rule_id/batch、给旁观/投影用的摘要字段。 -4. 写维护语句约定:形状、必填键、与隐藏段/settlement 的关系、示例一句。 +4. 写维护语句约定:形状、必填键、与隐藏段/旁观维护的关系、示例一句。 5. 写 side_effects(once/every_edge → replace_tag 等)。 6. 自评 + 追问;输出 JSON。 @@ -71,7 +71,7 @@ summary:`变量设计 · …`(点题主真值 + 有无映射表)。 1. 承重才立真值;派生性格不落第二可写格。 2. 映射内容:具体实例生成 → 本步索引汇总 → side_effects/变量控制上下文供给旁观与主世界层。 3. 维护靠「声明 + 程序合并」,不靠模型直接改库: - - 主路径:裁决包 variable_changes 和/或正文隐藏段(正则/定界)→ Runtime 合并进 变量.当前 + - 主路径:旁观维护 maintain.v1(读本轮正文)和/或正文隐藏段(正则/定界)→ Runtime 合并进 变量.当前 - 边沿:side_effects 换投影 tag - 用户手改;chance 只出随机数,不替代维护语句 4. 维护语句必须可生成、可校验:固定键名、短值、禁止散文糊弄。 @@ -82,7 +82,7 @@ summary:`变量设计 · …`(点题主真值 + 有无映射表)。 ## probe ```probe -每轮 1~2 点:主真值;连续数字还是阶段枚举;维护走裁决包还是隐藏段还是两者;映射表是否已有具体实例可索引;旁观要不要当前档摘要。 +每轮 1~2 点:主真值;连续数字还是阶段枚举;维护走旁观 maintain / 隐藏段 / 两者;映射表是否已有具体实例可索引;旁观要不要当前档摘要。 ``` ## output @@ -127,18 +127,18 @@ summary:`变量设计 · …`(点题主真值 + 有无映射表)。 } ], "维护语句约定": { - "通道": ["settlement.variable_changes", "隐藏段", "二者都要"], - "与正文组成对齐": "隐藏段段id / 定界约定;无隐藏段则写「仅用裁决包」", + "通道": ["旁观maintain.v1", "隐藏段", "二者都要"], + "与正文组成对齐": "隐藏段段id / 定界约定;无隐藏段则写「仅旁观维护」", "语句形状": { - "格式": "JSON行|键值列表|与 settlement 字段同构", + "格式": "maintain.v1 table_ops|隐藏段键值列表", "必填键": ["key", "delta|to"], - "禁止": ["无键散文", "改未立真值"] + "禁止": ["无键散文", "改未立真值", "主世界层交 settlement JSON"] }, - "生成要求": "主世界层每轮须输出可解析维护语句(哪怕本轮无变更也给空列表)", + "生成要求": "旁观维护按本轮正文推断变更(多数轮可空);主世界层专注故事正文", "示例": [ { "key": "好感", "delta": 8, "reason": "送礼成功" } ], - "程序侧": "Runtime 合并进 变量.当前;再跑 side_effects;非 chance toolcall" + "程序侧": "Runtime 合并 maintain.v1 → 变量.当前;再跑 side_effects;非 chance toolcall" }, "side_effects": [ { @@ -208,7 +208,7 @@ summary:`变量设计 · …`(点题主真值 + 有无映射表)。 ```checklist - [ ] 真值有写入源与更新时机? - [ ] 映射索引指向规则/实例或标待补?旁观摘要键清楚? -- [ ] 维护语句可解析且与正文组成/settlement 对齐? +- [ ] 维护语句可解析且与正文组成/旁观维护对齐? - [ ] 未把分档长文做成可写真值?未用 chance 当改表? ``` diff --git a/skills/dialogue/world-simulator/modules/worker-spec/prompt.md b/skills/dialogue/world-simulator/modules/worker-spec/prompt.md index 0f1c729..a099841 100644 --- a/skills/dialogue/world-simulator/modules/worker-spec/prompt.md +++ b/skills/dialogue/world-simulator/modules/worker-spec/prompt.md @@ -38,7 +38,7 @@ boundary: | 世界模拟类常见: 1. 主世界层(推进 + 默认直接写可见正文)——要(几乎总要) -2. 叙事转述(独立文风层再改写正文)——要 / 不要(默认不要) +2. 叙事转述(读主世界正文再改写/镶壳;可补残稿)——要 / 不要(默认不要) 3. 旁观维护(有变量/表/规则要盯时才开;默认不要)——要 / 不要 4. 角色视角(仅当有强秘密、不能进主世界层时)——要 / 不要(默认不要) diff --git a/skills/dialogue/world-simulator/recipes/world-simulator/recipe.yaml b/skills/dialogue/world-simulator/recipes/world-simulator/recipe.yaml index 9985d97..3800d36 100644 --- a/skills/dialogue/world-simulator/recipes/world-simulator/recipe.yaml +++ b/skills/dialogue/world-simulator/recipes/world-simulator/recipe.yaml @@ -30,7 +30,7 @@ principles: - 有编排参数的步骤:进执行前钉齐 params(askUser 选项+其它);缺参时先问再进 design-step。 - 〔可反复〕可再追加(彻底新建);已验收 id 留在 steps。 - 舞台骨架通常都排(备案,只分详细度);代入后几乎不移动、只在一处待着则不排。实现机制对照其 meta。 - - 百科与查表归主世界层;默认可见正文也归主世界层;仅显式开转述时正文才改由转述写。 + - 百科与查表归主世界层;可见正文默认由主世界层直接写;显式开转述时转述在主世界正文上改写/镶壳。 brief: 世界模拟类:美学卡上在意的部分→挂槽写上下文→晚排序投影→收成运行规格 diff --git a/skills/dialogue/world-simulator/worker-templates/README.md b/skills/dialogue/world-simulator/worker-templates/README.md index 3d07840..9dc3093 100644 --- a/skills/dialogue/world-simulator/worker-templates/README.md +++ b/skills/dialogue/world-simulator/worker-templates/README.md @@ -25,14 +25,14 @@ Runtime 执行时读 Worker 集条目,不直接读本目录。 | 槽 | 默认 ref | 模板 | |----|----------|------| -| auditor | `auditor` | `auditor.yaml` — maintain.v1;默认空操作;无长对话史(默认关) | -| gm | `world-simulator` | `world-simulator.yaml` — 默认直接写用户可见正文;开转述时改出 settlement.v1;`play_slots.chance` 开时 harness 批量机遇工具 | -| narrator | `narrator` | `narrator.yaml` — 只读裁决包写正文(默认关) | +| auditor | `auditor` | `auditor.yaml` — maintain.v1;读本轮正文;默认空操作;无长对话史(默认关) | +| gm | `world-simulator` | `world-simulator.yaml` — 始终写 Markdown 故事正文;`play_slots.chance` 开时 harness 批量机遇工具 | +| narrator | `narrator` | `narrator.yaml` — 读主世界正文,提取/改写/镶壳(默认关) | | perspective | `role-decide` | `role-decide.yaml` — 可选知密视角 | | chance(拓扑开关) | `chance` | `play_slots.chance` 控制 gm harness;可选声明按需 `chance` ref 供显式 run_worker | 变量 / Progressive:**不是**独立 LLM 模板;见 `docs/progressive-data-design.md`(真值 + side_effects)。 -Runtime:写入 `运行.本轮.裁决` 时自动合并 `variable_changes` → `变量.当前`;写入 `运行.本轮.旁观` 时按需合并 `table_ops`。 +Runtime:写入 `运行.本轮.旁观` 时按需合并 `table_ops`;遗留 `运行.本轮.裁决` 仍可合并 `variable_changes`(兼容旧存档)。 ## 其它文件 diff --git a/skills/dialogue/world-simulator/worker-templates/auditor.yaml b/skills/dialogue/world-simulator/worker-templates/auditor.yaml index d18aec1..25a9e18 100644 --- a/skills/dialogue/world-simulator/worker-templates/auditor.yaml +++ b/skills/dialogue/world-simulator/worker-templates/auditor.yaml @@ -2,17 +2,17 @@ id: auditor label: 旁观维护 role: auxiliary duty: > - 副 LLM:回合末检查表字段与生成规则是否需要补充;默认空操作。 - 读本轮裁决 + 变量合同 + 当前表;改表结果供下一轮主世界层读取。 + 副 LLM:回合末根据本轮正文与合同,检查表字段与生成规则是否需要补充;默认空操作。 + 读本轮用户可见正文(或主世界草稿)+ 变量合同 + 当前表;改表结果供下一轮主世界层读取。 只出 maintain.v1(运行.本轮.旁观);不写用户正文、不写真相权威。 -when: 叙事转述完成后(回合末) +when: 叙事转述完成后(无转述则主世界正文写入后;回合末) suggested_context: static: - 设计.变量设计与更新规则 - 设计.生成规则 - 设计.变量控制上下文 dynamic: - - 运行.本轮.裁决 + - 输出.用户展示 - 用户.最新输入 - 变量.当前 - 上下文.旁观.状态摘要 @@ -20,7 +20,7 @@ suggested_outputs: - 运行.本轮.旁观 prompt_excerpt: | 你是旁观维护(副 LLM),不是主世界层,也不是叙事作者。 - 读合同(生成规则/变量规则/旁观摘要)、当前表,以及本轮主世界层裁决包。 + 读合同(生成规则/变量规则/旁观摘要)、当前表,以及本轮正文(输出.用户展示;相对完整上下文通常极短的一层楼新文)。 不读对话历史。表维护合并后供下一轮主世界层读取,不在本轮重裁。 「运行.本轮.旁观」必须是 JSON,schema 为 maintain.v1: { @@ -30,7 +30,7 @@ prompt_excerpt: | "notes": [] } 多数轮保持空:need_generate=false、table_ops=[]、notes=[]。 - table_ops 仅在确需改「变量.当前」等表字段时使用(op=set|delta)。 + table_ops 仅在确需改「变量.当前」等表字段时使用(op=set|delta);依据本轮正文里已发生的后果推断,勿臆造正文未支持的变更。 need_generate 仅在确需按某条 rule_id 补实例时写成 { "rule_id", "reason" }。 程序只会抽该规则里的元素池;方向池由主世界层按规则自创,不要指望抽签出标签。 禁止编造长对话续写;禁止输出用户可见正文;禁止修改未点名的无关字段。 diff --git a/skills/dialogue/world-simulator/worker-templates/narrator.yaml b/skills/dialogue/world-simulator/worker-templates/narrator.yaml index c4eed20..3084336 100644 --- a/skills/dialogue/world-simulator/worker-templates/narrator.yaml +++ b/skills/dialogue/world-simulator/worker-templates/narrator.yaml @@ -2,24 +2,25 @@ id: narrator label: 叙事转述 role: transcription duty: > - 只读「运行.本轮.裁决」结构化包 + 叙事指南/正文组成(壳由程序套用), - 组装「输出.用户展示」。只改表达,不改事实、不补裁决;不读对话历史。 -when: 主世界层裁决包已写入后 + 只读主世界层本轮正文(输出.用户展示)+ 叙事指南/正文组成(壳由程序或提示约束), + 提取场面、按文风改写、按壳镶嵌前端可读终稿。可补全残缺叙述;不改剧情事实;不读对话历史与百科长文。 +when: 主世界层本轮正文已写入后 suggested_context: static: - 设计.叙事指南与故事推进 - 设计.正文组成 - 设计.监控栏 dynamic: - - 运行.本轮.裁决 + - 输出.用户展示 suggested_outputs: - 输出.用户展示 presentation_hints: mode: present.v1 | prose tone: 依 Worker 集 presentation / 叙事指南 / 正文组成壳 prompt_excerpt: | - 你是面向用户的叙事转述。输入是结构化裁决包(settlement.v1),不是自由发挥的世界真相。 - 不读对话历史;连续性来自裁决包内的 visible_now / resolved / npc_moves。 + 你是面向用户的叙事转述。输入是主世界层已写出的本轮正文,不是结构化裁决包,也不是自由发挥的世界真相。 + 不读对话历史与百科;连续性与事实以主世界正文为准。正文残缺时:可补描写、衔接与版式,但不得新增/改写剧情事实、角色未给出的行动或对话。 + 工作顺序:① 从主世界正文提取用户应看见的场面与信息;② 按叙事指南/模仿要点改遣词造句与节奏(不改情节);③ 按「设计.正文组成」壳写出终稿。 先读「设计.正文组成」的呈现壳 shell_id(prose|chat_monitor|spotlight|turn_panel|split_board|choice_dock|chapter_reader): - prose:可直接输出 Markdown 正文(程序会落入 body)。 - 其它壳:优先输出 JSON,schema 为 present.v1: @@ -38,9 +39,9 @@ prompt_excerpt: | monitor 用短键值(芯片),勿写长文。只填该壳已有区域。 用户主读正文(body)默认 1000~2000 字(汉字);以「设计.监控栏」回复呈现已钉字数为准。 采用完整长自然段叙述,不要单句成段;不要几句短信就结束。 - 用户输入用法以「设计.叙事指南与故事推进」为准。常见两套: - - 大纲扩写:输入当大纲扩写为事实,细化后写正文;不超出大纲未涉及的剧情,完整演绎大纲。 - - 扮演停笔:把用户本轮行为与对话作正文开头并扩写填满;不可写用户角色未给出的其它行为/对话;不可替用户角色做决定;正文停在其他角色等用户角色回应之刻。 + 用户输入用法以「设计.叙事指南与故事推进」为准: + - 大纲扩写:把主世界正文当事实底稿,细化文风后写完整终稿;不超出底稿未涉及的剧情;一次成篇,勿停等用户行动。 + - 扮演停笔:仅当叙事指南明确钉了此用法时——终稿停在其他角色等用户角色回应之刻;不可写用户角色未给出的其它行为/对话。 + - 未钉叙事指南时:按大纲扩写,禁止默认扮演停笔。 禁止在正文里写出 tag 名(「输出.用户展示」)、压缩摘要或过程日志。 - 禁止写出 do_not_say;禁止编造包中没有的事实。 - 若裁决含 tone_hint / npc_moves / suggested_actions,须体现在 body 或 meta。 + 禁止编造主世界正文中没有的事实。 diff --git a/skills/dialogue/world-simulator/worker-templates/world-simulator.yaml b/skills/dialogue/world-simulator/worker-templates/world-simulator.yaml index 74b5433..c239d22 100644 --- a/skills/dialogue/world-simulator/worker-templates/world-simulator.yaml +++ b/skills/dialogue/world-simulator/worker-templates/world-simulator.yaml @@ -3,7 +3,7 @@ label: 主世界层 role: core duty: > 读用户输入、真值与 Progressive 投影并推进世界。 - 默认无独立转述:直接写用户可见正文(输出.用户展示);可选开转述时改出 settlement.v1 裁决包。 + 直接写出本轮故事正文(Markdown);无转述时原文即终稿,有转述时正文仍由本层先交,转述再提取/改写/镶壳。 when: 每轮用户输入后上场 suggested_context: static: @@ -21,16 +21,17 @@ suggested_context: - 大纲.当前章 suggested_outputs: - 输出.用户展示 - - 运行.本轮.裁决 - 运行.事件流 - - 运行.本轮.变量变更 prompt_excerpt: | 你是主世界层。读真值、已投影上下文、对话历史与用户本轮输入。 - 默认无独立转述层:把本轮场面直接写成用户可见正文(present.v1 或 Markdown),原文即终稿; - 用户输入用法以「设计.叙事指南与故事推进」为准(扮演停笔则把用户本轮行为/对话作正文开头并扩写填满)。 - 若本局启用了叙事转述:则改为输出 settlement.v1 裁决包(player_action、resolved[]、visible_now、npc_moves[]、variable_changes[]、do_not_say[]、tone_hint),正文交给转述。 - variable_changes 由 Runtime 合并进变量.当前——若写裁决包请写清 key 与 to/delta。 - 若叙事指南为「扮演停笔」:只推进用户本轮已声明的行为/对话;禁止替用户角色发明其它行动或做决定;可推进到需要用户角色回应之刻即停。 + 直接写出本轮故事正文(优先纯 Markdown / 自然语言叙述)。重心在故事如何推进、人物如何扮演——不要为程序交 JSON 裁决包。 + 无独立转述时:你写的正文就是用户终稿。 + 若本局启用了叙事转述:你仍写完整(或尽量完整)的场面正文;文风润色、残稿补全、前端壳由转述处理。不要改成「只交事实包」。 + 真值/表字段变更不靠你交结构化包:有旁观维护时由副 LLM 根据本轮正文改表;若正文组成钉了隐藏段维护语句,可按约定写短键值,否则不要为维护打断叙述。 + 用户输入用法以「设计.叙事指南与故事推进」为准: + - 钉了「大纲扩写」:本轮输入=待扩写大纲/场面要点;一次交完整篇正文,勿停在等用户行动处。 + - 钉了「扮演停笔」:才只推进用户已声明行为/对话,并停在需用户回应之刻。 + - 未钉叙事指南时:按大纲扩写处理,禁止默认扮演停笔。 需要随机数时:只用本轮上下文里程序已插入的备用随机数(见「设计.随机范围整理」);禁止口算编造。 不要把未投影的分档长文整表塞进输出;只写本轮可见后果。 禁止在正文里写出 tag 名、压缩摘要或过程日志。 diff --git a/src/runtime/phase-runtime.ts b/src/runtime/phase-runtime.ts index d1ef9e9..227205a 100644 --- a/src/runtime/phase-runtime.ts +++ b/src/runtime/phase-runtime.ts @@ -152,6 +152,7 @@ import { } from "../skills/play-turn.js"; import { PRESENT_TAG, + isPlayFinalVisibleWorker, isPlayPresentWorker, parseShellAdaptationFromReplyFormat, presentOutputFromFallback, @@ -1253,7 +1254,7 @@ export class PhaseRuntime { } /** - * 游玩层不走创作 HITL:转述必须交出用户可见正文,其它执行单元空产物也继续。 + * 游玩层不走创作 HITL:终稿槽必须交出用户可见正文,其它执行单元空产物也继续。 */ private settlePlayWorkerResult( workerId: string, @@ -1261,15 +1262,14 @@ export class PhaseRuntime { ): import("../worker/executor.js").WorkerRunResult { if (!isPlayLayerActive(this.session.slots)) return result; const outputs = { ...result.outputs }; + const narratorEnabled = isNarratorEnabled( + parseWorkerSetYaml( + this.blackboard.getContentByTag("设计.worker集") ?? "", + )?.play_slots, + ); if ( Object.keys(outputs).length === 0 && - isPlayPresentWorker(workerId, { - narratorEnabled: isNarratorEnabled( - parseWorkerSetYaml( - this.blackboard.getContentByTag("设计.worker集") ?? "", - )?.play_slots, - ), - }) + isPlayFinalVisibleWorker(workerId, { narratorEnabled }) ) { const shell = parseShellAdaptationFromReplyFormat( @@ -1279,10 +1279,12 @@ export class PhaseRuntime { const preview = result.preview?.trim() ?? ""; const looksAsk = /可验收|追问|请再补|askUser/.test(preview) || /^\s*-\s/.test(preview); - outputs[PRESENT_TAG] = presentOutputFromFallback( - looksAsk ? "" : preview, - shell, - ); + // 主世界终稿用纯正文;转述仍可落 present 包 + outputs[PRESENT_TAG] = isPlayPresentWorker(workerId) + ? presentOutputFromFallback(looksAsk ? "" : preview, shell) + : looksAsk + ? "(本轮场面未写完)" + : preview || "(本轮场面未写完)"; } return { ...result, @@ -2578,7 +2580,18 @@ export class PhaseRuntime { inferLifecycleStage(this.session) === "play" && (tag === "输出.用户展示" || tag === "输出.开场白") ) { - this.appendDialogueHistory("助手", toWrite); + const narratorEnabled = isNarratorEnabled( + parseWorkerSetYaml( + this.blackboard.getContentByTag("设计.worker集") ?? "", + )?.play_slots, + ); + // 有转述时:主世界草稿不进对话史,只记终稿(转述);开场白始终记 + if ( + tag === "输出.开场白" || + isPlayFinalVisibleWorker(workerId, { narratorEnabled }) + ) { + this.appendDialogueHistory("助手", toWrite); + } } if (nextDoc) { this.applyTableSideEffects(prevDoc, nextDoc, workerId); @@ -2586,7 +2599,7 @@ export class PhaseRuntime { if (tag === OPENING_CURRENT_VARS_TAG || tag === "变量.当前") { this.applyValueMapProjections(workerId); } - // 裁决包 / 旁观维护包 → 合并进变量.当前(可触发 side_effects) + // 遗留裁决包 / 旁观维护包 → 合并进变量.当前(可触发 side_effects) if (tag === SETTLEMENT_TAG) { this.applySettlementVariableChanges(toWrite, workerId); } else if (tag === MAINTAIN_TAG) { diff --git a/src/skills/context-order.ts b/src/skills/context-order.ts index 5d18ed9..d4a3deb 100644 --- a/src/skills/context-order.ts +++ b/src/skills/context-order.ts @@ -498,7 +498,7 @@ const SYNTH_FALLBACK_CONTEXT: Record< "设计.变量控制上下文", ], dynamic: [ - "运行.本轮.裁决", + "输出.用户展示", "用户.最新输入", "变量.当前", "上下文.旁观.状态摘要", @@ -528,12 +528,12 @@ const SYNTH_FALLBACK_CONTEXT: Record< "设计.正文组成", "设计.监控栏", ], - dynamic: ["运行.本轮.裁决"], + dynamic: ["输出.用户展示"], skipHistory: true, }, "role-decide": { static: [], - dynamic: ["用户.最新输入", "运行.本轮.裁决"], + dynamic: ["用户.最新输入", "输出.用户展示"], }, }; diff --git a/src/skills/declared-worker.ts b/src/skills/declared-worker.ts index ab97ed5..540f9f7 100644 --- a/src/skills/declared-worker.ts +++ b/src/skills/declared-worker.ts @@ -32,7 +32,6 @@ import { } from "./context-order.js"; import type { ContextSegmentDef } from "./context-segments.js"; import { PLAY_VISIBLE_BODY_INSTRUCTION } from "./present-packet.js"; -import { isNarratorEnabled } from "./play-slots.js"; type WorkerTemplateDoc = { id?: string; @@ -223,7 +222,7 @@ export function buildDeclaredWorkerSkill(params: { const visibleBodyRule = id === "narrator" || id === "round-present" || - (id === "world-simulator" && !isNarratorEnabled(params.workerSet?.play_slots)) + id === "world-simulator" ? PLAY_VISIBLE_BODY_INSTRUCTION : ""; const excerptWithRule = [excerpt, visibleBodyRule].filter(Boolean).join("\n\n"); diff --git a/src/skills/gm-tools.ts b/src/skills/gm-tools.ts index c6d0725..31a4843 100644 --- a/src/skills/gm-tools.ts +++ b/src/skills/gm-tools.ts @@ -114,6 +114,6 @@ export const GM_CHANCE_HARNESS_INSTRUCTION = ` ## 机遇工具(游玩期) 需要真随机(掷骰/比点/抽签/加权抽取)时:调用工具 \`chance\`,在 \`requests\` 数组中**一次性**列出本步全部随机需求(每项必须有唯一 \`id\`)。 -禁止口算或编造随机结果。收到工具结果后,再在 settlement 的 \`resolved\` 等处引用对应 \`id\` 写出后果。 -无随机需求时:直接输出 settlement.v1 JSON,不要调用工具。 +禁止口算或编造随机结果。收到工具结果后,在本轮 **Markdown 正文**里演绎对应 \`id\` 的后果(写进场面,不要交 JSON 裁决包)。 +无随机需求时:直接输出故事正文,不要调用工具。 `; diff --git a/src/skills/play-slots.ts b/src/skills/play-slots.ts index f8610d6..99d1196 100644 --- a/src/skills/play-slots.ts +++ b/src/skills/play-slots.ts @@ -54,12 +54,13 @@ export const PLAY_SLOT_META: Record< gm: { label: "主世界层", purpose: - "读真值与 Progressive 投影并推进世界;无独立转述时直接写用户可见正文,有转述时出裁决包。", + "读真值与 Progressive 投影并推进世界;直接写本轮故事正文(Markdown);无转述时原文即终稿。", defaultAcceptance: "review", }, narrator: { label: "叙事转述", - purpose: "只读裁决包 + 文风常驻,输出用户可见正文(可选;默认关)。", + purpose: + "读主世界正文,提取/改写/镶壳为用户终稿;可补残稿,不改剧情(可选;默认关)。", defaultAcceptance: "review", }, perspective: { @@ -191,37 +192,39 @@ export function workerEntryForSlot( ): WorkerSetEntry { const meta = PLAY_SLOT_META[id]; const ref = refForSlot(slots, id); - const soloGm = id === "gm" && !isNarratorEnabled(slots); + const withNarrator = isNarratorEnabled(slots); + const soloGm = id === "gm" && !withNarrator; return { ref, name: meta.label, role: id, - duty: soloGm - ? "读真值与投影,推进世界,并直接写出用户可见正文(无独立转述层,原文即终稿)。" - : meta.purpose, - when: + duty: id === "gm" ? soloGm - ? "每轮用户输入后上场;直接交用户可见正文" - : "每轮用户输入后首先上场(主 LLM / 裁决权威)" + ? "读真值与投影,推进世界,并直接写出用户可见正文(无独立转述层,原文即终稿)。" + : "读真值与投影,推进世界,写出本轮故事正文(Markdown);文风润色与壳交给转述。" + : meta.purpose, + when: + id === "gm" + ? "每轮用户输入后上场;直接交故事正文" : id === "perspective" ? "强信息隔离且本轮需要该角色独立反应时(主世界层之后)" : id === "narrator" - ? "主世界层裁决包就绪后" - : "转述完成后;表维护合并进变量.当前,供下一轮主世界层读取", + ? "主世界层本轮正文就绪后" + : "终稿写入后;表维护合并进变量.当前,供下一轮主世界层读取", rationale: id === "gm" ? soloGm ? "默认少槽:主世界层兼呈现,避免转述再改写一遍" - : "有独立转述时:主世界层出裁决,正文交给转述" + : "有独立转述时:主世界专注剧情与扮演;转述再提取/改写/镶壳" : id === "perspective" ? "知密内容不能进主世界层上下文" : id === "narrator" - ? "用户可见正文与裁决分离,避免文风与规则互相挤压" + ? "文风与前端壳与世界推演分离,避免互相挤压" : "表与规则补充延后执行,改表结果作用于下一轮而非本轮重裁", acceptance: - id === "gm" && isNarratorEnabled(slots) ? "continue" : meta.defaultAcceptance, - outputs: soloGm ? ["输出.用户展示"] : undefined, + id === "gm" && withNarrator ? "continue" : meta.defaultAcceptance, + outputs: id === "gm" ? ["输出.用户展示"] : undefined, invocation: "turn", }; } diff --git a/src/skills/present-packet.ts b/src/skills/present-packet.ts index 3bfb6f0..85d03c1 100644 --- a/src/skills/present-packet.ts +++ b/src/skills/present-packet.ts @@ -192,23 +192,40 @@ export function stripPresentSourceFences(raw: string): string { } export const PLAY_VISIBLE_BODY_INSTRUCTION = [ - "用户可见正文(present.v1 的 blocks.body,或纯 Markdown 主读)默认 **1000~2000 字**(按汉字计);以「设计.监控栏/回复呈现」已钉字数为准。", + "用户可见正文(主世界层:纯 Markdown;转述:present.v1 的 blocks.body,或纯 Markdown 主读)默认 **1000~2000 字**(按汉字计);以「设计.监控栏/回复呈现」已钉字数为准。", "采用完整长自然段叙述,不要单句成段。", "不要写成几句气泡短信就结束,除非用户明确要求极短。", "禁止把 tag 名(如「输出.用户展示」)、压缩摘要、过程日志写进正文。", ].join("\n"); -/** 游玩期把用户可见终稿写出的执行单元(有转述用转述;无则主世界层直接交原文) */ -export function isPlayPresentWorker( +/** + * 游玩期走 present.v1 结构化投递的执行单元(转述 / 回合陈述)。 + * 主世界层不在此列:始终自由写 Markdown 正文。 + */ +export function isPlayPresentWorker(workerId: string): boolean { + const id = workerId.trim(); + return id === "narrator" || id === "round-present"; +} + +/** + * 本轮最终用户可见正文的执行单元(写入对话.历史 / 空产物兜底)。 + * 无转述:主世界层;有转述:转述(主世界草稿可被覆盖,不进历史)。 + */ +export function isPlayFinalVisibleWorker( workerId: string, opts?: { narratorEnabled?: boolean }, ): boolean { const id = workerId.trim(); if (id === "narrator" || id === "round-present") return true; - if (id === "world-simulator" && opts?.narratorEnabled === false) return true; + if (id === "world-simulator" && opts?.narratorEnabled !== true) return true; return false; } +/** 游玩主世界层:自由 Markdown 正文(程序可事后落入 present 壳,模型不交 JSON) */ +export function isPlayGmBodyWorker(workerId: string): boolean { + return workerId.trim() === "world-simulator"; +} + /** 按能力探测走 json_schema / forced_tool 时用的 present.v1 形状(全 required,可 strict) */ export const PRESENT_JSON_SCHEMA: Record = { type: "object", @@ -243,21 +260,35 @@ export const PRESENT_JSON_SCHEMA: Record = { }, }; +/** 游玩主世界层:直接写 Markdown,禁止 JSON 裁决包 */ +export function playGmBodyOutputInstruction(): string { + return ` + +--- + +## 运行时输出协议(主世界正文) + +直接输出本轮 **Markdown / 自然语言故事正文**(写入「输出.用户展示」)。 +不要输出 JSON、不要交 settlement/裁决包、不要套 present.v1 外壳。 +重心在场面推进与人物扮演;残稿也可以交,有转述时会再处理。 +不要写追问、askUser、自评或「可验收产物」。`; +} + /** 游玩转述:按探测到的 structured 投递写出 present.v1,不要创作追问 */ export function playPresentOutputInstruction(shell: PresentShellId): string { return ` --- -## 运行时输出协议(游玩正文) +## 运行时输出协议(游玩终稿) -本步产物是给用户看的终稿。程序按已探测的模型能力用 schema / tool / json_object 投递,你输出 **一个 present.v1 对象**: +本步读主世界层已写正文,产出给用户看的终稿。程序按已探测的模型能力用 schema / tool / json_object 投递,你输出 **一个 present.v1 对象**: {"schema":"present.v1","shell":"${shell}","blocks":{"body":"用户可读正文","monitor":"","header":"","footer":"","aside":""},"meta":{"suggested_actions":[]}} \`shell\` 必须是 \`${shell}\`。只填该壳已有区域,没有的键留空字符串。 prose 壳也可直接输出 Markdown,程序会落入 body。 -材料不够也先写一版场面。不要写追问、askUser、自评或「可验收产物」。`; +可补全残缺叙述与版式,但不得改剧情事实。不要写追问、askUser、自评或「可验收产物」。`; } export function stringifyPresentPacket(packet: PresentPacket): string { diff --git a/src/skills/settlement-packet.ts b/src/skills/settlement-packet.ts index 15990f8..f02d5d8 100644 --- a/src/skills/settlement-packet.ts +++ b/src/skills/settlement-packet.ts @@ -1,6 +1,7 @@ /** - * 主世界层 → 叙事转述的裁决包(settlement / direction packet)。 - * 存黑板 tag:运行.本轮.裁决 + * 遗留:settlement.v1 裁决包(运行.本轮.裁决)。 + * 现行游玩管线:主世界层直接写 Markdown 正文;转述读正文;旁观维护读正文出 maintain.v1。 + * 仍保留解析/合并,兼容旧存档或手工写入的 variable_changes。 */ import { createTableFromValues, diff --git a/src/skills/worker-set-view.ts b/src/skills/worker-set-view.ts index 344efa8..9d54f1b 100644 --- a/src/skills/worker-set-view.ts +++ b/src/skills/worker-set-view.ts @@ -221,6 +221,7 @@ const DEFAULT_WORKER_CONTRACTS: Record< "设计.变量控制上下文", ], dynamic: [ + "输出.用户展示", "用户.最新输入", "变量.当前", "上下文.旁观.状态摘要", @@ -249,12 +250,12 @@ const DEFAULT_WORKER_CONTRACTS: Record< "运行.本轮.旁观", ], }, - outputs: ["输出.用户展示", "运行.本轮.裁决", "运行.事件流", "运行.本轮.变量变更"], + outputs: ["输出.用户展示", "运行.事件流"], }, "variable-update": { context: { static: ["设计.变量设计与更新规则"], - dynamic: ["运行.本轮.裁决", "变量.当前"], + dynamic: ["输出.用户展示", "变量.当前"], }, outputs: ["运行.本轮.变量变更", "变量.当前"], }, @@ -267,7 +268,7 @@ const DEFAULT_WORKER_CONTRACTS: Record< "设计.正文组成", "设计.监控栏", ], - dynamic: ["运行.本轮.裁决", "用户.最新输入", "变量.当前"], + dynamic: ["输出.用户展示", "用户.最新输入", "变量.当前"], }, outputs: ["输出.用户展示"], }, diff --git a/src/worker/executor.ts b/src/worker/executor.ts index 3fc5de9..df28c57 100644 --- a/src/worker/executor.ts +++ b/src/worker/executor.ts @@ -21,17 +21,15 @@ import { } from "../parse/json-doc.js"; import { assembleWorkerContext } from "../skills/context-segments.js"; import { isPlayLayerActive } from "../skills/play-turn.js"; -import { loadAppSettings } from "../config/settings.js"; -import { resolveActivePreset } from "../preset/store.js"; -import { assemblePlayWorkerMessages } from "../preset/play-frame.js"; -import { worldInfoPackFromSegments } from "../preset/world-info-pack.js"; import { PRESENT_TAG, PRESENT_JSON_SCHEMA, + isPlayGmBodyWorker, isPlayPresentWorker, isPresentLikeObject, parsePresentPacket, parseShellAdaptationFromReplyFormat, + playGmBodyOutputInstruction, playPresentOutputInstruction, presentOutputFromFallback, stringifyPresentPacket, @@ -46,7 +44,6 @@ import { GM_CHANCE_HARNESS_INSTRUCTION, } from "../skills/gm-tools.js"; import { runGmHarness } from "./gm-harness.js"; -import { isNarratorEnabled } from "../skills/play-slots.js"; import { mergeQuestionsPreferFragment, normalizeQuestions, @@ -397,10 +394,15 @@ function lookupOutputValue( function parseWorkerResponse( raw: string, outputTags: string[], - opts?: { fallbackShell?: PresentShellId }, + opts?: { fallbackShell?: PresentShellId; preferPlainBody?: boolean }, ): WorkerRunResult { const fallbackShell = opts?.fallbackShell ?? "prose"; + const preferPlainBody = opts?.preferPlainBody === true; const playTarget = playVisibleTarget(outputTags); + const packBody = (body: string) => + preferPlainBody + ? body.trim() || "(本轮场面未写完)" + : presentOutputFromFallback(body, fallbackShell); const parsed = tryParseJsonDoc(raw); if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) { @@ -411,10 +413,10 @@ function parseWorkerResponse( ? raw : ""; return finalizeParsedOutputs( - { [playTarget]: presentOutputFromFallback(body, fallbackShell) }, + { [playTarget]: packBody(body) }, { summary: body.slice(0, 80) || "游玩正文" }, outputTags, - { playVisible: true, fallbackShell }, + { playVisible: true, fallbackShell, preferPlainBody }, ); } if (target && looksLikeStructuredDraft(raw)) { @@ -449,7 +451,7 @@ function parseWorkerResponse( const obj = parsed as Record; - if (playTarget && isPresentLikeObject(obj)) { + if (playTarget && isPresentLikeObject(obj) && !preferPlainBody) { const present = canonicalizePresentOutput(obj, fallbackShell); if (present) { return finalizeParsedOutputs( @@ -473,15 +475,32 @@ function parseWorkerResponse( } } - // 游玩正文:片段里的场面字收成 present,不要扁形成创作稿 + // 游玩正文:片段里的场面字收成 present(或主世界纯正文) if (playTarget && !isPresentLikeObject(obj)) { const scene = fragmentSceneText(obj); if (scene) { return finalizeParsedOutputs( - { [playTarget]: presentOutputFromFallback(scene, fallbackShell) }, + { [playTarget]: packBody(scene) }, obj, outputTags, - { playVisible: true, fallbackShell }, + { playVisible: true, fallbackShell, preferPlainBody }, + ); + } + } + + // 主世界偶发 JSON:尽量抽出可读字符串当正文 + if (playTarget && preferPlainBody) { + const scene = + fragmentSceneText(obj) || + (typeof obj.body === "string" ? obj.body.trim() : "") || + (typeof obj.visible_now === "string" ? obj.visible_now.trim() : "") || + raw.trim(); + if (scene) { + return finalizeParsedOutputs( + { [playTarget]: packBody(scene) }, + obj, + outputTags, + { playVisible: true, fallbackShell, preferPlainBody }, ); } } @@ -510,6 +529,16 @@ function parseWorkerResponse( for (const tag of productOutputTags(outputTags)) { const val = lookupOutputValue(bag, tag); if (playTarget && tag === playTarget) { + if (preferPlainBody) { + const scene = + (typeof val === "string" ? val.trim() : "") || + fragmentSceneText(val) || + ""; + if (scene) { + outputs[tag] = packBody(scene); + continue; + } + } const present = canonicalizePresentOutput(val, fallbackShell); if (present) { outputs[tag] = present; @@ -526,7 +555,7 @@ function parseWorkerResponse( } recoverFragmentOutputs(obj, outputs, outputTags, fallbackShell); - if (playTarget && outputs[playTarget]) { + if (playTarget && outputs[playTarget] && !preferPlainBody) { const present = canonicalizePresentOutput(outputs[playTarget], fallbackShell); if (present) outputs[playTarget] = present; } @@ -534,6 +563,7 @@ function parseWorkerResponse( return finalizeParsedOutputs(outputs, obj, outputTags, { playVisible: Boolean(playTarget), fallbackShell, + preferPlainBody, rawFallback: raw, }); } @@ -545,6 +575,7 @@ function finalizeParsedOutputs( opts?: { playVisible?: boolean; fallbackShell?: PresentShellId; + preferPlainBody?: boolean; rawFallback?: string; }, ): WorkerRunResult { @@ -552,6 +583,11 @@ function finalizeParsedOutputs( const playTarget = playVisibleTarget(outputTags); const playVisible = Boolean(opts?.playVisible || playTarget); const fallbackShell = opts?.fallbackShell ?? "prose"; + const preferPlainBody = opts?.preferPlainBody === true; + const packBody = (body: string) => + preferPlainBody + ? body.trim() || "(本轮场面未写完)" + : presentOutputFromFallback(body, fallbackShell); let askUser: QuestionItem[] | undefined; if (!playVisible) { @@ -615,7 +651,7 @@ function finalizeParsedOutputs( scene || summaryText || usablePlayFallbackText(opts?.rawFallback); - outputs[playTarget] = presentOutputFromFallback(fallback, fallbackShell); + outputs[playTarget] = packBody(fallback); } if (Object.keys(outputs).length === 0 && (!askUser || askUser.length === 0)) { @@ -782,54 +818,43 @@ export async function runWorkerSkill(params: WorkerRunParams): Promise m.role === "system"); @@ -841,8 +866,10 @@ export async function runWorkerSkill(params: WorkerRunParams): Promise { template: { id: "world-simulator", label: "主世界层", - duty: "裁决", + duty: "写正文", suggested_context: { static: ["设计.worker集"], dynamic: ["用户.最新输入"] }, }, workerSet: parsed, }); - expect(worker.contextSegments?.some((s) => s.inline?.includes("裁决"))).toBe( - true, - ); + expect( + worker.contextSegments?.some((s) => s.inline?.includes("正文")), + ).toBe(true); expect( worker.contextSegments?.some( (s) => s.tier === "dynamic" && s.tags.includes("用户.最新输入"), diff --git a/tests/present-packet.test.ts b/tests/present-packet.test.ts index d1fa117..bd39e9c 100644 --- a/tests/present-packet.test.ts +++ b/tests/present-packet.test.ts @@ -5,6 +5,8 @@ import { parseShellAdaptationFromReplyFormat, presentFromPlainText, presentOutputFromFallback, + isPlayFinalVisibleWorker, + isPlayGmBodyWorker, isPlayPresentWorker, shellDefaultRegions, } from "../src/skills/present-packet.js"; @@ -118,16 +120,24 @@ describe("shell adaptation from reply-format", () => { }); describe("play present worker", () => { - it("recognizes narrator, round-present, and solo gm", () => { + it("structured present only for narrator / round-present", () => { expect(isPlayPresentWorker("narrator")).toBe(true); expect(isPlayPresentWorker("round-present")).toBe(true); expect(isPlayPresentWorker("world-simulator")).toBe(false); + }); + + it("gm body worker and final visible roles", () => { + expect(isPlayGmBodyWorker("world-simulator")).toBe(true); + expect(isPlayGmBodyWorker("narrator")).toBe(false); expect( - isPlayPresentWorker("world-simulator", { narratorEnabled: false }), + isPlayFinalVisibleWorker("world-simulator", { narratorEnabled: false }), ).toBe(true); expect( - isPlayPresentWorker("world-simulator", { narratorEnabled: true }), + isPlayFinalVisibleWorker("world-simulator", { narratorEnabled: true }), ).toBe(false); + expect( + isPlayFinalVisibleWorker("narrator", { narratorEnabled: true }), + ).toBe(true); }); it("fallback packet uses the given shell", () => { diff --git a/tests/worker-set-view.test.ts b/tests/worker-set-view.test.ts index ff7e78d..42a7138 100644 --- a/tests/worker-set-view.test.ts +++ b/tests/worker-set-view.test.ts @@ -18,7 +18,7 @@ workers: - 用户.最新输入 - 运行.事件流 outputs: - - 运行.本轮.裁决 + - 输出.用户展示 - 运行.事件流 - ref: narrator duty: 组装用户可见回复 @@ -30,14 +30,14 @@ workers: static: - 叙事.指南.确认稿 dynamic: - - 运行.事件流 + - 输出.用户展示 outputs: - 输出.用户展示 instantiate_hints: invoke: [opening-generator] tag_flow: - - "用户.最新输入 → 运行.本轮.裁决" - - "运行.本轮.裁决 → 输出.用户展示" + - "用户.最新输入 → 输出.用户展示" + - "输出.用户展示 → 输出.用户展示(转述终稿)" `; describe("formatWorkerSetForUser", () => { @@ -62,7 +62,7 @@ describe("formatWorkerSetForUser", () => { false, ); expect(view.workers[0].context.staticTags[0].tag).toBe("设计.worker集"); - expect(view.workers[0].writes).toEqual(["运行.本轮.裁决", "运行.事件流"]); + expect(view.workers[0].writes).toEqual(["输出.用户展示", "运行.事件流"]); expect(view.workers[0].context.explicit).toBe(true); expect(view.workers[1].presentation?.[0].label).toBe("语气"); expect(view.tagFlow).toHaveLength(2);