diff --git a/docs/briefs/capability-authoring-brief.md b/docs/briefs/capability-authoring-brief.md index e6fdae7..a3b8926 100644 --- a/docs/briefs/capability-authoring-brief.md +++ b/docs/briefs/capability-authoring-brief.md @@ -376,7 +376,7 @@ task 必须写:禁止重复同一开场;在首答上补洞。 "稳变": "stable", "正文": { }, "自评": { - "维度": [{ "名": "…", "分数": 0, "说明": "…" }], + "维度": [{ "名": "…", "分数": 8, "说明": "…" }], "薄弱点": "…" }, "追问": { @@ -396,7 +396,7 @@ task 必须写:禁止重复同一开场;在首答上补洞。 | 段 | 说明 | |----|------| | `正文` | **产物主体**。技能自定内部结构(如美学步=设定逻辑 + 交互范式 + 美学纲领);须含「详细各个方面」。禁止整份只交散文。 | -| `自评` | **自评评分**。`维度[]` 名目按技能定(美学步常用:交互范式 / 美学纲领 / 整体协调);可含 `薄弱点`。 | +| `自评` | **自评评分**。`维度[]` 名目按技能定(美学步常用:交互范式 / 美学纲领 / 整体协调);**`分数` 为 0–10 十分制**(可一位小数;高质约 8–10);可含 `薄弱点`。禁止用 0–100 百分数。 | | `追问` | **示例 + 建议选项**。给用户的下一问;`题目` 可空数组。与程序 askUser 可并存,但产物里要留结构化题面。 | | 规则 | 说明 | @@ -404,10 +404,11 @@ task 必须写:禁止重复同一开场;在首答上补洞。 | `schema` | 必须原样写出,供前端友好渲染 | | `mount` | 挂哪些槽/ref;**不要**写最终数字 `order` | | `稳变` | `stable` \| `semi` \| `volatile`,供「上下文投影排序」 | +| **视图** | 产出形状固定后,该技能须有对应**专用正文卡**(前端按 `技能` / 正文键分流);无专用卡时退回通用结构化。作者交付物=`prompt.md`(含稳定 output)+ 可验收的阅读视图(实现或在清单中登记待做)。 | | 收成例外 | 游玩拓扑 / 上下文投影排序 / 细化终稿用自有 schema,不套本头 | 范例对齐:`modules/aesthetics-interaction/prompt.md`(output 块)。 - +前端已落地专用卡示例:生成规则 / 舞台骨架 / 实现机制 / 美学 mosaic;自评条显示为 `x/10`。 --- # 第五部分:撰写任一能力时的工作步骤 diff --git a/docs/briefs/prompt-revision-inventory.md b/docs/briefs/prompt-revision-inventory.md index 2fd3005..d1382a4 100644 --- a/docs/briefs/prompt-revision-inventory.md +++ b/docs/briefs/prompt-revision-inventory.md @@ -17,7 +17,7 @@ | `modules/context-order/prompt.md` | **骨架已写**(`context-order.v1`) | | `docs/context-fragment-design.md` | **已写**;范例技能已对齐 `context-fragment.v1` | | 程序:片段解析 / 扁平投影序 / `对话.历史` 可投影 | **已落地**(非硬双锚) | -| `orchestrator.md` | **仍待**:play 调度顺序 perspective? → gm → narrator | +| `orchestrator.md` | **已改**:play 调度 auditor → perspective? → gm → narrator | ## 优先级 P1(能力归属 feeds) @@ -56,7 +56,7 @@ ```json // 设计.worker集(世界模拟) { - "play_slots": { "gm": true, "narrator": true, "perspective": false }, + "play_slots": { "auditor": true, "gm": true, "narrator": true, "perspective": false }, "tables": { "side_effects": [/* 来自变量设计 */] }, "workers": [/* 可空,由程序按槽展开;或覆盖 context/outputs */] } @@ -76,6 +76,16 @@ } ``` +```json +// 运行.本轮.旁观(多数轮空) +{ + "schema": "maintain.v1", + "need_generate": false, + "table_ops": [], + "notes": [] +} +``` + ## 建议你改的顺序 1. `refine` + `worker-spec`(或新拓扑技能)对齐 `play_slots` diff --git a/docs/context-fragment-design.md b/docs/context-fragment-design.md index 2b5df38..1b95e47 100644 --- a/docs/context-fragment-design.md +++ b/docs/context-fragment-design.md @@ -21,11 +21,13 @@ | 槽键 | 默认 ref | 用户可见 | 默认 | |------|----------|----------|------| +| `auditor` | `auditor` | 旁观维护 | 开 | | `gm` | `world-simulator` | 主世界层 | 开 | | `narrator` | `narrator` | 叙事转述 | 开 | | `perspective` | `role-decide` | 角色视角 | 关 | -推荐游玩调度:`perspective? → gm → narrator`。 +推荐游玩调度:`auditor → perspective? → gm → narrator`。 +旁观维护默认**不注入**「对话.历史」;主世界层 / 转述按投影排序表裁剪历史。 ## 3. 上下文片段:`context-fragment.v1` @@ -45,7 +47,7 @@ 生成规则正文:必要性判断 + `rules[]`(生成与描写 + **单层**产物格式 + 可选多池绑顶层字段);自评=必要性/属性妥当/格式准确。 美学纲领自评仍为:交互范式 / 美学纲领 / 整体协调(勿与生成规则三维混用)。 叙事指南与故事推进正文:纲领 / 风格与遣词(+示范) / 笔墨焦点 / 禁忌与不偏好 / 情境备用 / 推进与决策 / 内容与表达;自评=写法可执行/推进可执行/与美学对齐;artifact=`设计.叙事指南与故事推进`。 -正文组成:用户可见块序 + 监控栏锚点 + 隐藏变量段 + 前端拆分意图(非程序报文);artifact=`设计.正文组成`。 +正文组成:**基于固定呈现壳适配微调**(选 `shell_id` + 微调轴;块只挂壳已有区域);artifact=`设计.正文组成`。见 `docs/play-presentation-shells.md`。 设计监控栏:只监控会变字段(可含攻略目标/场景交互物);artifact=`设计.监控栏`(旧称状态栏)。 变量设计:真值 + **维护语句** + **Data映射索引**(指向具体实例)+ side_effects。 变量控制上下文:各槽视野 + **旁观汇总**(当前档摘要)。 @@ -121,11 +123,20 @@ API:`POST /api/sessions/:id/context-order`(`move` / `set_projection` / `set_ ## 6. UI 渲染 -| schema | 友好渲染 | +| schema / 技能 | 友好渲染 | |--------|----------| -| `context-fragment.v1` | 概要 / 挂载 / 正文 | +| `context-fragment.v1` 外壳 | brief / 挂载 / **自评十分制(x/10)** / 追问 | +| 美学纲领与交互范式 | mosaic(设定逻辑 / 交互范式 / 美学纲领) | +| 生成规则 | 必要性 + 规则卡(方法、单层字段表、池) | +| 舞台骨架 | 尺度 / 社会 / 状况 / 关键区实体卡 | +| 实现机制 | 支撑点卡 + 关系 + 覆盖检验 | +| 其它技能正文 | 通用结构化(KV);**应补专用卡** | | `context-order.v1` | 扁平序表;历史行高亮 | +解析失败时勿只甩 JSON 墙:提示无法解析并保留原文折叠。自评 `分数` 契约为 **0–10**(旧百分数会按 /10 兼容显示)。 + +技能作者:output 键稳定后,须登记或实现对应视图(见 `capability-authoring-brief.md` §4.9「视图」)。 + ## 7. 配方链路 ```text diff --git a/docs/play-presentation-shells.md b/docs/play-presentation-shells.md new file mode 100644 index 0000000..e9942e4 --- /dev/null +++ b/docs/play-presentation-shells.md @@ -0,0 +1,143 @@ +# 游玩呈现壳(P1)· 可微调前端模板 + +> **阶段**:P1 = 定类与契约;**P2** = 程序/样式适配与灌数。 +> 上游:游玩管线 `auditor → gm → narrator`(已落地)。 +> 创作挂钩:`设计.正文组成` / `设计.监控栏`;终稿 tag 通常 `输出.用户展示`。 +> **创作定位:基于固定壳做适配微调,不是从零设计前端。** + +## 1. 创作怎么做(权威口径) + +```text +固定壳(本仓库预置,少数几种) + → 创作期:选一个 shell_id + → 只改允许的微调轴(字段、显示名、开关、tone_chrome…) + → 禁止改分区骨架、禁止发明第五种布局 +游玩期:往壳的区域里灌数(present.v1 / 投影 / settlement) +``` + +「正文组成」= **壳适配单**,不是 UI 创意稿。 +用户看见的差异应来自**同壳不同绑定**,而不是每局一套新布局。 + +## 2. 问题(若不用壳) + +创作期若只写散文式「想要好看界面」,模型难稳定产出可跑界面,也会和程序渲染脱节。 +壳把布局钉死,创作只填内容契约。 + +```text +创作:选壳 + 微调绑定(字段/文案/开关) +游玩:narrator / 程序灌入对应区域 +伪 0 层:壳自带的 HUD 区,不是新 LLM +``` + +## 3. 四大常用壳(微调模板,非成品皮肤) + +| id | 中文 | 主场景 | 默认分区 | 默认可调 | +|----|------|--------|----------|----------| +| `prose` | 纯散文流 | 只要沉浸正文、不要 HUD | 单流 `body` | 文末可选折叠备注 | +| `chat_monitor` | 对话 + 顶栏监控 | 网恋 / 日常扮演 / 轻 AIRP | `monitor` 顶栏 + `body` 主聊 | 监控字段;是否显示建议行动 | +| `turn_panel` | 回合面板 | 强机制、资源/检定、场景交互 | `monitor` + `body` + `footer`(建议行动/检定结果) | 栏位;footer 开闭;机遇区 | +| `chapter_reader` | 章节阅读 | 长文 / 爽文 / 助手分段 | `header`(章题/卷)+ `body` + 可选 `aside`(进度) | 章标题样式;是否显示大纲进度 | + +旧规格未写 `shell_id` → 运行时视为 `prose`(与「可不排正文组成」一致)。 + +### 3.1 各壳固定槽位(程序认这些区域 id;创作不得增删分区) + +| 区域 id | prose | chat_monitor | turn_panel | chapter_reader | +|---------|-------|--------------|------------|----------------| +| `monitor` | — | 顶栏 | 顶/侧栏 | — 或弱化进 header | +| `header` | — | 可选会话抬头 | 可选回合号 | **章题/卷信息** | +| `body` | **主文** | **主聊/场面** | **场面叙述** | **章正文** | +| `footer` | 可选 | 可选短吐槽 | **建议行动 / 机遇摘要** | 可选作者注 | +| `aside` | — | — | 可选场景交互物 | **进度/卷纲摘要** | +| `hidden` | 变量段 | 变量段 | 变量段 | 变量段 | + +隐藏段从不进用户主视线(默认折叠/调试可见)。 + +### 3.2 微调轴(创作只许改这些;布局骨架只读) + +每壳允许覆盖、禁止改布局骨架: + +| 轴 | 说明 | 例 | +|----|------|-----| +| `monitor_fields` | 监控栏字段子集(来自设计.监控栏) | 好感、体力、今日话题 | +| `block_labels` | 可见块显示名 | 「场面」→「短信记录」 | +| `show_suggested_actions` | 是否渲染 settlement.suggested_actions | turn_panel 默认开 | +| `tone_chrome` | 极轻装饰关键词(非完整 CSS) | `messenger` / `terminal` / `book` | +| `empty_states` | 监控/aside 空态文案 | 「尚无攻略目标」 | + +**禁止**创作期输出完整 CSS/工程;`tone_chrome` 只是壳内预设皮肤键。 +**禁止**增删壳的分区、禁止自造第五种 `shell_id`(要新壳走仓库改模板,不走单局创作)。 + +## 4. 与创作产物的映射 + +```text +设计.正文组成 + 正文.呈现壳.shell_id → 必须是四大之一(选壳) + 正文.呈现壳.微调 → 只填允许轴(适配) + 正文.可见块[] → 挂到壳已有区域,不新开区域 + 正文.监控栏锚点 → chat_monitor / turn_panel 的字段意图 + 正文.隐藏段 → 区域 hidden + +设计.监控栏 + → 灌入该壳 monitor 区的字段表(仍是微调,不是新布局) + +运行.本轮.裁决 (settlement.v1) + → body 事实来源;suggested_actions → footer(若壳开启) + +输出.用户展示 + → 优先 present.v1 灌进壳区域;纯 Markdown → body +``` + +## 5. 游玩灌数契约(P2 实现目标) + +推荐 `输出.用户展示` 在分块壳下为: + +```json +{ + "schema": "present.v1", + "shell": "chat_monitor", + "blocks": { + "monitor": { "好感": "暖络", "今日": "下雨" }, + "body": "……用户可读正文……", + "footer": "" + }, + "meta": { + "suggested_actions": ["回一句关心", "约周末"], + "hide": ["<<>>…<<>>"] + } +} +``` + +- `shell` 可省略 → 用规格里正文组成的 `shell_id`。 +- 若整段非 JSON:壳把全文写入 `body`,其它区用变量投影/监控栏程序填(P2)。 +- `prose`:只需 `body` 字符串或 Markdown。 + +## 6. 创作期只做适配(正文组成) + +1. **选壳**(四选一):无 HUD → `prose`;对话+状态 → `chat_monitor`;机制重 → `turn_panel`;写章 → `chapter_reader`。 +2. **微调**:监控字段、块显示名、建议行动开关、`tone_chrome`、空态文案。 +3. **不要**:重画分区、发明新区域 id、写 CSS、谈「另一种全新界面」。 +4. `版式隐喻` 仅供人读;**程序只认 `shell_id` + 微调轴**。 + +## 7. P2 适配清单 + +| 序 | 项 | 状态 | +|----|----|------| +| 1 | `web/` 四壳最小渲染器 | **已接** `web/present-shells.js` + CSS | +| 2 | 读 `设计.正文组成` 微调进 SessionView | **已接** `presentationTweaks` | +| 3 | 解析 `present.v1` / 纯文本回退 | **已接** `src/skills/present-packet.ts` | +| 4 | narrator 模板按壳输出 | **已接** `worker-templates/narrator.yaml` | +| 5 | 正文组成只产出壳适配单 | **已接**(P1 prompt) | + +仍可加深:监控栏字段程序灌 monitor、tone 皮肤精修、章阅读 aside 自动进度。 + +## 8. 与路线图 + +| 本文 | `px-roadmap` | +|------|----------------| +| P1 定壳 | 创作 = 选壳适配 | +| P2 灌数渲染 | 挂 **PX2 阅读面** | + +--- + +修订记录:2026-08-11 · P1;口径明确为创作基于壳适配微调。 diff --git a/docs/progressive-data-design.md b/docs/progressive-data-design.md index bda538e..f0759a6 100644 --- a/docs/progressive-data-design.md +++ b/docs/progressive-data-design.md @@ -94,8 +94,9 @@ Progressive 投影 本轮主世界层只看见「当前档」;未 ```text 用户输入 - → 主世界层(世界推演):读变量 + 已投影 data,交事件/改真值(按规则) - →(可选)旁观审计:只出意见,不写 canon + → 旁观维护(副 LLM):合同+表+本轮工单;默认空操作(maintain.v1) + →(可选)角色视角:强信息隔离时出反应建议 + → 主世界层(世界推演):读变量 + 已投影 data + 旁观包,交事件/改真值(按规则) →(可选)叙事转述:把裁决写成用户可见正文 → 用户看到的终稿通常来自「呈现/叙事」执行单元,或主世界层兼呈现(规格决定) ``` @@ -105,10 +106,13 @@ Progressive 投影 本轮主世界层只看见「当前档」;未 | 世界观、地点、势力百科 | 主世界层 + 常驻/查表 | 「世界观执行单元」 | | 好感与分档性格 | 真值 + Progressive 投影 | 「性格执行单元」 | | 大纲按章推进 | 真值 `章节` + 章表投影 | 每章一个执行单元 | -| 知密视角 / 强信息隔离 | 才考虑额外执行单元 | 为「更聪明」而拆 | -| 长跑漂移检查 | 旁观审计(只读建议) | 第三个创作主笔 | +| 知密视角 / 强信息隔离 | 才考虑角色视角槽 | 为「更聪明」而拆 | +| 表字段补充 / 按需触发生成规则 | 旁观维护(默认空操作) | 第三个创作主笔 | +| 长跑漂移检查 | 旁观维护 notes | 第三个创作主笔 | -用户可见名:`ui-glossary.md`(世界推演、叙事转述…)。作者文档可称 **主世界层 / 旁观**,与酒馆「主 GM + auditor」同构;**不要**对用户改称整套系统为「GM 框架」而废弃「执行单元」。 +用户可见名:`ui-glossary.md`(主世界层、旁观维护、叙事转述…)。作者文档可称 **主世界层 / 旁观维护**,与酒馆「主 GM + auditor」同构;**不要**对用户改称整套系统为「GM 框架」而废弃「执行单元」。 + +**维护路径(不是 chance toolcall)**:主世界层输出 settlement.v1 → Runtime 合并 `variable_changes` 进 `变量.当前` → `side_effects` 换投影;旁观维护输出 maintain.v1 → Runtime 按需合并 `table_ops`;chance 只负责骰子/抽签。 ## 6. 世界模拟器技能落点 @@ -122,6 +126,4 @@ Progressive 投影 本轮主世界层只看见「当前档」;未 | **开场白与开场变量** | 开场正文(守版式)+ 与开场同真相的初值 | | **游玩拓扑 / 上下文投影排序 / 细化终稿** | 固定槽;扁平投影序;tables 收成 | -**维护路径(不是 chance toolcall)**:主世界层输出可解析变更 → Runtime 合并 `变量.当前` → `side_effects` 换投影;chance 只负责骰子/抽签。 - 编排口诀:体验与机制 → 要不要真值 → 映射表用生成规则/实例 → 维护语句与旁观汇总 → 开场同真相 → 收成。 diff --git a/docs/ui-glossary.md b/docs/ui-glossary.md index cc0df8c..a90cf29 100644 --- a/docs/ui-glossary.md +++ b/docs/ui-glossary.md @@ -124,6 +124,7 @@ | `narrator` | **叙事转述** | | `role-decide` | **角色决策** | | `world-simulator` | **主世界层**(旧称:世界推演) | +| `auditor` | **旁观维护**(表/规则检查;默认空操作) | | `chance` | **机遇裁定**(按需:骰子/抽签/比点;程序工具) | | `round-present` | **回合呈现** | @@ -139,10 +140,13 @@ | 作者可用 | 典型 ref | 含义 | |----------|----------|------| +| **旁观维护** | `auditor` | 副 LLM:表/规则检查与按需补充;默认每轮上场、多数轮空操作;不写真相 | | **主世界层** | `world-simulator` | 读变量与 Progressive 投影、按规则改真值/交事件;多数世界观与查表归这里 | | **叙事转述** | `narrator` | 把裁决写成用户可见正文(若规格拆了呈现) | | **机遇裁定** | `chance` | 按需程序工具(掷骰/比点/抽签);不进每轮管线;结果 tag `运行.本轮.机遇` | -| **旁观 / 审计** | 可选自建 | 只出意见,不写 canon;不为「更聪明」而拆 | +| **角色视角** | `role-decide` | 仅强信息隔离;只出反应建议 | + +推荐调度:`auditor → perspective? → gm → narrator`。 默认少拆:百科、分档性格、章大纲投影 → 主世界层 + 表副作用,**不要**再拆「世界观执行单元」「性格执行单元」。详见 `docs/progressive-data-design.md` §5。 diff --git a/docs/world-simulator-modules.md b/docs/world-simulator-modules.md index 174be10..2ecef20 100644 --- a/docs/world-simulator-modules.md +++ b/docs/world-simulator-modules.md @@ -5,6 +5,7 @@ > **标准范例**:`modules/aesthetics-interaction/prompt.md`。 > **真值 / Data / Progressive(变量与门控)**:**`docs/progressive-data-design.md`**。 > **上下文片段 / 槽位 / 投影排序**:**`docs/context-fragment-design.md`**。 +> **游玩呈现壳(P1)**:**`docs/play-presentation-shells.md`**。 > **提示词待改清单**:**`docs/briefs/prompt-revision-inventory.md`**。 > **给外部 AI 的完整泛用规范**:**`docs/briefs/capability-authoring-brief.md`**(含 `context-fragment.v1`)。 @@ -124,15 +125,15 @@ recipes/world-simulator|expand-assistant/recipe.yaml | 能力 | id | 状态 | |------|-----|------| -| 美学纲领与交互范式 | `aesthetics-interaction` | **范例已写** | -| 实现机制 | `mechanism` | **已写**:`context-fragment.v1`(支撑点正文 + 自评 + 追问);`feeds: gm` | -| 舞台骨架 | `world-blueprint` | **已写**:`context-fragment.v1`;社会结构 + 世界状况 | -| 生成规则 | `generation-rules` | **已写**:`context-fragment.v1`;宽原则 + 严合同键;可反复;`params.target` | +| 美学纲领与交互范式 | `aesthetics-interaction` | **范例已写**;前端 mosaic 视图 | +| 实现机制 | `mechanism` | **已写**:`context-fragment.v1`(支撑点正文 + 自评 + 追问);`feeds: gm`;**专用卡已接** | +| 舞台骨架 | `world-blueprint` | **已写**:`context-fragment.v1`;社会结构 + 世界状况;**专用卡已接** | +| 生成规则 | `generation-rules` | **已写**:`context-fragment.v1`;宽原则 + 严合同键;可反复;`params.target`;**专用卡已接** | | 具体实例 | `concrete-instances` | **已写**:`context-fragment.v1`;只按规则执行;可反复;`params.rule_id` | | 叙事指南与故事推进 | `narrative` | **已写**:遣词/笔墨焦点/禁忌与不偏好 + 推进;挂转述 | | 拓扑图谱 | `topology` | 骨架,待细写 | | 设计监控栏 | `status-bar` | **已写**:只盯会变信息;旧称状态栏 | -| 正文组成 | `reply-format` | **已写**:用户可见版式+隐藏段+前端拆分;旧称回复格式 | +| 正文组成 | `reply-format` | **已写**:壳适配微调;P2 present 渲染已接;见 `docs/play-presentation-shells.md` | | 变量设计与更新规则 | `variable-design` | **已写**:真值+维护语句+Data映射索引+side_effects | | 变量控制上下文 | `variable-context` | **已写**:挂载+旁观汇总 | | 开场白与开场变量 | `opening-setup` | **已写**:开场守版式+同真相初值 | @@ -141,13 +142,13 @@ recipes/world-simulator|expand-assistant/recipe.yaml | 能力 | id | 状态 | |------|-----|------| -| 游玩拓扑 | `worker-spec` | **已写**:勾选固定槽,禁止自由发明 worker(旧称 Worker 规格) | -| 上下文投影排序 | `context-order` | **已写**:扁平投影序(含对话.历史);收成进 `context_order`;Runtime 已拼装 | +| 游玩拓扑 | `worker-spec` | **已写**:勾选固定槽(含旁观维护 auditor);禁自由发明 worker | +| 上下文投影排序 | `context-order` | **已写**:扁平投影序(含对话.历史;旁观维护默认无历史);收成进 `context_order`;Runtime 已拼装 | | 细化终稿 | `refine` | **已写**:按 `play_slots`(及排序表)收成 `设计.worker集` | | 编排器 | 状态 | |------|------| -| 世界模拟器 | 链路已写:体验→挂槽写上下文→投影排序→终稿;起点:美学纲领与交互范式 | +| 世界模拟器 | 创作链路已写;**play**:`auditor → perspective? → gm → narrator`;settlement/maintain 合并变量已接 | | 扩写助手 | 方法论已写;起点:美学纲领与交互范式;勿默认套世界模拟全套 | --- diff --git a/skills/dialogue/world-simulator/modules/aesthetics-interaction/prompt.md b/skills/dialogue/world-simulator/modules/aesthetics-interaction/prompt.md index 9265e5f..b9c5138 100644 --- a/skills/dialogue/world-simulator/modules/aesthetics-interaction/prompt.md +++ b/skills/dialogue/world-simulator/modules/aesthetics-interaction/prompt.md @@ -1,4 +1,4 @@ -# 美学纲领与交互范式 +# 美学纲领与交互范式 > **技能标准形范例**(其它中段技能对齐本结构)。 > 程序只切割下方 **fence 块**(`meta` / `opening` / `task` / …);`##` 标题仅供人读。 @@ -180,9 +180,9 @@ feeds: design_only }, "自评": { "维度": [ - { "名": "交互范式", "分数": 0, "说明": "权限边界是否锁住核心体验" }, - { "名": "美学纲领", "分数": 0, "说明": "体验内核是否点题、有质感" }, - { "名": "整体协调", "分数": 0, "说明": "范式是否在保护纲领" } + { "名": "交互范式", "分数": 8, "说明": "权限边界是否锁住核心体验" }, + { "名": "美学纲领", "分数": 8, "说明": "体验内核是否点题、有质感" }, + { "名": "整体协调", "分数": 8, "说明": "范式是否在保护纲领" } ], "薄弱点": "一句话" }, @@ -202,7 +202,7 @@ feeds: design_only 硬规则(公共三段 + 本技正文): 1. 合法 JSON;必含 `schema` / `技能` / `brief` / `正文` / `自评` / `追问`(题目可 `[]`)。 -2. **公共三段**:`正文`=产物主体;`自评`=完备度打分;`追问`=导语+题目(建议选项/示例)。禁止用散文代替这三段。 +2. **公共三段**:`正文`=产物主体;`自评`=完备度打分(**分数 0–10 十分制,禁止百分数**);`追问`=导语+题目(建议选项/示例)。禁止用散文代替这三段。 3. **本技正文三块**:`设定逻辑`(参与/内容维度/区域/复杂性/完备度)+ `交互范式` + `美学纲领`。未知省略或「未定」,禁止编造百科。 4. `mount` 建议挂主世界层+转述;**禁止**写数字 `order`。`稳变` 定稿后多为 `stable`。 5. `追问.题目` 一次宜 1~3 题;有建议选项;允许「其它」。已答清的不要再问。 diff --git a/skills/dialogue/world-simulator/modules/catalog.yaml b/skills/dialogue/world-simulator/modules/catalog.yaml index 70056ab..cada232 100644 --- a/skills/dialogue/world-simulator/modules/catalog.yaml +++ b/skills/dialogue/world-simulator/modules/catalog.yaml @@ -120,8 +120,8 @@ modules: - id: reply-format name: 正文组成 declaration: >- - 钉用户可见的一轮回复由哪些块、何顺序组成(含监控栏位置、隐藏变量段、前端拆分); - 不是程序报文格式;旧称设计回复格式 + 基于固定呈现壳做适配微调(选 shell_id + 字段/显示名等); + 产出壳适配单,禁止从零发明布局;旧称设计回复格式 artifact: 设计.正文组成 # —— 开局(创作偏晚) —— diff --git a/skills/dialogue/world-simulator/modules/concrete-instances/prompt.md b/skills/dialogue/world-simulator/modules/concrete-instances/prompt.md index 22d95e7..ac0146e 100644 --- a/skills/dialogue/world-simulator/modules/concrete-instances/prompt.md +++ b/skills/dialogue/world-simulator/modules/concrete-instances/prompt.md @@ -87,7 +87,7 @@ summary:`具体实例 · {对象} · {n}条` "维度": [ { "名": "合规模", - "分数": 0, + "分数": 8, "说明": "是否严格按该规则的方法、单层格式、池与约束生成" } ], diff --git a/skills/dialogue/world-simulator/modules/generation-rules/prompt.md b/skills/dialogue/world-simulator/modules/generation-rules/prompt.md index 7090b14..778dd47 100644 --- a/skills/dialogue/world-simulator/modules/generation-rules/prompt.md +++ b/skills/dialogue/world-simulator/modules/generation-rules/prompt.md @@ -1,4 +1,4 @@ -# 生成规则 +# 生成规则 > 技能文档。程序只切割下方 **fence 块**;`##` 标题仅供人读。 > 产物外壳:`docs/context-fragment-design.md`;范例:`aesthetics-interaction` / `mechanism`。 @@ -197,17 +197,17 @@ summary:建立 → `生成规则 · {对象} · {仅动态|仅预生成|预生 "维度": [ { "名": "必要性", - "分数": 0, + "分数": 8, "说明": "整条规则:是否有必要单独用「生成规则」承接该类内容;跳过时理由是否充分" }, { "名": "属性妥当", - "分数": 0, + "分数": 8, "说明": "逐字段:有无冗余(如女主规则的性别)或缺失(如 D&D 怪物缺等级/攻击骰);跳过时写 N/A" }, { "名": "格式准确", - "分数": 0, + "分数": 8, "说明": "是否严格单层 JSON、类型/必填/枚举/边界可对接动态表格·旁观者生成·投影;跳过时写 N/A" } ], @@ -229,7 +229,7 @@ summary:建立 → `生成规则 · {对象} · {仅动态|仅预生成|预生 硬规则(公共三段 + 本技正文): 1. 合法 JSON;必含 `schema` / `技能` / `brief` / `正文` / `自评` / `追问`(题目可 `[]`)。 -2. **公共三段**:正文=规则主体;自评=必要性/属性妥当/格式准确;追问=导语+建议选项。 +2. **公共三段**:正文=规则主体;自评=必要性/属性妥当/格式准确(**分数 0–10 十分制**);追问=导语+建议选项。 3. **正文键固定**:本步参数、必要性判断、rules、增量说明。 4. **每条 rule 必含**:生成与描写、产物格式;**池可空数组**。有池时 `绑定字段` 必须是单层 schema 的顶层键。 5. **产物格式强制单层**:`schema` 的 `type` 不得为 `object`;`array` 的 `items` 不得为 object。禁止 `外貌.发色` 这类嵌套路径。 diff --git a/skills/dialogue/world-simulator/modules/mechanism/prompt.md b/skills/dialogue/world-simulator/modules/mechanism/prompt.md index 0e05ecb..c40c29d 100644 --- a/skills/dialogue/world-simulator/modules/mechanism/prompt.md +++ b/skills/dialogue/world-simulator/modules/mechanism/prompt.md @@ -1,4 +1,4 @@ -# 实现机制 +# 实现机制 > 技能文档。程序只切割下方 **fence 块**;`##` 标题仅供人读。 > 产物外壳对齐:`docs/context-fragment-design.md`;范例:`aesthetics-interaction`。 @@ -203,17 +203,17 @@ summary:`实现机制 · …`(点题主要支撑)。 "维度": [ { "名": "覆盖度", - "分数": 0, + "分数": 8, "说明": "能否完全覆盖用户已表达的规则/前提/硬条件(未漏掉用户点名的承重约束)" }, { "名": "充分度", - "分数": 0, + "分数": 8, "说明": "支撑点合起来能否充分展示用户想要的核心体验(缺了是否撑不住感觉)" }, { "名": "克制度", - "分数": 0, + "分数": 8, "说明": "是否精确到支撑切面;无多余百科、人物卡、起源史或题材默认拓展" } ], @@ -235,7 +235,7 @@ summary:`实现机制 · …`(点题主要支撑)。 硬规则(公共三段 + 本技正文): 1. 合法 JSON;必含 `schema` / `技能` / `brief` / `正文` / `自评` / `追问`(题目可 `[]`)。 -2. **公共三段**:`正文`=产物主体;`自评`=打分;`追问`=导语+建议选项。禁止整份散文壳。 +2. **公共三段**:`正文`=产物主体;`自评`=打分(**分数 0–10 十分制**);`追问`=导语+建议选项。禁止整份散文壳。 3. **本技正文**:`依据的核心体验` + `支撑点[]` + `支撑点关系[]` + `覆盖检验`。基准世界/推理过程/淘汰候选不得写入。 4. 每个支撑点须有归属元素、支撑切面、如何支撑、缺失后果;简单支点结构=「一句话」、展开=`[]`。 5. `mount` 默认主世界层(`world-simulator`);**禁止**写 `order`。承重规则进 GM,不进转述全文。 diff --git a/skills/dialogue/world-simulator/modules/narrative/prompt.md b/skills/dialogue/world-simulator/modules/narrative/prompt.md index 0b6aaa5..ef70f98 100644 --- a/skills/dialogue/world-simulator/modules/narrative/prompt.md +++ b/skills/dialogue/world-simulator/modules/narrative/prompt.md @@ -212,17 +212,17 @@ summary:`叙事指南与故事推进 · …`(点题体裁/笔墨重心 + 推 "维度": [ { "名": "写法可执行", - "分数": 0, + "分数": 8, "说明": "遣词+笔墨焦点+禁忌/不偏好+示范是否够转述落地" }, { "名": "推进可执行", - "分数": 0, + "分数": 8, "说明": "主世界层是否知道如何推进;变量钩子清楚或明确无需" }, { "名": "与美学对齐", - "分数": 0, + "分数": 8, "说明": "是否把感觉落成做法;笔墨与禁忌服务体验而非跑偏" } ], diff --git a/skills/dialogue/world-simulator/modules/opening-setup/prompt.md b/skills/dialogue/world-simulator/modules/opening-setup/prompt.md index 125e135..303433a 100644 --- a/skills/dialogue/world-simulator/modules/opening-setup/prompt.md +++ b/skills/dialogue/world-simulator/modules/opening-setup/prompt.md @@ -121,17 +121,17 @@ summary:`开场白与开场变量 · …`(点题场景)。 "维度": [ { "名": "契约符合", - "分数": 0, + "分数": 8, "说明": "是否遵循正文组成/叙事/监控栏" }, { "名": "同真相", - "分数": 0, + "分数": 8, "说明": "开场叙述与开场变量是否一致" }, { "名": "可开玩", - "分数": 0, + "分数": 8, "说明": "是否短、可感、有下一步行动空间" } ], diff --git a/skills/dialogue/world-simulator/modules/refine/prompt.md b/skills/dialogue/world-simulator/modules/refine/prompt.md index 2831fa9..38d2685 100644 --- a/skills/dialogue/world-simulator/modules/refine/prompt.md +++ b/skills/dialogue/world-simulator/modules/refine/prompt.md @@ -58,15 +58,15 @@ boundary: | 执行顺序: 1. 复述站位、体验内核、禁忌;矛盾处 askUser 1 点。 2. 写入 `play_slots`(世界模拟)或等价写手槽;**workers 只含白名单 ref**: - - world_sim 每轮:world-simulator / narrator / role-decide(仅 perspective 开时) + - world_sim 每轮:auditor / world-simulator / narrator / role-decide(仅 perspective 开时) - world_sim 按需:chance(仅 chance 开时;`invocation: on_demand`) - writing:outline / chapter-writer - 程序也会按 play_slots 展开;你仍应写出与槽一致的 workers[](含 acceptance),便于人读验收。 3. **禁止**自造 ref、禁止添加 variable-update / lore-keeper / 自造骰子 LLM 等。 -4. resident_context:稳定句挂到 gm 或 narrator(或 outline/chapter-writer);勿塞聊天过程。 +4. resident_context:稳定句挂到 gm 或 narrator(或 outline/chapter-writer);旁观合同可挂 auditor;勿塞聊天过程。 5. tables:吸入变量设计的 side_effects;无则空数组或省略。 6. core_premises、design_end.opening 按需。 -7. summary:`细化终稿 · 槽 gm+转述 · …` 或 `细化终稿 · 大纲+章节 · …` +7. summary:`细化终稿 · 槽 旁观+gm+转述 · …` 或 `细化终稿 · 大纲+章节 · …` 进游玩不在本步完成。 ``` @@ -75,9 +75,9 @@ boundary: | ```principles 1. 合并优于重写;槽位优于发明演员。 -2. 面向用户的终稿点 acceptance=review(通常是 narrator 或 chapter-writer);gm/outline 常用 continue。 -3. 真值变更写在 gm 的 outputs(运行.本轮.变量变更 / 裁决包内 variable_changes),不靠第三变量 Worker。 -4. 裁决包约定:运行.本轮.裁决 使用 settlement.v1(见 progressive-data-design / 模板)。 +2. 面向用户的终稿点 acceptance=review(通常是 narrator 或 chapter-writer);auditor/gm/outline 常用 continue。 +3. 真值变更写在 gm 的 outputs(运行.本轮.变量变更 / 裁决包内 variable_changes),旁观维护只出 maintain.v1;不靠第三变量 Worker。 +4. 裁决包约定:运行.本轮.裁决 使用 settlement.v1(见 progressive-data-design / 模板);Runtime 合并 variable_changes。 5. 键名稳定;未决进 open_questions。 ``` @@ -110,16 +110,25 @@ boundary: | "satisfaction_source": "…" }, "play_slots": { + "auditor": true, "gm": true, "narrator": true, "perspective": false }, "workers": [ + { + "name": "旁观维护", + "ref": "auditor", + "duty": "表/规则检查;maintain.v1;默认空操作;无长对话史", + "when": "每轮用户输入后、主世界层之前", + "rationale": "删掉则表补与规则触发放回主世界层,挤占历史上下文", + "acceptance": "continue" + }, { "name": "主世界层", "ref": "world-simulator", "duty": "读投影与真值,输出 settlement.v1 裁决包;可提议变量变更", - "when": "每轮用户输入后", + "when": "每轮用户输入后(旁观之后)", "rationale": "删掉则无程序化裁决与真值更新", "acceptance": "continue" }, @@ -175,7 +184,7 @@ boundary: | ```examples 好: -- play_slots gm+narrator;workers 两条与槽一致;side_effects 从变量设计拷入。 +- play_slots auditor+gm+narrator;workers 与槽一致;side_effects 从变量设计拷入。 - 扩写:outline continue + chapter-writer review;无自造 ref。 坏: diff --git a/skills/dialogue/world-simulator/modules/reply-format/prompt.md b/skills/dialogue/world-simulator/modules/reply-format/prompt.md index f99b234..6de4ca8 100644 --- a/skills/dialogue/world-simulator/modules/reply-format/prompt.md +++ b/skills/dialogue/world-simulator/modules/reply-format/prompt.md @@ -1,8 +1,8 @@ # 正文组成 > 技能文档。程序只切割下方 **fence 块**;`##` 标题仅供人读。 +> **创作定位:基于固定呈现壳做适配微调**(`docs/play-presentation-shells.md`)。 > 产物外壳:`docs/context-fragment-design.md`。 -> **不是**程序 API/JSON schema 格式;钉的是用户最后看见的「一封信怎么拼起来」。 > 旧称:设计回复格式。 ## meta @@ -12,37 +12,37 @@ name: 正文组成 id: reply-format artifact: 设计.正文组成 declaration: > - 钉用户可见的一轮回复由哪些块、何顺序组成(抬头/日期/正文/监控栏/文末吐槽等), - 含对 LLM 隐藏的变量维护段与前端拆分/美化提示;不等于程序报文格式 + 基于四大固定呈现壳做适配微调(选 shell_id + 填字段/显示名/开关等); + 产出壳适配单;禁止从零发明布局或完整 UI when: | - 用户看见的不该只是「一整段散文」,需要版式块(抬头、监控栏、文末小块等); - 或需要约定隐藏变量段供模型维护表格;或需要前端分区渲染时。 + 需要为本局选定呈现壳,或微调监控字段/块显示名/建议行动开关时; + 或需要约定隐藏变量段时。 when_not: | - 纯单段叙事、明确不要分块版式 → 可不排(转述直接出一段即可)。 - 只谈文风遣词 → 「叙事指南与故事推进」。 - 只钉监控栏字段清单、版式已定 → 「设计监控栏」。 - 块结构未定时不要先空谈 CSS 细节。 + 明确只要纯散文 → 可不排(运行时默认 prose)。 + 只谈文风 → 「叙事指南与故事推进」。 + 壳已定、只筛监控字段明细 → 「设计监控栏」。 + 不要在本步设计新布局或写 CSS。 boundary: | - 本技能:用户可见组成(块清单、顺序、可见/隐藏、前端拆分意图);产出 context-fragment.v1。 - 不是 OpenAI/函数调用报文,也不是结算包 schema。 - 设计监控栏:监控栏内有哪些可变字段——可本步给骨架,细则可交监控栏技能。 - 叙事指南与故事推进:怎么写正文——本步定「正文」块在版式里的位置与职责,不重写文风。 - 变量设计:真值如何变——本步只约定隐藏段如何承载变更声明,不写 side_effects。 + 本技能:壳适配单(shell_id + 微调轴 + 块挂到壳已有区域)。 + 布局骨架只读:docs/play-presentation-shells.md。 + 设计监控栏:字段表细则。叙事指南:文风。变量设计:真值规则。 feeds: narrator ``` ## opening ```opening -用户每一轮「看见的东西」希望像什么?(想到什么写什么) +本局在固定呈现壳上做适配(不是设计新界面)。 -1. 由哪些部分组成?什么顺序? - 例:抬头 + 日期/地点 + 正文;或正文 + 监控栏;或文末再加小吐槽/模拟书评…… +0. 四选一壳: + · prose — 纯散文 + · chat_monitor — 对话 + 顶栏监控 + · turn_panel — 回合面板 + · chapter_reader — 章节阅读 -2. 有没有要「藏起来给模型/程序看、用户界面默认不展示」的段? - 例:用特殊标记包住的变量变更,方便拆解维护表格。 - -3. 前端要不要拆成多个区域美化?(监控栏固定顶栏、正文滚动、文末折叠……)没有就说没有。 +1. 要微调什么?(监控字段、块称呼、要不要建议行动、tone_chrome…) +2. 有没有隐藏段给模型/程序维护变量? +3. 不要谈「再发明一种全新布局」。 ``` ## task @@ -50,42 +50,38 @@ feeds: narrator ```task 你正在执行「正文组成」。产物必须是 **context-fragment.v1** JSON,写入「设计.正文组成」。 -本步设计的是**用户最后看见的内容版式**——像一封信由收信人、正文、日期、发信人组成; -也可以是抬头、日期、地点、正文、监控栏、文末吐槽、小故事、模拟书评等。 -**不是**程序接口格式。 +本步 = **壳适配单**:四大壳里选一个,只改允许的微调轴。 +权威:`docs/play-presentation-shells.md`。 +禁止从零设计分区、禁止自造 shell_id、禁止完整 CSS。 执行顺序: -1. 读美学呈现与叙事指南:继承人称/终稿由谁写;不重开文风问卷。 -2. 列出「可见块」:每块职责、顺序、是否可空、由谁填充(转述/程序拼装/监控栏技能)。 -3. 若有监控栏:写清它在版式中的位置与职责;字段级细则可引用或留给「设计监控栏」(只留会变、要盯的信息)。 -4. 若有隐藏段:写清标记约定(如正则/定界符)、内容用途(变量变更声明)、用户侧默认隐藏、模型须输出以便拆表。 -5. 写「前端拆分与美化」意图(区域、可否折叠、是否 HTML 片段);无则说明纯 Markdown/纯文本。 -6. 自评 + 追问;输出 JSON。禁止写最终 context `order`。 +1. 读美学/叙事:继承人称与终稿归属;不重开文风问卷。 +2. **选 shell_id**;说明为何是这个壳而不是另三个。 +3. 可见块:只挂到该壳已有区域;写显示名与职责(适配文案,不是新分区)。 +4. 监控栏:字段意图(细则可交「设计监控栏」);无 monitor 的壳写「不启用」。 +5. 隐藏段(若有):定界与用途。 +6. 填「呈现壳.微调」;前端拆分区域必须与壳一致。 +7. 自评 + 追问;禁止写 context `order`。 -summary:`正文组成 · …`(点题主要块序)。 +summary:`正文组成 · 适配 {shell_id} · …`。 ``` ## principles ```principles -1. 用户看见的版式,不是程序报文 - 用「信/报纸/游戏 HUD」类比思考块;禁止把本步写成 API 字段说明书。 +1. 创作 = 选壳 + 适配微调 + 不是 UI 创意;布局骨架只读。 -2. 块要少、每块有职责 - 宁缺毋滥;装饰块必须服务体验(吐槽/书评若只是玩梗也要写清触发与长度)。 +2. 只改微调轴 + 字段、显示名、开关、tone_chrome、空态;不改分区、不增区域、不自造壳。 3. 监控栏 ≠ 名片卡 - 监控的是会变、影响决策或沉浸的信息(好感、体力、场景可交互对象、攻略目标状态…)。 - 主角「姓名」「性别」等几乎不变的内容默认不要进监控栏(绝大多数局)。 + 只盯会变、影响决策或沉浸的信息。 4. 隐藏段服务维护,不污染阅读 - 变量变更等可用定界符包住,供 LLM 输出、程序/旁路拆解入表;用户 UI 默认不渲染或折叠。 -5. 前端意图只写「拆哪里、干什么」 - 可提:顶栏监控 / 正文区 / 文末折叠 / 简易 HTML 片段。不写完整 CSS 工程。 - -6. 与转述、变量分工 - 正文块文风归叙事指南;真值规则归变量设计;本步定拼装契约。 +5. 与转述、变量分工 + 文风归叙事指南;真值归变量设计;本步只交壳适配契约。 ``` ## probe @@ -93,8 +89,8 @@ summary:`正文组成 · …`(点题主要块序)。 ```probe 每轮 1~2 点。 -优先:必须有哪些可见块;监控栏要不要、盯什么类型信息;隐藏变量段要不要及标记长什么样;前端要不要分区。 -不追问:完整 CSS、事件池 schema、文风遣词细节(除非块职责不清)。 +优先:选哪个壳;微调哪些字段/显示名;隐藏段要不要。 +不追问:新布局、完整 CSS、事件池 schema、文风遣词。 ``` ## output @@ -103,30 +99,41 @@ summary:`正文组成 · …`(点题主要块序)。 { "schema": "context-fragment.v1", "技能": "正文组成", - "brief": "一句话:用户看见的主要块序", + "brief": "一句话:壳 + 主要块序", "mount": ["narrator"], "稳变": "stable", "正文": { "依据的体验与呈现": [ "从美学/叙事继承的呈现要点" ], - "版式隐喻": "例:一封信 / 网文章节页 / 游戏回合面板 / 无(纯散文)", + "呈现壳": { + "shell_id": "prose|chat_monitor|turn_panel|chapter_reader", + "为何选它": "一句话", + "微调": { + "tone_chrome": "messenger|terminal|book|default", + "show_suggested_actions": true, + "block_labels": {}, + "empty_states": {} + } + }, + "版式隐喻": "给人读的比喻;程序以 shell_id 为准", "可见块": [ { "块id": "英文 kebab 或中文短名,同局稳定", + "区域": "monitor|header|body|footer|aside", "显示名": "用户可理解的标题;可无标题则空", "职责": "这块给用户什么信息", "顺序": 1, "可空": true, "填充方": "叙事转述|程序拼装|监控栏|其它", - "内容形态": "散文|短列表|键值行|HTML片段|其它", + "内容形态": "散文|短列表|键值行|其它", "示例": "可选一句/三行示意" } ], "监控栏锚点": { "本局是否启用": "是|否", "在可见块中的块id": "若启用则指向可见块之一", - "职责摘要": "盯哪些类信息(详表见设计.监控栏或本步字段草稿)", + "职责摘要": "盯哪些类信息", "字段草稿": [ { "名": "会变且值得盯的字段", @@ -138,8 +145,8 @@ summary:`正文组成 · …`(点题主要块序)。 "隐藏段": [ { "段id": "variable-delta", - "用途": "变量维护语句(与「变量设计.维护语句约定」同构),供程序拆进变量.当前", - "定界或正则约定": "例:<<>>...<<>>;形状细节以变量设计为准", + "用途": "变量维护语句,供程序拆进变量.当前", + "定界或正则约定": "例:<<>>...<<>>", "用户界面": "默认隐藏|折叠可见|调试可见", "模型必须输出": true, "内容形状": "短键值/JSON行;非 chance toolcall", @@ -150,17 +157,18 @@ summary:`正文组成 · …`(点题主要块序)。 "需要前端分区": "是|否", "区域": [ { - "区域id": "monitor|body|footer|…", + "区域id": "monitor|header|body|footer|aside", "对应块id": ["…"], "意图": "固定顶栏|主滚动|折叠文末|…", - "渲染提示": "纯文本|Markdown|受限HTML;勿写完整工程" + "渲染提示": "纯文本|Markdown;勿写完整工程" } ], - "说明": "无分区则写「单流渲染」" + "说明": "无分区则写「单流渲染」(通常 prose)" }, "拼装与终稿": { "终稿tag": "通常 输出.用户展示", "拼装方": "叙事转述一次写出各块|程序按块拼接|混合", + "推荐灌数": "分块壳优先 present.v1;prose 可为纯 Markdown", "与裁决包关系": "转述读 settlement 再填各块;隐藏段可含 variable_changes 镜像" } }, @@ -168,18 +176,18 @@ summary:`正文组成 · …`(点题主要块序)。 "维度": [ { "名": "可见完备", - "分数": 0, + "分数": 8, "说明": "用户该看见的块是否齐、顺序是否服务体验" }, { "名": "监控克制", - "分数": 0, - "说明": "监控栏是否只盯会变信息;无姓名性别等死字段堆砌(若无监控栏可 N/A)" + "分数": 8, + "说明": "监控栏是否只盯会变信息(若无监控栏可 N/A)" }, { "名": "可实现", - "分数": 0, - "说明": "隐藏段约定与前端拆分是否清楚到可交给转述/程序/前端" + "分数": 8, + "说明": "shell_id 是否在四大壳内;分区与隐藏段是否可交给转述/程序" } ], "薄弱点": "一句话" @@ -200,32 +208,32 @@ summary:`正文组成 · …`(点题主要块序)。 硬规则: 1. 合法 JSON;含公共三段外壳。 -2. 正文键固定:依据的体验与呈现、版式隐喻、可见块、监控栏锚点、隐藏段、前端拆分与美化、拼装与终稿。 -3. `可见块` 至少 1 条(通常含正文);`隐藏段`/`字段草稿` 可 `[]`。 -4. 禁止把本步写成程序 API schema;禁止 `order`(上下文投影序)。 -5. 自评:可见完备 / 监控克制 / 可实现。 -6. summary:`正文组成 · {brief 缩略}`。 -7. 兼容旧称产物 tag「设计.回复格式」:细化终稿可读新 tag,旧局可仍挂旧名。 +2. 正文须含 `呈现壳.shell_id`(四大之一)与 `微调`;可见块只挂壳已有区域。 +3. `可见块` 至少 1 条(通常含 body);禁止自造区域 id / 第五种壳。 +4. 禁止程序 API schema、context `order`、完整 CSS。 +5. 自评:可见完备 / 监控克制 / 可实现(=壳适配是否可执行)。 +6. summary:`正文组成 · 适配 {shell_id} · {brief 缩略}`。 +7. 兼容旧称产物 tag「设计.回复格式」。 ## checklist ```checklist -- [ ] 明确是用户可见版式,而非程序报文? -- [ ] 可见块有顺序与职责?监控栏未塞不变名片字段? -- [ ] 隐藏段(若有)定界与用途清楚?前端拆分有或明确单流? -- [ ] 自评三维?未越权重写文风或 side_effects? +- [ ] 是「选壳+微调」,不是从零设计布局? +- [ ] shell_id 在四大壳内?可见块未超出该壳分区? +- [ ] 微调轴已填或明确默认?监控栏未塞死名片字段? +- [ ] 隐藏段(若有)定界清楚?未写 CSS 工程? ``` ## examples ```examples 好: -- 块序:监控栏 → 正文 → 文末吐槽;隐藏段 <<>> 维护好感变更。 -- 书信局:抬头/日期/正文/落款;无监控栏。 -- 监控栏只盯:体力、攻略目标好感、场景可交互对象——不写主角姓名性别。 +- 适配 chat_monitor:微调监控字段=好感/今日话题;body 显示名=「短信」。 +- 适配 chapter_reader:只开 header+body;aside 关。 +- 适配 turn_panel:开 suggested_actions;监控只盯体力/好感。 坏: -- 把 settlement.v1 字段表当「回复格式」。 -- 监控栏列出姓名、性别、种族等死设定。 -- 只写「要好看的 UI」无块清单。 +- 「我们自定义一种左右分栏新界面」。 +- 不写 shell_id,只写「要好看的 UI」。 +- 监控栏列出姓名、性别等死设定。 ``` diff --git a/skills/dialogue/world-simulator/modules/status-bar/prompt.md b/skills/dialogue/world-simulator/modules/status-bar/prompt.md index 4f71a6b..6d1ab5b 100644 --- a/skills/dialogue/world-simulator/modules/status-bar/prompt.md +++ b/skills/dialogue/world-simulator/modules/status-bar/prompt.md @@ -129,17 +129,17 @@ summary:`设计监控栏 · …`(点题监控对象)。 "维度": [ { "名": "属性妥当", - "分数": 0, + "分数": 8, "说明": "逐字段:无死名片;该盯的生存/关系/场景信息未漏" }, { "名": "克制度", - "分数": 0, + "分数": 8, "说明": "字段是否够少、展示是否够短" }, { "名": "可接线", - "分数": 0, + "分数": 8, "说明": "来源意图是否接得上变量/投影/正文组成" } ], diff --git a/skills/dialogue/world-simulator/modules/variable-context/prompt.md b/skills/dialogue/world-simulator/modules/variable-context/prompt.md index f49aef1..cfd442b 100644 --- a/skills/dialogue/world-simulator/modules/variable-context/prompt.md +++ b/skills/dialogue/world-simulator/modules/variable-context/prompt.md @@ -111,17 +111,17 @@ summary:`变量控制上下文 · …`。 "维度": [ { "名": "视野正确", - "分数": 0, + "分数": 8, "说明": "各槽该见/不该见是否清楚" }, { "名": "旁观可用", - "分数": 0, + "分数": 8, "说明": "旁观汇总是否够用且未整表;不需要旁观则 N/A" }, { "名": "防剧透", - "分数": 0, + "分数": 8, "说明": "未解锁与禁止泄露是否落实" } ], diff --git a/skills/dialogue/world-simulator/modules/variable-design/prompt.md b/skills/dialogue/world-simulator/modules/variable-design/prompt.md index d8dcfa8..22f0fe6 100644 --- a/skills/dialogue/world-simulator/modules/variable-design/prompt.md +++ b/skills/dialogue/world-simulator/modules/variable-design/prompt.md @@ -166,17 +166,17 @@ summary:`变量设计 · …`(点题主真值 + 有无映射表)。 "维度": [ { "名": "承重度", - "分数": 0, + "分数": 8, "说明": "真值是否只覆盖必须跨轮记住的状态" }, { "名": "可维护", - "分数": 0, + "分数": 8, "说明": "维护语句是否可解析;映射索引是否接得上实例/规则" }, { "名": "克制度", - "分数": 0, + "分数": 8, "说明": "无派生可写格膨胀;无用 chance 冒充改表" } ], diff --git a/skills/dialogue/world-simulator/modules/worker-spec/prompt.md b/skills/dialogue/world-simulator/modules/worker-spec/prompt.md index f716178..32a647e 100644 --- a/skills/dialogue/world-simulator/modules/worker-spec/prompt.md +++ b/skills/dialogue/world-simulator/modules/worker-spec/prompt.md @@ -11,11 +11,11 @@ name: 游玩拓扑 id: worker-spec artifact: 设计.worker规格 declaration: > - 勾选固定游玩槽位(主世界层 / 叙事转述 / 可选角色视角 / 可选机遇裁定;写手路径为大纲+章节), + 勾选固定游玩槽位(旁观维护 / 主世界层 / 叙事转述 / 可选角色视角 / 可选机遇裁定;写手路径为大纲+章节), 禁止自由发明新的执行单元 ref when: | 体验契约已大致清楚,需要决定游玩期启用哪些固定槽时; - 或要修订已勾选槽位(开/关 perspective、chance 等)时。 + 或要修订已勾选槽位(开/关 auditor、perspective、chance 等)时。 when_not: | 体验/站位仍混沌 → 先美学纲领与交互范式。 只需收成完整运行规格 → 交给细化终稿(本步只交槽位勾选)。 @@ -24,6 +24,7 @@ boundary: | 本能力:输出 play_slots(及写手路径的 writing_slots),可选覆盖挂载说明;不写完整 设计.worker集。 细化终稿:按本步勾选展开 workers、合并常驻与 tables。 变量设计 / 变量控制上下文:真值与投影,不是推理槽。 + 旁观维护(auditor):副 LLM,表/规则检查,默认空操作;无长对话史。 机遇裁定(chance):按需程序工具槽,不进每轮管线。 叙事指南与故事推进:挂到转述槽(推进可兼挂主世界层);世界/机制:挂到主世界层——本步只点名槽。 ``` @@ -34,10 +35,11 @@ boundary: | 游玩时用哪些固定槽?(只勾选,不要发明新角色名当「新系统」) 世界模拟类常见: -1. 主世界层(裁决,几乎总要)——要 / 不要 -2. 叙事转述(写你看见的正文,几乎总要)——要 / 不要 -3. 角色视角(仅当有强秘密、不能进主世界层时)——要 / 不要(默认不要) -4. 机遇裁定(骰子/抽签/比点等真随机,按需调用、不进每轮)——要 / 不要(默认不要;有战斗检定、抽签事件时建议开) +1. 旁观维护(表/规则检查,默认每轮上场但多数轮空操作)——要 / 不要(默认要) +2. 主世界层(裁决,几乎总要)——要 / 不要 +3. 叙事转述(写你看见的正文,几乎总要)——要 / 不要 +4. 角色视角(仅当有强秘密、不能进主世界层时)——要 / 不要(默认不要) +5. 机遇裁定(骰子/抽签/比点等真随机,按需调用、不进每轮)——要 / 不要(默认不要;有战斗检定、抽签事件时建议开) 写手/扩写类常见:大纲/细纲 + 章节正文(固定两槽,同上只勾选)。 @@ -52,18 +54,19 @@ boundary: | 核心操作:让用户勾选固定槽,写成 `play_slots`(世界模拟)或 `writing_slots`(扩写)。**禁止**新建未在固定列表中的 ref。 固定 ref 白名单: -- 世界模拟每轮:`world-simulator`(gm)、`narrator`、`role-decide`(perspective,默认关) +- 世界模拟每轮:`auditor`(旁观维护)、`world-simulator`(gm)、`narrator`、`role-decide`(perspective,默认关) - 世界模拟按需:`chance`(机遇裁定,默认关;invocation=on_demand) - 扩写:`outline`、`chapter-writer` - 禁止:`variable-update`、自造 kebab、为世界观/性格再拆槽 执行顺序: 1. 读配方与体验契约,判断路径:世界模拟 vs 写手分段。 -2. 默认世界模拟:`gm: true, narrator: true, perspective: false, chance: false`; +2. 默认世界模拟:`auditor: true, gm: true, narrator: true, perspective: false, chance: false`; + 调度序 auditor → perspective? → gm → narrator; 仅信息隔离才开 perspective;需要骰子/抽签/比点等真随机时开 chance。 3. 写手路径:`outline` + `chapter-writer` 默认都开;用户明确只要正文则可关 outline。 4. 可写简短 `mount_notes`(哪类上游产物挂哪槽),不粘贴长文。 -5. 输出 JSON。summary:`游玩拓扑 · gm+转述` 或 `游玩拓扑 · gm+转述+机遇` 等。 +5. 输出 JSON。summary:`游玩拓扑 · 旁观+gm+转述` 或 `游玩拓扑 · gm+转述+机遇` 等。 若程序已发默认问题:禁止重复同一开场;在首答上补洞。 ``` @@ -72,8 +75,8 @@ boundary: | ```principles 1. 只勾选,不发明:ref 必须在白名单内。 -2. 默认少槽:世界模拟 = 主世界层 + 转述;perspective 默认关。 -3. 变量 / Data / Progressive 不是执行单元。 +2. 默认:世界模拟 = 旁观维护 + 主世界层 + 转述;perspective 默认关。 +3. 变量 / Data / Progressive 不是执行单元;旁观维护只出 maintain.v1,不写真相。 4. 删掉检验仍适用:关某个槽要说得清损失什么。 5. 本步不输出完整 Worker 集、不写 tables 全文(交给细化终稿 / 变量设计)。 6. 可修订已有勾选,不要为「更聪明」加槽。 @@ -98,6 +101,7 @@ boundary: | "brief": "一句话:本局启用哪些固定槽", "path": "world_sim|writing", "play_slots": { + "auditor": true, "gm": true, "narrator": true, "perspective": false, @@ -108,6 +112,7 @@ boundary: | "chapter_writer": true }, "mount_notes": [ + "生成规则/变量合同/旁观摘要 → auditor(无长对话史)", "叙事指南与故事推进 → narrator(推进兼 gm)", "世界/机制/变量规则 → gm", "真随机检定 → chance(按需)", @@ -120,7 +125,7 @@ boundary: | 填写规则: - `path=world_sim` 时必须有 `play_slots`;`writing_slots` 可省略。 - `path=writing` 时必须有 `writing_slots`;`play_slots` 可省略。 -- `chance` 缺省视为 false;为 true 时不进每轮序,仅可按需调度。 +- `auditor` / `chance` 缺省:auditor 视为 true,chance 视为 false;chance 为 true 时不进每轮序,仅可按需调度。 - 不要输出自造 `actors[]` / 自由 `workers[]`。 - 旧产物若含 `actors[]`:本步应改写为槽位勾选,不再追加自定义 ref。 @@ -128,7 +133,7 @@ boundary: | ```checklist - [ ] 是否只有白名单槽,无自造 ref? -- [ ] perspective / chance / 只要正文等非常规选择是否有理由? +- [ ] auditor / perspective / chance / 只要正文等非常规选择是否有理由? - [ ] 是否误把变量管理做成槽?是否误把 chance 当成每轮 LLM? - [ ] 是否误交完整 设计.worker集? ``` @@ -137,7 +142,7 @@ boundary: | ```examples 好: -- play_slots: gm+narrator,perspective/chance false;mount_notes 一行。 +- play_slots: auditor+gm+narrator,perspective/chance false;mount_notes 一行。 - 有凶手真名不能进 GM:perspective true,并说明只出反应建议。 - 需要检定/抽签:chance true(按需程序工具)。 diff --git a/skills/dialogue/world-simulator/modules/world-blueprint/prompt.md b/skills/dialogue/world-simulator/modules/world-blueprint/prompt.md index 84e38b4..e6c7dec 100644 --- a/skills/dialogue/world-simulator/modules/world-blueprint/prompt.md +++ b/skills/dialogue/world-simulator/modules/world-blueprint/prompt.md @@ -1,4 +1,4 @@ -# 舞台骨架 +# 舞台骨架 > 技能文档。程序只切割下方 **fence 块**;`##` 标题仅供人读。 > 产物外壳:`docs/context-fragment-design.md`;范例:`aesthetics-interaction` / `mechanism`。 @@ -212,17 +212,17 @@ summary:`舞台骨架 · …`(点题尺度 / 社会结构或世界状况) "维度": [ { "名": "覆盖度", - "分数": 0, + "分数": 8, "说明": "能否覆盖用户已表达的舞台/社会/状况/变造硬要求" }, { "名": "充分度", - "分数": 0, + "分数": 8, "说明": "骨架是否够下游引用,并撑起用户想要的体验发生与运转" }, { "名": "克制度", - "分数": 0, + "分数": 8, "说明": "是否只写会上台部分;无地图百科、物种志、民俗全书膨胀" } ], @@ -244,7 +244,7 @@ summary:`舞台骨架 · …`(点题尺度 / 社会结构或世界状况) 硬规则(公共三段 + 本技正文): 1. 合法 JSON;必含 `schema` / `技能` / `brief` / `正文` / `自评` / `追问`(题目可 `[]`)。 -2. **公共三段**:正文=舞台主体;自评=覆盖度/充分度/克制度;追问=导语+建议选项。 +2. **公共三段**:正文=舞台主体;自评=覆盖度/充分度/克制度(**分数 0–10 十分制**);追问=导语+建议选项。 3. **本技正文**须含:依据体验、舞台尺度、基底与变造、**社会结构**、**世界状况**、关键舞台区、未展开范围、覆盖检验。 4. 社会结构=谁在台上互动(种类/势力/阶层,格局级);世界状况=世界如何运转(灾变/通道/习俗等硬部件 + 日常软条件)。无则空数组,但须在覆盖检验说明「为何无需」。 5. `mount` 默认主世界层;**禁止**写 `order`。`稳变` 多为 `stable`。 diff --git a/skills/dialogue/world-simulator/orchestrator.md b/skills/dialogue/world-simulator/orchestrator.md index 4bbeb76..ef10637 100644 --- a/skills/dialogue/world-simulator/orchestrator.md +++ b/skills/dialogue/world-simulator/orchestrator.md @@ -76,7 +76,12 @@ design → 用户手动进 play play - 用户输入 → 声明内 worker → 终稿 + 用户输入 + → auditor(旁观维护,默认空操作;无长对话史) + → perspective?(仅强信息隔离) + → gm / world-simulator(主世界层,settlement.v1;variable_changes 由 Runtime 合并) + → narrator(用户可见正文;通常在此 review) + chance 仅按需;禁止发明未声明 ref ``` ## 启动(agent-first) @@ -104,6 +109,8 @@ play |--------|------------------|----------------| | design-* | true | user_confirmed | | opening-generator | true | user_confirmed | +| auditor / world-simulator / role-decide | false(play) | continue(声明默认) | +| narrator | false(play 调度) | review(等人看终稿) | ## 总管优先行为 @@ -113,6 +120,7 @@ play 4. `waiting_user(review_artifact)` → 引导验收 5. reject → 收修订 → 重跑同一 worker(含修订流程 = 再调味) 6. 终稿(含 `设计.worker集`)已 accept 且需开局 → `opening-generator` +7. **play**:按 `play_slots` 序 `auditor → perspective? → gm → narrator`;`pendingSideEffectWorkers` 优先 ## 禁用行为 @@ -121,3 +129,4 @@ play - 一次 design-flow 排死全程固定长链(应增量) - 调度声明未列出的 play ref - Agent 挑选模型 +- play 中把旁观维护当正文作者,或跳过 gm 直接 narrator diff --git a/skills/dialogue/world-simulator/recipes/world-simulator/recipe.yaml b/skills/dialogue/world-simulator/recipes/world-simulator/recipe.yaml index 80f02b6..ea3b599 100644 --- a/skills/dialogue/world-simulator/recipes/world-simulator/recipe.yaml +++ b/skills/dialogue/world-simulator/recipes/world-simulator/recipe.yaml @@ -16,7 +16,7 @@ process: - 体验契约稍清后即可确认游玩槽(「游玩拓扑」或按默认:主世界层 + 叙事转述,角色视角默认关);此后写上下文时只标挂谁,不标最终顺序。 - 对照技能池「何时用 / 何时不用」,只排近期缺的 1~4 步;不预设固定长链。 - 中段软顺序:机制/舞台支点 → 要不要真值 → 有门控再排变量与分档/事件 → 叙事/正文组成/监控栏按需;每块产物带 mount(挂哪些槽)与可选「稳/变」信号,勿写死 order。 - - 用户看见的版式用「正文组成」(非程序报文);会变信息用「设计监控栏」(忌姓名性别等死字段)。 + - 用户看见的呈现:用「正文组成」**选固定壳并微调**(见 docs/play-presentation-shells.md),不是从零设计 UI;会变信息用「设计监控栏」。 - 需要可引用舞台时用「舞台骨架」;识别体验支点用实现机制;同类内容难稳定生成用生成规则,需预生成再排具体实例。 - 需要门控/分档/程序事件:先「变量设计与更新规则」,再「变量控制上下文」。变量与 Progressive 不是执行单元。 - 挂载归属(非顺序):世界/机制/生成 → 主世界层;叙事指南与故事推进 → 叙事转述(推进兼挂主世界层);美学进常驻。勿为世界观或性格表另开槽。拓扑图谱在固定槽已够用时通常跳过。 diff --git a/skills/dialogue/world-simulator/worker-templates/README.md b/skills/dialogue/world-simulator/worker-templates/README.md index a6160ea..3000a12 100644 --- a/skills/dialogue/world-simulator/worker-templates/README.md +++ b/skills/dialogue/world-simulator/worker-templates/README.md @@ -18,19 +18,21 @@ Runtime 执行时读 Worker 集条目,不直接读本目录。 ```json { - "play_slots": { "gm": true, "narrator": true, "perspective": false, "chance": false }, + "play_slots": { "auditor": true, "gm": true, "narrator": true, "perspective": false }, "workers": [] } ``` | 槽 | 默认 ref | 模板 | |----|----------|------| +| auditor | `auditor` | `auditor.yaml` — maintain.v1;默认空操作;无长对话史 | | gm | `world-simulator` | `world-simulator.yaml` — 裁决包 settlement.v1 | | narrator | `narrator` | `narrator.yaml` — 只读裁决包写正文 | | perspective | `role-decide` | `role-decide.yaml` — 可选知密视角 | | chance(按需) | `chance` | `chance.yaml` — 程序骰子/抽签/比点;不进每轮管线 | -变量 / Progressive:**不是**独立 LLM 模板;见 `docs/progressive-data-design.md`(真值 + side_effects)。 +变量 / Progressive:**不是**独立 LLM 模板;见 `docs/progressive-data-design.md`(真值 + side_effects)。 +Runtime:写入 `运行.本轮.裁决` 时自动合并 `variable_changes` → `变量.当前`;写入 `运行.本轮.旁观` 时按需合并 `table_ops`。 ## 其它文件 diff --git a/skills/dialogue/world-simulator/worker-templates/auditor.yaml b/skills/dialogue/world-simulator/worker-templates/auditor.yaml new file mode 100644 index 0000000..3848300 --- /dev/null +++ b/skills/dialogue/world-simulator/worker-templates/auditor.yaml @@ -0,0 +1,35 @@ +id: auditor +label: 旁观维护 +role: auxiliary +duty: > + 副 LLM:每轮检查表字段与生成规则是否需要补充;默认空操作。 + 只出 maintain.v1(运行.本轮.旁观);不写用户正文、不写真相权威。 + 上下文几乎不要对话历史——合同 + 表 + 本轮工单即可。 +when: 每轮用户输入后、主世界层裁决之前 +suggested_context: + static: + - 设计.worker集 + - 设计.变量设计与更新规则 + - 设计.生成规则 + - 设计.变量控制上下文 + dynamic: + - 用户.最新输入 + - 变量.当前 + - 上下文.旁观.状态摘要 + - 运行.本轮.工单 +suggested_outputs: + - 运行.本轮.旁观 +prompt_excerpt: | + 你是旁观维护(副 LLM),不是主世界层,也不是叙事作者。 + 读合同(生成规则/变量规则/旁观摘要)与当前表,对照「用户.最新输入」判断本轮要不要补表或触发规则。 + 「运行.本轮.旁观」必须是 JSON,schema 为 maintain.v1: + { + "schema": "maintain.v1", + "need_generate": false, + "table_ops": [], + "notes": [] + } + 多数轮保持空:need_generate=false、table_ops=[]、notes=[]。 + 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 393edeb..a865d5b 100644 --- a/skills/dialogue/world-simulator/worker-templates/narrator.yaml +++ b/skills/dialogue/world-simulator/worker-templates/narrator.yaml @@ -2,23 +2,42 @@ id: narrator label: 叙事转述 role: transcription duty: > - 只读「运行.本轮.裁决」结构化包 + 文风/叙事常驻,组装「输出.用户展示」。 - 只改表达,不改事实、不补裁决、不读完整真值表(除非包内已写明可见后果)。 + 只读「运行.本轮.裁决」结构化包 + 文风/叙事常驻 + 设计.正文组成(壳适配), + 组装「输出.用户展示」。只改表达,不改事实、不补裁决。 when: 主世界层裁决包已写入 suggested_context: static: - 设计.worker集 - 设计.叙事指南与故事推进 - 设计.叙事指南 + - 设计.正文组成 + - 设计.监控栏 dynamic: - 运行.本轮.裁决 - 用户.最新输入 + - 变量.当前 suggested_outputs: - 输出.用户展示 presentation_hints: - mode: prose | markdown | mixed - tone: 依 Worker 集 presentation / 叙事指南 + mode: present.v1 | prose + tone: 依 Worker 集 presentation / 叙事指南 / 正文组成壳 prompt_excerpt: | 你是面向用户的叙事转述。输入是结构化裁决包(settlement.v1),不是自由发挥的世界真相。 - 按文风写可读正文;禁止写出 do_not_say;禁止编造包中没有的事实或变量。 - 若裁决包含 tone_hint / npc_moves,须体现在叙述中。 + 先读「设计.正文组成」的呈现壳 shell_id(prose|chat_monitor|turn_panel|chapter_reader): + - prose:可直接输出 Markdown 正文(程序会落入 body)。 + - 其它壳:优先输出 JSON,schema 为 present.v1: + { + "schema": "present.v1", + "shell": "<与正文组成一致>", + "blocks": { + "monitor": { "字段名": "短值" }, + "header": "可选", + "body": "用户可读正文", + "footer": "可选", + "aside": "可选" + }, + "meta": { "suggested_actions": ["…"] } + } + 只填该壳已有区域;监控字段来自设计.监控栏 / 投影,禁止编造真值数字若 do_not_say 禁止。 + 禁止写出 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 e942282..bb98bfa 100644 --- a/skills/dialogue/world-simulator/worker-templates/world-simulator.yaml +++ b/skills/dialogue/world-simulator/worker-templates/world-simulator.yaml @@ -14,6 +14,7 @@ suggested_context: - 用户.最新输入 - 运行.事件流 - 变量.当前 + - 运行.本轮.旁观 - 上下文.角色态度 - 大纲.当前章 suggested_outputs: @@ -21,9 +22,10 @@ suggested_outputs: - 运行.事件流 - 运行.本轮.变量变更 prompt_excerpt: | - 你是中立主世界层(GM),不是文学作者。读真值与已投影上下文,对用户行动作可复核裁决。 - 「运行.本轮.裁决」必须是 JSON,schema 为 settlement.v1,至少尽量包含: + 你是中立主世界层(GM),不是文学作者。读真值、已投影上下文,以及本轮旁观维护包(若有)。 + 对用户行动作可复核裁决。「运行.本轮.裁决」必须是 JSON,schema 为 settlement.v1,至少尽量包含: player_action、resolved[]、visible_now、npc_moves[]、variable_changes[]、do_not_say[]、tone_hint。 + variable_changes 由 Runtime 合并进变量.当前——请写清 key 与 to/delta。 叙事质量 intentionally 低——用户正文交给 narrator。 需要真随机(骰子/抽签/比点)时:不要自己编结果;依赖已启用的机遇裁定(chance) 写出的「运行.本轮.机遇」,或由编排器先 run_worker chance。 diff --git a/src/main-agent/main-agent.ts b/src/main-agent/main-agent.ts index de29a2f..ec2fd85 100644 --- a/src/main-agent/main-agent.ts +++ b/src/main-agent/main-agent.ts @@ -57,6 +57,14 @@ ${workerLines} - **禁止**替用户猜测或改选配方 - **禁止**一次编排排死全程固定 DAG +## play 优先顺序(lifecycleStage=play) +- 严格按声明管线顺序调度(play_slots 展开序):**auditor(旁观维护)→ perspective? → gm(主世界层)→ narrator(转述)** +- auditor / gm / perspective 的 acceptance 多为 continue:跑完立刻调度下一槽,不要中途 ask_user +- 仅 narrator(或规格标明 review 的终稿槽)停下来等人看 +- chance 仅在需要真随机时按需 run_worker,不插入每轮固定序 +- slots.pendingSideEffectWorkers 非空时优先调度其中 workerId +- **禁止**发明未声明的 play ref;**禁止**把旁观维护当成正文作者 + 输出必须是 JSON 对象,字段: { "action": "ask_user" | "run_worker" | "create_temp_worker" | "review_blackboard" | "finish", @@ -102,15 +110,19 @@ export class MainAgent { export function buildMainAgentUserPrompt(context: MainAgentContext): string { const { session, blackboardIndex, availableWorkers } = context; + const stage = String(session.slots?.uiLifecycleStage ?? "design"); + const pendingSide = session.slots?.pendingSideEffectWorkers; return JSON.stringify( { runtimePhase: session.phase, waitingReason: session.waitingReason, + lifecycleStage: stage, flowId: session.flowId, currentStepId: session.currentStepId, currentWorkerId: session.currentWorkerId, acceptanceMode: session.acceptanceMode, slots: session.slots, + pendingSideEffectWorkers: pendingSide ?? null, pendingDecision: session.pendingDecision ? { id: session.pendingDecision.id, @@ -129,7 +141,9 @@ export function buildMainAgentUserPrompt(context: MainAgentContext): string { blackboardIndex, availableWorkers, instruction: - "根据当前状态决定下一步。尚无设计.创作流程 → design-flow;有未完成步骤 → design-step;steps 做完但 status=open → 再 design-flow;禁止旧 design-core/fixed/worker/refine。已 accept 且需开局 → opening-generator。进 play 由用户手动。", + stage === "play" + ? "play:按声明序 auditor→perspective?→gm→narrator;continue 槽连跑;仅终稿 review 停;pendingSideEffectWorkers 优先;禁止未声明 ref。" + : "根据当前状态决定下一步。尚无设计.创作流程 → design-flow;有未完成步骤 → design-step;steps 做完但 status=open → 再 design-flow;禁止旧 design-core/fixed/worker/refine。已 accept 且需开局 → opening-generator。进 play 由用户手动。", }, null, 2, diff --git a/src/main-agent/tool-loop.ts b/src/main-agent/tool-loop.ts index 7ae3d8c..40fda55 100644 --- a/src/main-agent/tool-loop.ts +++ b/src/main-agent/tool-loop.ts @@ -93,6 +93,14 @@ function buildToolLoopSystemPrompt( - 未声明开局、用户也未要求开场时,不要硬调 opening-generator。 - **禁止**替用户猜测或改选配方。 - **禁止**一次编排排死全程固定长链。 + +## play 阶段提示(lifecycleStage=play 时优先) +- 管线顺序:**auditor(旁观维护)→ perspective? → gm(主世界层)→ narrator**。 +- auditor/gm/perspective 多为 continue:写完立刻 run 下一槽,不要中途 ask_user。 +- 只在 narrator(或 review 终稿槽)停给人看。 +- chance 按需;pendingSideEffectWorkers 非空则优先调度。 +- 禁止未声明 play ref;旁观维护默认空操作,不是正文作者。 + 当前 skill 可用 worker: ${workerLines} diff --git a/src/runtime/phase-runtime.ts b/src/runtime/phase-runtime.ts index fc13b16..5384aca 100644 --- a/src/runtime/phase-runtime.ts +++ b/src/runtime/phase-runtime.ts @@ -91,6 +91,16 @@ import { parseSideEffectRules, stringifyFiredRegistry, } from "../blackboard/table-side-effects.js"; +import { + SETTLEMENT_TAG, + parseSettlementPacket, + settlementChangesToTablePatch, +} from "../skills/settlement-packet.js"; +import { + MAINTAIN_TAG, + maintainOpsToTablePatch, + parseMaintainPacket, +} from "../skills/maintain-packet.js"; import { mountResidentContextForWorker as writeResidentContextTags, parseResidentContext, @@ -1058,9 +1068,61 @@ export class PhaseRuntime { if (nextDoc) { this.applyTableSideEffects(prevDoc, nextDoc, workerId); } + // 裁决包 / 旁观维护包 → 合并进变量.当前(可触发 side_effects) + if (tag === SETTLEMENT_TAG) { + this.applySettlementVariableChanges(toWrite, workerId); + } else if (tag === MAINTAIN_TAG) { + this.applyMaintainTableOps(toWrite, workerId); + } return toWrite; } + /** settlement.v1.variable_changes → 变量.当前 */ + private applySettlementVariableChanges( + settlementRaw: string, + workerId: string, + ): void { + const view = parseSettlementPacket(settlementRaw); + const changes = view.packet?.variable_changes; + if (!view.ok || !changes?.length) return; + const current = parseTableDoc( + this.blackboard.getContentByTag("变量.当前"), + ); + const patch = settlementChangesToTablePatch(changes, current); + if (!patch) return; + this.writeWorkerTagContent( + "变量.当前", + stringifyTableDoc(patch), + workerId, + ); + this.onMessage( + `[裁决合并] 变量.当前 ← ${changes.map((c) => c.key).join("、")}`, + ); + } + + /** maintain.v1.table_ops → 变量.当前(及其他已支持表) */ + private applyMaintainTableOps(maintainRaw: string, workerId: string): void { + const view = parseMaintainPacket(maintainRaw); + const ops = view.packet?.table_ops; + if (!view.ok || !ops?.length) return; + const current = parseTableDoc( + this.blackboard.getContentByTag("变量.当前"), + ); + const patch = maintainOpsToTablePatch(ops, current, "变量.当前"); + if (!patch) return; + this.writeWorkerTagContent( + "变量.当前", + stringifyTableDoc(patch), + workerId, + ); + const keys = ops + .filter((o) => (o.tag?.trim() || "变量.当前") === "变量.当前") + .map((o) => o.key); + if (keys.length) { + this.onMessage(`[旁观维护] 变量.当前 ← ${keys.join("、")}`); + } + } + /** 避免 syncSlots 重复把同一句用户输入追加进历史 */ private lastAppendedUserHistory = ""; diff --git a/src/server/display-labels.ts b/src/server/display-labels.ts index 17a2c67..05d1dd5 100644 --- a/src/server/display-labels.ts +++ b/src/server/display-labels.ts @@ -28,6 +28,7 @@ const WORKER_LABELS: Record = { narrator: "叙事转述", "role-decide": "角色决策", "world-simulator": "主世界层", + auditor: "旁观维护", chance: "机遇裁定", "round-present": "回合呈现", outline: "大纲 / 细纲", diff --git a/src/server/session-manager.ts b/src/server/session-manager.ts index 45d3b76..01fcf8c 100644 --- a/src/server/session-manager.ts +++ b/src/server/session-manager.ts @@ -62,6 +62,7 @@ import type { ActiveSkillSnapshot } from "../types/runtime.js"; import type { BlackboardItem } from "../types/blackboard.js"; import { Blackboard } from "../blackboard/blackboard.js"; import { parseWorkerSetYaml } from "../skills/worker-set-parse.js"; +import { parseShellAdaptationFromReplyFormat } from "../skills/present-packet.js"; import { parseAcceptedUnits, isDesignDiskWorker } from "../skills/creation-units.js"; import { formatWorkerSetForUser, @@ -185,6 +186,14 @@ export type SessionView = { selectedRecipe?: { id: string; name: string; declaration: string } | null; /** 黑板与定稿上下文(强可读) */ boardPanel?: BoardPanelView; + /** 游玩呈现壳微调(来自设计.正文组成) */ + presentationTweaks?: { + shell_id?: string; + tone_chrome?: string; + show_suggested_actions?: boolean; + block_labels?: Record; + empty_states?: Record; + }; /** 总管正在输出的思维链(流式,轮询用) */ agentThinking?: string; /** Agent / Worker 流式输出(轮询用) */ @@ -1691,6 +1700,10 @@ export class SessionManager { lifecycleStage, { workerSetYaml }, ); + const shellAdapt = parseShellAdaptationFromReplyFormat( + s.runtime.getBlackboard().getContentByTag("设计.正文组成") ?? + s.runtime.getBlackboard().getContentByTag("设计.回复格式"), + ); return { id, @@ -1735,6 +1748,12 @@ export class SessionManager { s.recipeOptions, ), boardPanel: buildBoardPanelFromRuntime(s.runtime.getBlackboard()), + presentationTweaks: shellAdapt + ? { + shell_id: shellAdapt.shell_id, + ...shellAdapt.tweaks, + } + : undefined, liveStream: this.buildLiveStreamView(id), agentThinking: this.agentThinkingLive.get(id), resumed: resumed || undefined, diff --git a/src/skills/context-fragment.ts b/src/skills/context-fragment.ts index 1cb8c95..5c63f1f 100644 --- a/src/skills/context-fragment.ts +++ b/src/skills/context-fragment.ts @@ -40,6 +40,29 @@ function asStringList(v: unknown): string[] { return v.map((x) => asString(x)).filter((x): x is string => Boolean(x)); } +/** 自评分数展示:0–10;兼容旧百分数 */ +function formatSelfScoreLabel(score: unknown): string { + if (typeof score === "number" && Number.isFinite(score)) { + if (score >= 0 && score <= 10) { + const shown = Number.isInteger(score) ? String(score) : String(Math.round(score * 10) / 10); + return `${shown}/10`; + } + if (score > 10 && score <= 100) { + const ten = Math.round((score / 10) * 10) / 10; + const shown = Number.isInteger(ten) ? String(ten) : String(ten); + return `${shown}/10`; + } + } + if (typeof score === "string" && score.trim()) { + const s = score.trim(); + if (/\/\s*10$/i.test(s)) return s.replace(/\s+/g, ""); + const n = Number(s.replace(/%$/, "")); + if (Number.isFinite(n)) return formatSelfScoreLabel(n); + return s; + } + return "?"; +} + function normalizeStability(v: unknown): ContextFragmentStability | undefined { if (v === "stable" || v === "semi" || v === "volatile") return v; if (v === "稳" || v === "少变") return "stable"; @@ -164,7 +187,7 @@ export function contextFragmentToView(raw: unknown): ContextFragmentView { const lines = dims.map((d) => { if (!d || typeof d !== "object") return String(d); const r = d as Record; - return `${r.名 ?? "?"}:${r.分数 ?? "?"}%${r.说明 ? ` — ${r.说明}` : ""}`; + return `${r.名 ?? "?"}:${formatSelfScoreLabel(r.分数)}${r.说明 ? ` — ${r.说明}` : ""}`; }); if (typeof row.薄弱点 === "string" && row.薄弱点.trim()) { lines.push(`薄弱点:${row.薄弱点.trim()}`); @@ -233,10 +256,7 @@ export function extractFragmentAskSidecar(raw: unknown): { const name = asString(r.名) ?? asString(r.维度) ?? "?"; const score = r.分数; const note = asString(r.说明); - const scoreText = - typeof score === "number" || typeof score === "string" - ? `${score}%` - : "?"; + const scoreText = formatSelfScoreLabel(score); assessmentParts.push( note ? `${name} ${scoreText} — ${note}` : `${name} ${scoreText}`, ); diff --git a/src/skills/context-order.ts b/src/skills/context-order.ts index 539f570..aa19e75 100644 --- a/src/skills/context-order.ts +++ b/src/skills/context-order.ts @@ -5,7 +5,12 @@ */ import type { ContextSegmentDef } from "./context-segments.js"; import { extractJsonObjectText } from "./worker-set-parse.js"; -import { parsePlaySlots, type PlaySlotsConfig } from "./play-slots.js"; +import { + parsePlaySlots, + playSlotIdForRef, + PLAY_SLOTS_WITHOUT_DIALOGUE_HISTORY, + type PlaySlotsConfig, +} from "./play-slots.js"; import { DIALOGUE_HISTORY_TAG, isDialogueHistoryRef, @@ -430,8 +435,22 @@ export function mergeContextOrderIntoWorkerSetJson( /** 声明缺 context 时的兜底(与模板/检查器默认对齐的最小集) */ const SYNTH_FALLBACK_CONTEXT: Record< string, - { static: string[]; dynamic: string[] } + { static: string[]; dynamic: string[]; /** 默认不注入对话.历史 */ skipHistory?: boolean } > = { + auditor: { + static: [ + "设计.变量设计与更新规则", + "设计.生成规则", + "设计.变量控制上下文", + ], + dynamic: [ + "用户.最新输入", + "变量.当前", + "上下文.旁观.状态摘要", + "运行.本轮.工单", + ], + skipHistory: true, + }, "world-simulator": { static: ["设计.变量设计与更新规则"], dynamic: ["变量.当前", "运行.事件流", "用户.最新输入", "上下文.角色态度"], @@ -460,7 +479,12 @@ export function synthesizeContextOrderFromWorkers( if (!ref) continue; const fallback = SYNTH_FALLBACK_CONTEXT[ref]; const staticTags = w.context?.static ?? fallback?.static ?? []; - const dynamicTags = w.context?.dynamic ?? fallback?.dynamic ?? ["用户.最新输入"]; + const dynamicTags = + w.context?.dynamic ?? fallback?.dynamic ?? ["用户.最新输入"]; + const slotId = playSlotIdForRef(ref); + const skipHistory = + Boolean(fallback?.skipHistory) || + (slotId != null && PLAY_SLOTS_WITHOUT_DIALOGUE_HISTORY.has(slotId)); const inserts: ContextOrderInsert[] = [ { order: 0, @@ -479,16 +503,17 @@ export function synthesizeContextOrderFromWorkers( projection: tag.includes("事件流") ? "summary" : "full", }); } - inserts.push({ - order: o++, - ref: DIALOGUE_HISTORY_TAG, - projection: "summary", - note: "对话历史(按投影裁剪)", - }); + if (!skipHistory) { + inserts.push({ + order: o++, + ref: DIALOGUE_HISTORY_TAG, + projection: "summary", + note: "对话历史(按投影裁剪)", + }); + } for (const tag of dynamicTags) { if (!tag?.trim()) continue; if (isDialogueHistoryRef(tag)) continue; - // 事件流已可由历史兜底;仍可单独挂 inserts.push({ order: o++, ref: tag.trim(), @@ -504,8 +529,18 @@ export function synthesizeContextOrderFromWorkers( if (!slots.length) return undefined; return renumberContextOrder({ schema: CONTEXT_ORDER_SCHEMA, - brief: "扁平投影序(含对话.历史标签,可编排)", + brief: skipHistoryBrief(slots) + ? "扁平投影序(旁观维护无对话.历史;主世界层等可含历史)" + : "扁平投影序(含对话.历史标签,可编排)", play_slots: playSlots, slots, }); } + +function skipHistoryBrief(slots: ContextOrderSlot[]): boolean { + return slots.some( + (s) => + !s.inserts.some((i) => isDialogueHistoryRef(i.ref)) && + playSlotIdForRef(s.ref) === "auditor", + ); +} diff --git a/src/skills/maintain-packet.ts b/src/skills/maintain-packet.ts new file mode 100644 index 0000000..3b9f6d9 --- /dev/null +++ b/src/skills/maintain-packet.ts @@ -0,0 +1,220 @@ +/** + * 旁观维护包(副 LLM):表/规则补充工单。 + * 存黑板 tag:运行.本轮.旁观 + * 多数轮为空操作;不写真相权威、不写用户正文。 + */ +import { + createTableFromValues, + type TableDoc, +} from "../blackboard/table-cells.js"; +import { extractJsonObjectText } from "./worker-set-parse.js"; + +export const MAINTAIN_SCHEMA = "maintain.v1" as const; +export const MAINTAIN_TAG = "运行.本轮.旁观"; + +export type MaintainNeedGenerate = + | false + | { + rule_id: string; + reason: string; + }; + +export type MaintainTableOp = { + /** set:写入绝对值;delta:在现有值上加减 */ + op: "set" | "delta"; + /** 目标表 tag;默认 变量.当前 */ + tag?: string; + key: string; + value?: unknown; + delta?: number; + note?: string; +}; + +export type MaintainPacket = { + schema: typeof MAINTAIN_SCHEMA; + /** 是否需要按生成规则补产物;默认 false */ + need_generate?: MaintainNeedGenerate; + /** 表字段操作;空数组 = 本轮无需改表 */ + table_ops?: MaintainTableOp[]; + /** 只读意见(漂移/口径),不进用户正文 */ + notes?: string[]; +}; + +export type MaintainPacketView = { + ok: boolean; + parseError?: string; + packet?: MaintainPacket; + empty: boolean; +}; + +function asString(v: unknown): string | undefined { + if (typeof v !== "string") return undefined; + const t = v.trim(); + return t || undefined; +} + +function parseNeedGenerate(raw: unknown): MaintainNeedGenerate | undefined { + if (raw === false || raw === null || raw === undefined) return false; + if (raw === true) return false; + if (typeof raw === "object" && !Array.isArray(raw)) { + const row = raw as Record; + const ruleId = + asString(row.rule_id) ?? asString(row.ruleId) ?? asString(row.规则); + const reason = asString(row.reason) ?? asString(row.原因) ?? ""; + if (!ruleId) return false; + return { rule_id: ruleId, reason }; + } + return undefined; +} + +function parseTableOps(raw: unknown): MaintainTableOp[] | undefined { + if (!Array.isArray(raw)) return undefined; + const out: MaintainTableOp[] = []; + for (const item of raw) { + if (!item || typeof item !== "object" || Array.isArray(item)) continue; + const row = item as Record; + const key = asString(row.key) ?? asString(row.name) ?? asString(row.变量); + if (!key) continue; + const opRaw = asString(row.op) ?? "set"; + const op: "set" | "delta" = opRaw === "delta" ? "delta" : "set"; + const delta = + typeof row.delta === "number" && Number.isFinite(row.delta) + ? row.delta + : undefined; + if (op === "delta" && delta == null) continue; + if (op === "set" && row.value === undefined && row.to === undefined) continue; + out.push({ + op, + tag: asString(row.tag) ?? asString(row.表), + key, + value: row.value ?? row.to, + delta, + note: asString(row.note) ?? asString(row.备注), + }); + } + return out.length ? out : undefined; +} + +export function normalizeMaintainPacket( + row: Record, +): MaintainPacket { + const notesRaw = row.notes ?? row.备注; + const notes = Array.isArray(notesRaw) + ? notesRaw + .map((x) => asString(x)) + .filter((x): x is string => Boolean(x)) + : undefined; + return { + schema: MAINTAIN_SCHEMA, + need_generate: parseNeedGenerate(row.need_generate ?? row.needGenerate), + table_ops: parseTableOps(row.table_ops ?? row.tableOps ?? row.表操作), + notes: notes?.length ? notes : undefined, + }; +} + +export function emptyMaintainPacket(): MaintainPacket { + return { + schema: MAINTAIN_SCHEMA, + need_generate: false, + table_ops: [], + notes: [], + }; +} + +export function isMaintainPacketEmpty(packet: MaintainPacket): boolean { + const need = + packet.need_generate && + typeof packet.need_generate === "object" && + Boolean(packet.need_generate.rule_id); + const ops = packet.table_ops?.length ?? 0; + const notes = packet.notes?.length ?? 0; + return !need && ops === 0 && notes === 0; +} + +export function parseMaintainPacket( + raw: string | undefined | null, +): MaintainPacketView { + const text = raw?.trim(); + if (!text) { + return { + ok: true, + packet: emptyMaintainPacket(), + empty: true, + }; + } + + // 允许模型用极短词表示空操作 + if (/^(无需|无|空|skip|none|noop)$/i.test(text)) { + return { ok: true, packet: emptyMaintainPacket(), empty: true }; + } + + const jsonText = + extractJsonObjectText(text) ?? (text.startsWith("{") ? text : null); + if (!jsonText) { + return { + ok: false, + parseError: "旁观维护包不是 JSON", + empty: true, + }; + } + + try { + const doc = JSON.parse(jsonText) as unknown; + if (!doc || typeof doc !== "object" || Array.isArray(doc)) { + return { + ok: false, + parseError: "旁观维护包不是 JSON 对象", + empty: true, + }; + } + const packet = normalizeMaintainPacket(doc as Record); + return { + ok: true, + packet, + empty: isMaintainPacketEmpty(packet), + }; + } catch (err) { + return { + ok: false, + parseError: + err instanceof Error ? err.message : "旁观维护包 JSON 解析失败", + empty: true, + }; + } +} + +/** + * 将针对某一表 tag 的 ops 编成 TableDoc patch(供 mergeTableCells)。 + * 仅处理 op=set|delta;其它 tag 由调用方过滤。 + */ +export function maintainOpsToTablePatch( + ops: MaintainTableOp[], + current: TableDoc | null, + targetTag = "变量.当前", +): TableDoc | null { + const relevant = ops.filter( + (op) => (op.tag?.trim() || "变量.当前") === targetTag, + ); + if (!relevant.length) return null; + + const byKey = new Map((current?.rows ?? []).map((r) => [r.key, r])); + const values: Record = {}; + const meta: Record = {}; + + for (const op of relevant) { + const existing = byKey.get(op.key); + if (op.op === "delta") { + const cur = existing?.value; + const n = typeof cur === "number" ? cur : Number(cur); + values[op.key] = Number.isFinite(n) + ? (n as number) + (op.delta ?? 0) + : (op.delta ?? 0); + } else { + values[op.key] = op.value; + } + if (op.note) meta[op.key] = { note: op.note }; + } + + if (!Object.keys(values).length) return null; + return createTableFromValues(values, "system", meta); +} diff --git a/src/skills/play-slots.ts b/src/skills/play-slots.ts index 7e8c504..2890969 100644 --- a/src/skills/play-slots.ts +++ b/src/skills/play-slots.ts @@ -1,15 +1,20 @@ /** * 固定游玩槽位:世界模拟路径不再自由发明 workers[], - * 以勾选 gm / narrator / perspective 展开为声明条目。 + * 以勾选 auditor / gm / narrator / perspective 展开为声明条目。 * `chance` 为按需程序槽:不进每轮管线,可被 run_worker / toolcall 调用。 */ import type { WorkerAcceptance, WorkerSetEntry } from "./worker-set-parse.js"; -export type PlaySlotId = "gm" | "narrator" | "perspective"; -/** 按需槽(不进入 perspective→gm→narrator 管线) */ +export type PlaySlotId = "auditor" | "gm" | "narrator" | "perspective"; +/** 按需槽(不进入 auditor→perspective→gm→narrator 管线) */ export type OnDemandSlotId = "chance"; export type PlaySlotsConfig = { + /** + * 旁观维护(副 LLM:表/规则检查,默认空操作);默认 true。 + * 每轮可上场,但不等于每轮改表。缺省键视为开启。 + */ + auditor?: boolean; /** 主世界层(裁决);默认 true */ gm: boolean; /** 叙事转述;默认 true */ @@ -26,6 +31,7 @@ export type PlaySlotsConfig = { }; export const DEFAULT_PLAY_SLOT_REFS: Record = { + auditor: "auditor", gm: "world-simulator", narrator: "narrator", perspective: "role-decide", @@ -39,6 +45,12 @@ export const PLAY_SLOT_META: Record< PlaySlotId, { label: string; purpose: string; defaultAcceptance: WorkerAcceptance } > = { + auditor: { + label: "旁观维护", + purpose: + "副 LLM:检查表与生成规则是否需补充;默认空操作;不写正文、不写真相权威。", + defaultAcceptance: "continue", + }, gm: { label: "主世界层", purpose: "读真值与 Progressive 投影,输出结构化裁决包;可提议变量变更。", @@ -69,21 +81,47 @@ export const ON_DEMAND_SLOT_META: Record< }; /** 推荐回合顺序(启用的槽按此排序;不含按需槽) */ -export const PLAY_SLOT_ORDER: PlaySlotId[] = ["perspective", "gm", "narrator"]; +export const PLAY_SLOT_ORDER: PlaySlotId[] = [ + "auditor", + "perspective", + "gm", + "narrator", +]; + +/** 这些槽的默认上下文不注入「对话.历史」长聊 */ +export const PLAY_SLOTS_WITHOUT_DIALOGUE_HISTORY: ReadonlySet = + new Set(["auditor"]); + +export function playSlotIdForRef(ref: string): PlaySlotId | undefined { + const trimmed = ref.trim(); + for (const id of Object.keys(DEFAULT_PLAY_SLOT_REFS) as PlaySlotId[]) { + if (DEFAULT_PLAY_SLOT_REFS[id] === trimmed) return id; + } + return undefined; +} export function defaultPlaySlots(): PlaySlotsConfig { - return { gm: true, narrator: true, perspective: false, chance: false }; + return { + auditor: true, + gm: true, + narrator: true, + perspective: false, + chance: false, + }; } export function parsePlaySlots(raw: unknown): PlaySlotsConfig | undefined { if (!raw || typeof raw !== "object" || Array.isArray(raw)) return undefined; const row = raw as Record; const base = defaultPlaySlots(); + if (typeof row.auditor === "boolean") base.auditor = row.auditor; + // 兼容旧键名 / 别名 + if (typeof row.maintain === "boolean") base.auditor = row.maintain; + if (typeof row.旁观 === "boolean") base.auditor = row.旁观; if (typeof row.gm === "boolean") base.gm = row.gm; if (typeof row.narrator === "boolean") base.narrator = row.narrator; if (typeof row.perspective === "boolean") base.perspective = row.perspective; if (typeof row.chance === "boolean") base.chance = row.chance; - // 兼容旧键名 if (typeof row.world === "boolean") base.gm = row.world; if (typeof row.transcription === "boolean") base.narrator = row.transcription; if (typeof row.oracle === "boolean") base.chance = row.oracle; @@ -116,7 +154,10 @@ export function refForSlot( } export function enabledPlaySlotIds(slots: PlaySlotsConfig): PlaySlotId[] { - return PLAY_SLOT_ORDER.filter((id) => Boolean(slots[id])); + return PLAY_SLOT_ORDER.filter((id) => { + if (id === "auditor") return slots.auditor !== false; + return Boolean(slots[id]); + }); } export function enabledOnDemandSlotIds(slots: PlaySlotsConfig): OnDemandSlotId[] { @@ -153,17 +194,21 @@ export function workerEntryForSlot( role: id, duty: meta.purpose, when: - id === "perspective" - ? "强信息隔离且本轮需要该角色独立反应时" - : id === "gm" - ? "每轮用户输入后" - : "主世界层裁决包就绪后", + id === "auditor" + ? "每轮用户输入后、主世界层之前" + : id === "perspective" + ? "强信息隔离且本轮需要该角色独立反应时" + : id === "gm" + ? "每轮用户输入后(旁观维护与可选视角之后)" + : "主世界层裁决包就绪后", rationale: - id === "perspective" - ? "知密内容不能进主世界层上下文" - : id === "gm" - ? "裁决与真值变更需要独立推理槽" - : "用户可见正文与裁决分离,避免文风与规则互相挤压", + id === "auditor" + ? "表与规则补充与主裁决分槽,避免挤占对话历史上下文" + : id === "perspective" + ? "知密内容不能进主世界层上下文" + : id === "gm" + ? "裁决与真值变更需要独立推理槽" + : "用户可见正文与裁决分离,避免文风与规则互相挤压", acceptance: meta.defaultAcceptance, invocation: "turn", }; @@ -251,12 +296,23 @@ export function inferPlaySlotsFromWorkers( const refs = new Set( workers.map((w) => w.ref?.trim()).filter((r): r is string => Boolean(r)), ); + const hasAuditor = refs.has(DEFAULT_PLAY_SLOT_REFS.auditor); const hasGm = refs.has(DEFAULT_PLAY_SLOT_REFS.gm); const hasNarrator = refs.has(DEFAULT_PLAY_SLOT_REFS.narrator); const hasPerspective = refs.has(DEFAULT_PLAY_SLOT_REFS.perspective); const hasChance = refs.has(DEFAULT_ON_DEMAND_REFS.chance); - if (!hasGm && !hasNarrator && !hasPerspective && !hasChance) return undefined; + if ( + !hasAuditor && + !hasGm && + !hasNarrator && + !hasPerspective && + !hasChance + ) { + return undefined; + } return { + // 旧规格无 auditor ref → 视为关,避免强行插入 + auditor: hasAuditor, gm: hasGm, narrator: hasNarrator, perspective: hasPerspective, diff --git a/src/skills/present-packet.ts b/src/skills/present-packet.ts new file mode 100644 index 0000000..850be4d --- /dev/null +++ b/src/skills/present-packet.ts @@ -0,0 +1,345 @@ +/** + * 游玩呈现包 present.v1 + 从「设计.正文组成」解析壳适配。 + * 规范:docs/play-presentation-shells.md + */ +import { extractJsonObjectText } from "./worker-set-parse.js"; + +export const PRESENT_SCHEMA = "present.v1" as const; +export const PRESENT_TAG = "输出.用户展示"; + +export const PRESENT_SHELL_IDS = [ + "prose", + "chat_monitor", + "turn_panel", + "chapter_reader", +] as const; + +export type PresentShellId = (typeof PRESENT_SHELL_IDS)[number]; + +export type PresentRegionId = + | "monitor" + | "header" + | "body" + | "footer" + | "aside" + | "hidden"; + +export type PresentShellTweaks = { + tone_chrome?: string; + show_suggested_actions?: boolean; + block_labels?: Record; + empty_states?: Record; +}; + +export type ShellAdaptation = { + shell_id: PresentShellId; + why?: string; + tweaks: PresentShellTweaks; +}; + +export type PresentBlocks = Partial< + Record | unknown[]> +>; + +export type PresentPacket = { + schema: typeof PRESENT_SCHEMA; + shell: PresentShellId; + blocks: PresentBlocks; + meta?: { + suggested_actions?: string[]; + hide?: string[]; + }; +}; + +export type PresentPacketView = { + ok: boolean; + parseError?: string; + packet: PresentPacket; + /** 原文不是 JSON 时回退为 prose body */ + fallbackPlain: boolean; +}; + +const SHELL_SET = new Set(PRESENT_SHELL_IDS); + +export function isPresentShellId(v: unknown): v is PresentShellId { + return typeof v === "string" && SHELL_SET.has(v); +} + +export function defaultShellTweaks( + shell: PresentShellId, +): PresentShellTweaks { + return { + tone_chrome: "default", + show_suggested_actions: shell === "turn_panel", + block_labels: {}, + empty_states: {}, + }; +} + +function asString(v: unknown): string | undefined { + if (typeof v !== "string") return undefined; + const t = v.trim(); + return t || undefined; +} + +function asStringList(v: unknown): string[] | undefined { + if (!Array.isArray(v)) return undefined; + const out = v.map((x) => asString(x)).filter((x): x is string => Boolean(x)); + return out.length ? out : undefined; +} + +function asStringMap(v: unknown): Record | undefined { + if (!v || typeof v !== "object" || Array.isArray(v)) return undefined; + const out: Record = {}; + for (const [k, val] of Object.entries(v as Record)) { + const key = k.trim(); + const s = asString(val); + if (key && s) out[key] = s; + } + return Object.keys(out).length ? out : undefined; +} + +export function normalizePresentShellId( + raw: unknown, + fallback: PresentShellId = "prose", +): PresentShellId { + if (isPresentShellId(raw)) return raw; + const s = asString(raw); + if (s && isPresentShellId(s)) return s; + return fallback; +} + +function normalizeBlocks(raw: unknown): PresentBlocks { + if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {}; + const row = raw as Record; + const out: PresentBlocks = {}; + const keys: PresentRegionId[] = [ + "monitor", + "header", + "body", + "footer", + "aside", + "hidden", + ]; + for (const k of keys) { + if (row[k] !== undefined) out[k] = row[k] as PresentBlocks[typeof k]; + } + // 兼容中文键 + if (out.body === undefined && row.正文 !== undefined) out.body = row.正文 as string; + if (out.monitor === undefined && row.监控 !== undefined) { + out.monitor = row.监控 as PresentBlocks["monitor"]; + } + return out; +} + +export function normalizePresentPacket( + row: Record, + fallbackShell: PresentShellId = "prose", +): PresentPacket { + const shell = normalizePresentShellId( + row.shell ?? row.shell_id ?? row.呈现壳, + fallbackShell, + ); + return { + schema: PRESENT_SCHEMA, + shell, + blocks: normalizeBlocks(row.blocks ?? row.区域), + meta: { + suggested_actions: asStringList( + row.meta && typeof row.meta === "object" + ? (row.meta as Record).suggested_actions ?? + (row.meta as Record).建议行动 + : row.suggested_actions, + ), + hide: asStringList( + row.meta && typeof row.meta === "object" + ? (row.meta as Record).hide + : undefined, + ), + }, + }; +} + +export function presentFromPlainText( + text: string, + shell: PresentShellId = "prose", +): PresentPacket { + return { + schema: PRESENT_SCHEMA, + shell, + blocks: { body: text }, + }; +} + +export function parsePresentPacket( + raw: string | undefined | null, + fallbackShell: PresentShellId = "prose", +): PresentPacketView { + const text = raw?.trim() ?? ""; + if (!text) { + return { + ok: true, + packet: presentFromPlainText("", fallbackShell), + fallbackPlain: true, + }; + } + + const jsonText = + extractJsonObjectText(text) ?? (text.startsWith("{") ? text : null); + if (!jsonText) { + return { + ok: true, + packet: presentFromPlainText(text, fallbackShell), + fallbackPlain: true, + }; + } + + try { + const doc = JSON.parse(jsonText) as unknown; + if (!doc || typeof doc !== "object" || Array.isArray(doc)) { + return { + ok: true, + packet: presentFromPlainText(text, fallbackShell), + fallbackPlain: true, + }; + } + const row = doc as Record; + // 明确 present 或带 blocks/shell 的展示包 + const looksPresent = + row.schema === PRESENT_SCHEMA || + row.blocks != null || + isPresentShellId(row.shell) || + isPresentShellId(row.shell_id); + if (!looksPresent && row.schema) { + // 其它 schema(如 settlement)不当作 present + return { + ok: true, + packet: presentFromPlainText(text, fallbackShell), + fallbackPlain: true, + }; + } + const packet = normalizePresentPacket(row, fallbackShell); + if ( + packet.blocks.body === undefined && + !packet.blocks.monitor && + !packet.blocks.header + ) { + // JSON 但无区域 → 整段当 body 说明 + if (!looksPresent) { + return { + ok: true, + packet: presentFromPlainText(text, fallbackShell), + fallbackPlain: true, + }; + } + } + return { ok: true, packet, fallbackPlain: false }; + } catch (err) { + return { + ok: false, + parseError: err instanceof Error ? err.message : "present JSON 解析失败", + packet: presentFromPlainText(text, fallbackShell), + fallbackPlain: true, + }; + } +} + +export function isPresentLikeObject(doc: unknown): boolean { + if (!doc || typeof doc !== "object" || Array.isArray(doc)) return false; + const row = doc as Record; + if (row.schema === PRESENT_SCHEMA) return true; + if (isPresentShellId(row.shell) && row.blocks != null) return true; + return false; +} + +/** + * 从「设计.正文组成」context-fragment 解析壳适配。 + */ +export function parseShellAdaptationFromReplyFormat( + raw: string | undefined | null, +): ShellAdaptation | undefined { + if (!raw?.trim()) return undefined; + const jsonText = + extractJsonObjectText(raw) ?? (raw.trim().startsWith("{") ? raw.trim() : null); + if (!jsonText) return undefined; + try { + const doc = JSON.parse(jsonText) as unknown; + if (!doc || typeof doc !== "object" || Array.isArray(doc)) return undefined; + const row = doc as Record; + const body = + row.正文 && typeof row.正文 === "object" && !Array.isArray(row.正文) + ? (row.正文 as Record) + : row; + const shellBox = + body.呈现壳 && typeof body.呈现壳 === "object" && !Array.isArray(body.呈现壳) + ? (body.呈现壳 as Record) + : body.shell && typeof body.shell === "object" + ? (body.shell as Record) + : body; + + const shellId = normalizePresentShellId( + shellBox.shell_id ?? shellBox.shellId ?? body.shell_id ?? body.shell, + "prose", + ); + // 若正文组成完全没提壳,且也不是明确 prose 局,仍返回 prose 默认 + const tweaksRaw = + shellBox.微调 && typeof shellBox.微调 === "object" + ? (shellBox.微调 as Record) + : shellBox.tweaks && typeof shellBox.tweaks === "object" + ? (shellBox.tweaks as Record) + : {}; + const base = defaultShellTweaks(shellId); + return { + shell_id: shellId, + why: asString(shellBox.为何选它 ?? shellBox.why), + tweaks: { + tone_chrome: + asString(tweaksRaw.tone_chrome) ?? base.tone_chrome, + show_suggested_actions: + typeof tweaksRaw.show_suggested_actions === "boolean" + ? tweaksRaw.show_suggested_actions + : base.show_suggested_actions, + block_labels: + asStringMap(tweaksRaw.block_labels) ?? base.block_labels, + empty_states: + asStringMap(tweaksRaw.empty_states) ?? base.empty_states, + }, + }; + } catch { + return undefined; + } +} + +/** 区域是否应在该壳默认展示(无内容时可按 empty_states 显示) */ +export function shellDefaultRegions( + shell: PresentShellId, +): PresentRegionId[] { + switch (shell) { + case "chat_monitor": + return ["monitor", "body", "footer"]; + case "turn_panel": + return ["monitor", "header", "body", "footer", "aside"]; + case "chapter_reader": + return ["header", "body", "aside", "footer"]; + case "prose": + default: + return ["body"]; + } +} + +export function formatPresentBlockContent( + value: string | Record | unknown[] | undefined, +): string { + if (value === undefined || value === null) return ""; + if (typeof value === "string") return value; + if (Array.isArray(value)) { + return value + .map((x) => (typeof x === "string" ? x : JSON.stringify(x))) + .filter(Boolean) + .join("\n"); + } + // 监控键值 → 短行 + return Object.entries(value) + .map(([k, v]) => `${k}:${typeof v === "string" ? v : JSON.stringify(v)}`) + .join("\n"); +} diff --git a/src/skills/settlement-packet.ts b/src/skills/settlement-packet.ts index 56fefc3..15990f8 100644 --- a/src/skills/settlement-packet.ts +++ b/src/skills/settlement-packet.ts @@ -2,6 +2,10 @@ * 主世界层 → 叙事转述的裁决包(settlement / direction packet)。 * 存黑板 tag:运行.本轮.裁决 */ +import { + createTableFromValues, + type TableDoc, +} from "../blackboard/table-cells.js"; import { extractJsonObjectText } from "./worker-set-parse.js"; export const SETTLEMENT_SCHEMA = "settlement.v1" as const; @@ -229,3 +233,37 @@ export function isSettlementLikeObject(doc: unknown): boolean { for (const k of keys) if (k in row) hits += 1; return hits >= 2; } + +/** + * 将 variable_changes 编成 TableDoc patch,供合并进「变量.当前」。 + * - 有 `to` → 写绝对值 + * - 仅有 `delta` → 在现有数值上加减(无现值则从 0) + */ +export function settlementChangesToTablePatch( + changes: SettlementVariableChange[], + current: TableDoc | null, +): TableDoc | null { + if (!changes.length) return null; + const byKey = new Map((current?.rows ?? []).map((r) => [r.key, r])); + const values: Record = {}; + const meta: Record = {}; + + for (const c of changes) { + const key = c.key.trim(); + if (!key) continue; + const existing = byKey.get(key); + if (c.to !== undefined) { + values[key] = c.to; + } else if (c.delta != null) { + const cur = existing?.value; + const n = typeof cur === "number" ? cur : Number(cur); + values[key] = Number.isFinite(n) ? (n as number) + c.delta : c.delta; + } else { + continue; + } + if (c.note) meta[key] = { note: c.note }; + } + + if (!Object.keys(values).length) return null; + return createTableFromValues(values, "system", meta); +} diff --git a/src/skills/worker-set-view.ts b/src/skills/worker-set-view.ts index 897dd47..046c80c 100644 --- a/src/skills/worker-set-view.ts +++ b/src/skills/worker-set-view.ts @@ -212,6 +212,23 @@ const DEFAULT_WORKER_CONTRACTS: Record< string, { context: { static: string[]; dynamic: string[] }; outputs: string[] } > = { + auditor: { + context: { + static: [ + "设计.worker集", + "设计.变量设计与更新规则", + "设计.生成规则", + "设计.变量控制上下文", + ], + dynamic: [ + "用户.最新输入", + "变量.当前", + "上下文.旁观.状态摘要", + "运行.本轮.工单", + ], + }, + outputs: ["运行.本轮.旁观"], + }, "world-simulator": { context: { static: [ @@ -226,6 +243,7 @@ const DEFAULT_WORKER_CONTRACTS: Record< "用户.最新输入", "上下文.角色态度", "大纲.当前章", + "运行.本轮.旁观", ], }, outputs: ["运行.本轮.裁决", "运行.事件流", "运行.本轮.变量变更"], @@ -243,8 +261,10 @@ const DEFAULT_WORKER_CONTRACTS: Record< "设计.worker集", "设计.叙事指南与故事推进", "设计.叙事指南", + "设计.正文组成", + "设计.监控栏", ], - dynamic: ["运行.本轮.裁决", "用户.最新输入"], + dynamic: ["运行.本轮.裁决", "用户.最新输入", "变量.当前"], }, outputs: ["输出.用户展示"], }, @@ -289,6 +309,7 @@ const WORKER_ROLE_LABELS: Record = { core: "核心", auxiliary: "辅助", transcription: "转述", + auditor: "旁观维护", gm: "主世界层", narrator: "叙事转述", perspective: "角色视角", @@ -746,7 +767,10 @@ export function formatWorkerSetForUser( ? PLAY_SLOT_ORDER.map((id: PlaySlotId) => ({ id, label: PLAY_SLOT_META[id].label, - enabled: Boolean(parsed.play_slots![id]), + enabled: + id === "auditor" + ? parsed.play_slots!.auditor !== false + : Boolean(parsed.play_slots![id]), ref: refForSlot(parsed.play_slots!, id), })) : undefined, diff --git a/tests/context-fragment.test.ts b/tests/context-fragment.test.ts index 9c9b236..6a4d8fa 100644 --- a/tests/context-fragment.test.ts +++ b/tests/context-fragment.test.ts @@ -45,8 +45,8 @@ describe("context-fragment.v1", () => { 正文: { 美学纲领: { 体验内核: "…" } }, 自评: { 维度: [ - { 名: "交互范式", 分数: 70, 说明: "权限尚可" }, - { 名: "美学纲领", 分数: 55, 说明: "边界未锁" }, + { 名: "交互范式", 分数: 7, 说明: "权限尚可" }, + { 名: "美学纲领", 分数: 5.5, 说明: "边界未锁" }, ], 薄弱点: "暴力尺度未定", }, @@ -61,7 +61,8 @@ describe("context-fragment.v1", () => { ], }, }); - expect(side.assessment).toContain("交互范式 70%"); + expect(side.assessment).toContain("交互范式 7/10"); + expect(side.assessment).toContain("美学纲领 5.5/10"); expect(side.assessment).toContain("薄弱点:暴力尺度未定"); expect(side.assessment).toContain("为贴近质感"); expect(side.questions).toHaveLength(1); diff --git a/tests/play-slots.test.ts b/tests/play-slots.test.ts index 702efe5..0b302cf 100644 --- a/tests/play-slots.test.ts +++ b/tests/play-slots.test.ts @@ -11,53 +11,104 @@ import { deriveRunWorkerScope, parseWorkerSetYaml, } from "../src/skills/worker-set-parse.js"; -import { parseSettlementPacket } from "../src/skills/settlement-packet.js"; +import { + parseSettlementPacket, + settlementChangesToTablePatch, +} from "../src/skills/settlement-packet.js"; +import { + emptyMaintainPacket, + isMaintainPacketEmpty, + maintainOpsToTablePatch, + parseMaintainPacket, +} from "../src/skills/maintain-packet.js"; +import { + createTableFromValues, + mergeTableCells, +} from "../src/blackboard/table-cells.js"; import { executeChance, parseChanceRequest, } from "../src/skills/chance-tools.js"; +import { synthesizeContextOrderFromWorkers } from "../src/skills/context-order.js"; +import { DIALOGUE_HISTORY_TAG } from "../src/skills/dialogue-history.js"; describe("play_slots", () => { - it("expands default gm+narrator", () => { - const slots = parsePlaySlots({ gm: true, narrator: true, perspective: false })!; - expect(refsFromPlaySlots(slots)).toEqual(["world-simulator", "narrator"]); + it("expands default auditor+gm+narrator", () => { + const slots = parsePlaySlots({ + gm: true, + narrator: true, + perspective: false, + })!; + expect(slots.auditor).toBe(true); + expect(refsFromPlaySlots(slots)).toEqual([ + "auditor", + "world-simulator", + "narrator", + ]); const workers = expandPlaySlotsToWorkers(slots); - expect(workers.map((w) => w.ref)).toEqual(["world-simulator", "narrator"]); + expect(workers.map((w) => w.ref)).toEqual([ + "auditor", + "world-simulator", + "narrator", + ]); expect(workers[0].acceptance).toBe("continue"); - expect(workers[1].acceptance).toBe("review"); + expect(workers[1].acceptance).toBe("continue"); + expect(workers[2].acceptance).toBe("review"); }); - it("orders perspective before gm", () => { + it("orders auditor then perspective before gm", () => { const slots = parsePlaySlots({ + auditor: true, gm: true, narrator: true, perspective: true, })!; expect(refsFromPlaySlots(slots)).toEqual([ + "auditor", "role-decide", "world-simulator", "narrator", ]); }); + it("can disable auditor", () => { + const slots = parsePlaySlots({ + auditor: false, + gm: true, + narrator: true, + perspective: false, + })!; + expect(refsFromPlaySlots(slots)).toEqual(["world-simulator", "narrator"]); + }); + it("merges play_slots into worker set JSON", () => { const parsed = parseWorkerSetYaml( JSON.stringify({ form_summary: "恋爱升温", - play_slots: { gm: true, narrator: true, perspective: false }, + play_slots: { + auditor: true, + gm: true, + narrator: true, + perspective: false, + }, workers: [], }), ); - expect(parsed?.play_slots?.gm).toBe(true); - expect(parsed?.workers).toHaveLength(2); + expect(parsed?.play_slots?.auditor).toBe(true); + expect(parsed?.workers).toHaveLength(3); expect(deriveRunWorkerScope(parsed)).toEqual([ + "auditor", "world-simulator", "narrator", ]); }); it("preserves custom context on merge", () => { - const slots = parsePlaySlots({ gm: true, narrator: true })!; + const slots = parsePlaySlots({ + auditor: false, + gm: true, + narrator: true, + })!; const merged = mergeWorkersWithPlaySlots( [ { @@ -76,6 +127,7 @@ describe("play_slots", () => { it("chance is on-demand and not in turn pipeline", () => { const slots = parsePlaySlots({ + auditor: false, gm: true, narrator: true, perspective: false, @@ -96,6 +148,7 @@ describe("play_slots", () => { const parsed = parseWorkerSetYaml( JSON.stringify({ play_slots: { + auditor: false, gm: true, narrator: true, perspective: false, @@ -166,4 +219,91 @@ describe("settlement packet", () => { expect(view.ok).toBe(true); expect(view.packet?.visible_now).toContain("画册"); }); + + it("merges variable_changes into table patch with delta", () => { + const current = createTableFromValues({ 好感: 10 }, "system"); + const patch = settlementChangesToTablePatch( + [{ key: "好感", delta: 8 }], + current, + )!; + const { doc } = mergeTableCells({ + current, + patch, + actor: "worker:world-simulator", + }); + expect(doc.rows.find((r) => r.key === "好感")?.value).toBe(18); + }); + + it("merges variable_changes with absolute to", () => { + const current = createTableFromValues({ 阶段: "相识" }, "system"); + const patch = settlementChangesToTablePatch( + [{ key: "阶段", to: "暧昧" }], + current, + )!; + const { doc } = mergeTableCells({ + current, + patch, + actor: "worker:world-simulator", + }); + expect(doc.rows.find((r) => r.key === "阶段")?.value).toBe("暧昧"); + }); +}); + +describe("maintain packet", () => { + it("defaults to empty ops", () => { + const view = parseMaintainPacket( + JSON.stringify({ + schema: "maintain.v1", + need_generate: false, + table_ops: [], + notes: [], + }), + ); + expect(view.ok).toBe(true); + expect(view.empty).toBe(true); + expect(isMaintainPacketEmpty(emptyMaintainPacket())).toBe(true); + }); + + it("accepts noop shorthand", () => { + expect(parseMaintainPacket("无需").empty).toBe(true); + }); + + it("applies table_ops delta to 变量.当前", () => { + const current = createTableFromValues({ 好感: 5 }, "system"); + const view = parseMaintainPacket( + JSON.stringify({ + schema: "maintain.v1", + table_ops: [{ op: "delta", key: "好感", delta: 3 }], + }), + ); + const patch = maintainOpsToTablePatch( + view.packet!.table_ops!, + current, + "变量.当前", + )!; + const { doc } = mergeTableCells({ + current, + patch, + actor: "worker:auditor", + }); + expect(doc.rows.find((r) => r.key === "好感")?.value).toBe(8); + }); +}); + +describe("context order synth", () => { + it("skips dialogue history for auditor", () => { + const order = synthesizeContextOrderFromWorkers( + [ + { ref: "auditor", name: "旁观维护" }, + { ref: "world-simulator", name: "主世界层" }, + ], + parsePlaySlots({ auditor: true, gm: true, narrator: false }), + )!; + const auditor = order.slots.find((s) => s.ref === "auditor")!; + const gm = order.slots.find((s) => s.ref === "world-simulator")!; + expect(auditor.inserts.some((i) => i.ref === DIALOGUE_HISTORY_TAG)).toBe( + false, + ); + expect(gm.inserts.some((i) => i.ref === DIALOGUE_HISTORY_TAG)).toBe(true); + }); }); diff --git a/tests/present-packet.test.ts b/tests/present-packet.test.ts new file mode 100644 index 0000000..ba7f43a --- /dev/null +++ b/tests/present-packet.test.ts @@ -0,0 +1,91 @@ +import { describe, expect, it } from "vitest"; +import { + isPresentLikeObject, + parsePresentPacket, + parseShellAdaptationFromReplyFormat, + presentFromPlainText, + shellDefaultRegions, +} from "../src/skills/present-packet.js"; + +describe("present.v1", () => { + it("parses present packet with blocks", () => { + const view = parsePresentPacket( + JSON.stringify({ + schema: "present.v1", + shell: "chat_monitor", + blocks: { + monitor: { 好感: "暖络" }, + body: "她回了一句短谢。", + }, + meta: { suggested_actions: ["回一句关心"] }, + }), + ); + expect(view.ok).toBe(true); + expect(view.fallbackPlain).toBe(false); + expect(view.packet.shell).toBe("chat_monitor"); + expect(view.packet.blocks.body).toContain("短谢"); + expect(view.packet.meta?.suggested_actions?.[0]).toBe("回一句关心"); + }); + + it("falls back plain text to prose body", () => { + const view = parsePresentPacket("林晚点头收下了画册。"); + expect(view.fallbackPlain).toBe(true); + expect(view.packet.shell).toBe("prose"); + expect(view.packet.blocks.body).toContain("画册"); + }); + + it("does not treat settlement as present", () => { + const view = parsePresentPacket( + JSON.stringify({ + schema: "settlement.v1", + visible_now: "场面", + player_action: "送礼", + }), + ); + expect(view.fallbackPlain).toBe(true); + expect(isPresentLikeObject({ schema: "settlement.v1" })).toBe(false); + }); + + it("presentFromPlainText helper", () => { + const p = presentFromPlainText("hello", "chapter_reader"); + expect(p.shell).toBe("chapter_reader"); + expect(p.blocks.body).toBe("hello"); + }); +}); + +describe("shell adaptation from reply-format", () => { + it("reads 呈现壳 from context-fragment", () => { + const adapt = parseShellAdaptationFromReplyFormat( + JSON.stringify({ + schema: "context-fragment.v1", + 技能: "正文组成", + 正文: { + 呈现壳: { + shell_id: "turn_panel", + 为何选它: "机制重", + 微调: { + tone_chrome: "terminal", + show_suggested_actions: true, + block_labels: { body: "场面" }, + }, + }, + 可见块: [{ 块id: "body", 区域: "body" }], + }, + }), + ); + expect(adapt?.shell_id).toBe("turn_panel"); + expect(adapt?.tweaks.tone_chrome).toBe("terminal"); + expect(adapt?.tweaks.block_labels?.body).toBe("场面"); + expect(shellDefaultRegions("turn_panel")).toContain("footer"); + }); + + it("defaults missing shell to prose", () => { + const adapt = parseShellAdaptationFromReplyFormat( + JSON.stringify({ + schema: "context-fragment.v1", + 正文: { 可见块: [{ 块id: "body" }] }, + }), + ); + expect(adapt?.shell_id).toBe("prose"); + }); +}); diff --git a/tests/worker-set-sanitize.test.ts b/tests/worker-set-sanitize.test.ts index a8b6c08..cbdfa51 100644 --- a/tests/worker-set-sanitize.test.ts +++ b/tests/worker-set-sanitize.test.ts @@ -95,7 +95,7 @@ describe("sanitizeWorkerSetOutputs", () => { brief: "孤立免疫", 正文: { 美学纲领: { 体验内核: "特权与惊惶" } }, 自评: { - 维度: [{ 名: "美学纲领", 分数: 60, 说明: "边界未锁" }], + 维度: [{ 名: "美学纲领", 分数: 6, 说明: "边界未锁" }], 薄弱点: "尺度", }, 追问: { @@ -116,7 +116,7 @@ describe("sanitizeWorkerSetOutputs", () => { ); expect(result.askUser?.length).toBe(1); expect(result.askUser?.[0]?.options?.length).toBe(2); - expect(result.askAssessment).toContain("美学纲领 60%"); + expect(result.askAssessment).toContain("美学纲领 6/10"); expect(result.askAssessment).toContain("还需确认"); }); }); diff --git a/web/agent-ui.js b/web/agent-ui.js index af60fd7..2bb08c4 100644 --- a/web/agent-ui.js +++ b/web/agent-ui.js @@ -1,6 +1,12 @@ import { renderIntakePanel } from "./intake-ui.js"; import { getActiveQuestions, renderQuestionsCard } from "./questions-ui.js"; import { displayWorkerLabel, formatWorkerDisplayTitle } from "./display-labels.js"; +import { + PRESENT_SHELL_IDS, + parsePresentDoc, + presentFromPlain, + renderPresentShellHtml, +} from "./present-shells.js"; const HIDE_KINDS = new Set(["worker_stub"]); @@ -341,6 +347,44 @@ function formatQuestionsHtml(body) { return `
    ${lines.map((l) => `
  1. ${esc(l)}
  2. `).join("")}
`; } +/** play 期助手终稿(含已验收的用户展示)用呈现壳渲染 */ +function shouldRenderPlayPresent(msg, view) { + if (view?.lifecycleStage !== "play") return false; + if (msg.role === "user") return false; + const kind = msg.kind ?? ""; + if (kind === "worker_questions" || kind === "orchestrator_thinking") return false; + if (kind === "worker_output") return false; // 已走 formatArtifactBodyHtml + const actor = String(msg.actor ?? ""); + if (actor === "narrator" || /用户展示|开场白/.test(String(msg.title ?? ""))) { + return true; + } + // 助手可见终稿:无特殊 kind 的长文 + return kind === "system_info" || kind === "worker_stub" || !kind; +} + +function formatPlayPresentHtml(body, view) { + const trimmed = (body || "").trim(); + if (!trimmed) return `

(无正文)

`; + const tweaks = view?.presentationTweaks; + const fallbackShell = + tweaks?.shell_id && PRESENT_SHELL_IDS?.includes?.(tweaks.shell_id) + ? tweaks.shell_id + : tweaks?.shell_id && + ["prose", "chat_monitor", "turn_panel", "chapter_reader"].includes( + tweaks.shell_id, + ) + ? tweaks.shell_id + : "prose"; + const parsed = tryParseJsonDoc(trimmed); + const presentView = parsed ? parsePresentDoc(parsed, fallbackShell) : null; + if (presentView) { + return renderPresentShellHtml(presentView.packet, esc, { tweaks }); + } + return renderPresentShellHtml(presentFromPlain(trimmed, fallbackShell), esc, { + tweaks, + }); +} + /** 旧会话 skill 选择提示,新流程不再展示 */ const SKILL_SELECTION_RE = /请选择创作 skill|请选择创作类型/; @@ -985,6 +1029,13 @@ function splitTaggedArtifactSections(text) { }); } +/** 轻度修复模型常出的尾逗号,便于友好渲染而不是整墙原文 */ +function softenJsonText(s) { + return String(s || "") + .replace(/^\uFEFF/, "") + .replace(/,\s*([\]}])/g, "$1"); +} + function tryParseJsonDoc(text) { const tryParse = (s) => { try { @@ -993,7 +1044,7 @@ function tryParseJsonDoc(text) { return null; } }; - const cleaned = stripCodeFences(text); + const cleaned = softenJsonText(stripCodeFences(text)); let parsed = tryParse(cleaned); if (parsed != null) return parsed; const objStart = cleaned.indexOf("{"); @@ -1082,9 +1133,9 @@ function summarizeArtifactDoc(doc, tagTitle = "") { .map((d) => { if (!d || typeof d !== "object") return ""; const name = d.名 || d.维度 || "?"; - const pct = parseCompleteness(d.分数); - if (pct == null) return ""; - return `${esc(String(name))} ${Math.round(pct)}%`; + const ten = parseTenScore(d.分数); + if (ten == null) return ""; + return `${esc(String(name))} ${formatTenScore(ten)}/10`; }) .filter(Boolean); if (pills.length) scoresHtml = pills.join(""); @@ -1125,6 +1176,10 @@ function renderArtifactCompactCard(doc, tagTitle, rawSlice, opts = {}) { function renderPlainCompactCard(title, content, opts = {}) { const preview = clampPreviewText(content, 160); const openAttr = opts.open ? " open" : ""; + const looksJson = /^\s*[{\[]/.test(String(content || "")); + const body = looksJson + ? `

未能解析为 JSON,以下为原文(可展开原始块排查)。

${esc(content)}
` + : `
${esc(content)}
`; return `
@@ -1135,7 +1190,7 @@ function renderPlainCompactCard(title, content, opts = {}) {
-
${esc(content)}
+
${body}
`; } @@ -1159,6 +1214,14 @@ function formatArtifactBodyHtml(body, opts = {}) { if (parsed != null && typeof parsed === "object") { return renderArtifactCompactCard(parsed, sec.title, sec.content, cardOpts); } + const presentTag = + /用户展示|开场白/.test(sec.title || "") || opts.asPresentFallback; + if (presentTag) { + return `
${renderPresentShellHtml( + presentFromPlain(sec.content, "prose"), + esc, + )}
`; + } return renderPlainCompactCard(sec.title || "产物", sec.content, cardOpts); }); return `
${cards.join("")}
`; @@ -1169,6 +1232,10 @@ function formatArtifactBodyHtml(body, opts = {}) { return `
${renderArtifactCompactCard(parsed, "", trimmed, cardOpts)}
`; } + if (opts.asPresentFallback) { + return renderPresentShellHtml(presentFromPlain(trimmed, "prose"), esc); + } + return `
${renderPlainCompactCard("产物", trimmed, cardOpts)}
`; } @@ -1178,6 +1245,12 @@ function renderKnownArtifactHtml(doc, opts = {}) { return `
${renderStructuredValueHtml(doc, 0)}
`; } const schema = doc.schema; + const presentView = parsePresentDoc(doc); + if (presentView) { + return renderPresentShellHtml(presentView.packet, esc, { + tweaks: opts.shellTweaks, + }); + } if (schema === "settlement.v1" || isSettlementLike(doc)) { return renderSettlementSectionsHtml(doc); } @@ -1199,6 +1272,7 @@ function renderKnownArtifactHtml(doc, opts = {}) { function renderPlaySlotsHtml(doc) { const slotLabels = { + auditor: "旁观维护", gm: "主世界层", "world-simulator": "主世界层", narrator: "叙事转述", @@ -1301,14 +1375,41 @@ function renderContextFragmentHtml(doc, opts = {}) { return `
${parts.join("")}
`; } -/** 美学纲领等:顶层板块并排 mosaic,子项用瓷砖网格 */ +/** 按技能分流正文视图;无专用模板则返回 ""(调用方走通用结构化) */ function renderSpecialtyBodyHtml(body, skill) { if (!body || typeof body !== "object" || Array.isArray(body)) return ""; + const skillName = typeof skill === "string" ? skill : ""; + + if ( + body.rules != null || + body.必要性判断 != null || + skillName.includes("生成规则") + ) { + const html = renderGenerationRulesBodyHtml(body); + if (html) return html; + } + if ( + body.社会结构 != null || + body.世界状况 != null || + skillName.includes("舞台骨架") + ) { + const html = renderWorldBlueprintBodyHtml(body); + if (html) return html; + } + if ( + body.支撑点 != null || + (Array.isArray(body.依据的核心体验) && body.覆盖检验 != null) || + skillName.includes("实现机制") + ) { + const html = renderMechanismBodyHtml(body); + if (html) return html; + } + const looksAesthetics = body.设定逻辑 != null || body.交互范式 != null || body.美学纲领 != null || - (typeof skill === "string" && skill.includes("美学")); + skillName.includes("美学"); if (!looksAesthetics) return ""; const order = ["设定逻辑", "交互范式", "美学纲领"]; @@ -1331,6 +1432,361 @@ function renderSpecialtyBodyHtml(body, skill) { return parts.length ? `
${parts.join("")}
` : ""; } +function skillSection(title, inner, cls = "") { + if (!inner) return ""; + return `

${esc(title)}

${inner}
`; +} + +function skillChipRow(items) { + const chips = (items || []).filter(Boolean).map((t) => `${esc(String(t))}`); + return chips.length ? `
${chips.join("")}
` : ""; +} + +function skillProseList(arr) { + if (!Array.isArray(arr) || !arr.length) return ""; + return `
    ${arr.map((x) => `
  • ${esc(String(x))}
  • `).join("")}
`; +} + +function skillKvBlock(obj, keys) { + if (!obj || typeof obj !== "object") return ""; + const entries = (keys && keys.length ? keys.map((k) => [k, obj[k]]) : Object.entries(obj)).filter( + ([, v]) => v != null && v !== "", + ); + if (!entries.length) return ""; + return `
${entries + .map(([k, v]) => { + if (Array.isArray(v)) { + return `
${esc(String(k))}
${skillProseList(v)}
`; + } + if (v && typeof v === "object") { + return `
${esc(String(k))}
${renderStructuredValueHtml(v, 1)}
`; + } + return `
${esc(String(k))}
${renderProseHtml(String(v))}
`; + }) + .join("")}
`; +} + +/** 生成规则 · 专用正文卡 */ +function renderGenerationRulesBodyHtml(body) { + const parts = []; + if (body.本步参数 && typeof body.本步参数 === "object") { + const p = body.本步参数; + parts.push( + skillSection( + "本步参数", + skillChipRow([ + p.target && `对象:${p.target}`, + p.rule_id && `rule_id:${p.rule_id}`, + p.lifecycle_intent && `生命周期:${p.lifecycle_intent}`, + ]) || skillKvBlock(p), + ), + ); + } + if (body.必要性判断 && typeof body.必要性判断 === "object") { + const n = body.必要性判断; + const verdict = n.结论 != null ? String(n.结论) : ""; + const tone = /无需|跳过/.test(verdict) ? "warn" : "ok"; + parts.push( + skillSection( + "必要性判断", + `${verdict ? `

${esc(verdict)}

` : ""}${skillKvBlock(n, [ + "生成对象", + "对游玩的重要性", + "基础生成的不足", + ])}`, + ), + ); + } + + const rules = Array.isArray(body.rules) ? body.rules : []; + if (rules.length) { + const cards = rules.map((rule, i) => renderOneGenerationRuleCard(rule, i)).filter(Boolean); + parts.push(skillSection("规则", `
${cards.join("")}
`)); + } else if (body.必要性判断) { + parts.push(skillSection("规则", `

本步未建立专门规则(rules 为空)

`)); + } + + if (body.增量说明 != null && String(body.增量说明).trim()) { + parts.push(skillSection("增量说明", renderProseHtml(String(body.增量说明)))); + } + + const used = new Set(["本步参数", "必要性判断", "rules", "增量说明"]); + for (const [k, v] of Object.entries(body)) { + if (used.has(k) || v == null || v === "") continue; + parts.push(skillSection(k, renderStructuredValueHtml(v, 0))); + } + return parts.length ? `
${parts.join("")}
` : ""; +} + +function renderOneGenerationRuleCard(rule, index) { + if (!rule || typeof rule !== "object") return ""; + const title = rule.对象 || rule.rule_id || `规则 ${index + 1}`; + const head = `
+
${esc(String(title))}
+ ${skillChipRow([ + rule.rule_id && `id · ${rule.rule_id}`, + rule.生命周期 && String(rule.生命周期), + ])} +
`; + + const blocks = []; + if (rule.上下文策略 && typeof rule.上下文策略 === "object") { + const flags = Object.entries(rule.上下文策略) + .map(([k, v]) => `${esc(k)}`) + .join(""); + blocks.push(`
上下文策略
${flags}
`); + } + if (rule.数量 != null) { + blocks.push(`
数量
${skillKvBlock(typeof rule.数量 === "object" ? rule.数量 : { 值: rule.数量 })}
`); + } + if (rule.生成与描写 && typeof rule.生成与描写 === "object") { + const g = rule.生成与描写; + const methodKeys = ["依据", "方法", "硬约束", "字段间约束", "变化维度", "禁止项", "去重规则", "校验"]; + blocks.push( + `
生成与描写
${methodKeys + .filter((k) => Array.isArray(g[k]) && g[k].length) + .map( + (k) => + `
${esc(k)}${skillProseList(g[k])}
`, + ) + .join("")}
`, + ); + } + if (rule.产物格式 && typeof rule.产物格式 === "object") { + blocks.push(renderProductSchemaCard(rule.产物格式)); + } + const pools = Array.isArray(rule.池) ? rule.池 : []; + if (pools.length) { + blocks.push( + `
池(${pools.length})
${pools + .map((p) => { + if (!p || typeof p !== "object") return ""; + const n = Array.isArray(p.条目) ? p.条目.length : 0; + return `
+
${esc(String(p.名称 || p.pool_id || "池"))}
+ ${skillChipRow([ + p.绑定字段 && `绑 · ${p.绑定字段}`, + p.用途 && String(p.用途), + n ? `${n} 条` : "", + ])} + ${p.说明 ? `

${esc(clampPreviewText(String(p.说明), 120))}

` : ""} +
`; + }) + .join("")}
`, + ); + } + + return `
${head}${blocks.join("")}
`; +} + +function renderProductSchemaCard(fmt) { + const schema = fmt.schema && typeof fmt.schema === "object" ? fmt.schema : null; + const fields = schema + ? Object.entries(schema) + .map(([name, spec]) => { + if (!spec || typeof spec !== "object") { + return `${esc(name)}${esc(String(spec))}`; + } + const type = spec.type != null ? String(spec.type) : "—"; + const req = spec.required === true ? "必填" : spec.required === false ? "可选" : "—"; + const desc = spec.description != null ? clampPreviewText(String(spec.description), 80) : ""; + const extra = []; + if (Array.isArray(spec.allowed_values) && spec.allowed_values.length) { + extra.push( + `枚举 ${spec.allowed_values + .slice(0, 6) + .map((x) => (x && typeof x === "object" ? x.value : x)) + .map(String) + .join("/")}${spec.allowed_values.length > 6 ? "…" : ""}`, + ); + } + if (spec.minimum != null || spec.maximum != null) { + extra.push(`[${spec.minimum ?? "…"}, ${spec.maximum ?? "…"}]`); + } + return ` + ${esc(name)} + ${esc(type)} + ${esc(req)} + ${esc(desc)}${extra.length ? `
${esc(extra.join(" · "))}
` : ""} + `; + }) + .join("") + : ""; + + return `
+
产物格式
+ ${skillChipRow([fmt.格式 && String(fmt.格式), fmt.批量时 && `批量 · ${fmt.批量时}`])} + ${ + fields + ? `
${fields}
字段类型必填说明
` + : "" + } + ${ + fmt.示例形状 && typeof fmt.示例形状 === "object" + ? `
示例形状
${highlightJson(JSON.stringify(fmt.示例形状, null, 2))}
` + : "" + } +
`; +} + +/** 舞台骨架 · 专用正文卡 */ +function renderWorldBlueprintBodyHtml(body) { + const parts = []; + if (Array.isArray(body.依据的体验) && body.依据的体验.length) { + parts.push(skillSection("依据的体验", skillProseList(body.依据的体验))); + } + if (body.舞台尺度) { + parts.push(skillSection("舞台尺度", skillKvBlock(body.舞台尺度))); + } + if (body.基底与变造) { + parts.push(skillSection("基底与变造", skillKvBlock(body.基底与变造))); + } + + const social = Array.isArray(body.社会结构) ? body.社会结构 : []; + if (social.length) { + parts.push( + skillSection( + "社会结构", + `
${social + .map((s) => { + if (!s || typeof s !== "object") return ""; + return `
+
${esc(String(s.名称 || "?"))}
+ ${skillChipRow([s.性质, s.细化程度])} + ${s.在舞台上的位置 ? `

${esc(clampPreviewText(String(s.在舞台上的位置), 140))}

` : ""} + ${s.服务体验 ? `

服务:${esc(clampPreviewText(String(s.服务体验), 100))}

` : ""} +
`; + }) + .join("")}
`, + ), + ); + } + + const status = Array.isArray(body.世界状况) ? body.世界状况 : []; + if (status.length) { + parts.push( + skillSection( + "世界状况", + `
${status + .map((s) => { + if (!s || typeof s !== "object") return ""; + return `
+
${esc(String(s.名称 || "?"))}
+ ${skillChipRow([s.性质, s.作用范围, s.节奏或触发])} + ${s.是什么 ? `

${esc(clampPreviewText(String(s.是什么), 140))}

` : ""} + ${s.如何影响运转 ? `

${esc(clampPreviewText(String(s.如何影响运转), 120))}

` : ""} +
`; + }) + .join("")}
`, + ), + ); + } + + const zones = Array.isArray(body.关键舞台区) ? body.关键舞台区 : []; + if (zones.length) { + parts.push( + skillSection( + "关键舞台区", + `
${zones + .map((z) => { + if (!z || typeof z !== "object") return ""; + return `
+
${esc(String(z.名称 || "?"))}
+ ${z.是什么 ? `

${esc(clampPreviewText(String(z.是什么), 120))}

` : ""} + ${z.为何需要 ? `

${esc(clampPreviewText(String(z.为何需要), 100))}

` : ""} + ${Array.isArray(z.格局要点) && z.格局要点.length ? skillProseList(z.格局要点) : ""} +
`; + }) + .join("")}
`, + ), + ); + } + + if (Array.isArray(body.未展开范围) && body.未展开范围.length) { + parts.push(skillSection("未展开范围", skillProseList(body.未展开范围))); + } + if (body.覆盖检验 && typeof body.覆盖检验 === "object") { + parts.push(skillSection("覆盖检验", skillKvBlock(body.覆盖检验))); + } + + const used = new Set([ + "依据的体验", + "舞台尺度", + "基底与变造", + "社会结构", + "世界状况", + "关键舞台区", + "未展开范围", + "覆盖检验", + ]); + for (const [k, v] of Object.entries(body)) { + if (used.has(k) || v == null || v === "") continue; + parts.push(skillSection(k, renderStructuredValueHtml(v, 0))); + } + return parts.length ? `
${parts.join("")}
` : ""; +} + +/** 实现机制 · 专用正文卡 */ +function renderMechanismBodyHtml(body) { + const parts = []; + if (Array.isArray(body.依据的核心体验) && body.依据的核心体验.length) { + parts.push(skillSection("依据的核心体验", skillProseList(body.依据的核心体验))); + } + const points = Array.isArray(body.支撑点) ? body.支撑点 : []; + if (points.length) { + parts.push( + skillSection( + "支撑点", + `
${points + .map((p) => { + if (!p || typeof p !== "object") return ""; + const pct = parseCompleteness(p.完备度); + const mini = pct != null ? renderMiniPctHtml(pct) : ""; + const morph = p.切面形态 && typeof p.切面形态 === "object" ? p.切面形态 : null; + return `
+
${esc(String(p.名称 || "?"))}
${mini}
+ ${skillChipRow([p.归属元素, p.支撑切面])} + ${morph?.概述 ? `

${esc(clampPreviewText(String(morph.概述), 140))}

` : ""} + ${p.如何支撑 ? `

${esc(clampPreviewText(String(p.如何支撑), 120))}

` : ""} + ${p.缺失后果 ? `

缺失:${esc(clampPreviewText(String(p.缺失后果), 100))}

` : ""} +
`; + }) + .join("")}
`, + ), + ); + } + const rels = Array.isArray(body.支撑点关系) ? body.支撑点关系 : []; + if (rels.length) { + parts.push( + skillSection( + "支撑点关系", + `
    ${rels + .map((r) => { + if (!r || typeof r !== "object") return ""; + const who = Array.isArray(r.涉及) ? r.涉及.join(" · ") : ""; + return `
  • ${esc(String(r.关系 || "关系"))}${who ? ` · ${esc(who)}` : ""}${ + r.体验作用 ? `
    ${esc(String(r.体验作用))}
    ` : "" + }
  • `; + }) + .join("")}
`, + ), + ); + } + if (body.覆盖检验 && typeof body.覆盖检验 === "object") { + parts.push(skillSection("覆盖检验", skillKvBlock(body.覆盖检验))); + } + const used = new Set(["依据的核心体验", "支撑点", "支撑点关系", "覆盖检验"]); + for (const [k, v] of Object.entries(body)) { + if (used.has(k) || v == null || v === "") continue; + parts.push(skillSection(k, renderStructuredValueHtml(v, 0))); + } + return parts.length ? `
${parts.join("")}
` : ""; +} + +/** + * 完备度等:0–100 百分制(正文诊断块仍用 %)。 + * 自评维度请用 parseTenScore。 + */ function parseCompleteness(v) { if (v == null) return null; if (typeof v === "number" && Number.isFinite(v)) { @@ -1349,6 +1805,38 @@ function parseCompleteness(v) { return null; } +/** 自评分数:0–10;兼容旧百分数(>10…100 → /10) */ +function parseTenScore(v) { + if (v == null) return null; + const fromNumber = (n) => { + if (!Number.isFinite(n) || n < 0) return null; + if (n <= 10) return Math.round(n * 10) / 10; + if (n <= 100) return Math.round((n / 10) * 10) / 10; + return null; + }; + if (typeof v === "number") return fromNumber(v); + const s = String(v).trim(); + if (!s) return null; + const slash = s.match(/^(\d+(?:\.\d+)?)\s*\/\s*10$/i); + if (slash) return fromNumber(Number(slash[1])); + const withPct = s.match(/^(\d+(?:\.\d+)?)\s*%$/); + if (withPct) return fromNumber(Number(withPct[1])); + const bare = s.match(/^(\d+(?:\.\d+)?)$/); + if (bare) return fromNumber(Number(bare[1])); + return null; +} + +function formatTenScore(ten) { + if (ten == null || !Number.isFinite(ten)) return "?"; + return Number.isInteger(ten) ? String(ten) : String(Math.round(ten * 10) / 10); +} + +function tenTone(ten) { + if (ten < 4) return "low"; + if (ten < 7) return "mid"; + return "high"; +} + function pctTone(pct) { if (pct < 40) return "low"; if (pct < 70) return "mid"; @@ -1369,6 +1857,22 @@ function renderPctMeterHtml(pct, label = "完备度") { `; } +/** 自评十分制条 */ +function renderTenMeterHtml(ten, label = "评分") { + if (ten == null || !Number.isFinite(ten)) return ""; + const score = Math.max(0, Math.min(10, ten)); + const width = Math.round(score * 10); + const tone = tenTone(score); + const shown = formatTenScore(score); + return `
+
+ ${esc(label)} + ${shown}/10 +
+
+
`; +} + /** 瓷砖用迷你百分比(数字 + 细条),省纵向空间 */ function renderMiniPctHtml(pct) { if (pct == null || !Number.isFinite(pct)) return ""; @@ -1559,14 +2063,14 @@ function renderSelfScoreHtml(自评) { .map((d) => { if (!d || typeof d !== "object") return ""; const name = d.名 || d.维度 || d.name || "?"; - const pct = parseCompleteness(d.分数); + const ten = parseTenScore(d.分数); const note = d.说明 ? `

${esc(clampPreviewText(String(d.说明), 80))}

` : ""; - if (pct == null) { + if (ten == null) { return `
${esc(String(name))}未评分
${note}
`; } - return `
${renderPctMeterHtml(pct, String(name))}${note}
`; + return `
${renderTenMeterHtml(ten, String(name))}${note}
`; }) .filter(Boolean); let weak = ""; @@ -2085,6 +2589,10 @@ function renderReviewFeedCard(review, opts = {}) { const artifactHtml = formatArtifactBodyHtml(review.body, { defaultOpen: true, hideAskSidecar: opts.hideAskSidecar === true, + asPresentFallback: + review.workerId === "narrator" || + review.workerId === "opening-generator" || + /用户展示|开场白/.test(review.body || ""), }); const contextId = review.sourceMessageId || review.id || ""; const hasContext = review.contextTrace ? "1" : "0"; @@ -2293,7 +2801,9 @@ export function renderMessageFeed(view, loading, handlers = {}) { ? formatQuestionsHtml(body) : kind === "worker_output" ? formatArtifactBodyHtml(body) - : esc(body) + : shouldRenderPlayPresent(msg, view) + ? formatPlayPresentHtml(body, view) + : esc(body) } ${isUser ? `
${headInner}
` : ""} `; diff --git a/web/present-shells.js b/web/present-shells.js new file mode 100644 index 0000000..cb86425 --- /dev/null +++ b/web/present-shells.js @@ -0,0 +1,189 @@ +/** + * 游玩呈现壳渲染(P2 最小适配) + * 契约:docs/play-presentation-shells.md · present.v1 + */ + +export const PRESENT_SHELL_IDS = [ + "prose", + "chat_monitor", + "turn_panel", + "chapter_reader", +]; + +const SHELL_LABELS = { + prose: "纯散文", + chat_monitor: "对话+监控", + turn_panel: "回合面板", + chapter_reader: "章节阅读", +}; + +const REGION_LABELS = { + monitor: "监控", + header: "抬头", + body: "正文", + footer: "文末", + aside: "侧栏", + hidden: "隐藏", +}; + +function isPresentShellId(v) { + return typeof v === "string" && PRESENT_SHELL_IDS.includes(v); +} + +function shellDefaultRegions(shell) { + switch (shell) { + case "chat_monitor": + return ["monitor", "body", "footer"]; + case "turn_panel": + return ["monitor", "header", "body", "footer", "aside"]; + case "chapter_reader": + return ["header", "body", "aside", "footer"]; + default: + return ["body"]; + } +} + +function formatBlockContent(value) { + if (value === undefined || value === null) return ""; + if (typeof value === "string") return value; + if (Array.isArray(value)) { + return value + .map((x) => (typeof x === "string" ? x : JSON.stringify(x))) + .filter(Boolean) + .join("\n"); + } + if (typeof value === "object") { + return Object.entries(value) + .map(([k, v]) => `${k}:${typeof v === "string" ? v : JSON.stringify(v)}`) + .join("\n"); + } + return String(value); +} + +function normalizeBlocks(raw) { + if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {}; + const out = {}; + for (const k of ["monitor", "header", "body", "footer", "aside", "hidden"]) { + if (raw[k] !== undefined) out[k] = raw[k]; + } + if (out.body === undefined && raw.正文 !== undefined) out.body = raw.正文; + if (out.monitor === undefined && raw.监控 !== undefined) out.monitor = raw.监控; + return out; +} + +/** + * @returns {{ ok: boolean, packet: object, fallbackPlain: boolean } | null} + * null = 不是 present 包(交给其它渲染器) + */ +export function parsePresentDoc(doc, fallbackShell = "prose") { + if (!doc || typeof doc !== "object" || Array.isArray(doc)) return null; + const looks = + doc.schema === "present.v1" || + (isPresentShellId(doc.shell) && doc.blocks != null) || + (isPresentShellId(doc.shell_id) && doc.blocks != null); + if (!looks) return null; + + const shell = isPresentShellId(doc.shell) + ? doc.shell + : isPresentShellId(doc.shell_id) + ? doc.shell_id + : fallbackShell; + const meta = + doc.meta && typeof doc.meta === "object" && !Array.isArray(doc.meta) + ? doc.meta + : {}; + return { + ok: true, + fallbackPlain: false, + packet: { + schema: "present.v1", + shell, + blocks: normalizeBlocks(doc.blocks ?? doc.区域), + meta: { + suggested_actions: Array.isArray(meta.suggested_actions) + ? meta.suggested_actions.filter((x) => typeof x === "string") + : Array.isArray(doc.suggested_actions) + ? doc.suggested_actions.filter((x) => typeof x === "string") + : [], + }, + }, + }; +} + +/** 纯文本 → prose 包(供调用方在非 JSON 时使用) */ +export function presentFromPlain(text, shell = "prose") { + return { + schema: "present.v1", + shell: isPresentShellId(shell) ? shell : "prose", + blocks: { body: text ?? "" }, + meta: { suggested_actions: [] }, + }; +} + +/** + * @param {object} packet present.v1 + * @param {(s: string) => string} esc + * @param {{ tweaks?: object }} [opts] + */ +export function renderPresentShellHtml(packet, esc, opts = {}) { + if (!packet || typeof packet !== "object") return ""; + const shell = isPresentShellId(packet.shell) ? packet.shell : "prose"; + const tweaks = opts.tweaks ?? {}; + const labels = { + ...(tweaks.block_labels && typeof tweaks.block_labels === "object" + ? tweaks.block_labels + : {}), + }; + const emptyStates = + tweaks.empty_states && typeof tweaks.empty_states === "object" + ? tweaks.empty_states + : {}; + const showActions = + typeof tweaks.show_suggested_actions === "boolean" + ? tweaks.show_suggested_actions + : shell === "turn_panel"; + const tone = + typeof tweaks.tone_chrome === "string" && tweaks.tone_chrome.trim() + ? tweaks.tone_chrome.trim() + : "default"; + + const blocks = packet.blocks ?? {}; + const regions = shellDefaultRegions(shell); + const parts = []; + + for (const region of regions) { + if (region === "hidden") continue; + let text = formatBlockContent(blocks[region]).trim(); + if (!text && emptyStates[region]) text = String(emptyStates[region]); + if (!text && region !== "body") continue; + if (!text && region === "body") text = "(暂无正文)"; + + const title = + labels[region] || + labels[REGION_LABELS[region]] || + REGION_LABELS[region] || + region; + const isBody = region === "body"; + parts.push(` +
+ ${isBody && shell === "prose" ? "" : `

${esc(title)}

`} +
${esc(text).replace(/\n/g, "
")}
+
`); + } + + const actions = showActions ? packet.meta?.suggested_actions ?? [] : []; + if (actions.length) { + parts.push(` +
+

建议行动

+
    ${actions.map((a) => `
  • ${esc(a)}
  • `).join("")}
+
`); + } + + const shellLabel = SHELL_LABELS[shell] || shell; + return ` +
+
${esc(shellLabel)}
+ ${parts.join("")} +
`; +} diff --git a/web/styles.css b/web/styles.css index 356ded0..3c4f4e7 100644 --- a/web/styles.css +++ b/web/styles.css @@ -817,6 +817,170 @@ body.rail-collapsed .af-mosaic { grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 5px; } + +/* 技能专用正文卡 */ +.skill-view { + display: flex; + flex-direction: column; + gap: 14px; +} +.skill-card-section > h4 { + margin: 0 0 8px; + font-size: 13px; + font-weight: 700; +} +.skill-kv { + display: flex; + flex-direction: column; + gap: 8px; +} +.skill-kv-row { + display: grid; + grid-template-columns: minmax(72px, 120px) 1fr; + gap: 8px 12px; + align-items: start; +} +.skill-kv-key { + font-size: 11px; + font-weight: 700; + color: var(--muted); + padding-top: 2px; +} +.skill-kv-val { + min-width: 0; + font-size: 13px; + line-height: 1.45; +} +@media (max-width: 640px) { + .skill-kv-row { grid-template-columns: 1fr; gap: 2px; } +} +.skill-verdict { + margin: 0 0 8px; + padding: 8px 10px; + border-radius: 8px; + font-weight: 700; + font-size: 13px; +} +.skill-verdict.tone-ok { + background: color-mix(in srgb, var(--ok) 14%, var(--surface)); + color: var(--ok); +} +.skill-verdict.tone-warn { + background: color-mix(in srgb, var(--warn) 18%, var(--surface)); + color: color-mix(in srgb, var(--warn) 80%, #000); +} +.skill-rule-stack, +.skill-entity-grid, +.skill-pool-grid { + display: flex; + flex-direction: column; + gap: 10px; +} +.skill-entity-grid, +.skill-pool-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); + gap: 10px; +} +.skill-rule-card, +.skill-entity-card, +.skill-pool-card { + padding: 12px 14px; + border-radius: 10px; + border: 1px solid var(--border); + background: color-mix(in srgb, var(--surface) 92%, var(--bg)); +} +.skill-rule-head, +.skill-entity-head { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + gap: 8px; + margin-bottom: 8px; +} +.skill-rule-title, +.skill-entity-name, +.skill-pool-name { + font-size: 14px; + font-weight: 700; + line-height: 1.3; +} +.skill-sub { + margin-top: 10px; + padding-top: 10px; + border-top: 1px solid color-mix(in srgb, var(--border) 80%, transparent); +} +.skill-sub-title { + font-size: 11px; + font-weight: 700; + color: var(--muted); + letter-spacing: 0.04em; + margin-bottom: 6px; +} +.skill-method { + margin-bottom: 8px; +} +.skill-method-label { + display: block; + font-size: 11px; + font-weight: 600; + color: var(--muted); + margin-bottom: 4px; +} +.skill-table-wrap { + overflow-x: auto; + margin-top: 6px; + border-radius: 8px; + border: 1px solid var(--border); +} +.skill-field-table { + width: 100%; + border-collapse: collapse; + font-size: 12px; +} +.skill-field-table th, +.skill-field-table td { + padding: 7px 9px; + text-align: left; + border-bottom: 1px solid var(--border); + vertical-align: top; +} +.skill-field-table th { + background: var(--bg); + font-size: 11px; + color: var(--muted); + font-weight: 700; +} +.skill-field-table tr:last-child td { border-bottom: 0; } +.skill-field-table code { + font-size: 11.5px; + font-weight: 600; +} +.skill-example { + margin-top: 8px; + font-size: 12px; +} +.skill-example summary { + cursor: pointer; + color: var(--muted); + font-weight: 600; +} +.skill-pool-desc { + margin: 6px 0 0; + font-size: 12px; + color: var(--muted); + line-height: 1.4; +} +.artifact-parse-hint { + margin: 0 0 8px; + font-size: 12px; +} +.skill-entity-card > p { + margin: 6px 0 0; + font-size: 12.5px; + line-height: 1.45; +} .pct-mini { display: inline-flex; align-items: center; @@ -1836,3 +2000,82 @@ body.rail-collapsed .af-mosaic { body:not(.rail-collapsed) .path-bar { display: none !important; } body:not(.rail-collapsed) .rail-panels { display: none; } } + +/* ֿ present.v1P2 С */ +.present-shell { + display: flex; + flex-direction: column; + gap: 10px; + position: relative; + padding: 10px 12px 12px; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--surface); +} +.present-shell-badge { + align-self: flex-start; + font-size: 11px; + font-weight: 600; + letter-spacing: 0.04em; + color: var(--muted); + padding: 1px 7px; + border-radius: 4px; + border: 1px solid var(--border); + background: var(--surface-2, rgba(0, 0, 0, 0.03)); +} +.present-region { + display: flex; + flex-direction: column; + gap: 4px; +} +.present-region-title { + margin: 0; + font-size: 11px; + font-weight: 700; + color: var(--muted); + letter-spacing: 0.04em; +} +.present-region-body { + margin: 0; + line-height: 1.55; + white-space: pre-wrap; + font-size: 14px; +} +.present-region--monitor .present-region-body, +.present-region--aside .present-region-body { + font-size: 13px; + color: var(--text, inherit); + padding: 6px 8px; + border-radius: 6px; + border: 1px solid var(--border); + background: var(--surface-2, rgba(0, 0, 0, 0.03)); +} +.present-shell--chat_monitor .present-region--monitor { + order: -1; +} +.present-shell--turn_panel { + display: grid; + grid-template-columns: minmax(0, 1fr); + gap: 10px; +} +.present-shell--chapter_reader .present-region--header .present-region-body { + font-size: 15px; + font-weight: 600; +} +.present-actions { + margin: 0; + padding-left: 18px; + line-height: 1.45; + font-size: 13px; +} +.present-tone--messenger .present-region--body { + border-left: 3px solid var(--border); + padding-left: 10px; +} +.present-tone--book .present-region--body { + max-width: 42rem; +} +.present-tone--terminal .present-region-body { + font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; + font-size: 13px; +}