引入游玩呈现壳与旁观维护槽,完善结算合并、十分制自评与技能产物 UI。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -376,7 +376,7 @@ task 必须写:禁止重复同一开场;在首答上补洞。
|
||||
"稳变": "stable",
|
||||
"正文": { },
|
||||
"自评": {
|
||||
"维度": [{ "名": "…", "分数": 0, "说明": "…" }],
|
||||
"维度": [{ "名": "…", "分数": 8, "说明": "…" }],
|
||||
"薄弱点": "…"
|
||||
},
|
||||
"追问": {
|
||||
@@ -396,7 +396,7 @@ task 必须写:禁止重复同一开场;在首答上补洞。
|
||||
| 段 | 说明 |
|
||||
|----|------|
|
||||
| `正文` | **产物主体**。技能自定内部结构(如美学步=设定逻辑 + 交互范式 + 美学纲领);须含「详细各个方面」。禁止整份只交散文。 |
|
||||
| `自评` | **自评评分**。`维度[]` 名目按技能定(美学步常用:交互范式 / 美学纲领 / 整体协调);可含 `薄弱点`。 |
|
||||
| `自评` | **自评评分**。`维度[]` 名目按技能定(美学步常用:交互范式 / 美学纲领 / 整体协调);**`分数` 为 0–10 十分制**(可一位小数;高质约 8–10);可含 `薄弱点`。禁止用 0–100 百分数。 |
|
||||
| `追问` | **示例 + 建议选项**。给用户的下一问;`题目` 可空数组。与程序 askUser 可并存,但产物里要留结构化题面。 |
|
||||
|
||||
| 规则 | 说明 |
|
||||
@@ -404,10 +404,11 @@ task 必须写:禁止重复同一开场;在首答上补洞。
|
||||
| `schema` | 必须原样写出,供前端友好渲染 |
|
||||
| `mount` | 挂哪些槽/ref;**不要**写最终数字 `order` |
|
||||
| `稳变` | `stable` \| `semi` \| `volatile`,供「上下文投影排序」 |
|
||||
| **视图** | 产出形状固定后,该技能须有对应**专用正文卡**(前端按 `技能` / 正文键分流);无专用卡时退回通用结构化。作者交付物=`prompt.md`(含稳定 output)+ 可验收的阅读视图(实现或在清单中登记待做)。 |
|
||||
| 收成例外 | 游玩拓扑 / 上下文投影排序 / 细化终稿用自有 schema,不套本头 |
|
||||
|
||||
范例对齐:`modules/aesthetics-interaction/prompt.md`(output 块)。
|
||||
|
||||
前端已落地专用卡示例:生成规则 / 舞台骨架 / 实现机制 / 美学 mosaic;自评条显示为 `x/10`。
|
||||
---
|
||||
|
||||
# 第五部分:撰写任一能力时的工作步骤
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -21,11 +21,13 @@
|
||||
|
||||
| 槽键 | 默认 ref | 用户可见 | 默认 |
|
||||
|------|----------|----------|------|
|
||||
| `auditor` | `auditor` | 旁观维护 | 开 |
|
||||
| `gm` | `world-simulator` | 主世界层 | 开 |
|
||||
| `narrator` | `narrator` | 叙事转述 | 开 |
|
||||
| `perspective` | `role-decide` | 角色视角 | 关 |
|
||||
|
||||
推荐游玩调度:`perspective? → gm → narrator`。
|
||||
推荐游玩调度:`auditor → perspective? → gm → narrator`。
|
||||
旁观维护默认**不注入**「对话.历史」;主世界层 / 转述按投影排序表裁剪历史。
|
||||
|
||||
## 3. 上下文片段:`context-fragment.v1`
|
||||
|
||||
@@ -45,7 +47,7 @@
|
||||
生成规则正文:必要性判断 + `rules[]`(生成与描写 + **单层**产物格式 + 可选多池绑顶层字段);自评=必要性/属性妥当/格式准确。
|
||||
美学纲领自评仍为:交互范式 / 美学纲领 / 整体协调(勿与生成规则三维混用)。
|
||||
叙事指南与故事推进正文:纲领 / 风格与遣词(+示范) / 笔墨焦点 / 禁忌与不偏好 / 情境备用 / 推进与决策 / 内容与表达;自评=写法可执行/推进可执行/与美学对齐;artifact=`设计.叙事指南与故事推进`。
|
||||
正文组成:用户可见块序 + 监控栏锚点 + 隐藏变量段 + 前端拆分意图(非程序报文);artifact=`设计.正文组成`。
|
||||
正文组成:**基于固定呈现壳适配微调**(选 `shell_id` + 微调轴;块只挂壳已有区域);artifact=`设计.正文组成`。见 `docs/play-presentation-shells.md`。
|
||||
设计监控栏:只监控会变字段(可含攻略目标/场景交互物);artifact=`设计.监控栏`(旧称状态栏)。
|
||||
变量设计:真值 + **维护语句** + **Data映射索引**(指向具体实例)+ side_effects。
|
||||
变量控制上下文:各槽视野 + **旁观汇总**(当前档摘要)。
|
||||
@@ -121,11 +123,20 @@ API:`POST /api/sessions/:id/context-order`(`move` / `set_projection` / `set_
|
||||
|
||||
## 6. UI 渲染
|
||||
|
||||
| schema | 友好渲染 |
|
||||
| schema / 技能 | 友好渲染 |
|
||||
|--------|----------|
|
||||
| `context-fragment.v1` | 概要 / 挂载 / 正文 |
|
||||
| `context-fragment.v1` 外壳 | brief / 挂载 / **自评十分制(x/10)** / 追问 |
|
||||
| 美学纲领与交互范式 | mosaic(设定逻辑 / 交互范式 / 美学纲领) |
|
||||
| 生成规则 | 必要性 + 规则卡(方法、单层字段表、池) |
|
||||
| 舞台骨架 | 尺度 / 社会 / 状况 / 关键区实体卡 |
|
||||
| 实现机制 | 支撑点卡 + 关系 + 覆盖检验 |
|
||||
| 其它技能正文 | 通用结构化(KV);**应补专用卡** |
|
||||
| `context-order.v1` | 扁平序表;历史行高亮 |
|
||||
|
||||
解析失败时勿只甩 JSON 墙:提示无法解析并保留原文折叠。自评 `分数` 契约为 **0–10**(旧百分数会按 /10 兼容显示)。
|
||||
|
||||
技能作者:output 键稳定后,须登记或实现对应视图(见 `capability-authoring-brief.md` §4.9「视图」)。
|
||||
|
||||
## 7. 配方链路
|
||||
|
||||
```text
|
||||
|
||||
143
docs/play-presentation-shells.md
Normal file
143
docs/play-presentation-shells.md
Normal 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;口径明确为创作基于壳适配微调。
|
||||
@@ -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 只负责骰子/抽签。
|
||||
|
||||
编排口诀:体验与机制 → 要不要真值 → 映射表用生成规则/实例 → 维护语句与旁观汇总 → 开场同真相 → 收成。
|
||||
|
||||
@@ -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。
|
||||
|
||||
|
||||
@@ -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 合并变量已接 |
|
||||
| 扩写助手 | 方法论已写;起点:美学纲领与交互范式;勿默认套世界模拟全套 |
|
||||
|
||||
---
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# 美学纲领与交互范式
|
||||
# 美学纲领与交互范式
|
||||
|
||||
> **技能标准形范例**(其它中段技能对齐本结构)。
|
||||
> 程序只切割下方 **fence 块**(`meta` / `opening` / `task` / …);`##` 标题仅供人读。
|
||||
@@ -180,9 +180,9 @@ feeds: design_only
|
||||
},
|
||||
"自评": {
|
||||
"维度": [
|
||||
{ "名": "交互范式", "分数": 0, "说明": "权限边界是否锁住核心体验" },
|
||||
{ "名": "美学纲领", "分数": 0, "说明": "体验内核是否点题、有质感" },
|
||||
{ "名": "整体协调", "分数": 0, "说明": "范式是否在保护纲领" }
|
||||
{ "名": "交互范式", "分数": 8, "说明": "权限边界是否锁住核心体验" },
|
||||
{ "名": "美学纲领", "分数": 8, "说明": "体验内核是否点题、有质感" },
|
||||
{ "名": "整体协调", "分数": 8, "说明": "范式是否在保护纲领" }
|
||||
],
|
||||
"薄弱点": "一句话"
|
||||
},
|
||||
@@ -202,7 +202,7 @@ feeds: design_only
|
||||
|
||||
硬规则(公共三段 + 本技正文):
|
||||
1. 合法 JSON;必含 `schema` / `技能` / `brief` / `正文` / `自评` / `追问`(题目可 `[]`)。
|
||||
2. **公共三段**:`正文`=产物主体;`自评`=完备度打分;`追问`=导语+题目(建议选项/示例)。禁止用散文代替这三段。
|
||||
2. **公共三段**:`正文`=产物主体;`自评`=完备度打分(**分数 0–10 十分制,禁止百分数**);`追问`=导语+题目(建议选项/示例)。禁止用散文代替这三段。
|
||||
3. **本技正文三块**:`设定逻辑`(参与/内容维度/区域/复杂性/完备度)+ `交互范式` + `美学纲领`。未知省略或「未定」,禁止编造百科。
|
||||
4. `mount` 建议挂主世界层+转述;**禁止**写数字 `order`。`稳变` 定稿后多为 `stable`。
|
||||
5. `追问.题目` 一次宜 1~3 题;有建议选项;允许「其它」。已答清的不要再问。
|
||||
|
||||
@@ -120,8 +120,8 @@ modules:
|
||||
- id: reply-format
|
||||
name: 正文组成
|
||||
declaration: >-
|
||||
钉用户可见的一轮回复由哪些块、何顺序组成(含监控栏位置、隐藏变量段、前端拆分);
|
||||
不是程序报文格式;旧称设计回复格式
|
||||
基于固定呈现壳做适配微调(选 shell_id + 字段/显示名等);
|
||||
产出壳适配单,禁止从零发明布局;旧称设计回复格式
|
||||
artifact: 设计.正文组成
|
||||
|
||||
# —— 开局(创作偏晚) ——
|
||||
|
||||
@@ -87,7 +87,7 @@ summary:`具体实例 · {对象} · {n}条`
|
||||
"维度": [
|
||||
{
|
||||
"名": "合规模",
|
||||
"分数": 0,
|
||||
"分数": 8,
|
||||
"说明": "是否严格按该规则的方法、单层格式、池与约束生成"
|
||||
}
|
||||
],
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# 生成规则
|
||||
# 生成规则
|
||||
|
||||
> 技能文档。程序只切割下方 **fence 块**;`##` 标题仅供人读。
|
||||
> 产物外壳:`docs/context-fragment-design.md`;范例:`aesthetics-interaction` / `mechanism`。
|
||||
@@ -197,17 +197,17 @@ summary:建立 → `生成规则 · {对象} · {仅动态|仅预生成|预生
|
||||
"维度": [
|
||||
{
|
||||
"名": "必要性",
|
||||
"分数": 0,
|
||||
"分数": 8,
|
||||
"说明": "整条规则:是否有必要单独用「生成规则」承接该类内容;跳过时理由是否充分"
|
||||
},
|
||||
{
|
||||
"名": "属性妥当",
|
||||
"分数": 0,
|
||||
"分数": 8,
|
||||
"说明": "逐字段:有无冗余(如女主规则的性别)或缺失(如 D&D 怪物缺等级/攻击骰);跳过时写 N/A"
|
||||
},
|
||||
{
|
||||
"名": "格式准确",
|
||||
"分数": 0,
|
||||
"分数": 8,
|
||||
"说明": "是否严格单层 JSON、类型/必填/枚举/边界可对接动态表格·旁观者生成·投影;跳过时写 N/A"
|
||||
}
|
||||
],
|
||||
@@ -229,7 +229,7 @@ summary:建立 → `生成规则 · {对象} · {仅动态|仅预生成|预生
|
||||
|
||||
硬规则(公共三段 + 本技正文):
|
||||
1. 合法 JSON;必含 `schema` / `技能` / `brief` / `正文` / `自评` / `追问`(题目可 `[]`)。
|
||||
2. **公共三段**:正文=规则主体;自评=必要性/属性妥当/格式准确;追问=导语+建议选项。
|
||||
2. **公共三段**:正文=规则主体;自评=必要性/属性妥当/格式准确(**分数 0–10 十分制**);追问=导语+建议选项。
|
||||
3. **正文键固定**:本步参数、必要性判断、rules、增量说明。
|
||||
4. **每条 rule 必含**:生成与描写、产物格式;**池可空数组**。有池时 `绑定字段` 必须是单层 schema 的顶层键。
|
||||
5. **产物格式强制单层**:`schema` 的 `type` 不得为 `object`;`array` 的 `items` 不得为 object。禁止 `外貌.发色` 这类嵌套路径。
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# 实现机制
|
||||
# 实现机制
|
||||
|
||||
> 技能文档。程序只切割下方 **fence 块**;`##` 标题仅供人读。
|
||||
> 产物外壳对齐:`docs/context-fragment-design.md`;范例:`aesthetics-interaction`。
|
||||
@@ -203,17 +203,17 @@ summary:`实现机制 · …`(点题主要支撑)。
|
||||
"维度": [
|
||||
{
|
||||
"名": "覆盖度",
|
||||
"分数": 0,
|
||||
"分数": 8,
|
||||
"说明": "能否完全覆盖用户已表达的规则/前提/硬条件(未漏掉用户点名的承重约束)"
|
||||
},
|
||||
{
|
||||
"名": "充分度",
|
||||
"分数": 0,
|
||||
"分数": 8,
|
||||
"说明": "支撑点合起来能否充分展示用户想要的核心体验(缺了是否撑不住感觉)"
|
||||
},
|
||||
{
|
||||
"名": "克制度",
|
||||
"分数": 0,
|
||||
"分数": 8,
|
||||
"说明": "是否精确到支撑切面;无多余百科、人物卡、起源史或题材默认拓展"
|
||||
}
|
||||
],
|
||||
@@ -235,7 +235,7 @@ summary:`实现机制 · …`(点题主要支撑)。
|
||||
|
||||
硬规则(公共三段 + 本技正文):
|
||||
1. 合法 JSON;必含 `schema` / `技能` / `brief` / `正文` / `自评` / `追问`(题目可 `[]`)。
|
||||
2. **公共三段**:`正文`=产物主体;`自评`=打分;`追问`=导语+建议选项。禁止整份散文壳。
|
||||
2. **公共三段**:`正文`=产物主体;`自评`=打分(**分数 0–10 十分制**);`追问`=导语+建议选项。禁止整份散文壳。
|
||||
3. **本技正文**:`依据的核心体验` + `支撑点[]` + `支撑点关系[]` + `覆盖检验`。基准世界/推理过程/淘汰候选不得写入。
|
||||
4. 每个支撑点须有归属元素、支撑切面、如何支撑、缺失后果;简单支点结构=「一句话」、展开=`[]`。
|
||||
5. `mount` 默认主世界层(`world-simulator`);**禁止**写 `order`。承重规则进 GM,不进转述全文。
|
||||
|
||||
@@ -212,17 +212,17 @@ summary:`叙事指南与故事推进 · …`(点题体裁/笔墨重心 + 推
|
||||
"维度": [
|
||||
{
|
||||
"名": "写法可执行",
|
||||
"分数": 0,
|
||||
"分数": 8,
|
||||
"说明": "遣词+笔墨焦点+禁忌/不偏好+示范是否够转述落地"
|
||||
},
|
||||
{
|
||||
"名": "推进可执行",
|
||||
"分数": 0,
|
||||
"分数": 8,
|
||||
"说明": "主世界层是否知道如何推进;变量钩子清楚或明确无需"
|
||||
},
|
||||
{
|
||||
"名": "与美学对齐",
|
||||
"分数": 0,
|
||||
"分数": 8,
|
||||
"说明": "是否把感觉落成做法;笔墨与禁忌服务体验而非跑偏"
|
||||
}
|
||||
],
|
||||
|
||||
@@ -121,17 +121,17 @@ summary:`开场白与开场变量 · …`(点题场景)。
|
||||
"维度": [
|
||||
{
|
||||
"名": "契约符合",
|
||||
"分数": 0,
|
||||
"分数": 8,
|
||||
"说明": "是否遵循正文组成/叙事/监控栏"
|
||||
},
|
||||
{
|
||||
"名": "同真相",
|
||||
"分数": 0,
|
||||
"分数": 8,
|
||||
"说明": "开场叙述与开场变量是否一致"
|
||||
},
|
||||
{
|
||||
"名": "可开玩",
|
||||
"分数": 0,
|
||||
"分数": 8,
|
||||
"说明": "是否短、可感、有下一步行动空间"
|
||||
}
|
||||
],
|
||||
|
||||
@@ -58,15 +58,15 @@ boundary: |
|
||||
执行顺序:
|
||||
1. 复述站位、体验内核、禁忌;矛盾处 askUser 1 点。
|
||||
2. 写入 `play_slots`(世界模拟)或等价写手槽;**workers 只含白名单 ref**:
|
||||
- world_sim 每轮:world-simulator / narrator / role-decide(仅 perspective 开时)
|
||||
- world_sim 每轮:auditor / world-simulator / narrator / role-decide(仅 perspective 开时)
|
||||
- world_sim 按需:chance(仅 chance 开时;`invocation: on_demand`)
|
||||
- writing:outline / chapter-writer
|
||||
- 程序也会按 play_slots 展开;你仍应写出与槽一致的 workers[](含 acceptance),便于人读验收。
|
||||
3. **禁止**自造 ref、禁止添加 variable-update / lore-keeper / 自造骰子 LLM 等。
|
||||
4. resident_context:稳定句挂到 gm 或 narrator(或 outline/chapter-writer);勿塞聊天过程。
|
||||
4. resident_context:稳定句挂到 gm 或 narrator(或 outline/chapter-writer);旁观合同可挂 auditor;勿塞聊天过程。
|
||||
5. tables:吸入变量设计的 side_effects;无则空数组或省略。
|
||||
6. core_premises、design_end.opening 按需。
|
||||
7. summary:`细化终稿 · 槽 gm+转述 · …` 或 `细化终稿 · 大纲+章节 · …`
|
||||
7. summary:`细化终稿 · 槽 旁观+gm+转述 · …` 或 `细化终稿 · 大纲+章节 · …`
|
||||
|
||||
进游玩不在本步完成。
|
||||
```
|
||||
@@ -75,9 +75,9 @@ boundary: |
|
||||
|
||||
```principles
|
||||
1. 合并优于重写;槽位优于发明演员。
|
||||
2. 面向用户的终稿点 acceptance=review(通常是 narrator 或 chapter-writer);gm/outline 常用 continue。
|
||||
3. 真值变更写在 gm 的 outputs(运行.本轮.变量变更 / 裁决包内 variable_changes),不靠第三变量 Worker。
|
||||
4. 裁决包约定:运行.本轮.裁决 使用 settlement.v1(见 progressive-data-design / 模板)。
|
||||
2. 面向用户的终稿点 acceptance=review(通常是 narrator 或 chapter-writer);auditor/gm/outline 常用 continue。
|
||||
3. 真值变更写在 gm 的 outputs(运行.本轮.变量变更 / 裁决包内 variable_changes),旁观维护只出 maintain.v1;不靠第三变量 Worker。
|
||||
4. 裁决包约定:运行.本轮.裁决 使用 settlement.v1(见 progressive-data-design / 模板);Runtime 合并 variable_changes。
|
||||
5. 键名稳定;未决进 open_questions。
|
||||
```
|
||||
|
||||
@@ -110,16 +110,25 @@ boundary: |
|
||||
"satisfaction_source": "…"
|
||||
},
|
||||
"play_slots": {
|
||||
"auditor": true,
|
||||
"gm": true,
|
||||
"narrator": true,
|
||||
"perspective": false
|
||||
},
|
||||
"workers": [
|
||||
{
|
||||
"name": "旁观维护",
|
||||
"ref": "auditor",
|
||||
"duty": "表/规则检查;maintain.v1;默认空操作;无长对话史",
|
||||
"when": "每轮用户输入后、主世界层之前",
|
||||
"rationale": "删掉则表补与规则触发放回主世界层,挤占历史上下文",
|
||||
"acceptance": "continue"
|
||||
},
|
||||
{
|
||||
"name": "主世界层",
|
||||
"ref": "world-simulator",
|
||||
"duty": "读投影与真值,输出 settlement.v1 裁决包;可提议变量变更",
|
||||
"when": "每轮用户输入后",
|
||||
"when": "每轮用户输入后(旁观之后)",
|
||||
"rationale": "删掉则无程序化裁决与真值更新",
|
||||
"acceptance": "continue"
|
||||
},
|
||||
@@ -175,7 +184,7 @@ boundary: |
|
||||
|
||||
```examples
|
||||
好:
|
||||
- play_slots gm+narrator;workers 两条与槽一致;side_effects 从变量设计拷入。
|
||||
- play_slots auditor+gm+narrator;workers 与槽一致;side_effects 从变量设计拷入。
|
||||
- 扩写:outline continue + chapter-writer review;无自造 ref。
|
||||
|
||||
坏:
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# 正文组成
|
||||
|
||||
> 技能文档。程序只切割下方 **fence 块**;`##` 标题仅供人读。
|
||||
> **创作定位:基于固定呈现壳做适配微调**(`docs/play-presentation-shells.md`)。
|
||||
> 产物外壳:`docs/context-fragment-design.md`。
|
||||
> **不是**程序 API/JSON schema 格式;钉的是用户最后看见的「一封信怎么拼起来」。
|
||||
> 旧称:设计回复格式。
|
||||
|
||||
## meta
|
||||
@@ -12,37 +12,37 @@ name: 正文组成
|
||||
id: reply-format
|
||||
artifact: 设计.正文组成
|
||||
declaration: >
|
||||
钉用户可见的一轮回复由哪些块、何顺序组成(抬头/日期/正文/监控栏/文末吐槽等),
|
||||
含对 LLM 隐藏的变量维护段与前端拆分/美化提示;不等于程序报文格式
|
||||
基于四大固定呈现壳做适配微调(选 shell_id + 填字段/显示名/开关等);
|
||||
产出壳适配单;禁止从零发明布局或完整 UI
|
||||
when: |
|
||||
用户看见的不该只是「一整段散文」,需要版式块(抬头、监控栏、文末小块等);
|
||||
或需要约定隐藏变量段供模型维护表格;或需要前端分区渲染时。
|
||||
需要为本局选定呈现壳,或微调监控字段/块显示名/建议行动开关时;
|
||||
或需要约定隐藏变量段时。
|
||||
when_not: |
|
||||
纯单段叙事、明确不要分块版式 → 可不排(转述直接出一段即可)。
|
||||
只谈文风遣词 → 「叙事指南与故事推进」。
|
||||
只钉监控栏字段清单、版式已定 → 「设计监控栏」。
|
||||
块结构未定时不要先空谈 CSS 细节。
|
||||
明确只要纯散文 → 可不排(运行时默认 prose)。
|
||||
只谈文风 → 「叙事指南与故事推进」。
|
||||
壳已定、只筛监控字段明细 → 「设计监控栏」。
|
||||
不要在本步设计新布局或写 CSS。
|
||||
boundary: |
|
||||
本技能:用户可见组成(块清单、顺序、可见/隐藏、前端拆分意图);产出 context-fragment.v1。
|
||||
不是 OpenAI/函数调用报文,也不是结算包 schema。
|
||||
设计监控栏:监控栏内有哪些可变字段——可本步给骨架,细则可交监控栏技能。
|
||||
叙事指南与故事推进:怎么写正文——本步定「正文」块在版式里的位置与职责,不重写文风。
|
||||
变量设计:真值如何变——本步只约定隐藏段如何承载变更声明,不写 side_effects。
|
||||
本技能:壳适配单(shell_id + 微调轴 + 块挂到壳已有区域)。
|
||||
布局骨架只读:docs/play-presentation-shells.md。
|
||||
设计监控栏:字段表细则。叙事指南:文风。变量设计:真值规则。
|
||||
feeds: narrator
|
||||
```
|
||||
|
||||
## opening
|
||||
|
||||
```opening
|
||||
用户每一轮「看见的东西」希望像什么?(想到什么写什么)
|
||||
本局在固定呈现壳上做适配(不是设计新界面)。
|
||||
|
||||
1. 由哪些部分组成?什么顺序?
|
||||
例:抬头 + 日期/地点 + 正文;或正文 + 监控栏;或文末再加小吐槽/模拟书评……
|
||||
0. 四选一壳:
|
||||
· prose — 纯散文
|
||||
· chat_monitor — 对话 + 顶栏监控
|
||||
· turn_panel — 回合面板
|
||||
· chapter_reader — 章节阅读
|
||||
|
||||
2. 有没有要「藏起来给模型/程序看、用户界面默认不展示」的段?
|
||||
例:用特殊标记包住的变量变更,方便拆解维护表格。
|
||||
|
||||
3. 前端要不要拆成多个区域美化?(监控栏固定顶栏、正文滚动、文末折叠……)没有就说没有。
|
||||
1. 要微调什么?(监控字段、块称呼、要不要建议行动、tone_chrome…)
|
||||
2. 有没有隐藏段给模型/程序维护变量?
|
||||
3. 不要谈「再发明一种全新布局」。
|
||||
```
|
||||
|
||||
## task
|
||||
@@ -50,42 +50,38 @@ feeds: narrator
|
||||
```task
|
||||
你正在执行「正文组成」。产物必须是 **context-fragment.v1** JSON,写入「设计.正文组成」。
|
||||
|
||||
本步设计的是**用户最后看见的内容版式**——像一封信由收信人、正文、日期、发信人组成;
|
||||
也可以是抬头、日期、地点、正文、监控栏、文末吐槽、小故事、模拟书评等。
|
||||
**不是**程序接口格式。
|
||||
本步 = **壳适配单**:四大壳里选一个,只改允许的微调轴。
|
||||
权威:`docs/play-presentation-shells.md`。
|
||||
禁止从零设计分区、禁止自造 shell_id、禁止完整 CSS。
|
||||
|
||||
执行顺序:
|
||||
1. 读美学呈现与叙事指南:继承人称/终稿由谁写;不重开文风问卷。
|
||||
2. 列出「可见块」:每块职责、顺序、是否可空、由谁填充(转述/程序拼装/监控栏技能)。
|
||||
3. 若有监控栏:写清它在版式中的位置与职责;字段级细则可引用或留给「设计监控栏」(只留会变、要盯的信息)。
|
||||
4. 若有隐藏段:写清标记约定(如正则/定界符)、内容用途(变量变更声明)、用户侧默认隐藏、模型须输出以便拆表。
|
||||
5. 写「前端拆分与美化」意图(区域、可否折叠、是否 HTML 片段);无则说明纯 Markdown/纯文本。
|
||||
6. 自评 + 追问;输出 JSON。禁止写最终 context `order`。
|
||||
1. 读美学/叙事:继承人称与终稿归属;不重开文风问卷。
|
||||
2. **选 shell_id**;说明为何是这个壳而不是另三个。
|
||||
3. 可见块:只挂到该壳已有区域;写显示名与职责(适配文案,不是新分区)。
|
||||
4. 监控栏:字段意图(细则可交「设计监控栏」);无 monitor 的壳写「不启用」。
|
||||
5. 隐藏段(若有):定界与用途。
|
||||
6. 填「呈现壳.微调」;前端拆分区域必须与壳一致。
|
||||
7. 自评 + 追问;禁止写 context `order`。
|
||||
|
||||
summary:`正文组成 · …`(点题主要块序)。
|
||||
summary:`正文组成 · 适配 {shell_id} · …`。
|
||||
```
|
||||
|
||||
## principles
|
||||
|
||||
```principles
|
||||
1. 用户看见的版式,不是程序报文
|
||||
用「信/报纸/游戏 HUD」类比思考块;禁止把本步写成 API 字段说明书。
|
||||
1. 创作 = 选壳 + 适配微调
|
||||
不是 UI 创意;布局骨架只读。
|
||||
|
||||
2. 块要少、每块有职责
|
||||
宁缺毋滥;装饰块必须服务体验(吐槽/书评若只是玩梗也要写清触发与长度)。
|
||||
2. 只改微调轴
|
||||
字段、显示名、开关、tone_chrome、空态;不改分区、不增区域、不自造壳。
|
||||
|
||||
3. 监控栏 ≠ 名片卡
|
||||
监控的是会变、影响决策或沉浸的信息(好感、体力、场景可交互对象、攻略目标状态…)。
|
||||
主角「姓名」「性别」等几乎不变的内容默认不要进监控栏(绝大多数局)。
|
||||
只盯会变、影响决策或沉浸的信息。
|
||||
|
||||
4. 隐藏段服务维护,不污染阅读
|
||||
变量变更等可用定界符包住,供 LLM 输出、程序/旁路拆解入表;用户 UI 默认不渲染或折叠。
|
||||
|
||||
5. 前端意图只写「拆哪里、干什么」
|
||||
可提:顶栏监控 / 正文区 / 文末折叠 / 简易 HTML 片段。不写完整 CSS 工程。
|
||||
|
||||
6. 与转述、变量分工
|
||||
正文块文风归叙事指南;真值规则归变量设计;本步定拼装契约。
|
||||
5. 与转述、变量分工
|
||||
文风归叙事指南;真值归变量设计;本步只交壳适配契约。
|
||||
```
|
||||
|
||||
## probe
|
||||
@@ -93,8 +89,8 @@ summary:`正文组成 · …`(点题主要块序)。
|
||||
```probe
|
||||
每轮 1~2 点。
|
||||
|
||||
优先:必须有哪些可见块;监控栏要不要、盯什么类型信息;隐藏变量段要不要及标记长什么样;前端要不要分区。
|
||||
不追问:完整 CSS、事件池 schema、文风遣词细节(除非块职责不清)。
|
||||
优先:选哪个壳;微调哪些字段/显示名;隐藏段要不要。
|
||||
不追问:新布局、完整 CSS、事件池 schema、文风遣词。
|
||||
```
|
||||
|
||||
## output
|
||||
@@ -103,30 +99,41 @@ summary:`正文组成 · …`(点题主要块序)。
|
||||
{
|
||||
"schema": "context-fragment.v1",
|
||||
"技能": "正文组成",
|
||||
"brief": "一句话:用户看见的主要块序",
|
||||
"brief": "一句话:壳 + 主要块序",
|
||||
"mount": ["narrator"],
|
||||
"稳变": "stable",
|
||||
"正文": {
|
||||
"依据的体验与呈现": [
|
||||
"从美学/叙事继承的呈现要点"
|
||||
],
|
||||
"版式隐喻": "例:一封信 / 网文章节页 / 游戏回合面板 / 无(纯散文)",
|
||||
"呈现壳": {
|
||||
"shell_id": "prose|chat_monitor|turn_panel|chapter_reader",
|
||||
"为何选它": "一句话",
|
||||
"微调": {
|
||||
"tone_chrome": "messenger|terminal|book|default",
|
||||
"show_suggested_actions": true,
|
||||
"block_labels": {},
|
||||
"empty_states": {}
|
||||
}
|
||||
},
|
||||
"版式隐喻": "给人读的比喻;程序以 shell_id 为准",
|
||||
"可见块": [
|
||||
{
|
||||
"块id": "英文 kebab 或中文短名,同局稳定",
|
||||
"区域": "monitor|header|body|footer|aside",
|
||||
"显示名": "用户可理解的标题;可无标题则空",
|
||||
"职责": "这块给用户什么信息",
|
||||
"顺序": 1,
|
||||
"可空": true,
|
||||
"填充方": "叙事转述|程序拼装|监控栏|其它",
|
||||
"内容形态": "散文|短列表|键值行|HTML片段|其它",
|
||||
"内容形态": "散文|短列表|键值行|其它",
|
||||
"示例": "可选一句/三行示意"
|
||||
}
|
||||
],
|
||||
"监控栏锚点": {
|
||||
"本局是否启用": "是|否",
|
||||
"在可见块中的块id": "若启用则指向可见块之一",
|
||||
"职责摘要": "盯哪些类信息(详表见设计.监控栏或本步字段草稿)",
|
||||
"职责摘要": "盯哪些类信息",
|
||||
"字段草稿": [
|
||||
{
|
||||
"名": "会变且值得盯的字段",
|
||||
@@ -138,8 +145,8 @@ summary:`正文组成 · …`(点题主要块序)。
|
||||
"隐藏段": [
|
||||
{
|
||||
"段id": "variable-delta",
|
||||
"用途": "变量维护语句(与「变量设计.维护语句约定」同构),供程序拆进变量.当前",
|
||||
"定界或正则约定": "例:<<<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.v1;prose 可为纯 Markdown",
|
||||
"与裁决包关系": "转述读 settlement 再填各块;隐藏段可含 variable_changes 镜像"
|
||||
}
|
||||
},
|
||||
@@ -168,18 +176,18 @@ summary:`正文组成 · …`(点题主要块序)。
|
||||
"维度": [
|
||||
{
|
||||
"名": "可见完备",
|
||||
"分数": 0,
|
||||
"分数": 8,
|
||||
"说明": "用户该看见的块是否齐、顺序是否服务体验"
|
||||
},
|
||||
{
|
||||
"名": "监控克制",
|
||||
"分数": 0,
|
||||
"说明": "监控栏是否只盯会变信息;无姓名性别等死字段堆砌(若无监控栏可 N/A)"
|
||||
"分数": 8,
|
||||
"说明": "监控栏是否只盯会变信息(若无监控栏可 N/A)"
|
||||
},
|
||||
{
|
||||
"名": "可实现",
|
||||
"分数": 0,
|
||||
"说明": "隐藏段约定与前端拆分是否清楚到可交给转述/程序/前端"
|
||||
"分数": 8,
|
||||
"说明": "shell_id 是否在四大壳内;分区与隐藏段是否可交给转述/程序"
|
||||
}
|
||||
],
|
||||
"薄弱点": "一句话"
|
||||
@@ -200,32 +208,32 @@ summary:`正文组成 · …`(点题主要块序)。
|
||||
|
||||
硬规则:
|
||||
1. 合法 JSON;含公共三段外壳。
|
||||
2. 正文键固定:依据的体验与呈现、版式隐喻、可见块、监控栏锚点、隐藏段、前端拆分与美化、拼装与终稿。
|
||||
3. `可见块` 至少 1 条(通常含正文);`隐藏段`/`字段草稿` 可 `[]`。
|
||||
4. 禁止把本步写成程序 API schema;禁止 `order`(上下文投影序)。
|
||||
5. 自评:可见完备 / 监控克制 / 可实现。
|
||||
6. summary:`正文组成 · {brief 缩略}`。
|
||||
7. 兼容旧称产物 tag「设计.回复格式」:细化终稿可读新 tag,旧局可仍挂旧名。
|
||||
2. 正文须含 `呈现壳.shell_id`(四大之一)与 `微调`;可见块只挂壳已有区域。
|
||||
3. `可见块` 至少 1 条(通常含 body);禁止自造区域 id / 第五种壳。
|
||||
4. 禁止程序 API schema、context `order`、完整 CSS。
|
||||
5. 自评:可见完备 / 监控克制 / 可实现(=壳适配是否可执行)。
|
||||
6. summary:`正文组成 · 适配 {shell_id} · {brief 缩略}`。
|
||||
7. 兼容旧称产物 tag「设计.回复格式」。
|
||||
|
||||
## checklist
|
||||
|
||||
```checklist
|
||||
- [ ] 明确是用户可见版式,而非程序报文?
|
||||
- [ ] 可见块有顺序与职责?监控栏未塞不变名片字段?
|
||||
- [ ] 隐藏段(若有)定界与用途清楚?前端拆分有或明确单流?
|
||||
- [ ] 自评三维?未越权重写文风或 side_effects?
|
||||
- [ ] 是「选壳+微调」,不是从零设计布局?
|
||||
- [ ] shell_id 在四大壳内?可见块未超出该壳分区?
|
||||
- [ ] 微调轴已填或明确默认?监控栏未塞死名片字段?
|
||||
- [ ] 隐藏段(若有)定界清楚?未写 CSS 工程?
|
||||
```
|
||||
|
||||
## examples
|
||||
|
||||
```examples
|
||||
好:
|
||||
- 块序:监控栏 → 正文 → 文末吐槽;隐藏段 <<<VARS>>> 维护好感变更。
|
||||
- 书信局:抬头/日期/正文/落款;无监控栏。
|
||||
- 监控栏只盯:体力、攻略目标好感、场景可交互对象——不写主角姓名性别。
|
||||
- 适配 chat_monitor:微调监控字段=好感/今日话题;body 显示名=「短信」。
|
||||
- 适配 chapter_reader:只开 header+body;aside 关。
|
||||
- 适配 turn_panel:开 suggested_actions;监控只盯体力/好感。
|
||||
|
||||
坏:
|
||||
- 把 settlement.v1 字段表当「回复格式」。
|
||||
- 监控栏列出姓名、性别、种族等死设定。
|
||||
- 只写「要好看的 UI」无块清单。
|
||||
- 「我们自定义一种左右分栏新界面」。
|
||||
- 不写 shell_id,只写「要好看的 UI」。
|
||||
- 监控栏列出姓名、性别等死设定。
|
||||
```
|
||||
|
||||
@@ -129,17 +129,17 @@ summary:`设计监控栏 · …`(点题监控对象)。
|
||||
"维度": [
|
||||
{
|
||||
"名": "属性妥当",
|
||||
"分数": 0,
|
||||
"分数": 8,
|
||||
"说明": "逐字段:无死名片;该盯的生存/关系/场景信息未漏"
|
||||
},
|
||||
{
|
||||
"名": "克制度",
|
||||
"分数": 0,
|
||||
"分数": 8,
|
||||
"说明": "字段是否够少、展示是否够短"
|
||||
},
|
||||
{
|
||||
"名": "可接线",
|
||||
"分数": 0,
|
||||
"分数": 8,
|
||||
"说明": "来源意图是否接得上变量/投影/正文组成"
|
||||
}
|
||||
],
|
||||
|
||||
@@ -111,17 +111,17 @@ summary:`变量控制上下文 · …`。
|
||||
"维度": [
|
||||
{
|
||||
"名": "视野正确",
|
||||
"分数": 0,
|
||||
"分数": 8,
|
||||
"说明": "各槽该见/不该见是否清楚"
|
||||
},
|
||||
{
|
||||
"名": "旁观可用",
|
||||
"分数": 0,
|
||||
"分数": 8,
|
||||
"说明": "旁观汇总是否够用且未整表;不需要旁观则 N/A"
|
||||
},
|
||||
{
|
||||
"名": "防剧透",
|
||||
"分数": 0,
|
||||
"分数": 8,
|
||||
"说明": "未解锁与禁止泄露是否落实"
|
||||
}
|
||||
],
|
||||
|
||||
@@ -166,17 +166,17 @@ summary:`变量设计 · …`(点题主真值 + 有无映射表)。
|
||||
"维度": [
|
||||
{
|
||||
"名": "承重度",
|
||||
"分数": 0,
|
||||
"分数": 8,
|
||||
"说明": "真值是否只覆盖必须跨轮记住的状态"
|
||||
},
|
||||
{
|
||||
"名": "可维护",
|
||||
"分数": 0,
|
||||
"分数": 8,
|
||||
"说明": "维护语句是否可解析;映射索引是否接得上实例/规则"
|
||||
},
|
||||
{
|
||||
"名": "克制度",
|
||||
"分数": 0,
|
||||
"分数": 8,
|
||||
"说明": "无派生可写格膨胀;无用 chance 冒充改表"
|
||||
}
|
||||
],
|
||||
|
||||
@@ -11,11 +11,11 @@ name: 游玩拓扑
|
||||
id: worker-spec
|
||||
artifact: 设计.worker规格
|
||||
declaration: >
|
||||
勾选固定游玩槽位(主世界层 / 叙事转述 / 可选角色视角 / 可选机遇裁定;写手路径为大纲+章节),
|
||||
勾选固定游玩槽位(旁观维护 / 主世界层 / 叙事转述 / 可选角色视角 / 可选机遇裁定;写手路径为大纲+章节),
|
||||
禁止自由发明新的执行单元 ref
|
||||
when: |
|
||||
体验契约已大致清楚,需要决定游玩期启用哪些固定槽时;
|
||||
或要修订已勾选槽位(开/关 perspective、chance 等)时。
|
||||
或要修订已勾选槽位(开/关 auditor、perspective、chance 等)时。
|
||||
when_not: |
|
||||
体验/站位仍混沌 → 先美学纲领与交互范式。
|
||||
只需收成完整运行规格 → 交给细化终稿(本步只交槽位勾选)。
|
||||
@@ -24,6 +24,7 @@ boundary: |
|
||||
本能力:输出 play_slots(及写手路径的 writing_slots),可选覆盖挂载说明;不写完整 设计.worker集。
|
||||
细化终稿:按本步勾选展开 workers、合并常驻与 tables。
|
||||
变量设计 / 变量控制上下文:真值与投影,不是推理槽。
|
||||
旁观维护(auditor):副 LLM,表/规则检查,默认空操作;无长对话史。
|
||||
机遇裁定(chance):按需程序工具槽,不进每轮管线。
|
||||
叙事指南与故事推进:挂到转述槽(推进可兼挂主世界层);世界/机制:挂到主世界层——本步只点名槽。
|
||||
```
|
||||
@@ -34,10 +35,11 @@ boundary: |
|
||||
游玩时用哪些固定槽?(只勾选,不要发明新角色名当「新系统」)
|
||||
|
||||
世界模拟类常见:
|
||||
1. 主世界层(裁决,几乎总要)——要 / 不要
|
||||
2. 叙事转述(写你看见的正文,几乎总要)——要 / 不要
|
||||
3. 角色视角(仅当有强秘密、不能进主世界层时)——要 / 不要(默认不要)
|
||||
4. 机遇裁定(骰子/抽签/比点等真随机,按需调用、不进每轮)——要 / 不要(默认不要;有战斗检定、抽签事件时建议开)
|
||||
1. 旁观维护(表/规则检查,默认每轮上场但多数轮空操作)——要 / 不要(默认要)
|
||||
2. 主世界层(裁决,几乎总要)——要 / 不要
|
||||
3. 叙事转述(写你看见的正文,几乎总要)——要 / 不要
|
||||
4. 角色视角(仅当有强秘密、不能进主世界层时)——要 / 不要(默认不要)
|
||||
5. 机遇裁定(骰子/抽签/比点等真随机,按需调用、不进每轮)——要 / 不要(默认不要;有战斗检定、抽签事件时建议开)
|
||||
|
||||
写手/扩写类常见:大纲/细纲 + 章节正文(固定两槽,同上只勾选)。
|
||||
|
||||
@@ -52,18 +54,19 @@ boundary: |
|
||||
核心操作:让用户勾选固定槽,写成 `play_slots`(世界模拟)或 `writing_slots`(扩写)。**禁止**新建未在固定列表中的 ref。
|
||||
|
||||
固定 ref 白名单:
|
||||
- 世界模拟每轮:`world-simulator`(gm)、`narrator`、`role-decide`(perspective,默认关)
|
||||
- 世界模拟每轮:`auditor`(旁观维护)、`world-simulator`(gm)、`narrator`、`role-decide`(perspective,默认关)
|
||||
- 世界模拟按需:`chance`(机遇裁定,默认关;invocation=on_demand)
|
||||
- 扩写:`outline`、`chapter-writer`
|
||||
- 禁止:`variable-update`、自造 kebab、为世界观/性格再拆槽
|
||||
|
||||
执行顺序:
|
||||
1. 读配方与体验契约,判断路径:世界模拟 vs 写手分段。
|
||||
2. 默认世界模拟:`gm: true, narrator: true, perspective: false, chance: false`;
|
||||
2. 默认世界模拟:`auditor: true, gm: true, narrator: true, perspective: false, chance: false`;
|
||||
调度序 auditor → perspective? → gm → narrator;
|
||||
仅信息隔离才开 perspective;需要骰子/抽签/比点等真随机时开 chance。
|
||||
3. 写手路径:`outline` + `chapter-writer` 默认都开;用户明确只要正文则可关 outline。
|
||||
4. 可写简短 `mount_notes`(哪类上游产物挂哪槽),不粘贴长文。
|
||||
5. 输出 JSON。summary:`游玩拓扑 · gm+转述` 或 `游玩拓扑 · gm+转述+机遇` 等。
|
||||
5. 输出 JSON。summary:`游玩拓扑 · 旁观+gm+转述` 或 `游玩拓扑 · gm+转述+机遇` 等。
|
||||
|
||||
若程序已发默认问题:禁止重复同一开场;在首答上补洞。
|
||||
```
|
||||
@@ -72,8 +75,8 @@ boundary: |
|
||||
|
||||
```principles
|
||||
1. 只勾选,不发明:ref 必须在白名单内。
|
||||
2. 默认少槽:世界模拟 = 主世界层 + 转述;perspective 默认关。
|
||||
3. 变量 / Data / Progressive 不是执行单元。
|
||||
2. 默认:世界模拟 = 旁观维护 + 主世界层 + 转述;perspective 默认关。
|
||||
3. 变量 / Data / Progressive 不是执行单元;旁观维护只出 maintain.v1,不写真相。
|
||||
4. 删掉检验仍适用:关某个槽要说得清损失什么。
|
||||
5. 本步不输出完整 Worker 集、不写 tables 全文(交给细化终稿 / 变量设计)。
|
||||
6. 可修订已有勾选,不要为「更聪明」加槽。
|
||||
@@ -98,6 +101,7 @@ boundary: |
|
||||
"brief": "一句话:本局启用哪些固定槽",
|
||||
"path": "world_sim|writing",
|
||||
"play_slots": {
|
||||
"auditor": true,
|
||||
"gm": true,
|
||||
"narrator": true,
|
||||
"perspective": false,
|
||||
@@ -108,6 +112,7 @@ boundary: |
|
||||
"chapter_writer": true
|
||||
},
|
||||
"mount_notes": [
|
||||
"生成规则/变量合同/旁观摘要 → auditor(无长对话史)",
|
||||
"叙事指南与故事推进 → narrator(推进兼 gm)",
|
||||
"世界/机制/变量规则 → gm",
|
||||
"真随机检定 → chance(按需)",
|
||||
@@ -120,7 +125,7 @@ boundary: |
|
||||
填写规则:
|
||||
- `path=world_sim` 时必须有 `play_slots`;`writing_slots` 可省略。
|
||||
- `path=writing` 时必须有 `writing_slots`;`play_slots` 可省略。
|
||||
- `chance` 缺省视为 false;为 true 时不进每轮序,仅可按需调度。
|
||||
- `auditor` / `chance` 缺省:auditor 视为 true,chance 视为 false;chance 为 true 时不进每轮序,仅可按需调度。
|
||||
- 不要输出自造 `actors[]` / 自由 `workers[]`。
|
||||
- 旧产物若含 `actors[]`:本步应改写为槽位勾选,不再追加自定义 ref。
|
||||
|
||||
@@ -128,7 +133,7 @@ boundary: |
|
||||
|
||||
```checklist
|
||||
- [ ] 是否只有白名单槽,无自造 ref?
|
||||
- [ ] perspective / chance / 只要正文等非常规选择是否有理由?
|
||||
- [ ] auditor / perspective / chance / 只要正文等非常规选择是否有理由?
|
||||
- [ ] 是否误把变量管理做成槽?是否误把 chance 当成每轮 LLM?
|
||||
- [ ] 是否误交完整 设计.worker集?
|
||||
```
|
||||
@@ -137,7 +142,7 @@ boundary: |
|
||||
|
||||
```examples
|
||||
好:
|
||||
- play_slots: gm+narrator,perspective/chance false;mount_notes 一行。
|
||||
- play_slots: auditor+gm+narrator,perspective/chance false;mount_notes 一行。
|
||||
- 有凶手真名不能进 GM:perspective true,并说明只出反应建议。
|
||||
- 需要检定/抽签:chance true(按需程序工具)。
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# 舞台骨架
|
||||
# 舞台骨架
|
||||
|
||||
> 技能文档。程序只切割下方 **fence 块**;`##` 标题仅供人读。
|
||||
> 产物外壳:`docs/context-fragment-design.md`;范例:`aesthetics-interaction` / `mechanism`。
|
||||
@@ -212,17 +212,17 @@ summary:`舞台骨架 · …`(点题尺度 / 社会结构或世界状况)
|
||||
"维度": [
|
||||
{
|
||||
"名": "覆盖度",
|
||||
"分数": 0,
|
||||
"分数": 8,
|
||||
"说明": "能否覆盖用户已表达的舞台/社会/状况/变造硬要求"
|
||||
},
|
||||
{
|
||||
"名": "充分度",
|
||||
"分数": 0,
|
||||
"分数": 8,
|
||||
"说明": "骨架是否够下游引用,并撑起用户想要的体验发生与运转"
|
||||
},
|
||||
{
|
||||
"名": "克制度",
|
||||
"分数": 0,
|
||||
"分数": 8,
|
||||
"说明": "是否只写会上台部分;无地图百科、物种志、民俗全书膨胀"
|
||||
}
|
||||
],
|
||||
@@ -244,7 +244,7 @@ summary:`舞台骨架 · …`(点题尺度 / 社会结构或世界状况)
|
||||
|
||||
硬规则(公共三段 + 本技正文):
|
||||
1. 合法 JSON;必含 `schema` / `技能` / `brief` / `正文` / `自评` / `追问`(题目可 `[]`)。
|
||||
2. **公共三段**:正文=舞台主体;自评=覆盖度/充分度/克制度;追问=导语+建议选项。
|
||||
2. **公共三段**:正文=舞台主体;自评=覆盖度/充分度/克制度(**分数 0–10 十分制**);追问=导语+建议选项。
|
||||
3. **本技正文**须含:依据体验、舞台尺度、基底与变造、**社会结构**、**世界状况**、关键舞台区、未展开范围、覆盖检验。
|
||||
4. 社会结构=谁在台上互动(种类/势力/阶层,格局级);世界状况=世界如何运转(灾变/通道/习俗等硬部件 + 日常软条件)。无则空数组,但须在覆盖检验说明「为何无需」。
|
||||
5. `mount` 默认主世界层;**禁止**写 `order`。`稳变` 多为 `stable`。
|
||||
|
||||
@@ -76,7 +76,12 @@ design
|
||||
→ 用户手动进 play
|
||||
|
||||
play
|
||||
用户输入 → 声明内 worker → 终稿
|
||||
用户输入
|
||||
→ auditor(旁观维护,默认空操作;无长对话史)
|
||||
→ perspective?(仅强信息隔离)
|
||||
→ gm / world-simulator(主世界层,settlement.v1;variable_changes 由 Runtime 合并)
|
||||
→ narrator(用户可见正文;通常在此 review)
|
||||
chance 仅按需;禁止发明未声明 ref
|
||||
```
|
||||
|
||||
## 启动(agent-first)
|
||||
@@ -104,6 +109,8 @@ play
|
||||
|--------|------------------|----------------|
|
||||
| design-* | true | user_confirmed |
|
||||
| opening-generator | true | user_confirmed |
|
||||
| auditor / world-simulator / role-decide | false(play) | continue(声明默认) |
|
||||
| narrator | false(play 调度) | review(等人看终稿) |
|
||||
|
||||
## 总管优先行为
|
||||
|
||||
@@ -113,6 +120,7 @@ play
|
||||
4. `waiting_user(review_artifact)` → 引导验收
|
||||
5. reject → 收修订 → 重跑同一 worker(含修订流程 = 再调味)
|
||||
6. 终稿(含 `设计.worker集`)已 accept 且需开局 → `opening-generator`
|
||||
7. **play**:按 `play_slots` 序 `auditor → perspective? → gm → narrator`;`pendingSideEffectWorkers` 优先
|
||||
|
||||
## 禁用行为
|
||||
|
||||
@@ -121,3 +129,4 @@ play
|
||||
- 一次 design-flow 排死全程固定长链(应增量)
|
||||
- 调度声明未列出的 play ref
|
||||
- Agent 挑选模型
|
||||
- play 中把旁观维护当正文作者,或跳过 gm 直接 narrator
|
||||
|
||||
@@ -16,7 +16,7 @@ process:
|
||||
- 体验契约稍清后即可确认游玩槽(「游玩拓扑」或按默认:主世界层 + 叙事转述,角色视角默认关);此后写上下文时只标挂谁,不标最终顺序。
|
||||
- 对照技能池「何时用 / 何时不用」,只排近期缺的 1~4 步;不预设固定长链。
|
||||
- 中段软顺序:机制/舞台支点 → 要不要真值 → 有门控再排变量与分档/事件 → 叙事/正文组成/监控栏按需;每块产物带 mount(挂哪些槽)与可选「稳/变」信号,勿写死 order。
|
||||
- 用户看见的版式用「正文组成」(非程序报文);会变信息用「设计监控栏」(忌姓名性别等死字段)。
|
||||
- 用户看见的呈现:用「正文组成」**选固定壳并微调**(见 docs/play-presentation-shells.md),不是从零设计 UI;会变信息用「设计监控栏」。
|
||||
- 需要可引用舞台时用「舞台骨架」;识别体验支点用实现机制;同类内容难稳定生成用生成规则,需预生成再排具体实例。
|
||||
- 需要门控/分档/程序事件:先「变量设计与更新规则」,再「变量控制上下文」。变量与 Progressive 不是执行单元。
|
||||
- 挂载归属(非顺序):世界/机制/生成 → 主世界层;叙事指南与故事推进 → 叙事转述(推进兼挂主世界层);美学进常驻。勿为世界观或性格表另开槽。拓扑图谱在固定槽已够用时通常跳过。
|
||||
|
||||
@@ -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`。
|
||||
|
||||
## 其它文件
|
||||
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
id: auditor
|
||||
label: 旁观维护
|
||||
role: auxiliary
|
||||
duty: >
|
||||
副 LLM:每轮检查表字段与生成规则是否需要补充;默认空操作。
|
||||
只出 maintain.v1(运行.本轮.旁观);不写用户正文、不写真相权威。
|
||||
上下文几乎不要对话历史——合同 + 表 + 本轮工单即可。
|
||||
when: 每轮用户输入后、主世界层裁决之前
|
||||
suggested_context:
|
||||
static:
|
||||
- 设计.worker集
|
||||
- 设计.变量设计与更新规则
|
||||
- 设计.生成规则
|
||||
- 设计.变量控制上下文
|
||||
dynamic:
|
||||
- 用户.最新输入
|
||||
- 变量.当前
|
||||
- 上下文.旁观.状态摘要
|
||||
- 运行.本轮.工单
|
||||
suggested_outputs:
|
||||
- 运行.本轮.旁观
|
||||
prompt_excerpt: |
|
||||
你是旁观维护(副 LLM),不是主世界层,也不是叙事作者。
|
||||
读合同(生成规则/变量规则/旁观摘要)与当前表,对照「用户.最新输入」判断本轮要不要补表或触发规则。
|
||||
「运行.本轮.旁观」必须是 JSON,schema 为 maintain.v1:
|
||||
{
|
||||
"schema": "maintain.v1",
|
||||
"need_generate": false,
|
||||
"table_ops": [],
|
||||
"notes": []
|
||||
}
|
||||
多数轮保持空:need_generate=false、table_ops=[]、notes=[]。
|
||||
table_ops 仅在确需改「变量.当前」等表字段时使用(op=set|delta)。
|
||||
need_generate 仅在确需按某条 rule_id 补实例时写成 { "rule_id", "reason" }。
|
||||
禁止编造长对话续写;禁止输出用户可见正文;禁止修改未点名的无关字段。
|
||||
@@ -2,23 +2,42 @@ id: narrator
|
||||
label: 叙事转述
|
||||
role: transcription
|
||||
duty: >
|
||||
只读「运行.本轮.裁决」结构化包 + 文风/叙事常驻,组装「输出.用户展示」。
|
||||
只改表达,不改事实、不补裁决、不读完整真值表(除非包内已写明可见后果)。
|
||||
只读「运行.本轮.裁决」结构化包 + 文风/叙事常驻 + 设计.正文组成(壳适配),
|
||||
组装「输出.用户展示」。只改表达,不改事实、不补裁决。
|
||||
when: 主世界层裁决包已写入
|
||||
suggested_context:
|
||||
static:
|
||||
- 设计.worker集
|
||||
- 设计.叙事指南与故事推进
|
||||
- 设计.叙事指南
|
||||
- 设计.正文组成
|
||||
- 设计.监控栏
|
||||
dynamic:
|
||||
- 运行.本轮.裁决
|
||||
- 用户.最新输入
|
||||
- 变量.当前
|
||||
suggested_outputs:
|
||||
- 输出.用户展示
|
||||
presentation_hints:
|
||||
mode: prose | markdown | mixed
|
||||
tone: 依 Worker 集 presentation / 叙事指南
|
||||
mode: present.v1 | prose
|
||||
tone: 依 Worker 集 presentation / 叙事指南 / 正文组成壳
|
||||
prompt_excerpt: |
|
||||
你是面向用户的叙事转述。输入是结构化裁决包(settlement.v1),不是自由发挥的世界真相。
|
||||
按文风写可读正文;禁止写出 do_not_say;禁止编造包中没有的事实或变量。
|
||||
若裁决包含 tone_hint / npc_moves,须体现在叙述中。
|
||||
先读「设计.正文组成」的呈现壳 shell_id(prose|chat_monitor|turn_panel|chapter_reader):
|
||||
- prose:可直接输出 Markdown 正文(程序会落入 body)。
|
||||
- 其它壳:优先输出 JSON,schema 为 present.v1:
|
||||
{
|
||||
"schema": "present.v1",
|
||||
"shell": "<与正文组成一致>",
|
||||
"blocks": {
|
||||
"monitor": { "字段名": "短值" },
|
||||
"header": "可选",
|
||||
"body": "用户可读正文",
|
||||
"footer": "可选",
|
||||
"aside": "可选"
|
||||
},
|
||||
"meta": { "suggested_actions": ["…"] }
|
||||
}
|
||||
只填该壳已有区域;监控字段来自设计.监控栏 / 投影,禁止编造真值数字若 do_not_say 禁止。
|
||||
禁止写出 do_not_say;禁止编造包中没有的事实。
|
||||
若裁决含 tone_hint / npc_moves / suggested_actions,须体现在 body 或 meta。
|
||||
|
||||
@@ -14,6 +14,7 @@ suggested_context:
|
||||
- 用户.最新输入
|
||||
- 运行.事件流
|
||||
- 变量.当前
|
||||
- 运行.本轮.旁观
|
||||
- 上下文.角色态度
|
||||
- 大纲.当前章
|
||||
suggested_outputs:
|
||||
@@ -21,9 +22,10 @@ suggested_outputs:
|
||||
- 运行.事件流
|
||||
- 运行.本轮.变量变更
|
||||
prompt_excerpt: |
|
||||
你是中立主世界层(GM),不是文学作者。读真值与已投影上下文,对用户行动作可复核裁决。
|
||||
「运行.本轮.裁决」必须是 JSON,schema 为 settlement.v1,至少尽量包含:
|
||||
你是中立主世界层(GM),不是文学作者。读真值、已投影上下文,以及本轮旁观维护包(若有)。
|
||||
对用户行动作可复核裁决。「运行.本轮.裁决」必须是 JSON,schema 为 settlement.v1,至少尽量包含:
|
||||
player_action、resolved[]、visible_now、npc_moves[]、variable_changes[]、do_not_say[]、tone_hint。
|
||||
variable_changes 由 Runtime 合并进变量.当前——请写清 key 与 to/delta。
|
||||
叙事质量 intentionally 低——用户正文交给 narrator。
|
||||
需要真随机(骰子/抽签/比点)时:不要自己编结果;依赖已启用的机遇裁定(chance)
|
||||
写出的「运行.本轮.机遇」,或由编排器先 run_worker chance。
|
||||
|
||||
@@ -57,6 +57,14 @@ ${workerLines}
|
||||
- **禁止**替用户猜测或改选配方
|
||||
- **禁止**一次编排排死全程固定 DAG
|
||||
|
||||
## play 优先顺序(lifecycleStage=play)
|
||||
- 严格按声明管线顺序调度(play_slots 展开序):**auditor(旁观维护)→ perspective? → gm(主世界层)→ narrator(转述)**
|
||||
- auditor / gm / perspective 的 acceptance 多为 continue:跑完立刻调度下一槽,不要中途 ask_user
|
||||
- 仅 narrator(或规格标明 review 的终稿槽)停下来等人看
|
||||
- chance 仅在需要真随机时按需 run_worker,不插入每轮固定序
|
||||
- slots.pendingSideEffectWorkers 非空时优先调度其中 workerId
|
||||
- **禁止**发明未声明的 play ref;**禁止**把旁观维护当成正文作者
|
||||
|
||||
输出必须是 JSON 对象,字段:
|
||||
{
|
||||
"action": "ask_user" | "run_worker" | "create_temp_worker" | "review_blackboard" | "finish",
|
||||
@@ -102,15 +110,19 @@ export class MainAgent {
|
||||
|
||||
export function buildMainAgentUserPrompt(context: MainAgentContext): string {
|
||||
const { session, blackboardIndex, availableWorkers } = context;
|
||||
const stage = String(session.slots?.uiLifecycleStage ?? "design");
|
||||
const pendingSide = session.slots?.pendingSideEffectWorkers;
|
||||
return JSON.stringify(
|
||||
{
|
||||
runtimePhase: session.phase,
|
||||
waitingReason: session.waitingReason,
|
||||
lifecycleStage: stage,
|
||||
flowId: session.flowId,
|
||||
currentStepId: session.currentStepId,
|
||||
currentWorkerId: session.currentWorkerId,
|
||||
acceptanceMode: session.acceptanceMode,
|
||||
slots: session.slots,
|
||||
pendingSideEffectWorkers: pendingSide ?? null,
|
||||
pendingDecision: session.pendingDecision
|
||||
? {
|
||||
id: session.pendingDecision.id,
|
||||
@@ -129,7 +141,9 @@ export function buildMainAgentUserPrompt(context: MainAgentContext): string {
|
||||
blackboardIndex,
|
||||
availableWorkers,
|
||||
instruction:
|
||||
"根据当前状态决定下一步。尚无设计.创作流程 → design-flow;有未完成步骤 → design-step;steps 做完但 status=open → 再 design-flow;禁止旧 design-core/fixed/worker/refine。已 accept 且需开局 → opening-generator。进 play 由用户手动。",
|
||||
stage === "play"
|
||||
? "play:按声明序 auditor→perspective?→gm→narrator;continue 槽连跑;仅终稿 review 停;pendingSideEffectWorkers 优先;禁止未声明 ref。"
|
||||
: "根据当前状态决定下一步。尚无设计.创作流程 → design-flow;有未完成步骤 → design-step;steps 做完但 status=open → 再 design-flow;禁止旧 design-core/fixed/worker/refine。已 accept 且需开局 → opening-generator。进 play 由用户手动。",
|
||||
},
|
||||
null,
|
||||
2,
|
||||
|
||||
@@ -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}
|
||||
|
||||
|
||||
@@ -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 = "";
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ const WORKER_LABELS: Record<string, string> = {
|
||||
narrator: "叙事转述",
|
||||
"role-decide": "角色决策",
|
||||
"world-simulator": "主世界层",
|
||||
auditor: "旁观维护",
|
||||
chance: "机遇裁定",
|
||||
"round-present": "回合呈现",
|
||||
outline: "大纲 / 细纲",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -40,6 +40,29 @@ function asStringList(v: unknown): string[] {
|
||||
return v.map((x) => asString(x)).filter((x): x is string => Boolean(x));
|
||||
}
|
||||
|
||||
/** 自评分数展示:0–10;兼容旧百分数 */
|
||||
function formatSelfScoreLabel(score: unknown): string {
|
||||
if (typeof score === "number" && Number.isFinite(score)) {
|
||||
if (score >= 0 && score <= 10) {
|
||||
const shown = Number.isInteger(score) ? String(score) : String(Math.round(score * 10) / 10);
|
||||
return `${shown}/10`;
|
||||
}
|
||||
if (score > 10 && score <= 100) {
|
||||
const ten = Math.round((score / 10) * 10) / 10;
|
||||
const shown = Number.isInteger(ten) ? String(ten) : String(ten);
|
||||
return `${shown}/10`;
|
||||
}
|
||||
}
|
||||
if (typeof score === "string" && score.trim()) {
|
||||
const s = score.trim();
|
||||
if (/\/\s*10$/i.test(s)) return s.replace(/\s+/g, "");
|
||||
const n = Number(s.replace(/%$/, ""));
|
||||
if (Number.isFinite(n)) return formatSelfScoreLabel(n);
|
||||
return s;
|
||||
}
|
||||
return "?";
|
||||
}
|
||||
|
||||
function normalizeStability(v: unknown): ContextFragmentStability | undefined {
|
||||
if (v === "stable" || v === "semi" || v === "volatile") return v;
|
||||
if (v === "稳" || v === "少变") return "stable";
|
||||
@@ -164,7 +187,7 @@ export function contextFragmentToView(raw: unknown): ContextFragmentView {
|
||||
const lines = dims.map((d) => {
|
||||
if (!d || typeof d !== "object") return String(d);
|
||||
const r = d as Record<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}`,
|
||||
);
|
||||
|
||||
@@ -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",
|
||||
);
|
||||
}
|
||||
|
||||
220
src/skills/maintain-packet.ts
Normal file
220
src/skills/maintain-packet.ts
Normal 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);
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
345
src/skills/present-packet.ts
Normal file
345
src/skills/present-packet.ts
Normal 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");
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
91
tests/present-packet.test.ts
Normal file
91
tests/present-packet.test.ts
Normal 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");
|
||||
});
|
||||
});
|
||||
@@ -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("还需确认");
|
||||
});
|
||||
});
|
||||
|
||||
532
web/agent-ui.js
532
web/agent-ui.js
@@ -1,6 +1,12 @@
|
||||
import { renderIntakePanel } from "./intake-ui.js";
|
||||
import { getActiveQuestions, renderQuestionsCard } from "./questions-ui.js";
|
||||
import { displayWorkerLabel, formatWorkerDisplayTitle } from "./display-labels.js";
|
||||
import {
|
||||
PRESENT_SHELL_IDS,
|
||||
parsePresentDoc,
|
||||
presentFromPlain,
|
||||
renderPresentShellHtml,
|
||||
} from "./present-shells.js";
|
||||
|
||||
const HIDE_KINDS = new Set(["worker_stub"]);
|
||||
|
||||
@@ -341,6 +347,44 @@ function formatQuestionsHtml(body) {
|
||||
return `<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>` : "";
|
||||
}
|
||||
|
||||
/**
|
||||
* 完备度等:0–100 百分制(正文诊断块仍用 %)。
|
||||
* 自评维度请用 parseTenScore。
|
||||
*/
|
||||
function parseCompleteness(v) {
|
||||
if (v == null) return null;
|
||||
if (typeof v === "number" && Number.isFinite(v)) {
|
||||
@@ -1349,6 +1805,38 @@ function parseCompleteness(v) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 自评分数:0–10;兼容旧百分数(>10…100 → /10) */
|
||||
function parseTenScore(v) {
|
||||
if (v == null) return null;
|
||||
const fromNumber = (n) => {
|
||||
if (!Number.isFinite(n) || n < 0) return null;
|
||||
if (n <= 10) return Math.round(n * 10) / 10;
|
||||
if (n <= 100) return Math.round((n / 10) * 10) / 10;
|
||||
return null;
|
||||
};
|
||||
if (typeof v === "number") return fromNumber(v);
|
||||
const s = String(v).trim();
|
||||
if (!s) return null;
|
||||
const slash = s.match(/^(\d+(?:\.\d+)?)\s*\/\s*10$/i);
|
||||
if (slash) return fromNumber(Number(slash[1]));
|
||||
const withPct = s.match(/^(\d+(?:\.\d+)?)\s*%$/);
|
||||
if (withPct) return fromNumber(Number(withPct[1]));
|
||||
const bare = s.match(/^(\d+(?:\.\d+)?)$/);
|
||||
if (bare) return fromNumber(Number(bare[1]));
|
||||
return null;
|
||||
}
|
||||
|
||||
function formatTenScore(ten) {
|
||||
if (ten == null || !Number.isFinite(ten)) return "?";
|
||||
return Number.isInteger(ten) ? String(ten) : String(Math.round(ten * 10) / 10);
|
||||
}
|
||||
|
||||
function tenTone(ten) {
|
||||
if (ten < 4) return "low";
|
||||
if (ten < 7) return "mid";
|
||||
return "high";
|
||||
}
|
||||
|
||||
function pctTone(pct) {
|
||||
if (pct < 40) return "low";
|
||||
if (pct < 70) return "mid";
|
||||
@@ -1369,6 +1857,22 @@ function renderPctMeterHtml(pct, label = "完备度") {
|
||||
</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
189
web/present-shells.js
Normal 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>`;
|
||||
}
|
||||
243
web/styles.css
243
web/styles.css
@@ -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.v1(P2 最小) —— */
|
||||
.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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user