引入游玩呈现壳与旁观维护槽,完善结算合并、十分制自评与技能产物 UI。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-12 02:03:29 +08:00
parent 94f67fa744
commit 120d67ee82
46 changed files with 2513 additions and 236 deletions

View File

@@ -376,7 +376,7 @@ task 必须写:禁止重复同一开场;在首答上补洞。
"稳变": "stable",
"正文": { },
"自评": {
"维度": [{ "名": "…", "分数": 0, "说明": "…" }],
"维度": [{ "名": "…", "分数": 8, "说明": "…" }],
"薄弱点": "…"
},
"追问": {
@@ -396,7 +396,7 @@ task 必须写:禁止重复同一开场;在首答上补洞。
| 段 | 说明 |
|----|------|
| `正文` | **产物主体**。技能自定内部结构(如美学步=设定逻辑 + 交互范式 + 美学纲领);须含「详细各个方面」。禁止整份只交散文。 |
| `自评` | **自评评分**。`维度[]` 名目按技能定(美学步常用:交互范式 / 美学纲领 / 整体协调);可含 `薄弱点`。 |
| `自评` | **自评评分**。`维度[]` 名目按技能定(美学步常用:交互范式 / 美学纲领 / 整体协调);**`分数` 为 010 十分制**(可一位小数;高质约 810可含 `薄弱点`。禁止用 0100 百分数。 |
| `追问` | **示例 + 建议选项**。给用户的下一问;`题目` 可空数组。与程序 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`。
---
# 第五部分:撰写任一能力时的工作步骤

View File

@@ -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`

View File

@@ -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 墙:提示无法解析并保留原文折叠。自评 `分数` 契约为 **010**(旧百分数会按 /10 兼容显示)。
技能作者output 键稳定后,须登记或实现对应视图(见 `capability-authoring-brief.md` §4.9「视图」)。
## 7. 配方链路
```text

View File

@@ -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": ["<<<VARS>>>…<<<END>>>"]
}
}
```
- `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口径明确为创作基于壳适配微调。

View File

@@ -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 只负责骰子/抽签。
编排口诀:体验与机制 → 要不要真值 → 映射表用生成规则/实例 → 维护语句与旁观汇总 → 开场同真相 → 收成。

View File

@@ -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。

View File

@@ -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 合并变量已接 |
| 扩写助手 | 方法论已写;起点:美学纲领与交互范式;勿默认套世界模拟全套 |
---

View File

@@ -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. **公共三段**`正文`=产物主体;`自评`=完备度打分**分数 010 十分制,禁止百分数**`追问`=导语+题目(建议选项/示例)。禁止用散文代替这三段。
3. **本技正文三块**`设定逻辑`(参与/内容维度/区域/复杂性/完备度)+ `交互范式` + `美学纲领`。未知省略或「未定」,禁止编造百科。
4. `mount` 建议挂主世界层+转述;**禁止**写数字 `order``稳变` 定稿后多为 `stable`
5. `追问.题目` 一次宜 13 题;有建议选项;允许「其它」。已答清的不要再问。

View File

@@ -120,8 +120,8 @@ modules:
- id: reply-format
name: 正文组成
declaration: >-
钉用户可见的一轮回复由哪些块、何顺序组成(含监控栏位置、隐藏变量段、前端拆分
不是程序报文格式;旧称设计回复格式
基于固定呈现壳做适配微调(选 shell_id + 字段/显示名等
产出壳适配单,禁止从零发明布局;旧称设计回复格式
artifact: 设计.正文组成
# —— 开局(创作偏晚) ——

View File

@@ -87,7 +87,7 @@ summary`具体实例 · {对象} · {n}条`
"维度": [
{
"名": "合规模",
"分数": 0,
"分数": 8,
"说明": "是否严格按该规则的方法、单层格式、池与约束生成"
}
],

View File

@@ -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. **公共三段**:正文=规则主体;自评=必要性/属性妥当/格式准确**分数 010 十分制**;追问=导语+建议选项。
3. **正文键固定**本步参数、必要性判断、rules、增量说明。
4. **每条 rule 必含**:生成与描写、产物格式;**池可空数组**。有池时 `绑定字段` 必须是单层 schema 的顶层键。
5. **产物格式强制单层**`schema``type` 不得为 `object``array``items` 不得为 object。禁止 `外貌.发色` 这类嵌套路径。

View File

@@ -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. **公共三段**`正文`=产物主体;`自评`=打分**分数 010 十分制**`追问`=导语+建议选项。禁止整份散文壳。
3. **本技正文**`依据的核心体验` + `支撑点[]` + `支撑点关系[]` + `覆盖检验`。基准世界/推理过程/淘汰候选不得写入。
4. 每个支撑点须有归属元素、支撑切面、如何支撑、缺失后果;简单支点结构=「一句话」、展开=`[]`
5. `mount` 默认主世界层(`world-simulator`**禁止**写 `order`。承重规则进 GM不进转述全文。

View File

@@ -212,17 +212,17 @@ summary`叙事指南与故事推进 · …`(点题体裁/笔墨重心 + 推
"维度": [
{
"名": "写法可执行",
"分数": 0,
"分数": 8,
"说明": "遣词+笔墨焦点+禁忌/不偏好+示范是否够转述落地"
},
{
"名": "推进可执行",
"分数": 0,
"分数": 8,
"说明": "主世界层是否知道如何推进;变量钩子清楚或明确无需"
},
{
"名": "与美学对齐",
"分数": 0,
"分数": 8,
"说明": "是否把感觉落成做法;笔墨与禁忌服务体验而非跑偏"
}
],

View File

@@ -121,17 +121,17 @@ summary`开场白与开场变量 · …`(点题场景)。
"维度": [
{
"名": "契约符合",
"分数": 0,
"分数": 8,
"说明": "是否遵循正文组成/叙事/监控栏"
},
{
"名": "同真相",
"分数": 0,
"分数": 8,
"说明": "开场叙述与开场变量是否一致"
},
{
"名": "可开玩",
"分数": 0,
"分数": 8,
"说明": "是否短、可感、有下一步行动空间"
}
],

View File

@@ -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`
- writingoutline / 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-writergm/outline 常用 continue。
3. 真值变更写在 gm 的 outputs运行.本轮.变量变更 / 裁决包内 variable_changes不靠第三变量 Worker。
4. 裁决包约定:运行.本轮.裁决 使用 settlement.v1见 progressive-data-design / 模板)。
2. 面向用户的终稿点 acceptance=review通常是 narrator 或 chapter-writerauditor/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+narratorworkers 两条与槽一致side_effects 从变量设计拷入。
- play_slots auditor+gm+narratorworkers 与槽一致side_effects 从变量设计拷入。
- 扩写outline continue + chapter-writer review无自造 ref。
坏:

View File

@@ -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
每轮 12 点。
优先:必须有哪些可见块;监控栏要不要、盯什么类型信息;隐藏变量段要不要及标记长什么样;前端要不要分区
不追问:完整 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",
"用途": "变量维护语句(与「变量设计.维护语句约定」同构),供程序拆进变量.当前",
"定界或正则约定": "例:<<<VARS>>>...<<<END>>>;形状细节以变量设计为准",
"用途": "变量维护语句,供程序拆进变量.当前",
"定界或正则约定": "例:<<<VARS>>>...<<<END>>>",
"用户界面": "默认隐藏|折叠可见|调试可见",
"模型必须输出": 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.v1prose 可为纯 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
好:
- 块序:监控栏 → 正文 → 文末吐槽;隐藏段 <<<VARS>>> 维护好感变更
- 书信局:抬头/日期/正文/落款;无监控栏
- 监控只盯体力、攻略目标好感、场景可交互对象——不写主角姓名性别
- 适配 chat_monitor微调监控字段=好感/今日话题body 显示名=「短信」
- 适配 chapter_reader只开 header+bodyaside 关
- 适配 turn_panel开 suggested_actions监控只盯体力/好感
坏:
- 把 settlement.v1 字段表当「回复格式」。
- 监控栏列出姓名、性别、种族等死设定
- 只写「要好看的 UI」无块清单
- 「我们自定义一种左右分栏新界面」。
- 不写 shell_id只写「要好看的 UI」
- 监控栏列出姓名、性别等死设定
```

View File

@@ -129,17 +129,17 @@ summary`设计监控栏 · …`(点题监控对象)。
"维度": [
{
"名": "属性妥当",
"分数": 0,
"分数": 8,
"说明": "逐字段:无死名片;该盯的生存/关系/场景信息未漏"
},
{
"名": "克制度",
"分数": 0,
"分数": 8,
"说明": "字段是否够少、展示是否够短"
},
{
"名": "可接线",
"分数": 0,
"分数": 8,
"说明": "来源意图是否接得上变量/投影/正文组成"
}
],

View File

@@ -111,17 +111,17 @@ summary`变量控制上下文 · …`。
"维度": [
{
"名": "视野正确",
"分数": 0,
"分数": 8,
"说明": "各槽该见/不该见是否清楚"
},
{
"名": "旁观可用",
"分数": 0,
"分数": 8,
"说明": "旁观汇总是否够用且未整表;不需要旁观则 N/A"
},
{
"名": "防剧透",
"分数": 0,
"分数": 8,
"说明": "未解锁与禁止泄露是否落实"
}
],

View File

@@ -166,17 +166,17 @@ summary`变量设计 · …`(点题主真值 + 有无映射表)。
"维度": [
{
"名": "承重度",
"分数": 0,
"分数": 8,
"说明": "真值是否只覆盖必须跨轮记住的状态"
},
{
"名": "可维护",
"分数": 0,
"分数": 8,
"说明": "维护语句是否可解析;映射索引是否接得上实例/规则"
},
{
"名": "克制度",
"分数": 0,
"分数": 8,
"说明": "无派生可写格膨胀;无用 chance 冒充改表"
}
],

View File

@@ -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 视为 truechance 视为 falsechance 为 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+narratorperspective/chance falsemount_notes 一行。
- play_slots: auditor+gm+narratorperspective/chance falsemount_notes 一行。
- 有凶手真名不能进 GMperspective true并说明只出反应建议。
- 需要检定/抽签chance true按需程序工具

View File

@@ -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. **公共三段**:正文=舞台主体;自评=覆盖度/充分度/克制度**分数 010 十分制**;追问=导语+建议选项。
3. **本技正文**须含:依据体验、舞台尺度、基底与变造、**社会结构**、**世界状况**、关键舞台区、未展开范围、覆盖检验。
4. 社会结构=谁在台上互动(种类/势力/阶层,格局级);世界状况=世界如何运转(灾变/通道/习俗等硬部件 + 日常软条件)。无则空数组,但须在覆盖检验说明「为何无需」。
5. `mount` 默认主世界层;**禁止**写 `order``稳变` 多为 `stable`

View File

@@ -76,7 +76,12 @@ design
→ 用户手动进 play
play
用户输入 → 声明内 worker → 终稿
用户输入
→ auditor旁观维护默认空操作无长对话史
→ perspective?(仅强信息隔离)
→ gm / world-simulator主世界层settlement.v1variable_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 | falseplay | continue声明默认 |
| narrator | falseplay 调度) | 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

View File

@@ -16,7 +16,7 @@ process:
- 体验契约稍清后即可确认游玩槽(「游玩拓扑」或按默认:主世界层 + 叙事转述,角色视角默认关);此后写上下文时只标挂谁,不标最终顺序。
- 对照技能池「何时用 / 何时不用」,只排近期缺的 14 步;不预设固定长链。
- 中段软顺序:机制/舞台支点 → 要不要真值 → 有门控再排变量与分档/事件 → 叙事/正文组成/监控栏按需;每块产物带 mount挂哪些槽与可选「稳/变」信号,勿写死 order。
- 用户看见的版式用「正文组成」(非程序报文);会变信息用「设计监控栏」(忌姓名性别等死字段)
- 用户看见的呈现:用「正文组成」**选固定壳并微调**(见 docs/play-presentation-shells.md不是从零设计 UI;会变信息用「设计监控栏」。
- 需要可引用舞台时用「舞台骨架」;识别体验支点用实现机制;同类内容难稳定生成用生成规则,需预生成再排具体实例。
- 需要门控/分档/程序事件:先「变量设计与更新规则」,再「变量控制上下文」。变量与 Progressive 不是执行单元。
- 挂载归属(非顺序):世界/机制/生成 → 主世界层;叙事指南与故事推进 → 叙事转述(推进兼挂主世界层);美学进常驻。勿为世界观或性格表另开槽。拓扑图谱在固定槽已够用时通常跳过。

View File

@@ -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`
## 其它文件

View File

@@ -0,0 +1,35 @@
id: auditor
label: 旁观维护
role: auxiliary
duty: >
副 LLM每轮检查表字段与生成规则是否需要补充默认空操作。
只出 maintain.v1运行.本轮.旁观);不写用户正文、不写真相权威。
上下文几乎不要对话历史——合同 + 表 + 本轮工单即可。
when: 每轮用户输入后、主世界层裁决之前
suggested_context:
static:
- 设计.worker集
- 设计.变量设计与更新规则
- 设计.生成规则
- 设计.变量控制上下文
dynamic:
- 用户.最新输入
- 变量.当前
- 上下文.旁观.状态摘要
- 运行.本轮.工单
suggested_outputs:
- 运行.本轮.旁观
prompt_excerpt: |
你是旁观维护(副 LLM不是主世界层也不是叙事作者。
读合同(生成规则/变量规则/旁观摘要)与当前表,对照「用户.最新输入」判断本轮要不要补表或触发规则。
「运行.本轮.旁观」必须是 JSONschema 为 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" }。
禁止编造长对话续写;禁止输出用户可见正文;禁止修改未点名的无关字段。

View File

@@ -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_idprose|chat_monitor|turn_panel|chapter_reader
- prose可直接输出 Markdown 正文(程序会落入 body
- 其它壳:优先输出 JSONschema 为 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。

View File

@@ -14,6 +14,7 @@ suggested_context:
- 用户.最新输入
- 运行.事件流
- 变量.当前
- 运行.本轮.旁观
- 上下文.角色态度
- 大纲.当前章
suggested_outputs:
@@ -21,9 +22,10 @@ suggested_outputs:
- 运行.事件流
- 运行.本轮.变量变更
prompt_excerpt: |
你是中立主世界层GM不是文学作者。读真值已投影上下文,对用户行动作可复核裁决
「运行.本轮.裁决」必须是 JSONschema 为 settlement.v1至少尽量包含
你是中立主世界层GM不是文学作者。读真值已投影上下文,以及本轮旁观维护包(若有)
对用户行动作可复核裁决。「运行.本轮.裁决」必须是 JSONschema 为 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。

View File

@@ -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-stepsteps 做完但 status=open → 再 design-flow禁止旧 design-core/fixed/worker/refine。已 accept 且需开局 → opening-generator。进 play 由用户手动。",
stage === "play"
? "play按声明序 auditor→perspective?→gm→narratorcontinue 槽连跑;仅终稿 review 停pendingSideEffectWorkers 优先;禁止未声明 ref。"
: "根据当前状态决定下一步。尚无设计.创作流程 → design-flow有未完成步骤 → design-stepsteps 做完但 status=open → 再 design-flow禁止旧 design-core/fixed/worker/refine。已 accept 且需开局 → opening-generator。进 play 由用户手动。",
},
null,
2,

View File

@@ -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}

