壳完善

This commit is contained in:
2026-08-14 14:25:31 +08:00
parent 120d67ee82
commit 20938b04d7
24 changed files with 1495 additions and 426 deletions

View File

@@ -0,0 +1,24 @@
# 创作产物 · 前端美化卡覆盖(快查)
> 随 `web/agent-ui.js` 的 `renderSpecialtyBodyHtml` / `renderKnownArtifactHtml` 维护。
| 产物/技能 | 美化程度 | 渲染入口 |
|-----------|----------|----------|
| 美学纲领与交互范式 | **专用 mosaic**(设定逻辑 / 交互范式 / 美学纲领) | `renderSpecialtyBodyHtml` → aesthetics |
| 实现机制 | **专用卡**(支撑点) | `renderMechanismBodyHtml` |
| 舞台骨架 | **专用卡** | `renderWorldBlueprintBodyHtml` |
| 生成规则 | **专用卡** | `renderGenerationRulesBodyHtml` |
| 具体实例 | 通用 fragment / 结构化 | context-fragment 外壳 |
| 叙事指南与故事推进 | **专用 mosaic**(风格与写法 \| 推进与决策) | `renderNarrativeBodyHtml` |
| 变量设计 | **结构化卡**(真值 / side_effects | `renderVariableDesignHtml` |
| 变量控制上下文 | 通用 fragment | context-fragment |
| 正文组成 | 通用 fragment + 游玩期 **present 壳** | fragment游玩 `present-shells.js` |
| 设计监控栏 | 通用 fragment | context-fragment |
| 游玩拓扑 | **槽位芯片卡** | `renderPlaySlotsHtml` |
| 上下文投影排序 | **排序卡** | `renderContextOrderHtml` |
| 裁决包 settlement.v1 | **分节卡** | `renderSettlementSectionsHtml` |
| 开场白与开场变量 | 通用 fragment | context-fragment |
| 拓扑图谱 | (骨架未写) | — |
**说明:** 「通用 fragment」= 公共头brief/mount/自评)+ 正文结构化树,已比生 JSON 友好;「专用」= 按技能排版的 mosaic/卡片。
游玩美化面板另见 `docs/play-presentation-shells.md`(与创作验收卡不同层)。

View File

@@ -27,13 +27,13 @@
| `aesthetics-interaction` | `feeds: design_only`(再压缩进常驻);呈现要点可摘要给 narrator |
| `mechanism` | **已改** output→`context-fragment.v1``feeds: gm` |
| `world-blueprint` | **已改**→「舞台骨架」+ `context-fragment.v1``feeds: gm` |
| `generation-rules` / `concrete-instances` | **已改**`context-fragment.v1``feeds: gm`;宽原则+严外壳 |
| `narrative` | **已改**→「叙事指南与故事推进」;遣词/笔墨/禁忌+推进;`feeds: narrator` |
| `variable-design` | **已写**;核对 output 与 `tables.side_effects`、勿暗示「变量管理 worker」 |
| `variable-context` | **已**;核对挂载对象用「主世界层/叙事转述」而非乱拆 |
| `reply-format` / `status-bar` | **已改**→正文组成 + 设计监控栏;`context-fragment.v1`;非程序报文 |
| `topology` | 骨架;若固定槽后可能降级为可选或删 |
| `worker-templates/*.yaml` | **程序已改** gm/narrator`role-decide` 可再收紧「只出建议」 |
| `generation-rules` / `concrete-instances` | **已改** fragment生成规则 **mount/feeds 含 auditor合同级** |
| `narrative` | **已改**:一步两块(风格与写法|推进与决策)+ 双挂;专用 mosaic |
| `variable-design` | **已写** |
| `variable-context` | **已**mount 含 auditor + 旁观汇总 |
| `reply-format` / `status-bar` | 已有正文;见 artifact-card-coverage |
| `topology` | 骨架;等你定「小循环」选项后再写 |
| `worker-templates/*.yaml` | gm/narrator/auditor 已齐 |
## 优先级 P2文档口径

View File