View File

@@ -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 = "";

View File

@@ -28,6 +28,7 @@ const WORKER_LABELS: Record<string, string> = {
narrator: "叙事转述",
"role-decide": "角色决策",
"world-simulator": "主世界层",
auditor: "旁观维护",
chance: "机遇裁定",
"round-present": "回合呈现",
outline: "大纲 / 细纲",

View File

@@ -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<string, string>;
empty_states?: Record<string, string>;
};
/** 总管正在输出的思维链(流式,轮询用) */
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,

View File

@@ -40,6 +40,29 @@ function asStringList(v: unknown): string[] {
return v.map((x) => asString(x)).filter((x): x is string => Boolean(x));
}
/** 自评分数展示010兼容旧百分数 */
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<string, unknown>;
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}`,
);

View File

@@ -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",
);
}

View File

@@ -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<string, unknown>;
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<string, unknown>;
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<string, unknown>,
): 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<string, unknown>);
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<string, unknown> = {};
const meta: Record<string, { note?: string }> = {};
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);
}

View File

@@ -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<PlaySlotId, string> = {
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<PlaySlotId> =
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<string, unknown>;
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,

View File

@@ -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<string, string>;
empty_states?: Record<string, string>;
};
export type ShellAdaptation = {
shell_id: PresentShellId;
why?: string;
tweaks: PresentShellTweaks;
};
export type PresentBlocks = Partial<
Record<PresentRegionId, string | Record<string, unknown> | 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<string>(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<string, string> | undefined {
if (!v || typeof v !== "object" || Array.isArray(v)) return undefined;
const out: Record<string, string> = {};
for (const [k, val] of Object.entries(v as Record<string, unknown>)) {
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<string, unknown>;
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<string, unknown>,
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<string, unknown>).suggested_actions ??
(row.meta as Record<string, unknown>).建议行动
: row.suggested_actions,
),
hide: asStringList(
row.meta && typeof row.meta === "object"
? (row.meta as Record<string, unknown>).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<string, unknown>;
// 明确 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<string, unknown>;
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<string, unknown>;
const body =
row. && typeof row. === "object" && !Array.isArray(row.)
? (row. as Record<string, unknown>)
: row;
const shellBox =
body. && typeof body. === "object" && !Array.isArray(body.)
? (body. as Record<string, unknown>)
: body.shell && typeof body.shell === "object"
? (body.shell as Record<string, unknown>)
: 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<string, unknown>)
: shellBox.tweaks && typeof shellBox.tweaks === "object"
? (shellBox.tweaks as Record<string, unknown>)
: {};
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<string, unknown> | 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");
}

View File

@@ -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<string, unknown> = {};
const meta: Record<string, { note?: string }> = {};
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);
}

View File

@@ -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<string, string> = {
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,

View File

@@ -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);

View File

@@ -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);
});
});

View File

@@ -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");
});
});

View File

@@ -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("还需确认");
});
});

View File

@@ -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 `<ol class="msg-q-list">${lines.map((l) => `<li>${esc(l)}</li>`).join("")}</ol>`;
}
/** 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 `<p class="empty-sm">(无正文)</p>`;
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 `<span class="pct-pill tone-${pctTone(pct)}">${esc(String(name))} ${Math.round(pct)}%</span>`;
const ten = parseTenScore(d.分数);
if (ten == null) return "";
return `<span class="pct-pill tone-${tenTone(ten)}">${esc(String(name))} ${formatTenScore(ten)}/10</span>`;
})
.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
? `<p class="ws-muted artifact-parse-hint">未能解析为 JSON以下为原文可展开原始块排查。</p><pre class="review-feed-plain">${esc(content)}</pre>`
: `<pre class="review-feed-plain">${esc(content)}</pre>`;
return `<details class="artifact-compact"${openAttr}>
<summary class="artifact-compact-sum">
<div class="artifact-compact-row">
@@ -1135,7 +1190,7 @@ function renderPlainCompactCard(title, content, opts = {}) {
<span class="artifact-compact-toggle" aria-hidden="true"></span>
</div>
</summary>
<div class="artifact-compact-body"><pre class="review-feed-plain">${esc(content)}</pre></div>
<div class="artifact-compact-body">${body}</div>
</details>`;
}
@@ -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 `<div class="artifact-compact-list">${renderPresentShellHtml(
presentFromPlain(sec.content, "prose"),
esc,
)}</div>`;
}
return renderPlainCompactCard(sec.title || "产物", sec.content, cardOpts);
});
return `<div class="artifact-compact-list">${cards.join("")}</div>`;
@@ -1169,6 +1232,10 @@ function formatArtifactBodyHtml(body, opts = {}) {
return `<div class="artifact-compact-list">${renderArtifactCompactCard(parsed, "", trimmed, cardOpts)}</div>`;
}
if (opts.asPresentFallback) {
return renderPresentShellHtml(presentFromPlain(trimmed, "prose"), esc);
}
return `<div class="artifact-compact-list">${renderPlainCompactCard("产物", trimmed, cardOpts)}</div>`;
}
@@ -1178,6 +1245,12 @@ function renderKnownArtifactHtml(doc, opts = {}) {
return `<div class="artifact-friendly">${renderStructuredValueHtml(doc, 0)}</div>`;
}
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 `<div class="artifact-friendly artifact-context-fragment">${parts.join("")}</div>`;
}
/** 美学纲领等:顶层板块并排 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 ? `<div class="af-mosaic">${parts.join("")}</div>` : "";
}
function skillSection(title, inner, cls = "") {
if (!inner) return "";
return `<section class="skill-card-section${cls ? ` ${cls}` : ""}"><h4>${esc(title)}</h4>${inner}</section>`;
}
function skillChipRow(items) {
const chips = (items || []).filter(Boolean).map((t) => `<span class="ws-badge ws-badge-continue">${esc(String(t))}</span>`);
return chips.length ? `<div class="artifact-chip-row">${chips.join("")}</div>` : "";
}
function skillProseList(arr) {
if (!Array.isArray(arr) || !arr.length) return "";
return `<ul class="artifact-list">${arr.map((x) => `<li>${esc(String(x))}</li>`).join("")}</ul>`;
}
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 `<div class="skill-kv">${entries
.map(([k, v]) => {
if (Array.isArray(v)) {
return `<div class="skill-kv-row"><span class="skill-kv-key">${esc(String(k))}</span><div class="skill-kv-val">${skillProseList(v)}</div></div>`;
}
if (v && typeof v === "object") {
return `<div class="skill-kv-row"><span class="skill-kv-key">${esc(String(k))}</span><div class="skill-kv-val">${renderStructuredValueHtml(v, 1)}</div></div>`;
}
return `<div class="skill-kv-row"><span class="skill-kv-key">${esc(String(k))}</span><div class="skill-kv-val">${renderProseHtml(String(v))}</div></div>`;
})
.join("")}</div>`;
}
/** 生成规则 · 专用正文卡 */
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 ? `<p class="skill-verdict tone-${tone}">${esc(verdict)}</p>` : ""}${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("规则", `<div class="skill-rule-stack">${cards.join("")}</div>`));
} else if (body.必要性判断) {
parts.push(skillSection("规则", `<p class="ws-muted">本步未建立专门规则rules 为空)</p>`));
}
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 ? `<div class="skill-view skill-view-generation-rules">${parts.join("")}</div>` : "";
}
function renderOneGenerationRuleCard(rule, index) {
if (!rule || typeof rule !== "object") return "";
const title = rule.对象 || rule.rule_id || `规则 ${index + 1}`;
const head = `<header class="skill-rule-head">
<div class="skill-rule-title">${esc(String(title))}</div>
${skillChipRow([
rule.rule_id && `id · ${rule.rule_id}`,
rule.生命周期 && String(rule.生命周期),
])}
</header>`;
const blocks = [];
if (rule.上下文策略 && typeof rule.上下文策略 === "object") {
const flags = Object.entries(rule.上下文策略)
.map(([k, v]) => `<span class="ws-slot-chip ${v ? "ws-slot-on" : "ws-slot-off"}">${esc(k)}</span>`)
.join("");
blocks.push(`<div class="skill-sub"><div class="skill-sub-title">上下文策略</div><div class="ws-slot-row">${flags}</div></div>`);
}
if (rule.数量 != null) {
blocks.push(`<div class="skill-sub"><div class="skill-sub-title">数量</div>${skillKvBlock(typeof rule.数量 === "object" ? rule.数量 : { : rule.数量 })}</div>`);
}
if (rule.生成与描写 && typeof rule.生成与描写 === "object") {
const g = rule.生成与描写;
const methodKeys = ["依据", "方法", "硬约束", "字段间约束", "变化维度", "禁止项", "去重规则", "校验"];
blocks.push(
`<div class="skill-sub"><div class="skill-sub-title">生成与描写</div>${methodKeys
.filter((k) => Array.isArray(g[k]) && g[k].length)
.map(
(k) =>
`<div class="skill-method"><span class="skill-method-label">${esc(k)}</span>${skillProseList(g[k])}</div>`,
)
.join("")}</div>`,
);
}
if (rule.产物格式 && typeof rule.产物格式 === "object") {
blocks.push(renderProductSchemaCard(rule.产物格式));
}
const pools = Array.isArray(rule.) ? rule. : [];
if (pools.length) {
blocks.push(
`<div class="skill-sub"><div class="skill-sub-title">池(${pools.length}</div><div class="skill-pool-grid">${pools
.map((p) => {
if (!p || typeof p !== "object") return "";
const n = Array.isArray(p.条目) ? p.条目.length : 0;
return `<article class="skill-pool-card">
<div class="skill-pool-name">${esc(String(p.名称 || p.pool_id || "池"))}</div>
${skillChipRow([
p.绑定字段 && `绑 · ${p.绑定字段}`,
p.用途 && String(p.用途),
n ? `${n}` : "",
])}
${p.说明 ? `<p class="skill-pool-desc">${esc(clampPreviewText(String(p.说明), 120))}</p>` : ""}
</article>`;
})
.join("")}</div></div>`,
);
}
return `<article class="skill-rule-card">${head}${blocks.join("")}</article>`;
}
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 `<tr><td>${esc(name)}</td><td colspan="3">${esc(String(spec))}</td></tr>`;
}
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 `<tr>
<td><code>${esc(name)}</code></td>
<td>${esc(type)}</td>
<td>${esc(req)}</td>
<td>${esc(desc)}${extra.length ? `<div class="ws-muted">${esc(extra.join(" · "))}</div>` : ""}</td>
</tr>`;
})
.join("")
: "";
return `<div class="skill-sub">
<div class="skill-sub-title">产物格式</div>
${skillChipRow([fmt.格式 && String(fmt.格式), fmt.批量时 && `批量 · ${fmt.批量时}`])}
${
fields
? `<div class="skill-table-wrap"><table class="skill-field-table"><thead><tr><th>字段</th><th>类型</th><th>必填</th><th>说明</th></tr></thead><tbody>${fields}</tbody></table></div>`
: ""
}
${
fmt.示例形状 && typeof fmt.示例形状 === "object"
? `<details class="skill-example"><summary>示例形状</summary><pre class="json-pretty"><code>${highlightJson(JSON.stringify(fmt.示例形状, null, 2))}</code></pre></details>`
: ""
}
</div>`;
}
/** 舞台骨架 · 专用正文卡 */
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(
"社会结构",
`<div class="skill-entity-grid">${social
.map((s) => {
if (!s || typeof s !== "object") return "";
return `<article class="skill-entity-card">
<div class="skill-entity-name">${esc(String(s.名称 || "?"))}</div>
${skillChipRow([s.性质, s.细化程度])}
${s.在舞台上的位置 ? `<p>${esc(clampPreviewText(String(s.在舞台上的位置), 140))}</p>` : ""}
${s.服务体验 ? `<p class="ws-muted">服务:${esc(clampPreviewText(String(s.服务体验), 100))}</p>` : ""}
</article>`;
})
.join("")}</div>`,
),
);
}
const status = Array.isArray(body.世界状况) ? body.世界状况 : [];
if (status.length) {
parts.push(
skillSection(
"世界状况",
`<div class="skill-entity-grid">${status
.map((s) => {
if (!s || typeof s !== "object") return "";
return `<article class="skill-entity-card">
<div class="skill-entity-name">${esc(String(s.名称 || "?"))}</div>
${skillChipRow([s.性质, s.作用范围, s.节奏或触发])}
${s.是什么 ? `<p>${esc(clampPreviewText(String(s.是什么), 140))}</p>` : ""}
${s.如何影响运转 ? `<p class="ws-muted">${esc(clampPreviewText(String(s.如何影响运转), 120))}</p>` : ""}
</article>`;
})
.join("")}</div>`,
),
);
}
const zones = Array.isArray(body.关键舞台区) ? body.关键舞台区 : [];
if (zones.length) {
parts.push(
skillSection(
"关键舞台区",
`<div class="skill-entity-grid">${zones
.map((z) => {
if (!z || typeof z !== "object") return "";
return `<article class="skill-entity-card">
<div class="skill-entity-name">${esc(String(z.名称 || "?"))}</div>
${z.是什么 ? `<p>${esc(clampPreviewText(String(z.是什么), 120))}</p>` : ""}
${z.为何需要 ? `<p class="ws-muted">${esc(clampPreviewText(String(z.为何需要), 100))}</p>` : ""}
${Array.isArray(z.格局要点) && z.格局要点.length ? skillProseList(z.格局要点) : ""}
</article>`;
})
.join("")}</div>`,
),
);
}
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 ? `<div class="skill-view skill-view-world-blueprint">${parts.join("")}</div>` : "";
}
/** 实现机制 · 专用正文卡 */
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(
"支撑点",
`<div class="skill-entity-grid">${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 `<article class="skill-entity-card">
<header class="skill-entity-head"><div class="skill-entity-name">${esc(String(p.名称 || "?"))}</div>${mini}</header>
${skillChipRow([p.归属元素, p.支撑切面])}
${morph?.概述 ? `<p>${esc(clampPreviewText(String(morph.概述), 140))}</p>` : ""}
${p.如何支撑 ? `<p class="ws-muted">${esc(clampPreviewText(String(p.如何支撑), 120))}</p>` : ""}
${p.缺失后果 ? `<p class="artifact-fact pending">缺失:${esc(clampPreviewText(String(p.缺失后果), 100))}</p>` : ""}
</article>`;
})
.join("")}</div>`,
),
);
}
const rels = Array.isArray(body.支撑点关系) ? body.支撑点关系 : [];
if (rels.length) {
parts.push(
skillSection(
"支撑点关系",
`<ul class="artifact-list">${rels
.map((r) => {
if (!r || typeof r !== "object") return "";
const who = Array.isArray(r.涉及) ? r.涉及.join(" · ") : "";
return `<li><strong>${esc(String(r.关系 || "关系"))}</strong>${who ? ` · ${esc(who)}` : ""}${
r.体验作用 ? `<div class="ws-muted">${esc(String(r.体验作用))}</div>` : ""
}</li>`;
})
.join("")}</ul>`,
),
);
}
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 ? `<div class="skill-view skill-view-mechanism">${parts.join("")}</div>` : "";
}
/**
* 完备度等0100 百分制(正文诊断块仍用 %)。
* 自评维度请用 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;
}
/** 自评分数010兼容旧百分数>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 = "完备度") {
</div>`;
}
/** 自评十分制条 */
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 `<div class="pct-meter tone-${tone}" role="meter" aria-valuenow="${score}" aria-valuemin="0" aria-valuemax="10" aria-label="${esc(label)} ${shown}/10">
<div class="pct-meter-head">
<span class="pct-meter-label">${esc(label)}</span>
<span class="pct-meter-value">${shown}/10</span>
</div>
<div class="pct-meter-track"><i style="width:${width}%"></i></div>
</div>`;
}
/** 瓷砖用迷你百分比(数字 + 细条),省纵向空间 */
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.说明
? `<p class="artifact-score-note">${esc(clampPreviewText(String(d.说明), 80))}</p>`
: "";
if (pct == null) {
if (ten == null) {
return `<div class="artifact-score"><div class="pct-meter-head"><span class="pct-meter-label">${esc(String(name))}</span><span class="ws-muted">未评分</span></div>${note}</div>`;
}
return `<div class="artifact-score">${renderPctMeterHtml(pct, String(name))}${note}</div>`;
return `<div class="artifact-score">${renderTenMeterHtml(ten, String(name))}${note}</div>`;
})
.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)
}</div>
${isUser ? `<footer class="msg-foot">${headInner}</footer>` : ""}
</div>`;

189
web/present-shells.js Normal file
View File

@@ -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(`
<section class="present-region present-region--${esc(region)}" data-region="${esc(region)}">
${isBody && shell === "prose" ? "" : `<h4 class="present-region-title">${esc(title)}</h4>`}
<div class="present-region-body">${esc(text).replace(/\n/g, "<br>")}</div>
</section>`);
}
const actions = showActions ? packet.meta?.suggested_actions ?? [] : [];
if (actions.length) {
parts.push(`
<section class="present-region present-region--actions" data-region="actions">
<h4 class="present-region-title">建议行动</h4>
<ul class="present-actions">${actions.map((a) => `<li>${esc(a)}</li>`).join("")}</ul>
</section>`);
}
const shellLabel = SHELL_LABELS[shell] || shell;
return `
<div class="present-shell present-shell--${esc(shell)} present-tone--${esc(tone)}" data-shell="${esc(shell)}">
<div class="present-shell-badge" title="呈现壳">${esc(shellLabel)}</div>
${parts.join("")}
</div>`;
}

View File

@@ -817,6 +817,170 @@ body.rail-collapsed .af-mosaic {
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 5px;
}
/* 鎶€鑳戒笓鐢ㄦ<E990A2>鏂囧崱 */
.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;
}