@@ -46,11 +46,12 @@
舞台骨架正文:尺度 / 基底与变造 / **社会结构** / **世界状况** / 关键舞台区 / 未展开范围(见 `world-blueprint`)。
生成规则正文:必要性判断 + `rules[]`(生成与描写 + **单层**产物格式 + 可选多池绑顶层字段);自评=必要性/属性妥当/格式准确。
美学纲领自评仍为:交互范式 / 美学纲领 / 整体协调(勿与生成规则三维混用)。
叙事指南与故事推进正文:纲领 / 风格与遣词(+示范) / 笔墨焦点 / 禁忌与不偏好 / 情境备用 / 推进与决策 / 内容与表达;自评=写法可执行/推进可执行/与美学对齐;artifact=`设计.叙事指南与故事推进`
叙事指南与故事推进正文:一份全文(纲领/遣词/笔墨/禁忌/推进等);**整份**双挂转述与主世界层,不为省 token 拆两套投影。artifact=`设计.叙事指南与故事推进`
正文组成:**基于固定呈现壳适配微调**(选 `shell_id` + 微调轴块只挂壳已有区域artifact=`设计.正文组成`。见 `docs/play-presentation-shells.md`
设计监控栏:只监控会变字段(可含攻略目标/场景交互物artifact=`设计.监控栏`(旧称状态栏)。
变量设计:真值 + **维护语句** + **Data映射索引**(指向具体实例)+ side_effects。
变量控制上下文:各槽视野 + **旁观汇总**(当前档摘要)
变量控制上下文:各槽视野 + **旁观汇总**mount 含主世界层与旁观维护
生成规则:合同挂主世界层 + 旁观(旁观读 rule_id/格式/必要性,描写长文可 summary
开场白与开场变量:开场正文(守正文组成)+ 开场初值;可经 opening-generator 落库。

68
docs/play-dm-auditor.md Normal file
View File

@@ -0,0 +1,68 @@
# 主世界层 与 旁观维护:怎么配合
> 用白话说明「DM」和「副 LLM旁观」谁先谁后、谁干什么。
> 实现上当前管线是:`旁观 →(可选角色视角)→ 主世界层 → 转述`。
## 1. 先说清楚两个角色
| 称呼 | 槽 | 干什么 | 不干什么 |
|------|-----|--------|----------|
| **主世界层DM** | gm | 读设定与当前状态,对本轮玩家行动做**裁决**;可提议改变量 | 不负责长文文风;不靠「再想一遍」修表 |
| **旁观维护(副 LLM** | auditor | **查漏补缺**:表缺字段、该不该按生成规则补一条实例;多数轮**空操作** | 不当作者写正文;不替代裁决 |
转述只吃裁决包写用户可见文字——与「俩人怎么配合」无关。
## 2. 你提到的两种做法
**做法甲DM 先提要求 → 副 LLM 交作业 → DM 再过一遍**
```text
DM「按怪物规则补 1 个」→ 旁观生成 → DM 再裁决一次
```
- 优点DM 始终握笔。
- 缺点:同一回合容易 **3 次模型调用**,贵、慢;旁观一不小心变成「小作者」。
**做法乙DM 先交齐裁决 → 把结果和要求交给副 LLM → 副 LLM 用工具改表**
```text
DM 出裁决包(含变量变更)→ 旁观只动表/触发生成 → 程序合并,不再开一轮 DM
```
- 优点:叙事权威仍在 DM旁观像**库管/校对**,适合工具改表。
- 缺点:若裁决里已经写错事实,旁观「修表」救不了叙事,只能修状态。
## 3. 本仓库采用的(推荐)
**既不是甲也不是乙的「整段返工」,而是:旁观轻扫 → DM 拍板 →(程序合并旁观的改表)→ 转述。**
```text
每轮:
1. 旁观:看规则合同 + 当前表 + 玩家输入
→ 多数轮:什么都不改(空 maintain
→ 少数轮:写 table_ops / need_generate补字段或点名 rule_id
2. 主世界层:出 settlement 裁决包(可含变量变更)
3. 程序:合并旁观改表 + DM 变量变更(边沿副作用照常)
4. 转述:只根据裁决包写正文
```
要点:
1. **不要**默认「DM 写完再让旁观润色全文」(贵,且旁观会抢笔)。
2. **不要**默认「旁观先写一大段设定再交给 DM」旁观变作者
3. **生成规则给旁观**:只要**合同**(有哪些 rule、格式、何时该生成不要整本描写范文。
4. 真要「按规则生成实例」:旁观点 `need_generate.rule_id`,由**程序/既定生成通路**落表,而不是旁观写小说。
若以后要改成「乙」:把旁观挪到 **DM 之后**、只保留工具改表、禁止旁观输出正文——可以再开一刀改调度序;当前以已落地的「旁观在前、轻维护」为准。
## 4. 和「生成规则要不要给副 LLM」的关系
问题本质不是「塞不塞那一份文档」,而是:
- 旁观要不要能说「该按某条规则补东西」?→ **要**,就得看见规则目录/格式。
- 旁观要不要会写角色描写?→ **不要**,所以描写长文不必进旁观上下文。
## 5. 一句话
**旁观看门与补表DM 断事;转述说话。**
交互上优先「轻维护 + 一次裁决」,不优先「来回多轮 LLM 传话」。

View File

@@ -1,143 +1,122 @@
# 游玩呈现壳P1· 可微调前端模板
# 游玩呈现壳 · 可微调前端模板
> **阶段**P1 = 定类与契约;**P2** = 程序/样式适配与灌数。
> 上游:游玩管线 `auditor → gm → narrator`(已落地)
> 上游:游玩管线 `auditor → gm → narrator`。
> 创作挂钩:`设计.正文组成` / `设计.监控栏`;终稿 tag 通常 `输出.用户展示`。
> **创作定位:基于固定壳做适配微调,不是从零设计前端。**
> 预览:`/shells.html`
## 1. 创作怎么做(权威口径)
```text
固定壳(本仓库预置,少数几种)
固定壳(本仓库预置,种)
→ 创作期:选一个 shell_id
→ 只改允许的微调轴字段、显示名、开关、tone_chrome…
→ 禁止改分区骨架、禁止发明第种布局
→ 禁止改分区骨架、禁止发明第种布局
游玩期往壳的区域里灌数present.v1 / 投影 / settlement
```
「正文组成」= **壳适配单**,不是 UI 创意稿。
用户看见的差异应来自**同壳不同绑定**,而不是每局一套新布局。
## 2. 问题(若不用壳)
## 2. 信息层级与长文容量
创作期若只写散文式「想要好看界面」,模型难稳定产出可跑界面,也会和程序渲染脱节。
壳把布局钉死,创作只填内容契约。
所有壳共用同一优先级——**改壳只是改谁占主面积**
```text
创作:选壳 + 微调绑定(字段/文案/开关)
游玩narrator / 程序灌入对应区域
伪 0 层:壳自带的 HUD 区,不是新 LLM
```
| 优先级 | 区域 | 视觉待遇 |
|--------|------|----------|
| **P0** | `body` | 阅读主轴;**随字数撑高**(单轮按 600~3000 字设计),由页面/消息区滚动 |
| **P1** | 建议行动 | 要决策时醒目;`choice_dock`**sticky 贴底** |
| **P2** | `monitor` | 芯片密排,高度固定矮 |
| **P3** | `header` / `footer` / `aside` | 辅助;双栏 **aside sticky**,长文仍可对照 |
## 3. 四大常用壳(微调模板,非成品皮肤)
**不要**把正文塞进固定高度小框再内部硬滚(除非外层消息流另有约束)。
选壳时问:用户 80% 时间是在「读长场面」还是「做选择」还是「对线索」?
| id | 中文 | 主场景 | 默认分区 | 默认可调 |
|----|------|--------|----------|----------|
| `prose` | 纯散文流 | 只要沉浸正文、不要 HUD | 单流 `body` | 文末可选折叠备注 |
| `chat_monitor` | 对话 + 顶栏监控 | 网恋 / 日常扮演 / 轻 AIRP | `monitor` 顶栏 + `body` 主聊 | 监控字段;是否显示建议行动 |
| `turn_panel` | 回合面板 | 强机制、资源/检定、场景交互 | `monitor` + `body` + `footer`(建议行动/检定结果) | 栏位footer 开闭;机遇区 |
| `chapter_reader` | 章节阅读 | 长文 / 爽文 / 助手分段 | `header`(章题/卷)+ `body` + 可选 `aside`(进度) | 章标题样式;是否显示大纲进度 |
## 3. 七种常用壳
旧规格未写 `shell_id` → 运行时视为 `prose`(与「可不排正文组成」一致)。
| id | 中文 | 主面积给谁 | 主场景 |
|----|------|------------|--------|
| `prose` | 纯散文 | 正文 ≈ 全屏 | 只要沉浸、不要 HUD |
| `chat_monitor` | 对话 + 监控 | 正文(聊天气泡区) | 网恋 / 日常 / 轻 AIRP |
| `spotlight` | 场面主视 | 正文极大(追光) | 电影感一幕、少机制 |
| `turn_panel` | 回合面板 | 中栏场面 + 侧栏交互 | 资源/检定/跑团 |
| `split_board` | 双栏看板 | 左正文 · 右线索 | 调查、推理 |
| `choice_dock` | 选择坞 | 正文 + **底部大行动坞** | 选项驱动 / AVG 关口 |
| `chapter_reader` | 章节阅读 | 阅读柱 + 窄进度侧栏 | 长文 / 爽文 |
### 3.1 各壳固定槽位(程序认这些区域 id创作不得增删分区
旧规格未写 `shell_id` → 运行时视为 `prose`
| 区域 id | prose | chat_monitor | turn_panel | chapter_reader |
|---------|-------|--------------|------------|----------------|
| `monitor` | — | 顶栏 | 顶/侧栏 | — 或弱化进 header |
| `header` | — | 可选会话抬头 | 可选回合号 | **章题/卷信息** |
| `body` | **主文** | **主聊/场面** | **场面叙述** | **章正文** |
| `footer` | 可选 | 可选短吐槽 | **建议行动 / 机遇摘要** | 可选作者注 |
| `aside` | — | — | 可选场景交互物 | **进度/卷纲摘要** |
| `hidden` | 变量段 | 变量段 | 变量段 | 变量段 |
### 3.1 各壳固定槽位
隐藏段从不进用户主视线(默认折叠/调试可见)。
| 区域 id | prose | chat_monitor | spotlight | turn_panel | split_board | choice_dock | chapter_reader |
|---------|-------|--------------|-----------|------------|-------------|-------------|----------------|
| `monitor` | — | 顶栏芯片 | 顶栏芯片 | 顶栏 | — | 可选顶栏 | — |
| `header` | — | — | — | 回合号 | 场景抬头 | — | **章题** |
| `body` | **主** | **主聊** | **主舞台** | **场面** | **主读** | **局面** | **章正文** |
| `footer` | 可选弱 | 可选 | 可选 | 检定摘要 | 可选 | 可选 | 作者注 |
| `aside` | — | — | — | 交互物 | **线索栏** | — | **进度** |
| `hidden` | 变量段 | 变量段 | 变量段 | 变量段 | 变量段 | 变量段 | 变量段 |
### 3.2 微调轴(创作只许改这些;布局骨架只读)
### 3.2 微调轴
每壳允许覆盖、禁止改布局骨架:
| 轴 | 说明 |
|----|------|
| `monitor_fields` | 监控栏字段子集 |
| `block_labels` | 可见块显示名(如 body→「短信」 |
| `show_suggested_actions` | 是否渲染建议行动;`turn_panel` / `choice_dock` 默认开 |
| `tone_chrome` | `default` / `messenger` / `book` / `terminal` |
| `empty_states` | 空态文案 |
| 轴 | 说明 | 例 |
|----|------|-----|
| `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`(要新壳走仓库改模板,不走单局创作)。
**禁止**创作期输出完整 CSS**禁止**自造 `shell_id`(要新壳改仓库模板)。
## 4. 与创作产物的映射
```text
设计.正文组成
正文.呈现壳.shell_id → 必须是四大之一(选壳)
正文.呈现壳.微调 → 只填允许轴(适配)
正文.可见块[] → 挂到壳已有区域,不新开区域
正文.监控栏锚点 → chat_monitor / turn_panel 的字段意图
正文.隐藏段 → 区域 hidden
正文.呈现壳.shell_id → 七选一
正文.呈现壳.微调 → 只填允许轴
正文.可见块[] → 挂到壳已有区域
设计.监控栏
→ 灌入该壳 monitor 区的字段表(仍是微调,不是新布局)
运行.本轮.裁决 (settlement.v1)
→ body 事实来源suggested_actions → footer若壳开启
输出.用户展示
→ 优先 present.v1 灌进壳区域;纯 Markdown → body
设计.监控栏 → monitor 字段表
运行.本轮.裁决 → body 事实suggested_actions → 行动区
输出.用户展示 → present.v1 或纯 Markdown→ body
```
## 5. 游玩灌数契约P2 实现目标)
推荐 `输出.用户展示` 在分块壳下为:
## 5. 游玩灌数契约
```json
{
"schema": "present.v1",
"shell": "chat_monitor",
"shell": "spotlight",
"blocks": {
"monitor": { "好感": "暖络", "今日": "下雨" },
"body": "……用户可读正文……",
"footer": ""
"monitor": { "地点": "天桥下", "张力": "" },
"body": "……用户可读正文……"
},
"meta": {
"suggested_actions": ["回一句关心", "约周末"],
"hide": ["<<<VARS>>>…<<<END>>>"]
}
"meta": { "suggested_actions": ["先开口", "递伞"] }
}
```
- `shell` 可省略 → 用规格里正文组成的 `shell_id`
- 若整段非 JSON壳把全文写入 `body`,其它区用变量投影/监控栏程序填P2
- `monitor` 宜为 **短键值对象**(程序渲染成芯片),不要长文
- `prose`:只需 `body` 字符串或 Markdown。
## 6. 创作期只做适配(正文组成)
## 6. 创作期只做适配
1. **选壳**(四选一):无 HUD → `prose`;对话+状态 → `chat_monitor`;机制重 → `turn_panel`;写章 → `chapter_reader`
2. **微调**监控字段、显示名、建议行动开关、`tone_chrome`、空态文案
3. **不要**:重画分区、发明区域 id、写 CSS、谈「另一种全新界面」。
4. `版式隐喻` 仅供人读;**程序只认 `shell_id` + 微调轴**。
1. **选壳**按「主面积给谁」§2七选一
2. **微调**字段、显示名、行动开关、tone、空态。
3. **不要**:重画分区、发明区域 id、写 CSS
## 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 阅读面** |
| 1 | `web/` 壳渲染器 + 层级样式 | **已接** `present-shells.js` + `styles.css` |
| 1b | 壳预览页 | **已接** `/shells.html` |
| 2 | 正文组成微调进 SessionView | **已接** |
| 3 | present.v1 解析 | **已接** `present-packet.ts` |
| 4 | narrator 按壳输出 | **已接** |
| 5 | 正文组成产出壳适配单 | **已接** |
---
修订记录2026-08-11 · P1口径明确为创作基于壳适配微调
修订2026-08-13 · 七壳;明确信息层级与主面积

View File

@@ -5,7 +5,8 @@
> **标准范例**`modules/aesthetics-interaction/prompt.md`。
> **真值 / Data / Progressive变量与门控****`docs/progressive-data-design.md`**。
> **上下文片段 / 槽位 / 投影排序****`docs/context-fragment-design.md`**。
> **游玩呈现壳P1****`docs/play-presentation-shells.md`**。
> **游玩呈现壳****`docs/play-presentation-shells.md`**(预览页 `/shells.html`
> **主世界层↔旁观怎么配合****`docs/play-dm-auditor.md`**。
> **提示词待改清单****`docs/briefs/prompt-revision-inventory.md`**。
> **给外部 AI 的完整泛用规范****`docs/briefs/capability-authoring-brief.md`**(含 `context-fragment.v1`)。
@@ -128,14 +129,14 @@ recipes/world-simulator|expand-assistant/recipe.yaml
| 美学纲领与交互范式 | `aesthetics-interaction` | **范例已写**;前端 mosaic 视图 |
| 实现机制 | `mechanism` | **已写**`context-fragment.v1`(支撑点正文 + 自评 + 追问);`feeds: gm`**专用卡已接** |
| 舞台骨架 | `world-blueprint` | **已写**`context-fragment.v1`;社会结构 + 世界状况;**专用卡已接** |
| 生成规则 | `generation-rules` | **已写**`context-fragment.v1`;宽原则 + 严合同键;可反复;`params.target`**专用卡已接** |
| 生成规则 | `generation-rules` | **已写**`context-fragment.v1`;宽原则 + 严合同键;可反复;`params.target`挂 gm+auditor**专用卡已接** |
| 具体实例 | `concrete-instances` | **已写**`context-fragment.v1`;只按规则执行;可反复;`params.rule_id` |
| 叙事指南与故事推进 | `narrative` | **已写**遣词/笔墨焦点/禁忌与不偏好 + 推进;挂转述 |
| 拓扑图谱 | `topology` | 骨架,待细写 |
| 设计监控栏 | `status-bar` | **已写**:只盯会变信息;旧称状态栏 |
| 正文组成 | `reply-format` | **已写**:壳适配微调P2 present 渲染已接;见 `docs/play-presentation-shells.md` |
| 变量设计与更新规则 | `variable-design` | **已写**:真值+维护语句+Data映射索引+side_effects |
| 变量控制上下文 | `variable-context` | **已写**挂载+旁观汇总 |
| 叙事指南与故事推进 | `narrative` | **已写**一份全文双挂;**不**为省 token 拆投影;结构化卡 |
| 拓扑图谱 | `topology` | 骨架,待细写(拟改为选项小循环或降级) |
| 设计监控栏 | `status-bar` | **已写**:只盯会变信息;通用 fragment 卡 |
| 正文组成 | `reply-format` | **已写**壳适配P2 present 渲染已接 |
| 变量设计与更新规则 | `variable-design` | **已写**;专用/结构化卡 |
| 变量控制上下文 | `variable-context` | **已写**:旁观汇总mount 含 auditor通用 fragment 卡 |
| 开场白与开场变量 | `opening-setup` | **已写**:开场守版式+同真相初值 |
共用收成(池内保留,按需):

View File

@@ -79,8 +79,8 @@ modules:
- id: narrative
name: 叙事指南与故事推进
declaration: >-
钉转述写法与故事推进:纲领、遣词造句、笔墨焦点、禁忌/不偏好、示范与备用、
推进决策(可含变量钩子);美学写感觉,本步写怎么写、往哪推
一次钉清写法与推进(遣词/笔墨/禁忌/示范 + 故事如何往前);
整份双挂转述与主世界层,不为省 token 拆投影
artifact: 设计.叙事指南与故事推进
# —— 机制与数据 ——

View File

@@ -29,7 +29,8 @@ boundary: |
具体实例:按规则填最终 records本步不写最终实例池条目≠实例
舞台骨架 / 实现机制:可引用,不重写百科、不重做移除检验。
变量*:游玩期状态更新——本步只约束生成时的类型与合法初值范围。
feeds: gm
旁观维护可读「合同」——rule_id、产物格式、何时可 need_generate不依赖整份描写长文。
feeds: gm,auditor
```
## opening
@@ -105,7 +106,7 @@ summary建立 → `生成规则 · {对象} · {仅动态|仅预生成|预生
"schema": "context-fragment.v1",
"技能": "生成规则",
"brief": "一句话:对象族 + 建立规则|无需专门规则 + 生命周期(若建立)+ 有池|无池",
"mount": ["world-simulator"],
"mount": ["world-simulator", "auditor"],
"稳变": "stable",
"正文": {
"本步参数": {
@@ -235,7 +236,7 @@ summary建立 → `生成规则 · {对象} · {仅动态|仅预生成|预生
5. **产物格式强制单层**`schema``type` 不得为 `object``array``items` 不得为 object。禁止 `外貌.发色` 这类嵌套路径。
6. 结论为「无需专门规则」时:`rules` 必须 `[]`。上下文策略布尔须与生命周期一致。
7. 池条目≠最终实例;禁止输出具体实例 `records`、变量更新规则、执行单元列表。
8. `mount` 默认主世界层;**禁止**写 `order`。summary 见 task。
8. `mount` 默认主世界层 + 旁观维护旁观只需合同键rule_id/格式/必要性;描写长文可在投影时 summary**禁止**写 `order`。summary 见 task。
## checklist

View File

@@ -1,9 +1,9 @@
# 叙事指南与故事推进
> 技能文档。程序只切割下方 **fence 块**`##` 标题仅供人读。
> 产物外壳:`docs/context-fragment-design.md`;对照`aesthetics-interaction`(感觉)↔ 本技(怎么写 / 往哪推)
> 默认服务**叙事转述**;「推进与决策」同时给主世界层用
> 旧称:叙事指南(流程 name / artifact 兼容见 creation-flow
> 产物外壳:`docs/context-fragment-design.md`;对照美学:感觉 → 本步「怎么写、故事怎么往前」
> **一份产物、一次询问**;转述与主世界层都挂同一份(不必为省 token 拆成两套投影)
> 旧称:叙事指南。
## meta
@@ -12,41 +12,27 @@ name: 叙事指南与故事推进
id: narrative
artifact: 设计.叙事指南与故事推进
declaration: >
钉转述「怎么写」与主世界「怎么推」:叙事纲领、遣词造句与笔墨焦点、绝对禁忌/不偏好、
示范与情境备用,以及推进/决策(可含变量门控);不重做美学感觉问卷
一次钉清写法与推进:遣词、笔墨、禁忌、示范,以及故事如何往前(可含变量钩子);
不重做美学感觉问卷;整份双挂转述与主世界层
when: |
已有(或等价)美学纲领,需要把「要的感觉落成可执行的写法与推进时
或转述缺遣词/示范/笔墨重心(如男频写女、女频写男、末日绝望 vs 日常小确幸);
或缺少情境切换语料、或主世界层缺推进框架(可含好感/日期/冷却钩子)时。
已有美学纲领,需要把感觉落成可执行的写法与推进时
when_not: |
美学未钉、还在谈「要什么感觉」→ 先「美学纲领与交互范式」。
契约已细到可直接转述、用户明确不要本步 → 可不排
用户可见块序/监控栏版式 → 「正文组成」/「设计监控栏」。
事件池 schema / 预生成 → 「生成规则」/「具体实例」。
真值与 side_effects → 「变量设计与更新规则」。
美学未钉 → 先「美学纲领与交互范式」。
版式/监控栏 → 「正文组成」/「设计监控栏」
事件池 schema / side_effects → 「生成规则」/「变量设计」。
boundary: |
本技能:叙事宪法 + 遣词造句/笔墨焦点 + 禁忌与不偏好 + 示范/备用 + 推进与决策 + 内容表达;
产出 context-fragment.v1。美学写「感觉」本步写「花笔墨在哪、怎么遣词、故事怎么推」
美学纲领与交互范式:体验与轮转——已写清的感觉勿重问。
生成规则 / 变量*:合同与副作用规格——本步只给推进钩子与写法,不建表。
设计回复格式:块结构——本步不定。
feeds: narrator
本技能:一份 context-fragment写清怎么写 + 怎么推;不必拆技能、也不必为省 token 做精细双投影。
美学:感觉与轮转。生成规则/变量*:合同与表
feeds: narrator,gm
```
## opening
```opening
美学纲领里的感觉已有方向时,请谈叙事「怎么写、往哪推」(想到什么写什么):
美学里的感觉已有方向时,请谈「正文怎么写、故事怎么往前」(想到什么写什么):
1. 正文应该像什么?遣词大概什么味道?
例:第三人称网文、偏爽文、简明带张力;句短、少文艺腔……
2. 笔墨该花在谁/什么上?有什么绝对不能写、或不想多写?
例:男频爱情 → 多写女角色美丽与反应;女频相反;
末日 → 生存绝望,禁止轻松解决一切;日常 → 小确幸,禁止突然飞来横祸……
3. 故事怎么往前推?要不要跟变量挂钩?有没有情境要换一套语料?
好感够了才插入倒追NSFW 改官能笔法;丛林三两句带过 vs 荒野逐步细写……
1. 像什么文风?遣词什么味道?笔墨花在谁身上?有什么绝对不能写?
2. 故事怎么推进?要不要跟好感/进度挂钩?有没有情境要换一套写法?
```
## task
@@ -54,60 +40,32 @@ feeds: narrator
```task
你正在执行「叙事指南与故事推进」。产物必须是 **context-fragment.v1** JSON写入「设计.叙事指南与故事推进」。
美学钉感觉;本步钉:**怎么遣词造句、笔墨花在哪里、绝对禁忌与不偏好、示范长什么样、故事如何推进**
一次对话钉清写法与推进(可交叉追问),输出**一份**正文即可
mount`narrator` 与 `world-simulator` 都挂同一份全文——不要为省一点点 token 拆两套裁剪方案。
执行顺序:
1. 读美学纲领(及可选机制/舞台):继承体验内核与禁忌改写成创作导向句,不重复问卷
2. 写「叙事纲领」:核心定位 + 叙事使命 + 叙事追求(浓缩)
3. 写「风格与遣词」:叙述者人格、整体基调、**遣词造句**(用什么词/句式/节奏、避免什么)、修辞;附至少 1 段示范
4. 写「笔墨焦点」:观众/代入取向下应详写谁、略写谁(例:男用户向爱情线详女角色;女用户向详男角色);与体验类型对齐的场景重心
5. 写「禁忌与不偏好」:绝对禁忌(硬禁)与不偏好(能避则避、出现须极克制)——含题材气质禁令(末日禁轻松通关、日常禁无端灾难等)。
6. 需要时写「情境备用」多套语料;写「推进与决策」(驱动力、速度/详略、成败、困境、节奏、可选变量钩子)。
7. 写「内容与表达」:视角执行、场景详略、信息组织。
8. 自评 + 追问;输出 JSON。
1. 继承美学体验与禁忌改写成做法向句子
2. 写纲领、遣词(含示范)、笔墨焦点、禁忌/不偏好;需要时写情境备用
3. 写推进与决策(可写「无专门推进」);变量只写钩子
4. 自评 + 追问
mount默认挂叙事转述 + 主世界层。禁止写最终 `order`。
summary`叙事指南与故事推进 · …`(点题体裁/笔墨重心 + 推进有无)。
summary`叙事指南与故事推进 · …`
```
## principles
```principles
1. 感觉 vs 做法
美学:装逼的爽感。本步:第三人称爽文、遣词短促有力、笔墨落在哪、示范段落、推进是否心想事成……
禁止复读体验口号
2. 遣词造句必须可执行
不只写「要有张力」:写句长、用词场、可否网感/文言、感官词偏好、情绪直给 vs show。
每条倾向尽量带「做法 → 目的」。
3. 笔墨焦点跟体验与观众站位走
爱情线:男用户向通常详写女角色外貌/气质/反应;女用户向详写男角色——写清本局取向,勿默认「男女均衡」。
末日天灾:详写匮乏、威胁、代价与绝望感;禁止轻松解决一切、轻松段子冲淡生存压。
日常小确幸:详写细微暖意与节奏;禁止无铺垫的突发致命危险或崩坏(除非用户明确要)。
其它类型同理:问「读者眼神会停在哪」。
4. 绝对禁忌 ≠ 不偏好
绝对禁忌:出现即破坏体验,硬禁。
不偏好:能避则避;偶尔出现须克制、不抢主戏。
两者都要写,并说明与美学的关系。
5. 示范与情境备用
至少一段默认示范NSFW/战斗/日常等用「情境备用」换遣词与禁忌增量。
6. 推进与写法同框,推进可轻可无
变量门控只写钩子;表规格交给变量/生成规则。
7. 继承优先;与美学硬禁忌冲突时以体验禁忌为准并点明。
1. 感觉 vs 做法:美学写要什么爽;本步写怎么遣词、笔墨落哪、故事怎么推。
2. 遣词与禁忌要可执行;绝对禁忌 ≠ 不偏好。
3. 推进可轻可无;表规格交给变量/生成规则
4. 整份双挂即可;禁止拆成两个技能。
```
## probe
```probe
每轮 12 点。已在美学写清的感觉禁止重问。
优先:体裁与遣词味道;笔墨焦点(详谁/略谁);绝对禁忌 vs 不偏好;末日/日常等气质禁令;推进门控与详略成败;是否要情境备用。
不追问:回复块排版、事件池 schema、side_effects 细节、舞台百科。
每轮 12 点。已在美学写清的感觉重问。
优先:体裁遣词、笔墨详谁、禁忌、推进门控。
```
## output
@@ -116,164 +74,70 @@ summary`叙事指南与故事推进 · …`(点题体裁/笔墨重心 + 推
{
"schema": "context-fragment.v1",
"技能": "叙事指南与故事推进",
"brief": "一句话:体裁遣词 + 笔墨重心 + 推进有无",
"brief": "一句话:体裁 + 笔墨重心 + 推进有无",
"mount": ["narrator", "world-simulator"],
"稳变": "stable",
"正文": {
"依据的体验": [
"从美学纲领忠实转化;可点名禁忌继承"
],
"依据的体验": ["…"],
"叙事纲领": {
"核心定位": "一句话:叙事本质/灵魂",
"叙事使命": "12 句:要让用户体验到什么",
"叙事追求": [
"关键词 - 简短解释"
]
"核心定位": "",
"叙事使命": "",
"叙事追求": ["…"]
},
"风格与遣词": {
"叙述者人格": "形象化一句话:这个声音是谁",
"整体基调": "温度/节奏/质感/距离的整体描述",
"遣词造句": [
"原则 - 具体做法(用词/句式/段落节奏) → 目的"
],
"核心修辞": [
"修辞名 - 示例 → 作用"
],
"示范": [
{
"情境": "常态/开场/高潮…",
"段落": "一段可朗读的示范(体现遣词与张力)"
}
]
"叙述者人格": "",
"整体基调": "",
"遣词造句": ["原则 - 做法 → 目的"],
"示范": [{ "情境": "常态", "段落": "…" }]
},
"笔墨焦点": {
"观众与代入取向": "男用户向|女用户向|中性/双关|未定;可附一句站位",
"详写优先": [
"应花笔墨的对象或维度(例:女角色外貌与反应;生存压迫与资源匮乏;微表情与小确幸)"
],
"可略写": [
"可三两句带过或背景化的部分"
],
"说明": "与体验类型如何对齐(男频/女频/末日/日常…)"
"观众与代入取向": "",
"详写优先": ["…"],
"可略写": ["…"]
},
"禁忌与不偏好": {
"绝对禁忌": [
"禁止… → 原因(硬禁;破坏体验)"
],
"不偏好": [
"尽量避免… → 若出现则如何克制"
]
"绝对禁忌": ["…"],
"不偏好": ["…"]
},
"情境备用": [
{
"触发": "例NSFW / 战斗 / 日常过场",
"改用风格": "例:官能小说笔法",
"遣词与禁忌增量": ["相对默认改什么"],
"笔墨增量": "该情境笔墨是否偏移;无则空字符串",
"示范": "可选示范段"
}
],
"情境备用": [],
"推进与决策": {
"有无专门推进": "有|无(沿用常识与体验即可)",
"驱动力来源": "故事主要由什么推动",
"叙事速度与详略": "何种过程可带过,何种必须细写",
"成败与难度": "心想事成 / 允许受挫 / 其它;与体验关系",
"变量门控钩子": [
{
"变量或信号": "好感|日期|冷却|进度|seed|其它",
"条件意图": "例:好感≥某档;冷却归零;纯随机",
"插入或推进什么": "例:倒追线;诡异遭遇",
"实现指向": "投影/动态表|生成规则事件池|主世界层临场判断",
"说明": "可较长;本步不写 side_effects"
}
],
"困境应对": [
"困境简述 → 应对方向(关键词)"
],
"节奏意识": [
"情境 → 应对"
]
"有无专门推进": "有|无",
"驱动力来源": "",
"叙事速度与详略": "",
"成败与难度": "",
"变量门控钩子": []
},
"内容与表达": {
"视角执行": {
"主次原则": "主信息源占比/辅助源条件",
"切换时机": ["情境 → 应对"],
"切换方式": "如何平滑切换"
},
"场景详略映射": [
"场景类型: 聚焦… | 详写条件 → 原因(与笔墨焦点对齐)"
],
"信息组织模式": [
"模式名: 结构 → 适用情境"
]
"视角执行": "…",
"场景详略映射": ["…"]
}
},
"自评": {
"维度": [
{
"名": "写法可执行",
"分数": 8,
"说明": "遣词+笔墨焦点+禁忌/不偏好+示范是否够转述落地"
},
{
"名": "推进可执行",
"分数": 8,
"说明": "主世界层是否知道如何推进;变量钩子清楚或明确无需"
},
{
"名": "与美学对齐",
"分数": 8,
"说明": "是否把感觉落成做法;笔墨与禁忌服务体验而非跑偏"
}
{ "名": "写法可执行", "分数": 8, "说明": "…" },
{ "名": "推进可执行", "分数": 8, "说明": "…" },
{ "名": "与美学对齐", "分数": 8, "说明": "…" }
],
"薄弱点": "一句话"
},
"追问": {
"导语": "为钉准写法与推进,还需确认:",
"题目": [
{
"问": "…",
"建议选项": ["选项A", "选项B", "其它(请写明)"],
"示例": "可选短示范句"
}
]
"薄弱点": ""
},
"追问": { "导语": "", "题目": [] },
"开放问题": []
}
```
硬规则:
1. 合法 JSON`schema` / `技能` / `brief` / `正文` / `自评` / `追问`
2. 正文键固定:依据的体验、叙事纲领、**风格与遣词**、**笔墨焦点**、**禁忌与不偏好**、情境备用、推进与决策、内容与表达。
3. `风格与遣词.示范` 至少 1 条;`遣词造句` 不得空数组(建立本步时)。`情境备用``[]`
4. `绝对禁忌``不偏好` 均须出现(可短,但不可整段省略);须能对上体验类型(如末日/日常/频向)。
5. 自评:写法可执行 / 推进可执行 / 与美学对齐。禁止 `order`
6. 不要输出事件池 schema、side_effects、回复块结构、执行单元列表。
7. summary`叙事指南与故事推进 · {brief 缩略}`
8. 兼容:若下游仍读旧 tag「设计.叙事指南」,细化终稿可将本产物同步或改挂新 tag本步只写新 artifact
硬规则:合法 JSON示范至少 1 段;禁忌与不偏好都要有;双挂;禁止 ordersummary 见 task。
## checklist
```checklist
- [ ] 含 schema / brief / 正文 / 自评 / 追问
- [ ] 自评为写法可执行 / 推进可执行 / 与美学对齐
- [ ] 遣词造句具体?有示范?笔墨焦点写清详谁略谁
- [ ] 绝对禁忌与不偏好都有,且贴合体裁(男频/女频/末日/日常等)?
- [ ] 推进钩子清楚或明确无需?未越权写生成规则/变量规格?
- [ ] 一份产物同时服务转述与主世界层
- [ ] 遣词+示范+禁忌够用?推进清楚或明确无
- [ ] 未为省 token 拆两套投影
```
## examples
```examples
好:
- 男频爱情爽文:遣词网文短句;笔墨详女角色美丽与心动反应;略写男主外貌流水账;禁忌:禁止把女主写成无魅力背景板
- 女频:笔墨详男角色气质/行动力;不偏好过度描写女主外貌抢戏。
- 末日:详写寒冷/饥饿/信任崩裂;绝对禁忌「轻松解决危机」;推进允许失败与代价。
- 日常小确幸:详写琐碎暖意;绝对禁忌无铺垫的突发灾难;推进忌强行高潮。
- NSFW 备用:触发后改官能遣词;笔墨可临时偏移身体感官。
坏:
- 只写「要有沉浸感」无遣词、无笔墨焦点。
- 默认「男女都要均衡描写」却无视本局男/女用户向。
- 末日指南里鼓励轻松打闹通关。
好:一次问清爽文遣词 + 好感门控倒追,整份挂两边。
坏:拆成「文风技能」和「推进技能」两步;或为省 token 给两边裁成两套残缺稿
```

View File

@@ -12,7 +12,7 @@ name: 正文组成
id: reply-format
artifact: 设计.正文组成
declaration: >
基于四大固定呈现壳做适配微调(选 shell_id + 填字段/显示名/开关等);
基于七种固定呈现壳做适配微调(选 shell_id + 填字段/显示名/开关等);
产出壳适配单;禁止从零发明布局或完整 UI
when: |
需要为本局选定呈现壳,或微调监控字段/块显示名/建议行动开关时;
@@ -34,10 +34,13 @@ feeds: narrator
```opening
本局在固定呈现壳上做适配(不是设计新界面)。
0. 选一壳:
· prose — 纯散文
· chat_monitor — 对话 + 顶栏监控
· turn_panel — 回合面板
0. 选一壳(主面积给谁)
· prose — 纯散文(正文≈全屏)
· chat_monitor — 对话 + 顶栏芯片
· spotlight — 场面主视(正文最大)
· turn_panel — 回合面板(场面+侧栏交互)
· split_board — 双栏(左故事·右线索)
· choice_dock — 选择坞(底部大行动区)
· chapter_reader — 章节阅读
1. 要微调什么监控字段、块称呼、要不要建议行动、tone_chrome…
@@ -50,13 +53,13 @@ feeds: narrator
```task
你正在执行「正文组成」。产物必须是 **context-fragment.v1** JSON写入「设计.正文组成」。
本步 = **壳适配单**四大壳里选一个,只改允许的微调轴。
本步 = **壳适配单**七种壳里选一个,只改允许的微调轴。
权威:`docs/play-presentation-shells.md`。
禁止从零设计分区、禁止自造 shell_id、禁止完整 CSS。
执行顺序:
1. 读美学/叙事:继承人称与终稿归属;不重开文风问卷。
2. **选 shell_id**;说明为何是这个壳而不是另三个
2. **选 shell_id**;说明为何是这个壳而不是其它壳(主面积给谁)
3. 可见块:只挂到该壳已有区域;写显示名与职责(适配文案,不是新分区)。
4. 监控栏:字段意图(细则可交「设计监控栏」);无 monitor 的壳写「不启用」。
5. 隐藏段(若有):定界与用途。
@@ -107,7 +110,7 @@ summary`正文组成 · 适配 {shell_id} · …`。
"从美学/叙事继承的呈现要点"
],
"呈现壳": {
"shell_id": "prose|chat_monitor|turn_panel|chapter_reader",
"shell_id": "prose|chat_monitor|spotlight|turn_panel|split_board|choice_dock|chapter_reader",
"为何选它": "一句话",
"微调": {
"tone_chrome": "messenger|terminal|book|default",
@@ -187,7 +190,7 @@ summary`正文组成 · 适配 {shell_id} · …`。
{
"名": "可实现",
"分数": 8,
"说明": "shell_id 是否在四大壳内;分区与隐藏段是否可交给转述/程序"
"说明": "shell_id 是否在七种壳内;分区与隐藏段是否可交给转述/程序"
}
],
"薄弱点": "一句话"
@@ -208,7 +211,7 @@ summary`正文组成 · 适配 {shell_id} · …`。
硬规则:
1. 合法 JSON含公共三段外壳。
2. 正文须含 `呈现壳.shell_id`四大之一)与 `微调`;可见块只挂壳已有区域。
2. 正文须含 `呈现壳.shell_id`七种之一)与 `微调`;可见块只挂壳已有区域。
3. `可见块` 至少 1 条(通常含 body禁止自造区域 id / 第五种壳。
4. 禁止程序 API schema、context `order`、完整 CSS。
5. 自评:可见完备 / 监控克制 / 可实现(=壳适配是否可执行)。
@@ -219,7 +222,7 @@ summary`正文组成 · 适配 {shell_id} · …`。
```checklist
- [ ] 是「选壳+微调」,不是从零设计布局?
- [ ] shell_id 在四大壳内?可见块未超出该壳分区?
- [ ] shell_id 在七种壳内?可见块未超出该壳分区?
- [ ] 微调轴已填或明确默认?监控栏未塞死名片字段?
- [ ] 隐藏段(若有)定界清楚?未写 CSS 工程?
```

View File

@@ -23,7 +23,7 @@ boundary: |
变量设计字段、映射索引、维护语句形状、side_effects。
具体实例:表行正文——本步不重生成,只声明「旁观读当前档/哪几个摘要键」。
设计监控栏 / 正文组成:用户可见层;本步管提示词视野。
feeds: gm
feeds: gm,auditor
```
## opening
@@ -72,13 +72,13 @@ summary`变量控制上下文 · …`。
"schema": "context-fragment.v1",
"技能": "变量控制上下文",
"brief": "一句话:各槽看见什么状态",
"mount": ["world-simulator"],
"mount": ["world-simulator", "auditor"],
"稳变": "stable",
"正文": {
"挂载": [
{
"对象": "主世界层|叙事转述|旁观|常驻",
"ref或mount": "world-simulator|narrator|…",
"ref或mount": "world-simulator|narrator|auditor|…",
"真值键": ["好感"],
"投影tag": ["上下文.角色态度"],
"映射摘要": ["从映射id取当前档的哪些键无则 []"],

View File

@@ -121,6 +121,7 @@ play
5. reject → 收修订 → 重跑同一 worker含修订流程 = 再调味)
6. 终稿(含 `设计.worker集`)已 accept 且需开局 → `opening-generator`
7. **play**:按 `play_slots``auditor → perspective? → gm → narrator``pendingSideEffectWorkers` 优先
(旁观与主世界层怎么配合:见 `docs/play-dm-auditor.md`——旁观看门补表DM 断事,勿默认多轮传话)
## 禁用行为

View File

@@ -23,7 +23,7 @@ presentation_hints:
tone: 依 Worker 集 presentation / 叙事指南 / 正文组成壳
prompt_excerpt: |
你是面向用户的叙事转述。输入是结构化裁决包settlement.v1不是自由发挥的世界真相。
先读「设计.正文组成」的呈现壳 shell_idprose|chat_monitor|turn_panel|chapter_reader
先读「设计.正文组成」的呈现壳 shell_idprose|chat_monitor|spotlight|turn_panel|split_board|choice_dock|chapter_reader
- prose可直接输出 Markdown 正文(程序会落入 body
- 其它壳:优先输出 JSONschema 为 present.v1
{
@@ -34,10 +34,10 @@ prompt_excerpt: |
"header": "可选",
"body": "用户可读正文",
"footer": "可选",
"aside": "可选"
"aside": "可选短文或键值"
},
"meta": { "suggested_actions": ["…"] }
}
只填该壳已有区域;监控字段来自设计.监控栏 / 投影,禁止编造真值数字若 do_not_say 禁止
monitor 用短键值(芯片),勿写长文。只填该壳已有区域。
禁止写出 do_not_say禁止编造包中没有的事实。
若裁决含 tone_hint / npc_moves / suggested_actions须体现在 body 或 meta。

View File

@@ -10,7 +10,10 @@ export const PRESENT_TAG = "输出.用户展示";
export const PRESENT_SHELL_IDS = [
"prose",
"chat_monitor",
"spotlight",
"turn_panel",
"split_board",
"choice_dock",
"chapter_reader",
] as const;
@@ -70,7 +73,7 @@ export function defaultShellTweaks(
): PresentShellTweaks {
return {
tone_chrome: "default",
show_suggested_actions: shell === "turn_panel",
show_suggested_actions: shell === "turn_panel" || shell === "choice_dock",
block_labels: {},
empty_states: {},
};
@@ -317,13 +320,19 @@ export function shellDefaultRegions(
switch (shell) {
case "chat_monitor":
return ["monitor", "body", "footer"];
case "spotlight":
return ["monitor", "body", "footer"];
case "turn_panel":
return ["monitor", "header", "body", "footer", "aside"];
return ["monitor", "header", "body", "aside", "footer"];
case "split_board":
return ["header", "body", "aside", "footer"];
case "choice_dock":
return ["monitor", "body", "footer"];
case "chapter_reader":
return ["header", "body", "aside", "footer"];
case "prose":
default:
return ["body"];
return ["body", "footer"];
}
}

View File

@@ -370,9 +370,15 @@ function formatPlayPresentHtml(body, view) {
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,
)
[
"prose",
"chat_monitor",
"spotlight",
"turn_panel",
"split_board",
"choice_dock",
"chapter_reader",
].includes(tweaks.shell_id)
? tweaks.shell_id
: "prose";
const parsed = tryParseJsonDoc(trimmed);
@@ -1404,6 +1410,14 @@ function renderSpecialtyBodyHtml(body, skill) {
const html = renderMechanismBodyHtml(body);
if (html) return html;
}
if (
body.风格与写法 != null ||
body.推进与决策 != null ||
skillName.includes("叙事指南")
) {
const html = renderNarrativeBodyHtml(body);
if (html) return html;
}
const looksAesthetics =
body.设定逻辑 != null ||
@@ -1432,6 +1446,60 @@ function renderSpecialtyBodyHtml(body, skill) {
return parts.length ? `<div class="af-mosaic">${parts.join("")}</div>` : "";
}
/** 叙事指南 · 一份全文结构化卡(不强调双投影裁剪) */
function renderNarrativeBodyHtml(body) {
const order = [
"依据的体验",
"叙事纲领",
"风格与遣词",
"笔墨焦点",
"禁忌与不偏好",
"情境备用",
"推进与决策",
"内容与表达",
"风格与写法",
];
const used = new Set();
const parts = [];
// 若仍是旧的「风格与写法 / 推进与决策」两大包,整块展示
if (body.风格与写法 != null || body.推进与决策 != null) {
if (Array.isArray(body.依据的体验) && body.依据的体验.length) {
parts.push(
`<section class="af-panel"><h4>依据的体验</h4>${skillProseList(body.依据的体验)}</section>`,
);
used.add("依据的体验");
}
if (body.风格与写法 != null) {
parts.push(
`<section class="af-panel af-panel-wide"><h4>风格与写法</h4>${renderStructuredValueHtml(body.风格与写法, 0)}</section>`,
);
used.add("风格与写法");
}
if (body.推进与决策 != null) {
parts.push(
`<section class="af-panel af-panel-wide"><h4>推进与决策</h4>${renderStructuredValueHtml(body.推进与决策, 0)}</section>`,
);
used.add("推进与决策");
}
} else {
for (const key of order) {
if (body[key] == null) continue;
used.add(key);
const wide = key === "风格与遣词" || key === "推进与决策";
parts.push(
`<section class="af-panel${wide ? " af-panel-wide" : ""}"><h4>${esc(key)}</h4>${renderStructuredValueHtml(body[key], 0)}</section>`,
);
}
}
for (const [k, v] of Object.entries(body)) {
if (used.has(k) || v == null || v === "") continue;
parts.push(
`<section class="af-panel"><h4>${esc(k)}</h4>${renderStructuredValueHtml(v, 0)}</section>`,
);
}
return parts.length ? `<div class="af-mosaic af-narrative">${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>`;

View File

@@ -61,6 +61,7 @@
</div>
<div class="main-head-right">
<a class="link" href="/modules.html">技能</a>
<a class="link" href="/shells.html">呈现壳</a>
<a class="link" href="/stats.html">Token</a>
<a class="link" href="/settings.html">设置</a>
<button type="button" class="btn-sm" id="btn-save-instance" hidden>保存定稿</button>

View File

@@ -13,6 +13,7 @@
<nav class="st-nav" aria-label="主导航">
<a href="/">对话</a>
<a href="/modules.html" aria-current="page">技能</a>
<a href="/shells.html">呈现壳</a>
<a href="/stats.html">Token</a>
<a href="/settings.html">设置</a>
</nav>

View File

@@ -1,19 +1,31 @@
/**
* 游玩呈现壳渲染P2 最小适配
* 游玩呈现壳渲染P2
* 契约docs/play-presentation-shells.md · present.v1
*
* 信息层级约定(所有壳共用):
* P0 body — 最大字号与面积,阅读主轴
* P1 actions — 需要决策时醒目,但不压过正文
* P2 monitor — 芯片密排,一眼扫完
* P3 header / footer / aside — 辅助,紧凑
*/
export const PRESENT_SHELL_IDS = [
"prose",
"chat_monitor",
"spotlight",
"turn_panel",
"split_board",
"choice_dock",
"chapter_reader",
];
const SHELL_LABELS = {
prose: "纯散文",
chat_monitor: "对话+监控",
spotlight: "场面主视",
turn_panel: "回合面板",
split_board: "双栏看板",
choice_dock: "选择坞",
chapter_reader: "章节阅读",
};
@@ -26,20 +38,30 @@ const REGION_LABELS = {
hidden: "隐藏",
};
/** 默认开建议行动的壳 */
const ACTIONS_DEFAULT_ON = new Set(["turn_panel", "choice_dock"]);
function isPresentShellId(v) {
return typeof v === "string" && PRESENT_SHELL_IDS.includes(v);
}
function shellDefaultRegions(shell) {
export function shellDefaultRegions(shell) {
switch (shell) {
case "chat_monitor":
return ["monitor", "body", "footer"];
case "spotlight":
return ["monitor", "body", "footer"];
case "turn_panel":
return ["monitor", "header", "body", "footer", "aside"];
return ["monitor", "header", "body", "aside", "footer"];
case "split_board":
return ["header", "body", "aside", "footer"];
case "choice_dock":
return ["monitor", "body", "footer"];
case "chapter_reader":
return ["header", "body", "aside", "footer"];
case "prose":
default:
return ["body"];
return ["body", "footer"];
}
}
@@ -71,9 +93,35 @@ function normalizeBlocks(raw) {
return out;
}
function renderChipsHtml(value, esc) {
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
const entries = Object.entries(value).filter(([k]) => k != null && String(k).trim());
if (!entries.length) return null;
return `<div class="present-chips" role="list">${entries
.map(
([k, v]) =>
`<span class="present-chip" role="listitem"><span class="present-chip-k">${esc(k)}</span><span class="present-chip-v">${esc(
typeof v === "string" ? v : JSON.stringify(v),
)}</span></span>`,
)
.join("")}</div>`;
}
function regionInnerHtml(region, rawValue, esc) {
if (region === "monitor") {
const chips = renderChipsHtml(rawValue, esc);
if (chips) return chips;
}
if (region === "aside" && rawValue && typeof rawValue === "object" && !Array.isArray(rawValue)) {
const chips = renderChipsHtml(rawValue, esc);
if (chips) return chips;
}
const text = formatBlockContent(rawValue).trim();
return `<div class="present-region-body">${esc(text).replace(/\n/g, "<br>")}</div>`;
}
/**
* @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;
@@ -110,7 +158,6 @@ export function parsePresentDoc(doc, fallbackShell = "prose") {
};
}
/** 纯文本 → prose 包(供调用方在非 JSON 时使用) */
export function presentFromPlain(text, shell = "prose") {
return {
schema: "present.v1",
@@ -120,6 +167,65 @@ export function presentFromPlain(text, shell = "prose") {
};
}
function regionTitle(region, labels, _shell) {
// 正文默认不挂标题(面积留给文字);创作填了 block_labels.body 才显示
if (region === "body") {
return labels.body || labels["正文"] || null;
}
return (
labels[region] ||
labels[REGION_LABELS[region]] ||
REGION_LABELS[region] ||
region
);
}
function renderRegion(region, rawValue, esc, { labels, emptyStates, shell }) {
let hasContent =
rawValue !== undefined &&
rawValue !== null &&
(typeof rawValue === "string"
? rawValue.trim()
: Array.isArray(rawValue)
? rawValue.length
: typeof rawValue === "object"
? Object.keys(rawValue).length
: true);
let displayRaw = rawValue;
if (!hasContent && emptyStates[region]) {
displayRaw = String(emptyStates[region]);
hasContent = true;
}
if (!hasContent && region !== "body") return "";
if (!hasContent && region === "body") displayRaw = "(暂无正文)";
const title = regionTitle(region, labels, shell);
const titleHtml = title
? `<h4 class="present-region-title">${esc(title)}</h4>`
: "";
return `
<section class="present-region present-region--${esc(region)}" data-region="${esc(region)}">
${titleHtml}
${regionInnerHtml(region, displayRaw, esc)}
</section>`;
}
function renderActionsHtml(actions, esc) {
if (!actions.length) return "";
return `
<section class="present-region present-region--actions" data-region="actions">
<h4 class="present-region-title">建议行动</h4>
<div class="present-action-row" role="list">${actions
.map(
(a) =>
`<span class="present-action" role="listitem">${esc(a)}</span>`,
)
.join("")}</div>
</section>`;
}
/**
* @param {object} packet present.v1
* @param {(s: string) => string} esc
@@ -141,7 +247,7 @@ export function renderPresentShellHtml(packet, esc, opts = {}) {
const showActions =
typeof tweaks.show_suggested_actions === "boolean"
? tweaks.show_suggested_actions
: shell === "turn_panel";
: ACTIONS_DEFAULT_ON.has(shell);
const tone =
typeof tweaks.tone_chrome === "string" && tweaks.tone_chrome.trim()
? tweaks.tone_chrome.trim()
@@ -149,41 +255,96 @@ export function renderPresentShellHtml(packet, esc, opts = {}) {
const blocks = packet.blocks ?? {};
const regions = shellDefaultRegions(shell);
const parts = [];
const ctx = { labels, emptyStates, shell };
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 by = {};
for (const r of regions) {
by[r] = renderRegion(r, blocks[r], esc, ctx);
}
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 actionsHtml = renderActionsHtml(actions, esc);
let layoutHtml = "";
switch (shell) {
case "chat_monitor":
layoutHtml = `
<div class="present-stack">
${by.monitor || ""}
<div class="present-stage">${by.body || ""}</div>
${by.footer || ""}
${actionsHtml}
</div>`;
break;
case "spotlight":
layoutHtml = `
<div class="present-stack present-stack--spotlight">
${by.monitor || ""}
<div class="present-stage present-stage--hero">${by.body || ""}</div>
${by.footer || ""}
${actionsHtml}
</div>`;
break;
case "turn_panel":
layoutHtml = `
<div class="present-board">
<div class="present-board-top">${by.monitor || ""}${by.header || ""}</div>
<div class="present-board-mid">
<div class="present-stage">${by.body || ""}</div>
<div class="present-board-side">${by.aside || ""}${by.footer || ""}</div>
</div>
${actionsHtml}
</div>`;
break;
case "split_board":
layoutHtml = `
<div class="present-split">
<div class="present-split-main">
${by.header || ""}
<div class="present-stage">${by.body || ""}</div>
${by.footer || ""}
</div>
<div class="present-split-side">${by.aside || ""}</div>
${actionsHtml}
</div>`;
break;
case "choice_dock":
layoutHtml = `
<div class="present-stack present-stack--dock">
${by.monitor || ""}
<div class="present-stage">${by.body || ""}</div>
${by.footer || ""}
<div class="present-dock">${actionsHtml}</div>
</div>`;
break;
case "chapter_reader":
layoutHtml = `
<div class="present-reader">
<div class="present-reader-main">
${by.header || ""}
<div class="present-stage present-stage--read">${by.body || ""}</div>
${by.footer || ""}
</div>
<div class="present-reader-aside">${by.aside || ""}</div>
</div>`;
break;
case "prose":
default:
layoutHtml = `
<div class="present-stack present-stack--prose">
<div class="present-stage present-stage--read">${by.body || ""}</div>
${by.footer || ""}
${actionsHtml}
</div>`;
break;
}
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("")}
${layoutHtml}
</div>`;
}
export { SHELL_LABELS, REGION_LABELS, isPresentShellId };

View File

@@ -12,6 +12,7 @@
<nav class="st-nav" aria-label="主导航">
<a href="/">对话</a>
<a href="/modules.html">技能</a>
<a href="/shells.html">呈现壳</a>
<a href="/stats.html">Token</a>
<a href="/settings.html" aria-current="page">设置</a>
</nav>

173
web/shells.css Normal file
View File

@@ -0,0 +1,173 @@
/* 呈现壳预览页
* 注意:本页也引入 styles.csspresent 样式),会给 body 设 overflow:hidden
* 必须在此覆盖,否则侧栏后几项点不到。
*/
html,
body.shells-page {
height: auto;
min-height: 100%;
overflow: auto;
}
.shells-page .st-shell.shells-shell {
max-width: 1180px;
align-items: start;
}
.shells-page .st-rail {
--st-rail-w: 220px;
width: var(--st-rail-w);
position: sticky;
top: var(--st-top-h, 48px);
max-height: calc(100dvh - var(--st-top-h, 48px));
overflow-y: auto;
min-height: 0;
align-self: start;
}
.shells-tone-label {
margin-top: 1rem !important;
}
.shells-density {
display: flex;
flex-wrap: wrap;
gap: 6px 10px;
align-items: baseline;
margin: 0.5rem 0 0.35rem;
padding: 8px 10px;
border-radius: 8px;
background: color-mix(in srgb, var(--accent-soft, #e8f0fe) 70%, transparent);
font-size: 13px;
line-height: 1.45;
}
.shells-density span {
font-size: 11px;
font-weight: 700;
letter-spacing: 0.04em;
color: var(--muted, #666);
text-transform: uppercase;
}
.shells-list {
display: flex;
flex-direction: column;
gap: 4px;
flex: 0 0 auto;
}
.shells-tones {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.shells-tone-chip {
font: inherit;
font-size: 12px;
padding: 4px 10px;
border-radius: 999px;
border: 1px solid var(--border, #ddd);
background: var(--surface, #fff);
cursor: pointer;
color: inherit;
}
.shells-tone-chip.active {
border-color: transparent;
background: var(--accent-soft, #e8f0fe);
font-weight: 600;
}
.shells-body {
display: flex;
flex-direction: column;
gap: 1.25rem;
}
.shells-meta p {
margin: 0 0 0.35rem;
line-height: 1.5;
}
.shells-preview-label,
.shells-tweaks h2,
.shells-help h2 {
margin: 0 0 0.5rem;
font-size: 13px;
font-weight: 700;
color: var(--muted, #666);
}
.shells-preview {
min-height: 280px;
padding: 16px;
border-radius: 12px;
border: 1px dashed var(--border, #ccc);
background:
radial-gradient(ellipse at 20% 0%, rgba(15, 118, 110, 0.06), transparent 50%),
var(--bg, #f6f6f4);
}
.shells-preview .present-shell {
max-width: 640px;
margin: 0 auto;
box-shadow: 0 10px 28px rgba(28, 25, 23, 0.07);
}
.shells-preview[data-shell="turn_panel"] .present-shell,
.shells-preview[data-shell="split_board"] .present-shell,
.shells-preview[data-shell="chapter_reader"] .present-shell {
max-width: 780px;
}
/* 预览区不裁切长正文 */
.shells-preview {
overflow: visible;
}
.shells-tweak-grid {
display: flex;
flex-direction: column;
gap: 10px;
}
.shells-tweak-grid .field {
display: flex;
flex-direction: column;
gap: 4px;
font-size: 13px;
}
.shells-tweak-grid input[type="text"] {
font: inherit;
padding: 6px 8px;
border-radius: 6px;
border: 1px solid var(--border, #ddd);
}
.shells-help ul {
margin: 0;
padding-left: 1.2rem;
line-height: 1.55;
font-size: 13px;
}
.shells-help .muted {
font-size: 12px;
color: var(--muted, #888);
}
@media (max-width: 800px) {
.shells-page .st-rail {
position: static;
max-height: none;
width: auto;
overflow: visible;
}
.shells-preview .present-shell {
max-width: 100%;
}
}

74
web/shells.html Normal file
View File

@@ -0,0 +1,74 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>呈现壳预览 · Writing Agent</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;600;700&family=Noto+Serif+SC:wght@500;600;700&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="/settings.css" />
<link rel="stylesheet" href="/styles.css" />
<link rel="stylesheet" href="/shells.css" />
</head>
<body class="settings-page shells-page">
<header class="st-top">
<a class="st-brand" href="/">Writing Agent</a>
<nav class="st-nav" aria-label="主导航">
<a href="/">对话</a>
<a href="/modules.html">技能</a>
<a href="/shells.html" aria-current="page">呈现壳</a>
<a href="/stats.html">Token</a>
<a href="/settings.html">设置</a>
</nav>
</header>
<div class="st-shell shells-shell">
<aside class="st-rail" aria-label="壳列表">
<p class="st-rail-label">呈现壳</p>
<nav class="shells-list" id="shell-list" role="tablist"></nav>
<p class="st-rail-label shells-tone-label">装饰语气</p>
<div class="shells-tones" id="tone-list" role="group" aria-label="tone_chrome"></div>
<p class="st-rail-label shells-tone-label">正文长度</p>
<div class="shells-tones" id="length-list" role="group" aria-label="body length"></div>
<p class="st-rail-note">
正文按 600~3000 字设计:随字数撑高、页面滚动;侧栏/选择坞钉住。创作只改内容绑定。
</p>
</aside>
<main class="st-main">
<header class="st-main-head">
<div>
<h1 id="panel-title">呈现壳预览</h1>
<p class="st-sub" id="panel-subtitle">用示例数据看布局与可调项</p>
</div>
</header>
<div class="st-body shells-body">
<section class="shells-meta" id="shell-meta"></section>
<section class="shells-preview-wrap">
<h2 class="shells-preview-label">预览</h2>
<div id="shell-preview" class="shells-preview"></div>
</section>
<section class="shells-tweaks">
<h2>本页可调(对应创作「微调轴」)</h2>
<div class="shells-tweak-grid" id="tweak-controls"></div>
</section>
<section class="shells-help">
<h2>设计意图</h2>
<ul>
<li>正文默认装得下单轮 <strong>600~3000 字</strong>:区域随内容长高,由页面/消息区滚动,不塞进固定小框。</li>
<li>双栏壳:侧栏 sticky选择坞选项区 sticky 贴底——长文读完控件还在。</li>
<li>壳差在「谁占主柱」;可改的是字段/显示名/行动开关/tone不是重画布局。</li>
</ul>
<p class="muted">说明文档:<code>docs/play-presentation-shells.md</code></p>
</section>
</div>
</main>
</div>
<script type="module" src="/shells.js"></script>
</body>
</html>

358
web/shells.js Normal file
View File

@@ -0,0 +1,358 @@
/**
* 呈现壳预览页:切换壳 / 装饰语气 / 正文长度,用示例数据渲染 present-shells。
* 正文设计目标:单轮常见 600~3000 字 —— 随字数撑高,外层滚动;侧栏/选项坞钉住。
*/
import {
PRESENT_SHELL_IDS,
renderPresentShellHtml,
} from "./present-shells.js";
const SHELL_INFO = {
prose: {
name: "纯散文",
blurb: "整块都是正文,无 HUD。长文直接往下滚。",
density: "正文随字数伸展;无侧栏争面积",
scenes: "轻对话、纯叙事、不要状态栏",
},
chat_monitor: {
name: "对话 + 监控",
blurb: "顶栏芯片固定矮;主聊/场面再长也只往下长。",
density: "监控一行;正文撑高滚动",
scenes: "网恋 / 日常扮演 / 轻 AIRP",
},
spotlight: {
name: "场面主视",
blurb: "芯片一行 + 大正文柱。适合一幕写满。",
density: "正文主导并伸展;监控不占高",
scenes: "电影感 RP、一幕一景、少机制",
},
turn_panel: {
name: "回合面板",
blurb: "场面叙述再长,右侧交互物钉在视口内对照。",
density: "正文撑高;侧栏 sticky行动在下",
scenes: "资源、检定、场景交互、跑团感",
},
split_board: {
name: "双栏看板",
blurb: "左栏长文滚动,右栏线索钉住——对照读。",
density: "左正文伸展;右侧栏 sticky",
scenes: "调查、推理、多线索并行",
},
choice_dock: {
name: "选择坞",
blurb: "先读完长局面;选项坞 sticky 贴视口底,不用翻回顶。",
density: "正文撑高;底部坞钉住",
scenes: "选项驱动、AVG、分支关口",
},
chapter_reader: {
name: "章节阅读",
blurb: "章题 + 长阅读柱;窄进度侧栏 sticky。",
density: "正文伸展为主;侧栏窄且钉住",
scenes: "长文 / 爽文 / 先纲后章",
},
};
const TONES = [
{ id: "default", label: "默认" },
{ id: "messenger", label: "讯息感" },
{ id: "book", label: "书页感" },
{ id: "terminal", label: "终端感" },
];
const LENGTHS = [
{ id: "short", label: "短(~120" },
{ id: "mid", label: "中(~800" },
{ id: "long", label: "长(~2500" },
];
/** 中等长度 ≈800 字 */
const BODY_MID = `雨还在下。你把伞往她那边偏了偏,她没说话,只是把购物袋往怀里收紧了一点。水从伞骨边缘连成线,砸在柏油路上,溅起细碎的白点。
路口的灯跳成绿色。你们一起迈步——这一次,谁也没有先松开。对面便利店的玻璃上映着两个人并肩的轮廓,忽明忽暗,像谁随手画的草稿。
「伞……」她终于开口,声音被雨声压得很低,「明天不用还。」
你点头,又觉得这回答太轻,补了一句:「那我请你喝热的?」她侧过脸,雨丝粘在睫毛上,笑意只闪了一下,像怕被看穿。
电梯门开的时候,楼道里干燥得过分。她把袋子换到另一只手,钥匙在指间转了一圈,忽然停住:「其实我今天本来想绕路的。」
「为什么?」
「因为预报说会停。结果没有。」她把钥匙推进锁孔,「所以——只好借你的伞了。」
门开了一条缝。暖光漏出来,照在你们脚边的水渍上。她没有立刻进去,只是抬眼看你,像在等一句还没说出口的话。你把伞收拢,水仍顺着伞尖滴到地毯边缘。谁也没有提「进来坐坐」,但空气里已经有了那句话的形状。`;
/** 长文 ≈2500 字(重复段落拼成,预览用) */
const BODY_LONG = `${BODY_MID}
天桥下的风从另一头灌过来,把塑料袋吹得啪啪响。你想起上一次走过这里,还是夏天,蝉声吵得人没法好好说话;如今只剩雨,以及偶尔驶过的车灯,把积水切成一条条亮痕。
便利店里的收音机断断续续播着交通路况。店员打着哈欠换班,玻璃门开合时带进一阵湿冷。你站在门口抖伞,余光看见货架尽头有人停了很久——拿起一瓶水,又放下,再拿起,像在跟自己商量一件比购物更大的事。
她的短信迟了三分钟才到:「到家了吗?」你盯着屏幕上的光标,打了又删:刚进小区 / 伞还在我这 / 明天……最后只留下一句:「刚到。伞明天给你。」她回了个省略号,又补:「不用那么快。」
你把手机塞回口袋,忽然觉得「不用那么快」四个字比天气预报更准。有些东西一旦说破就会散,只好让它停在半湿半干之间。
楼道灯感应得慢,你摸黑上了两级台阶才亮。隔壁门缝里漏出电视剧的笑声,被雨声削成模糊的边。你靠着墙站了一会儿,听自己的呼吸从急变匀,才继续往上走。
到家后你把伞撑在阳台,水沿着伞骨汇成细流,滴进盆里,一声一声,像在数今晚还剩多少没说清的话。窗玻璃上凝着雾,你用指尖随便划了一道,很快又糊住——外面的城市灯火依旧,只是被雨帘隔成另一层世界。
你泡了杯热水,坐到桌前,本想写点什么,笔尖却停在空白处。纸页吸走一点水汽,皱起细纹。你写下一句又涂掉:关于伞,关于绿灯,关于她说「绕路」时那种几乎听不见的停顿。最后只留下日期,和一行没写完的「明天」。
夜里雨势小了些。你关灯前又看了一眼阳台——伞还在,水还在滴,只是间隔更长了,像什么人放慢了脚步,却没有真的离开。
(——以下为加长段,模拟单轮 2000+ 字场面——)
次日清晨,天边只剩薄云。你带着伞出门,金属骨架在布套里轻轻碰撞。电梯里遇见邻居遛狗,它的爪子在地垫上留下两串湿印,很快被后来的人踩散。你想:痕迹就是这样,被人看见时已不是原样。
到了约定的路口,她比你早到半分钟,手里多了一杯热饮,杯套上印着歪歪扭扭的笑脸。「还给你。」你把伞递过去。她却摇头:「今天晴。」伞停在两人之间,像一句还没决定归属的句子。
你们沿着河堤走。水面上漂着昨夜的落叶,偶有涟漪被风抹平。她讲起最近在忙的展览,讲到一半又停住,说其实不太想聊工作。于是话题滑向更碎的东西:哪家面馆换了老板、哪条巷子的灯坏了三天、哪一次下雨你们刚好都没带伞。
「所以才需要借。」她说。
「借完还要还。」你说。
「有的不用还。」她看着河对岸的塔吊,「有的还了,反而像没发生过。」
塔吊缓缓旋转,吊臂在灰蓝天空里划出钝角。你忽然明白,呈现壳要解决的不是「字大一点」,而是这一长段话落在界面里时,读者还愿不愿意往下看——状态栏不能抢戏,选项不能在中途消失,侧栏得钉住好让人对照线索。
你们在桥上站了一会儿。风把伞套吹得鼓起,又瘪下去。她把热饮塞进你手里:「暖的。你昨晚肯定没睡好。」你没有否认。杯壁的温度从掌心爬上手腕,像把雨夜重新烘了一遍,只留下能说出口的那一层。
分开前,她抬手点了点伞:「这个,先放你那儿。等下一场雨。」
你点头。伞重新沉进包里,重量不大,却让背包的一侧微微倾斜——刚好提醒你:故事还会继续,而界面必须装得下继续。`;
const BODY_SHORT = {
prose:
"雨还在下。你把伞往她那边偏了偏,她没说话,只是把购物袋往怀里收紧了一点。\n\n路口的灯跳成绿色。你们一起迈步——这一次谁也没有先松开。",
chat_monitor:
"【短信】\n她到家了吗\n你刚进小区。伞借你的明天还给你\n她……不用那么快。",
spotlight:
"水从铁栏杆上砸下来,把对面那人的轮廓冲得忽明忽暗。\n\n他抬起头——认出是你的瞬间肩膀松了一下又立刻绷紧。",
turn_panel:
"货架后传来塑料袋摩擦声。你屏住呼吸,左手按住刀柄——也许只是风。\n\n收银台抽屉半开里面有未拆封的电池。",
split_board:
"抽屉夹层里有一张折痕发白的收据。日期是失踪前夜,商户栏写着「北港五金」。\n\n窗台积灰被抹过一道——有人近来开过窗。",
choice_dock:
"她把钥匙放在桌上,没有推过来,也没有收回。\n\n「你要是现在走我就当今晚什么都没发生。」",
chapter_reader:
"  天亮得比往日晚。林晚站在窗边,把画册合上,指尖在封面上停了一息。\n\n  「今天」她说「我们换条路回家。」",
};
const SAMPLE_META = {
prose: {
blocks: { footer: "本段为示例备注(可关)" },
meta: { suggested_actions: [] },
},
chat_monitor: {
blocks: {
monitor: { 好感: "暖络", 今日话题: "下雨天", 体力: "还好" },
footer: "",
},
meta: { suggested_actions: ["回一句关心", "约周末还伞"] },
},
spotlight: {
blocks: {
monitor: { 地点: "天桥下", 天气: "暴雨", 张力: "紧" },
footer: "",
},
meta: { suggested_actions: ["先开口", "递伞过去"] },
},
turn_panel: {
blocks: {
monitor: { 体力: "6/10", 物资: "压缩饼干×2", 威胁: "低" },
header: "第 12 回合 · 废弃便利店",
footer: "检定:潜行(通过)",
aside: { 可交互: "电池 / 后门 / 死角", 噪音: "低" },
},
meta: { suggested_actions: ["摸电池", "从后门离开", "大声喝问"] },
},
split_board: {
blocks: {
header: "案发现场 · 二楼书房",
aside: {
线索: "北港收据",
证物: "窗台指痕",
待查: "五金店监控",
},
footer: "",
},
meta: { suggested_actions: ["去北港", "采指纹", "先问管家"] },
},
choice_dock: {
blocks: {
monitor: { 信任: "中", 时间: "子夜前" },
footer: "",
},
meta: {
suggested_actions: ["拿走钥匙留下", "把钥匙推回去", "什么也不说,坐着"],
},
},
chapter_reader: {
blocks: {
header: "卷三 · 第 7 章 雨停之前",
aside: "进度:卷三 7/18\n下一钩母亲来电",
footer: "",
},
meta: { suggested_actions: [] },
},
};
function pickBody(shell, length) {
if (length === "short") return BODY_SHORT[shell] || BODY_SHORT.prose;
if (length === "mid") return BODY_MID;
return BODY_LONG;
}
function countChars(s) {
return Array.from(String(s ?? "").replace(/\s/g, "")).length;
}
function esc(s) {
return String(s ?? "")
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
const state = {
shell: "spotlight",
tone: "default",
length: "mid",
showActions: true,
blockLabels: {},
};
function buildTweaks() {
return {
tone_chrome: state.tone,
show_suggested_actions: state.showActions,
block_labels: { ...state.blockLabels },
empty_states: {
aside: "(侧栏空:可在创作里改空态文案)",
footer: "(文末空)",
},
};
}
function buildPacket() {
const base = SAMPLE_META[state.shell] || SAMPLE_META.prose;
const body = pickBody(state.shell, state.length);
return {
schema: "present.v1",
shell: state.shell,
blocks: { ...base.blocks, body },
meta: base.meta,
_chars: countChars(body),
};
}
function render() {
const info = SHELL_INFO[state.shell] || {
name: state.shell,
blurb: "",
scenes: "",
density: "",
};
const packet = buildPacket();
document.getElementById("panel-title").textContent = info.name;
document.getElementById("panel-subtitle").textContent = `${info.scenes} · 正文约 ${packet._chars} 字(不计空白)`;
document.getElementById("shell-meta").innerHTML = `
<p><strong>${esc(info.name)}</strong><code>${esc(state.shell)}</code></p>
<p>${esc(info.blurb)}</p>
<p class="shells-density"><span>长文策略</span>${esc(info.density)} · 当前试读 <strong>${packet._chars}</strong> 字</p>
<p class="muted">适用:${esc(info.scenes)}。请用上方「正文长度」切到长文,确认侧栏/选项是否仍可用。</p>
`;
const preview = document.getElementById("shell-preview");
preview.dataset.shell = state.shell;
preview.innerHTML = renderPresentShellHtml(packet, esc, { tweaks: buildTweaks() });
document.querySelectorAll("[data-shell]").forEach((btn) => {
btn.classList.toggle("active", btn.getAttribute("data-shell") === state.shell);
});
document.querySelectorAll("[data-tone]").forEach((btn) => {
btn.classList.toggle("active", btn.getAttribute("data-tone") === state.tone);
});
document.querySelectorAll("[data-length]").forEach((btn) => {
btn.classList.toggle("active", btn.getAttribute("data-length") === state.length);
});
const showActionsEl = document.getElementById("tweak-show-actions");
if (showActionsEl) showActionsEl.checked = state.showActions;
}
function mount() {
const list = document.getElementById("shell-list");
list.innerHTML = PRESENT_SHELL_IDS.map((id) => {
const info = SHELL_INFO[id];
return `<button type="button" class="st-rail-item" data-shell="${esc(id)}" role="tab">
${esc(info?.name || id)}
</button>`;
}).join("");
document.getElementById("tone-list").innerHTML = TONES.map(
(t) =>
`<button type="button" class="shells-tone-chip" data-tone="${esc(t.id)}">${esc(t.label)}</button>`,
).join("");
document.getElementById("length-list").innerHTML = LENGTHS.map(
(t) =>
`<button type="button" class="shells-tone-chip" data-length="${esc(t.id)}">${esc(t.label)}</button>`,
).join("");
document.getElementById("tweak-controls").innerHTML = `
<label class="field">
<input type="checkbox" id="tweak-show-actions" />
显示「建议行动」(对应微调轴 show_suggested_actions
</label>
<label class="field">
正文区显示名block_labels.body
<input type="text" id="tweak-label-body" placeholder="例如:短信记录 / 场面 / 本章" />
</label>
<label class="field">
监控区显示名block_labels.monitor
<input type="text" id="tweak-label-monitor" placeholder="例如:状态 / 今日" />
</label>
`;
list.addEventListener("click", (e) => {
const btn = e.target.closest("[data-shell]");
if (!btn) return;
state.shell = btn.getAttribute("data-shell");
render();
});
document.getElementById("tone-list").addEventListener("click", (e) => {
const btn = e.target.closest("[data-tone]");
if (!btn) return;
state.tone = btn.getAttribute("data-tone");
render();
});
document.getElementById("length-list").addEventListener("click", (e) => {
const btn = e.target.closest("[data-length]");
if (!btn) return;
state.length = btn.getAttribute("data-length");
render();
});
const syncLabels = () => {
state.showActions = document.getElementById("tweak-show-actions").checked;
const body = document.getElementById("tweak-label-body").value.trim();
const monitor = document.getElementById("tweak-label-monitor").value.trim();
state.blockLabels = {};
if (body) state.blockLabels.body = body;
if (monitor) state.blockLabels.monitor = monitor;
render();
};
document.getElementById("tweak-controls").addEventListener("change", syncLabels);
document.getElementById("tweak-controls").addEventListener("input", syncLabels);
render();
}
mount();

View File

@@ -67,7 +67,7 @@
<body>
<div class="stats-page">
<header>
<p><a href="/">← 对话</a> · <a href="/modules.html">技能</a> · <a href="/settings.html">设置</a></p>
<p><a href="/">← 对话</a> · <a href="/modules.html">技能</a> · <a href="/shells.html">呈现壳</a> · <a href="/settings.html">设置</a></p>
<h1>Token 统计</h1>
<p class="stats-muted">按时间戳筛选调用记录(存于本地 stats/*.jsonl</p>
</header>

View File

@@ -1,4 +1,4 @@
/* Functional layout <EFBFBD> agent-first, no production-line chrome */
/* Functional layout — agent-first, no production-line chrome */
:root {
--bg: #f4f4f5;
@@ -22,7 +22,7 @@
color: var(--text);
}
/* 寮€鍒涗綔鍚庣缉杩涘乏渚ф爮 */
/* 开创作后缩进左侧栏 */
body.rail-collapsed {
--books-w: var(--books-w-collapsed);
}
@@ -31,7 +31,7 @@ body.rail-collapsed {
html, body {
height: 100%;
margin: 0;
overflow: hidden; /* 娴忚<EFBFBD>鍣ㄥ眰姘镐笉鍑虹幇椤甸潰婊氬姩鏉<EFBFBD> */
overflow: hidden; /* 浏览器层永不出现页面滚动条 */
}
button, input, textarea { font: inherit; }
a.link { color: var(--accent); text-decoration: none; }
@@ -138,7 +138,7 @@ body.rail-collapsed .rail-tab {
}
body.rail-collapsed .rail-panels { display: none; }
body.rail-collapsed .panel-rail { align-items: stretch; }
/* composer 鎸夊唴瀹硅嚜閫傚簲锛涗腑闂村尯鍚冩帀鍓╀綑楂樺害锛屾暣椤甸攣鍦ㄨ<EFBFBD> */
/* composer 按内容自适应;中间区吃掉剩余高度,整页锁在视口内 */
.panel-main {
display: grid;
grid-template-rows: var(--head-h) minmax(0, 1fr) auto;
@@ -195,7 +195,7 @@ body.rail-collapsed .panel-rail { align-items: stretch; }
.panel-head-actions { display: flex; gap: 4px; align-items: center; }
.btn-sm.active { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); }
/* Books <EFBFBD> explorer breadcrumb + list */
/* Books — explorer breadcrumb + list */
.path-bar {
display: flex; align-items: center; flex-wrap: nowrap; gap: 2px;
margin: 6px 8px 4px; padding: 6px 10px;
@@ -267,7 +267,7 @@ body.rail-collapsed .panel-rail { align-items: stretch; }
body[data-lifecycle="play"] { --accent: #16a34a; --accent-soft: #dcfce7; --bg: #f0fdf4; }
body[data-lifecycle="play"] .panel-rail { background: #f7fef9; }
/* Skill picker <EFBFBD> primary action when starting */
/* Skill picker — primary action when starting */
.skill-picker {
padding: 16px;
border-bottom: 2px solid var(--accent);
@@ -285,7 +285,7 @@ body[data-lifecycle="play"] .panel-rail { background: #f7fef9; }
.skill-card-desc { font-size: 12px; color: var(--muted); line-height: 1.4; }
.skill-card-tag { font-size: 11px; color: var(--accent); margin-top: 6px; }
/* Feed <EFBFBD> SillyTavern 寮忓<EFBFBD>璇濇皵娉<EFBFBD> */
/* Feed — SillyTavern 式对话气泡 */
.message-feed {
flex: 1;
overflow-y: auto;
@@ -557,7 +557,7 @@ body.rail-collapsed .message-feed {
margin: 0 0 8px;
}
/* 涓诲<EFBFBD>璇濋獙鏀跺崱锛氬悆婊腑闂存爮 */
/* 主对话验收卡:吃满中间栏 */
.msg-review {
align-self: stretch;
width: 100%;
@@ -594,7 +594,7 @@ body.rail-collapsed .message-feed {
.msg-review .ws-focus-hint { margin-bottom: 0; font-size: 11px; }
.msg-review .review-worker-set { margin: 0 0 6px; }
/* х墿绱у噾鍗★細妯<EFBFBD>悜涓昏酱锛岄<EFBFBD>瑙堜竴琛<EFBFBD> */
/* 产物紧凑卡:横向主轴,预览一行 */
.artifact-compact-list {
display: flex;
flex-direction: column;
@@ -650,8 +650,8 @@ body.rail-collapsed .message-feed {
border-radius: 999px;
background: var(--accent-soft);
}
.artifact-compact-toggle::before { content: "灞曞紑"; }
.artifact-compact[open] .artifact-compact-toggle::before { content: "鏀惰捣"; }
.artifact-compact-toggle::before { content: "展开"; }
.artifact-compact[open] .artifact-compact-toggle::before { content: "收起"; }
.artifact-compact-preview {
margin: 0;
font-family: var(--font-read);
@@ -707,7 +707,7 @@ body.rail-collapsed .message-feed {
}
.ws-focus-body .artifact-compact-list { margin: 0; }
/* 灞曞紑鍖猴細mosaic / 鐡风爾缃戞牸锛屽悆婊℃í鍚戝<E98D9A><EFBFBD> */
/* 展开区:mosaic / 瓷砖网格,吃满横向宽度 */
.af-mosaic {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
@@ -818,7 +818,7 @@ body.rail-collapsed .af-mosaic {
gap: 5px;
}
/* 鎶€鑳戒笓鐢ㄦ<EFBFBD>鏂囧崱 */
/* 技能专用正文卡 */
.skill-view {
display: flex;
flex-direction: column;
@@ -1244,7 +1244,7 @@ body.rail-collapsed .af-mosaic {
.msg-q-list li { margin: 0.35rem 0; }
.msg-q-lead { margin: 0; line-height: 1.55; font-size: 14px; }
/* Cursor 寮忚<EFBFBD><EFBFBD>崱锛氳创鍦ㄦ秷鎭<EFBFBD>祦涓嬫柟锛屽崰婊″崗璋冨垪瀹藉害锛屼笉閬<EFBFBD>洊鍘嗗彶 */
/* Cursor 式询问卡:贴在消息流下方,占满协调列宽度,不遮盖历史 */
.questions-card-host {
flex: 0 1 auto;
width: 100%;
@@ -1255,7 +1255,7 @@ body.rail-collapsed .af-mosaic {
min-height: 0;
}
.questions-card-host[hidden] { display: none !important; }
/* 寮€鍚<EFBFBD>椂鍗<EFBFBD> feed 鍓╀綑楂樺害鐨勪笂闄愶紝涓嶆妸鏁撮〉鎾戝嚭瑙嗗彛锛涘嬁涓<E5AC81> [hidden] 骞跺瓨鎶<EFBFBD> display */
/* 开启时占 feed 剩余高度的上限,不把整页撑出视口;勿与 [hidden] 并存抢 display */
.questions-card-host.is-open:not([hidden]) {
display: flex;
flex-direction: column;
@@ -1276,7 +1276,7 @@ body.rail-collapsed .af-mosaic {
background: var(--surface, #fff);
overflow: hidden;
}
/* <EFBFBD>诞鎺т欢灞傦細涓嶅崰鏂囨。娴侀珮搴<EFBFBD> */
/* 悬浮控件层:不占文档流高度 */
.qcard-float {
position: absolute;
inset: 0;
@@ -1349,7 +1349,7 @@ body.rail-collapsed .af-mosaic {
flex: 1 1 auto;
min-height: 0;
padding: 10px 12px;
overflow: auto; /* 浠呭崱鍐呮粴鍔<EFBFBD>紱椤甸潰鏈<EFBFBD>韩涓嶆粴 */
overflow: auto; /* 仅卡内滚动;页面本身不滚 */
}
.qcard.has-pager .qcard-body { padding-top: 36px; }
.qcard.has-skip .qcard-body { padding-bottom: 40px; }
@@ -1461,7 +1461,7 @@ body.rail-collapsed .af-mosaic {
}
.ws-slot-chip.ws-slot-on { background: var(--accent-soft); border-color: transparent; }
.ws-slot-chip.ws-slot-off { opacity: 0.55; text-decoration: line-through; }
/* х墿缁撴瀯鍖栧睍绀<EFBFBD><> 瀵归綈 qcard / flow-step / ws-badge */
/* 产物结构化展示 — 对齐 qcard / flow-step / ws-badge */
.artifact-friendly { display: flex; flex-direction: column; gap: 10px; }
.artifact-list { margin: 0; padding-left: 18px; line-height: 1.5; }
.artifact-hero {
@@ -1561,7 +1561,7 @@ body.rail-collapsed .af-mosaic {
.artifact-kv-row.complex .artifact-kv-key { margin-bottom: 2px; }
.artifact-kv-val { min-width: 0; font-size: 13px; line-height: 1.45; }
/* 鐧惧垎姣旇繘搴︽潯 */
/* 百分比进度条 */
.pct-meter {
display: flex;
flex-direction: column;
@@ -1913,7 +1913,7 @@ body.rail-collapsed .af-mosaic {
.timeline-item { padding: 6px 0; border-bottom: 1px solid var(--border); }
.timeline-empty { color: var(--muted); }
/* Composer <EFBFBD> 楂樺害闅忓唴瀹癸紝浣嗘湁涓婇檺锛岄伩鍏嶆尋鐖嗚<E99096><EFBFBD> */
/* Composer — 高度随内容,但有上限,避免挤爆视口 */
.composer {
border-top: 1px solid var(--border);
padding: 10px 12px;
@@ -2001,81 +2001,362 @@ body.rail-collapsed .af-mosaic {
body:not(.rail-collapsed) .rail-panels { display: none; }
}
/* —— 游玩呈现壳 present.v1P2 最小) —— */
/* —— 游玩呈现壳 present.v1 —— */
.present-shell {
--present-ink: #1c1917;
--present-muted: #78716c;
--present-line: #e7e5e4;
--present-soft: #f5f5f4;
--present-accent: #0f766e;
--present-body: 17.5px;
--present-body-lh: 1.78;
--present-body-hero: 19px;
--present-body-read: 18.5px;
display: flex;
flex-direction: column;
gap: 10px;
gap: 0;
position: relative;
padding: 10px 12px 12px;
border: 1px solid var(--border);
border-radius: 10px;
background: var(--surface);
padding: 14px 16px 16px;
border: 1px solid var(--present-line);
border-radius: 12px;
background: var(--surface, #fff);
color: var(--present-ink);
}
.present-shell-badge {
align-self: flex-start;
font-size: 11px;
margin-bottom: 10px;
font-size: 10px;
font-weight: 600;
letter-spacing: 0.04em;
color: var(--muted);
padding: 1px 7px;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--present-muted);
padding: 2px 8px;
border-radius: 4px;
border: 1px solid var(--border);
background: var(--surface-2, rgba(0, 0, 0, 0.03));
border: 1px solid var(--present-line);
background: var(--present-soft);
}
.present-stack,
.present-board,
.present-split,
.present-reader {
display: flex;
flex-direction: column;
gap: 12px;
min-width: 0;
}
.present-region {
display: flex;
flex-direction: column;
gap: 4px;
gap: 6px;
min-width: 0;
}
.present-region-title {
margin: 0;
font-size: 11px;
font-weight: 700;
color: var(--muted);
letter-spacing: 0.04em;
color: var(--present-muted);
letter-spacing: 0.05em;
}
.present-region-body {
margin: 0;
line-height: 1.55;
line-height: 1.65;
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;
/* P0正文舞台 —— 随字数伸展600~3000 靠外层滚动,不塞进小框) */
.present-stage {
flex: 1 1 auto;
min-height: 0;
min-width: 0;
}
.present-region--body {
min-width: 0;
}
.present-region--body .present-region-body,
.present-stage .present-region--body .present-region-body,
.present-stage.present-region--body .present-region-body {
font-size: var(--present-body);
line-height: var(--present-body-lh);
letter-spacing: 0.01em;
max-height: none;
overflow: visible;
}
.present-stage--hero .present-region-body {
font-size: var(--present-body-hero);
line-height: 1.82;
padding: 4px 2px 8px;
}
.present-stage--read .present-region-body {
font-family: var(--font-read);
font-size: var(--present-body-read);
line-height: 1.9;
max-width: 42rem;
}
/* 双栏:侧栏钉住,正文再长也能对照 */
.present-board-side,
.present-split-side,
.present-reader-aside {
position: sticky;
top: 0.75rem;
align-self: start;
max-height: calc(100dvh - 3rem);
overflow-y: auto;
}
/* P2监控芯片 */
.present-chips {
display: flex;
flex-wrap: wrap;
gap: 6px 8px;
}
.present-chip {
display: inline-flex;
align-items: baseline;
gap: 6px;
padding: 4px 10px;
border-radius: 6px;
border: 1px solid var(--border);
background: var(--surface-2, rgba(0, 0, 0, 0.03));
border: 1px solid var(--present-line);
background: var(--present-soft);
font-size: 12px;
line-height: 1.35;
}
.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;
.present-chip-k {
color: var(--present-muted);
font-weight: 600;
}
.present-chip-v {
color: var(--present-ink);
font-weight: 500;
}
.present-region--monitor .present-region-body,
.present-region--aside .present-region-body,
.present-region--footer .present-region-body {
font-size: 12.5px;
line-height: 1.45;
color: var(--present-ink);
padding: 8px 10px;
border-radius: 8px;
border: 1px solid var(--present-line);
background: var(--present-soft);
}
.present-region--header .present-region-body {
font-size: 14px;
font-weight: 650;
letter-spacing: 0.01em;
line-height: 1.4;
}
/* P1行动 */
.present-action-row {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.present-action {
display: inline-flex;
align-items: center;
padding: 7px 12px;
border-radius: 8px;
border: 1px solid color-mix(in srgb, var(--present-accent) 35%, var(--present-line));
background: color-mix(in srgb, var(--present-accent) 8%, #fff);
color: var(--present-ink);
font-size: 13px;
font-weight: 550;
line-height: 1.3;
}
.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-shell--chat_monitor {
--present-accent: #0369a1;
background:
linear-gradient(180deg, #f0f9ff 0%, #fff 48px);
}
.present-tone--book .present-region--body {
max-width: 42rem;
.present-shell--chat_monitor .present-stage {
padding: 14px 16px;
border-radius: 10px;
border: 1px solid var(--present-line);
background: #fff;
}
.present-tone--terminal .present-region-body {
.present-shell--spotlight {
--present-accent: #b45309;
padding-top: 14px;
}
.present-shell--spotlight .present-stack--spotlight {
gap: 14px;
}
.present-shell--spotlight .present-stage--hero {
padding: 6px 4px 4px;
}
.present-shell--spotlight .present-region--monitor .present-region-title {
display: none;
}
.present-shell--turn_panel {
--present-accent: #047857;
background: linear-gradient(165deg, #f0fdf4 0%, #fff 28%);
}
.present-board-top {
display: flex;
flex-direction: column;
gap: 8px;
}
.present-board-mid {
display: grid;
grid-template-columns: minmax(0, 1.7fr) minmax(140px, 0.85fr);
gap: 12px;
align-items: start;
}
.present-board-side {
display: flex;
flex-direction: column;
gap: 8px;
}
.present-shell--turn_panel .present-stage {
padding: 16px;
border-radius: 10px;
border: 1px solid var(--present-line);
background: #fff;
}
.present-shell--split_board {
--present-accent: #4338ca;
}
.present-split {
display: grid;
grid-template-columns: minmax(0, 1.55fr) minmax(150px, 0.9fr);
gap: 14px 16px;
align-items: start;
}
.present-split-main {
display: flex;
flex-direction: column;
gap: 10px;
min-width: 0;
}
.present-split-side {
min-width: 0;
padding: 10px;
border-radius: 10px;
border: 1px solid var(--present-line);
background: var(--present-soft);
}
.present-split .present-region--actions {
grid-column: 1 / -1;
}
.present-shell--choice_dock {
--present-accent: #be123c;
}
.present-stack--dock {
gap: 10px;
}
.present-dock {
margin-top: 4px;
padding: 12px;
border-radius: 12px;
border: 1px solid color-mix(in srgb, var(--present-accent) 28%, var(--present-line));
background: color-mix(in srgb, var(--present-accent) 6%, #fff);
/* 长正文时选项坞钉在视口底,读完仍看得到 */
position: sticky;
bottom: 0;
z-index: 2;
box-shadow: 0 -8px 20px color-mix(in srgb, #fff 70%, transparent);
}
.present-dock .present-region-title {
margin-bottom: 2px;
}
.present-dock .present-action {
padding: 9px 14px;
font-size: 13.5px;
border-width: 1.5px;
}
.present-shell--chapter_reader {
--present-accent: #44403c;
background:
linear-gradient(90deg, transparent 0, transparent),
linear-gradient(180deg, #fafaf9 0%, #fff 40%);
}
.present-reader {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(120px, 0.42fr);
gap: 18px;
align-items: start;
}
.present-reader-main {
display: flex;
flex-direction: column;
gap: 12px;
min-width: 0;
}
.present-reader-aside {
font-size: 12px;
padding-top: 4px;
border-left: 1px solid var(--present-line);
padding-left: 14px;
}
.present-shell--chapter_reader .present-region--header .present-region-body {
font-family: var(--font-read);
font-size: 20px;
font-weight: 600;
}
.present-shell--prose .present-stage--read {
padding: 6px 2px;
}
.present-shell--prose .present-region--footer .present-region-body {
opacity: 0.85;
}
@media (max-width: 640px) {
.present-board-mid,
.present-split,
.present-reader {
grid-template-columns: 1fr;
}
.present-board-side,
.present-split-side,
.present-reader-aside {
position: static;
max-height: none;
}
.present-reader-aside {
border-left: none;
border-top: 1px solid var(--present-line);
padding-left: 0;
padding-top: 12px;
}
}
.present-tone--messenger .present-stage {
border-left: 3px solid #7dd3fc;
padding-left: 12px;
}
.present-tone--book .present-stage--read .present-region-body,
.present-tone--book .present-stage .present-region-body {
font-family: var(--font-read);
max-width: 38rem;
}
.present-tone--terminal {
--present-ink: #d6d3d1;
--present-muted: #a8a29e;
--present-line: #3f3f46;
--present-soft: #27272a;
background: #18181b;
color: #e7e5e4;
}
.present-tone--terminal .present-chip,
.present-tone--terminal .present-stage,
.present-tone--terminal .present-dock,
.present-tone--terminal .present-split-side {
background: #27272a;
border-color: #3f3f46;
}
.present-tone--terminal .present-region-body,
.present-tone--terminal .present-action {
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 13px;
}
.present-tone--terminal .present-region--body .present-region-body {
font-size: calc(var(--present-body) - 0.5px);
}
.present-tone--terminal .present-action {
font-size: 13px;
color: #e7e5e4;
background: #3f3f46;
}