From 94f67fa7444b53d71e467aa7e93a25bd2de2a33c Mon Sep 17 00:00:00 2001 From: moranzhi Date: Mon, 3 Aug 2026 01:36:32 +0800 Subject: [PATCH] =?UTF-8?q?=E5=BC=95=E5=85=A5=E4=B8=8A=E4=B8=8B=E6=96=87?= =?UTF-8?q?=E7=89=87=E6=AE=B5=E3=80=81=E5=9B=BA=E5=AE=9A=E6=A7=BD=E4=B8=8E?= =?UTF-8?q?=E6=8A=95=E5=BD=B1=E6=8E=92=E5=BA=8F=EF=BC=8C=E5=B9=B6=E5=AE=8C?= =?UTF-8?q?=E5=96=84=E6=9C=BA=E9=81=87=E8=A3=81=E5=AE=9A=E4=B8=8E=E6=B8=B8?= =?UTF-8?q?=E7=8E=A9=E6=9C=9F=20UI=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 把创作产物收敛为可挂载片段与 play_slots/context_order,同步修订世界模拟器模块与运行时拼装。 Co-authored-by: Cursor --- docs/briefs/capability-authoring-brief.md | 76 +- docs/briefs/prompt-revision-inventory.md | 86 + docs/context-assembly.md | 10 + docs/context-fragment-design.md | 133 ++ docs/design-orchestrator-guide.md | 8 +- docs/progressive-data-design.md | 127 ++ docs/references.md | 9 + docs/ui-design.md | 10 +- docs/ui-glossary.md | 22 +- docs/world-simulator-modules.md | 31 +- .../modules/aesthetics-interaction/prompt.md | 180 ++- .../world-simulator/modules/catalog.yaml | 79 +- .../modules/concrete-instances/prompt.md | 133 +- .../modules/context-order/prompt.md | 129 ++ .../modules/generation-rules/prompt.md | 346 ++-- .../modules/mechanism/prompt.md | 281 ++-- .../modules/narrative/prompt.md | 274 +++- .../modules/opening-setup/prompt.md | 173 ++ .../world-simulator/modules/refine/prompt.md | 156 +- .../modules/reply-format/prompt.md | 211 ++- .../modules/status-bar/prompt.md | 168 +- .../modules/topology/prompt.md | 2 +- .../modules/variable-context/prompt.md | 124 +- .../modules/variable-design/prompt.md | 205 ++- .../modules/worker-spec/prompt.md | 158 +- .../modules/world-blueprint/prompt.md | 356 +++-- .../recipes/expand-assistant/recipe.yaml | 7 +- .../recipes/world-simulator/recipe.yaml | 36 +- .../worker-templates/README.md | 42 +- .../worker-templates/chance.yaml | 22 + .../worker-templates/narrator.yaml | 19 +- .../worker-templates/opening-generator.yaml | 10 +- .../worker-templates/world-simulator.yaml | 23 +- .../workers/design-flow/SKILL.md | 10 +- .../workers/opening-generator/SKILL.md | 25 +- src/main-agent/main-agent.ts | 24 +- src/main-agent/tools.ts | 7 +- src/runtime/phase-machine.ts | 5 + src/runtime/phase-runtime.ts | 133 +- src/runtime/tool-registry.ts | 14 +- src/server/display-labels.ts | 5 +- src/server/session-manager.ts | 83 + src/server/web-server.ts | 90 ++ src/skills/chance-tools.ts | 325 ++++ src/skills/context-fragment.ts | 352 +++++ src/skills/context-order.ts | 511 ++++++ src/skills/context-segments.ts | 63 +- src/skills/creation-flow.ts | 12 +- src/skills/creation-units.ts | 2 +- src/skills/declared-worker.ts | 80 +- src/skills/dialogue-history.ts | 96 ++ src/skills/play-slots.ts | 265 ++++ src/skills/settlement-packet.ts | 231 +++ src/skills/worker-declaration.ts | 16 +- src/skills/worker-set-parse.ts | 113 +- src/skills/worker-set-view.ts | 98 +- src/types/runtime.ts | 24 +- src/worker/executor.ts | 28 + tests/context-fragment.test.ts | 216 +++ tests/dialogue-history.test.ts | 140 ++ tests/play-slots.test.ts | 169 ++ tests/worker-set-sanitize.test.ts | 34 + tests/worker-set-view.test.ts | 25 +- web/agent-ui.js | 1386 +++++++++++++++-- web/app.js | 19 +- web/display-labels.js | 5 +- web/index.html | 69 +- web/questions-ui.js | 5 + web/styles.css | 743 ++++++++- 69 files changed, 7786 insertions(+), 1283 deletions(-) create mode 100644 docs/briefs/prompt-revision-inventory.md create mode 100644 docs/context-fragment-design.md create mode 100644 docs/progressive-data-design.md create mode 100644 skills/dialogue/world-simulator/modules/context-order/prompt.md create mode 100644 skills/dialogue/world-simulator/modules/opening-setup/prompt.md create mode 100644 skills/dialogue/world-simulator/worker-templates/chance.yaml create mode 100644 src/skills/chance-tools.ts create mode 100644 src/skills/context-fragment.ts create mode 100644 src/skills/context-order.ts create mode 100644 src/skills/dialogue-history.ts create mode 100644 src/skills/play-slots.ts create mode 100644 src/skills/settlement-packet.ts create mode 100644 tests/context-fragment.test.ts create mode 100644 tests/dialogue-history.test.ts create mode 100644 tests/play-slots.test.ts diff --git a/docs/briefs/capability-authoring-brief.md b/docs/briefs/capability-authoring-brief.md index 017c340..e6fdae7 100644 --- a/docs/briefs/capability-authoring-brief.md +++ b/docs/briefs/capability-authoring-brief.md @@ -351,7 +351,8 @@ task 必须写:禁止重复同一开场;在首答上补洞。 - **键名对人友好**(中文或稳定中文标签)。 - 机器 id(如 `ref`)若需要,与中文名成对出现。 - 写清「本步完成的定义」;未决放 `开放问题`,不要假完备。 -- 不要在本技能产物里偷偷交下游能力该交的终稿(除非本技能就是收成步)。 +- 不要在本技能产物里偷偷交下游能力该交的终稿(除非本技能就是收成步)。 +- **中段「上下文创作」技能**须输出 §4.9 的 `context-fragment.v1`(含 `schema` / `brief` / `正文`);收成类技能用自有 schema(见 `docs/context-fragment-design.md`)。 ## 4.8 追问(`probe`)通用要求 @@ -360,15 +361,62 @@ task 必须写:禁止重复同一开场;在首答上补洞。 - 能推断则先写入再复述;只问影响本步核心且不能瞎填的点。 - 依赖产物已钉死的内容禁止重问。 +## 4.9 上下文片段公共头(`context-fragment.v1`) + +中段技能(美学、机制、世界、叙事、变量…)的产物**不是**自由发明执行单元,而是可挂载的上下文片段。完整说明见 **`docs/context-fragment-design.md`**。 + +**所有上下文创作技能**共用三段外壳(键名固定);**正文内部维度**与**自评维度名**按技能自定,但两块都必须有。 + +```json +{ + "schema": "context-fragment.v1", + "技能": "{与 catalog 一致的中文名}", + "brief": "一句话概括", + "mount": ["world-simulator"], + "稳变": "stable", + "正文": { }, + "自评": { + "维度": [{ "名": "…", "分数": 0, "说明": "…" }], + "薄弱点": "…" + }, + "追问": { + "导语": "…", + "题目": [ + { + "问": "…", + "建议选项": ["…", "其它(请写明)"], + "示例": "可选短钩子" + } + ] + }, + "开放问题": [] +} +``` + +| 段 | 说明 | +|----|------| +| `正文` | **产物主体**。技能自定内部结构(如美学步=设定逻辑 + 交互范式 + 美学纲领);须含「详细各个方面」。禁止整份只交散文。 | +| `自评` | **自评评分**。`维度[]` 名目按技能定(美学步常用:交互范式 / 美学纲领 / 整体协调);可含 `薄弱点`。 | +| `追问` | **示例 + 建议选项**。给用户的下一问;`题目` 可空数组。与程序 askUser 可并存,但产物里要留结构化题面。 | + +| 规则 | 说明 | +|------|------| +| `schema` | 必须原样写出,供前端友好渲染 | +| `mount` | 挂哪些槽/ref;**不要**写最终数字 `order` | +| `稳变` | `stable` \| `semi` \| `volatile`,供「上下文投影排序」 | +| 收成例外 | 游玩拓扑 / 上下文投影排序 / 细化终稿用自有 schema,不套本头 | + +范例对齐:`modules/aesthetics-interaction/prompt.md`(output 块)。 + --- # 第五部分:撰写任一能力时的工作步骤 1. **定身份**:中文名、id、artifact;写清 when / when_not / boundary。 2. **看邻接**:上/下游能力各定什么;禁止重叠问卷。 -3. **定产物形状**:先定 `output` JSON,再写 task/probe 如何填满它。 +3. **定产物形状**:先定 `output` JSON(中段须含 §4.9 公共头),再写 task/probe 如何填满它。 4. **定 opening**:需要程序先问再 LLM → 写 opening;否则留空。 -5. **写 principles / probe / checklist**:正推、缩减、禁止套件。 +5. **写 principles / probe / checklist**:正推、缩减、禁止套件;checklist 含 schema/brief/正文。 6. **对照范例**:深度不低于 `aesthetics-interaction`。 7. **自检**:§4 块齐全;name 与 catalog 一致;用户文案无裸英文 id。 @@ -411,17 +459,18 @@ artifact: |------|-----|-------------------| | 美学纲领与交互范式 | `aesthetics-interaction` | 范例 | | 实现机制 | `mechanism` | 已写 | -| 世界蓝图与人文地理 | `world-blueprint` | 已写 | -| 生成规则 | `generation-rules` | 已写(可反复) | -| 具体实例 | `concrete-instances` | 已写(可反复) | -| 叙事指南 | `narrative` | 已写 | +| 舞台骨架 | `world-blueprint` | 已写(`context-fragment.v1`;社会结构 + 世界状况) | +| 生成规则 | `generation-rules` | 已写(`context-fragment.v1`;可反复;合同严、正文可长) | +| 具体实例 | `concrete-instances` | 已写(`context-fragment.v1`;可反复;只按规则执行) | +| 叙事指南与故事推进 | `narrative` | 已写(`context-fragment.v1`;遣词/笔墨/禁忌+推进;旧称叙事指南) | +| 正文组成 | `reply-format` | 已写(用户可见版式+隐藏段+前端拆分;旧称设计回复格式) | +| 设计监控栏 | `status-bar` | 已写(只盯会变信息;旧称设计状态栏) | +| 开场白与开场变量 | `opening-setup` | 已写(开场正文+初值;守正文组成) | | 拓扑图谱 | `topology` | 待细写 | -| 变量设计与更新规则 | `variable-design` | 待细写 | -| 变量控制上下文 | `variable-context` | 待细写 | -| 设计状态栏 | `status-bar` | 待细写 | -| 设计回复格式 | `reply-format` | 待细写 | -| Worker 规格 | `worker-spec` | 已写(可反复) | -| 细化终稿 | `refine` | 已写 | +| 变量设计与更新规则 | `variable-design` | 已写;见 progressive-data-design.md | +| 变量控制上下文 | `variable-context` | 已写 | +| 游玩拓扑 | `worker-spec` | 已写(勾选固定槽,不可反复发明) | +| 细化终稿 | `refine` | 已写(按槽收成) | 配方:世界模拟器、扩写助手(见 `recipes/`;写 `core`/`process`/`principles` + 起步 steps)。 @@ -433,6 +482,7 @@ artifact: |------|------| | 本文 | **泛用**能力撰写 + 项目/称呼;给外部 AI 的主交接 | | `world-simulator-modules.md` | 仓库内清单与格式摘要 | +| `context-fragment-design.md` | 片段 schema、槽位、投影排序与拼装阶段 | | `ui-glossary.md` | 用户可见文案权威 | | `architecture.md` | 运行内核;写能力时不必复述实现细节 | | (已删)单能力特例简报 | 以本文为准;勿再恢复特例交接文 | diff --git a/docs/briefs/prompt-revision-inventory.md b/docs/briefs/prompt-revision-inventory.md new file mode 100644 index 0000000..2fd3005 --- /dev/null +++ b/docs/briefs/prompt-revision-inventory.md @@ -0,0 +1,86 @@ +# 提示词 / 能力文档修订清单(待你改) + +> 程序侧已落地:`play_slots`、`settlement.v1`、模板默认契约、前端友好渲染雏形。 +> **本清单只整理「可能需要你改的提示词/能力文」——此处不改正文,等你确认后再动。** + +权威方法:`docs/progressive-data-design.md`、`docs/ui-glossary.md` §5.1。 + +--- + +## 优先级 P0(与固定槽 + 裁决包强相关) + +| 文件 | 状态 | +|------|------| +| `modules/worker-spec/prompt.md` | **已改** → 游玩拓扑,只勾选固定槽 | +| `modules/refine/prompt.md` | **已改** → 按 `play_slots` 收成,禁发明 ref | +| `modules/catalog.yaml` / 配方 / `design-flow` | **已改**(配方含:挂谁可早、排第几须晚) | +| `modules/context-order/prompt.md` | **骨架已写**(`context-order.v1`) | +| `docs/context-fragment-design.md` | **已写**;范例技能已对齐 `context-fragment.v1` | +| 程序:片段解析 / 扁平投影序 / `对话.历史` 可投影 | **已落地**(非硬双锚) | +| `orchestrator.md` | **仍待**:play 调度顺序 perspective? → gm → narrator | + +## 优先级 P1(能力归属 feeds) + +| 文件 | 建议补什么 | +|------|------------| +| `modules/*/prompt.md` 的 `meta` | 统一加 `feeds: gm \| narrator \| runtime \| design_only \| perspective`(约定字段,程序以后可读) | +| `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` 可再收紧「只出建议」 | + +## 优先级 P2(文档口径) + +| 文件 | 建议 | +|------|------| +| `docs/design-orchestrator-guide.md` | § 三大步 / Worker 创造:改为固定槽 + 创作上下文映射;保留表副作用 | +| `docs/world-simulator-modules.md` | 清单状态:worker-spec→拓扑;链到本清单 | +| `docs/tag-blackboard.md` | 强调 `运行.本轮.裁决` = settlement.v1;转述少读 `变量.当前` | +| `docs/skill-design-guide.md` | 若仍教「创造 worker」,加世界模拟例外 | +| `ui-glossary.md` | §5.1 已有主世界层;可补「游玩槽位」用户可见词(可选) | + +## 明确不必改成「第三 Worker」的 + +- **变量管理**:保持 `variable-design` / `variable-context` + Runtime `side_effects`,不要新 `variable-update` 必选槽。 +- **世界观**:常驻挂 GM,不要世界观执行单元。 +- **性格分档**:Data + Progressive tag,不要性格 agent。 + +## 程序已做、提示词需跟上的契约 + +```json +// 设计.worker集(世界模拟) +{ + "play_slots": { "gm": true, "narrator": true, "perspective": false }, + "tables": { "side_effects": [/* 来自变量设计 */] }, + "workers": [/* 可空,由程序按槽展开;或覆盖 context/outputs */] +} +``` + +```json +// 运行.本轮.裁决 +{ + "schema": "settlement.v1", + "player_action": "…", + "resolved": ["…"], + "visible_now": "…", + "npc_moves": [{ "who": "…", "move": "…" }], + "variable_changes": [{ "key": "好感", "delta": 8 }], + "do_not_say": ["好感数字"], + "tone_hint": "…" +} +``` + +## 建议你改的顺序 + +1. `refine` + `worker-spec`(或新拓扑技能)对齐 `play_slots` +2. `world-simulator` 配方 + `orchestrator` 调度顺序 +3. 各能力 meta 补 `feeds` +4. 骨架能力(status-bar / reply-format / topology)按需细写或降级 + +改完后告诉我,我可以按你的口径改提示词并加解析 `feeds`(若要程序注入)。 diff --git a/docs/context-assembly.md b/docs/context-assembly.md index 3c7689e..58f582d 100644 --- a/docs/context-assembly.md +++ b/docs/context-assembly.md @@ -39,6 +39,16 @@ Worker / skill 执行时,Runtime 将黑板 tag 与固定体裁说明拼成 LLM 越增量、每轮变 → 越靠下(dynamic) ``` +创作期若产出了 `context_order`(见 `context-fragment-design.md`): + +```text +按 inserts 扁平 order 顺序拼装(不再按 static/dynamic 重排) +「对话.历史」是排序表中的普通标签,按 projection 动态裁剪后插入 +``` + +`order 0` 建议为槽位人设(`worker.persona`)。 +`tier` 仅缓存提示;权威顺序是投影排序表。 + **不是** agent 在游玩时自由往 prompt 里插段落;agent 只决定 **invoke 哪个 skill**;该 skill 的契约决定看见什么。 --- diff --git a/docs/context-fragment-design.md b/docs/context-fragment-design.md new file mode 100644 index 0000000..2b5df38 --- /dev/null +++ b/docs/context-fragment-design.md @@ -0,0 +1,133 @@ +# 上下文片段 · 槽位 · 投影排序(产物与拼装规范) + +> 作者与实现共用。用户侧术语见 `ui-glossary.md` §0。 +> 技能撰写契约见 `briefs/capability-authoring-brief.md` §4.7 / §4.9。 +> 真值门控见 `progressive-data-design.md`;拼装见 `context-assembly.md`。 + +## 1. 问题与分工 + +创作期不再「发明执行单元清单」,而是产出**可挂载的上下文片段**,再收成固定槽上的运行规格。 + +| 何时 | 钉什么 | 权威产物 | +|------|--------|----------| +| 中段各技能 | 片段正文 + **挂谁**(mount)+ 可选稳/变 | `设计.*`(`context-fragment.v1`) | +| 游玩拓扑 | **启用哪些固定槽** | `设计.worker规格` → `play_slots` | +| 上下文投影排序(晚) | **扁平投影序**(含「对话.历史」标签) | `设计.上下文投影排序`(`context-order.v1`) | +| 细化终稿 | 合并进运行规格 | `设计.worker集`(含 `play_slots` + `context_order`) | + +**挂谁可早、排第几须晚。** 中段禁止写死最终 `order`。 + +## 2. 固定槽(play_slots) + +| 槽键 | 默认 ref | 用户可见 | 默认 | +|------|----------|----------|------| +| `gm` | `world-simulator` | 主世界层 | 开 | +| `narrator` | `narrator` | 叙事转述 | 开 | +| `perspective` | `role-decide` | 角色视角 | 关 | + +推荐游玩调度:`perspective? → gm → narrator`。 + +## 3. 上下文片段:`context-fragment.v1` + +公共外壳(**所有上下文创作技能相同**): + +| 段 | 含义 | +|----|------| +| `schema` / `技能` / `brief` / `mount` / `稳变` | 索引与挂载;不写 `order` | +| `正文` | **产物主体**(技能自定内部维度,但必须有「各个方面」) | +| `自评` | **自评评分**(`维度[]` + 可选薄弱点;维度名按技能) | +| `追问` | **导语 + 题目**(建议选项 / 示例);可无题则 `题目: []` | +| `开放问题` | 未结构化的残项(可选) | + +美学步正文:`设定逻辑` + `交互范式` + `美学纲领`(见 `aesthetics-interaction`)。 +实现机制正文:`依据的核心体验` + `支撑点[]` + `支撑点关系[]` + `覆盖检验`(见 `mechanism`)。 +舞台骨架正文:尺度 / 基底与变造 / **社会结构** / **世界状况** / 关键舞台区 / 未展开范围(见 `world-blueprint`)。 +生成规则正文:必要性判断 + `rules[]`(生成与描写 + **单层**产物格式 + 可选多池绑顶层字段);自评=必要性/属性妥当/格式准确。 +美学纲领自评仍为:交互范式 / 美学纲领 / 整体协调(勿与生成规则三维混用)。 +叙事指南与故事推进正文:纲领 / 风格与遣词(+示范) / 笔墨焦点 / 禁忌与不偏好 / 情境备用 / 推进与决策 / 内容与表达;自评=写法可执行/推进可执行/与美学对齐;artifact=`设计.叙事指南与故事推进`。 +正文组成:用户可见块序 + 监控栏锚点 + 隐藏变量段 + 前端拆分意图(非程序报文);artifact=`设计.正文组成`。 +设计监控栏:只监控会变字段(可含攻略目标/场景交互物);artifact=`设计.监控栏`(旧称状态栏)。 +变量设计:真值 + **维护语句** + **Data映射索引**(指向具体实例)+ side_effects。 +变量控制上下文:各槽视野 + **旁观汇总**(当前档摘要)。 +开场白与开场变量:开场正文(守正文组成)+ 开场初值;可经 opening-generator 落库。 + + +具体实例正文:`rule_id` + `records[]`(单层,键随规则);自评=合规模(只按规则执行)。 +其它技能换掉正文内块名与自评维度名即可,三段外壳不要拆。 +## 4. 投影排序:`context-order.v1`(扁平序) + +**不要**再把上下文硬拆成「历史前区 / 历史后区」。排序表就是一条投影流水线: + +```text +order 0: worker.persona(fixed) +order 1: 设计.实现机制(summary) +order 2: 对话.历史(summary) ← 历史本身也是标签 +order 3: 变量.当前(fields) +order 4: 用户.最新输入(full) +``` + +```json +{ + "schema": "context-order.v1", + "brief": "扁平投影序", + "play_slots": { "gm": true, "narrator": true, "perspective": false }, + "slots": [ + { + "ref": "world-simulator", + "inserts": [ + { "order": 0, "ref": "worker.persona", "projection": "fixed" }, + { "order": 1, "ref": "设计.实现机制", "projection": "summary" }, + { "order": 2, "ref": "对话.历史", "projection": "summary", "note": "按投影裁剪" }, + { "order": 3, "ref": "变量.当前", "projection": "fields" }, + { "order": 4, "ref": "用户.最新输入", "projection": "full" } + ] + } + ] +} +``` + +| 字段 | 说明 | +|------|------| +| `order` | 每槽独立扁平数字序;**0 建议为人设** | +| `ref` | 黑板 tag、`worker.persona`、或 **`对话.历史`** | +| `projection` | `fixed` \| `full` \| `summary` \| `fields`——对历史标签也生效 | +| `anchor` | **已废弃**;旧表可读,编排 API 的 `set_anchor` 现表示「移到历史标签前/后」 | + +### 4.1 `对话.历史` 标签 + +- 程序在游玩期维护黑板 tag `对话.历史`(用户输入 + 助手可见输出追加)。 +- 排序表里出现该 ref 时,按 **projection** 动态裁剪后注入(不是整段永远塞满)。 +- 若尚未写入历史,可用 `运行.事件流` + `用户.最新输入` 拼兜底。 +- 投影建议:`summary`(默认)/ `fields`(更短)/ `full`(更长)/ `fixed`(几乎不注入)。 + +### 4.2 拼装 + +Runtime 按 inserts 数组顺序拼进单条 user 上下文(`assembleWorkerContext` **不再**按 static/dynamic 重排)。 +`tier` 仅作缓存提示(历史标签之前偏 static,之后偏 dynamic)。 + +### 4.3 可编排 + +检查器「上下文投影排序 · 可编排」: + +- ↑↓ 调整扁平位置 +- 改 projection +- 「史前 / 史后」:把该项移到 `对话.历史` 之前或之后 + +API:`POST /api/sessions/:id/context-order`(`move` / `set_projection` / `set_anchor` / `replace`)。 + +## 5. 运行规格落点 + +`设计.worker集` 含 `play_slots` + `context_order`;无表时可由声明合成(含历史标签)。 + +## 6. UI 渲染 + +| schema | 友好渲染 | +|--------|----------| +| `context-fragment.v1` | 概要 / 挂载 / 正文 | +| `context-order.v1` | 扁平序表;历史行高亮 | + +## 7. 配方链路 + +```text +体验契约 → 确认槽 → 中段片段(mount)→ 投影排序(含对话.历史)→ 细化终稿 +``` diff --git a/docs/design-orchestrator-guide.md b/docs/design-orchestrator-guide.md index f46d2a3..2aea42f 100644 --- a/docs/design-orchestrator-guide.md +++ b/docs/design-orchestrator-guide.md @@ -81,7 +81,7 @@ - **扮演什么**(世界执行者?对白对手?写作助手?陈述实验?) - **输出什么**(叙事、摘要、选项、表、大纲块…) -- **怎么输出**(单段终稿 / 程序拼状态栏+正文 / 多面板) +- **怎么输出**(单段终稿 / 监控栏+正文分块 / 文末小块 / 隐藏变量段;见正文组成) - **怎么与用户交互**(回合对话、填表驱动、先大纲后正文…) 由此才谈:要不要转述、程序拼格式、表是否上屏等。 @@ -198,7 +198,7 @@ | 记忆 / 长线 / 大纲 / 随机分支 / 人设阶段 | **统一用表**(§5):维护 + 显隐 + 副作用插换上下文 | | 信息差玩法 | 分角 worker;极贵,慎用 | | 格式 | 程序拼 | -| 真随机 / 骰子卡组计算 | 用户触发或填入,或表;已有 tool 手动调;不临时造 tool | +| 真随机 / 骰子卡组计算 | 固定按需槽 `chance`(程序工具:roll/compare/draw/pick)→ `运行.本轮.机遇`;`play_slots.chance` 开启后由编排器 run_worker;禁止临时造 LLM「骰子演员」 | | 用户不应知道的信息 | 程序固定注入,不进可见区;非 input 整理 worker | | 输入协议 | 常驻上下文(`()` 元要求、`""` 对白、无包裹=事实等) | @@ -278,6 +278,8 @@ value, rev, updatedAt, source # source: user | worker: | system 实现:`src/blackboard/table-side-effects.ts`;写 `变量.当前` / `运行.初始变量` 后由 runtime 求值;fired 存 `运行.表副作用.fired`。 +方法与技能落点(真值 / Data / Progressive、写入源、少拆执行单元):**`progressive-data-design.md`**;创作技能:`variable-design`、`variable-context`。 + ### 5.6 与交互流程 助手流:生成大纲/细纲 → **用户写入/改表** → 副作用或调度读表继续;始终尊重字段 `rev`。 @@ -340,7 +342,7 @@ phase:core(核心体验 + 交互) ``` **禁止默认**「先写完 worker 列表,再为每个 worker 填写上下文」。 -Worker 规格应回答:职责是什么、读哪些已有 tag、写哪些 tag——而不是顺带发明一份私有美学。 +游玩拓扑只勾选固定槽(主世界层 / 转述 / 可选视角等),禁止自由发明执行单元;读写与美学长文分别由模板合并与上游常驻上下文承担。 **UI**:检查器把固定上下文做成 **独立卡片区**(非 Worker 子项),每张卡展示 **塞进哪些 Worker**;见 `worker-set-view.ts` 的 `contextTags`。 diff --git a/docs/progressive-data-design.md b/docs/progressive-data-design.md new file mode 100644 index 0000000..bda538e --- /dev/null +++ b/docs/progressive-data-design.md @@ -0,0 +1,127 @@ +# 真值变量 · Data · Progressive(设计方法) + +> 给作者与世界模拟器技能用。运行内核细节见 `design-orchestrator-guide.md` §5、`src/blackboard/table-side-effects.ts`。 +> 借鉴对照(非耦合):SillyTavern 世界书条目的 data / progressive 去向、evented「派生不落盘」。 + +## 1. 要解决什么 + +把「好感变了性格就变」「等级到了推进大纲」「场景切换」「程序抽事件」做成**可设计、可程序门控**的能力,而不是: + +- 靠模型自觉「记得改性格」; +- 为每张表拆一个执行单元; +- 把大段设定整坨塞进每轮 prompt。 + +## 2. 三层分工 + +```text +真值变量(state) 好、等级、章节、场景 id、任务进度、已触发集合、seed… + │ 程序比较 / 边沿检测(tables.side_effects) + ▼ +Data(分档·大纲·场景·事件池) 事先写好的大表;按真值查档,不整表常驻 + │ 命中后 write_tag / replace_tag / queue_worker + ▼ +Progressive 投影 本轮主世界层只看见「当前档」;未解锁不进上下文 +``` + +| 层 | 存什么 | 谁改 | 例子 | +|----|--------|------|------| +| **真值** | 少数字段,跨轮必须记住 | 见 §3 | `好感=37`、`章节=2`、`地点=旧港` | +| **Data** | 阈值→切片、章→细纲、场景包、事件池 | 创作期钉死;游玩期通常只读 | 好感分档性格表、章大纲表 | +| **Progressive** | 当前应注入的上下文 tag | **程序**按边沿写入 | `上下文.角色态度`、`大纲.当前章` | + +**派生不落盘:** 「当前性格长文」是 `f(好感)`,不要再设一个可写变量「当前性格」让模型去改。真值变了,副作用重写投影 tag 即可。 + +## 3. 变量如何维护(三种写入源) + +存储:黑板表 tag `变量.当前`(字段格 + `rev`);开局可与 `运行.初始变量` 对齐。合并规则:`src/blackboard/table-cells.ts`(**用户手改默认不被执行单元覆盖**)。 + +| 来源 | 怎么发生 | 适用 | +|------|----------|------| +| **执行单元(LLM)** | 某执行单元 `outputs` 含 `变量.当前` / `运行.本轮.变量变更`,经 Runtime 合并 | 叙事后果导致的增量(送礼→好感+…);规则须在规格里写清 | +| **用户手动** | UI/检查器改字段格;`source=user`,高 `rev` | 导演改档、纠错、助手流填表驱动 | +| **程序(副作用)** | 真值边沿触发 `tables.side_effects` → `write_tag` / `replace_tag` / `queue_worker` | **按某值切换投影**、once 解锁、插队某执行单元 | + +当前副作用**直接改的是投影 tag / 队列**,不是再写一格「派生真值」。若要用程序把 A 映射成 B 真值,优先: + +1. 仍用 A 做真值,B 做成 data 投影;或 +2. 在更新规则里写明「仅程序/仅用户可写 B」,由后续 Runtime 扩展 `set_field` 类 action(未实现前不要假装已有)。 + +### 3.1 「根据某个值设置」= Progressive,不是第二真值 + +```text +好感 >= 60(mode: once) + → replace_tag 上下文.角色态度 =「恋爱档:……」 + +章节 changed 且 章节 == 2(或 eq) + → replace_tag 大纲.当前章 =「第二章细纲……」 + +场景 id 边沿 + → replace_tag 上下文.当前场景包 =「……」 +``` + +规格形状见 `design-orchestrator-guide.md` §5.5;`fired` 在 `运行.表副作用.fired`。 + +### 3.2 程序「随机」事件 + +不要靠模型「感觉该来点意外」: + +1. Data:事件池(id、权重、前置条件、正文/指令)。 +2. 真值:可选 `seed` / 回合计数 / 场景权重档。 +3. 程序:在边沿或回合结算时抽一条 → 写入 `运行.事件流` 或投影 tag;主世界层只演绎已抽中的条目。 +4. once / fired:同一事件 id 不重复(除非规则允许)。 + +(真随机:启用 `play_slots.chance`,由按需执行单元 `chance` 程序掷骰/抽签/比点,结果在 `运行.本轮.机遇`;主世界层只演绎已出的结果。事件池权重抽签也可走 chance.pick。) + +## 4. 哪些需要变量(判定) + +只问:**下游要不要可靠查询、门控或防重复?** + +| 做成真值变量 | 做成 Data / Progressive | 留散文即可 | +|--------------|-------------------------|------------| +| 好感、等级、章节、场景 id、任务阶段 | 分档性格、章细纲、场景描写包 | 纯氛围、一次性修辞 | +| 已初吻 / 已背叛(once 门) | 门触发后的态度切片 | — | +| 已触发事件 id 集合 | 事件池正文 | — | +| 已揭示秘密集合 | 秘密正文(揭示前勿进主上下文) | — | +| 程序随机用 seed / 权重档 | 事件池 | 「今天好像该有事」 | + +承重优先:满屏状态栏不如少量真值 + 若干分档表。 + +## 5. 游玩拓扑:主世界层 + 可选旁观(勿盲目拆执行单元) + +内部仍叫 **执行单元(Worker)**——一次 `run_worker` 的调度单位,这个词准确。 + +易混的是**职责命名**:多数世界模拟局应是: + +```text +用户输入 + → 主世界层(世界推演):读变量 + 已投影 data,交事件/改真值(按规则) + →(可选)旁观审计:只出意见,不写 canon + →(可选)叙事转述:把裁决写成用户可见正文 + → 用户看到的终稿通常来自「呈现/叙事」执行单元,或主世界层兼呈现(规格决定) +``` + +| 内容 | 给谁 | 不要做成 | +|------|------|----------| +| 世界观、地点、势力百科 | 主世界层 + 常驻/查表 | 「世界观执行单元」 | +| 好感与分档性格 | 真值 + Progressive 投影 | 「性格执行单元」 | +| 大纲按章推进 | 真值 `章节` + 章表投影 | 每章一个执行单元 | +| 知密视角 / 强信息隔离 | 才考虑额外执行单元 | 为「更聪明」而拆 | +| 长跑漂移检查 | 旁观审计(只读建议) | 第三个创作主笔 | + +用户可见名:`ui-glossary.md`(世界推演、叙事转述…)。作者文档可称 **主世界层 / 旁观**,与酒馆「主 GM + auditor」同构;**不要**对用户改称整套系统为「GM 框架」而废弃「执行单元」。 + +## 6. 世界模拟器技能落点 + +| 技能 | 钉什么 | +|------|--------| +| **变量设计与更新规则** | 真值;**维护语句**形状(settlement / 隐藏段);**Data 映射索引**(指向生成规则+具体实例);`side_effects` | +| **变量控制上下文** | 各槽视野;**旁观汇总**(当前档摘要,非整表);剧透边界 | +| **生成规则 / 具体实例** | 分档表 schema 与行数据(如好感→性格);生成后由变量设计索引 | +| **正文组成** | 隐藏段定界(供解析维护语句);用户可见块序 | +| **设计监控栏** | 用户可见的会变字段子集 | +| **开场白与开场变量** | 开场正文(守版式)+ 与开场同真相的初值 | +| **游玩拓扑 / 上下文投影排序 / 细化终稿** | 固定槽;扁平投影序;tables 收成 | + +**维护路径(不是 chance toolcall)**:主世界层输出可解析变更 → Runtime 合并 `变量.当前` → `side_effects` 换投影;chance 只负责骰子/抽签。 + +编排口诀:体验与机制 → 要不要真值 → 映射表用生成规则/实例 → 维护语句与旁观汇总 → 开场同真相 → 收成。 diff --git a/docs/references.md b/docs/references.md index 960ed7b..b24ba58 100644 --- a/docs/references.md +++ b/docs/references.md @@ -43,6 +43,13 @@ - **可借鉴**:角色卡产品概念、世界书编辑体验、聊天存档与分支心流、角色扮演工作方式、导入导出与兼容性概念 - **不照搬**:Prompt 拼接逻辑;世界书检索与权限由 Context Compiler 实现 +### tavern2agent(方法层) + +- 仓库: +- **可借鉴**:卡语义拆解中的 data / progressive 去向、真值与派生分离、主 GM + 旁观审计(少拆执行单元)、承重才立可变概念 +- **落本地文档**:`progressive-data-design.md`;技能 `variable-design` / `variable-context` +- **不借鉴为内核**:Pi 宿主、per-card TS engine/reducer 代码生成、CodeAct 万能入口 + ### LangGraph.js - 仓库: @@ -81,6 +88,8 @@ |------------|----------------| | Runtime / 调度 / 恢复 | LangGraph.js、AutoGen、OpenHands、Mastra(边界) | | 上下文与记忆 | Letta、Mastra Memory 接口形态 | +| 真值 / Data / Progressive 设计方法 | tavern2agent(方法层)、SillyTavern 世界书概念 | +| 上下文片段 / 双锚点插入序 | SillyTavern prompt_order / injection 概念(只借鉴形态,见 `context-fragment-design.md`) | | 聊天与 HITL UI | assistant-ui、CopilotKit、LobeChat | | 角色卡 / 世界书 / 存档体验 | SillyTavern、LobeChat | | 模型接入与可观测 | Mastra | diff --git a/docs/ui-design.md b/docs/ui-design.md index 5f20a1f..48b926a 100644 --- a/docs/ui-design.md +++ b/docs/ui-design.md @@ -135,7 +135,7 @@ QQ 等 IM 仅保留一条可借鉴点:**底栏作为唯一行动锚点**。其 | 原则 | 含义 | 反面 | |------|------|------| -| **一屏一事** | 同一时刻只有一个主任务(审产物 / 答提问 / 读叙事) | 右侧同时堆历史 + 验收 + 进度 + Agent focus | +| **一屏一事** | 同一时刻只有一个主任务(审产物 / 答提问 / 读叙事) | 侧栏与主画布抢同一决策 | | **行动点唯一** | Accept、发送、swipe 只在一处出现 | composer 与检查器各放一套接受按钮 | | **对话是索引** | 气泡放摘要 + 跳转;全文在工作面 | Worker 长产出塞进中间气泡 | | **底栏锚定** | 输入/确认永远在底部同一区域 | 底栏在纯按钮与 textarea 间大幅换位 | @@ -163,7 +163,7 @@ QQ 等 IM 仅保留一条可借鉴点:**底栏作为唯一行动锚点**。其 实现:`web/agent-ui.js` 中 `shouldShowInFeed`、`FEED_HIDDEN_KINDS`。 -### 7.2 检查器(目标态;当前部分在 `panel-agent`) +### 7.2 检查器(目标态;调度日志已迁左侧「调度」Tab,右侧 `panel-agent` 已取消) **应承载:** @@ -405,13 +405,13 @@ flowchart TB | 现状(`web/`) | 心流问题 | 目标 | |----------------|----------|------| -| 右侧常驻 `panel-agent`(约 300px) | 空跑时也占视线 | 按需检查器,默认收起 | -| focus / tool-trace 在右侧顶部 | 像 IDE 调试台 | 并入折叠日志或顶栏小状态 | +| ~~右侧常驻 `panel-agent`~~ | ~~空跑时也占视线~~ | **已做**:取消右侧栏;调度日志迁左侧「调度」Tab;开写后缩进侧栏 | +| ~~focus / tool-trace 在右侧顶部~~ | ~~像 IDE 调试台~~ | **已做**:并入左侧「调度」 | | intake 可在 feed 与 composer 两处 | 信息重复 | 表格进检查器,feed 仅摘要 | | `worker_questions` = 文本列表 + composer | 用户写长段、选项语义易丢 | **§8 询问卡**:文案可改、点字母选中 | | `askUser: string[]` | 无 options / Other / 分页 | 升级 `QuestionItem` 协议 | | 验收时 composer 与「验收」Tab 均可操作 | 行动点分散 | 主按钮只在检查器 | -| 移动端隐藏整个 `panel-agent` | 无产物面 | 全屏 drawer | +| ~~移动端隐藏整个 `panel-agent`~~ | ~~无产物面~~ | **已做**:无右侧栏;窄屏缩进左侧轨 | | 主区标题写「agent-first」 | 与产品定位不符 | 创作 = 产物优先;游玩 = 叙事优先 | 已有正确方向(保持并强化): diff --git a/docs/ui-glossary.md b/docs/ui-glossary.md index 7814b9b..cc0df8c 100644 --- a/docs/ui-glossary.md +++ b/docs/ui-glossary.md @@ -101,6 +101,7 @@ | `design-flow` | **创作 · 流程编排** | 以已选【配方】为起点,编排/增量修订工作流计划(可变 DAG) | | `design-step` | **创作 · 执行步骤** | 按工作流计划执行当前【技能】 | | `opening-generator` | **开局 · 开场白** | 创作末尾可选 | +| `worker-spec`(技能) | **游玩拓扑** | 勾选固定槽;旧称「Worker 规格」,勿再当自由发明演员 | | `design-core` 等 | (已废弃) | 旧分步 skill;勿再调度 | 标题动作后缀(拼在中文名后): @@ -122,13 +123,29 @@ |---------|----------| | `narrator` | **叙事转述** | | `role-decide` | **角色决策** | -| `world-simulator` | **世界推演** | +| `world-simulator` | **主世界层**(旧称:世界推演) | +| `chance` | **机遇裁定**(按需:骰子/抽签/比点;程序工具) | | `round-present` | **回合呈现** | 配方选项展示名示例:`world-simulator`(recipe)→ **世界模拟器**;`expand-assistant` → **扩写助手**。 **创造执行单元时**:规格里另写 `name`(中文展示名)。`ref` 仍用英文 kebab;UI 优先 `name`。 +### 5.1 游玩拓扑称呼(作者文档) + +内部调度单位仍叫 **执行单元(Worker)**——一次上场调用,这个词准确,**不要废除**。 + +作者讨论职责时可用与酒馆主 GM 同构的说法(对用户 UI 仍用上表): + +| 作者可用 | 典型 ref | 含义 | +|----------|----------|------| +| **主世界层** | `world-simulator` | 读变量与 Progressive 投影、按规则改真值/交事件;多数世界观与查表归这里 | +| **叙事转述** | `narrator` | 把裁决写成用户可见正文(若规格拆了呈现) | +| **机遇裁定** | `chance` | 按需程序工具(掷骰/比点/抽签);不进每轮管线;结果 tag `运行.本轮.机遇` | +| **旁观 / 审计** | 可选自建 | 只出意见,不写 canon;不为「更聪明」而拆 | + +默认少拆:百科、分档性格、章大纲投影 → 主世界层 + 表副作用,**不要**再拆「世界观执行单元」「性格执行单元」。详见 `docs/progressive-data-design.md` §5。 + --- ## 6. 创作单位 id(检查器 / 进度) @@ -147,7 +164,7 @@ |-------|----------| | `aesthetics-interaction` | 美学纲领与交互范式 | | `interaction` | 交互范式(旧;已并入上一行) | -| `narrative_guide` | 叙事指南 | +| `narrative_guide` | 叙事指南与故事推进 | | `input_protocol` | 输入协议 | | `core_premise` | 核心前提 | | `aesthetics` | 美学纲领(旧;已并入美学纲领与交互范式) | @@ -190,3 +207,4 @@ | `creation-playbook.md` | 创作/游玩流程概念 | | `design-orchestrator-guide.md` | 创作方法(可继续写英文 id,面向作者) | | `briefs/capability-authoring-brief.md` | 技能撰写交接(术语须与本文一致) | +| `context-fragment-design.md` | 上下文片段格式、固定槽、投影排序与双锚点 | diff --git a/docs/world-simulator-modules.md b/docs/world-simulator-modules.md index 33e2d66..174be10 100644 --- a/docs/world-simulator-modules.md +++ b/docs/world-simulator-modules.md @@ -3,7 +3,10 @@ > 给作者用。用户侧术语:**`docs/ui-glossary.md` §0**。 > 运行:选配方 → 编排**增量**工作流计划 DAG → `design-step` 执行技能;可再扩步反复调用。 > **标准范例**:`modules/aesthetics-interaction/prompt.md`。 -> **给外部 AI 的完整泛用规范**:**`docs/briefs/capability-authoring-brief.md`**(项目概述 + 称呼 + 格式契约)。 +> **真值 / Data / Progressive(变量与门控)**:**`docs/progressive-data-design.md`**。 +> **上下文片段 / 槽位 / 投影排序**:**`docs/context-fragment-design.md`**。 +> **提示词待改清单**:**`docs/briefs/prompt-revision-inventory.md`**。 +> **给外部 AI 的完整泛用规范**:**`docs/briefs/capability-authoring-brief.md`**(含 `context-fragment.v1`)。 ## 两层 @@ -122,27 +125,29 @@ recipes/world-simulator|expand-assistant/recipe.yaml | 能力 | id | 状态 | |------|-----|------| | 美学纲领与交互范式 | `aesthetics-interaction` | **范例已写** | -| 实现机制 | `mechanism` | **已写** | -| 世界蓝图与人文地理 | `world-blueprint` | **已写** | -| 生成规则 | `generation-rules` | **已重写**,可反复;双门槛 + schema + 生命周期;编排必填 `params.target` | -| 具体实例 | `concrete-instances` | **已重写**,可反复;执行预生成规则;编排必填 `params.rule_id` | -| 叙事指南 | `narrative` | **已写** | +| 实现机制 | `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` | 骨架,待细写 | -| 变量设计与更新规则 | `variable-design` | 骨架,待细写 | -| 变量控制上下文 | `variable-context` | 骨架,待细写 | -| 设计回复格式 | `reply-format` | 骨架,待细写 | +| 设计监控栏 | `status-bar` | **已写**:只盯会变信息;旧称状态栏 | +| 正文组成 | `reply-format` | **已写**:用户可见版式+隐藏段+前端拆分;旧称回复格式 | +| 变量设计与更新规则 | `variable-design` | **已写**:真值+维护语句+Data映射索引+side_effects | +| 变量控制上下文 | `variable-context` | **已写**:挂载+旁观汇总 | +| 开场白与开场变量 | `opening-setup` | **已写**:开场守版式+同真相初值 | 共用收成(池内保留,按需): | 能力 | id | 状态 | |------|-----|------| -| Worker 规格 | `worker-spec` | **已写**,可反复 | -| 细化终稿 | `refine` | **已写**(产物=`设计.worker集`) | +| 游玩拓扑 | `worker-spec` | **已写**:勾选固定槽,禁止自由发明 worker(旧称 Worker 规格) | +| 上下文投影排序 | `context-order` | **已写**:扁平投影序(含对话.历史);收成进 `context_order`;Runtime 已拼装 | +| 细化终稿 | `refine` | **已写**:按 `play_slots`(及排序表)收成 `设计.worker集` | | 编排器 | 状态 | |------|------| -| 世界模拟器 | 方法论已写(core/process/principles);起点:美学纲领与交互范式 | +| 世界模拟器 | 链路已写:体验→挂槽写上下文→投影排序→终稿;起点:美学纲领与交互范式 | | 扩写助手 | 方法论已写;起点:美学纲领与交互范式;勿默认套世界模拟全套 | --- diff --git a/skills/dialogue/world-simulator/modules/aesthetics-interaction/prompt.md b/skills/dialogue/world-simulator/modules/aesthetics-interaction/prompt.md index 94f0810..9265e5f 100644 --- a/skills/dialogue/world-simulator/modules/aesthetics-interaction/prompt.md +++ b/skills/dialogue/world-simulator/modules/aesthetics-interaction/prompt.md @@ -1,7 +1,8 @@ # 美学纲领与交互范式 -> 能力标准形范例。程序只切割下方 **fence 块**(`meta` / `opening` / `task` / …);`##` 标题仅供人读。 -> 写法说明:`docs/world-simulator-modules.md`。 +> **技能标准形范例**(其它中段技能对齐本结构)。 +> 程序只切割下方 **fence 块**(`meta` / `opening` / `task` / …);`##` 标题仅供人读。 +> 写法:`docs/world-simulator-modules.md`;产物公共头:`docs/context-fragment-design.md`。 ## meta @@ -14,15 +15,16 @@ declaration: > 询问相近故合为一步,勿拆成「交互」「美学」两步 when: | 可玩/可沉浸体验,但站位、呈现、核心感觉或轮转边界仍不清时; - 世界模拟类导演通常作为剧本第一步(无上游依赖)。 + 世界模拟类配方通常作为工作流计划第一步(无上游依赖)。 已有等价定稿且用户未要求重谈 → 不要重复排入。 when_not: | 只改实现细节且体验契约已验收;纯工具/排版;与「给玩家什么体验」无关。 boundary: | - 本能力:体验是什么、怎么发生在用户身上、轮转怎么停。 - 叙事指南:世界/助手态度口径(契约已写清的勿重复问卷)。 - 世界蓝图 / 具体实例 / 生成规则:世界内容与可执行规则——本步不定。 - 实现机制 / 拓扑 / 变量* / 状态栏 / 回复格式 / Worker 规格:落到可运行结构——本步不定。 + 本技能:体验是什么、怎么发生在用户身上、轮转怎么停;产出 context-fragment.v1。 + 叙事指南与故事推进:把感觉落成遣词/笔墨/禁忌与推进——本步只钉感觉与轮转。 + 舞台骨架 / 具体实例 / 生成规则:舞台与可执行规则——本步不定。 + 实现机制 / 变量* / 监控栏 / 正文组成 / 游玩拓扑 / 上下文投影排序 / 细化终稿:落到可运行结构——本步不定。 +feeds: design_only ``` ## opening @@ -48,7 +50,8 @@ boundary: | ```task 与用户一起钉清一份「体验契约」:给玩家什么感受、如何参与、正文如何呈现、何时等待用户。 -写入产物 tag(通常为 设计.美学纲领与交互范式)。 +产物必须是 **context-fragment.v1** JSON,写入 tag「设计.美学纲领与交互范式」。 +业务字段全部放在「正文」内;须含 schema / 技能 / brief;建议填 mount 与稳变(本步不写数字 order)。 一次 design-step、一场对话、一次验收。参与方式与美学内容在同一步内交叉追问,不要先交「半份交互」再开「半份美学」。 @@ -65,6 +68,7 @@ boundary: | 5. 用户常说不清想要什么 → 展示优于提问、大胆优于保守、去道德化/常态化、具体化;负面信号同样有效。 6. 变造世界:先抓「原型 + 变在哪里」;核心体验往往来自变种,不要写成设定百科。 7. 同一设定不同用户要的东西可以完全不同(角斗士:荣耀 / 自由 / 血与酒)——找到核心与禁忌,不是补全世界观。 +8. 本步是上下文片段,不是执行单元清单;禁止输出 Worker 列表或最终插入 order。 ``` ## probe @@ -96,68 +100,136 @@ boundary: | ```output { - "brief": "一句话:用户要的核心体验", - "变造": { - "原型": "…", - "变点": "…" + "schema": "context-fragment.v1", + "技能": "美学纲领与交互范式", + "brief": "一句话点题:用户最想反复感受到什么", + "mount": ["world-simulator", "narrator"], + "稳变": "stable", + "正文": { + "设定逻辑": { + "变造": { "原型": "…", "变点": "…" }, + "参与方式": { + "用户与user关系": { "结论": "…", "完备度": "…%", "依据": "…" }, + "焦点位置": { "结论": "…", "完备度": "…%", "依据": "…" }, + "满足来源": { "结论": "…", "完备度": "…%", "依据": "…" } + }, + "内容维度": { + "核心感觉": { "完备度": "…%", "已知": "…", "待探": "无|…" }, + "人物与关系": { "完备度": "…%", "已知": "…", "待探": "无|…" }, + "身体与感官": { "完备度": "…%", "已知": "…", "待探": "无|…" }, + "背景与规则": { "完备度": "…%", "已知": "…", "待探": "无|…" }, + "意义与主题": { "完备度": "…%", "已知": "…", "待探": "无|…" } + }, + "特殊概念": "无|短列", + "区域化诊断": { + "是否需要": false, + "判断依据": "…", + "区域识别": [], + "边界类型": "无|空间|阶段|…", + "转换机制": "…" + }, + "复杂性识别": { + "内容复杂性": "简单|可结构化复杂|…", + "体验者位置复杂性": "简单|…", + "判断依据": "…" + }, + "完备度判断": { + "核心状态": "核心明确|仍混沌|…", + "判断依据": "…", + "薄弱点": "…" + } + }, + "交互范式": { + "前置配置": { + "用户与user关系": "…", + "人称": "第二人称称「你」|…", + "元叙事": "不得跳出故事直接问卷;决策用情节呈现|…" + }, + "叙事视角": { + "主要视角": "…", + "视角切换": "禁止|例外…", + "可写内心": "仅 user|…", + "不呈现的信息": "…" + }, + "描写权限": { + "思考与抉择": "何时可写 / 何时必须停等用户", + "言语": "…", + "主动行动": "…", + "情绪与身体反应": "…", + "习惯与既有技能": "…", + "外在状态": "…", + "用户输入后": "只确认 / 补细节 / 连带后果…" + }, + "后果与叙事控制": { + "行为后果判定": "…", + "世界对user的影响": "…", + "时间与场景": "…", + "引入新内容": "…", + "设计困境": "…" + }, + "区域差异": "无|按区域写权限差异(与设定逻辑.区域化诊断对齐)" + }, + "美学纲领": { + "体验内核": "给用户读的纲领正文(可多段;写清要反复感到什么、权力/关系如何成立)", + "呈现要点": { + "氛围基调": "…", + "边界与尺度": "色情/暴力/血腥等:服务体验的用法与禁忌", + "其它要点": ["权力日常化|反差|…"] + } + } }, - "参与": { - "站位": "…", - "代入": "完全|部分|操控|旁观|…", - "用户与user": "情感距离 / 控制期待 / 多角色", - "输入解释": "主视角行动 vs 世界行动等", - "焦点": "主焦点;次要(若有)", - "满足来源": "…" + "自评": { + "维度": [ + { "名": "交互范式", "分数": 0, "说明": "权限边界是否锁住核心体验" }, + { "名": "美学纲领", "分数": 0, "说明": "体验内核是否点题、有质感" }, + { "名": "整体协调", "分数": 0, "说明": "范式是否在保护纲领" } + ], + "薄弱点": "一句话" }, - "呈现": { - "正文人称或体裁": "…", - "系统扮演": "世界执行者 / …", - "输出形态": "单段叙事 / …" + "追问": { + "导语": "为贴近你要的质感,还需确认:", + "题目": [ + { + "问": "…", + "建议选项": ["选项A(可带短场景)", "选项B", "其它(请写明)"], + "示例": "可选:一句示范句或短钩子" + } + ] }, - "体验": { - "内核": "最想反复感受到的…", - "形状": "过程|张力|层次|构成|循环|…", - "呈现要点": ["…"], - "边界禁忌": ["…"] - }, - "内容摘要": { - "核心感觉": "…", - "人物与关系": "…", - "身体与感官": "…", - "背景与规则": "…", - "意义与主题": "…" - }, - "轮转": { - "思考与抉择": "何时等、怎么等、例外、输入后怎么补", - "言语": "…" - } + "开放问题": [] } ``` -未知字段省略或写「未定」,禁止编造。summary:`美学纲领与交互范式 · …`(点题核心体验)。 +硬规则(公共三段 + 本技正文): +1. 合法 JSON;必含 `schema` / `技能` / `brief` / `正文` / `自评` / `追问`(题目可 `[]`)。 +2. **公共三段**:`正文`=产物主体;`自评`=完备度打分;`追问`=导语+题目(建议选项/示例)。禁止用散文代替这三段。 +3. **本技正文三块**:`设定逻辑`(参与/内容维度/区域/复杂性/完备度)+ `交互范式` + `美学纲领`。未知省略或「未定」,禁止编造百科。 +4. `mount` 建议挂主世界层+转述;**禁止**写数字 `order`。`稳变` 定稿后多为 `stable`。 +5. `追问.题目` 一次宜 1~3 题;有建议选项;允许「其它」。已答清的不要再问。 +6. summary:`美学纲领与交互范式 · {brief 缩略}`。 ## checklist ```checklist -- [ ] 变造:原型与变点是否清楚(或已标未定)? -- [ ] 代入与否 / 站位是否清楚? -- [ ] 最想反复感受到的核心是否一句话能说清? -- [ ] 体验内核 + 呈现要点 + 边界禁忌是否成套(可短,不可互相矛盾)? -- [ ] 重大抉择与言语的等待边界是否够下游引用? -- [ ] 有没有为「完整」强行填充的设定百科? -- [ ] 有没有完善着偏离用户原话里的核心? -- [ ] 是否误写成 Worker 列表? +- [ ] 含 schema / brief / 正文 / 自评 / 追问 三段外壳? +- [ ] 正文含设定逻辑 + 交互范式 + 美学纲领?自评有交互/美学/协调维度? +- [ ] 未写 order、未输出执行单元列表? +- [ ] brief 能点题核心体验?变造/代入/满足来源清楚或已标未定? +- [ ] 交互范式权限与美学纲领互相保护、不矛盾? +- [ ] 追问只打薄弱点,带建议选项? +- [ ] 有没有为「完整」强行填充百科或偏离用户核心? ``` ## examples ```examples 好: -- 「丧尸世界,变点是只有我不会被感染;完全代入;最想感到特权下的求生紧张与道德压力。」 -- 用户要艰苦角斗 → 禁忌写「轻松连胜」;选项带短场景。 +- brief 点题「垄断生存资源的绝对掌控感」;正文三块齐(设定逻辑含完备度/待探、交互范式锁权限、美学纲领写体验内核);自评打分;追问带建议选项打薄弱点。 +- 内容质感可参考「权力孤岛」类长描写,但必须落在上述 JSON 键里,不要输出 XML/自定义标签壳。 坏: -- 「现代都市,有公司有地铁…」(百科,无变点、无核心感受) -- 用户要爽文角斗,仍按真实艰苦写满受苦 -- 「你想要什么体验?」抽象难答;或一张表全勾选 +- 只有散文纲领,无 JSON / 无自评 / 无追问结构 +- 百科式背景堆砌,无满足来源与权限边界 +- 输出 workers[] 或 order 表 +- 追问空洞「你想要什么体验?」无选项 ``` diff --git a/skills/dialogue/world-simulator/modules/catalog.yaml b/skills/dialogue/world-simulator/modules/catalog.yaml index b101037..70056ab 100644 --- a/skills/dialogue/world-simulator/modules/catalog.yaml +++ b/skills/dialogue/world-simulator/modules/catalog.yaml @@ -29,19 +29,18 @@ modules: # —— 世界与内容 —— - id: world-blueprint - name: 世界蓝图与人文地理 + name: 舞台骨架 declaration: >- - 当体验需要可引用的「舞台背景」时使用:钉舞台尺度、熟悉基底与变造、 - 关键格局与人文地理;只细化舞台上会出现的部分,不做设定百科或纸面地图 - artifact: 设计.世界蓝图与人文地理 + 体验需要可引用舞台时:钉尺度、基底与变造、关键舞台区,以及社会结构与世界状况; + 只细化会上台部分,不做地图百科 + artifact: 设计.舞台骨架 - id: generation-rules name: 生成规则 repeatable: true declaration: >- - 仅当某类内容既对游玩重要、又无法凭世界基底稳定生成时使用: - 定义严格 schema、生成约束与仅动态/仅预生成/预生成并动态三种生命周期; - 可按生成对象反复调用 + 仅当某类内容既重要又无法凭基底稳定生成时:写出规则 + (怎么描写/生成 + 产物格式 + 可选多池,池绑 JSON 字段);可按对象反复调用 artifact: 设计.生成规则 params: - key: target @@ -61,8 +60,7 @@ modules: name: 具体实例 repeatable: true declaration: >- - 执行一条要求预生成的生成规则,直接产出符合其 schema 与约束的具体实例; - 仅动态规则不调用,可按 rule_id 或批次反复调用 + 按预生成规则原样产出实际产物;不设计、不改合同;可按 rule_id/批次反复调用 artifact: 设计.具体实例 params: - key: rule_id @@ -79,16 +77,18 @@ modules: hint: 数字或「按规则」;contextual 时写清规模依据 - id: narrative - name: 叙事指南 - declaration: 世界/助手态度与体验边界(不等于文风;契约已写清的态度勿重复问卷) - artifact: 设计.叙事指南 + name: 叙事指南与故事推进 + declaration: >- + 钉转述写法与故事推进:纲领、遣词造句、笔墨焦点、禁忌/不偏好、示范与备用、 + 推进决策(可含变量钩子);美学写感觉,本步写怎么写、往哪推 + artifact: 设计.叙事指南与故事推进 # —— 机制与数据 —— - id: mechanism name: 实现机制 declaration: >- - 当核心体验已经明确,需要识别由哪些角色、世界、关系、规则或情境切面支撑, - 并把体验落成可供后续设定工作的核心支点时使用 + 核心体验已明时:识别承重切面(如何支撑 / 缺失后果),产出可挂主世界层的上下文片段; + 不重定体验、不写百科或执行单元 artifact: 设计.实现机制 - id: topology @@ -98,33 +98,60 @@ modules: - id: variable-design name: 变量设计与更新规则 - declaration: 钉变量字段、初值与更新时机/规则;与表副作用对齐 + declaration: >- + 钉真值字段、写入源(执行单元/用户/程序)、更新规则,以及按真值门控的 + side_effects(Progressive 投影);与运行规格 tables 对齐 artifact: 设计.变量设计与更新规则 - id: variable-context name: 变量控制上下文 - declaration: 钉哪些变量如何挂进常驻上下文 / 控制生成口径(可见性与措辞) + declaration: >- + 钉真值与投影如何挂进主世界层等上下文:可见性、措辞、未解锁不注入 artifact: 设计.变量控制上下文 # —— 呈现 —— - id: status-bar - name: 设计状态栏 - declaration: 钉用户可见状态栏:字段、刷新时机、与正文如何拼装 - artifact: 设计.状态栏 + name: 设计监控栏 + declaration: >- + 钉用户可见监控栏字段:只留会变、影响决策或沉浸的信息 + (主角/攻略目标/场景交互物等);旧称设计状态栏 + artifact: 设计.监控栏 - id: reply-format - name: 设计回复格式 - declaration: 钉单轮可见输出的结构(正文块、面板、拼接顺序),服务体验契约 - artifact: 设计.回复格式 + name: 正文组成 + declaration: >- + 钉用户可见的一轮回复由哪些块、何顺序组成(含监控栏位置、隐藏变量段、前端拆分); + 不是程序报文格式;旧称设计回复格式 + artifact: 设计.正文组成 + + # —— 开局(创作偏晚) —— + - id: opening-setup + name: 开场白与开场变量 + declaration: >- + 写出遵循正文组成/叙事等契约的开场一小段,并钉同真相的开场变量初值; + 开场为主、填表为辅;可与 opening-generator 落库 + artifact: 设计.开场白与开场变量 # —— 收成 / 钉声明(共用;世界模拟器按需) —— - id: worker-spec - name: Worker 规格 - repeatable: true - declaration: 钉一个游玩期执行单元(职责、读写、挂载);多演员时可多次调用 + name: 游玩拓扑 + repeatable: false + declaration: >- + 勾选固定游玩槽位(主世界层/叙事转述/可选角色视角/可选机遇裁定,或写手大纲+章节); + 禁止自由发明执行单元 artifact: 设计.worker规格 + - id: context-order + name: 上下文投影排序 + repeatable: false + declaration: >- + 根据各上游上下文概括,为启用槽写出扁平投影序(含对话.历史与 projection); + 挂谁已定、排第几在本步;不发明执行单元、不重写长文 + artifact: 设计.上下文投影排序 + - id: refine name: 细化终稿 - declaration: 钉死关键前提、表与副作用,收成可进游玩的规格 + declaration: >- + 按已勾选固定槽与投影排序表收成运行规格(play_slots、常驻、表副作用); + 不发明新执行单元 artifact: 设计.worker集 diff --git a/skills/dialogue/world-simulator/modules/concrete-instances/prompt.md b/skills/dialogue/world-simulator/modules/concrete-instances/prompt.md index abe01c1..22d95e7 100644 --- a/skills/dialogue/world-simulator/modules/concrete-instances/prompt.md +++ b/skills/dialogue/world-simulator/modules/concrete-instances/prompt.md @@ -1,7 +1,8 @@ # 具体实例 -> 能力文档。程序只切割下方 **fence 块**;`##` 标题仅供人读。 -> 写法说明:`docs/world-simulator-modules.md`;范例:`aesthetics-interaction`。 +> 技能文档。程序只切割下方 **fence 块**;`##` 标题仅供人读。 +> 产物外壳:`docs/context-fragment-design.md`。 +> 本步极简:严格按已验收的「生成规则」执行,交出实际产物。 ## meta @@ -10,100 +11,114 @@ name: 具体实例 id: concrete-instances artifact: 设计.具体实例 declaration: > - 执行一条要求预生成的生成规则,直接产出符合该规则的具体实例; - 可按 rule_id 或批次反复调用 + 按一条预生成规则原样产出实际产物(单层 JSON records);可按 rule_id/批次反复调用; + 本步不设计、不改合同 when: | - 「设计.生成规则」中已有生命周期为 `seed_only` 或 `seed_and_runtime` 的规则, - 现在需要按该规则生成实际内容。 + 「设计.生成规则」中已有 `seed_only` 或 `seed_and_runtime` 的规则,需要生成实际内容。 when_not: | - 规则生命周期为 `runtime_only` → 留到实际游玩中生成,本步不预生成。 - 没有对应生成规则,或规则缺少执行所需信息 → 返回「生成规则」补齐。 - 想修改 schema、约束或生命周期 → 返回「生成规则」,本步不改合同。 + `runtime_only` → 游玩期再生成。 + 无规则 / 规则不完整 / 想改格式或方法 → 回「生成规则」。 boundary: | - 本能力只做一件事:读取指定生成规则,按规则规定的数量、schema、生成步骤与约束, - 生成可直接使用的实例。它不重新论证规则、不修改规则,也不扩写其它设计。 + 本技能只做一件事:按指定规则生成 records。规则说怎么写、什么格式、如何用池,就照做。 + 生成规则:合同制定方——本步不重论证、不修改。 +feeds: gm +``` + +## opening + +```opening +规则已由【本步参数】rule_id 钉死。有批次目标或数量偏好可一句带过;否则直接「按规则生成」。 ``` ## task ```task -你正在执行剧本中的「具体实例」步骤。产物写入「设计.具体实例」。 +你正在执行「具体实例」。产物必须是 **context-fragment.v1** JSON,写入「设计.具体实例」。 -本步只是「生成规则」的执行器:根据规则生成具体实例,不做第二轮设计。 -要执行的 `rule_id` 已在【本步参数】;禁止再问用户要生成什么或重新讨论规则。 +本步 = 规则执行器。禁止重议对象族、生命周期、schema 或描写方法。 执行顺序: -1. 从「设计.生成规则」找到 `params.rule_id` 对应规则。不存在、不是预生成生命周期或缺少执行所需信息时,停止并准确指出缺口。 -2. 确定本批数量:`params.count` 有明确数字时采用;否则完全按规则的「数量」决定。`params.batch_goal` 若有,只作为不违反规则的本批筛选条件。 -3. 按规则的「生成依据」和「生成步骤」生成实例。`records` 只使用规则 schema 声明的字段,填满所有必填字段,并遵守类型、枚举、边界、嵌套结构、硬约束、字段间约束、变化维度、禁止项和去重规则。 -4. 逐条按规则的「校验」检查;不合格的实例直接重生成,不把错误项或设计过程写进产物。 -5. 若已有「设计.具体实例」,按 `rule_id` 追加新批次;除非用户明确要求替换,不改动旧批次。 -6. 输出符合 output 的 JSON。 +1. 取出 `params.rule_id` 对应规则;非预生成生命周期或缺信息 → 停止并指出缺口。 +2. 数量:`params.count` 有数字用数字,否则用规则「数量」;`batch_goal` 只作不违反规则的筛选。 +3. 完全按规则的「生成与描写」+「产物格式」+「池」生成 `records`(单层 JSON;键=schema 顶层字段)。 +4. 按规则自检;不合格内部重做。已有产物则追加批次(除非用户要求替换)。 +5. 输出 JSON;追问通常 `[]`。 -若程序已发出默认问题:用户首答在「用户.worker答复」。禁止重复同一开场。 - -summary:`具体实例 · {生成对象} · {本批数量}条` +summary:`具体实例 · {对象} · {n}条` ``` ## principles ```principles -1. 规则说什么就生成什么:不补字段、不改边界、不新增规则。 -2. 只交实例:`records` 放最终可用数据,不放 schema、解释、草稿或占位符。 -3. 先生成后自检:不合格项内部重做,只交通过规则校验的结果。 -4. 保持差异:在规则允许的变化维度内避免重复,不用变化破坏共同约束。 +1. 只按规则来:方法、格式、池、数量、约束——规则有什么用什么;没有的不发明。 +2. 只交 records:单层 JSON;不交解释、草稿、schema 副本、整池粘贴。 +3. 要改合同 → 回「生成规则」,本步不加字段、不改枚举。 ``` ## probe ```probe -通常不追问,直接按规则生成。 -仅当规则使用 contextual 数量而现有参数和依赖无法算出数量时,询问缺失的地区、阶段或规模。 -若用户要求 schema 外内容,提示返回「生成规则」修改合同;不要在本步临时加字段。 +通常不追问。仅当 contextual 数量算不出时问一句规模依据。 +用户要 schema 外内容 → 提示回「生成规则」。 ``` ## output ```output { - "batches": [ - { - "batch_id": "稳定的英文 kebab-case id", - "rule_id": "来源生成规则 id", - "对象": "本批生成的内容类型", - "数量": 3, - "批次条件": ["本批额外筛选条件;没有则为空"], - "records": [ - { - "这里直接使用来源规则的实际字段": "不得使用通用人物/地点/物件名片代替" - } - ] - } - ], - "增量说明": "本次新增或替换了哪个 batch_id" + "schema": "context-fragment.v1", + "技能": "具体实例", + "brief": "rule_id + 本批条数", + "mount": ["world-simulator"], + "稳变": "stable", + "正文": { + "rule_id": "与 params 一致", + "本批数量": 3, + "批次条件": ["params.batch_goal 等;无则 []"], + "batch_id": "英文 kebab-case", + "records": [ + { + "键来自规则产物格式 schema": "值" + } + ], + "增量说明": "新建|追加|替换某 batch_id" + }, + "自评": { + "维度": [ + { + "名": "合规模", + "分数": 0, + "说明": "是否严格按该规则的方法、单层格式、池与约束生成" + } + ], + "薄弱点": "一句话;无则空字符串" + }, + "追问": { + "导语": "", + "题目": [] + }, + "开放问题": [] } - -`records` 的示意键必须被实际规则 schema 完整替换。不得把 schema 定义复制进 `records`。 ``` +硬规则: +1. 合法 JSON;含 `schema` / `技能` / `brief` / `正文` / `自评` / `追问`。 +2. 正文键固定:`rule_id` / `本批数量` / `批次条件` / `batch_id` / `records` / `增量说明`。 +3. `records` 每条必须是规则约定的单层 JSON;`本批数量 === records.length`。 +4. 自评仅「合规模」一维。禁止改规则、写 `order`、输出执行单元列表。 +5. summary:`具体实例 · {对象} · {n}条`。 + ## checklist ```checklist -- [ ] 是否执行了 params.rule_id 指向的预生成规则? -- [ ] 数量是否来自 params.count 或规则本身? -- [ ] records 是否完整符合 schema 及全部约束? -- [ ] 是否只输出最终实例,没有混入规则解释或校验过程? -- [ ] 是否保留未要求替换的已有批次? +- [ ] 按 params.rule_id 的预生成规则执行? +- [ ] records 单层、键值符合产物格式与全部约束? +- [ ] 自评为合规模?未改合同? ``` ## examples ```examples -好: -- `heroine-design` 要求 1 条:直接生成 1 条完整女主记录,字段和值全部服从规则。 -- `major-npc` 要求 5 条:直接生成 5 名互不重复、满足阵营与地区约束的重要 NPC。 - -坏: -- 无视 schema,给实例补上“背景故事”等未声明字段。 -- 输出一段生成思路和校验报告,却没有直接给可用实例。 +好:规则要 3 条怪物 → 交出 3 条单层 record,字段与池用法全服从规则。 +坏:加规则没有的字段;输出一段「我是怎么想的」却没有 records;本步改 schema。 ``` diff --git a/skills/dialogue/world-simulator/modules/context-order/prompt.md b/skills/dialogue/world-simulator/modules/context-order/prompt.md new file mode 100644 index 0000000..aef944c --- /dev/null +++ b/skills/dialogue/world-simulator/modules/context-order/prompt.md @@ -0,0 +1,129 @@ +# 上下文投影排序 + +> 技能文档。程序只切割下方 **fence 块**;`##` 标题仅供人读。 +> **本步只排出扁平投影序,不发明执行单元,不重写设定长文。** +> 「对话.历史」也是可投影标签(改 projection 裁剪长度);见 `docs/context-fragment-design.md`。 + +## meta + +```meta +name: 上下文投影排序 +id: context-order +artifact: 设计.上下文投影排序 +declaration: > + 根据各上游上下文的概括,为每个启用槽写出扁平投影序 + (含对话.历史标签与 projection);不发明槽、不重写正文 +when: | + 游玩槽已确认(或可按默认),且主要上下文块已有可摘要产物, + 需要决定插入顺序与投影级别、再收成运行规格时。 +when_not: | + 体验/站位仍混沌 → 先美学纲领与交互范式。 + 槽位未定且用户拒绝默认 → 先游玩拓扑。 + 上游上下文仍大量空洞、无法判断稳/变 → 先补缺口,勿空排。 + 只需勾选槽 → 游玩拓扑;只需合并终稿 → 细化终稿。 +boundary: | + 本技能:读各块概括 → 产出每槽 inserts[](order / ref / projection / 可选 note);须含对话.历史。 + 无「历史前/后」硬分区;对话.历史本身是可排序的一项。 + 游玩拓扑:槽位权威;本步不增白名单外 ref(含按需 chance 不进每轮序)。 + 中段技能:可带 mount 与稳/变信号,最终 order 以本步为准。 + 细化终稿:把本步排序表收进运行规格 context_order;本步不输出完整 设计.worker集。 +feeds: design_only +``` + +## opening + +```opening +准备排出各游玩槽的扁平投影序(只排序,不改设定正文)。 + +请确认或补充: +1. 启用槽是否仍是默认?(世界模拟:主世界层 + 叙事转述;角色视角默认关) +2. 「对话.历史」希望夹在哪、投影多狠?(默认 summary) +3. 有没有「几乎永不改」或「每轮必变」的块要特别强调? + +若前面产物够用,可直接回复「按已有概括排序」。 +``` + +## task + +```task +你正在执行「上下文投影排序」。产物写入「设计.上下文投影排序」。 + +输入:已启用槽;各上游上下文的短摘要(及 mount / 稳变信号,若有)。 +输出:按槽分组的格式化排序表——Runtime/终稿据此把投影插到固定位置。 + +规则: +1. 禁止发明执行单元或白名单外 ref。 +2. 禁止重写上游长文;只写 order、ref、projection 与一句 note(勿再写硬锚点分区)。 +3. 每槽扁平数字序:order 0=该槽固定人设;其后按「稳→对话.历史→易变」软排。 +4. 必须包含一项 ref=`对话.历史`(默认 projection=summary);历史由程序维护,本步只决定夹在哪、投影多狠。 +5. 常见块名用新 artifact:设计.舞台骨架 / 设计.叙事指南与故事推进 / 设计.正文组成 / 设计.监控栏 等。 +6. 越稳越靠前;本轮输入/真值/裁决通常在历史之后。 +7. 同一内容可挂多槽;chance 为按需槽,不必排进每轮 inserts。 +8. summary:如 `投影排序 · 主世界层 N 条 · 含历史`。 + +若程序已发默认问题:禁止重复同一开场;在首答上产出表。 +``` + +## principles + +```principles +1. 晚排序:此时才能区分常变与重要;勿回退去发明新槽。 +2. 概括驱动:凭摘要与稳变信号排序,不把百科全文再读一遍当创作。 +3. 历史是标签:用投影裁剪,不要假装「整段历史永远在中间锚点」。 +4. 缓存优先:少变块靠前;高频变块靠后。 +5. 投影分级:full / summary / fields / fixed——历史默认 summary。 +6. 可修订已有排序表;用户改稳变判断时只改序,不改上游产物 id。 +``` + +## probe + +```probe +一次 askUser 1~2 点,优先 options: +- 某块稳/变判断冲突时,问用户更偏「少改利缓存」还是「每轮必须看见」。 +- 角色视角若开启,问哪些块禁止进主世界层。 +已齐则直接产出,勿为凑问题而问。 +``` + +## output + +```output +{ + "schema": "context-order.v1", + "brief": "一句话:本局扁平投影序(含对话.历史)", + "play_slots": { "gm": true, "narrator": true, "perspective": false }, + "slots": [ + { + "ref": "world-simulator", + "label": "主世界层", + "inserts": [ + { "order": 0, "ref": "worker.persona", "projection": "fixed", "note": "槽位人设" }, + { "order": 1, "ref": "设计.实现机制", "projection": "summary", "note": "承重规则摘要" }, + { "order": 2, "ref": "对话.历史", "projection": "summary", "note": "按投影裁剪" }, + { "order": 3, "ref": "变量.当前", "projection": "fields", "note": "真值" }, + { "order": 4, "ref": "用户.最新输入", "projection": "full", "note": "本轮" } + ] + }, + { + "ref": "narrator", + "label": "叙事转述", + "inserts": [ + { "order": 0, "ref": "worker.persona", "projection": "fixed", "note": "转述人设" }, + { "order": 1, "ref": "设计.叙事指南与故事推进", "projection": "summary" }, + { "order": 2, "ref": "对话.历史", "projection": "summary" }, + { "order": 3, "ref": "运行.本轮.裁决", "projection": "full", "note": "settlement" } + ] + } + ] +} +``` + +## checklist + +```checklist +- [ ] 未发明白名单外执行单元 +- [ ] 未重写上游长文,只有排序/投影 +- [ ] 每启用槽有 order 0 人设位 +- [ ] 每槽含「对话.历史」且有合理 projection +- [ ] 少变块整体靠前、本轮/真值靠后(软规则) +- [ ] 与游玩拓扑勾选一致 +``` diff --git a/skills/dialogue/world-simulator/modules/generation-rules/prompt.md b/skills/dialogue/world-simulator/modules/generation-rules/prompt.md index 65f61f2..7090b14 100644 --- a/skills/dialogue/world-simulator/modules/generation-rules/prompt.md +++ b/skills/dialogue/world-simulator/modules/generation-rules/prompt.md @@ -1,7 +1,8 @@ # 生成规则 -> 能力文档。程序只切割下方 **fence 块**;`##` 标题仅供人读。 -> 写法说明:`docs/world-simulator-modules.md`;范例:`aesthetics-interaction`。 +> 技能文档。程序只切割下方 **fence 块**;`##` 标题仅供人读。 +> 产物外壳:`docs/context-fragment-design.md`;范例:`aesthetics-interaction` / `mechanism`。 +> 正文高度自定义(描写文案、schema 字段、池条目随对象族而变);外壳与规则键必须严。 ## meta @@ -10,183 +11,254 @@ name: 生成规则 id: generation-rules artifact: 设计.生成规则 declaration: > - 仅为重要且模型无法仅凭世界基底稳定生成的同类内容,定义可执行的生成规则、 - 严格数据格式与生命周期;可按生成对象反复调用 + 仅当某类内容既重要又无法凭基底稳定生成时:为该对象族写出一条可执行规则 + (怎么描写/生成 + 产物格式 + 可选多池,池绑 JSON 字段);可按对象反复调用;本步不填最终实例 when: | - 上游体验、机制与世界基底已足以判断某类内容: - 1. 它对实际游玩非常重要;并且 - 2. 模型仅凭现有上下文无法稳定产出符合期待、彼此一致且可供下游使用的结果。 - 两项同时成立时,才为该类对象建立专门生成规则。 + 上游体验与世界基底已足以判断某类内容同时满足: + 1. 对游玩重要;2. 仅凭现有上下文无法稳定产出合格、一致、可供下游使用的结果。 when_not: | - 内容虽重要,但凭世界基底与常识即可稳定生成 → 不调用。 - 内容难生成,但只是装饰、偶尔出现或删掉不影响核心体验 → 不调用。 - 只想预先写一个普通人/地/物,且不需要先建立专门格式与生成约束 → 不调用。 - 只需定义变量在游玩中如何更新 → 变量设计与更新规则。 - 只需定义剧情、章节或世界如何推进 → 交给对应 Worker 或其它推进能力,不借本步泛化。 + 重要但常识/世界基底已够 → 不建规则。 + 难生成但不影响核心体验 → 不建规则。 + 只要预写最终记录、且不需专门规则 → 通常不必本步。 + 变量如何增减 → 「变量设计与更新规则」。 + 剧情如何推进 → 不借本步泛化。 boundary: | - 本能力:为“一类格式相近的内容”定义生成合同,包括生成依据、字段 schema、 - 枚举值、数值上下界、约束、随机维度、校验方式与规则生命周期。 - 生成对象不限于人物,也可为怪物、装备、任务、职业、组织、事件等。 - 具体实例:执行本步中要求预生成的规则,产出符合合同的实际记录;本步不填实例数据。 - 世界蓝图与人文地理:提供可直接依赖的世界基底;本步不重写世界百科。 - 实现机制:说明某类内容为何支撑体验;本步先验证其必要性,再定义怎么生成。 - 变量设计与更新规则:定义游玩期状态如何变化;本步只约束实例生成时的数据类型与初始合法范围。 - Worker 规格 / 细化终稿:决定谁在游玩期执行、哪些产物进入常驻上下文;本步只声明所需生命周期。 + 本技能产出的是「规则」本身,一条规则包含: + (1)应该怎么描写、生成;(2)产物格式——单层 JSON(对接表格/旁观者/投影); + (3)可选多「池」——各挂一个顶层字段,供抽取/组装/枚举。 + 具体实例:按规则填最终 records;本步不写最终实例(池条目≠实例)。 + 舞台骨架 / 实现机制:可引用,不重写百科、不重做移除检验。 + 变量*:游玩期状态更新——本步只约束生成时的类型与合法初值范围。 +feeds: gm +``` + +## opening + +```opening +本轮生成对象已由编排参数钉死(见【本步参数】target)。请直接谈该类内容: + +1. 若不做专门规则、只靠世界设定临场写,最怕出现什么不可接受的结果? +2. 更需要:开局先钉一批、游玩中再随机补,还是只在游玩期动态生成? +3. 哪些产物字段需要「池」?(池挂在 JSON 字段上,如气质标签池→`气质`;可多个;没有就说没有) + +想到什么写什么;完整规则在正式产物里钉。 ``` ## task ```task -你正在执行剧本中的「生成规则」步骤。产物写入「设计.生成规则」。 +你正在执行「生成规则」。产物必须是 **context-fragment.v1** JSON,写入「设计.生成规则」。 -本步不是泛用写作建议,也不是“凡是重要就做一张表”。调用目标已由编排期写入【本步参数】(至少含 target);禁止再问「这一步生成什么」。 +本步写出的最终物是**规则**,不是实例。一条规则必须说清两件事,并可附第三件: +1. **生成与描写**:应该怎么写、怎么生成(步骤、依据、语气/切面、约束)。 +2. **产物格式**:生成结果必须遵循的可校验格式(schema)。 +3. **池**(可选):每个池挂在产物格式里的某个字段(变量)上,供该字段抽取/组装/枚举; + 一条规则可多个池;无则 `池: []`。 + +对象族已由【本步参数】`target` 钉死;禁止再问「这一步生成什么」,禁止顺手扩到其它族。 执行顺序: -1. 读取【本步参数】与依赖产物。`target` 决定本轮唯一生成对象族;不要顺手扩展到其它类别。若缺必填参数,停止产出并提示返回 design-flow 补参。 -2. 执行双门槛判断: - - 重要性门槛:没有专门生成合同,核心玩法、关键关系、长期一致性或必要数据接口是否会明显受损? - - 不可替代门槛:模型是否无法依靠世界基底、常识和普通提示稳定生成合格结果? - 两项有任一不成立,输出“无需专门规则”的判断与理由,不为了完成步骤硬造 schema。 -3. 两项均成立时,从三种生命周期中选且只选一种(若 params.lifecycle_intent 已给且合理,优先采用;不合理则说明并 askUser): - - `runtime_only`:仅游玩期动态生成。规则进入后续上下文,不要求「具体实例」预生成。 - - `seed_only`:下一步由「具体实例」按规则生成;实例进入后续上下文,规则在完成生成与校验后丢弃。 - - `seed_and_runtime`:下一步先生成种子实例;实例与规则都进入后续上下文,游玩期仍可按同一规则继续生成。 -4. 建立真正可执行的数据合同: - - 明确顶层 JSON 形状、单批数量或数量决定方式、每个字段是否必填。 - - 每个字段固定类型。至少区分 `string`、`number`、`integer`、`boolean`、`enum`、`array`、`object`;不得只给示例值让下游猜类型。 - - `enum` 必须同时给出允许值及各值含义;`number/integer` 必须给上下界,并说明边界含义或单位;`array` 必须定义元素类型与数量范围;`object` 必须继续定义子字段。 - - 写清字段间约束、与世界基底的引用关系、禁止项、去重规则和一致性校验。 -5. 只有确实需要不可预测性时才定义随机维度;区分“允许变化的字段”和“不能随机破坏的约束”。随机不是省略设计。 -6. 若 params.rule_id 已给,优先用作本规则 id;否则生成稳定 kebab-case `rule_id`。若已有「设计.生成规则」,按 `rule_id` 增量更新或追加;不得无故重写其它对象的规则。 -7. 输出符合 output 的 JSON。规则本身不得夹带实际实例。 +1. 读 params 与依赖。缺 `target` → 停止并提示补参。 +2. 双门槛:重要性 + 基础上下文不足。任一项不成立 → 结论「无需专门规则」,`rules: []`。 +3. 两项成立时,生命周期三选一(params.lifecycle_intent 合理则采用): + - `runtime_only`:仅游玩期动态生成;不要求「具体实例」。 + - `seed_only`:由「具体实例」预生成;规则用完可丢。 + - `seed_and_runtime`:先种子实例;规则与实例都保留,游玩期仍可按同规则生成。 +4. 写满规则三块:生成与描写(可较长)+ 产物格式(**单层 JSON**:每条产物是一层键值,无嵌套对象)+ 池(需要才填;绑顶层字段)。 +5. 逐字段过筛:每个属性都要能回答「对本对象族是否必要」。冗余字段删掉(如女主规则里的「性别」);对象族必需字段不得漏(如 D&D 怪物的等级、攻击骰)。 +6. `rule_id`:params 已给则用;否则稳定 kebab-case。已有产物按 rule_id 增量。 +7. 按自评三维打分并填追问;输出 JSON。禁止夹带最终实例 records。 -若程序已发出默认问题:用户首答在「用户.worker答复」。禁止重复同一开场。只追问会改变必要性、生命周期或 schema 的信息。 - -summary: -- 建立规则:`生成规则 · {生成对象} · {仅动态|仅预生成|预生成并动态}` -- 不建立规则:`生成规则 · {生成对象} · 无需专门规则` +若程序已发默认问题:禁止重复开场;在首答上补洞。 +summary:建立 → `生成规则 · {对象} · {仅动态|仅预生成|预生成并动态}`;跳过 → `生成规则 · {对象} · 无需专门规则`。 ``` ## principles ```principles -1. 双门槛:必须“对游玩重要”且“基础上下文不足以稳定生成”同时成立。 -2. 反表格冲动:核心玩法是古董拍卖,不代表纯现代背景中的普通古董必须有专门规则;常识足够时直接生成。 -3. 一规则一对象族:怪物与装备若 schema、约束和用途不同,分次调用;不要做万能内容生成器。 -4. 格式也是规则:字段名、类型、枚举、上下界、嵌套结构与必填性都必须可校验。 -5. 生命周期先行:是否预生成、规则最终是否保留,必须在生成实例前决定。 -6. 规则服务差异:只固定影响体验和接口的字段;不要把所有可描写细节都结构化。 -7. 可追溯:每项关键约束应能指向世界基底、实现机制、体验禁忌或用户明确要求。 -8. 不混淆状态更新:好感度可在实例 schema 中规定为有界数字;何时增减、如何结算属于变量设计。 +1. 整条规则先问必要性:是否值得单独用「生成规则」承接?双门槛不成立则跳过,勿硬造表。 +2. 一调用一对象族;格式或用途不同则分次编排。 +3. 规则 = 描写/生成方法 + 产物格式;池是可选附属(可多池),不是必填第三设计。 +4. 属性逐个过筛:每个字段必须对本对象族有意义;禁塞通用名片字段,也禁漏掉玩法接口字段。 +5. 产物格式 = **单层 JSON**:一条产物是一层扁平对象;值仅为标量或「标量数组」。 + 禁止嵌套 object、禁止 object 数组。此格式要对齐动态表格列、旁观者→主世界层生成、以及投影取字段。 + 类型/必填/枚举/上下界必须可校验;禁止只给示例让下游猜类型。 +6. 池绑顶层字段名(如 `气质`、`攻击花招`);一条规则可多池。池条目是该字段材料,不是整条 record。 +7. 生命周期先行;未定前不假装可执行「具体实例」。 +8. 不写变量更新公式、不写剧情推进、不填最终实例。 ``` ## probe ```probe -一轮 askUser 1~2 点;优先问会改变结论的分叉。禁止重问【本步参数】已钉的 target / rule_id。 +每轮 1~2 点。禁止重问 params 已钉的 target / rule_id。 -必要性不明:如果不做专门规则、只让模型按世界设定直接生成,最可能出现的不可接受结果是什么? -生命周期不明且 params 未给:这些实例需要每局变化、固定为开局设定,还是固定一批后仍要继续随机补充? -枚举不明:这个字段只能从哪些世界内类别中选择?是否允许“其它”? -数值不明:上下界分别代表什么,生成时是否允许取到边界? -数量不明:需要固定数量,还是由场景/规模决定?允许范围是多少? - -若用户的答案证明双门槛不成立,直接建议跳过,不再追问 schema。 +优先:整条规则是否必要;某字段该不该进表(冗余 vs 漏接口);单层格式类型/枚举/边界;哪些字段要池。 +若双门槛不成立 → 建议跳过,停追问字段与格式。 +不追问:最终实例正文、变量增减公式、其它对象族、完整世界百科。 ``` ## output ```output { - "brief": "一句话:本轮判断与其服务的核心体验", - "必要性判断": { - "生成对象": "例:重要 NPC / 怪物 / 装备 / 任务 / 职业体系", - "对游玩的重要性": "没有专门规则会具体损失什么", - "基础生成的不足": "现有世界基底为何不足以稳定产出合格结果", - "结论": "建立专门规则|无需专门规则" - }, - "rules": [ - { - "rule_id": "稳定的英文 kebab-case id", - "对象": "该规则生成什么", - "生命周期": "runtime_only|seed_only|seed_and_runtime", - "上下文策略": { - "预生成实例": true, - "保留规则到游玩期": false, - "保留实例到游玩期": true - }, - "生成依据": ["引用的上游产物、世界事实或用户要求"], - "数量": { - "模式": "fixed|range|contextual", - "值": 1, - "最小": 1, - "最大": 1, - "决定规则": "contextual 时必填" - }, - "输出合同": { - "格式": "JSON", - "顶层": "array", - "schema": { - "字段名": { - "type": "string|number|integer|boolean|enum|array|object", - "required": true, - "description": "字段语义", - "allowed_values": [ - { "value": "枚举值", "meaning": "含义与适用边界" } - ], - "minimum": 0, - "maximum": 100, - "items": { "type": "string" }, - "min_items": 1, - "max_items": 3, - "properties": {} + "schema": "context-fragment.v1", + "技能": "生成规则", + "brief": "一句话:对象族 + 建立规则|无需专门规则 + 生命周期(若建立)+ 有池|无池", + "mount": ["world-simulator"], + "稳变": "stable", + "正文": { + "本步参数": { + "target": "与 params 一致", + "rule_id": "已钉或本步拟定;跳过时可空字符串", + "lifecycle_intent": "params 原值或空" + }, + "必要性判断": { + "生成对象": "本轮对象族", + "对游玩的重要性": "没有专门规则会损失什么", + "基础生成的不足": "现有上下文为何不够;若足够则写清", + "结论": "建立专门规则|无需专门规则" + }, + "rules": [ + { + "rule_id": "英文 kebab-case", + "对象": "该规则生成什么", + "生命周期": "runtime_only|seed_only|seed_and_runtime", + "上下文策略": { + "预生成实例": true, + "保留规则到游玩期": false, + "保留实例到游玩期": true + }, + "数量": { + "模式": "fixed|range|contextual", + "值": 1, + "最小": 1, + "最大": 1, + "决定规则": "contextual 时必填,可较长" + }, + "生成与描写": { + "依据": ["可较长;引用上游产物或用户要求"], + "方法": ["可较长:生成顺序、如何组装、描写口径、语气/切面、如何用池"], + "硬约束": ["任何产物不得违反"], + "字段间约束": ["例:阵营=A 时权限≥3"], + "变化维度": ["允许随机或变化的部分;无则 []"], + "禁止项": ["不得出现的组合或内容"], + "去重规则": ["同批/跨批"], + "校验": ["类型、枚举、边界、世界与体验一致性;以及是否按方法生成"] + }, + "产物格式": { + "格式": "单层JSON", + "批量时": "array of flat object(每条记录单层)|单条则一个 flat object", + "schema": { + "字段名": { + "type": "string|number|integer|boolean|enum|array", + "required": true, + "description": "语义 + 为何本对象族需要此字段;可较长", + "allowed_values": [ + { "value": "枚举值", "meaning": "含义" } + ], + "minimum": 0, + "maximum": 100, + "items": { "type": "string|number|integer|boolean|enum" }, + "min_items": 1, + "max_items": 3 + } + }, + "示例形状": { + "名称": "……", + "等级": 3, + "攻击骰": "2d6+2", + "气质": "阴郁" } - } + }, + "池": [ + { + "pool_id": "英文 kebab-case;同规则内唯一", + "名称": "人话名", + "绑定字段": "单层 schema 的顶层字段名,例:气质 / 攻击花招", + "用途": "灵感组装|随机抽取|枚举生成|其它", + "说明": "该字段如何用本池;与「生成与描写.方法」如何配合;可较长", + "条目": [ + { + "id": "可选稳定 id", + "内容": "材料正文或短标签;结构随池自定义,但同池宜一致", + "权重": 1, + "标签": ["可选分类标签"] + } + ], + "抽取或组装规则": "可较长;无特殊则写「均匀随机」或「按方法写入绑定字段」" + } + ] + } + ], + "增量说明": "相对已有规则新增或修改了什么;首条可写「新建」" + }, + "自评": { + "维度": [ + { + "名": "必要性", + "分数": 0, + "说明": "整条规则:是否有必要单独用「生成规则」承接该类内容;跳过时理由是否充分" }, - "生成步骤": ["按什么顺序决定字段,如何利用上游依据"], - "硬约束": ["任何实例都不得违反的条件"], - "字段间约束": ["例:阵营=A 时,权限等级不得低于 3"], - "变化维度": ["允许随机或变化的部分;不需要随机则为空"], - "禁止项": ["即使随机抽到也不得出现的组合或内容"], - "去重规则": ["同批及跨批如何避免同质化或重复"], - "校验": ["类型、枚举、边界、世界一致性与体验一致性检查"] - } - ], - "增量说明": "相对已有规则新增或修改了什么", - "开放问题": ["仅保留会阻止规则执行的问题"] + { + "名": "属性妥当", + "分数": 0, + "说明": "逐字段:有无冗余(如女主规则的性别)或缺失(如 D&D 怪物缺等级/攻击骰);跳过时写 N/A" + }, + { + "名": "格式准确", + "分数": 0, + "说明": "是否严格单层 JSON、类型/必填/枚举/边界可对接动态表格·旁观者生成·投影;跳过时写 N/A" + } + ], + "薄弱点": "一句话" + }, + "追问": { + "导语": "为钉准生成规则,还需确认:", + "题目": [ + { + "问": "…", + "建议选项": ["选项A", "选项B", "其它(请写明)"], + "示例": "可选" + } + ] + }, + "开放问题": [] } - -当结论为「无需专门规则」时,省略 `rules`,并在 `brief` 中说明应直接依赖哪些现有世界基底生成。 -schema 中只保留实际字段适用的类型属性:例如非枚举字段不写 `allowed_values`,非数字字段不写 `minimum/maximum`。 ``` +硬规则(公共三段 + 本技正文): +1. 合法 JSON;必含 `schema` / `技能` / `brief` / `正文` / `自评` / `追问`(题目可 `[]`)。 +2. **公共三段**:正文=规则主体;自评=必要性/属性妥当/格式准确;追问=导语+建议选项。 +3. **正文键固定**:本步参数、必要性判断、rules、增量说明。 +4. **每条 rule 必含**:生成与描写、产物格式;**池可空数组**。有池时 `绑定字段` 必须是单层 schema 的顶层键。 +5. **产物格式强制单层**:`schema` 的 `type` 不得为 `object`;`array` 的 `items` 不得为 object。禁止 `外貌.发色` 这类嵌套路径。 +6. 结论为「无需专门规则」时:`rules` 必须 `[]`。上下文策略布尔须与生命周期一致。 +7. 池条目≠最终实例;禁止输出具体实例 `records`、变量更新规则、执行单元列表。 +8. `mount` 默认主世界层;**禁止**写 `order`。summary 见 task。 + ## checklist ```checklist -- [ ] 重要性与基础生成不足是否分别举证,且两项都成立? -- [ ] 是否因为“玩法重要”就误给常识足够的内容建表? -- [ ] 生命周期是否在三种模式中明确唯一,布尔策略与之相符? -- [ ] 每个字段是否有明确类型、必填性和语义? -- [ ] enum 是否给全允许值与含义,数字是否给上下界,嵌套类型是否定义完整? -- [ ] 是否写清字段间约束、禁止项、去重与校验? -- [ ] 是否把实际实例、变量更新规则、剧情推进规则或 Worker 规格混入本步? -- [ ] 是否只覆盖本轮对象族,并保留其它已有 rule_id? +- [ ] 含 schema / brief / 正文 / 自评 / 追问? +- [ ] 自评为必要性 / 属性妥当 / 格式准确? +- [ ] 整条规则必要性有举证?不成立时 rules=[]? +- [ ] 建立时:逐字段无冗余无漏接口?产物为单层 JSON 且类型可校验? +- [ ] 池:无则 [];有则绑顶层字段、未写成最终实例表? +- [ ] 未夹带最终实例、变量公式、其它对象族、百科? ``` ## examples ```examples -应建立: -- 怪物是核心战斗资源,需要每局变化,且世界有独特生态与战斗接口;使用 `runtime_only`,固定属性 schema、生态枚举、数值边界与组合禁忌。 -- 单女主必须承载特定关系体验,直接写人设容易遗漏关键切面;使用 `seed_only`,下一步生成女主后丢弃规则。 -- 重要 NPC 需要开局已有一批,后续也会随地区开放继续出现;使用 `seed_and_runtime`。 - -不应建立: -- 纯现代都市拍卖玩法中的普通古董;即使古董很重要,只要现实常识与世界基底足以直接生成,就不值得专门维护规则。 -- 只在背景里出现一次的路边摊菜单;即使模型可能写得普通,也不影响核心体验。 +好: +- D&D 怪物:单层含 名称/等级/攻击骰/……;池可绑 `气质`;自评属性妥当高(未漏战斗接口)。 +- 女主规则:不设「性别」字段;关系切面进表;池可空。 +- 事件钩子:池绑顶层 `钩子`;方法写抽 1~2 条写入该字段。 +- 普通古董 → 无需专门规则(必要性维度说明跳过理由)。 坏: -- 只写“人物要立体、装备要有趣”,没有 JSON schema 与可校验约束。 -- 枚举字段写“阵营:字符串”,却不生成允许的阵营项。 -- 好感度写“0~100”,又在本步编写每轮加减公式,侵入变量更新能力。 +- 产物写成 `{ "外貌": { "发色": "…" } }` → 非单层,前端表格/投影难对接。 +- 女主规则塞「性别」;或怪物规则只有「名字+性格」没有等级/攻击骰。 +- 把写好的 NPC 整份塞进池当最终实例。 ``` diff --git a/skills/dialogue/world-simulator/modules/mechanism/prompt.md b/skills/dialogue/world-simulator/modules/mechanism/prompt.md index 19c6dac..0e05ecb 100644 --- a/skills/dialogue/world-simulator/modules/mechanism/prompt.md +++ b/skills/dialogue/world-simulator/modules/mechanism/prompt.md @@ -1,7 +1,7 @@ # 实现机制 -> 能力文档。程序只切割下方 **fence 块**(`meta` / `opening` / `task` / …);`##` 标题仅供人读。 -> 写法说明:`docs/world-simulator-modules.md`;泛用规范:`docs/briefs/capability-authoring-brief.md`。 +> 技能文档。程序只切割下方 **fence 块**;`##` 标题仅供人读。 +> 产物外壳对齐:`docs/context-fragment-design.md`;范例:`aesthetics-interaction`。 ## meta @@ -11,25 +11,22 @@ id: mechanism artifact: 设计.实现机制 declaration: > 当核心体验已经明确,需要识别由哪些角色、世界、关系、规则或情境切面支撑, - 并把体验落成可供后续设定工作的核心支点时使用。 + 并把体验落成可供后续设定工作的核心支点时使用 when: | 已有「美学纲领与交互范式」或等价的体验声明,但还不能回答: “这个体验具体靠什么存在?” “拿掉哪些东西后,核心感觉就会明显变质?” “哪些设定切面必须被后续世界、角色与规则设计继承?” when_not: | - 核心体验尚未明确时,不用本能力代替「美学纲领与交互范式」决定想要什么体验。 - 只需要补写完整世界、地点、组织、人物生平或具体事件时,交给「世界蓝图与人文地理」或「具体实例」。 - 需要定义内容如何持续生成、演化或响应时,交给「生成规则」。 - 需要定义变量、状态及更新条件时,交给「变量设计与更新规则」。 - 需要决定文本如何叙述、取舍和呈现时,交给「叙事指南」。 + 核心体验尚未明确 → 先「美学纲领与交互范式」。 + 只需补舞台骨架/实例 → 「舞台骨架」或「具体实例」。 + 需持续生成规则 → 「生成规则」;需真值门控 → 「变量设计与更新规则」。 + 只需叙述呈现与推进口径 → 「叙事指南与故事推进」。 boundary: | - 本能力:识别支撑核心体验的关键元素及其支撑切面,说明该切面是什么、如何支撑体验,以及缺失后会损失什么。 - 美学纲领与交互范式:决定用户要什么体验、以什么交互关系接近该体验;本能力不重定体验目标,也不重定谁能做什么。 - 世界蓝图与人文地理:把已确认的世界支点展开成完整环境、社会和人文结构;本能力只钉住其中承担体验功能的切面。 - 具体实例:把机制实例化为具体人物、地点、组织或事件;本能力不补齐实例的全部设定。 - 生成规则:定义内容在游玩中如何产生、变化和延续;本能力只指出需要被规则维护的支撑条件。 - 叙事指南:决定如何写出这些体验;本能力不规定文风、镜头、节奏或信息揭示方式。 + 本技能:识别支撑核心体验的关键切面(如何支撑 / 缺失后果);产出 context-fragment.v1。 + 美学纲领与交互范式:体验目标与轮转——本步不重定。 + 舞台骨架 / 具体实例 / 生成规则 / 变量* / 叙事指南与故事推进:展开环境、实例、生成、真值、写法——本步只钉承重切面。 +feeds: gm ``` ## opening @@ -40,35 +37,28 @@ boundary: | ## task ```task -你正在执行剧本中的「实现机制」步骤。本步产物写入「设计.实现机制」。 +你正在执行「实现机制」。产物必须是 **context-fragment.v1** JSON,写入「设计.实现机制」。 -这里的「机制」不是一套必须解释到底的科学原理,也不等于程序规则。它更接近科幻小说中可以直接成立的初始设定:后续内容都依赖它,但本步不必为它补写起源史或完整理论。 +这里的「机制」不是必须解释到底的科学原理,也不等于程序规则。它更接近可直接成立的初始设定:后续内容依赖它,本步不必补起源史或完整理论。 -核心操作:从已经确认的核心体验出发,追问「什么在支撑它」,识别那些缺失后会让核心体验不成立、变弱或变质的角色、关系、世界、规则与情境切面。 +核心操作:从已确认的核心体验出发,追问「什么在支撑它」,识别缺失后会让体验不成立、变弱或变质的角色、关系、世界、规则与情境切面。 执行顺序: -1. 读取依赖产物和用户表述,提取已经确认的核心体验。只做忠实复述,不重新设计美学纲领或交互范式。 -2. 在思考阶段判断本叙事空间继承了怎样的可能性范围,以此检查候选支撑点是否可能存在。这个「基准世界」只用于推理和校验,不写入产物。 -3. 分别检查体验如何产生、如何增强、如何维持,以及多个支撑点如何共同成立。 -4. 对每个候选点做移除检验:「如果删掉或替换这个切面,核心体验会损失什么?」 -5. 只保留有明确支撑关系的主要支点。常识、装饰、完整人物设定和应由后续能力展开的内容不纳入。 -6. 描述每个支点所属的元素、承担支撑作用的切面、切面的必要结构,以及它与核心体验的关系。 -7. 若支点之间存在重要的前提、对照、制衡、循环或共同支撑关系,再单独说明;关系简单时不要强行建图。 -8. 输出符合 output 契约的 JSON,供用户验收。 +1. 读依赖产物与用户表述,忠实提取核心体验;不重做美学/交互。 +2. 思考阶段判断基准世界可能性范围(只校验,不写入产物)。 +3. 检查体验如何产生、增强、维持,及多支点如何共同成立。 +4. 对每个候选做移除检验。 +5. 只保留有明确支撑关系的主要支点;常识、装饰、完整人物卡、后续技能展开对象不纳入。 +6. 写入正文:依据体验、支撑点(含切面形态)、支撑点关系、覆盖判断。 +7. 填自评与追问(薄弱点才问;题目可空数组)。 +8. 输出符合 output 契约的 JSON。 -工作姿态——「识别已经在那里之物」: -- 不凭空另造一套体验。 -- 不把无关设定做成菜单让用户挑选。 -- 不因为某类题材常见某套设定,就直接套用固定机制。 -- 可以把用户已经表达但尚未命名的支撑关系整理成清晰语言。 -- 可以提出基于现有体验的暂定识别,交给用户修正。 -- 如果两种不同理解会导向不同的核心支点,先用一个针对性问题辨明,不要同时堆出多套方案。 +工作姿态——「识别已经在那里之物」:不另造体验;不设设定菜单;不题材套件;可整理未命名关系;可暂定请用户修正;二选一支点先问清再写。 -若依赖产物内部仍有含混或矛盾,不要越权重做上一步。只询问会改变本步支撑识别的关键差异;无法在本步解决的内容写入「开放问题」。 +依赖含混时不越权重做上一步。无法在本步解决的写入开放问题或追问。 -若程序已发出默认问题:用户首答在「用户.worker答复」。禁止重复同一开场;在首答与依赖产物上补洞。 - -summary:`实现机制 · …`(点题主要支撑,非题材标签)。 +若程序已发默认问题:禁止重复开场;在首答与依赖上补洞。 +summary:`实现机制 · …`(点题主要支撑)。 ``` ## principles @@ -170,141 +160,124 @@ summary:`实现机制 · …`(点题主要支撑,非题材标签)。 ## output ```output -最终只输出一个合法 JSON 对象,不加代码块外说明,不使用注释,不夹带未定义的英文 id。 - { - "依据的核心体验": [ - "从依赖产物中忠实提取的体验锚点,不在这里重新设计" - ], - "支撑点": [ - { - "名称": "简短、稳定、对人可读的支撑点名称", - "归属元素": "这个切面属于哪个角色、关系、群体、世界条件、规则或情境", - "支撑切面": "只指出该元素中与核心体验有关的那个面向", - "切面形态": { - "结构": "一句话|过程|张力|层次|构成|循环|关系网", - "概述": "用最短的充分描述说清这个切面是什么样的", - "展开": [ - { - "部分": "仅在确有内部结构时填写", - "描述": "该部分在结构中的位置或作用" - } - ] + "schema": "context-fragment.v1", + "技能": "实现机制", + "brief": "一句话:主要靠什么撑住核心体验", + "mount": ["world-simulator"], + "稳变": "stable", + "正文": { + "依据的核心体验": [ + "从依赖产物忠实提取的体验锚点;此处不重做美学纲领" + ], + "支撑点": [ + { + "名称": "简短、稳定、对人可读", + "归属元素": "角色 / 关系 / 群体 / 世界条件 / 规则 / 情境…", + "支撑切面": "该元素中真正相关的那一面", + "切面形态": { + "结构": "一句话|过程|张力|层次|构成|循环|关系网", + "概述": "最短充分描述", + "展开": [{ "部分": "仅必要时", "描述": "…" }] + }, + "对应体验": ["直接服务的体验锚点"], + "如何支撑": "条件 / 限制 / 对照 / 张力如何让体验成立", + "缺失后果": "拿掉或改弱后具体损失什么(禁止只写「变差」)", + "完备度": "…%" + } + ], + "支撑点关系": [ + { + "涉及": ["支撑点名称"], + "关系": "前提|共同支撑|反衬|制衡|递进|循环|…", + "体验作用": "为何必须保留这段关系" + } + ], + "覆盖检验": { + "主要支撑是否覆盖核心体验": "是|否|部分", + "说明": "还有哪块体验没有支点、或支点过密", + "宁少勿多说明": "为何到此停止" + } + }, + "自评": { + "维度": [ + { + "名": "覆盖度", + "分数": 0, + "说明": "能否完全覆盖用户已表达的规则/前提/硬条件(未漏掉用户点名的承重约束)" }, - "对应体验": [ - "该支撑点直接服务的体验锚点" - ], - "如何支撑": "说明它通过什么条件、限制、对照或张力让体验成立", - "缺失后果": "说明拿掉或改弱这个切面后,体验会失去什么" - } - ], - "支撑点关系": [ - { - "涉及": ["支撑点名称"], - "关系": "前提、共同支撑、反衬、制衡、递进、循环或其它自然语言关系", - "体验作用": "这项关系为何需要被保留" - } - ], - "开放问题": [ - { - "问题": "尚不能可靠确定、且会影响本步结论的问题", - "影响": "不确定性会改变哪些支撑点或支撑关系", - "当前暂定": "若已有最可能的理解,用可撤销的方式写明;没有则留空字符串" - } - ] + { + "名": "充分度", + "分数": 0, + "说明": "支撑点合起来能否充分展示用户想要的核心体验(缺了是否撑不住感觉)" + }, + { + "名": "克制度", + "分数": 0, + "说明": "是否精确到支撑切面;无多余百科、人物卡、起源史或题材默认拓展" + } + ], + "薄弱点": "一句话" + }, + "追问": { + "导语": "为钉准支撑切面,还需确认:", + "题目": [ + { + "问": "移除检验或短场景落地题…", + "建议选项": ["选项A(可带短场景)", "选项B", "其它(请写明)"], + "示例": "可选示范句" + } + ] + }, + "开放问题": [] } - -填写规则: -- 「依据的核心体验」只建可追溯关系,不得扩写成新的美学纲领。 -- 每个「支撑点」必须同时写清归属元素、支撑切面和支撑关系(如何支撑 + 缺失后果)。 -- 简单支点「结构」填「一句话」,「展开」填空数组。 -- 只有一句话不足以保留关键结构时才用其它结构类型。 -- 「缺失后果」不能只写「体验变差」,必须指出具体损失。 -- 「支撑点关系」只记对体验有实际影响的关系;没有则空数组。 -- 基准世界、推理过程、候选菜单和被淘汰支点不得写入产物。 -- 没有开放问题时填空数组,不要制造问题。 -- 不要输出完整角色卡、世界观百科、剧情大纲、叙事规则、变量表或演员规格。 - -本步完成的定义: -- 核心体验的主要支撑已经被覆盖; -- 每个支撑点都通过了「如何支撑/缺了会怎样」的检验; -- 每个元素只保留了承担支撑作用的切面; -- 所有支点在当前叙事空间中都可能成立,没有暗中引入未经确认的变造; -- 剩余内容属于常识、具体实例或后续能力的展开范围。 ``` +硬规则(公共三段 + 本技正文): +1. 合法 JSON;必含 `schema` / `技能` / `brief` / `正文` / `自评` / `追问`(题目可 `[]`)。 +2. **公共三段**:`正文`=产物主体;`自评`=打分;`追问`=导语+建议选项。禁止整份散文壳。 +3. **本技正文**:`依据的核心体验` + `支撑点[]` + `支撑点关系[]` + `覆盖检验`。基准世界/推理过程/淘汰候选不得写入。 +4. 每个支撑点须有归属元素、支撑切面、如何支撑、缺失后果;简单支点结构=「一句话」、展开=`[]`。 +5. `mount` 默认主世界层(`world-simulator`);**禁止**写 `order`。承重规则进 GM,不进转述全文。 +6. `稳变` 多为 `stable`(支点定稿后少改);若用户明确机制会阶段性改换可标 `semi`。 +7. 不要输出角色卡、世界百科、剧情大纲、叙事文风、变量表、执行单元列表。 +8. summary:`实现机制 · {brief 缩略}`。 + +本步完成:主要支撑已覆盖;每点过「如何支撑/缺了怎样」;无暗中变造;剩余归常识或下游技能。 + ## checklist ```checklist -- [ ] 「依据的核心体验」是否忠实来自依赖产物,而非本步新设计? -- [ ] 每个支撑点是否都有:归属元素、支撑切面、如何支撑、缺失后果? -- [ ] 简单支点是否避免了不必要的「展开」? -- [ ] 是否误写成设定菜单、完整人物卡、起源史或叙事文风指南? -- [ ] 是否暗中引入了用户未确认的变造? -- [ ] 宁少勿多:主要支撑已覆盖后是否停住? -- [ ] 「支撑点关系」是否只含对体验有实际影响的项(或空数组)? -- [ ] 开放问题是否都真正影响本步结论(无则空)? +- [ ] 含 schema / brief / 正文 / 自评 / 追问? +- [ ] 依据的核心体验忠实来自上游,非本步新设计? +- [ ] 每个支撑点有归属元素、切面、如何支撑、缺失后果? +- [ ] 自评含覆盖度 / 充分度 / 克制度? +- [ ] 未写成设定菜单、人物卡、起源史、叙事指南与故事推进、order 表? +- [ ] 宁少勿多;支撑点关系仅保留有体验作用的项(可空)? +- [ ] 追问只打会改变支点的缺口,带建议选项? ``` ## examples ```examples -示例一:简单支撑点 +好: +- brief「秘密靠丈夫认知过滤与证据逼近维持」;正文有依据体验 + 支撑点(认知切面过移除检验)+ 覆盖检验;自评三维(覆盖/充分/克制);追问用短场景打待探。 +- 循环支点「力量与自我侵蚀同步」:展开诱因/收益/代价/反馈;与「身边人延迟识别」写进支撑点关系。 +- 整份落在 context-fragment.v1;支撑点切片示例: -核心体验:「秘密长期存在,但每次接近暴露时都令人紧张。」 - -合格: { "名称": "丈夫的认知过滤", "归属元素": "丈夫", - "支撑切面": "面对威胁完美家庭信念的证据时,会无意识地优先采用无害解释", - "切面形态": { - "结构": "一句话", - "概述": "他的信任不是单纯迟钝,而是一种维护既有信念的认知过滤。", - "展开": [] - }, - "对应体验": ["秘密能够长期存在", "证据接近暴露时的紧张"], - "如何支撑": "认知过滤让可疑证据能够出现而不立刻终结秘密,使暴露风险可以反复逼近。", - "缺失后果": "如果他能稳定、直接地识别证据,秘密会迅速结束;如果完全没有证据出现,濒临暴露的紧张也会消失。" -} -只描述认知切面,不补职业、外貌、成长史。 - -示例二:具有内部结构的支撑点 - -核心体验:「每次获得力量都伴随自我逐渐陌生化的诱惑与恐惧。」 - -合格: -{ - "名称": "力量与自我侵蚀的同步增长", - "归属元素": "超常力量", - "支撑切面": "力量的每次增长都会永久改变使用者的一项感知、欲望或判断方式", - "切面形态": { - "结构": "循环", - "概述": "危机促使角色使用力量,力量解决危机并造成侵蚀,侵蚀又让下一次使用更容易发生。", - "展开": [ - { "部分": "诱因", "描述": "现实危机让使用力量成为最有效的解决方式。" }, - { "部分": "收益", "描述": "力量立即兑现效果,使继续使用具有真实吸引力。" }, - { "部分": "代价", "描述": "每次使用都会留下不可完全逆转的自我改变。" }, - { "部分": "反馈", "描述": "改变后的角色更容易接受下一次使用,循环因此加深。" } - ] - }, - "对应体验": ["力量带来的诱惑", "逐渐失去自我的恐惧"], - "如何支撑": "收益与侵蚀来自同一次行动,角色不能只取其一,因此诱惑和恐惧能够持续共存。", - "缺失后果": "如果侵蚀可以轻易撤销,恐惧会退化成短期成本;如果力量没有即时收益,诱惑则无法成立。" -} -不解释力量由谁创造、完整物理理论或全部侵蚀实例。 - -示例三:支撑点之间的关系 - -{ - "涉及": ["力量与自我侵蚀的同步增长", "身边人对变化的延迟识别"], - "关系": "共同支撑并形成时间差", - "体验作用": "侵蚀让角色真实改变,延迟识别则给变化留下累积空间;两者共同维持「尚能隐藏但终将无法隐藏」的过程感。" + "支撑切面": "面对威胁完美家庭信念的证据时,优先采用无害解释", + "切面形态": { "结构": "一句话", "概述": "维护既有信念的认知过滤,而非单纯迟钝。", "展开": [] }, + "对应体验": ["秘密长期存在", "濒临暴露的紧张"], + "如何支撑": "可疑证据可出现而不立刻终结秘密。", + "缺失后果": "若能直接识别证据则秘密速终;若永无证据则紧张消失。", + "完备度": "90%" } -不合格: -- 「可以选择诅咒、寄生物、人格分裂、外星科技或邪神污染…」→ 脱离体验的设定菜单。 -- 「丈夫四十二岁,是律师,外表温和,童年…」→ 把认知切面扩成完整人物设定。 -- 「为了营造压抑美感,叙述应采用近距离视角…」→ 叙事呈现,不是支撑设定。 -- 「这种力量源于三千年前的实验事故…」→ 为可直接成立的初始设定补不必要起源史。 +坏: +- 设定菜单:「可选诅咒/寄生物/邪神…」 +- 人物卡膨胀;叙事文风;无必要起源史 +- 只有散文支点、无 schema / 自评 / 追问 ``` diff --git a/skills/dialogue/world-simulator/modules/narrative/prompt.md b/skills/dialogue/world-simulator/modules/narrative/prompt.md index 28cd4b3..0b6aaa5 100644 --- a/skills/dialogue/world-simulator/modules/narrative/prompt.md +++ b/skills/dialogue/world-simulator/modules/narrative/prompt.md @@ -1,109 +1,279 @@ -# 叙事指南 +# 叙事指南与故事推进 -> 能力文档。程序只切割下方 **fence 块**;`##` 标题仅供人读。 -> 写法说明:`docs/world-simulator-modules.md`;范例:`aesthetics-interaction`。 +> 技能文档。程序只切割下方 **fence 块**;`##` 标题仅供人读。 +> 产物外壳:`docs/context-fragment-design.md`;对照:`aesthetics-interaction`(感觉)↔ 本技(怎么写 / 往哪推)。 +> 默认服务**叙事转述**;「推进与决策」同时给主世界层用。 +> 旧称:叙事指南(流程 name / artifact 兼容见 creation-flow)。 ## meta ```meta -name: 叙事指南 +name: 叙事指南与故事推进 id: narrative -artifact: 设计.叙事指南 +artifact: 设计.叙事指南与故事推进 declaration: > - 世界/助手态度与体验边界(不等于文风;契约已写清的态度勿重复问卷) + 钉转述「怎么写」与主世界「怎么推」:叙事纲领、遣词造句与笔墨焦点、绝对禁忌/不偏好、 + 示范与情境备用,以及推进/决策(可含变量门控);不重做美学感觉问卷 when: | - 需要单独钉清「世界/助手如何说话、如何取舍信息、如何对待用户情绪」时; - 或美学纲领与交互范式里态度仍过粗、下游演员会飘时。 + 已有(或等价)美学纲领,需要把「要的感觉」落成可执行的写法与推进时; + 或转述缺遣词/示范/笔墨重心(如男频写女、女频写男、末日绝望 vs 日常小确幸); + 或缺少情境切换语料、或主世界层缺推进框架(可含好感/日期/冷却钩子)时。 when_not: | - 美学纲领与交互范式已写清态度/禁忌,用户未要求加细 → 不要重复问卷。 - 用户只要文风辞藻样本、不要态度边界 → 可放入常驻上下文短句,不必强开本步。 - 排版/回复块结构 → 设计回复格式。 + 美学未钉、还在谈「要什么感觉」→ 先「美学纲领与交互范式」。 + 契约已细到可直接转述、用户明确不要本步 → 可不排。 + 用户可见块序/监控栏版式 → 「正文组成」/「设计监控栏」。 + 事件池 schema / 预生成 → 「生成规则」/「具体实例」。 + 真值与 side_effects → 「变量设计与更新规则」。 boundary: | - 本能力:态度、信息取舍、体验边界、焦点偏好(给转述/写手演员的稳定口径)。 - 不等于文风词典:少堆形容词;多写「遇到 X 时怎么处理」。 - 美学纲领与交互范式:站位、呈现、核心体验、轮转——已写清的勿重问。 - 生成规则:可执行推进规则;本步不定章结构算法。 - 设计回复格式:块结构与拼接顺序。 + 本技能:叙事宪法 + 遣词造句/笔墨焦点 + 禁忌与不偏好 + 示范/备用 + 推进与决策 + 内容表达; + 产出 context-fragment.v1。美学写「感觉」,本步写「花笔墨在哪、怎么遣词、故事怎么推」。 + 美学纲领与交互范式:体验与轮转——已写清的感觉勿重问。 + 生成规则 / 变量*:合同与副作用规格——本步只给推进钩子与写法,不建表。 + 设计回复格式:块结构——本步不定。 +feeds: narrator ``` ## opening ```opening -用几句话说明「系统说话时该像什么人」(想到什么写什么): +美学纲领里的「感觉」已有方向时,请谈叙事「怎么写、往哪推」(想到什么写什么): -1. 态度:冷静如实 / 偏袒主角 / 狠心推进 / 温柔留余地 / 其它? -2. 信息:用户不知道的世界内情,默认藏多少?可以剧透结构吗? -3. 边界:什么绝对不写或不怎么写?(已有禁忌可写「同体验契约」) +1. 正文应该像什么?遣词大概什么味道? + 例:第三人称网文、偏爽文、简明带张力;句短、少文艺腔…… -若上游契约已够用,可回复「按美学纲领,只补……」。 +2. 笔墨该花在谁/什么上?有什么绝对不能写、或不想多写? + 例:男频爱情 → 多写女角色美丽与反应;女频相反; + 末日 → 生存绝望,禁止轻松解决一切;日常 → 小确幸,禁止突然飞来横祸…… + +3. 故事怎么往前推?要不要跟变量挂钩?有没有情境要换一套语料? + 例:好感够了才插入倒追;NSFW 改官能笔法;丛林三两句带过 vs 荒野逐步细写…… ``` ## task ```task -你正在执行剧本中的「叙事指南」步骤。产物写入「设计.叙事指南」。 +你正在执行「叙事指南与故事推进」。产物必须是 **context-fragment.v1** JSON,写入「设计.叙事指南与故事推进」。 -先读「设计.美学纲领与交互范式」:已写清的态度/禁忌直接继承,只补缺口。 +美学钉感觉;本步钉:**怎么遣词造句、笔墨花在哪里、绝对禁忌与不偏好、示范长什么样、故事如何推进**。 执行顺序: -1. 复述已确认的体验内核与呈现;不重定站位。 -2. 钉态度、信息策略、焦点、边界;写成下游可挂载的短规则,而非散文赏析。 -3. 与体验禁忌冲突时以体验禁忌为准,并在产物里点明。 -4. 输出符合 output 的 JSON。 +1. 读美学纲领(及可选机制/舞台):继承体验内核与禁忌;改写成创作导向句,不重复问卷。 +2. 写「叙事纲领」:核心定位 + 叙事使命 + 叙事追求(浓缩)。 +3. 写「风格与遣词」:叙述者人格、整体基调、**遣词造句**(用什么词/句式/节奏、避免什么)、修辞;附至少 1 段示范。 +4. 写「笔墨焦点」:观众/代入取向下应详写谁、略写谁(例:男用户向爱情线详女角色;女用户向详男角色);与体验类型对齐的场景重心。 +5. 写「禁忌与不偏好」:绝对禁忌(硬禁)与不偏好(能避则避、出现须极克制)——含题材气质禁令(末日禁轻松通关、日常禁无端灾难等)。 +6. 需要时写「情境备用」多套语料;写「推进与决策」(驱动力、速度/详略、成败、困境、节奏、可选变量钩子)。 +7. 写「内容与表达」:视角执行、场景详略、信息组织。 +8. 自评 + 追问;输出 JSON。 -若程序已发出默认问题:禁止重复同一开场。 - -summary:`叙事指南 · …`(点题态度,非「文学性」空话)。 +mount:默认挂叙事转述 + 主世界层。禁止写最终 `order`。 +summary:`叙事指南与故事推进 · …`(点题体裁/笔墨重心 + 推进有无)。 ``` ## principles ```principles -1. 态度是行为规则,不是形容词堆砌。 -2. 不重复问卷:契约已有的字段引用即可。 -3. 服务体验:狠心或温柔都必须能指向核心感受。 -4. 写手模式:可写「助手如何给大纲意见 / 如何改稿语气」,仍避免空泛文风课。 -5. 缩减:三条管用口径优于一页风格指南。 +1. 感觉 vs 做法 + 美学:装逼的爽感。本步:第三人称爽文、遣词短促有力、笔墨落在哪、示范段落、推进是否心想事成…… + 禁止复读体验口号。 + +2. 遣词造句必须可执行 + 不只写「要有张力」:写句长、用词场、可否网感/文言、感官词偏好、情绪直给 vs show。 + 每条倾向尽量带「做法 → 目的」。 + +3. 笔墨焦点跟体验与观众站位走 + 爱情线:男用户向通常详写女角色外貌/气质/反应;女用户向详写男角色——写清本局取向,勿默认「男女均衡」。 + 末日天灾:详写匮乏、威胁、代价与绝望感;禁止轻松解决一切、轻松段子冲淡生存压。 + 日常小确幸:详写细微暖意与节奏;禁止无铺垫的突发致命危险或崩坏(除非用户明确要)。 + 其它类型同理:问「读者眼神会停在哪」。 + +4. 绝对禁忌 ≠ 不偏好 + 绝对禁忌:出现即破坏体验,硬禁。 + 不偏好:能避则避;偶尔出现须克制、不抢主戏。 + 两者都要写,并说明与美学的关系。 + +5. 示范与情境备用 + 至少一段默认示范;NSFW/战斗/日常等用「情境备用」换遣词与禁忌增量。 + +6. 推进与写法同框,推进可轻可无 + 变量门控只写钩子;表规格交给变量/生成规则。 + +7. 继承优先;与美学硬禁忌冲突时以体验禁忌为准并点明。 ``` ## probe ```probe -一轮 1~2 点。 +每轮 1~2 点。已在美学写清的感觉禁止重问。 -缺态度:失败时系统更像「如实报损」还是「帮用户找补救」? -缺信息:用户是否允许「角色知道但用户暂不知」的悬置? -与禁忌冲突:用户既要无虐又要残酷真实——以哪边为先? +优先:体裁与遣词味道;笔墨焦点(详谁/略谁);绝对禁忌 vs 不偏好;末日/日常等气质禁令;推进门控与详略成败;是否要情境备用。 +不追问:回复块排版、事件池 schema、side_effects 细节、舞台百科。 ``` ## output ```output { - "brief": "一句话:叙事口径服务什么体验", - "态度": "…", - "信息策略": "…", - "焦点": "…", - "边界": ["…"], - "遇到冲突时": "优先保全…", - "继承自体验契约": ["已直接采用的字段"], - "开放问题": ["…"] + "schema": "context-fragment.v1", + "技能": "叙事指南与故事推进", + "brief": "一句话:体裁遣词 + 笔墨重心 + 推进有无", + "mount": ["narrator", "world-simulator"], + "稳变": "stable", + "正文": { + "依据的体验": [ + "从美学纲领忠实转化;可点名禁忌继承" + ], + "叙事纲领": { + "核心定位": "一句话:叙事本质/灵魂", + "叙事使命": "1~2 句:要让用户体验到什么", + "叙事追求": [ + "关键词 - 简短解释" + ] + }, + "风格与遣词": { + "叙述者人格": "形象化一句话:这个声音是谁", + "整体基调": "温度/节奏/质感/距离的整体描述", + "遣词造句": [ + "原则 - 具体做法(用词/句式/段落节奏) → 目的" + ], + "核心修辞": [ + "修辞名 - 示例 → 作用" + ], + "示范": [ + { + "情境": "常态/开场/高潮…", + "段落": "一段可朗读的示范(体现遣词与张力)" + } + ] + }, + "笔墨焦点": { + "观众与代入取向": "男用户向|女用户向|中性/双关|未定;可附一句站位", + "详写优先": [ + "应花笔墨的对象或维度(例:女角色外貌与反应;生存压迫与资源匮乏;微表情与小确幸)" + ], + "可略写": [ + "可三两句带过或背景化的部分" + ], + "说明": "与体验类型如何对齐(男频/女频/末日/日常…)" + }, + "禁忌与不偏好": { + "绝对禁忌": [ + "禁止… → 原因(硬禁;破坏体验)" + ], + "不偏好": [ + "尽量避免… → 若出现则如何克制" + ] + }, + "情境备用": [ + { + "触发": "例:NSFW / 战斗 / 日常过场", + "改用风格": "例:官能小说笔法", + "遣词与禁忌增量": ["相对默认改什么"], + "笔墨增量": "该情境笔墨是否偏移;无则空字符串", + "示范": "可选示范段" + } + ], + "推进与决策": { + "有无专门推进": "有|无(沿用常识与体验即可)", + "驱动力来源": "故事主要由什么推动", + "叙事速度与详略": "何种过程可带过,何种必须细写", + "成败与难度": "心想事成 / 允许受挫 / 其它;与体验关系", + "变量门控钩子": [ + { + "变量或信号": "好感|日期|冷却|进度|seed|其它", + "条件意图": "例:好感≥某档;冷却归零;纯随机", + "插入或推进什么": "例:倒追线;诡异遭遇", + "实现指向": "投影/动态表|生成规则事件池|主世界层临场判断", + "说明": "可较长;本步不写 side_effects" + } + ], + "困境应对": [ + "困境简述 → 应对方向(关键词)" + ], + "节奏意识": [ + "情境 → 应对" + ] + }, + "内容与表达": { + "视角执行": { + "主次原则": "主信息源占比/辅助源条件", + "切换时机": ["情境 → 应对"], + "切换方式": "如何平滑切换" + }, + "场景详略映射": [ + "场景类型: 聚焦… | 详写条件 → 原因(与笔墨焦点对齐)" + ], + "信息组织模式": [ + "模式名: 结构 → 适用情境" + ] + } + }, + "自评": { + "维度": [ + { + "名": "写法可执行", + "分数": 0, + "说明": "遣词+笔墨焦点+禁忌/不偏好+示范是否够转述落地" + }, + { + "名": "推进可执行", + "分数": 0, + "说明": "主世界层是否知道如何推进;变量钩子清楚或明确无需" + }, + { + "名": "与美学对齐", + "分数": 0, + "说明": "是否把感觉落成做法;笔墨与禁忌服务体验而非跑偏" + } + ], + "薄弱点": "一句话" + }, + "追问": { + "导语": "为钉准写法与推进,还需确认:", + "题目": [ + { + "问": "…", + "建议选项": ["选项A", "选项B", "其它(请写明)"], + "示例": "可选短示范句" + } + ] + }, + "开放问题": [] } ``` +硬规则: +1. 合法 JSON;含 `schema` / `技能` / `brief` / `正文` / `自评` / `追问`。 +2. 正文键固定:依据的体验、叙事纲领、**风格与遣词**、**笔墨焦点**、**禁忌与不偏好**、情境备用、推进与决策、内容与表达。 +3. `风格与遣词.示范` 至少 1 条;`遣词造句` 不得空数组(建立本步时)。`情境备用` 可 `[]`。 +4. `绝对禁忌` 与 `不偏好` 均须出现(可短,但不可整段省略);须能对上体验类型(如末日/日常/频向)。 +5. 自评:写法可执行 / 推进可执行 / 与美学对齐。禁止 `order`。 +6. 不要输出事件池 schema、side_effects、回复块结构、执行单元列表。 +7. summary:`叙事指南与故事推进 · {brief 缩略}`。 +8. 兼容:若下游仍读旧 tag「设计.叙事指南」,细化终稿可将本产物同步或改挂新 tag(本步只写新 artifact)。 + ## checklist ```checklist -- [ ] 与美学纲领与交互范式不重复盘问、不矛盾? -- [ ] 是否可被演员当规则执行(非纯文风赏析)? -- [ ] 是否误写成回复块结构或生成规则? +- [ ] 含 schema / brief / 正文 / 自评 / 追问? +- [ ] 自评为写法可执行 / 推进可执行 / 与美学对齐? +- [ ] 遣词造句具体?有示范?笔墨焦点写清详谁略谁? +- [ ] 绝对禁忌与不偏好都有,且贴合体裁(男频/女频/末日/日常等)? +- [ ] 推进钩子清楚或明确无需?未越权写生成规则/变量规格? ``` ## examples ```examples 好: -- 「如实推进代价;不替用户道德排雷;悬念可藏事实不可藏规则。」 +- 男频爱情爽文:遣词网文短句;笔墨详女角色美丽与心动反应;略写男主外貌流水账;禁忌:禁止把女主写成无魅力背景板。 +- 女频:笔墨详男角色气质/行动力;不偏好过度描写女主外貌抢戏。 +- 末日:详写寒冷/饥饿/信任崩裂;绝对禁忌「轻松解决危机」;推进允许失败与代价。 +- 日常小确幸:详写琐碎暖意;绝对禁忌无铺垫的突发灾难;推进忌强行高潮。 +- NSFW 备用:触发后改官能遣词;笔墨可临时偏移身体感官。 + 坏: -- 「要有诗意、电影感、高级感……」无可执行口径 +- 只写「要有沉浸感」无遣词、无笔墨焦点。 +- 默认「男女都要均衡描写」却无视本局男/女用户向。 +- 末日指南里鼓励轻松打闹通关。 ``` diff --git a/skills/dialogue/world-simulator/modules/opening-setup/prompt.md b/skills/dialogue/world-simulator/modules/opening-setup/prompt.md new file mode 100644 index 0000000..125e135 --- /dev/null +++ b/skills/dialogue/world-simulator/modules/opening-setup/prompt.md @@ -0,0 +1,173 @@ +# 开场白与开场变量 + +> 技能文档。程序只切割下方 **fence 块**;`##` 标题仅供人读。 +> 产物外壳:`docs/context-fragment-design.md`。 +> 创作偏晚:规格与呈现契约已齐,写「迈进世界的第一拍」并钉开场真值。 +> 磁盘 worker `opening-generator` 可执行/落库本产物到 `输出.开场白` 与 `运行.初始变量`。 + +## meta + +```meta +name: 开场白与开场变量 +id: opening-setup +artifact: 设计.开场白与开场变量 +declaration: > + 写出遵循正文组成/叙事/监控栏等契约的开场一小段,并钉与之同真相的开场变量初值; + 开场为主、填表为辅 +when: | + 体验、舞台、叙事、变量(若有)、正文组成等已大致可引用, + 需要可开玩的第一段剧情与初始真值快照时;通常在细化终稿前后、进游玩前。 +when_not: | + 体验/呈现仍混沌 → 先美学与正文组成等。 + 只要改文风不要开场 → 叙事指南。 + 只要改真值规则不要开场文 → 变量设计。 +boundary: | + 本技能:开场正文(按正文组成块序拼出用户可见第一屏)+ 开场变量初值 + 可选监控栏初值示意。 + 必须遵守已验收的叙事指南、正文组成、监控栏、变量设计;不重做这些契约。 + 具体实例:开场可引用已有实例名片,不在本步新造百科。 + opening-generator:可将本产物写入 输出.开场白 / 运行.初始变量 / 变量.当前。 +feeds: narrator +``` + +## opening + +```opening +设定已齐时,用几句话定开场第一拍: + +1. 玩家睁眼/进场时在哪、正发生什么? +2. 开场结束时,TA 显然可以做什么? +3. 此时各真值大概是多少?(能推断的直接说;推不出的标「要问」) +``` + +## task + +```task +你正在执行「开场白与开场变量」。产物必须是 **context-fragment.v1** JSON,写入「设计.开场白与开场变量」。 + +主产物是**开场一小段剧情**(用户第一屏),不是填表工具。 +必须遵循:叙事指南与故事推进、正文组成(可见块序、隐藏段若开场就要写)、监控栏字段(若启用)、变量真值名单。 + +执行顺序: +1. 读依赖:worker 集/美学/叙事/正文组成/监控栏/变量设计/舞台与实例。 +2. 按正文组成的可见块写出开场各块内容(至少正文块);隐藏段仅当开场即需声明初值变更时使用(通常初值走「开场变量」字段,隐藏段可空)。 +3. 钉开场变量:与开场事实同一真相;能从设定推断的填上;冲突则以开场叙述为准并改表。 +4. 若有监控栏:给出开场时监控栏展示快照(短)。 +5. 自评 + 追问;输出 JSON。 + +禁止:先问卷填表再糊开场;禁止开场与初值打架;禁止重写 Worker 集。 + +summary:`开场白与开场变量 · …`(点题场景)。 +``` + +## principles + +```principles +1. 开场为主,表为配套:表服务「这一刻世界是什么样」。 +2. 遵守版式:用户看见的结构按正文组成;文风按叙事指南;笔墨焦点同样适用。 +3. 同真相:开场写身无分文 → 资产真值不得很富。 +4. 可行动:第一拍结束要有「接下来能做什么」的空间,非说明书。 +5. 短:可读完、愿意进游玩;不是第一章全文。 +6. 隐藏真值/未解锁映射档不得剧透进可见开场。 +``` + +## probe + +```probe +开场地点/冲突不清;某真值开场值推不出;版式块是否都要在开场出现(有的块首轮可空)。一次 1~2 点。 +``` + +## output + +```output +{ + "schema": "context-fragment.v1", + "技能": "开场白与开场变量", + "brief": "一句话:开场场景 + 关键初值", + "mount": ["narrator"], + "稳变": "stable", + "正文": { + "依据契约": [ + "正文组成 / 叙事指南 / 监控栏 / 变量设计 等已遵循要点" + ], + "开场可见块": [ + { + "块id": "与正文组成.visible块一致", + "内容": "该块开场文本;可空块写空字符串并说明为何首轮空" + } + ], + "开场白全文": "按块序拼好的用户可见开场(主阅读件)", + "开场变量": [ + { + "名": "与变量设计真值名一致", + "值": "初值", + "依据": "开场事实/设定推断" + } + ], + "监控栏开场快照": [ + { + "名": "监控栏字段名", + "显示": "短展示" + } + ], + "隐藏段开场": "通常空;若有定界内容则写出", + "落库提示": { + "输出.开场白": "开场白全文", + "运行.初始变量": "开场变量 → 表格式", + "变量.当前": "与初始变量对齐" + }, + "用户可行动空间": "开场结束后玩家显然能尝试什么" + }, + "自评": { + "维度": [ + { + "名": "契约符合", + "分数": 0, + "说明": "是否遵循正文组成/叙事/监控栏" + }, + { + "名": "同真相", + "分数": 0, + "说明": "开场叙述与开场变量是否一致" + }, + { + "名": "可开玩", + "分数": 0, + "说明": "是否短、可感、有下一步行动空间" + } + ], + "薄弱点": "一句话" + }, + "追问": { + "导语": "为钉准开场,还需确认:", + "题目": [ + { + "问": "…", + "建议选项": ["选项A", "选项B", "其它(请写明)"], + "示例": "可选" + } + ] + }, + "开放问题": [] +} +``` + +硬规则: +1. context-fragment.v1;自评契约符合/同真相/可开玩。 +2. `开场白全文` 与 `开场变量` 必填;监控栏快照无监控栏则为 `[]`。 +3. 不输出完整 worker 集;不发明新真值名(须来自变量设计,若无变量设计则可极少数字段并标明临时)。 +4. summary:`开场白与开场变量 · {brief 缩略}`。 + +## checklist + +```checklist +- [ ] 开场遵循正文组成块序与叙事口径? +- [ ] 变量与开场同真相?可行动空间清楚? +- [ ] 未剧透隐藏档?未写成第一章全文? +``` + +## examples + +```examples +好:末日醒来缺水——开场正文写干渴与门外声响;口粮=1、威胁=邻层异响;监控栏短显示;无姓名性别栏。 +坏:先填完 20 个表字段再写两句「你醒了」;开场说破产表里却有百万资产。 +``` diff --git a/skills/dialogue/world-simulator/modules/refine/prompt.md b/skills/dialogue/world-simulator/modules/refine/prompt.md index 33f397b..2831fa9 100644 --- a/skills/dialogue/world-simulator/modules/refine/prompt.md +++ b/skills/dialogue/world-simulator/modules/refine/prompt.md @@ -1,8 +1,8 @@ # 细化终稿 > 能力文档。程序只切割下方 **fence 块**;`##` 标题仅供人读。 -> 写法说明:`docs/world-simulator-modules.md`;规格字段见 `docs/skill-design-guide.md`。 -> 本步产物 tag 为 **`设计.worker集`**(可进游玩的实例规格)。 +> 产物 tag:**`设计.worker集`**(JSON)。 +> **禁止自由发明 workers。** 按上游「游玩拓扑」的 `play_slots` / `writing_slots` 展开。 ## meta @@ -11,20 +11,22 @@ name: 细化终稿 id: refine artifact: 设计.worker集 declaration: > - 钉死关键前提、表与副作用,收成可进游玩的规格 + 按已勾选固定槽与「上下文投影排序」表收成可进游玩的规格: + play_slots、投影插入序、常驻上下文、表与副作用;不发明新执行单元 when: | - 前面能力已大致谈清(至少有体验契约,且演员职责可说清), - 需要收成可进游玩的「设计.worker集」JSON 时; - 编排应将本局流程 status 导向 closed。 + 前面技能已大致谈清(至少有体验契约;槽已勾选或可按默认; + 若已有多块上下文则宜先有投影排序表),需要输出「设计.worker集」JSON 时; + 编排应将流程 status 导向 closed。 when_not: | - 体验站位未定、或关键演员仍完全空白时,不要用本步代替上游。 - 用户只想改某一个演员细节 → 可先 Worker 规格,再本步合并。 + 体验站位未定 → 先上游。 + 拓扑未勾选且用户拒绝默认槽 → 先「游玩拓扑」。 + 上下文块已多且稳/变未排序 → 先「上下文投影排序」。 boundary: | - 本能力:合并上游产物,钉死不能瞎发挥的前提,输出完整「设计.worker集」JSON(含 interaction、workers、resident_context、tables 等)。 - 进 play 由用户手动决定;本步不自动切游玩。 - Worker 规格:分步钉演员;本步负责合并与终稿一致性。 - 美学纲领与交互范式等:只读引用,不重做问卷(矛盾处才问)。 - 开局·开场白:可选后续步骤,本步可用 design_end.opening 标记是否建议。 + 本能力:合并上游 → 完整运行规格 JSON(interaction、play_slots、context_order/inserts、 + workers 由槽展开、resident_context、tables)。 + 游玩拓扑:槽位权威;上下文投影排序:插入序与投影位权威;本步不新增白名单外的 ref。 + 变量设计:side_effects / 真值吸入 tables;不创建 variable-update 执行单元。 + 进 play 由用户手动决定。 ``` ## opening @@ -32,9 +34,10 @@ boundary: | ```opening 准备收成可进游玩的规格。请确认或补充: -1. 还有没有「绝不能瞎发挥」的前提要钉死?(一句一条) -2. 游玩时最少需要哪些演员上场?(用中文名即可) -3. 要不要表/状态栏?(不要就写「不要」) +1. 还有没有「绝不能瞎发挥」的前提?(一句一条) +2. 槽位是否按上游拓扑?(世界模拟默认:主世界层+转述;写手默认:大纲+章节)有无要改的勾选? +3. 若已有「上下文投影排序」,是否按该表收成?(不要在本步重排) +4. 要不要表/状态门控?(不要就写「不要」) 若前面产物已经够用,可直接回复「按已有产物收成」。 ``` @@ -42,50 +45,51 @@ boundary: | ## task ```task -你正在执行剧本中的「细化终稿」步骤。产物必须写入 **设计.worker集**,且为 **JSON**(不要 YAML)。 +你正在执行「细化终稿」。产物必须写入 **设计.worker集**,且为 **JSON**。 -本步是收成,不是再开一场题材发明。优先合并: -- 设计.美学纲领与交互范式 → interaction / experience_check / 呈现 -- 设计.worker规格 → workers[] -- 设计.叙事指南 / 生成规则 / 具体实例 / 世界蓝图 / 实现机制等 → resident_context 挂载或 core_premises -- 设计.变量* / 状态栏 / 拓扑 → tables / 显隐说明(有则写,无则省略) +本步是收成,不是发明新执行单元。优先合并: +- 设计.美学纲领与交互范式 → interaction / experience_check +- 设计.worker规格(游玩拓扑)→ play_slots 或 writing_slots +- 设计.上下文投影排序 → 写入规格的 context_order(每槽 inserts:order/anchor/ref/projection);无表且上下文很少时可按默认 static/dynamic 退化 +- 设计.叙事指南与故事推进(旧称 设计.叙事指南)/ 世界 / 机制 / 生成规则等 → resident_context(挂载以排序表与 mount 为准,本步不重排数字序) +- 设计.变量设计与更新规则 → tables.side_effects(及 schemas 摘要) +- 设计.变量控制上下文 → 核对挂载与剧透,写入 notes 或 resident 短句 执行顺序: -1. 忠实复述已确认的站位、体验内核、禁忌;矛盾处 askUser 1 点,勿静默覆盖。 -2. 组装 workers[]:每个含 ref、name(中文)、duty、rationale、acceptance;缺省 context/outputs 可标示留给模板合并,但 acceptance 必须写出。 -3. resident_context:把稳定长文(叙事态度、关键规则摘要、禁忌)挂到需要的 workers;不要把全过程聊天塞进去。 -4. tables:仅钉体验真正依赖的字段与副作用;无则 `tables` 省略或空 schemas。 -5. core_premises:不能瞎发挥的短列表。 -6. design_end:如 `{ "opening": "optional" }` 表示可随后跑开场白。 -7. 输出完整 JSON;summary:`细化终稿 · Worker集 · N 演员 · …` +1. 复述站位、体验内核、禁忌;矛盾处 askUser 1 点。 +2. 写入 `play_slots`(世界模拟)或等价写手槽;**workers 只含白名单 ref**: + - world_sim 每轮: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);勿塞聊天过程。 +5. tables:吸入变量设计的 side_effects;无则空数组或省略。 +6. core_premises、design_end.opening 按需。 +7. summary:`细化终稿 · 槽 gm+转述 · …` 或 `细化终稿 · 大纲+章节 · …` -若程序已发出默认问题:禁止重复同一开场。 - -进游玩不在本步完成;用户验收本产物后,由用户手动进入游玩。 +进游玩不在本步完成。 ``` ## principles ```principles -1. 合并优于重写:上游已验收内容优先进入规格,禁止无故改写体验内核。 -2. 每个面向用户的可读终稿点必须有 acceptance(review);中间层 continue。 -3. 正推缩减:能不建表就不建;能常驻一段话解决的不要新演员。 -4. 写手路径最小可运行:outline + chapter-writer(或用户只要分段写手);扮演路径按已钉演员。 -5. 键名稳定:workers[].ref 英文 kebab;name 中文给人看。 -6. 禁止题材固定套件;禁止在本步发明新 tool。 -7. 未决进 open_questions,不要假完备。 +1. 合并优于重写;槽位优于发明演员。 +2. 面向用户的终稿点 acceptance=review(通常是 narrator 或 chapter-writer);gm/outline 常用 continue。 +3. 真值变更写在 gm 的 outputs(运行.本轮.变量变更 / 裁决包内 variable_changes),不靠第三变量 Worker。 +4. 裁决包约定:运行.本轮.裁决 使用 settlement.v1(见 progressive-data-design / 模板)。 +5. 键名稳定;未决进 open_questions。 ``` ## probe ```probe -一轮 1~2 点,只问会挡住收成的矛盾: +只问挡住收成的矛盾: -- 上游演员列表与用户本轮说法冲突时,以谁为准? -- 终稿演员是「每轮世界+叙事」还是「先纲后章」? -- 有表需求但字段未定:先不要表,还是先钉 1~2 个关键字段? +- 拓扑与用户本轮说法冲突时以谁为准? +- 有表需求但副作用未定:先不要表,还是只钉 1~2 条 side_effects? -能按已有产物收成则不要为「完美」继续盘问。 +不要问「还要加哪个 Worker」。 ``` ## output @@ -93,6 +97,7 @@ boundary: | ```output { "version": 1, + "form_summary": "一句话体验", "interaction": { "user_stance": "…", "system_role": "…", @@ -104,27 +109,35 @@ boundary: | "focus": "…", "satisfaction_source": "…" }, + "play_slots": { + "gm": true, + "narrator": true, + "perspective": false + }, "workers": [ { - "name": "章节正文", - "ref": "chapter-writer", - "duty": "…", - "when": "…", - "rationale": "删掉则…", - "acceptance": "review", - "context": { - "static": ["设计.worker集", "大纲.当前"], - "dynamic": ["用户.最新输入"] - }, - "outputs": ["正文.当前段", "正文.已完成"] + "name": "主世界层", + "ref": "world-simulator", + "duty": "读投影与真值,输出 settlement.v1 裁决包;可提议变量变更", + "when": "每轮用户输入后", + "rationale": "删掉则无程序化裁决与真值更新", + "acceptance": "continue" + }, + { + "name": "叙事转述", + "ref": "narrator", + "duty": "只读裁决包,写用户可见正文", + "when": "裁决包就绪后", + "rationale": "删掉则无独立文风呈现(或需 gm 兼写,须用户明确)", + "acceptance": "review" } ], "resident_context": [ { "id": "experience-contract", "position": "static", - "content": "从上游压缩的稳定句(体验/禁忌/态度)", - "mount": ["chapter-writer"] + "content": "体验/禁忌压缩句", + "mount": ["world-simulator", "narrator"] } ], "tables": { @@ -132,7 +145,6 @@ boundary: | "side_effects": [] }, "core_premises": ["…"], - "narrative_guide": "可选短摘要;长文优先走 resident_context", "input_protocol": { "parens": "() 元要求", "quotes": "\"\" 角色对白", @@ -141,33 +153,33 @@ boundary: | "design_end": { "opening": "optional" }, - "open_questions": ["…"] + "open_questions": [] } ``` -必须是可解析 JSON。无表可省略 tables 或留空数组。每个 workers[] 元素必须有 acceptance。 +写手路径示例:不要 `play_slots`,workers 仅 `outline` + `chapter-writer`(acceptance 按是否验大纲)。 + +必须是可解析 JSON。每个 workers[] 元素必须有 acceptance,且 ref ∈ 白名单。 ## checklist ```checklist -- [ ] 是否 JSON 且可解析为设计.worker集? -- [ ] interaction 站位/轮转是否与美学纲领一致? -- [ ] 每个 worker 是否有 name、ref、duty、rationale、acceptance? -- [ ] 删掉任一 worker 的 rationale 是否说得清? -- [ ] 有没有把聊天过程塞进常驻上下文? -- [ ] 有没有题材默认灌入用户未要的演员/表? -- [ ] 验收复述能否一句话说清:站位、体验核心、演员、为何没有某件? +- [ ] 是否含 play_slots(世界模拟)或仅白名单写手 workers? +- [ ] 有无白名单外的 ref? +- [ ] interaction 是否与美学纲领一致? +- [ ] side_effects 是否来自变量设计(或明确不要表)? +- [ ] 有无把变量管理做成额外 worker? ``` ## examples ```examples 好: -- 扩写:interaction.turn_shape=助手分段;workers=outline(continue/review)+chapter-writer(review);resident 挂爽点与禁忌。 -- 扮演:world-simulator(continue)+narrator(review);core_premises 含变造点。 +- play_slots gm+narrator;workers 两条与槽一致;side_effects 从变量设计拷入。 +- 扩写:outline continue + chapter-writer review;无自造 ref。 坏: -- 输出 YAML 或半散文 -- workers 无 acceptance -- 无视上游,按「标准世界模拟套件」重写 +- 自由发明 affinity-agent、world-lore-worker。 +- 无 play_slots 却塞了 6 个自定义 workers。 +- YAML 或半散文 ``` diff --git a/skills/dialogue/world-simulator/modules/reply-format/prompt.md b/skills/dialogue/world-simulator/modules/reply-format/prompt.md index 1479fe8..f99b234 100644 --- a/skills/dialogue/world-simulator/modules/reply-format/prompt.md +++ b/skills/dialogue/world-simulator/modules/reply-format/prompt.md @@ -1,60 +1,231 @@ -# 设计回复格式 +# 正文组成 -> **状态:待完善** — 块格式见 `docs/world-simulator-modules.md`;范例见 `aesthetics-interaction`。 +> 技能文档。程序只切割下方 **fence 块**;`##` 标题仅供人读。 +> 产物外壳:`docs/context-fragment-design.md`。 +> **不是**程序 API/JSON schema 格式;钉的是用户最后看见的「一封信怎么拼起来」。 +> 旧称:设计回复格式。 ## meta ```meta -name: 设计回复格式 +name: 正文组成 id: reply-format -artifact: 设计.回复格式 -declaration: 钉单轮可见输出的结构(正文块、面板、拼接顺序),服务体验契约 -when: 需要钉单轮「用户看见什么、什么顺序」时(含多块拼接) -when_not: 美学纲领与交互范式里呈现已足够且无多块结构时 +artifact: 设计.正文组成 +declaration: > + 钉用户可见的一轮回复由哪些块、何顺序组成(抬头/日期/正文/监控栏/文末吐槽等), + 含对 LLM 隐藏的变量维护段与前端拆分/美化提示;不等于程序报文格式 +when: | + 用户看见的不该只是「一整段散文」,需要版式块(抬头、监控栏、文末小块等); + 或需要约定隐藏变量段供模型维护表格;或需要前端分区渲染时。 +when_not: | + 纯单段叙事、明确不要分块版式 → 可不排(转述直接出一段即可)。 + 只谈文风遣词 → 「叙事指南与故事推进」。 + 只钉监控栏字段清单、版式已定 → 「设计监控栏」。 + 块结构未定时不要先空谈 CSS 细节。 boundary: | - 本能力:单轮可见结构与拼接。 - 设计状态栏:状态栏块的字段细则。 - 美学纲领与交互范式:人称、系统扮演、体验边界——本步不重谈。 + 本技能:用户可见组成(块清单、顺序、可见/隐藏、前端拆分意图);产出 context-fragment.v1。 + 不是 OpenAI/函数调用报文,也不是结算包 schema。 + 设计监控栏:监控栏内有哪些可变字段——可本步给骨架,细则可交监控栏技能。 + 叙事指南与故事推进:怎么写正文——本步定「正文」块在版式里的位置与职责,不重写文风。 + 变量设计:真值如何变——本步只约定隐藏段如何承载变更声明,不写 side_effects。 +feeds: narrator ``` ## opening ```opening +用户每一轮「看见的东西」希望像什么?(想到什么写什么) + +1. 由哪些部分组成?什么顺序? + 例:抬头 + 日期/地点 + 正文;或正文 + 监控栏;或文末再加小吐槽/模拟书评…… + +2. 有没有要「藏起来给模型/程序看、用户界面默认不展示」的段? + 例:用特殊标记包住的变量变更,方便拆解维护表格。 + +3. 前端要不要拆成多个区域美化?(监控栏固定顶栏、正文滚动、文末折叠……)没有就说没有。 ``` ## task ```task -钉清单轮可见回复格式(块、顺序、可选面板),写入 设计.回复格式。对齐已验收的呈现契约。 -(待作者细写) +你正在执行「正文组成」。产物必须是 **context-fragment.v1** JSON,写入「设计.正文组成」。 + +本步设计的是**用户最后看见的内容版式**——像一封信由收信人、正文、日期、发信人组成; +也可以是抬头、日期、地点、正文、监控栏、文末吐槽、小故事、模拟书评等。 +**不是**程序接口格式。 + +执行顺序: +1. 读美学呈现与叙事指南:继承人称/终稿由谁写;不重开文风问卷。 +2. 列出「可见块」:每块职责、顺序、是否可空、由谁填充(转述/程序拼装/监控栏技能)。 +3. 若有监控栏:写清它在版式中的位置与职责;字段级细则可引用或留给「设计监控栏」(只留会变、要盯的信息)。 +4. 若有隐藏段:写清标记约定(如正则/定界符)、内容用途(变量变更声明)、用户侧默认隐藏、模型须输出以便拆表。 +5. 写「前端拆分与美化」意图(区域、可否折叠、是否 HTML 片段);无则说明纯 Markdown/纯文本。 +6. 自评 + 追问;输出 JSON。禁止写最终 context `order`。 + +summary:`正文组成 · …`(点题主要块序)。 ``` ## principles ```principles -结构服务体验;块要少;与状态栏/终稿 tag 约定一致。 +1. 用户看见的版式,不是程序报文 + 用「信/报纸/游戏 HUD」类比思考块;禁止把本步写成 API 字段说明书。 + +2. 块要少、每块有职责 + 宁缺毋滥;装饰块必须服务体验(吐槽/书评若只是玩梗也要写清触发与长度)。 + +3. 监控栏 ≠ 名片卡 + 监控的是会变、影响决策或沉浸的信息(好感、体力、场景可交互对象、攻略目标状态…)。 + 主角「姓名」「性别」等几乎不变的内容默认不要进监控栏(绝大多数局)。 + +4. 隐藏段服务维护,不污染阅读 + 变量变更等可用定界符包住,供 LLM 输出、程序/旁路拆解入表;用户 UI 默认不渲染或折叠。 + +5. 前端意图只写「拆哪里、干什么」 + 可提:顶栏监控 / 正文区 / 文末折叠 / 简易 HTML 片段。不写完整 CSS 工程。 + +6. 与转述、变量分工 + 正文块文风归叙事指南;真值规则归变量设计;本步定拼装契约。 ``` ## probe ```probe -(待作者细写) +每轮 1~2 点。 + +优先:必须有哪些可见块;监控栏要不要、盯什么类型信息;隐藏变量段要不要及标记长什么样;前端要不要分区。 +不追问:完整 CSS、事件池 schema、文风遣词细节(除非块职责不清)。 ``` ## output ```output { - "块顺序": ["状态栏", "正文", "…"], - "正文约定": "…", - "可选面板": [], - "终稿tag或拼装说明": "…" + "schema": "context-fragment.v1", + "技能": "正文组成", + "brief": "一句话:用户看见的主要块序", + "mount": ["narrator"], + "稳变": "stable", + "正文": { + "依据的体验与呈现": [ + "从美学/叙事继承的呈现要点" + ], + "版式隐喻": "例:一封信 / 网文章节页 / 游戏回合面板 / 无(纯散文)", + "可见块": [ + { + "块id": "英文 kebab 或中文短名,同局稳定", + "显示名": "用户可理解的标题;可无标题则空", + "职责": "这块给用户什么信息", + "顺序": 1, + "可空": true, + "填充方": "叙事转述|程序拼装|监控栏|其它", + "内容形态": "散文|短列表|键值行|HTML片段|其它", + "示例": "可选一句/三行示意" + } + ], + "监控栏锚点": { + "本局是否启用": "是|否", + "在可见块中的块id": "若启用则指向可见块之一", + "职责摘要": "盯哪些类信息(详表见设计.监控栏或本步字段草稿)", + "字段草稿": [ + { + "名": "会变且值得盯的字段", + "为何监控": "决策/沉浸理由", + "来源意图": "真值|投影|临场生成" + } + ] + }, + "隐藏段": [ + { + "段id": "variable-delta", + "用途": "变量维护语句(与「变量设计.维护语句约定」同构),供程序拆进变量.当前", + "定界或正则约定": "例:<<>>...<<>>;形状细节以变量设计为准", + "用户界面": "默认隐藏|折叠可见|调试可见", + "模型必须输出": true, + "内容形状": "短键值/JSON行;非 chance toolcall", + "示例": "可选" + } + ], + "前端拆分与美化": { + "需要前端分区": "是|否", + "区域": [ + { + "区域id": "monitor|body|footer|…", + "对应块id": ["…"], + "意图": "固定顶栏|主滚动|折叠文末|…", + "渲染提示": "纯文本|Markdown|受限HTML;勿写完整工程" + } + ], + "说明": "无分区则写「单流渲染」" + }, + "拼装与终稿": { + "终稿tag": "通常 输出.用户展示", + "拼装方": "叙事转述一次写出各块|程序按块拼接|混合", + "与裁决包关系": "转述读 settlement 再填各块;隐藏段可含 variable_changes 镜像" + } + }, + "自评": { + "维度": [ + { + "名": "可见完备", + "分数": 0, + "说明": "用户该看见的块是否齐、顺序是否服务体验" + }, + { + "名": "监控克制", + "分数": 0, + "说明": "监控栏是否只盯会变信息;无姓名性别等死字段堆砌(若无监控栏可 N/A)" + }, + { + "名": "可实现", + "分数": 0, + "说明": "隐藏段约定与前端拆分是否清楚到可交给转述/程序/前端" + } + ], + "薄弱点": "一句话" + }, + "追问": { + "导语": "为钉准用户看见的版式,还需确认:", + "题目": [ + { + "问": "…", + "建议选项": ["选项A", "选项B", "其它(请写明)"], + "示例": "可选" + } + ] + }, + "开放问题": [] } ``` +硬规则: +1. 合法 JSON;含公共三段外壳。 +2. 正文键固定:依据的体验与呈现、版式隐喻、可见块、监控栏锚点、隐藏段、前端拆分与美化、拼装与终稿。 +3. `可见块` 至少 1 条(通常含正文);`隐藏段`/`字段草稿` 可 `[]`。 +4. 禁止把本步写成程序 API schema;禁止 `order`(上下文投影序)。 +5. 自评:可见完备 / 监控克制 / 可实现。 +6. summary:`正文组成 · {brief 缩略}`。 +7. 兼容旧称产物 tag「设计.回复格式」:细化终稿可读新 tag,旧局可仍挂旧名。 + ## checklist ```checklist -- [ ] 是否与美学纲领的呈现/轮转一致? -- [ ] 状态栏块是否指向 设计.状态栏(若有)? +- [ ] 明确是用户可见版式,而非程序报文? +- [ ] 可见块有顺序与职责?监控栏未塞不变名片字段? +- [ ] 隐藏段(若有)定界与用途清楚?前端拆分有或明确单流? +- [ ] 自评三维?未越权重写文风或 side_effects? +``` + +## examples + +```examples +好: +- 块序:监控栏 → 正文 → 文末吐槽;隐藏段 <<>> 维护好感变更。 +- 书信局:抬头/日期/正文/落款;无监控栏。 +- 监控栏只盯:体力、攻略目标好感、场景可交互对象——不写主角姓名性别。 + +坏: +- 把 settlement.v1 字段表当「回复格式」。 +- 监控栏列出姓名、性别、种族等死设定。 +- 只写「要好看的 UI」无块清单。 ``` diff --git a/skills/dialogue/world-simulator/modules/status-bar/prompt.md b/skills/dialogue/world-simulator/modules/status-bar/prompt.md index d8bdc14..4f71a6b 100644 --- a/skills/dialogue/world-simulator/modules/status-bar/prompt.md +++ b/skills/dialogue/world-simulator/modules/status-bar/prompt.md @@ -1,59 +1,189 @@ -# 设计状态栏 +# 设计监控栏 -> **状态:待完善** — 块格式见 `docs/world-simulator-modules.md`;范例见 `aesthetics-interaction`。 +> 技能文档。程序只切割下方 **fence 块**;`##` 标题仅供人读。 +> 产物外壳:`docs/context-fragment-design.md`;字段筛选气质对齐「生成规则」属性妥当。 +> 旧称:设计状态栏。更名因:这里盯的是**会变、要监控的信息**,不是名片式「状态展览」。 ## meta ```meta -name: 设计状态栏 +name: 设计监控栏 id: status-bar -artifact: 设计.状态栏 -declaration: 钉用户可见状态栏:字段、刷新时机、与正文如何拼装 -when: 体验需要程序拼状态栏+正文,或用户要持续可见关键状态时 -when_not: 纯单段叙事、明确不要 HUD/状态条时 +artifact: 设计.监控栏 +declaration: > + 钉用户可见监控栏字段:只留会变、影响决策或沉浸的信息 + (主角状态/攻略目标/场景可交互对象等);不进姓名性别等死字段 +when: | + 「正文组成」已启用监控栏,或体验需要持续可见的监控信息时; + 需要逐字段筛「该不该监控」时。 +when_not: | + 明确不要 HUD/监控条 → 不排。 + 版式块序未定、连有没有监控栏都不清 → 先「正文组成」。 + 真值如何增减 → 「变量设计与更新规则」(本步只选展示哪些)。 boundary: | - 本能力:可见状态栏字段与刷新/拼装。 - 变量设计与更新规则:背后变量如何变。 - 设计回复格式:整轮输出结构(状态栏可为其一块)。 + 本技能:监控栏字段清单、来源意图、刷新与可见条件;产出 context-fragment.v1。 + 正文组成:监控栏在整轮版式中的位置——本步不重排其它块。 + 变量设计:真值与 side_effects——本步可引用字段名,不写更新公式。 + 生成规则:内容表 schema——监控栏不是生成实例表,但同样忌冗余字段。 +feeds: narrator ``` ## opening ```opening +这一局监控栏要让用户盯住什么?(想到什么写什么) + +1. 监控对象是谁/哪一层? + 例:主角生存资源;攻略目标态度;当前场景可交互对象;任务进度…… + +2. 哪些数字或短状态会变、且变了会影响你下一步? + 不要列几乎不变的名片(姓名、性别等——绝大多数局都不需要)。 + +3. 刷新节奏?每轮都刷,还是事件后才变? ``` ## task ```task -钉清用户可见状态栏:字段、来源、刷新时机、与正文拼装方式。写入 设计.状态栏。 -(待作者细写) +你正在执行「设计监控栏」。产物必须是 **context-fragment.v1** JSON,写入「设计.监控栏」。 + +监控栏 = 用户可见的**监控**区:只放有用、会变的信息。 +可含:主角资源/状态、攻略目标状态、当前场景可交互对象等——按体验需要选,不默认堆满。 + +执行顺序: +1. 读正文组成(若有)与变量设计:继承栏位位置与已有真值名。 +2. 逐字段过筛(同生成规则「属性妥当」):对本局是否值得监控?不变的名片字段删除。 +3. 写清每字段:名、为何监控、来源意图、可见条件、展示形态(短文本/数值/列表)。 +4. 写刷新时机与空态(无目标/无交互物时显示什么)。 +5. 自评 + 追问;输出 JSON。 + +summary:`设计监控栏 · …`(点题监控对象)。 ``` ## principles ```principles -只展示影响决策或沉浸的字段;与变量设计对齐。 +1. 监控 ≠ 角色卡 + 姓名、性别、种族等几乎不变内容默认排除(除非玩法就是改性别/改名且必须盯)。 + +2. 只留有用消息 + 能回答:变了以后用户会不会改决策或沉浸感?不能则删。 + +3. 对象可以多层 + 不限于「主角状态」:攻略目标、场景可交互列表、世界危机进度等都可进栏——但每层字段仍要克制。 + +4. 来源可追溯 + 字段应能指向真值、投影或临场短生成;禁止无来源的装饰假数。 + +5. 展示短 + 监控栏不是正文;长描写放叙事,栏内短标签/数字/一行列表。 ``` ## probe ```probe -(待作者细写) +每轮 1~2 点:缺哪类监控对象;某字段是否死字段该删;刷新过频还是过稀。 +不追问:整页 CSS、文风、完整变量更新公式。 ``` ## output ```output { - "字段": [{ "名": "…", "来源": "…", "可见条件": "…" }], - "刷新": "每轮|事件|…", - "拼装": "状态栏在正文前|后|旁|…" + "schema": "context-fragment.v1", + "技能": "设计监控栏", + "brief": "一句话:监控谁/什么", + "mount": ["narrator"], + "稳变": "stable", + "正文": { + "依据": ["正文组成锚点 / 变量设计字段 / 体验需要"], + "监控对象": [ + { + "对象id": "pc|target|scene|world|…", + "名称": "主角|攻略目标|当前场景|…", + "为何出现在栏内": "…" + } + ], + "字段": [ + { + "名": "短名,用户可见", + "对象id": "归属上述对象", + "为何监控": "变了如何影响决策或沉浸", + "来源意图": "真值键名|投影tag|临场", + "类型示意": "number|string|short_list|enum", + "可见条件": "始终|某阶段|有目标时…", + "展示形态": "例:❤ 37;或一行「可交互:门、柜」", + "禁止原因若曾候选": "例:性别——不变,已剔除;无则空" + } + ], + "刻意不监控": [ + "姓名、性别…及原因" + ], + "刷新": { + "时机": "每轮|事件后|变量边沿|手动", + "空态": "无数据时显示什么" + }, + "与版式关系": "在正文组成中的块位置;若尚无正文组成则写建议位置" + }, + "自评": { + "维度": [ + { + "名": "属性妥当", + "分数": 0, + "说明": "逐字段:无死名片;该盯的生存/关系/场景信息未漏" + }, + { + "名": "克制度", + "分数": 0, + "说明": "字段是否够少、展示是否够短" + }, + { + "名": "可接线", + "分数": 0, + "说明": "来源意图是否接得上变量/投影/正文组成" + } + ], + "薄弱点": "一句话" + }, + "追问": { + "导语": "为钉准监控栏,还需确认:", + "题目": [ + { + "问": "…", + "建议选项": ["选项A", "选项B", "其它(请写明)"], + "示例": "可选" + } + ] + }, + "开放问题": [] } ``` +硬规则: +1. 合法 JSON;公共三段外壳。 +2. 正文键固定:依据、监控对象、字段、刻意不监控、刷新、与版式关系。 +3. 自评:属性妥当 / 克制度 / 可接线。 +4. 字段不得无故包含姓名/性别等死设定(列入「刻意不监控」并说明)。 +5. summary:`设计监控栏 · {brief 缩略}`。 +6. 兼容旧 artifact「设计.状态栏」。 + ## checklist ```checklist -- [ ] 每个字段是否有变量/表来源? -- [ ] 是否与回复格式拼装不冲突? +- [ ] 字段均为会变且有用?死名片进了「刻意不监控」? +- [ ] 可含攻略目标/场景交互物等非主角对象? +- [ ] 来源意图清楚?未写成长文正文? +``` + +## examples + +```examples +好: +- 末日:体力、口粮、威胁等级;刻意不监控姓名性别。 +- 恋爱:攻略目标好感/心情;场景可交互:公园长椅、便利店。 +- 字段 3~7 个,短展示。 + +坏: +- 监控栏 = 完整人物卡(名/性/龄/身高/籍贯…)。 +- 把叙事指南整段塞进栏内。 ``` diff --git a/skills/dialogue/world-simulator/modules/topology/prompt.md b/skills/dialogue/world-simulator/modules/topology/prompt.md index 0f8a300..638da73 100644 --- a/skills/dialogue/world-simulator/modules/topology/prompt.md +++ b/skills/dialogue/world-simulator/modules/topology/prompt.md @@ -13,7 +13,7 @@ when: 实现机制大致清楚,需要钉依赖边、触发边与数据流向 when_not: 尚未决定有哪些执行单元就先画复杂图 boundary: | 本能力:依赖/触发/读写流向。 - Worker 规格:单个单元契约。 + 游玩拓扑:固定槽勾选;本步若仍需要,只画已启用槽之间的数据流。 实现机制:总览「要哪些件」,本步钉件与件的边。 ``` diff --git a/skills/dialogue/world-simulator/modules/variable-context/prompt.md b/skills/dialogue/world-simulator/modules/variable-context/prompt.md index 1ddb114..f49aef1 100644 --- a/skills/dialogue/world-simulator/modules/variable-context/prompt.md +++ b/skills/dialogue/world-simulator/modules/variable-context/prompt.md @@ -1,6 +1,7 @@ # 变量控制上下文 -> **状态:待完善** — 块格式见 `docs/world-simulator-modules.md`;范例见 `aesthetics-interaction`。 +> 技能文档。程序只切割下方 **fence 块**;`##` 标题仅供人读。 +> 上游:`variable-design`;方法:`docs/progressive-data-design.md`。 ## meta @@ -8,50 +9,145 @@ name: 变量控制上下文 id: variable-context artifact: 设计.变量控制上下文 -declaration: 钉哪些变量如何挂进常驻上下文 / 控制生成口径(可见性与措辞) -when: 变量已大致设计,需要规定它们如何进入 worker 上下文与措辞时 -when_not: 尚无变量,或变量仅程序内部、从不进提示词时 +declaration: > + 钉真值/投影/Data 摘要如何挂进主世界层、叙事转述与旁观: + 可见性、措辞、未解锁不注入;汇总映射当前档供旁观 agent +when: | + 变量设计已有真值或 Data 映射索引,需要规定谁看见什么、旁观看哪份摘要时。 +when_not: | + 尚无变量且无投影。 + 仅内部记账永不进提示词 → 可跳过,在变量设计备注即可。 + 只改字段与更新规则 → 变量设计与更新规则。 boundary: | - 本能力:变量 → 上下文挂载与生成口径。 - 变量设计与更新规则:字段与改值规则本身。 + 本技能:挂载表、剧透边界、旁观汇总视野、与维护语句/投影 tag 对齐。 + 变量设计:字段、映射索引、维护语句形状、side_effects。 + 具体实例:表行正文——本步不重生成,只声明「旁观读当前档/哪几个摘要键」。 + 设计监控栏 / 正文组成:用户可见层;本步管提示词视野。 +feeds: gm ``` ## opening ```opening +有没有「玩家不该看见、但主世界层/旁观要知道」或「到了某档才允许提起」的状态? +旁观需要看「当前好感档性格要点」这类汇总吗?有则简述;没有就「没有信息差」。 ``` ## task ```task -钉清变量如何挂进常驻上下文、控制哪些生成口径。写入 设计.变量控制上下文。 -(待作者细写) +你正在执行「变量控制上下文」。产物必须是 **context-fragment.v1** JSON,写入「设计.变量控制上下文」。 + +在已有真值 / Data 映射索引 / side_effects 上钉: +1. 主世界层每轮见哪些真值键与投影 tag +2. 叙事转述见哪些玩家安全切片 +3. **旁观汇总**:从映射索引+投影拼出的只读摘要(给旁观 agent,不写 canon) +4. 未达阈值不注入;禁止泄露列表 + +不重开变量清单;不重写具体实例整表。 + +summary:`变量控制上下文 · …`。 ``` ## principles ```principles -只挂影响生成的变量;措辞服务体验,不泄露不该剧透的内部态(除非体验需要)。 +1. 只挂影响生成的状态;整张 Data 不进 static。 +2. 旁观要「当前档汇总」,材料来自映射索引的摘要键 + 已投影 tag;不是第二份实例库。 +3. Progressive:未解锁档正文不进上下文。 +4. 维护语句由主世界层按变量设计输出;本步只保证挂载后能读到合并后的真值/投影。 +5. 默认拓扑:主世界层 + 可选转述 + 可选旁观挂载;不为挂载再拆执行单元。 ``` ## probe ```probe -(待作者细写) +主世界层要不要隐藏真值?旁观汇总要哪些摘要键?转述是否禁止好感数字?一次 1~2 点。 ``` ## output ```output { - "挂载": [{ "变量": "…", "挂到": "常驻|某worker|…", "措辞要点": "…" }], - "禁止泄露": ["…"] + "schema": "context-fragment.v1", + "技能": "变量控制上下文", + "brief": "一句话:各槽看见什么状态", + "mount": ["world-simulator"], + "稳变": "stable", + "正文": { + "挂载": [ + { + "对象": "主世界层|叙事转述|旁观|常驻", + "ref或mount": "world-simulator|narrator|…", + "真值键": ["好感"], + "投影tag": ["上下文.角色态度"], + "映射摘要": ["从映射id取当前档的哪些键;无则 []"], + "措辞要点": "…", + "未达阈值": "对应投影不注入" + } + ], + "旁观汇总": { + "启用": "是|否", + "tag": "上下文.旁观.状态摘要", + "组成": [ + "真值:好感数字(仅旁观)", + "映射 affinity-personality:当前档态度要点+禁触", + "已触发 once 门列表" + ], + "更新时机": "真值变更后 / 与 side_effects 同时", + "禁止": ["整表粘贴", "写回 canon"] + }, + "禁止泄露": [ + { + "内容": "…", + "对谁禁": "叙事转述|用户展示|…", + "原因": "…" + } + ], + "与side_effects对齐": ["上下文.角色态度 ← affinity-romance"], + "与维护语句对齐": "主世界层读合并后真值;维护语句形状见变量设计,本步不改" + }, + "自评": { + "维度": [ + { + "名": "视野正确", + "分数": 0, + "说明": "各槽该见/不该见是否清楚" + }, + { + "名": "旁观可用", + "分数": 0, + "说明": "旁观汇总是否够用且未整表;不需要旁观则 N/A" + }, + { + "名": "防剧透", + "分数": 0, + "说明": "未解锁与禁止泄露是否落实" + } + ], + "薄弱点": "一句话" + }, + "追问": { + "导语": "为钉准状态视野,还需确认:", + "题目": [] + }, + "开放问题": [] } ``` +硬规则:公共三段;旁观汇总可「启用=否」;禁止发明变量执行单元;summary 见 task。 + ## checklist ```checklist -- [ ] 挂载是否与变量设计名单一致? -- [ ] 剧透边界是否与体验禁忌一致? +- [ ] 挂载与变量设计名单一致? +- [ ] 旁观汇总用摘要键而非整表? +- [ ] 投影 tag 与 side_effects 一致?剧透边界清楚? +``` + +## examples + +```examples +好:旁观 tag 只含「当前档态度要点 + 好感」;转述只见投影态度不见数字。 +坏:把具体实例整份分档 JSON 挂进旁观 static。 ``` diff --git a/skills/dialogue/world-simulator/modules/variable-design/prompt.md b/skills/dialogue/world-simulator/modules/variable-design/prompt.md index 56226ad..d8dcfa8 100644 --- a/skills/dialogue/world-simulator/modules/variable-design/prompt.md +++ b/skills/dialogue/world-simulator/modules/variable-design/prompt.md @@ -1,6 +1,8 @@ # 变量设计与更新规则 -> **状态:待完善** — 块格式见 `docs/world-simulator-modules.md`;范例见 `aesthetics-interaction`。 +> 技能文档。程序只切割下方 **fence 块**;`##` 标题仅供人读。 +> 方法:`docs/progressive-data-design.md`;外壳:`docs/context-fragment-design.md`。 +> 与「正文组成」隐藏段、生成规则/具体实例的 Data 表、旁观汇总对齐。 ## meta @@ -8,58 +10,217 @@ name: 变量设计与更新规则 id: variable-design artifact: 设计.变量设计与更新规则 -declaration: 钉变量字段、初值与更新时机/规则;与表副作用对齐 -when: 需要可追踪状态(进度、关系、资源等)且要写清谁何时改时 -when_not: 无状态纯对话、或状态仅散文描述从不程序化时 +declaration: > + 钉真值、谁可写、如何变;起草 side_effects;约定维护语句形状; + 汇总 Data 映射索引(分档/实例来自生成规则与具体实例)供投影与旁观 +when: | + 体验需要可追踪状态,且下游要可靠查询、门控、防重复或按值换文案时。 +when_not: | + 无状态纯对话,或状态只临场描写、从不程序化。 + 只要大段设定不要门控 → 舞台骨架 / 生成规则。 + 只规定挂进谁的提示词 → 「变量控制上下文」。 + 只规定用户看见哪些监控字段 → 「设计监控栏」。 boundary: | - 本能力:字段、初值、更新规则、与副作用。 - 变量控制上下文:这些变量如何进入提示词口径。 - 设计状态栏:哪些对用户可见。 + 本技能:真值清单、更新规则、维护语句约定、Data 映射索引、side_effects 草案。 + 分档长文/性格切片正文:由生成规则定格式、具体实例填表;本步做索引与门控,不重写整表。 + 正文组成:隐藏段定界符——本步钉「维护语句写什么」;定界样式与之对齐。 + 机遇裁定 chance:真随机 toolcall,不是变量表维护手段。 + 变量控制上下文:挂载对象与旁观汇总视野。 + 细化终稿:收成 tables.side_effects。 +feeds: gm ``` ## opening ```opening +先说「玩的时候必须记住、否则体验会糊」的东西: + +1. 数字/阶段?(好感、章节、资源…)一次性门? +2. 有没有「到了某值换一套态度/大纲」的表?(表正文可已在具体实例里) +3. 每轮模型用什么方式声明变量变了?(裁决包字段 / 隐藏段键值 / 两者) +4. 旁观或主世界层是否需要一份「当前映射摘要」(如好感档→性格要点)? ``` ## task ```task -钉清变量字段、初值与更新规则,写入 设计.变量设计与更新规则。与表/副作用命名对齐。 -(待作者细写) +你正在执行「变量设计与更新规则」。产物必须是 **context-fragment.v1** JSON,写入「设计.变量设计与更新规则」。 + +三分: +- **真值**:跨轮必须记住的少数字段(好感、章节…) +- **Data 映射**:阈值→切片大表(性格档、章纲、事件池)——格式归生成规则,行归具体实例;本步建索引并指向实例/规则 id +- **Progressive**:side_effects 按真值边沿投影当前切片;旁观/主世界层读投影或汇总,不整表常驻 + +**维护语句**:规定模型每轮如何声明变更(供程序拆进 `变量.当前` / 裁决包 `variable_changes`)。 +这不是 chance toolcall;toolcall 只做骰子/抽签。隐藏段定界与「正文组成」对齐。 + +执行顺序: +1. 读美学/机制/生成规则/具体实例/正文组成;不重开体验。 +2. 立真值(承重才建);写写入源与更新规则。 +3. 建 Data 映射索引:每张表写清键依哪个真值、材料来自哪条 rule_id/batch、给旁观/投影用的摘要字段。 +4. 写维护语句约定:形状、必填键、与隐藏段/settlement 的关系、示例一句。 +5. 写 side_effects(once/every_edge → replace_tag 等)。 +6. 自评 + 追问;输出 JSON。 + +summary:`变量设计 · …`(点题主真值 + 有无映射表)。 ``` ## principles ```principles -字段要少;更新规则可检查;禁止隐式改值。 +1. 承重才立真值;派生性格不落第二可写格。 +2. 映射内容:具体实例生成 → 本步索引汇总 → side_effects/变量控制上下文供给旁观与主世界层。 +3. 维护靠「声明 + 程序合并」,不靠模型直接改库: + - 主路径:裁决包 variable_changes 和/或正文隐藏段(正则/定界)→ Runtime 合并进 变量.当前 + - 边沿:side_effects 换投影 tag + - 用户手改;chance 只出随机数,不替代维护语句 +4. 维护语句必须可生成、可校验:固定键名、短值、禁止散文糊弄。 +5. 旁观要看的是「当前档摘要」,不是整份实例库;汇总句或投影 tag 即可。 +6. 宁少勿多;与监控栏字段可交集但职责不同(监控=用户看见;真值=程序真相)。 ``` ## probe ```probe -(待作者细写) +每轮 1~2 点:主真值;连续数字还是阶段枚举;维护走裁决包还是隐藏段还是两者;映射表是否已有具体实例可索引;旁观要不要当前档摘要。 ``` ## output ```output { - "变量": [ - { - "名": "…", - "类型": "…", - "初值": "…", - "更新": "谁、何时、怎么变" - } - ], - "副作用备注": "…" + "schema": "context-fragment.v1", + "技能": "变量设计与更新规则", + "brief": "一句话:主真值 + 映射/维护方式", + "mount": ["world-simulator"], + "稳变": "stable", + "正文": { + "依据的体验": ["从机制/美学提取的状态化点"], + "真值": [ + { + "名": "好感", + "类型": "number|enum|boolean|string|set", + "初值": 0, + "对用户可见": true, + "对主世界层可见": true, + "写入源": ["llm", "用户"], + "更新": { + "时机": "…", + "方式": "delta|绝对值|枚举切换", + "钳制": "0..100|未定", + "执行单元": "world-simulator" + }, + "备注": "" + } + ], + "Data映射索引": [ + { + "映射id": "affinity-personality", + "用途": "好感分档性格|章大纲|场景包|事件池|…", + "键依真值": "好感", + "键规则": "例:区间 [0,30)/[30,60)/[60,100]", + "结构摘要": "阈值 → 态度/行为切片字段…", + "生成规则rule_id": "来自设计.生成规则;无则待补", + "具体实例batch_id": "来自设计.具体实例;无则待补", + "旁观与投影要用的摘要键": ["档名", "态度要点", "禁触"], + "说明": "长文在实例表;本索引只为查表与汇总" + } + ], + "维护语句约定": { + "通道": ["settlement.variable_changes", "隐藏段", "二者都要"], + "与正文组成对齐": "隐藏段段id / 定界约定;无隐藏段则写「仅用裁决包」", + "语句形状": { + "格式": "JSON行|键值列表|与 settlement 字段同构", + "必填键": ["key", "delta|to"], + "禁止": ["无键散文", "改未立真值"] + }, + "生成要求": "主世界层每轮须输出可解析维护语句(哪怕本轮无变更也给空列表)", + "示例": [ + { "key": "好感", "delta": 8, "reason": "送礼成功" } + ], + "程序侧": "Runtime 合并进 变量.当前;再跑 side_effects;非 chance toolcall" + }, + "side_effects": [ + { + "id": "affinity-romance", + "field": "好感", + "op": "gte", + "value": 60, + "mode": "once", + "action": { + "type": "replace_tag", + "tag": "上下文.角色态度", + "content": "恋爱档要点或「按映射id查当前档摘要」" + } + } + ], + "旁观汇总意图": { + "需要": "是|否", + "汇总什么": "例:当前好感档性格要点 + 已触发 once 门", + "建议tag": "上下文.旁观.状态摘要", + "材料来自": ["Data映射索引", "投影tag", "真值字段"] + }, + "不立变量的理由": ["…"] + }, + "自评": { + "维度": [ + { + "名": "承重度", + "分数": 0, + "说明": "真值是否只覆盖必须跨轮记住的状态" + }, + { + "名": "可维护", + "分数": 0, + "说明": "维护语句是否可解析;映射索引是否接得上实例/规则" + }, + { + "名": "克制度", + "分数": 0, + "说明": "无派生可写格膨胀;无用 chance 冒充改表" + } + ], + "薄弱点": "一句话" + }, + "追问": { + "导语": "为钉准变量与维护,还需确认:", + "题目": [ + { + "问": "…", + "建议选项": ["选项A", "选项B", "其它(请写明)"], + "示例": "可选" + } + ] + }, + "开放问题": [] } ``` +硬规则: +1. context-fragment.v1 外壳;自评承重度/可维护/克制度。 +2. Data 映射索引可指向尚未生成的实例(标待补),但键与用途必须清。 +3. 维护语句约定必填;须声明通道与示例。 +4. side_effects 可 `[]`。禁止输出完整 workers[]。 +5. summary:`变量设计 · {brief 缩略}`。 + ## checklist ```checklist -- [ ] 每个变量是否有明确更新者与时机? -- [ ] 是否与拓扑/表设计一致? +- [ ] 真值有写入源与更新时机? +- [ ] 映射索引指向规则/实例或标待补?旁观摘要键清楚? +- [ ] 维护语句可解析且与正文组成/settlement 对齐? +- [ ] 未把分档长文做成可写真值?未用 chance 当改表? +``` + +## examples + +```examples +好: +- 好感真值 + 映射索引指向 concrete batch「分档性格」+ side_effect 投影态度 + 维护语句走 variable_changes。 +- 旁观汇总意图:只挂当前档「态度要点」,不挂整表。 + +坏: +- 每轮让模型重写「当前性格」长字符串当真值。 +- 只写「好感会变」无维护语句形状。 +- 用 run_worker chance 来改好感。 ``` diff --git a/skills/dialogue/world-simulator/modules/worker-spec/prompt.md b/skills/dialogue/world-simulator/modules/worker-spec/prompt.md index 522de8a..f716178 100644 --- a/skills/dialogue/world-simulator/modules/worker-spec/prompt.md +++ b/skills/dialogue/world-simulator/modules/worker-spec/prompt.md @@ -1,135 +1,147 @@ -# Worker 规格 +# 游玩拓扑 > 能力文档。程序只切割下方 **fence 块**;`##` 标题仅供人读。 -> 写法说明:`docs/world-simulator-modules.md`;范例:`aesthetics-interaction`。 -> 可选默认契约见 `worker-templates/`(缺省合并用,非本步全文)。 +> **禁止自由发明执行单元。** 只从固定槽勾选;程序按 `play_slots` 展开 workers。 +> 方法:`docs/progressive-data-design.md`;模板:`worker-templates/`。 ## meta ```meta -name: Worker 规格 +name: 游玩拓扑 id: worker-spec artifact: 设计.worker规格 declaration: > - 钉一个游玩期执行单元(职责、读写、挂载);多演员时可多次调用 + 勾选固定游玩槽位(主世界层 / 叙事转述 / 可选角色视角 / 可选机遇裁定;写手路径为大纲+章节), + 禁止自由发明新的执行单元 ref when: | - 已能说出「游玩时谁上场做什么」,需要把某一个演员钉成可调度契约时; - 或多演员需分次钉清时(本能力可反复)。 + 体验契约已大致清楚,需要决定游玩期启用哪些固定槽时; + 或要修订已勾选槽位(开/关 perspective、chance 等)时。 when_not: | - 体验/机制仍混沌,还说不清删掉谁会坏体验时 → 先上游能力。 - 已在收成「细化终稿」且只需合并已有规格时 → 交给细化终稿,勿重复问卷。 + 体验/站位仍混沌 → 先美学纲领与交互范式。 + 只需收成完整运行规格 → 交给细化终稿(本步只交槽位勾选)。 + 不要用本步「发明」世界观/性格/变量专用执行单元。 boundary: | - 本能力:一次(或本步焦点内)钉清一个游玩期执行单元:中文名、ref、职责、何时上场、读写 tag、验收点、为何需要。 - 产物写入「设计.worker规格」(可含累积列表);最终合并进「设计.worker集」由「细化终稿」完成。 - 细化终稿:收成完整 Worker 集、表、常驻上下文;本步不假装交终稿。 - 拓扑图谱:多演员依赖与数据流总图;本步可写本单元读写,不画全图。 - 生成规则 / 叙事指南:规则与态度正文;本步只声明挂载哪些 tag,不重写全文。 + 本能力:输出 play_slots(及写手路径的 writing_slots),可选覆盖挂载说明;不写完整 设计.worker集。 + 细化终稿:按本步勾选展开 workers、合并常驻与 tables。 + 变量设计 / 变量控制上下文:真值与投影,不是推理槽。 + 机遇裁定(chance):按需程序工具槽,不进每轮管线。 + 叙事指南与故事推进:挂到转述槽(推进可兼挂主世界层);世界/机制:挂到主世界层——本步只点名槽。 ``` ## opening ```opening -先点名「下一个要钉的演员」(一个即可): +游玩时用哪些固定槽?(只勾选,不要发明新角色名当「新系统」) -1. 中文称呼:TA 在游玩里叫什么?(例:世界推进、叙事转述、大纲、章节正文) -2. 职责一句话:删掉 TA 会丢掉哪段体验? -3. 何时上场:每轮?用户点名写章时?某条件触发? +世界模拟类常见: +1. 主世界层(裁决,几乎总要)——要 / 不要 +2. 叙事转述(写你看见的正文,几乎总要)——要 / 不要 +3. 角色视角(仅当有强秘密、不能进主世界层时)——要 / 不要(默认不要) +4. 机遇裁定(骰子/抽签/比点等真随机,按需调用、不进每轮)——要 / 不要(默认不要;有战斗检定、抽签事件时建议开) -若你已有多个演员想法,先写最核心的一个;其余可再跑本能力。 +写手/扩写类常见:大纲/细纲 + 章节正文(固定两槽,同上只勾选)。 + +变量、性格分档、世界观:不是槽,后面用变量/世界等技能收进上下文。 ``` ## task ```task -你正在执行剧本中的「Worker 规格」步骤。产物写入「设计.worker规格」。 +你正在执行「游玩拓扑」。产物写入「设计.worker规格」。 -一次 design-step 以钉清**一个**游玩期执行单元为主;若用户一次抛出多个且关系简单,可写入 `actors` 数组但须逐个写清 rationale,并在 summary 标明本步焦点。 +核心操作:让用户勾选固定槽,写成 `play_slots`(世界模拟)或 `writing_slots`(扩写)。**禁止**新建未在固定列表中的 ref。 + +固定 ref 白名单: +- 世界模拟每轮:`world-simulator`(gm)、`narrator`、`role-decide`(perspective,默认关) +- 世界模拟按需:`chance`(机遇裁定,默认关;invocation=on_demand) +- 扩写:`outline`、`chapter-writer` +- 禁止:`variable-update`、自造 kebab、为世界观/性格再拆槽 执行顺序: -1. 读依赖产物与体验契约;正推「需要谁」——禁止题材默认演员套餐。 -2. 若已有「设计.worker规格」,增量:同 ref 更新;新 ref 追加;不要无故删除用户已验收条目(除非用户要求改)。 -3. 为该单元填写:name(中文)、ref(英文 kebab,可与 worker-templates 对齐)、duty、when、rationale、acceptance、context/outputs 建议。 -4. acceptance:面向用户的可读终稿倾向 `review`;纯中间裁决/整理倾向 `continue`;吃不准就 ask_user。 -5. ref 可参考包内模板(如 narrator、world-simulator、outline、chapter-writer),但必须以本局体验为准,勿强行两端都上。 -6. 输出符合 output 的 JSON。 +1. 读配方与体验契约,判断路径:世界模拟 vs 写手分段。 +2. 默认世界模拟:`gm: true, narrator: true, perspective: false, chance: false`; + 仅信息隔离才开 perspective;需要骰子/抽签/比点等真随机时开 chance。 +3. 写手路径:`outline` + `chapter-writer` 默认都开;用户明确只要正文则可关 outline。 +4. 可写简短 `mount_notes`(哪类上游产物挂哪槽),不粘贴长文。 +5. 输出 JSON。summary:`游玩拓扑 · gm+转述` 或 `游玩拓扑 · gm+转述+机遇` 等。 -若程序已发出默认问题:禁止重复同一开场。 - -summary:`Worker 规格 · {中文名} · …` +若程序已发默认问题:禁止重复同一开场;在首答上补洞。 ``` ## principles ```principles -1. 删掉检验:说不清「丢掉哪段体验」的演员不要。 -2. 正推:体验 → 手段 → 演员;禁止「世界模拟就一定要 world-simulator + narrator」。 -3. 写手/分段常见最小集:大纲/细纲(outline)+ 章节正文(chapter-writer);按需加减。 -4. 扮演/世界推进常见:世界裁决 + 叙事转述;能合并则问用户是否合并。 -5. name 给人看,ref 给机器;二者成对出现。 -6. 本步不写完整表 schema、不写整份 Worker 集终稿。 -7. 常驻上下文挂载只点名「需要挂哪些已有产物 tag」,不在本步粘贴长文。 +1. 只勾选,不发明:ref 必须在白名单内。 +2. 默认少槽:世界模拟 = 主世界层 + 转述;perspective 默认关。 +3. 变量 / Data / Progressive 不是执行单元。 +4. 删掉检验仍适用:关某个槽要说得清损失什么。 +5. 本步不输出完整 Worker 集、不写 tables 全文(交给细化终稿 / 变量设计)。 +6. 可修订已有勾选,不要为「更聪明」加槽。 ``` ## probe ```probe -一轮 1~2 点。 +一次 1~2 点: -缺验收点:本演员产出是「给用户读的一段」还是「给下一演员的中间结果」? -缺 ref:更接近包内哪个模板职责?(给中文选项,勿逼用户记英文) -多演员纠结:能否合并成一个?合并会损失什么? -写手路径:是否需要「先大纲后正文」两个演员,还是只要分段写手? +- 正文是否必须由独立转述写?(不要则 gm 兼呈现,narrator=false——需用户明确) +- 是否有「主世界层不该知道的角色秘密」?(有才 perspective=true) +- 扩写:要不要先验大纲再写章? + +不要问「还想加什么 Worker」。 ``` ## output ```output { - "brief": "一句话:本步钉的演员如何服务体验", - "actors": [ - { - "name": "叙事转述", - "ref": "narrator", - "duty": "…", - "when": "…", - "rationale": "删掉则…", - "acceptance": "review", - "context": { - "static": ["设计.worker集"], - "dynamic": ["用户.最新输入"] - }, - "outputs": ["输出.用户展示"], - "presentation": { - "tone": "可选;转述类可填" - } - } + "brief": "一句话:本局启用哪些固定槽", + "path": "world_sim|writing", + "play_slots": { + "gm": true, + "narrator": true, + "perspective": false, + "chance": false + }, + "writing_slots": { + "outline": true, + "chapter_writer": true + }, + "mount_notes": [ + "叙事指南与故事推进 → narrator(推进兼 gm)", + "世界/机制/变量规则 → gm", + "真随机检定 → chance(按需)", + "真值与 side_effects → 细化终稿 tables" ], - "增量说明": "相对旧稿新增/改了哪个 ref", - "开放问题": ["…"] + "开放问题": [] } ``` -`acceptance` 只能是 `review` 或 `continue`。未知字段省略。 +填写规则: +- `path=world_sim` 时必须有 `play_slots`;`writing_slots` 可省略。 +- `path=writing` 时必须有 `writing_slots`;`play_slots` 可省略。 +- `chance` 缺省视为 false;为 true 时不进每轮序,仅可按需调度。 +- 不要输出自造 `actors[]` / 自由 `workers[]`。 +- 旧产物若含 `actors[]`:本步应改写为槽位勾选,不再追加自定义 ref。 ## checklist ```checklist -- [ ] 每个演员能否通过删掉检验? -- [ ] name/ref 是否成对?acceptance 是否写出? -- [ ] 是否误交完整 设计.worker集 或表结构? -- [ ] 是否题材默认套演员? -- [ ] 增量是否误删已有条目? +- [ ] 是否只有白名单槽,无自造 ref? +- [ ] perspective / chance / 只要正文等非常规选择是否有理由? +- [ ] 是否误把变量管理做成槽?是否误把 chance 当成每轮 LLM? +- [ ] 是否误交完整 设计.worker集? ``` ## examples ```examples 好: -- 扩写:先钉「大纲/细纲」outline(continue 或 review 按用户是否要验大纲),再另一步钉「章节正文」chapter-writer(review)。 -- 扮演:世界推进 continue + 叙事转述 review。 +- play_slots: gm+narrator,perspective/chance false;mount_notes 一行。 +- 有凶手真名不能进 GM:perspective true,并说明只出反应建议。 +- 需要检定/抽签:chance true(按需程序工具)。 坏: -- 一次甩出 8 个演员且无 rationale -- 只写英文 id 给用户看 -- 本步直接输出整份 version:1 Worker 集冒充终稿 +- actors 里发明 affinity-manager、lore-keeper、dice-master(LLM)。 +- 为性格分档再拆一个执行单元。 ``` diff --git a/skills/dialogue/world-simulator/modules/world-blueprint/prompt.md b/skills/dialogue/world-simulator/modules/world-blueprint/prompt.md index 5531acb..84e38b4 100644 --- a/skills/dialogue/world-simulator/modules/world-blueprint/prompt.md +++ b/skills/dialogue/world-simulator/modules/world-blueprint/prompt.md @@ -1,36 +1,35 @@ -# 世界蓝图与人文地理 +# 舞台骨架 -> 能力文档。程序只切割下方 **fence 块**(`meta` / `opening` / `task` / …);`##` 标题仅供人读。 -> 写法说明:`docs/world-simulator-modules.md`;泛用规范:`docs/briefs/capability-authoring-brief.md`。 +> 技能文档。程序只切割下方 **fence 块**;`##` 标题仅供人读。 +> 产物外壳:`docs/context-fragment-design.md`;范例:`aesthetics-interaction` / `mechanism`。 ## meta ```meta -name: 世界蓝图与人文地理 +name: 舞台骨架 id: world-blueprint -artifact: 设计.世界蓝图与人文地理 +artifact: 设计.舞台骨架 declaration: > - 当体验需要可引用的「舞台背景」时使用:钉舞台尺度、熟悉基底与变造、 - 关键格局与人文地理;只细化舞台上会出现的部分,不做设定百科或纸面地图 + 体验需要可引用的舞台背景时:钉尺度、基底与变造、关键舞台区,以及社会结构与世界状况; + 只细化会上台的部分,不做地图或设定百科 when: | 体验契约(及可选的实现机制)已大致清楚,但仍不能回答: - “这局故事发生在多大的舞台上?” - “背景世界以什么大家熟悉的基底成立,又变在哪里?” - “下游开局与生成需要引用哪些格局、势力或人文条件?” + “这局发生在多大的舞台上?以什么基底成立、变在哪里?” + “台上是谁在互动?世界以何种状况运转故事?” + “下游要引用哪些舞台区、社会格局或世界状况?” when_not: | - 核心体验尚未钉清时,不用本能力代替「美学纲领与交互范式」发明想要什么感觉。 - 只需识别体验支撑切面、不必展开环境与社会时,交给「实现机制」。 - 需要可点名的人/地/物名片时,交给「具体实例」。 - 需要内容如何持续生成或推进时,交给「生成规则」。 - 用户只要轻设定回合、明确拒绝世界骨架时,不要排入。 + 核心体验未钉清 → 先「美学纲领与交互范式」。 + 只需识别体验支撑切面、不必展开环境与社会 → 「实现机制」。 + 需要可点名的人/地/物名片 → 「具体实例」。 + 需要内容如何持续生成 → 「生成规则」。 + 用户只要轻设定、明确拒绝世界骨架 → 不要排入。 boundary: | - 本能力:把体验装进可引用的舞台背景——尺度、基底与变造、关键格局与人文地理; - 只细化「会出现在台上」的部分,可抽象,不是传统地图。 - 美学纲领与交互范式:体验是什么、用户怎么参与;本步不重定体验目标。 - 实现机制:体验靠哪些切面成立;本步把其中世界侧支点展开成骨架,不重做支撑检验。 - 具体实例:可点名的人/地/物条目;本步不定逐条名片与生平。 - 生成规则:内容如何生成/推进;本步不定触发与节奏规则。 - 叙事指南:怎么写、世界态度;本步不定文风与镜头。 + 本技能:可引用舞台骨架——尺度、基底+变造、关键舞台区、社会结构、世界状况;产出 context-fragment.v1。 + 不是地理志:凡影响核心运转且会上台的(生灵种类、势力、灾变、通道、习俗等)都落在社会结构或世界状况里。 + 美学纲领与交互范式:体验与轮转——本步不重定。 + 实现机制:承重切面——本步展开世界侧骨架,不重做移除检验。 + 具体实例 / 生成规则 / 叙事指南与故事推进:名片、生成节奏、遣词与推进——本步不定。 +feeds: gm ``` ## opening @@ -38,13 +37,12 @@ boundary: | ```opening 先用几句话框住「这局会出现的背景舞台」(想到什么写什么): -1. 舞台有多大? - 例:整个地球与大国博弈;一座城市;一所学校的几个系; - 也可以很抽象——「魔族与人族对峙割据的前线」,不必是真地图。 +1. 舞台有多大?熟悉基底是什么?变在哪里? + 例:一座城 / 一所学校几个系 / 「魔族与人族对峙的前线」(可抽象,不必真地图)。 -2. 熟悉的基底是什么?变在哪里? - 例:现代都市,但没有国别之分;现代都市,但中美关系两极化; - 西方魔幻常见格局,但魔法极度稀缺……也可以只说基底,变点稍后补。 +2. 社会结构与世界状况里,什么在撑故事? + 例:人类与丧尸如何相处;每年寒灾;南极通往怪物世界的出口;村中活人祭祀…… + 只点会影响核心运转、会反复碰到的,不写物种大全或民俗全书。 3. 真正会反复出现的舞台区是哪里? 一句话即可——只点「会上台」的部分,其余可留黑。 @@ -53,71 +51,61 @@ boundary: | ## task ```task -你正在执行剧本中的「世界蓝图与人文地理」步骤。本步产物写入「设计.世界蓝图与人文地理」。 +你正在执行「舞台骨架」。产物必须是 **context-fragment.v1** JSON,写入「设计.舞台骨架」。 -这里的「蓝图」不是纸面地图,也不是设定集百科。它是**会出现在台上的背景内容**:空间可大可小、可具体可抽象,尺度完全由本局要展示的体验决定。 +钉的是**会出现在台上、影响故事核心运转的背景**——不只是地理: +空间格局、谁在台上互动(社会结构)、世界如何运转(世界状况:灾变、通道、习俗、禁忌、命脉等)都属本步。 +不是纸面地图,也不是设定集百科。尺度由本局体验决定。 同一「现代都市」基底下: - 「体验作为韩国顶级财阀」→ 舞台往往是地球级势力、国家与资本网络; - 「体验普通大学生活」→ 舞台可能只是一座城,甚至一所学校的几个系。 -西方魔幻也可以只钉「魔族与人族对峙割据」这类格局,作为舞台布景,而不画完整大陆。 -核心操作:在已确认的体验(及可选机制支点)之上,用「熟悉基底 + 变造」钉出可引用骨架,并只细化关键舞台区。 +核心操作:在已确认体验(及可选机制支点)之上,用「熟悉基底 + 变造」钉出可引用骨架;写清社会结构与世界状况;只细化关键舞台区。 执行顺序: -1. 读取依赖产物与用户表述,提取已确认的核心体验、变造暗示、以及实现机制里属于世界侧的支点。只忠实继承,不重做美学或机制。 -2. 判定舞台尺度:本局背景需要「装得下」多大范围——以体验会碰到的边界为准,不是以题材惯例为准。 -3. 选定文化/世界基底(大家有印象的原型),再写清变点;变点优先服务体验与关键舞台,不为完整而扩写。 -4. 只展开关键舞台区的格局、势力/社群、人文地理要点;舞台外用「刻意留黑 / 继承常识」交代即可。 -5. 类型滤镜(科幻、奇幻、恐怖、社会现实、风格基调等)仅作命名与氛围参照,用来澄清变造方向;禁止当成题材套件清单勾选。 -6. 输出符合 output 契约的 JSON,供用户验收。 +1. 读依赖与用户表述,忠实继承体验与世界侧机制支点;不重做美学/机制。 +2. 判定舞台尺度:以体验会碰到的边界为准。 +3. 选定熟悉基底,写清变点。 +4. 钉「社会结构」:台上有哪些种类/势力/阶层在互动(种类级与格局级,非名片)。 +5. 钉「世界状况」:灾变、通道、祭祀、资源命脉、禁忌等——须能回答如何影响核心运转;软性日常条件也归此。 +6. 只展开关键舞台区;舞台外「刻意留黑」。 +7. 类型滤镜仅作体裁/氛围参照;禁止题材套件勾选。 +8. 填齐正文 + 自评 + 追问,输出 JSON。 -工作姿态——「搭舞台,不写百科」: -- 不问「这个世界完整长什么样」,问「玩家会反复看见/碰到什么背景」。 -- 能继承常识与基底默认的,不追问(已知现代 → 不问常规科技树,除非体验依赖变造)。 -- 可以把用户已表达但尚未整理的尺度与变造写成清晰骨架,请用户校正。 -- 两种舞台尺度会显著改变后续设定时,先辨明再展开,不要并行堆两套地图。 +工作姿态——「搭舞台,不写百科」:不问世界完整长什么样,问玩家会反复看见/碰到什么背景与运转部件。 -若依赖产物内部仍有含混或矛盾,不要越权重做上一步。只询问会改变尺度、基底变造或关键舞台区的差异;无法在本步解决的写入「开放问题」。 - -若程序已发出默认问题:用户首答在「用户.worker答复」,开场白在「创作.能力开场白」。禁止重复同一开场;在首答与依赖产物上补洞。 - -summary:`世界蓝图与人文地理 · …`(点题尺度 + 基底变造,非题材标签)。 +若程序已发默认问题:禁止重复开场;在首答与依赖上补洞。 +summary:`舞台骨架 · …`(点题尺度 / 社会结构或世界状况)。 ``` ## principles ```principles 1. 舞台服务于体验 - 尺度、格局、人文条件都必须能回答:它让哪段已确认体验得以发生或被感觉到? + 尺度、格局、社会结构、世界状况都必须能回答:它让哪段已确认体验得以发生或被感觉到? 不能从题材标签反推「这类故事通常有完整大陆/完整国别」。 -2. 蓝图 ≠ 地图 ≠ 百科 - 可以是抽象格局(对峙、割据、阶层天井、一条街的生态)。 - 不要求接壤关系、比例尺、全史年表、全物种志。 - 下游需要点名条目时交给「具体实例」;本步给骨架与引用钩子即可。 +2. 骨架 ≠ 地图 ≠ 百科;社会与状况同等重要 + 地理只是一类钩子。社会结构(谁在台上、如何分层互动)与世界状况(寒灾、异界出口、活人祭祀、命脉资源等) + 往往比「哪条河接哪座山」更能决定故事如何运转——必须写,但不能写成物种志/民俗全书。 + 可以是抽象格局(对峙、割据、阶层天井)。不要求接壤比例尺、全史年表。 + 下游需要点名条目时交给「具体实例」;本步给格局级与状况级引用钩子。 3. 变造基底,再按舞台细化 - 优先路径:大家都有印象的基底世界 → 点明变在哪里 → 只细化关键舞台区。 - 例:「现代都市,但没有国别之分」「现代都市,中美关系两极化」。 + 优先路径:大家都有印象的基底世界 → 点明变在哪里 → 只细化关键舞台区与会撑故事的社会/状况条目。 细化粒度跟舞台走:财阀博弈可写到国家/财团层;校园日常写到系馆与社团层即可。 4. 类型滤镜是叠加在基底上的体裁/氛围参照,不是必选题单 - 可用于澄清「故事以何种体裁被讲述」,从而影响冲突模式与背景气质,例如: - - 科幻/未来:硬科幻、太空歌剧、社会科幻、赛博/蒸汽/柴油/原子/生物/太阳朋克、卡带未来、钟表朋克… - - 奇幻/超自然:高奇幻、剑与魔法、低魔、武侠/仙侠、都市奇幻、魔法现实主义、神话童话… - - 恐怖/悬疑:哥特、宇宙恐怖、心理/肉体/生存恐怖、悬疑惊悚、灵异… - - 社会/现实:历史、犯罪、黑色电影、西部、战争、谍战、冒险、日常、成长、言情、竞技… - - 风格/基调:喜剧讽刺、乌托邦/反乌托邦、后末日、超级英雄、歌舞、剥削/Cult 等 - 用法:用户已有印象或体验需要某一体裁气质时,用滤镜命名变造方向; + 可用于澄清「故事以何种体裁被讲述」,从而影响冲突模式与背景气质。 禁止:列出大菜单请用户勾选;禁止因选了滤镜就自动塞满该类型常见地理与势力。 5. 与实现机制的分工 - 机制已钉「世界侧支撑切面」时:本步展开其环境/社会骨架,不重做「如何支撑/缺了会怎样」。 + 机制已钉「世界侧支撑切面」时:本步展开其环境/社会/状况骨架,不重做「如何支撑/缺了会怎样」。 机制未排入时:仍可从体验直接推断最小舞台;不要假装已经做过支撑检验。 6. 宁少勿多,舞台外留黑 - 关键舞台区写清即可停止。舞台外、体验碰不到的大洲/朝代/组织,默认不写。 + 关键舞台区 + 撑故事的社会与状况写清即可停止。舞台外、体验碰不到的部分默认不写。 「未展开范围」写明刻意省略,避免下游把留黑当成缺口去补百科。 7. 正推,禁止否定式路由 @@ -126,161 +114,167 @@ summary:`世界蓝图与人文地理 · …`(点题尺度 + 基底变造, 8. 继承优先于发明 基底已蕴含的常识默认成立;只在变点与体验依赖处显式改写。 - 不要为显得严谨而补起源史、伪科学或全套神话谱系,除非它们本身就是舞台上会出现的背景。 + 不要为显得严谨而补起源史、伪科学或全套神话谱系,除非它们本身会上台。 ``` ## probe ```probe -只追问会改变舞台尺度、基底变造、关键舞台区或人文格局的缺口。每轮 1~2 点。依赖产物或用户已答清的禁止重问。 +只追问会改变尺度、变造、社会结构、世界状况或台上格局的缺口。每轮 1~2 点。已答清的禁止重问。 -【默认问题已覆盖】舞台大小、基底+变点、会反复出现的舞台区。首答后按缺口补,勿重问已答清的。 +【默认问题已覆盖】尺度、基底+变点、社会结构/世界状况、会反复出现的舞台区。首答后按缺口补。 优先方式: 1. 尺度对照 - 用同一基底的两种舞台问差异。 例:「同是现代都市——更接近『全国财阀与国家势力都在台上』,还是『基本不出这座城/这所学校』?」 2. 变造落句 - 把模糊变点收成可引用短句,请用户改一个词即可采用。 例:「是否可以写成:现代都市基底,但国家边界弱化到几乎无国别,冲突主要在公司与城市场景里发生?」 -3. 关键舞台区边界 - 问「会反复上台」的部分,而不是「世界还有什么」。 - 例:「真正会反复出现的,是总部—宴会—监管听证这几类场合,还是还要经常切到海外子公司现场?」 +3. 社会与状况核对 + 例:「台上互动方是否就是『人类 + 无攻击欲的丧尸』?还有没有别的智能方会影响你的掌控?」 + 例:「『每年寒灾』是全局运转的硬时钟,还是只在边陲传说?祭祀是真要死人,还是可被你改写的习俗?」 -4. 滤镜澄清(可选) - 仅当体裁气质会改变背景冲突模式时才问;给 2~3 个完整句,不给类型树勾选。 - 例:「背景更偏赛博朋克式的巨企夜城,还是偏社会科幻式的制度压迫(科技外表不重要)?」 +4. 关键舞台区边界 + 例:「真正会反复出现的,是总部—宴会—监管听证,还是还要经常切到海外子公司现场?」 -5. 机制支点落地 - 若上游机制点了世界侧切面,问它在舞台上长什么样。 - 例:「『信息被巨企垄断』在台上主要体现为哪几个可见势力/场所,而不是再解释一遍为何垄断支撑体验。」 +5. 滤镜澄清 / 机制落地(可选) + 体裁气质会改变冲突模式时才问滤镜;机制已点世界侧切面时,问它在台上长什么样,不重做支撑检验。 -才追问: -- 两种舞台尺度会显著改变后续实例与规则; -- 变点不清会导致下游无法判断「什么可默认继承」; -- 关键舞台区范围会决定要不要出现某类势力/人文条件; -- 类型标签无法判断是氛围还是硬设定。 +才追问:两种尺度会显著改变后续;变点/社会/状况不清会改核心运转;关键舞台区范围决定要否某类势力;类型标签分不清氛围与硬设定。 -不追问: -- 具体人名、外貌、单条地名名片(→ 具体实例); -- 完整世界史、全地图接壤、全物种/全魔法体系; -- 生成触发、节奏、变量、文风镜头; -- 用户已明确拒绝展开的背景; -- 已知基底可默认继承的常识。 +不追问:具体名片(→ 具体实例);完整物种志/民俗志/世界史/全地图;生成/变量/文风;用户已拒绝展开的背景;基底可默认继承的常识。 ``` ## output ```output -最终只输出一个合法 JSON 对象,不加代码块外说明,不使用注释,不夹带未定义的英文 id。 - { - "依据的体验": [ - "从依赖产物忠实提取的体验锚点;可附带与舞台相关的机制支点名称" - ], - "舞台尺度": { - "范围": "一句话:地球级 / 国家 / 城市 / 机构内部 / 抽象割据带 / …", - "为何如此": "与核心体验的关系:为什么需要这么大或这么小", - "玩家常活动边界": "体验中反复碰到的空间/社会边界(可抽象)" - }, - "基底与变造": { - "基底": "大家都有印象的原型世界或文化骨架(如现代都市、西方高奇幻常见格局、近未来地球…)", - "变点": [ - "相对基底改了什么;每条宜短、可引用" + "schema": "context-fragment.v1", + "技能": "舞台骨架", + "brief": "一句话:舞台多大 + 社会/状况如何撑故事", + "mount": ["world-simulator"], + "稳变": "stable", + "正文": { + "依据的体验": [ + "从依赖产物忠实提取;可附带与舞台相关的机制支点名" ], - "类型滤镜": "可选:体裁/氛围参照名;无则空字符串。说明其如何影响背景气质,勿展开类型百科", - "默认可继承": "基底下可默认成立、本步不写的常识范围(一句话)" + "舞台尺度": { + "范围": "地球级 / 国家 / 城市 / 机构内部 / 抽象割据带 / …", + "为何如此": "与核心体验的关系", + "玩家常活动边界": "反复碰到的空间或社会边界(可抽象)" + }, + "基底与变造": { + "基底": "大家都有印象的原型(现代都市、西方高奇幻常见格局…)", + "变点": ["相对基底改了什么;每条宜短、可引用"], + "类型滤镜": "可选体裁/氛围参照名;无则空字符串", + "默认可继承": "基底下可默认成立、本步不写的常识范围(一句话)" + }, + "社会结构": [ + { + "名称": "种类/势力/阶层/阵营等格局级称呼——不是具体名片", + "性质": "智慧生灵种类|国家/财团|院系|帮派|阵营|阶层|其它", + "在舞台上的位置": "玩家如何碰到;相对权力/危险/依赖/互动方式", + "服务体验": "它让什么感觉或规则成立", + "细化程度": "点到为止|需要下游实例化|本步已够用" + } + ], + "世界状况": [ + { + "名称": "影响核心运转或台上氛围的状况条目", + "性质": "周期性灾变|异界/通道|祭祀习俗|资源命脉|禁忌律法|日常节奏|话语禁忌|其它", + "是什么": "最短可引用描述(例:每年寒灾;南极通往怪物世界的出口;村中活人祭祀)", + "如何影响运转": "它怎样推动、限制或框住故事与体验;软条件也可写「塑造氛围/日常感」", + "节奏或触发": "年周期 / 可往返 / 日常默认…;不明则「未定」", + "作用范围": "全局|仅某舞台区", + "服务体验": "对应哪段体验或机制支点" + } + ], + "关键舞台区": [ + { + "名称": "会反复上台的区/层/格局名", + "是什么": "空间、社会层或抽象舞台的最短描述", + "为何需要": "服务哪段体验或哪个机制支点", + "格局要点": ["与社会结构/世界状况如何交汇——只写台上用得上的"] + } + ], + "未展开范围": [ + "刻意留黑或仅继承常识;禁止下游当缺口补百科" + ], + "覆盖检验": { + "关键骨架是否够用": "是|否|部分", + "说明": "玩家会反复碰到的社会与状况是否已有钩子", + "宁少勿多说明": "为何到此停止" + } }, - "关键舞台区": [ - { - "名称": "会反复上台的区/层/格局名", - "是什么": "空间、社会层或抽象舞台的最短描述", - "为何需要": "服务哪段体验或哪个机制支点", - "格局要点": [ - "势力、场所类型、流通关系、可见冲突等——只写台上用得上的" - ] - } - ], - "势力与社群": [ - { - "名称": "…", - "性质": "国家/财团/院系/帮派/种族阵营/阶层…", - "在舞台上的作用": "玩家会如何感到它的存在", - "细化程度": "点到为止|需要下游实例化|本步已够用" - } - ], - "人文地理要点": [ - { - "要点": "习俗、阶层、话语、禁忌、日常节奏、资源分布等可引用条件", - "服务体验": "它让什么感觉成立", - "作用范围": "仅关键舞台区|全局默认" - } - ], - "未展开范围": [ - "刻意留黑或仅继承常识、禁止下游当缺口补百科的部分" - ], - "开放问题": [ - { - "问题": "尚不能可靠确定、且会影响尺度/变造/关键舞台的问题", - "影响": "不确定性会改变什么", - "当前暂定": "可撤销的暂定理解;没有则空字符串" - } - ] + "自评": { + "维度": [ + { + "名": "覆盖度", + "分数": 0, + "说明": "能否覆盖用户已表达的舞台/社会/状况/变造硬要求" + }, + { + "名": "充分度", + "分数": 0, + "说明": "骨架是否够下游引用,并撑起用户想要的体验发生与运转" + }, + { + "名": "克制度", + "分数": 0, + "说明": "是否只写会上台部分;无地图百科、物种志、民俗全书膨胀" + } + ], + "薄弱点": "一句话" + }, + "追问": { + "导语": "为钉准舞台骨架,还需确认:", + "题目": [ + { + "问": "…", + "建议选项": ["选项A(可带短场景)", "选项B", "其它(请写明)"], + "示例": "可选示范句" + } + ] + }, + "开放问题": [] } - -填写规则: -- 「依据的体验」只建追溯,不得扩写成新的美学纲领或机制表。 -- 「舞台尺度」必须能用体验解释;禁止「这类题材一般都这样」。 -- 「变点」为空数组仅当用户明确只要纯基底且无变造;否则至少标「未定」于开放问题。 -- 「关键舞台区」只含会上台的部分;不要为对称补齐未上场区域。 -- 「势力与社群」「人文地理要点」无则空数组;有则每条写清舞台作用,禁止百科句。 -- 「类型滤镜」不得连带输出该类型常见元素清单。 -- 「未展开范围」建议填写,防止下游过度补全。 -- 不要输出具体实例名片、生成规则、变量表、叙事文风或演员规格。 - -本步完成的定义: -- 舞台尺度已能被体验解释; -- 基底与变造可被下游引用(或明确未定); -- 关键舞台区已覆盖玩家会反复碰到的背景; -- 舞台外留黑已交代; -- 剩余点名条目属于「具体实例」,生成方式属于「生成规则」。 ``` +硬规则(公共三段 + 本技正文): +1. 合法 JSON;必含 `schema` / `技能` / `brief` / `正文` / `自评` / `追问`(题目可 `[]`)。 +2. **公共三段**:正文=舞台主体;自评=覆盖度/充分度/克制度;追问=导语+建议选项。 +3. **本技正文**须含:依据体验、舞台尺度、基底与变造、**社会结构**、**世界状况**、关键舞台区、未展开范围、覆盖检验。 +4. 社会结构=谁在台上互动(种类/势力/阶层,格局级);世界状况=世界如何运转(灾变/通道/习俗等硬部件 + 日常软条件)。无则空数组,但须在覆盖检验说明「为何无需」。 +5. `mount` 默认主世界层;**禁止**写 `order`。`稳变` 多为 `stable`。 +6. 不要输出具体实例名片、物种大全、民俗全书、生成规则、变量表、执行单元列表。 +7. summary:`舞台骨架 · {brief 缩略}`。 + ## checklist ```checklist -- [ ] 尺度是否由体验决定(而非题材默认地图)? -- [ ] 是否采用「基底 + 变造」,且变点可引用? -- [ ] 是否只细化关键舞台区,舞台外有「未展开范围」? -- [ ] 有无写成设定百科、接壤全图或完整世界史? -- [ ] 类型滤镜若出现,是否只作氛围/体裁参照而非套件勾选? -- [ ] 与美学/机制是否矛盾或越权重做? -- [ ] 是否误写成具体实例名单或生成规则? -- [ ] 开放问题是否都真正影响本步结论(无则空)? +- [ ] 含 schema / brief / 正文 / 自评 / 追问? +- [ ] 自评为覆盖度 / 充分度 / 克制度? +- [ ] 尺度由体验决定?基底+变造可引用? +- [ ] 社会结构与世界状况已写(或空数组且说明无需)?硬状况能说清如何影响运转? +- [ ] 只细化关键舞台区,且有未展开范围? +- [ ] 未写成地图百科、物种志、民俗全书、类型勾选菜单、具体名片? +- [ ] 追问只打尺度/社会/状况/变造缺口,带建议选项? ``` ## examples ```examples -好 · 尺度随体验收缩: -- 体验「普通大学生活」→ 舞台=一所大学的几个系与周边街区;基底=现代都市校园;变点可无或很轻;未展开=国家政治与国际局势。 -- 体验「作为韩国顶级财阀」→ 同属现代都市基底,但舞台升到财阀—国家—跨国资本;关键舞台区=董事会、政商宴、舆论与监管场合。 - -好 · 抽象舞台: -- 「魔族与人族对峙割据」作为关键舞台区名称;格局要点写前线、禁忌地带、两边话语;不画大陆全图。 - -好 · 变造落句: -- 基底「现代都市」;变点「几乎无国别之分,冲突在城市与公司层发生」;或「中美关系两极化渗入日常消费与舆论」。 - -好 · 滤镜作参照: -- 类型滤镜=「赛博朋克气质」;说明=巨企与霓虹夜城压迫感;不自动追加义体市场、全部帮派地图。 +好: +- 体验「普通大学生活」→ 舞台=校园几系;社会结构=师生/社团层;世界状况可轻;未展开=国家政治。 +- 体验「垄断粮食的末日支配」→ 社会结构=人类 + 无攻击欲丧尸 + 楼内依赖关系;世界状况=外出采集命脉;舞台区=楼内安全区 / 楼外采集区。 +- 世界状况示例:每年寒灾;南极通往怪物世界的出口;村中活人祭祀。 +- 抽象:「魔族与人族对峙割据」——社会结构两种阵营;不画大陆全图。 坏: -- 「先写完整七大国、货币史、三万年神话…」→ 百科,无舞台优先。 -- 「选一个类型:硬科幻/太空歌剧/赛博朋克/…(全表)」→ 题材套件菜单。 -- 「因为是日常所以不要任何社会结构」→ 否定式路由;日常也可以有系馆权力与阶层天井。 -- 「地点1:XX咖啡馆,店主叫…」→ 具体实例,不是蓝图骨架。 +- 只写山脉河流,不写祭祀/灾变/异界口等用户已点名的运转部件。 +- 「先写完整七大国、五十种魔族生态、三万年神话…」→ 百科。 +- 「地点1:XX咖啡馆,店主叫…」→ 具体实例,不是骨架。 +- 整份只有散文无三段外壳。 ``` diff --git a/skills/dialogue/world-simulator/recipes/expand-assistant/recipe.yaml b/skills/dialogue/world-simulator/recipes/expand-assistant/recipe.yaml index 8d0f2c5..f42f9d8 100644 --- a/skills/dialogue/world-simulator/recipes/expand-assistant/recipe.yaml +++ b/skills/dialogue/world-simulator/recipes/expand-assistant/recipe.yaml @@ -12,13 +12,14 @@ core: >- process: - 开始通常先做「美学纲领与交互范式」,钉清助手站位、分段节奏与体验禁忌。 - - 之后对照能力池选型:态度/信息边界不够时用叙事指南;重要同类内容难稳定生成时用生成规则,需预生成时再排具体实例。 - - 执行单元通常至少覆盖大纲/细纲与章节正文(可多次钉 Worker 规格),最后细化终稿并 closed。 - - 世界蓝图、实现机制、拓扑、变量、状态栏等:仅当体验需要可引用舞台或状态机时再选。 + - 之后对照能力池选型:写法/推进不够时用叙事指南与故事推进;重要同类内容难稳定生成时用生成规则,需预生成时再排具体实例。 + - 收成前排「游玩拓扑」勾选写手固定槽(大纲/细纲 + 章节正文,禁止发明新 ref)→「细化终稿」并 closed。 + - 舞台骨架、实现机制、变量、状态栏等:仅当体验需要可引用舞台或状态机时再选。 principles: - 正推写手体验,勿默认套「世界模拟」全套能力。 - 能力选型以能力 meta 为准;本配方不代替各能力写调用条件。 + - 禁止自由发明执行单元;写手路径只用 outline + chapter-writer 固定槽。 - 有编排参数的步骤须在进执行前钉齐 params;禁止空壳进 design-step。 - 同能力可反复编入;已验收步骤不得删除。 - 收成前 status: closed,产物进运行规格而非散文说明书。 diff --git a/skills/dialogue/world-simulator/recipes/world-simulator/recipe.yaml b/skills/dialogue/world-simulator/recipes/world-simulator/recipe.yaml index d629332..80f02b6 100644 --- a/skills/dialogue/world-simulator/recipes/world-simulator/recipe.yaml +++ b/skills/dialogue/world-simulator/recipes/world-simulator/recipe.yaml @@ -1,30 +1,44 @@ # 世界模拟器 · 配方 # 写方法论:适用、核心思路、设计流程、原则。 -# 各能力何时用 / 不用 → 读能力 meta(编排器会注入),勿在此重复。 +# 各技能何时用 / 不用 → 读技能 meta(编排器会注入),勿在此重复。 # steps = 近期起点,不是固定全程 DAG。 when: 回合互动、世界推进、角色扮演、沉浸推演类体验 core: >- 先钉清用户如何参与、正文如何呈现、核心体验与禁忌; - 再从体验反推:世界舞台、支撑机制、可生成内容、呈现与执行单元还缺什么; - 按缺口增量设计,最终收成可调度的运行规格(Worker 集),而不是一次性堆满设定百科。 + 再按缺口增量写出上下文,并标明挂哪些固定槽(默认:主世界层裁决 + 叙事转述); + 挂载归属可早定,插入顺序必须晚定——收成前用「上下文投影排序」按各块概括排出数字序与双锚点投影; + 再细化终稿收成运行规格。不发明执行单元、不为表/百科再拆槽。 process: - 开始通常先做「美学纲领与交互范式」,确认站位、轮转与要反复感受到什么。 - - 之后对照能力池的「何时用 / 何时不用」,只排近期真正缺的 1~4 步;不预设固定长链。 - - 需要可引用舞台时用世界蓝图;需要识别体验支点时用实现机制;同类内容难稳定生成时用生成规则,需预生成时再排具体实例。 - - 呈现、变量、拓扑、叙事等仅在体验真需要时再选。 - - 游玩期执行结构清楚后,钉 Worker 规格并细化终稿;收成前将流程 status 设为 closed。 + - 体验契约稍清后即可确认游玩槽(「游玩拓扑」或按默认:主世界层 + 叙事转述,角色视角默认关);此后写上下文时只标挂谁,不标最终顺序。 + - 对照技能池「何时用 / 何时不用」,只排近期缺的 1~4 步;不预设固定长链。 + - 中段软顺序:机制/舞台支点 → 要不要真值 → 有门控再排变量与分档/事件 → 叙事/正文组成/监控栏按需;每块产物带 mount(挂哪些槽)与可选「稳/变」信号,勿写死 order。 + - 用户看见的版式用「正文组成」(非程序报文);会变信息用「设计监控栏」(忌姓名性别等死字段)。 + - 需要可引用舞台时用「舞台骨架」;识别体验支点用实现机制;同类内容难稳定生成用生成规则,需预生成再排具体实例。 + - 需要门控/分档/程序事件:先「变量设计与更新规则」,再「变量控制上下文」。变量与 Progressive 不是执行单元。 + - 挂载归属(非顺序):世界/机制/生成 → 主世界层;叙事指南与故事推进 → 叙事转述(推进兼挂主世界层);美学进常驻。勿为世界观或性格表另开槽。拓扑图谱在固定槽已够用时通常跳过。 + - 需要把美学「感觉」落成遣词/笔墨焦点/禁忌与推进时用「叙事指南与故事推进」;事件表 schema 仍用生成规则,真值门控用变量设计。 + - 收成链(顺序固定):上游上下文大致齐 →「上下文投影排序」→「细化终稿」→ status: closed。禁止在排序步发明 ref 或重写长文。 + - 进游玩前可排「开场白与开场变量」(遵循正文组成/叙事/变量);再由 opening-generator 落库到输出.开场白与运行.初始变量。 + - 变量:真值 + Data 映射索引(实例表在具体实例)+ 维护语句(裁决包/隐藏段,非 chance)+ 旁观汇总挂载(变量控制上下文)。 principles: - - 正推:体验 → 缺口 → 能力;禁止题材默认全选世界模拟套件。 - - 能力选型以能力 meta 为准;本配方不代替各能力写调用条件。 + - 正推:体验 → 缺口 → 技能;禁止题材默认全选世界模拟套件。 + - 技能选型以技能 meta 为准;本配方不代替各技能写调用条件。 + - 禁止自由发明执行单元;只用固定槽(主世界层 / 叙事转述 / 可选角色视角 / 可选机遇裁定)。 + - 默认少槽:主世界层 + 叙事转述;仅强信息隔离才开角色视角;骰子/抽签/比点开机遇裁定(按需程序工具,不进每轮)。 + - 挂谁可早、排第几须晚:插入序与投影位只由「上下文投影排序」产出;中段技能禁止抢写最终 order。 + - 投影序扁平:「对话.历史」按 projection 动态裁剪;少变靠前、易变靠后是软规则,不是硬分区。 + - 真值 / Data / Progressive 三分;派生性格与章细纲不落可写真值(见 progressive-data-design)。 - 有编排参数的步骤必须在进执行前钉齐 params(askUser 选项+其它),禁止空壳进 design-step。 - - 同能力可反复编入(不同 step.id + params);已验收步骤不得删除。 + - 同技能可反复编入(不同 step.id + params);已验收步骤不得删除。 - 轻设定或用户明确不要世界骨架时,跳过不必要的世界/机制步骤。 + - 百科与查表归主世界层;裁决归主世界层、可见正文归叙事转述。 -brief: 世界模拟类:先定体验与轮转,再按缺口增量落到可运行规格 +brief: 世界模拟类:体验→挂槽写上下文→晚排序投影→收成运行规格 steps: - id: 美学纲领与交互范式 diff --git a/skills/dialogue/world-simulator/worker-templates/README.md b/skills/dialogue/world-simulator/worker-templates/README.md index dd6b8fc..a6160ea 100644 --- a/skills/dialogue/world-simulator/worker-templates/README.md +++ b/skills/dialogue/world-simulator/worker-templates/README.md @@ -3,30 +3,46 @@ ## 定位 **不是** play 时加载的 `workers/*/SKILL.md`。 -**是** 谈【剧本】、写 `设计.worker集` 时合并的 **默认建议**:context、outputs、职责摘要。 +**是** 写 `设计.worker集` 时合并的 **默认建议**:context、outputs、职责摘要。 | 层 | 权威来源 | |----|----------| -| 本局 play 怎么跑 | 用户 accept 的 **`设计.worker集`** | +| 本局 play 怎么跑 | 用户 accept 的 **`设计.worker集`**(优先 `play_slots` + workers) | | 可选模板 | 本目录 `{ref}.yaml` — 未写全 `context`/`outputs` 时合并 | Runtime 执行时读 Worker 集条目,不直接读本目录。 -## 文件 +## 世界模拟固定槽(推荐) + +规格里写 `play_slots`,由程序展开为 workers: + +```json +{ + "play_slots": { "gm": true, "narrator": true, "perspective": false, "chance": false }, + "workers": [] +} +``` + +| 槽 | 默认 ref | 模板 | +|----|----------|------| +| 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)。 + +## 其它文件 | 文件 | 说明 | |------|------| -| `world-simulator.yaml` | 世界推进 / 裁决 | -| `narrator.yaml` | 转述 / 展示 | -| `role-decide.yaml` | 单角色决策 | -| `round-present.yaml` | 结构化回合陈述 | +| `chance.yaml` | 机遇裁定(按需程序工具) | +| `round-present.yaml` | 结构化回合陈述(可选) | | `opening-generator.yaml` | 开局生成器(创作末尾) | -| `outline.yaml` | 大纲 / 细纲 | -| `chapter-writer.yaml` | 章节正文 | +| `outline.yaml` / `chapter-writer.yaml` | 扩写路径固定槽 | ## 用法 -1. 按用户意图选 `ref` -2. 读 `{ref}.yaml` 填默认字段 -3. 用户特殊需求覆盖后写入 Worker 集 -4. accept 后声明即实例规格 +1. 世界模拟:勾选 `play_slots`,按需覆盖某 ref 的 context/outputs +2. 扩写等:仍可用自由 `workers[]` 或另一套槽约定 +3. accept 后声明即实例规格 diff --git a/skills/dialogue/world-simulator/worker-templates/chance.yaml b/skills/dialogue/world-simulator/worker-templates/chance.yaml new file mode 100644 index 0000000..e6bdbc0 --- /dev/null +++ b/skills/dialogue/world-simulator/worker-templates/chance.yaml @@ -0,0 +1,22 @@ +id: chance +label: 机遇裁定 +role: chance +duty: > + 按需程序工具:掷骰、点数比拼、抽签/加权抽取。不进每轮管线;由编排器 + run_worker(workerContext.chance 或黑板 运行.机会请求)调用。禁止模型编造随机。 +when: 需要真随机或可复核比点时 +suggested_context: + static: + - 设计.worker集 + dynamic: + - 运行.机会请求 +suggested_outputs: + - 运行.本轮.机遇 +prompt_excerpt: | + 本执行单元由程序实现,不走 LLM。 + 请求形状(chance.v1 入参): + - roll: { "op":"roll", "expression":"2d6+1", "reason":"可选" } + - compare: { "op":"compare", "left":14, "right":12, "mode":"gte" } + - draw: { "op":"draw", "pool":["甲","乙"], "count":1, "unique":true } + - pick: { "op":"pick", "items":[{"id":"a","weight":2},{"id":"b"}], "count":1 } + 结果写入「运行.本轮.机遇」(schema: chance.v1)。 diff --git a/skills/dialogue/world-simulator/worker-templates/narrator.yaml b/skills/dialogue/world-simulator/worker-templates/narrator.yaml index 2e7db52..393edeb 100644 --- a/skills/dialogue/world-simulator/worker-templates/narrator.yaml +++ b/skills/dialogue/world-simulator/worker-templates/narrator.yaml @@ -1,23 +1,24 @@ id: narrator -label: 转述 / 展示 +label: 叙事转述 role: transcription duty: > - 读世界层 / 中间 tag,按 Worker 集 presentation 组装 输出.用户展示; - 只改表达,不改事实(除非用户要求摘要压缩)。 -when: 核心层产出齐、尚无本轮 输出.用户展示 + 只读「运行.本轮.裁决」结构化包 + 文风/叙事常驻,组装「输出.用户展示」。 + 只改表达,不改事实、不补裁决、不读完整真值表(除非包内已写明可见后果)。 +when: 主世界层裁决包已写入 suggested_context: static: - 设计.worker集 + - 设计.叙事指南与故事推进 + - 设计.叙事指南 dynamic: - - 运行.事件流 - 运行.本轮.裁决 - - 变量.当前 - 用户.最新输入 suggested_outputs: - 输出.用户展示 presentation_hints: mode: prose | markdown | mixed - tone: 依 Worker 集 presentation + tone: 依 Worker 集 presentation / 叙事指南 prompt_excerpt: | - 你是面向用户的展示编排者。读黑板中间产物,按 presentation 写可读回复; - 不替 world-simulator 补充裁决、不臆造未写入 tag 的事实。 + 你是面向用户的叙事转述。输入是结构化裁决包(settlement.v1),不是自由发挥的世界真相。 + 按文风写可读正文;禁止写出 do_not_say;禁止编造包中没有的事实或变量。 + 若裁决包含 tone_hint / npc_moves,须体现在叙述中。 diff --git a/skills/dialogue/world-simulator/worker-templates/opening-generator.yaml b/skills/dialogue/world-simulator/worker-templates/opening-generator.yaml index c5d0004..b84cf46 100644 --- a/skills/dialogue/world-simulator/worker-templates/opening-generator.yaml +++ b/skills/dialogue/world-simulator/worker-templates/opening-generator.yaml @@ -2,12 +2,13 @@ id: opening-generator label: 开局 · 开场白 stage: design-end duty: > - 创作末尾:结合已定世界/故事设定与表结构,写出开场白(主产物); - 初值表与开场同一真相,能推断则推断。不是填表工具。 + 创作末尾:优先落库设计.开场白与开场变量;否则现写开场白; + 初值与开场同真相。不是填表工具。 when: Worker 集 accept 后、进 play 前;design_end 或 workers 声明启用时 suggested_context: static: - 设计.worker集 + - 设计.开场白与开场变量 - 用户.需求 - 上下文.定稿摘要 dynamic: @@ -20,6 +21,5 @@ suggested_outputs: - 运行.初始变量 - 变量.当前 prompt_excerpt: | - 主产物是开场白:扎根已有世界与故事,把玩家放进可行动的第一拍。 - 表是配套:与开场事实一致;普通大学生等可推出的别问。 - 禁止先问卷填表再糊开场;禁止开场与表打架。 + 若有设计.开场白与开场变量:落库开场白全文与开场变量,遵守正文组成版式。 + 若无:现写开场,表与开场同真相。禁止先填表再糊开场。 diff --git a/skills/dialogue/world-simulator/worker-templates/world-simulator.yaml b/skills/dialogue/world-simulator/worker-templates/world-simulator.yaml index a012700..e942282 100644 --- a/skills/dialogue/world-simulator/worker-templates/world-simulator.yaml +++ b/skills/dialogue/world-simulator/worker-templates/world-simulator.yaml @@ -1,23 +1,30 @@ id: world-simulator -label: 世界模拟 +label: 主世界层 role: core duty: > - 中立世界层:读用户输入与当前状态,按 Worker 集 / notes 中的规则推进事件、 - 裁决组合结果;输出客观、干巴,默认叙事质量低(常需 narrator 转述)。 -when: 每轮用户输入后,或 tag_flow 要求产出本轮实质内容时 + 中立世界层:读用户输入、真值与 Progressive 投影,按规则推进并产出结构化裁决包 + (settlement.v1 → 运行.本轮.裁决);可提议变量变更;默认不写文学正文。 +when: 每轮用户输入后 suggested_context: static: - 设计.worker集 - 用户.需求 + - 设计.变量设计与更新规则 dynamic: - 用户.最新输入 - 运行.事件流 - 变量.当前 - - 运行.初始变量 + - 上下文.角色态度 + - 大纲.当前章 suggested_outputs: - 运行.本轮.裁决 - 运行.事件流 + - 运行.本轮.变量变更 prompt_excerpt: | - 你是中立世界层,不是文学作者。只对规则与已提交输入作机械反应; - 不写角色内心、不做剧情化推演。输出可复核的客观事实与状态变化。 - 叙事质量 intentionally 低——转述交给 narrator(若 Worker 集启用)。 + 你是中立主世界层(GM),不是文学作者。读真值与已投影上下文,对用户行动作可复核裁决。 + 「运行.本轮.裁决」必须是 JSON,schema 为 settlement.v1,至少尽量包含: + player_action、resolved[]、visible_now、npc_moves[]、variable_changes[]、do_not_say[]、tone_hint。 + 叙事质量 intentionally 低——用户正文交给 narrator。 + 需要真随机(骰子/抽签/比点)时:不要自己编结果;依赖已启用的机遇裁定(chance) + 写出的「运行.本轮.机遇」,或由编排器先 run_worker chance。 + 不要把未投影的分档长文整表塞进输出;只写本轮可见后果。 diff --git a/skills/dialogue/world-simulator/workers/design-flow/SKILL.md b/skills/dialogue/world-simulator/workers/design-flow/SKILL.md index 0f93345..18a5a4d 100644 --- a/skills/dialogue/world-simulator/workers/design-flow/SKILL.md +++ b/skills/dialogue/world-simulator/workers/design-flow/SKILL.md @@ -107,15 +107,17 @@ contextSegments: 7. 已验收步骤的 id **必须保留**;只能追加新步,或改未验收步的依赖/顺序/params 8. 配方建议 steps 只作近期起点;按能力 meta 按需选用,勿默认全选、勿一次排满 9. **禁止**把「交互」与「美学」拆成两步 -10. **禁止**在本步写能力正文、Worker 列表、表结构 -11. 参数或选型不够时,用 askUser 问 1~2 点(**优先带 options,并允许其它**) -12. 依赖真实产物的步骤(如具体实例依赖某条生成规则)须等上游验收后再编排,并从产物中列出可选项供用户选 -13. `summary`:`流程 · N 步 · open|closed · …` +10. **禁止**在本步写能力正文、自由 Worker 列表、表结构 +11. **禁止**编排「反复发明执行单元」;收成链:确认「游玩拓扑」→(上下文已多时)「上下文投影排序」→「细化终稿」。挂谁可早、排第几须晚,勿让中段技能写死 order +12. 参数或选型不够时,用 askUser 问 1~2 点(**优先带 options,并允许其它**) +13. 依赖真实产物的步骤(如具体实例依赖某条生成规则)须等上游验收后再编排,并从产物中列出可选项供用户选 +14. `summary`:`流程 · N 步 · open|closed · …` ## 自检 - 用户是否已选配方?(未选则不要硬编) - 是否只排了近期 horizon,而不是假固定全图? +- 是否误排了多次「发明 Worker」?应改为至多一次「游玩拓扑」 - 每步是否按能力 meta 的何时用/不用判断过,而非题材默认? - 每步 name 都在【能力】里?同名多次是否都有不同 id? - 有编排参数的步骤,必填 params 是否已钉齐?缺参是否应先 askUser 而不是产出空壳? diff --git a/skills/dialogue/world-simulator/workers/opening-generator/SKILL.md b/skills/dialogue/world-simulator/workers/opening-generator/SKILL.md index 15f2d54..f065d3f 100644 --- a/skills/dialogue/world-simulator/workers/opening-generator/SKILL.md +++ b/skills/dialogue/world-simulator/workers/opening-generator/SKILL.md @@ -3,12 +3,13 @@ id: opening-generator skill: world-simulator name: 开局 · 开场白 description: >- - 【创作末尾】结合已定世界/故事设定与表结构,写出可开玩的开场白; - 初值表与开场一致、能推断则推断。主产物是开场白,不是填表。须用户验收。 -version: 2 + 【创作末尾】优先落库「设计.开场白与开场变量」;若无则结合已定规格写出开场白, + 并钉与开场同真相的初值。主产物是开场白,不是填表。须用户验收。 +version: 3 stage: design-end inputTags: - "设计.worker集" + - "设计.开场白与开场变量" - "用户.需求" - "用户.最新输入" - "用户.worker答复" @@ -34,23 +35,25 @@ contextSegments: # 开局 · 开场白 -创作末尾:前面 **世界设定、故事设定、Worker 集、表结构** 已经定下来了。 -你要做的是——**站在这些已有内容上,写出玩家迈进世界的第一段开场**,并让状态表与之对齐。 +创作末尾:规格已定。优先读取 **`设计.开场白与开场变量`**: + +- 若已有且可用:把其中「开场白全文」写入 `输出.开场白`,把「开场变量」写成 `运行.初始变量` / `变量.当前`(表格式);可微调不可另起炉灶。 +- 若无:再站在 Worker 集 + 正文组成 + 叙事指南 + 变量设计上现写开场,并钉同真相初值。 ```text -主产物:输出.开场白(用户读的第一幕) -辅产物:运行.初始变量 / 变量.当前(与开场同一真相的状态快照) +主产物:输出.开场白(用户读的第一幕;须符合正文组成版式) +辅产物:运行.初始变量 / 变量.当前(与开场同一真相) ``` **不是**填表工具附带一句开场;**不是**玩回合;**不是**重做 Worker 集。 ## 角色 -你是开场作者 + 开局状态对齐者: +你是开场落库者(兼必要时的开场作者): -1. 先吃透已有设定(`设计.worker集` 里的世界观/notes/核心前提/叙事指南/表 schema、`用户.需求`、定稿摘要) -2. **写出开场白**:把玩家放进可感知、可行动的第一拍 -3. **顺带**落表:表字段取值须与开场里已经发生/成立的事实一致;能从设定与用户描述推出的直接填 +1. 先读 `设计.开场白与开场变量`(若有),再读 Worker 集 / 定稿摘要 / 用户需求 +2. **落库或写出开场白**:遵守叙事指南与正文组成(块序、监控栏、隐藏段约定) +3. **落表**:初值与开场事实一致;能推断则推断 ## 重心(务必遵守) diff --git a/src/main-agent/main-agent.ts b/src/main-agent/main-agent.ts index 88260be..de29a2f 100644 --- a/src/main-agent/main-agent.ts +++ b/src/main-agent/main-agent.ts @@ -32,7 +32,7 @@ function buildMainAgentSystemPrompt( 4. 当信息不足时,使用 ask_user:assessment 是主内容(完备度评价);questions 挂在其下且用户可跳过;一次 1~2 题。 5. 当需要执行任务时,使用 run_worker,只指定 workerId。不要指定 inputTags 或 outputTags——Runtime 从 Worker Skill 读取。 6. requiresApproval 表示运行 worker 前是否需要用户确认。代笔模式通常为 true。 -7. run_worker 可选 workerContext:{ "roleId": "A" },用于 role-decide 等指定当前决策角色(Runtime 写入 世界.当前角色.id)。 +7. run_worker 可选 workerContext:{ "roleId": "A" } 或 { "chance": { "op":"roll", "expression":"2d6" } }(机遇裁定走程序,勿让模型编随机)。 8. 你不能把未验收内容当作事实。 9. 向用户提问是 worker 的技能(ask_user tool),不是独立 worker。编排器只在调度层提问。 10. blackboardIndex 只有 tag 索引,不含正文 content。 @@ -175,9 +175,27 @@ export function parseMainAgentDecision(raw: string): MainAgentDecision { let workerContext: MainAgentDecision["workerContext"]; const ctxRaw = obj.workerContext; if (ctxRaw && typeof ctxRaw === "object" && !Array.isArray(ctxRaw)) { - const roleIdRaw = (ctxRaw as Record).roleId; + const ctx = ctxRaw as Record; + const roleIdRaw = ctx.roleId; const roleId = typeof roleIdRaw === "string" ? roleIdRaw.trim() : undefined; - if (roleId) workerContext = { roleId }; + const chanceRaw = ctx.chance; + const chance = + chanceRaw && typeof chanceRaw === "object" && !Array.isArray(chanceRaw) + ? (chanceRaw as Record) + : undefined; + if (roleId || chance) { + workerContext = { + ...(roleId ? { roleId } : {}), + ...(chance ? { chance } : {}), + }; + } + } + // 兼容 tool 扁平参数 chance(与 workerContext.chance 等价) + if (!workerContext?.chance && obj.chance && typeof obj.chance === "object") { + workerContext = { + ...(workerContext ?? {}), + chance: obj.chance as Record, + }; } const assessmentRaw = diff --git a/src/main-agent/tools.ts b/src/main-agent/tools.ts index a3d1fde..51b3418 100644 --- a/src/main-agent/tools.ts +++ b/src/main-agent/tools.ts @@ -133,7 +133,7 @@ export const MAIN_AGENT_TOOL_DEFINITIONS: ToolDefinition[] = [ function: { name: "run_worker", description: - "调度 worker 执行任务。只传 workerId,inputTags/outputTags 由 Runtime 从 Worker Skill 读取。", + "调度 worker。常规只传 workerId。调度 chance(机遇裁定)时传 chance 请求对象(程序掷骰/比点/抽签,禁止让模型编随机)。", parameters: { type: "object", properties: { @@ -147,6 +147,11 @@ export const MAIN_AGENT_TOOL_DEFINITIONS: ToolDefinition[] = [ type: "string", description: "role-decide 等 worker 的当前角色 id", }, + chance: { + type: "object", + description: + "仅 workerId=chance:{ op:'roll'|'compare'|'draw'|'pick', ... }。例 roll:{expression:'2d6+1'};draw:{pool:['a','b'],count:1}", + }, }, required: ["workerId", "reason", "requiresApproval"], additionalProperties: false, diff --git a/src/runtime/phase-machine.ts b/src/runtime/phase-machine.ts index d6d910c..8dc0102 100644 --- a/src/runtime/phase-machine.ts +++ b/src/runtime/phase-machine.ts @@ -686,6 +686,10 @@ export function applyEvent( const mode = session.acceptanceMode ?? "user_confirmed"; if (mode === "user_confirmed" && result.session.pendingArtifactId) { const sidecar = asOptionalSidecarQuestions(event.payload.questions); + const assessment = + typeof event.payload.assessment === "string" + ? event.payload.assessment.trim() + : ""; const effects = [...result.effects]; if (sidecar?.length) { effects.push({ @@ -700,6 +704,7 @@ export function applyEvent( kind: "review_artifact", artifactId: result.session.pendingArtifactId, questions: sidecar, + assessment: assessment || undefined, }), }; } diff --git a/src/runtime/phase-runtime.ts b/src/runtime/phase-runtime.ts index c2a01db..fc13b16 100644 --- a/src/runtime/phase-runtime.ts +++ b/src/runtime/phase-runtime.ts @@ -32,6 +32,10 @@ import { toActiveSkillSnapshot } from "../skills/snapshot.js"; import { runWorkerSkill } from "../worker/executor.js"; import { resolveWorkerId } from "../worker/resolve-id.js"; import { resolveWorkerLlmProvider } from "../skills/worker-llm.js"; +import { + DIALOGUE_HISTORY_TAG, + appendDialogueHistoryTurn, +} from "../skills/dialogue-history.js"; import { extractIntakeFromMessage } from "../intake/extract.js"; import { readIntakeValues } from "../intake/intake.js"; import { compressAfterWorkerAccept } from "./compress-after-worker.js"; @@ -65,6 +69,10 @@ import { } from "../skills/creation-flow.js"; import { normalizeQuestions } from "../skills/question-protocol.js"; import { parseWorkerSetYaml } from "../skills/worker-set-parse.js"; +import { + executeChance, + resolveChanceRequest, +} from "../skills/chance-tools.js"; import { resolveAcceptanceModeForWorker, resolveRunnableWorker, @@ -385,6 +393,10 @@ export class PhaseRuntime { content: latestInput.trim(), source: "user", }); + // 游玩期把用户话追加进可投影的「对话.历史」标签 + if (inferLifecycleStage(session) === "play") { + this.appendDialogueHistory("用户", latestInput.trim()); + } } const workerReply = session.slots["用户.worker答复"]; if (typeof workerReply === "string" && workerReply.trim()) { @@ -501,6 +513,7 @@ export class PhaseRuntime { sourceTag: declaration.sourceTag, accepted: declaration.accepted, playWorkerIds: declaration.playWorkerIds, + onDemandWorkerIds: declaration.onDemandWorkerIds, designEndWorkerIds: declaration.designEndWorkerIds, activeWorkerIds: declaration.activeWorkerIds, }, @@ -715,11 +728,18 @@ export class PhaseRuntime { if (!activeSkill?.name) { throw new Error("当前没有 active skill,无法运行 worker"); } + + const workerId = resolveWorkerId(effect.workerId); + + // 机遇裁定:纯程序,不走 LLM + if (workerId === "chance") { + await this.runChanceWorker(effect); + return; + } + if (!this.llm) { throw new Error("未配置 LLM,无法运行 worker"); } - - const workerId = resolveWorkerId(effect.workerId); this.lastWorkerRunSnapshot = { workerId, runtimeSession: structuredClone(this.session), @@ -842,6 +862,7 @@ export class PhaseRuntime { artifactId: artifact.id, // 有产物时 askUser 挂到验收态,不阻断 Accept questions: result.askUser?.length ? result.askUser : undefined, + assessment: result.askAssessment?.trim() || undefined, }, }); this.maybeCompressAcceptedArtifact(artifact.id); @@ -850,6 +871,83 @@ export class PhaseRuntime { } } + /** 机遇裁定:程序掷骰/比点/抽签,写入 运行.本轮.机遇 */ + private async runChanceWorker( + effect: Extract, + ): Promise { + const workerId = "chance"; + this.lastWorkerRunSnapshot = { + workerId, + runtimeSession: structuredClone(this.session), + blackboardItems: this.blackboard.exportItems(), + }; + + const acceptanceMode = resolveAcceptanceModeForWorker({ + session: this.session, + blackboard: this.blackboard, + workerId: effect.workerId, + }); + + this.session = ( + await this.dispatch({ + type: "worker_started", + payload: { + workerId: effect.workerId, + stepId: this.session.currentStepId, + acceptanceMode: + this.session.resumeContext?.acceptanceMode ?? acceptanceMode, + }, + }) + ).session; + + const boardRaw = this.blackboard.getContentByTag("运行.机会请求"); + const request = resolveChanceRequest({ + workerContext: effect.workerContext ?? null, + blackboardRequestJson: boardRaw, + }); + + const result = request + ? executeChance(request) + : { + schema: "chance.v1" as const, + op: "roll" as const, + ok: false, + summary: "机遇失败:缺少请求(workerContext.chance 或 运行.机会请求)", + detail: {}, + error: "missing_request", + }; + + const content = JSON.stringify(result, null, 2); + const written = this.writeWorkerTagContent( + "运行.本轮.机遇", + content, + workerId, + ); + this.session = { + ...this.session, + slots: { ...this.session.slots, "运行.本轮.机遇": written }, + }; + + const artifact = createArtifact({ + workerId: effect.workerId, + stepId: this.session.currentStepId, + outputTags: ["运行.本轮.机遇"], + summary: result.summary, + }); + this.session = { + ...this.session, + artifacts: [...this.session.artifacts, artifact], + }; + + this.onMessage(`[机遇裁定] ${result.summary}\n\n${content}`); + + await this.dispatch({ + type: "worker_completed", + payload: { artifactId: artifact.id }, + }); + this.maybeCompressAcceptedArtifact(artifact.id); + } + /** * 占位 worker:worker_started → 可选自动 worker_completed。 * 演示与单测用,真实环境应替换为真实 worker 调度。 @@ -951,12 +1049,43 @@ export class PhaseRuntime { content: toWrite, source: workerId, }); + if ( + inferLifecycleStage(this.session) === "play" && + (tag === "输出.用户展示" || tag === "输出.开场白") + ) { + this.appendDialogueHistory("助手", toWrite); + } if (nextDoc) { this.applyTableSideEffects(prevDoc, nextDoc, workerId); } return toWrite; } + /** 避免 syncSlots 重复把同一句用户输入追加进历史 */ + private lastAppendedUserHistory = ""; + + /** 追加一轮到黑板「对话.历史」(排序表可投影裁剪) */ + private appendDialogueHistory(role: "用户" | "助手" | "系统", text: string): void { + const body = text.trim(); + if (!body) return; + if (role === "用户") { + if (this.lastAppendedUserHistory === body) return; + this.lastAppendedUserHistory = body; + } + const prev = this.blackboard.getContentByTag(DIALOGUE_HISTORY_TAG) ?? ""; + const next = appendDialogueHistoryTurn(prev, { role, text: body }); + if (next === prev.trim()) return; + this.blackboard.write({ + tag: DIALOGUE_HISTORY_TAG, + content: next, + source: "runtime", + }); + this.session = { + ...this.session, + slots: { ...this.session.slots, [DIALOGUE_HISTORY_TAG]: next }, + }; + } + /** 从 Worker 集 tables.side_effects 算边沿触发并写 tag / 记 fired */ private applyTableSideEffects( prev: TableDoc | null, diff --git a/src/runtime/tool-registry.ts b/src/runtime/tool-registry.ts index 58a39b4..1a61a5d 100644 --- a/src/runtime/tool-registry.ts +++ b/src/runtime/tool-registry.ts @@ -79,12 +79,24 @@ export function toolCallToDecision(call: ParsedToolCall): MainAgentDecision { const reason = requireString(args, "reason"); const requiresApproval = Boolean(args.requiresApproval); const roleId = optionalString(args, "roleId"); + const chanceRaw = args.chance; + const chance = + chanceRaw && typeof chanceRaw === "object" && !Array.isArray(chanceRaw) + ? (chanceRaw as Record) + : undefined; + const workerContext = + roleId || chance + ? { + ...(roleId ? { roleId } : {}), + ...(chance ? { chance } : {}), + } + : undefined; return { id: randomUUID(), action: "run_worker", reason, workerId, - workerContext: roleId ? { roleId } : undefined, + workerContext, requiresApproval, statePatchAllowed: false, }; diff --git a/src/server/display-labels.ts b/src/server/display-labels.ts index 82b5b60..17a2c67 100644 --- a/src/server/display-labels.ts +++ b/src/server/display-labels.ts @@ -27,7 +27,8 @@ const WORKER_LABELS: Record = { "agent-burst": "编排器调度", narrator: "叙事转述", "role-decide": "角色决策", - "world-simulator": "世界推演", + "world-simulator": "主世界层", + chance: "机遇裁定", "round-present": "回合呈现", outline: "大纲 / 细纲", "chapter-writer": "章节正文", @@ -36,7 +37,7 @@ const WORKER_LABELS: Record = { const FIXED_TOPIC_LABELS: Record = { "aesthetics-interaction": "美学纲领与交互范式", interaction: "交互范式", - narrative_guide: "叙事指南", + narrative_guide: "叙事指南与故事推进", input_protocol: "输入协议", core_premise: "核心前提", aesthetics: "美学纲领", diff --git a/src/server/session-manager.ts b/src/server/session-manager.ts index 4fe0adf..45d3b76 100644 --- a/src/server/session-manager.ts +++ b/src/server/session-manager.ts @@ -67,6 +67,15 @@ import { formatWorkerSetForUser, type WorkerSetUserView, } from "../skills/worker-set-view.js"; +import { + CONTEXT_ORDER_TAG, + applyContextOrderEdit, + mergeContextOrderIntoWorkerSetJson, + parseContextOrder, + serializeContextOrder, + synthesizeContextOrderFromWorkers, + type ContextOrderEdit, +} from "../skills/context-order.js"; import { CREATION_FLOW_TAG, CREATION_SELECTED_RECIPE_TAG, @@ -292,6 +301,80 @@ export class SessionManager { return this.toView(sessionId); } + /** + * 编排上下文投影排序:写入 设计.worker集.context_order(若有规格), + * 并同步 设计.上下文投影排序。下次声明驱动拼装即按新序。 + */ + patchContextOrder(sessionId: string, edit: ContextOrderEdit): SessionView { + const s = this.require(sessionId); + const bb = s.runtime.getBlackboard(); + const workerSetRaw = + bb.getContentByTag("设计.worker集")?.trim() || + bb.getContentByTag("设计.worker集.草稿")?.trim() || + ""; + const orderTagRaw = bb.getContentByTag(CONTEXT_ORDER_TAG)?.trim() || ""; + + let current = parseContextOrder( + workerSetRaw + ? parseWorkerSetYaml(workerSetRaw).context_order + : undefined, + ); + if (!current) current = parseContextOrder(orderTagRaw); + if (!current && workerSetRaw) { + const parsed = parseWorkerSetYaml(workerSetRaw); + // 与检查器合成逻辑一致:缺 context 时用包内默认契约(视图侧也会合成) + current = synthesizeContextOrderFromWorkers( + parsed.workers.map((w) => ({ + ref: w.ref, + name: w.name, + context: w.context, + })), + parsed.play_slots, + ); + } + if (!current && edit.action !== "replace") { + throw new Error("尚无上下文投影排序可编辑;请先完成游玩拓扑/细化终稿,或提交完整 replace"); + } + if (!current && edit.action === "replace") { + current = parseContextOrder(edit.context_order); + if (!current) throw new Error("context_order 无法解析"); + } + + const next = applyContextOrderEdit(current!, edit); + const serialized = serializeContextOrder(next); + + bb.write({ + tag: CONTEXT_ORDER_TAG, + content: serialized, + source: "user", + }); + + if (workerSetRaw) { + const targetTag = bb.getContentByTag("设计.worker集")?.trim() + ? "设计.worker集" + : bb.getContentByTag("设计.worker集.草稿")?.trim() + ? "设计.worker集.草稿" + : null; + if (targetTag) { + bb.write({ + tag: targetTag, + content: mergeContextOrderIntoWorkerSetJson(workerSetRaw, next), + source: "user", + }); + } + } else { + // 尚无规格:至少落下排序表,供细化终稿合并 + bb.write({ + tag: CONTEXT_ORDER_TAG, + content: serialized, + source: "user", + }); + } + + if (s.bookId) this.persist(s); + return this.toView(sessionId); + } + /** * 打开 Book:优先恢复磁盘快照;无快照则新建 Session。 * 若该 Book 已在内存中,直接返回当前视图。 diff --git a/src/server/web-server.ts b/src/server/web-server.ts index ccc8c94..e46003a 100644 --- a/src/server/web-server.ts +++ b/src/server/web-server.ts @@ -291,6 +291,96 @@ const server = createServer(async (req, res) => { return; } + if (req.method === "POST" && sub === "/context-order") { + const body = JSON.parse(await readBody(req)) as { + action?: string; + slotRef?: string; + index?: number; + delta?: number; + anchor?: string; + projection?: string; + context_order?: unknown; + }; + try { + const action = body.action?.trim(); + if (action === "replace") { + const view = sessionManager.patchContextOrder(sessionId, { + action: "replace", + context_order: body.context_order, + }); + json(res, 200, view); + return; + } + if (action === "move") { + const delta = body.delta === 1 || body.delta === -1 ? body.delta : 0; + if (!body.slotRef?.trim() || typeof body.index !== "number" || !delta) { + json(res, 400, { error: "move 需要 slotRef、index、delta(±1)" }); + return; + } + const view = sessionManager.patchContextOrder(sessionId, { + action: "move", + slotRef: body.slotRef.trim(), + index: body.index, + delta, + }); + json(res, 200, view); + return; + } + if (action === "set_anchor") { + if ( + !body.slotRef?.trim() || + typeof body.index !== "number" || + (body.anchor !== "pre_history" && body.anchor !== "post_history") + ) { + json(res, 400, { + error: "set_anchor 需要 slotRef、index、anchor(pre_history|post_history)", + }); + return; + } + const view = sessionManager.patchContextOrder(sessionId, { + action: "set_anchor", + slotRef: body.slotRef.trim(), + index: body.index, + anchor: body.anchor, + }); + json(res, 200, view); + return; + } + if (action === "set_projection") { + const proj = body.projection; + if ( + !body.slotRef?.trim() || + typeof body.index !== "number" || + (proj !== "fixed" && + proj !== "full" && + proj !== "summary" && + proj !== "fields") + ) { + json(res, 400, { + error: "set_projection 需要 slotRef、index、projection", + }); + return; + } + const view = sessionManager.patchContextOrder(sessionId, { + action: "set_projection", + slotRef: body.slotRef.trim(), + index: body.index, + projection: proj, + }); + json(res, 200, view); + return; + } + json(res, 400, { + error: "action 须为 move | set_anchor | set_projection | replace", + }); + } catch (err) { + json(res, 400, { + error: err instanceof Error ? err.message : "编排失败", + }); + } + return; + } + if (req.method === "POST" && sub === "/context-traces/prune") { try { const result = sessionManager.pruneSessionContextTraces(sessionId); diff --git a/src/skills/chance-tools.ts b/src/skills/chance-tools.ts new file mode 100644 index 0000000..0e9cb35 --- /dev/null +++ b/src/skills/chance-tools.ts @@ -0,0 +1,325 @@ +/** + * 程序机遇工具:骰子、比点、抽签/加权抽取。 + * 供按需执行单元 `chance` 使用;禁止用模型「假装随机」。 + */ + +export type ChanceRollRequest = { + op: "roll"; + /** 如 1d20、2d6+3、d100;空格可忽略 */ + expression: string; + reason?: string; +}; + +export type ChanceCompareRequest = { + op: "compare"; + left: number; + right: number; + mode?: "gt" | "gte" | "lt" | "lte" | "eq"; + reason?: string; +}; + +export type ChanceDrawRequest = { + op: "draw"; + pool: string[]; + count?: number; + /** 默认 true:不放回 */ + unique?: boolean; + reason?: string; +}; + +export type ChancePickRequest = { + op: "pick"; + items: Array<{ id: string; weight?: number }>; + count?: number; + unique?: boolean; + reason?: string; +}; + +export type ChanceRequest = + | ChanceRollRequest + | ChanceCompareRequest + | ChanceDrawRequest + | ChancePickRequest; + +export type ChanceResult = { + schema: "chance.v1"; + op: ChanceRequest["op"]; + ok: boolean; + reason?: string; + /** 人话摘要 */ + summary: string; + detail: Record; + error?: string; +}; + +const DICE_RE = /^\s*(\d*)\s*[dD]\s*(\d+)\s*([+-]\s*\d+)?\s*$/; + +function randInt(min: number, max: number): number { + const lo = Math.ceil(min); + const hi = Math.floor(max); + return lo + Math.floor(Math.random() * (hi - lo + 1)); +} + +export function parseChanceRequest(raw: unknown): ChanceRequest | null { + if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null; + const row = raw as Record; + const op = typeof row.op === "string" ? row.op.trim() : ""; + if (op === "roll") { + const expression = typeof row.expression === "string" ? row.expression : ""; + if (!expression.trim()) return null; + return { + op: "roll", + expression: expression.trim(), + reason: typeof row.reason === "string" ? row.reason : undefined, + }; + } + if (op === "compare") { + if (typeof row.left !== "number" || typeof row.right !== "number") return null; + const mode = row.mode; + const okMode = + mode === "gt" || + mode === "gte" || + mode === "lt" || + mode === "lte" || + mode === "eq" || + mode === undefined; + if (!okMode) return null; + return { + op: "compare", + left: row.left, + right: row.right, + mode, + reason: typeof row.reason === "string" ? row.reason : undefined, + }; + } + if (op === "draw") { + if (!Array.isArray(row.pool)) return null; + const pool = row.pool.filter((x): x is string => typeof x === "string"); + if (!pool.length) return null; + return { + op: "draw", + pool, + count: typeof row.count === "number" ? row.count : undefined, + unique: typeof row.unique === "boolean" ? row.unique : undefined, + reason: typeof row.reason === "string" ? row.reason : undefined, + }; + } + if (op === "pick") { + if (!Array.isArray(row.items)) return null; + const items: Array<{ id: string; weight?: number }> = []; + for (const it of row.items) { + if (!it || typeof it !== "object" || Array.isArray(it)) continue; + const id = (it as { id?: unknown }).id; + const weight = (it as { weight?: unknown }).weight; + if (typeof id !== "string" || !id.trim()) continue; + items.push({ + id: id.trim(), + weight: typeof weight === "number" ? weight : undefined, + }); + } + if (!items.length) return null; + return { + op: "pick", + items, + count: typeof row.count === "number" ? row.count : undefined, + unique: typeof row.unique === "boolean" ? row.unique : undefined, + reason: typeof row.reason === "string" ? row.reason : undefined, + }; + } + return null; +} + +export function executeChance(request: ChanceRequest): ChanceResult { + const reason = request.reason; + try { + switch (request.op) { + case "roll": + return executeRoll(request, reason); + case "compare": + return executeCompare(request, reason); + case "draw": + return executeDraw(request, reason); + case "pick": + return executePick(request, reason); + default: + return fail("unknown", "不支持的 op", reason); + } + } catch (e) { + return fail( + (request as ChanceRequest).op, + e instanceof Error ? e.message : String(e), + reason, + ); + } +} + +function fail( + op: string, + error: string, + reason?: string, +): ChanceResult { + return { + schema: "chance.v1", + op: op as ChanceRequest["op"], + ok: false, + reason, + summary: `机遇失败:${error}`, + detail: {}, + error, + }; +} + +function executeRoll(req: ChanceRollRequest, reason?: string): ChanceResult { + const m = req.expression.replace(/\s+/g, "").match(DICE_RE); + if (!m) { + return fail("roll", `无法解析骰式:${req.expression}`, reason); + } + const count = m[1] ? Number(m[1]) : 1; + const sides = Number(m[2]); + const mod = m[3] ? Number(m[3].replace(/\s+/g, "")) : 0; + if (!Number.isFinite(count) || count < 1 || count > 100) { + return fail("roll", "骰子个数须在 1~100", reason); + } + if (!Number.isFinite(sides) || sides < 2 || sides > 1000) { + return fail("roll", "面数须在 2~1000", reason); + } + const dice: number[] = []; + let sum = 0; + for (let i = 0; i < count; i++) { + const v = randInt(1, sides); + dice.push(v); + sum += v; + } + const total = sum + mod; + const modText = mod === 0 ? "" : mod > 0 ? `+${mod}` : `${mod}`; + return { + schema: "chance.v1", + op: "roll", + ok: true, + reason, + summary: `掷骰 ${count}d${sides}${modText} → [${dice.join(",")}]${modText} = ${total}`, + detail: { + expression: `${count}d${sides}${modText}`, + dice, + modifier: mod, + total, + }, + }; +} + +function executeCompare( + req: ChanceCompareRequest, + reason?: string, +): ChanceResult { + const mode = req.mode ?? "gte"; + let win = false; + switch (mode) { + case "gt": + win = req.left > req.right; + break; + case "gte": + win = req.left >= req.right; + break; + case "lt": + win = req.left < req.right; + break; + case "lte": + win = req.left <= req.right; + break; + case "eq": + win = req.left === req.right; + break; + } + return { + schema: "chance.v1", + op: "compare", + ok: true, + reason, + summary: `比点 ${req.left} ${mode} ${req.right} → ${win ? "成立" : "不成立"}`, + detail: { left: req.left, right: req.right, mode, win }, + }; +} + +function executeDraw(req: ChanceDrawRequest, reason?: string): ChanceResult { + const count = req.count ?? 1; + const unique = req.unique !== false; + if (count < 1 || count > 50) { + return fail("draw", "抽取数量须在 1~50", reason); + } + if (unique && count > req.pool.length) { + return fail("draw", "不放回抽取数量超过池大小", reason); + } + const bag = [...req.pool]; + const drawn: string[] = []; + for (let i = 0; i < count; i++) { + const idx = randInt(0, bag.length - 1); + drawn.push(bag[idx]!); + if (unique) bag.splice(idx, 1); + } + return { + schema: "chance.v1", + op: "draw", + ok: true, + reason, + summary: `抽签 ×${count} → ${drawn.join("、")}`, + detail: { drawn, unique, poolSize: req.pool.length }, + }; +} + +function executePick(req: ChancePickRequest, reason?: string): ChanceResult { + const count = req.count ?? 1; + const unique = req.unique !== false; + if (count < 1 || count > 50) { + return fail("pick", "抽取数量须在 1~50", reason); + } + let bag = req.items.map((it) => ({ + id: it.id, + weight: it.weight && it.weight > 0 ? it.weight : 1, + })); + if (unique && count > bag.length) { + return fail("pick", "不放回加权抽取数量超过条目数", reason); + } + const picked: string[] = []; + for (let i = 0; i < count; i++) { + const totalW = bag.reduce((s, x) => s + x.weight, 0); + let r = Math.random() * totalW; + let chosen = bag[0]!; + for (const it of bag) { + r -= it.weight; + if (r <= 0) { + chosen = it; + break; + } + } + picked.push(chosen.id); + if (unique) bag = bag.filter((x) => x.id !== chosen.id); + } + return { + schema: "chance.v1", + op: "pick", + ok: true, + reason, + summary: `加权抽取 ×${count} → ${picked.join("、")}`, + detail: { picked, unique }, + }; +} + +/** 从黑板正文或 workerContext 解析请求 */ +export function resolveChanceRequest(params: { + workerContext?: Record | null; + blackboardRequestJson?: string | null; +}): ChanceRequest | null { + const ctx = params.workerContext; + if (ctx && typeof ctx === "object") { + const nested = ctx.chance ?? ctx.request ?? ctx; + const parsed = parseChanceRequest(nested); + if (parsed) return parsed; + } + const raw = params.blackboardRequestJson?.trim(); + if (!raw) return null; + try { + return parseChanceRequest(JSON.parse(raw)); + } catch { + return null; + } +} diff --git a/src/skills/context-fragment.ts b/src/skills/context-fragment.ts new file mode 100644 index 0000000..1cb8c95 --- /dev/null +++ b/src/skills/context-fragment.ts @@ -0,0 +1,352 @@ +/** + * 上下文片段产物(context-fragment.v1)解析与展示辅助。 + * 规范:docs/context-fragment-design.md + */ +import { extractJsonObjectText } from "./worker-set-parse.js"; +import { + normalizeQuestions, + type QuestionItem, +} from "./question-protocol.js"; + +export const CONTEXT_FRAGMENT_SCHEMA = "context-fragment.v1" as const; + +export type ContextFragmentStability = "stable" | "semi" | "volatile"; + +export type ContextFragment = { + schema: typeof CONTEXT_FRAGMENT_SCHEMA; + 技能: string; + brief: string; + mount: string[]; + 稳变?: ContextFragmentStability; + 正文: unknown; + 开放问题: string[]; +}; + +export type ContextFragmentView = { + ok: boolean; + parseError?: string; + fragment?: ContextFragment; + sections: Array<{ title: string; lines: string[] }>; +}; + +function asString(v: unknown): string | undefined { + if (typeof v !== "string") return undefined; + const t = v.trim(); + return t || undefined; +} + +function asStringList(v: unknown): string[] { + if (!Array.isArray(v)) return []; + return v.map((x) => asString(x)).filter((x): x is string => Boolean(x)); +} + +function normalizeStability(v: unknown): ContextFragmentStability | undefined { + if (v === "stable" || v === "semi" || v === "volatile") return v; + if (v === "稳" || v === "少变") return "stable"; + if (v === "中" || v === "偶发") return "semi"; + if (v === "变" || v === "常变") return "volatile"; + return undefined; +} + +/** 从任意 JSON 文本或对象解析片段;兼容无 schema 但含 brief+正文 的旧形 */ +export function parseContextFragment(raw: unknown): ContextFragment | undefined { + let row: Record | null = null; + if (typeof raw === "string") { + const text = extractJsonObjectText(raw) ?? raw.trim(); + try { + const parsed = JSON.parse(text) as unknown; + if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) { + row = parsed as Record; + } + } catch { + return undefined; + } + } else if (raw && typeof raw === "object" && !Array.isArray(raw)) { + row = raw as Record; + } + if (!row) return undefined; + + const hasSchema = row.schema === CONTEXT_FRAGMENT_SCHEMA; + const hasBody = "正文" in row || "body" in row; + const brief = asString(row.brief) ?? asString(row.概要); + if (!hasSchema && !(brief && hasBody)) return undefined; + + const skill = + asString(row.技能) ?? asString(row.skill) ?? asString(row.name) ?? ""; + const 正文 = row.正文 !== undefined ? row.正文 : row.body !== undefined ? row.body : {}; + const mount = asStringList(row.mount ?? row.挂载); + const 开放问题 = asStringList(row.开放问题 ?? row.open_questions); + + return { + schema: CONTEXT_FRAGMENT_SCHEMA, + 技能: skill, + brief: brief ?? "", + mount, + 稳变: normalizeStability(row.稳变 ?? row.stability), + 正文, + 开放问题, + }; +} + +export function isContextFragmentDoc(doc: unknown): boolean { + return Boolean(parseContextFragment(doc)); +} + +/** 投影级别:summary 优先 brief;fields 尝试列出正文顶层键 */ +export function projectFragmentContent( + raw: string, + projection: string, +): string { + const frag = parseContextFragment(raw); + const mode = projection.trim().toLowerCase(); + if (!frag) { + if (mode === "summary" && raw.length > 800) return `${raw.slice(0, 800)}…`; + return raw; + } + if (mode === "summary" || mode === "brief") { + return frag.brief || raw; + } + if (mode === "fields") { + if (frag.正文 && typeof frag.正文 === "object" && !Array.isArray(frag.正文)) { + const keys = Object.keys(frag.正文 as object); + return [`brief: ${frag.brief}`, `字段: ${keys.join("、") || "(无)"}`].join("\n"); + } + return frag.brief || raw; + } + if (mode === "fixed") { + return frag.brief || raw; + } + // full + return JSON.stringify( + { + schema: frag.schema, + 技能: frag.技能, + brief: frag.brief, + mount: frag.mount, + 稳变: frag.稳变, + 正文: frag.正文, + 开放问题: frag.开放问题, + }, + null, + 2, + ); +} + +export function contextFragmentToView(raw: unknown): ContextFragmentView { + const fragment = parseContextFragment(raw); + if (!fragment) { + return { ok: false, parseError: "无法解析为 context-fragment.v1", sections: [] }; + } + const sections: Array<{ title: string; lines: string[] }> = []; + if (fragment.brief) sections.push({ title: "概要", lines: [fragment.brief] }); + const meta: string[] = []; + if (fragment.技能) meta.push(`技能:${fragment.技能}`); + if (fragment.mount.length) meta.push(`挂载:${fragment.mount.join("、")}`); + if (fragment.稳变) meta.push(`稳变:${fragment.稳变}`); + if (meta.length) sections.push({ title: "挂载", lines: meta }); + + if (fragment.正文 != null && fragment.正文 !== "") { + if (typeof fragment.正文 === "string") { + sections.push({ title: "正文", lines: [fragment.正文] }); + } else if (typeof fragment.正文 === "object") { + const lines = flattenObjectLines(fragment.正文, 0, 4); + if (lines.length) sections.push({ title: "正文", lines }); + } + } + const rawDoc = + typeof raw === "object" && raw && !Array.isArray(raw) + ? (raw as Record) + : null; + const selfScore = rawDoc?.自评; + if (selfScore && typeof selfScore === "object" && !Array.isArray(selfScore)) { + const row = selfScore as Record; + const dims = Array.isArray(row.维度) ? row.维度 : []; + const lines = dims.map((d) => { + if (!d || typeof d !== "object") return String(d); + const r = d as Record; + return `${r.名 ?? "?"}:${r.分数 ?? "?"}%${r.说明 ? ` — ${r.说明}` : ""}`; + }); + if (typeof row.薄弱点 === "string" && row.薄弱点.trim()) { + lines.push(`薄弱点:${row.薄弱点.trim()}`); + } + if (lines.length) sections.push({ title: "自评", lines }); + } + const probe = rawDoc?.追问; + if (probe && typeof probe === "object" && !Array.isArray(probe)) { + const row = probe as Record; + const lines: string[] = []; + if (typeof row.导语 === "string" && row.导语.trim()) lines.push(row.导语.trim()); + const qs = Array.isArray(row.题目) ? row.题目 : []; + qs.forEach((q, i) => { + if (!q || typeof q !== "object") return; + const r = q as Record; + lines.push(`${i + 1}. ${r.问 ?? "?"}`); + if (Array.isArray(r.建议选项) && r.建议选项.length) { + lines.push(`选项:${r.建议选项.map(String).join(" / ")}`); + } + if (typeof r.示例 === "string" && r.示例.trim()) { + lines.push(`示例:${r.示例.trim()}`); + } + }); + if (lines.length) sections.push({ title: "追问", lines }); + } + if (fragment.开放问题.length) { + sections.push({ title: "开放问题", lines: fragment.开放问题 }); + } + return { ok: true, fragment, sections }; +} + +/** 从片段 JSON 抽出追问→询问卡;自评→评估导语(供验收挂载) */ +export function extractFragmentAskSidecar(raw: unknown): { + questions: QuestionItem[]; + assessment: string; +} { + let row: Record | null = null; + if (typeof raw === "string") { + const text = extractJsonObjectText(raw) ?? raw.trim(); + try { + const parsed = JSON.parse(text) as unknown; + if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) { + row = parsed as Record; + } + } catch { + return { questions: [], assessment: "" }; + } + } else if (raw && typeof raw === "object" && !Array.isArray(raw)) { + row = raw as Record; + } + if (!row) return { questions: [], assessment: "" }; + + const looksFragment = + row.schema === CONTEXT_FRAGMENT_SCHEMA || + (typeof row.brief === "string" && ("正文" in row || "body" in row)); + if (!looksFragment) return { questions: [], assessment: "" }; + + const assessmentParts: string[] = []; + const selfScore = row.自评; + if (selfScore && typeof selfScore === "object" && !Array.isArray(selfScore)) { + const sc = selfScore as Record; + const dims = Array.isArray(sc.维度) ? sc.维度 : []; + for (const d of dims) { + if (!d || typeof d !== "object") continue; + const r = d as Record; + const name = asString(r.名) ?? asString(r.维度) ?? "?"; + const score = r.分数; + const note = asString(r.说明); + const scoreText = + typeof score === "number" || typeof score === "string" + ? `${score}%` + : "?"; + assessmentParts.push( + note ? `${name} ${scoreText} — ${note}` : `${name} ${scoreText}`, + ); + } + const weak = asString(sc.薄弱点); + if (weak) assessmentParts.push(`薄弱点:${weak}`); + } + + const probe = row.追问; + const rawQs: unknown[] = []; + let lead = ""; + if (probe && typeof probe === "object" && !Array.isArray(probe)) { + const p = probe as Record; + lead = asString(p.导语) ?? ""; + const topics = Array.isArray(p.题目) ? p.题目 : []; + for (let i = 0; i < topics.length; i++) { + const t = topics[i]; + if (!t || typeof t !== "object") continue; + const q = t as Record; + const prompt = asString(q.问) ?? asString(q.prompt); + if (!prompt) continue; + const opts = Array.isArray(q.建议选项) + ? q.建议选项 + : Array.isArray(q.options) + ? q.options + : []; + const example = asString(q.示例); + const optLabels = opts + .map((o) => { + if (typeof o === "string") return o.trim(); + if (o && typeof o === "object") { + const r = o as Record; + return asString(r.label) ?? asString(r.文案) ?? asString(r.text) ?? ""; + } + return String(o ?? "").trim(); + }) + .filter(Boolean); + // 建议选项 → 选择题;示例并入题干提示,便于 ask 卡展示 + rawQs.push({ + id: `frag-q${i + 1}`, + prompt: example ? `${prompt}\n示例:${example}` : prompt, + options: optLabels, + allowOther: true, + required: false, + }); + } + } + // 开放问题兜底成无选项追问 + const openQs = asStringList(row.开放问题); + if (!rawQs.length && openQs.length) { + for (let i = 0; i < openQs.length; i++) { + rawQs.push({ + id: `frag-open${i + 1}`, + prompt: openQs[i], + allowOther: true, + required: false, + }); + } + } + + const questions = normalizeQuestions(rawQs); + const assessment = [lead, ...assessmentParts].filter(Boolean).join("\n"); + return { questions, assessment }; +} + +function flattenObjectLines(value: unknown, depth: number, maxDepth: number): string[] { + if (value == null) return []; + if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") { + return [String(value)]; + } + if (Array.isArray(value)) { + if (!value.length) return ["(空)"]; + return value.flatMap((v, i) => { + if (v == null || typeof v !== "object") return [String(v)]; + const row = v as Record; + const title = + [row.名, row.显示名, row.块id, row.rule_id, row.id, row.映射id, row.段id] + .find((x) => typeof x === "string" && x.trim()) ?? `#${i + 1}`; + const nested = flattenObjectLines(v, depth + 1, maxDepth); + if (depth + 1 >= maxDepth) { + const summary = Object.entries(row) + .filter(([, x]) => x != null && x !== "" && typeof x !== "object") + .slice(0, 4) + .map(([k, x]) => `${k}=${x}`) + .join(" · "); + return [`${title}${summary ? `(${summary})` : ""}`]; + } + return [`【${title}】`, ...nested.map((line) => ` ${line}`)]; + }); + } + if (typeof value === "object" && depth < maxDepth) { + const out: string[] = []; + for (const [k, v] of Object.entries(value as Record)) { + if (v == null || v === "") continue; + if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") { + out.push(`${k}:${v}`); + } else if (Array.isArray(v) || typeof v === "object") { + out.push(`${k}:`); + for (const line of flattenObjectLines(v, depth + 1, maxDepth)) { + out.push(` ${line}`); + } + } else { + out.push(`${k}:${String(v)}`); + } + } + return out; + } + if (typeof value === "object") { + const keys = Object.keys(value as object); + return [`(嵌套 ${keys.length} 键:${keys.slice(0, 8).join("、")}${keys.length > 8 ? "…" : ""})`]; + } + return [String(value)]; +} diff --git a/src/skills/context-order.ts b/src/skills/context-order.ts new file mode 100644 index 0000000..539f570 --- /dev/null +++ b/src/skills/context-order.ts @@ -0,0 +1,511 @@ +/** + * 上下文投影排序(context-order.v1)→ contextSegments。 + * 扁平数字序;「对话.历史」也是可投影标签,不是硬分区锚点。 + * 规范:docs/context-fragment-design.md + */ +import type { ContextSegmentDef } from "./context-segments.js"; +import { extractJsonObjectText } from "./worker-set-parse.js"; +import { parsePlaySlots, type PlaySlotsConfig } from "./play-slots.js"; +import { + DIALOGUE_HISTORY_TAG, + isDialogueHistoryRef, +} from "./dialogue-history.js"; + +export const CONTEXT_ORDER_SCHEMA = "context-order.v1" as const; +export const CONTEXT_ORDER_TAG = "设计.上下文投影排序"; +export const WORKER_PERSONA_REF = "worker.persona"; +export { DIALOGUE_HISTORY_TAG }; + +/** @deprecated 仅兼容旧表;拼装按扁平 order,历史靠 ref=对话.历史 */ +export type ContextOrderAnchor = "pre_history" | "post_history"; +export type ContextOrderProjection = "fixed" | "full" | "summary" | "fields"; + +export type ContextOrderInsert = { + order: number; + /** @deprecated 可选;set_anchor 语义改为「移到历史标签前/后」 */ + anchor?: ContextOrderAnchor; + ref: string; + projection: ContextOrderProjection; + note?: string; + label?: string; +}; + +export type ContextOrderSlot = { + ref: string; + label?: string; + inserts: ContextOrderInsert[]; +}; + +export type ContextOrderDoc = { + schema: typeof CONTEXT_ORDER_SCHEMA; + brief?: string; + play_slots?: PlaySlotsConfig; + slots: ContextOrderSlot[]; +}; + +function asString(v: unknown): string | undefined { + if (typeof v !== "string") return undefined; + const t = v.trim(); + return t || undefined; +} + +function parseAnchor(v: unknown): ContextOrderAnchor { + if (v === "post_history" || v === "dynamic" || v === "历史后") return "post_history"; + return "pre_history"; +} + +function parseProjection(v: unknown): ContextOrderProjection { + if (v === "summary" || v === "fields" || v === "fixed" || v === "full") return v; + if (v === "摘要") return "summary"; + if (v === "字段") return "fields"; + return "full"; +} + +function parseInsert(raw: unknown): ContextOrderInsert | undefined { + if (!raw || typeof raw !== "object" || Array.isArray(raw)) return undefined; + const row = raw as Record; + const ref = asString(row.ref) ?? asString(row.tag) ?? asString(row.源); + if (!ref) return undefined; + const orderRaw = row.order ?? row.序; + const order = typeof orderRaw === "number" && Number.isFinite(orderRaw) ? orderRaw : Number(orderRaw); + const hasAnchor = row.anchor != null || row.锚点 != null; + return { + order: Number.isFinite(order) ? order : 99, + anchor: hasAnchor ? parseAnchor(row.anchor ?? row.锚点) : undefined, + ref, + projection: parseProjection(row.projection ?? row.投影), + note: asString(row.note) ?? asString(row.说明), + label: asString(row.label) ?? asString(row.标题), + }; +} + +function parseSlot(raw: unknown): ContextOrderSlot | undefined { + if (!raw || typeof raw !== "object" || Array.isArray(raw)) return undefined; + const row = raw as Record; + const ref = asString(row.ref) ?? asString(row.id); + if (!ref) return undefined; + const insertsRaw = Array.isArray(row.inserts) ? row.inserts : []; + const inserts = insertsRaw + .map(parseInsert) + .filter((x): x is ContextOrderInsert => Boolean(x)) + .sort((a, b) => a.order - b.order); + return { + ref, + label: asString(row.label) ?? asString(row.name), + inserts, + }; +} + +export function parseContextOrder(raw: unknown): ContextOrderDoc | undefined { + let row: Record | null = null; + if (typeof raw === "string") { + const text = extractJsonObjectText(raw) ?? raw.trim(); + try { + const parsed = JSON.parse(text) as unknown; + if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) { + row = parsed as Record; + } + } catch { + return undefined; + } + } else if (raw && typeof raw === "object" && !Array.isArray(raw)) { + row = raw as Record; + } + if (!row) return undefined; + + const slotsRaw = Array.isArray(row.slots) ? row.slots : []; + const slots = slotsRaw + .map(parseSlot) + .filter((x): x is ContextOrderSlot => Boolean(x)); + if (!slots.length && row.schema !== CONTEXT_ORDER_SCHEMA) return undefined; + + return { + schema: CONTEXT_ORDER_SCHEMA, + brief: asString(row.brief), + play_slots: parsePlaySlots(row.play_slots ?? row.playSlots), + slots, + }; +} + +export function slotOrderForRef( + doc: ContextOrderDoc | undefined, + workerRef: string, +): ContextOrderSlot | undefined { + if (!doc) return undefined; + const id = workerRef.trim(); + return doc.slots.find((s) => s.ref.trim() === id); +} + +/** + * 将某槽的 inserts 转为 contextSegments:严格按扁平 order。 + * worker.persona 用 inline;对话.历史走专用 tag(拼装时动态投影)。 + */ +export function contextOrderToSegments(params: { + slot: ContextOrderSlot; + personaText?: string; +}): ContextSegmentDef[] { + const sorted = renumberContextOrder({ + schema: CONTEXT_ORDER_SCHEMA, + slots: [params.slot], + }).slots[0]!.inserts; + + const historyIdx = sorted.findIndex((i) => isDialogueHistoryRef(i.ref)); + const segments: ContextSegmentDef[] = []; + + for (const ins of sorted) { + const afterHistory = + historyIdx >= 0 && sorted.indexOf(ins) > historyIdx; + // tier 仅作缓存提示;拼装按数组顺序,不再重排 + const tier = afterHistory ? "dynamic" : "static"; + const id = `ord${ins.order}-${ins.ref}`.replace(/[^\w.\u4e00-\u9fff-]+/g, "_"); + const label = + ins.label || + (ins.note ? `## ${ins.ref}(${ins.note})` : `## ${ins.ref}`); + + if (ins.ref === WORKER_PERSONA_REF || ins.ref === "persona") { + const inline = params.personaText?.trim(); + if (!inline) continue; + segments.push({ + id, + tier, + tags: [], + label, + inline, + projection: ins.projection, + }); + continue; + } + + const tag = isDialogueHistoryRef(ins.ref) + ? DIALOGUE_HISTORY_TAG + : ins.ref; + segments.push({ + id, + tier, + tags: [tag], + label, + projection: ins.projection, + }); + } + return segments; +} + +export type ContextOrderInsertView = { + index: number; + order: number; + anchor?: ContextOrderAnchor; + ref: string; + projection: ContextOrderProjection; + note?: string; + label?: string; + isHistory?: boolean; + /** 展示一行 */ + line: string; +}; + +export function contextOrderToView(doc: ContextOrderDoc): { + brief?: string; + editable: true; + slots: Array<{ + ref: string; + label?: string; + inserts: ContextOrderInsertView[]; + lines: string[]; + }>; +} { + return { + brief: doc.brief, + editable: true, + slots: doc.slots.map((s) => { + const inserts: ContextOrderInsertView[] = s.inserts.map((i, index) => { + const hist = isDialogueHistoryRef(i.ref); + const line = + `${i.order} · ${hist ? "对话.历史" : i.ref}` + + (i.projection !== "full" ? ` · ${i.projection}` : "") + + (i.note ? ` — ${i.note}` : ""); + return { + index, + order: i.order, + anchor: i.anchor, + ref: hist ? DIALOGUE_HISTORY_TAG : i.ref, + projection: i.projection, + note: i.note, + label: i.label, + isHistory: hist, + line, + }; + }); + return { + ref: s.ref, + label: s.label, + inserts, + lines: inserts.map((x) => x.line), + }; + }), + }; +} + +/** 扁平重编号 0..n;persona 尽量置顶;保证至多一条对话.历史 */ +export function renumberContextOrder(doc: ContextOrderDoc): ContextOrderDoc { + return { + ...doc, + schema: CONTEXT_ORDER_SCHEMA, + slots: doc.slots.map((slot) => { + let inserts = slot.inserts.map((i) => ({ + ...i, + ref: isDialogueHistoryRef(i.ref) ? DIALOGUE_HISTORY_TAG : i.ref, + })); + // 合并重复历史标签:保留第一条 + const seenHist = new Set(); + inserts = inserts.filter((i, idx) => { + if (!isDialogueHistoryRef(i.ref)) return true; + if (seenHist.size) return false; + seenHist.add(idx); + return true; + }); + const personaIdx = inserts.findIndex( + (i) => i.ref === WORKER_PERSONA_REF || i.ref === "persona", + ); + if (personaIdx > 0) { + const [p] = inserts.splice(personaIdx, 1); + inserts.unshift(p!); + } + return { + ...slot, + inserts: inserts.map((i, idx) => ({ + ...i, + order: idx, + anchor: undefined, + })), + }; + }), + }; +} + +export type ContextOrderEdit = + | { action: "move"; slotRef: string; index: number; delta: -1 | 1 } + | { + action: "set_anchor"; + slotRef: string; + index: number; + anchor: ContextOrderAnchor; + } + | { + action: "set_projection"; + slotRef: string; + index: number; + projection: ContextOrderProjection; + } + | { action: "replace"; context_order: unknown }; + +function cloneDoc(doc: ContextOrderDoc): ContextOrderDoc { + return { + schema: CONTEXT_ORDER_SCHEMA, + brief: doc.brief, + play_slots: doc.play_slots ? { ...doc.play_slots } : undefined, + slots: doc.slots.map((s) => ({ + ...s, + inserts: s.inserts.map((i) => ({ ...i })), + })), + }; +} + +/** + * 扁平列表编排: + * - move:与相邻项交换 + * - set_anchor:把该项移到「对话.历史」之前(pre)或之后(post);无历史则先插入历史标签 + * - set_projection:只改投影 + */ +export function applyContextOrderEdit( + doc: ContextOrderDoc, + edit: ContextOrderEdit, +): ContextOrderDoc { + if (edit.action === "replace") { + const parsed = parseContextOrder(edit.context_order); + if (!parsed) throw new Error("context_order 无法解析"); + return renumberContextOrder(parsed); + } + + const next = cloneDoc(doc); + const slot = next.slots.find((s) => s.ref.trim() === edit.slotRef.trim()); + if (!slot) throw new Error(`未找到槽 ${edit.slotRef}`); + if (edit.index < 0 || edit.index >= slot.inserts.length) { + throw new Error("插入项下标越界"); + } + + if (edit.action === "set_projection") { + slot.inserts[edit.index]!.projection = edit.projection; + return renumberContextOrder(next); + } + + if (edit.action === "set_anchor") { + const item = slot.inserts[edit.index]!; + if (isDialogueHistoryRef(item.ref)) { + return renumberContextOrder(next); + } + slot.inserts.splice(edit.index, 1); + ensureHistoryInsert(slot); + const histIdx = slot.inserts.findIndex((i) => isDialogueHistoryRef(i.ref)); + const insertAt = + edit.anchor === "pre_history" ? Math.max(0, histIdx) : histIdx + 1; + slot.inserts.splice(insertAt, 0, item); + return renumberContextOrder(next); + } + + // move:整表相邻交换 + const to = edit.index + edit.delta; + if (to < 0 || to >= slot.inserts.length) { + return renumberContextOrder(next); + } + const tmp = slot.inserts[edit.index]!; + slot.inserts[edit.index] = slot.inserts[to]!; + slot.inserts[to] = tmp; + return renumberContextOrder(next); +} + +function ensureHistoryInsert(slot: ContextOrderSlot): void { + if (slot.inserts.some((i) => isDialogueHistoryRef(i.ref))) return; + // 插在中段:persona 后、易变前 + const mid = Math.min( + Math.max(1, Math.ceil(slot.inserts.length / 2)), + slot.inserts.length, + ); + slot.inserts.splice(mid, 0, { + order: mid, + ref: DIALOGUE_HISTORY_TAG, + projection: "summary", + note: "对话历史(按投影裁剪)", + }); +} + +export function serializeContextOrder(doc: ContextOrderDoc): string { + const normalized = renumberContextOrder(doc); + return JSON.stringify( + { + schema: CONTEXT_ORDER_SCHEMA, + brief: normalized.brief, + play_slots: normalized.play_slots, + slots: normalized.slots.map((s) => ({ + ref: s.ref, + label: s.label, + inserts: s.inserts.map((i) => ({ + order: i.order, + ref: i.ref, + projection: i.projection, + ...(i.note ? { note: i.note } : {}), + ...(i.label ? { label: i.label } : {}), + })), + })), + }, + null, + 2, + ); +} + +/** 把排序表写入运行规格 JSON 文本的 context_order 字段 */ +export function mergeContextOrderIntoWorkerSetJson( + workerSetRaw: string, + order: ContextOrderDoc, +): string { + const text = extractJsonObjectText(workerSetRaw) ?? workerSetRaw.trim(); + let row: Record; + try { + const parsed = JSON.parse(text) as unknown; + if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { + throw new Error("设计.worker集 不是 JSON 对象"); + } + row = parsed as Record; + } catch { + throw new Error("设计.worker集 无法解析为 JSON"); + } + const normalized = renumberContextOrder(order); + row.context_order = JSON.parse(serializeContextOrder(normalized)) as unknown; + delete row.contextOrder; + return JSON.stringify(row, null, 2); +} + +/** + * 无排序表时,从 worker 声明的 static/dynamic 合成可编辑初稿(含 persona)。 + */ +/** 声明缺 context 时的兜底(与模板/检查器默认对齐的最小集) */ +const SYNTH_FALLBACK_CONTEXT: Record< + string, + { static: string[]; dynamic: string[] } +> = { + "world-simulator": { + static: ["设计.变量设计与更新规则"], + dynamic: ["变量.当前", "运行.事件流", "用户.最新输入", "上下文.角色态度"], + }, + narrator: { + static: ["设计.叙事指南"], + dynamic: ["运行.本轮.裁决", "用户.最新输入"], + }, + "role-decide": { + static: [], + dynamic: ["用户.最新输入", "运行.本轮.裁决"], + }, +}; + +export function synthesizeContextOrderFromWorkers( + workers: Array<{ + ref?: string; + name?: string; + context?: { static?: string[]; dynamic?: string[] }; + }>, + playSlots?: PlaySlotsConfig, +): ContextOrderDoc | undefined { + const slots: ContextOrderSlot[] = []; + for (const w of workers) { + const ref = w.ref?.trim(); + if (!ref) continue; + const fallback = SYNTH_FALLBACK_CONTEXT[ref]; + const staticTags = w.context?.static ?? fallback?.static ?? []; + const dynamicTags = w.context?.dynamic ?? fallback?.dynamic ?? ["用户.最新输入"]; + const inserts: ContextOrderInsert[] = [ + { + order: 0, + ref: WORKER_PERSONA_REF, + projection: "fixed", + note: "槽位人设", + }, + ]; + let o = 1; + for (const tag of staticTags) { + if (!tag?.trim() || tag === "设计.worker集") continue; + if (isDialogueHistoryRef(tag)) continue; + inserts.push({ + order: o++, + ref: tag.trim(), + projection: tag.includes("事件流") ? "summary" : "full", + }); + } + 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(), + projection: "full", + }); + } + slots.push({ + ref, + label: w.name, + inserts, + }); + } + if (!slots.length) return undefined; + return renumberContextOrder({ + schema: CONTEXT_ORDER_SCHEMA, + brief: "扁平投影序(含对话.历史标签,可编排)", + play_slots: playSlots, + slots, + }); +} diff --git a/src/skills/context-segments.ts b/src/skills/context-segments.ts index 9cce413..1c650b3 100644 --- a/src/skills/context-segments.ts +++ b/src/skills/context-segments.ts @@ -9,16 +9,28 @@ import { CREATION_ACCEPTED_CONTENT_TAG, formatAcceptedContentForPrompt, } from "./creation-units.js"; +import { projectFragmentContent } from "./context-fragment.js"; +import { + buildHistoryFallback, + isDialogueHistoryRef, + projectDialogueHistory, + DIALOGUE_HISTORY_TAG, +} from "./dialogue-history.js"; export type ContextSegmentTier = "static" | "dynamic"; export type ContextSegmentDef = { id: string; + /** 缓存提示;有序拼装时不再按 tier 重排 */ tier: ContextSegmentTier; tags: string[]; label?: string; /** latest | concat | tail_lines_N */ policy?: string; + /** 不读黑板,直接注入(如 worker.persona) */ + inline?: string; + /** context-order 投影级别:full | summary | fields | fixed */ + projection?: string; }; export function parseContextSegments(raw: unknown): ContextSegmentDef[] { @@ -32,13 +44,16 @@ export function parseContextSegments(raw: unknown): ContextSegmentDef[] { const tags = Array.isArray(r.tags) ? r.tags.filter((t): t is string => typeof t === "string" && t.trim()).map((t) => t.trim()) : []; - if (!id || tags.length === 0) continue; + const inline = typeof r.inline === "string" ? r.inline.trim() : undefined; + if (!id || (tags.length === 0 && !inline)) continue; out.push({ id, tier, tags, label: typeof r.label === "string" ? r.label.trim() : undefined, policy: typeof r.policy === "string" ? r.policy.trim() : undefined, + inline: inline || undefined, + projection: typeof r.projection === "string" ? r.projection.trim() : undefined, }); } return out; @@ -78,20 +93,50 @@ function formatSegmentBody( blackboard: Blackboard, inputMerge: BlackboardInputMerge, ): string { + if (segment.inline?.trim()) { + return applyProjection(segment.inline.trim(), segment.projection); + } const parts: string[] = []; for (const tag of segment.tags) { - let content = readTagContent(tag, inputs, blackboard, inputMerge); - if (!content) continue; - if (tag === CREATION_ACCEPTED_CONTENT_TAG) { - content = formatAcceptedContentForPrompt(content); + let content: string; + if (isDialogueHistoryRef(tag)) { + content = resolveDialogueHistory(inputs, blackboard, inputMerge); + content = projectDialogueHistory(content, segment.projection); + } else { + content = readTagContent(tag, inputs, blackboard, inputMerge); + if (!content) continue; + if (tag === CREATION_ACCEPTED_CONTENT_TAG) { + content = formatAcceptedContentForPrompt(content); + } + content = applyPolicy(content, segment.policy); + content = applyProjection(content, segment.projection); } - content = applyPolicy(content, segment.policy); if (!content.trim()) continue; parts.push(content.trim()); } return parts.join("\n\n"); } +function resolveDialogueHistory( + inputs: Record, + blackboard: Blackboard, + inputMerge: BlackboardInputMerge, +): string { + const direct = + readTagContent(DIALOGUE_HISTORY_TAG, inputs, blackboard, inputMerge) || + readTagContent("对话历史", inputs, blackboard, inputMerge); + return buildHistoryFallback({ + dialogueHistory: direct, + eventStream: readTagContent("运行.事件流", inputs, blackboard, "concat"), + latestUser: readTagContent("用户.最新输入", inputs, blackboard, inputMerge), + }); +} + +function applyProjection(content: string, projection?: string): string { + if (!projection || projection === "full") return content; + return projectFragmentContent(content, projection); +} + /** * 按 contextSegments 拼装 user 侧上下文。 * 无 segments 时回退为 JSON inputs(兼容旧 skill)。 @@ -124,8 +169,6 @@ export function assembleWorkerContext(params: { ); } - const staticSegs = segments.filter((s) => s.tier === "static"); - const dynamicSegs = segments.filter((s) => s.tier === "dynamic"); const blocks: string[] = []; const render = (seg: ContextSegmentDef) => { @@ -138,8 +181,8 @@ export function assembleWorkerContext(params: { } }; - for (const seg of staticSegs) render(seg); - for (const seg of dynamicSegs) render(seg); + // 严格按 segments 数组顺序(投影排序表顺序);不再按 static/dynamic 重排 + for (const seg of segments) render(seg); // 定稿摘要:若未在 segments 中声明,仍附在末尾 const brief = params.inputs[CONTEXT_BRIEF_TAG]?.trim(); diff --git a/src/skills/creation-flow.ts b/src/skills/creation-flow.ts index 9203d14..8d11342 100644 --- a/src/skills/creation-flow.ts +++ b/src/skills/creation-flow.ts @@ -598,17 +598,25 @@ function slugFromName(name: string): string { 交互范式: "interaction", 美学纲领: "aesthetics", 叙事指南: "narrative", + 叙事指南与故事推进: "narrative", 实现机制: "mechanism", - 世界蓝图与人文地理: "world-blueprint", + 舞台骨架: "world-blueprint", + 世界蓝图与人文地理: "world-blueprint", // 旧称 + 世界蓝图: "world-blueprint", // 旧简称 生成规则: "generation-rules", 具体实例: "concrete-instances", 拓扑图谱: "topology", 设计状态栏: "status-bar", + 设计监控栏: "status-bar", 变量设计与更新规则: "variable-design", 变量控制上下文: "variable-context", 设计回复格式: "reply-format", - "Worker 规格": "worker-spec", + 正文组成: "reply-format", + 游玩拓扑: "worker-spec", + "Worker 规格": "worker-spec", // 旧称,等同游玩拓扑 细化终稿: "refine", + 开场白与开场变量: "opening-setup", + 开场白: "opening-setup", }; return map[name] ?? name; } diff --git a/src/skills/creation-units.ts b/src/skills/creation-units.ts index 065fd8c..6c0f624 100644 --- a/src/skills/creation-units.ts +++ b/src/skills/creation-units.ts @@ -112,7 +112,7 @@ export const FIXED_CONTEXT_CATALOG: Array<{ { id: "fixed:narrative_guide", flavor: "narrative_guide", - label: "叙事指南", + label: "叙事指南与故事推进", weighty: true, hint: "世界态度与体验边界(残酷/不有求必应/随机危险等);≠ 文风", }, diff --git a/src/skills/declared-worker.ts b/src/skills/declared-worker.ts index dcacd3d..580846e 100644 --- a/src/skills/declared-worker.ts +++ b/src/skills/declared-worker.ts @@ -24,6 +24,13 @@ import { parseResidentContext, residentTagFor, } from "./resident-context.js"; +import { + CONTEXT_ORDER_TAG, + contextOrderToSegments, + parseContextOrder, + slotOrderForRef, +} from "./context-order.js"; +import type { ContextSegmentDef } from "./context-segments.js"; type WorkerTemplateDoc = { id?: string; @@ -106,7 +113,14 @@ export async function resolveRunnableWorker(params: { } const raw = readWorkerSetYamlForDeclaration(params.blackboard, params.session); - const parsed = raw ? parseWorkerSetYaml(raw.yaml) : null; + let parsed = raw ? parseWorkerSetYaml(raw.yaml) : null; + // 用户编排的排序表优先:规格内 context_order → 独立 tag + if (parsed && !parseContextOrder(parsed.context_order)) { + const fromTag = parseContextOrder( + params.blackboard.getContentByTag(CONTEXT_ORDER_TAG), + ); + if (fromTag) parsed = { ...parsed, context_order: fromTag }; + } const entry = parsed?.workers.find( (w) => w.ref?.trim() === params.workerId.trim(), ); @@ -190,16 +204,6 @@ export function buildDeclaredWorkerSkill(params: { .filter((e) => e.position === "dynamic") .map(residentTagFor); - const inputTags = [ - ...new Set([ - ...staticTags, - ...residentStaticTags, - ...dynamicTags, - ...residentDynamicTags, - CONTEXT_BRIEF_TAG, - ]), - ]; - const duty = params.entry.duty?.trim() || params.template?.duty?.trim() || @@ -212,6 +216,59 @@ export function buildDeclaredWorkerSkill(params: { const premises = (params.workerSet?.core_premises ?? []).filter(Boolean); const residentSection = formatResidentPromptSection(resident, id); + const personaText = [duty, excerpt].filter(Boolean).join("\n\n"); + const orderDoc = parseContextOrder(params.workerSet?.context_order); + const orderSlot = slotOrderForRef(orderDoc, id); + let contextSegments: ContextSegmentDef[] | undefined; + if (orderSlot && orderSlot.inserts.length) { + contextSegments = contextOrderToSegments({ + slot: orderSlot, + personaText, + }); + } + + const orderTags = (contextSegments ?? []).flatMap((s) => s.tags); + const inputTags = [ + ...new Set([ + ...staticTags, + ...residentStaticTags, + ...dynamicTags, + ...residentDynamicTags, + ...orderTags, + CONTEXT_BRIEF_TAG, + ]), + ]; + + // 无 context_order 时:沿用模板 static→dynamic 两档 segments + if (!contextSegments?.length) { + contextSegments = [ + ...staticTags.map((tag, i) => ({ + id: `static-${i}`, + tier: "static" as const, + tags: [tag], + label: `## ${tag}`, + })), + ...residentStaticTags.map((tag, i) => ({ + id: `resident-s-${i}`, + tier: "static" as const, + tags: [tag], + label: `## ${tag}`, + })), + ...dynamicTags.map((tag, i) => ({ + id: `dynamic-${i}`, + tier: "dynamic" as const, + tags: [tag], + label: `## ${tag}`, + })), + ...residentDynamicTags.map((tag, i) => ({ + id: `resident-d-${i}`, + tier: "dynamic" as const, + tags: [tag], + label: `## ${tag}`, + })), + ]; + } + const body = [ `# ${params.template?.label ?? id}`, "", @@ -248,6 +305,7 @@ export function buildDeclaredWorkerSkill(params: { inputMerge: "latest", path: `declaration:${id}`, body, + contextSegments, }; const promptBody = [ diff --git a/src/skills/dialogue-history.ts b/src/skills/dialogue-history.ts new file mode 100644 index 0000000..4b7f3e4 --- /dev/null +++ b/src/skills/dialogue-history.ts @@ -0,0 +1,96 @@ +/** + * 对话.历史:排序表中的可投影「标签」,不是硬锚点分区。 + * 规范:docs/context-fragment-design.md + */ +export const DIALOGUE_HISTORY_TAG = "对话.历史"; + +/** 兼容别名(排序表 / 旧文档) */ +const HISTORY_ALIASES = new Set([ + DIALOGUE_HISTORY_TAG, + "history", + "chat.history", + "对话历史", + "历史对话", +]); + +export function isDialogueHistoryRef(ref: string): boolean { + return HISTORY_ALIASES.has(ref.trim()); +} + +export type HistoryProjection = "fixed" | "full" | "summary" | "fields"; + +/** + * 按投影级别裁剪历史正文。 + * - fixed:几乎不注入(占位一句) + * - fields:最近约 5 段 + * - summary:最近约 20 段 + * - full:最近约 80 段(或整段若更短) + */ +export function projectDialogueHistory( + raw: string, + projection?: string, +): string { + const text = (raw || "").trim(); + const mode = (projection || "full").trim().toLowerCase(); + if (!text) { + return mode === "fixed" ? "" : "(尚无对话历史)"; + } + if (mode === "fixed") return "(本步不注入历史正文)"; + + const blocks = splitHistoryBlocks(text); + const n = + mode === "fields" ? 5 : mode === "summary" || mode === "brief" ? 20 : 80; + const sliced = blocks.length > n ? blocks.slice(-n) : blocks; + const body = sliced.join("\n\n"); + if (blocks.length > n) { + return `(仅最近 ${n} 段,更早已省略)\n\n${body}`; + } + return body; +} + +function splitHistoryBlocks(text: string): string[] { + const byFence = text + .split(/\n(?=##\s|【|用户[::]|助手[::]|系统[::])/u) + .map((s) => s.trim()) + .filter(Boolean); + if (byFence.length >= 2) return byFence; + const byBlank = text + .split(/\n{2,}/) + .map((s) => s.trim()) + .filter(Boolean); + if (byBlank.length >= 2) return byBlank; + const lines = text.split(/\r?\n/).map((l) => l.trim()).filter(Boolean); + return lines.length ? lines : [text]; +} + +/** 追加一轮用户/系统可见内容到历史正文 */ +export function appendDialogueHistoryTurn( + existing: string, + turn: { role: "用户" | "助手" | "系统"; text: string }, +): string { + const body = turn.text.trim(); + if (!body) return (existing || "").trim(); + const block = `## ${turn.role}\n\n${body}`; + const prev = (existing || "").trim(); + return prev ? `${prev}\n\n${block}` : block; +} + +/** + * 若尚无 对话.历史,用事件流等拼一份可读底稿(仅用于投影,不强制回写)。 + */ +export function buildHistoryFallback(params: { + dialogueHistory?: string; + eventStream?: string; + latestUser?: string; +}): string { + const hist = params.dialogueHistory?.trim(); + if (hist) return hist; + const parts: string[] = []; + if (params.eventStream?.trim()) { + parts.push(`## 事件流\n\n${params.eventStream.trim()}`); + } + if (params.latestUser?.trim()) { + parts.push(`## 用户\n\n${params.latestUser.trim()}`); + } + return parts.join("\n\n"); +} diff --git a/src/skills/play-slots.ts b/src/skills/play-slots.ts new file mode 100644 index 0000000..7e8c504 --- /dev/null +++ b/src/skills/play-slots.ts @@ -0,0 +1,265 @@ +/** + * 固定游玩槽位:世界模拟路径不再自由发明 workers[], + * 以勾选 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 OnDemandSlotId = "chance"; + +export type PlaySlotsConfig = { + /** 主世界层(裁决);默认 true */ + gm: boolean; + /** 叙事转述;默认 true */ + narrator: boolean; + /** 单角色知密视角;默认 false */ + perspective: boolean; + /** + * 机遇裁定(程序骰子/抽签/比点);默认 false。 + * 启用后可按需 run_worker,不自动每轮上场。 + */ + chance?: boolean; + /** 可选覆盖默认 ref */ + refs?: Partial>; +}; + +export const DEFAULT_PLAY_SLOT_REFS: Record = { + gm: "world-simulator", + narrator: "narrator", + perspective: "role-decide", +}; + +export const DEFAULT_ON_DEMAND_REFS: Record = { + chance: "chance", +}; + +export const PLAY_SLOT_META: Record< + PlaySlotId, + { label: string; purpose: string; defaultAcceptance: WorkerAcceptance } +> = { + gm: { + label: "主世界层", + purpose: "读真值与 Progressive 投影,输出结构化裁决包;可提议变量变更。", + defaultAcceptance: "continue", + }, + narrator: { + label: "叙事转述", + purpose: "只读裁决包 + 文风常驻,输出用户可见正文。", + defaultAcceptance: "review", + }, + perspective: { + label: "角色视角", + purpose: "强信息隔离时出反应建议;不写真值、不写终稿。", + defaultAcceptance: "continue", + }, +}; + +export const ON_DEMAND_SLOT_META: Record< + OnDemandSlotId, + { label: string; purpose: string; defaultAcceptance: WorkerAcceptance } +> = { + chance: { + label: "机遇裁定", + purpose: + "程序工具:掷骰、比点、抽签/加权抽取;按需调用,禁止模型编造随机结果。", + defaultAcceptance: "continue", + }, +}; + +/** 推荐回合顺序(启用的槽按此排序;不含按需槽) */ +export const PLAY_SLOT_ORDER: PlaySlotId[] = ["perspective", "gm", "narrator"]; + +export function defaultPlaySlots(): PlaySlotsConfig { + return { gm: true, narrator: true, perspective: false, chance: false }; +} + +export function parsePlaySlots(raw: unknown): PlaySlotsConfig | undefined { + if (!raw || typeof raw !== "object" || Array.isArray(raw)) return undefined; + const row = raw as Record; + const base = defaultPlaySlots(); + if (typeof row.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; + if (typeof row.rng === "boolean") base.chance = row.rng; + + const refsRaw = row.refs; + if (refsRaw && typeof refsRaw === "object" && !Array.isArray(refsRaw)) { + const refs: Partial> = {}; + for (const id of Object.keys(DEFAULT_PLAY_SLOT_REFS) as PlaySlotId[]) { + const v = (refsRaw as Record)[id]; + if (typeof v === "string" && v.trim()) refs[id] = v.trim(); + } + for (const id of Object.keys(DEFAULT_ON_DEMAND_REFS) as OnDemandSlotId[]) { + const v = (refsRaw as Record)[id]; + if (typeof v === "string" && v.trim()) refs[id] = v.trim(); + } + if (Object.keys(refs).length) base.refs = refs; + } + return base; +} + +export function refForSlot( + slots: PlaySlotsConfig, + id: PlaySlotId | OnDemandSlotId, +): string { + if (id === "chance") { + return slots.refs?.chance?.trim() || DEFAULT_ON_DEMAND_REFS.chance; + } + return slots.refs?.[id]?.trim() || DEFAULT_PLAY_SLOT_REFS[id]; +} + +export function enabledPlaySlotIds(slots: PlaySlotsConfig): PlaySlotId[] { + return PLAY_SLOT_ORDER.filter((id) => Boolean(slots[id])); +} + +export function enabledOnDemandSlotIds(slots: PlaySlotsConfig): OnDemandSlotId[] { + return slots.chance ? ["chance"] : []; +} + +/** 每轮管线 refs(不含按需槽) */ +export function refsFromPlaySlots(slots: PlaySlotsConfig): string[] { + const seen = new Set(); + const out: string[] = []; + for (const id of enabledPlaySlotIds(slots)) { + const ref = refForSlot(slots, id); + if (seen.has(ref)) continue; + seen.add(ref); + out.push(ref); + } + return out; +} + +/** 按需可调度 refs(可并入 activeWorkerIds,但不进自动回合序) */ +export function onDemandRefsFromPlaySlots(slots: PlaySlotsConfig): string[] { + return enabledOnDemandSlotIds(slots).map((id) => refForSlot(slots, id)); +} + +export function workerEntryForSlot( + slots: PlaySlotsConfig, + id: PlaySlotId, +): WorkerSetEntry { + const meta = PLAY_SLOT_META[id]; + const ref = refForSlot(slots, id); + return { + ref, + name: meta.label, + role: id, + duty: meta.purpose, + when: + id === "perspective" + ? "强信息隔离且本轮需要该角色独立反应时" + : id === "gm" + ? "每轮用户输入后" + : "主世界层裁决包就绪后", + rationale: + id === "perspective" + ? "知密内容不能进主世界层上下文" + : id === "gm" + ? "裁决与真值变更需要独立推理槽" + : "用户可见正文与裁决分离,避免文风与规则互相挤压", + acceptance: meta.defaultAcceptance, + invocation: "turn", + }; +} + +export function workerEntryForOnDemandSlot( + slots: PlaySlotsConfig, + id: OnDemandSlotId, +): WorkerSetEntry { + const meta = ON_DEMAND_SLOT_META[id]; + const ref = refForSlot(slots, id); + return { + ref, + name: meta.label, + role: id, + duty: meta.purpose, + when: "需要程序随机、骰子、比点或抽签时由编排器按需调用", + rationale: "真随机必须由程序给出,不能靠模型编造", + acceptance: meta.defaultAcceptance, + invocation: "on_demand", + }; +} + +/** 仅由 play_slots 展开 workers(含按需槽条目) */ +export function expandPlaySlotsToWorkers(slots: PlaySlotsConfig): WorkerSetEntry[] { + return [ + ...enabledPlaySlotIds(slots).map((id) => workerEntryForSlot(slots, id)), + ...enabledOnDemandSlotIds(slots).map((id) => + workerEntryForOnDemandSlot(slots, id), + ), + ]; +} + +/** + * 若声明了 play_slots:用槽位展开补齐/覆盖同 ref 的骨架; + * 已有 workers 里同 ref 的 context/outputs/name 覆盖保留。 + */ +export function mergeWorkersWithPlaySlots( + existing: WorkerSetEntry[], + slots: PlaySlotsConfig | undefined, +): WorkerSetEntry[] { + if (!slots) return existing; + const expanded = expandPlaySlotsToWorkers(slots); + if (!existing.length) return expanded; + + const byRef = new Map(); + for (const w of existing) { + const ref = w.ref?.trim(); + if (ref) byRef.set(ref, w); + } + + const merged: WorkerSetEntry[] = expanded.map((slotEntry) => { + const ref = slotEntry.ref!.trim(); + const prev = byRef.get(ref); + if (!prev) return slotEntry; + return { + ...slotEntry, + name: prev.name ?? slotEntry.name, + duty: prev.duty ?? slotEntry.duty, + when: prev.when ?? slotEntry.when, + rationale: prev.rationale ?? slotEntry.rationale, + acceptance: prev.acceptance ?? slotEntry.acceptance, + invocation: prev.invocation ?? slotEntry.invocation, + context: prev.context ?? slotEntry.context, + outputs: prev.outputs ?? slotEntry.outputs, + presentation: prev.presentation ?? slotEntry.presentation, + merge_considered: prev.merge_considered ?? slotEntry.merge_considered, + }; + }); + + // 保留不在固定槽内的额外 worker(兼容旧规格 / 扩写路径) + const slotRefs = new Set(expanded.map((e) => e.ref!.trim())); + for (const w of existing) { + const ref = w.ref?.trim(); + if (!ref || slotRefs.has(ref)) continue; + merged.push(w); + } + return merged; +} + +export function inferPlaySlotsFromWorkers( + workers: WorkerSetEntry[], +): PlaySlotsConfig | undefined { + if (!workers.length) return undefined; + const refs = new Set( + workers.map((w) => w.ref?.trim()).filter((r): r is string => Boolean(r)), + ); + 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; + return { + gm: hasGm, + narrator: hasNarrator, + perspective: hasPerspective, + chance: hasChance, + }; +} diff --git a/src/skills/settlement-packet.ts b/src/skills/settlement-packet.ts new file mode 100644 index 0000000..56fefc3 --- /dev/null +++ b/src/skills/settlement-packet.ts @@ -0,0 +1,231 @@ +/** + * 主世界层 → 叙事转述的裁决包(settlement / direction packet)。 + * 存黑板 tag:运行.本轮.裁决 + */ +import { extractJsonObjectText } from "./worker-set-parse.js"; + +export const SETTLEMENT_SCHEMA = "settlement.v1" as const; +export const SETTLEMENT_TAG = "运行.本轮.裁决"; + +export type SettlementNpcMove = { + who: string; + move: string; + wants?: string; + refuses_to_say?: string; +}; + +export type SettlementVariableChange = { + key: string; + from?: unknown; + to?: unknown; + delta?: number; + note?: string; +}; + +export type SettlementPacket = { + schema: typeof SETTLEMENT_SCHEMA; + /** 玩家本轮行动摘要 */ + player_action?: string; + /** 已落地的客观变化(短句列表) */ + resolved?: string[]; + /** 转述应呈现的场面现状(玩家安全) */ + visible_now?: string; + /** NPC 主动动作(binding move) */ + npc_moves?: SettlementNpcMove[]; + /** 真值变更提议(Runtime 合并进 变量.当前) */ + variable_changes?: SettlementVariableChange[]; + /** 禁止写入用户正文的内容 */ + do_not_say?: string[]; + /** 语气提示(通常来自 Progressive 投影) */ + tone_hint?: string; + /** 可选:建议玩家选项(无主语动作短语) */ + suggested_actions?: string[]; +}; + +export type SettlementPacketView = { + ok: boolean; + parseError?: string; + packet?: SettlementPacket; + /** 用户友好分节,供 UI 渲染 */ + sections: Array<{ title: string; lines: string[] }>; +}; + +function asString(v: unknown): string | undefined { + if (typeof v !== "string") return undefined; + const t = v.trim(); + return t || undefined; +} + +function asStringList(v: unknown): string[] { + if (!Array.isArray(v)) return []; + return v.map((x) => asString(x)).filter((x): x is string => Boolean(x)); +} + +function parseNpcMoves(raw: unknown): SettlementNpcMove[] | undefined { + if (!Array.isArray(raw)) return undefined; + const out: SettlementNpcMove[] = []; + for (const item of raw) { + if (!item || typeof item !== "object" || Array.isArray(item)) continue; + const row = item as Record; + const who = asString(row.who) ?? asString(row.actor) ?? asString(row.name); + const move = asString(row.move) ?? asString(row.action); + if (!who || !move) continue; + out.push({ + who, + move, + wants: asString(row.wants), + refuses_to_say: asString(row.refuses_to_say) ?? asString(row.refusesToSay), + }); + } + return out.length ? out : undefined; +} + +function parseVariableChanges( + raw: unknown, +): SettlementVariableChange[] | undefined { + if (!Array.isArray(raw)) return undefined; + const out: SettlementVariableChange[] = []; + for (const item of raw) { + if (!item || typeof item !== "object" || Array.isArray(item)) continue; + const row = item as Record; + const key = asString(row.key) ?? asString(row.name) ?? asString(row.变量); + if (!key) continue; + const delta = + typeof row.delta === "number" && Number.isFinite(row.delta) + ? row.delta + : undefined; + out.push({ + key, + from: row.from ?? row.从, + to: row.to ?? row.到 ?? row.value, + delta, + note: asString(row.note) ?? asString(row.备注), + }); + } + return out.length ? out : undefined; +} + +export function normalizeSettlementPacket( + row: Record, +): SettlementPacket { + const resolved = asStringList(row.resolved); + const resolvedZh = asStringList(row.已解决); + const doNot = asStringList(row.do_not_say); + const doNotZh = asStringList(row.禁止说出); + const suggested = asStringList(row.suggested_actions); + const suggestedZh = asStringList(row.建议行动); + return { + schema: SETTLEMENT_SCHEMA, + player_action: asString(row.player_action) ?? asString(row.玩家行动), + resolved: resolved.length ? resolved : resolvedZh.length ? resolvedZh : undefined, + visible_now: asString(row.visible_now) ?? asString(row.可见现状), + npc_moves: parseNpcMoves(row.npc_moves ?? row.npcMoves ?? row.角色动作), + variable_changes: parseVariableChanges( + row.variable_changes ?? row.variableChanges ?? row.变量变更, + ), + do_not_say: doNot.length ? doNot : doNotZh.length ? doNotZh : undefined, + tone_hint: asString(row.tone_hint) ?? asString(row.语气), + suggested_actions: suggested.length + ? suggested + : suggestedZh.length + ? suggestedZh + : undefined, + }; +} + +export function parseSettlementPacket(raw: string | undefined | null): SettlementPacketView { + const text = raw?.trim(); + if (!text) { + return { ok: false, parseError: "空裁决包", sections: [] }; + } + + const jsonText = extractJsonObjectText(text) ?? (text.startsWith("{") ? text : null); + if (!jsonText) { + // 兼容旧散文裁决:整段当作 visible_now + return { + ok: true, + packet: { + schema: SETTLEMENT_SCHEMA, + visible_now: text, + }, + sections: [{ title: "场面(未结构化)", lines: [text] }], + }; + } + + try { + const doc = JSON.parse(jsonText) as unknown; + if (!doc || typeof doc !== "object" || Array.isArray(doc)) { + return { ok: false, parseError: "裁决包不是 JSON 对象", sections: [] }; + } + const packet = normalizeSettlementPacket(doc as Record); + return { ok: true, packet, sections: settlementSections(packet) }; + } catch (err) { + return { + ok: false, + parseError: err instanceof Error ? err.message : "裁决包 JSON 解析失败", + sections: [], + }; + } +} + +export function settlementSections(packet: SettlementPacket): Array<{ + title: string; + lines: string[]; +}> { + const sections: Array<{ title: string; lines: string[] }> = []; + if (packet.player_action) { + sections.push({ title: "玩家行动", lines: [packet.player_action] }); + } + if (packet.resolved?.length) { + sections.push({ title: "已落地", lines: packet.resolved }); + } + if (packet.visible_now) { + sections.push({ title: "可见现状", lines: [packet.visible_now] }); + } + if (packet.npc_moves?.length) { + sections.push({ + title: "角色动作", + lines: packet.npc_moves.map((m) => { + const extra = [m.wants && `想要:${m.wants}`, m.refuses_to_say && `回避:${m.refuses_to_say}`] + .filter(Boolean) + .join(";"); + return extra ? `${m.who}:${m.move}(${extra})` : `${m.who}:${m.move}`; + }), + }); + } + if (packet.variable_changes?.length) { + sections.push({ + title: "变量变更", + lines: packet.variable_changes.map((c) => { + if (c.delta != null) { + return `${c.key} ${c.delta >= 0 ? "+" : ""}${c.delta}` + (c.note ? `(${c.note})` : ""); + } + if (c.from !== undefined || c.to !== undefined) { + return `${c.key}:${String(c.from ?? "?")} → ${String(c.to ?? "?")}`; + } + return c.key; + }), + }); + } + if (packet.tone_hint) { + sections.push({ title: "语气", lines: [packet.tone_hint] }); + } + if (packet.do_not_say?.length) { + sections.push({ title: "勿写入正文", lines: packet.do_not_say }); + } + if (packet.suggested_actions?.length) { + sections.push({ title: "建议行动", lines: packet.suggested_actions }); + } + return sections; +} + +export function isSettlementLikeObject(doc: unknown): boolean { + if (!doc || typeof doc !== "object" || Array.isArray(doc)) return false; + const row = doc as Record; + if (row.schema === SETTLEMENT_SCHEMA) return true; + // 启发式:有裁决包典型键 + const keys = ["player_action", "visible_now", "resolved", "npc_moves", "variable_changes"]; + let hits = 0; + for (const k of keys) if (k in row) hits += 1; + return hits >= 2; +} diff --git a/src/skills/worker-declaration.ts b/src/skills/worker-declaration.ts index a05470c..36bbd7e 100644 --- a/src/skills/worker-declaration.ts +++ b/src/skills/worker-declaration.ts @@ -2,6 +2,7 @@ import type { Blackboard } from "../blackboard/blackboard.js"; import type { RuntimeSession } from "../types/runtime.js"; import { deriveDesignStageScope, + deriveOnDemandWorkerScope, deriveRunWorkerScope, parseWorkerSetYaml, runWorkerMeta, @@ -19,8 +20,10 @@ export type InstanceWorkerDeclaration = { parsed: ParsedWorkerSet | null; /** 当前 lifecycle 下总管可 run_worker 的 id 列表 */ activeWorkerIds: string[]; - /** play 阶段声明(deriveRunWorkerScope) */ + /** play 阶段声明(deriveRunWorkerScope;每轮管线) */ playWorkerIds: string[]; + /** play 按需调度(chance 等;不进自动回合序) */ + onDemandWorkerIds: string[]; /** 创作末尾声明(如 opening-generator) */ designEndWorkerIds: string[]; }; @@ -81,6 +84,8 @@ export function buildInstanceWorkerDeclaration( const raw = readWorkerSetYamlForDeclaration(blackboard, session); const parsed = raw ? parseWorkerSetYaml(raw.yaml) : null; const playWorkerIds = accepted && parsed ? deriveRunWorkerScope(parsed) : []; + const onDemandWorkerIds = + accepted && parsed ? deriveOnDemandWorkerScope(parsed) : []; const designEndWorkerIds = accepted && parsed ? deriveDesignStageScope(parsed) : []; @@ -89,7 +94,13 @@ export function buildInstanceWorkerDeclaration( let activeWorkerIds: string[]; if (lifecycle === "play") { - activeWorkerIds = [...playWorkerIds]; + activeWorkerIds = [...playWorkerIds, ...onDemandWorkerIds]; + const seen = new Set(); + activeWorkerIds = activeWorkerIds.filter((id) => { + if (seen.has(id)) return false; + seen.add(id); + return true; + }); } else if (!accepted) { activeWorkerIds = [...designStepIds]; } else { @@ -109,6 +120,7 @@ export function buildInstanceWorkerDeclaration( parsed, activeWorkerIds, playWorkerIds, + onDemandWorkerIds, designEndWorkerIds, }; } diff --git a/src/skills/worker-set-parse.ts b/src/skills/worker-set-parse.ts index 15dc57c..af17f02 100644 --- a/src/skills/worker-set-parse.ts +++ b/src/skills/worker-set-parse.ts @@ -1,4 +1,12 @@ import { parse as parseYaml } from "yaml"; +import { + inferPlaySlotsFromWorkers, + mergeWorkersWithPlaySlots, + onDemandRefsFromPlaySlots, + parsePlaySlots, + refsFromPlaySlots, + type PlaySlotsConfig, +} from "./play-slots.js"; export type WorkerSetPresentation = { tone?: string; @@ -15,6 +23,9 @@ export type WorkerSetContext = { export type WorkerAcceptance = "review" | "continue"; +/** turn = 每轮管线;on_demand = 仅显式 run_worker / toolcall */ +export type WorkerInvocation = "turn" | "on_demand"; + export type WorkerSetEntry = { ref: string | null; /** @@ -31,6 +42,8 @@ export type WorkerSetEntry = { * review = 用户验收;continue = 可连跑下一 worker。 */ acceptance?: WorkerAcceptance; + /** 缺省 turn;chance 等程序工具为 on_demand */ + invocation?: WorkerInvocation; merge_considered?: string; gap?: string | null; presentation?: WorkerSetPresentation | null; @@ -60,6 +73,13 @@ export type ParsedWorkerSet = { play_morphology?: string; input_protocol?: Record; workers: WorkerSetEntry[]; + /** + * 固定游玩槽位勾选(世界模拟路径首选)。 + * 若存在,解析时会与 workers 合并:槽位定骨架,条目可覆盖 context/outputs。 + */ + play_slots?: PlaySlotsConfig; + /** 投影排序表(context-order.v1);拼装优先于此 */ + context_order?: unknown; tag_flow?: string[]; resident_context?: unknown[]; tables?: Record; @@ -86,7 +106,7 @@ export const DESIGN_STAGE_SKILL_META: Record< "opening-generator": { label: "开局 · 开场白", purpose: - "创作末尾:结合已定世界/故事写开场白(主);表初值与开场对齐,能推则推。", + "创作末尾:优先落库设计.开场白与开场变量;否则现写开场;初值与开场同真相。", }, }; @@ -108,8 +128,8 @@ export const INSTANTIATE_SKILL_META: Record< { label: string; purpose: string } > = { "world-blueprint": { - label: "世界蓝图", - purpose: "背景板与核心设定,供 world-simulator 等读取。", + label: "舞台骨架", + purpose: "可引用舞台(尺度、基底变造、关键舞台区),供主世界层等读取。", }, topology: { label: "拓扑 / 关系", @@ -120,8 +140,8 @@ export const INSTANTIATE_SKILL_META: Record< purpose: "元规则:如何生成 NPC、物品等实例内容。", }, "narrative-guide": { - label: "叙事 / 描写指南", - purpose: "正文 POV、时态、文风(narrator 等 static 上下文)。", + label: "叙事指南与故事推进", + purpose: "遣词、笔墨焦点、禁忌与推进口径(narrator / gm 上下文)。", }, "variable-catalog": { label: "变量目录", @@ -163,6 +183,10 @@ export const RUN_WORKER_META: Record label: "回合陈述", purpose: "结构化陈述本轮事件与各方行动/思考摘要。", }, + chance: { + label: "机遇裁定", + purpose: "程序掷骰/比点/抽签;按需调用,不进每轮管线。", + }, }; function asString(value: unknown): string | undefined { @@ -214,6 +238,11 @@ function parseWorkers(raw: unknown): WorkerSetEntry[] { acceptanceRaw === "review" || acceptanceRaw === "continue" ? acceptanceRaw : undefined; + const invocationRaw = asString(row.invocation); + const invocation: WorkerInvocation | undefined = + invocationRaw === "turn" || invocationRaw === "on_demand" + ? invocationRaw + : undefined; return { ref, name: asString(row.name), @@ -222,6 +251,7 @@ function parseWorkers(raw: unknown): WorkerSetEntry[] { when: asString(row.when), rationale: asString(row.rationale), acceptance, + invocation, merge_considered: asString(row.merge_considered), gap: ref == null ? asString(row.gap) ?? null : asString(row.gap) ?? null, presentation: parsePresentation(row.presentation), @@ -256,6 +286,13 @@ function parseWorkerSetObject(row: Record): ParsedWorkerSet { ? (interactionRaw as ParsedWorkerSetInteraction) : undefined; + const workersRaw = parseWorkers(row.workers); + let play_slots = parsePlaySlots(row.play_slots ?? row.playSlots); + if (!play_slots) { + play_slots = inferPlaySlotsFromWorkers(workersRaw); + } + const workers = mergeWorkersWithPlaySlots(workersRaw, play_slots); + return { version: typeof row.version === "number" ? row.version : undefined, form_summary: asString(row.form_summary), @@ -280,7 +317,18 @@ function parseWorkerSetObject(row: Record): ParsedWorkerSet { .filter(([, v]) => v), ) : undefined, - workers: parseWorkers(row.workers), + play_slots, + workers, + context_order: + row.context_order && + typeof row.context_order === "object" && + !Array.isArray(row.context_order) + ? row.context_order + : row.contextOrder && + typeof row.contextOrder === "object" && + !Array.isArray(row.contextOrder) + ? row.contextOrder + : undefined, tag_flow: asStringList(row.tag_flow), resident_context: Array.isArray(row.resident_context) ? row.resident_context @@ -401,6 +449,9 @@ export function looksLikeProseNotSpec(text: string): boolean { export function isUsableWorkerSet(parsed: ParsedWorkerSet | null | undefined): boolean { if (!parsed || parsed.parseError) return false; if ((parsed.workers?.length ?? 0) > 0) return true; + if (parsed.play_slots && (parsed.play_slots.gm || parsed.play_slots.narrator)) { + return true; + } if (parsed.interaction && Object.keys(parsed.interaction).length > 0) return true; if (parsed.form_summary?.trim() || parsed.interaction_paradigm?.trim()) return true; if (parsed.narrative_guide?.trim() || (parsed.core_premises?.length ?? 0) > 0) { @@ -458,15 +509,63 @@ export function deriveInstantiateScope(workerSet: ParsedWorkerSet | null): strin return deriveDesignStageScope(workerSet); } -/** play 阶段启用的 run worker ref 列表(保序、去重;不含 opening-generator 等 design-end skill) */ +function isOnDemandWorker(worker: WorkerSetEntry): boolean { + if (worker.invocation === "on_demand") return true; + if (worker.invocation === "turn") return false; + // 未标注时:chance 默认按需 + return worker.ref?.trim() === "chance"; +} + +/** play 阶段每轮管线 refs(保序、去重;不含按需槽与 design-end) */ export function deriveRunWorkerScope(workerSet: ParsedWorkerSet | null): string[] { if (!workerSet) return []; + // 固定槽位:按 perspective → gm → narrator 顺序 + if (workerSet.play_slots) { + const fromSlots = refsFromPlaySlots(workerSet.play_slots); + if (fromSlots.length) { + const seen = new Set(fromSlots); + const ordered = [...fromSlots]; + for (const worker of workerSet.workers) { + const ref = worker.ref?.trim(); + if (!ref || seen.has(ref) || DESIGN_STAGE_SKILL_META[ref]) continue; + if (isOnDemandWorker(worker)) continue; + seen.add(ref); + ordered.push(ref); + } + return ordered; + } + } const seen = new Set(); const ordered: string[] = []; for (const worker of workerSet.workers) { const ref = worker.ref?.trim(); if (!ref || seen.has(ref)) continue; if (DESIGN_STAGE_SKILL_META[ref]) continue; + if (isOnDemandWorker(worker)) continue; + seen.add(ref); + ordered.push(ref); + } + return ordered; +} + +/** play 阶段可显式调度的按需 refs(dice/抽签等) */ +export function deriveOnDemandWorkerScope( + workerSet: ParsedWorkerSet | null, +): string[] { + if (!workerSet) return []; + const seen = new Set(); + const ordered: string[] = []; + if (workerSet.play_slots) { + for (const ref of onDemandRefsFromPlaySlots(workerSet.play_slots)) { + if (seen.has(ref)) continue; + seen.add(ref); + ordered.push(ref); + } + } + for (const worker of workerSet.workers) { + const ref = worker.ref?.trim(); + if (!ref || seen.has(ref) || DESIGN_STAGE_SKILL_META[ref]) continue; + if (!isOnDemandWorker(worker)) continue; seen.add(ref); ordered.push(ref); } diff --git a/src/skills/worker-set-view.ts b/src/skills/worker-set-view.ts index dda0766..897dd47 100644 --- a/src/skills/worker-set-view.ts +++ b/src/skills/worker-set-view.ts @@ -11,6 +11,17 @@ import { residentTagFor, type ResidentContextEntry, } from "./resident-context.js"; +import { + PLAY_SLOT_META, + PLAY_SLOT_ORDER, + refForSlot, + type PlaySlotId, +} from "./play-slots.js"; +import { + contextOrderToView, + parseContextOrder, + synthesizeContextOrderFromWorkers, +} from "./context-order.js"; export type TagLineView = { tag: string; @@ -93,6 +104,30 @@ export type WorkerSetUserView = { reasoning?: string; playModeLabel?: string; playModeHint?: string; + /** 固定游玩槽位勾选摘要(世界模拟路径) */ + playSlots?: Array<{ id: string; label: string; enabled: boolean; ref: string }>; + /** 投影排序(可编排);无表时可由 workers.context 合成 */ + contextOrder?: { + brief?: string; + editable?: boolean; + /** 是否由声明合成(尚未写入规格) */ + synthesized?: boolean; + slots: Array<{ + ref: string; + label?: string; + lines: string[]; + inserts?: Array<{ + index: number; + order: number; + anchor: string; + ref: string; + projection: string; + note?: string; + label?: string; + line: string; + }>; + }>; + }; inputProtocol?: PresentationLineView[]; workers: WorkerCardView[]; /** @@ -183,16 +218,21 @@ const DEFAULT_WORKER_CONTRACTS: Record< "设计.worker集", "世界.蓝图.确认稿", "世界.拓扑.*", - "变量.目录.确认稿", - "变量.变化规则.确认稿", + "设计.变量设计与更新规则", + ], + dynamic: [ + "变量.当前", + "运行.事件流", + "用户.最新输入", + "上下文.角色态度", + "大纲.当前章", ], - dynamic: ["变量.当前", "运行.事件流", "用户.最新输入"], }, - outputs: ["运行.本轮.裁决", "运行.事件流"], + outputs: ["运行.本轮.裁决", "运行.事件流", "运行.本轮.变量变更"], }, "variable-update": { context: { - static: ["变量.目录.确认稿", "变量.变化规则.确认稿"], + static: ["设计.变量设计与更新规则"], dynamic: ["运行.本轮.裁决", "变量.当前"], }, outputs: ["运行.本轮.变量变更", "变量.当前"], @@ -201,11 +241,10 @@ const DEFAULT_WORKER_CONTRACTS: Record< context: { static: [ "设计.worker集", - "叙事.指南.确认稿", - "语料.场景策略集.确认稿", - "输出.回复格式.规范", + "设计.叙事指南与故事推进", + "设计.叙事指南", ], - dynamic: ["运行.事件流", "变量.当前", "运行.本轮.变量变更"], + dynamic: ["运行.本轮.裁决", "用户.最新输入"], }, outputs: ["输出.用户展示"], }, @@ -230,6 +269,13 @@ const DEFAULT_WORKER_CONTRACTS: Record< }, outputs: ["运行.本轮.角色决策"], }, + chance: { + context: { + static: ["设计.worker集"], + dynamic: ["运行.机会请求"], + }, + outputs: ["运行.本轮.机遇"], + }, }; const INPUT_PROTOCOL_LABELS: Record = { @@ -243,6 +289,10 @@ const WORKER_ROLE_LABELS: Record = { core: "核心", auxiliary: "辅助", transcription: "转述", + gm: "主世界层", + narrator: "叙事转述", + perspective: "角色视角", + chance: "机遇裁定", }; function tagMatchesPattern(tag: string, pattern: string): boolean { @@ -692,6 +742,36 @@ export function formatWorkerSetForUser( reasoning: parsed.reasoning, playModeLabel: morph?.label ?? morphKey, playModeHint: morph?.hint, + playSlots: parsed.play_slots + ? PLAY_SLOT_ORDER.map((id: PlaySlotId) => ({ + id, + label: PLAY_SLOT_META[id].label, + enabled: Boolean(parsed.play_slots![id]), + ref: refForSlot(parsed.play_slots!, id), + })) + : undefined, + contextOrder: (() => { + const order = parseContextOrder(parsed.context_order); + if (order) return contextOrderToView(order); + const workersForSynth = parsed.workers.map((w) => { + const ref = w.ref?.trim() ?? ""; + const def = ref ? DEFAULT_WORKER_CONTRACTS[ref] : undefined; + return { + ref: w.ref, + name: w.name, + context: { + static: w.context?.static ?? def?.context.static, + dynamic: w.context?.dynamic ?? def?.context.dynamic, + }, + }; + }); + const synth = synthesizeContextOrderFromWorkers( + workersForSynth, + parsed.play_slots, + ); + if (!synth) return undefined; + return { ...contextOrderToView(synth), synthesized: true }; + })(), inputProtocol, workers, contextTags, diff --git a/src/types/runtime.ts b/src/types/runtime.ts index 9300e45..051ad37 100644 --- a/src/types/runtime.ts +++ b/src/types/runtime.ts @@ -41,6 +41,8 @@ export type WaitingReason = * 有值时不阻断验收:用户可直接 Accept,也可先作答再 Accept。 */ questions?: QuestionItem[]; + /** 自评/导语,随追问展示在询问卡顶部 */ + assessment?: string; pageSize?: number; } // worker 产物待验收 | { @@ -144,8 +146,15 @@ export type MainAgentDecision = { action: MainAgentAction; reason: string; workerId?: string; - /** 调度 role-decide 等时指定当前决策角色,Runtime 写入 世界.当前角色.id */ - workerContext?: { roleId?: string }; + /** + * 调度附加上下文: + * - roleId:role-decide 等 + * - chance:机遇裁定请求(op=roll|compare|draw|pick) + */ + workerContext?: { + roleId?: string; + chance?: Record; + }; /** ask_user:给用户看的内容完备度评价(写入 waitingReason.message) */ assessment?: string; /** ask_user 结构化追问(有则前端询问卡) */ @@ -228,6 +237,8 @@ export type RuntimeEvent = artifactId: string; /** 有产物时的可选追问,挂到 review_artifact */ questions?: QuestionItem[] | string[]; + /** 自评摘要,挂到询问卡 */ + assessment?: string; }; } | { @@ -295,7 +306,14 @@ export type RuntimeSession = { */ export type PhaseEffect = | { type: "invoke_main_agent" } - | { type: "run_worker"; workerId: string; workerContext?: { roleId?: string } } + | { + type: "run_worker"; + workerId: string; + workerContext?: { + roleId?: string; + chance?: Record; + }; + } | { type: "resume_worker" } | { type: "run_programmatic_review"; artifactId: string } | { type: "emit_message"; message: string }; diff --git a/src/worker/executor.ts b/src/worker/executor.ts index 1b0e274..3adaf42 100644 --- a/src/worker/executor.ts +++ b/src/worker/executor.ts @@ -18,6 +18,7 @@ import { normalizeQuestions, type QuestionItem, } from "../skills/question-protocol.js"; +import { extractFragmentAskSidecar } from "../skills/context-fragment.js"; export type WorkerRunParams = { skillName: string; @@ -40,6 +41,8 @@ export type WorkerRunResult = { summary: string; preview: string; askUser?: QuestionItem[]; + /** 来自 context-fragment 自评/导语,挂到验收询问卡评估区 */ + askAssessment?: string; }; const WORKER_SET_OUTPUT_TAGS = new Set(["设计.worker集", "设计.worker集.草稿"]); @@ -210,6 +213,28 @@ function parseWorkerResponse( } } + // context-fragment.v1:正文内 追问/自评 → 挂到询问卡(不必再抄一份 askUser) + let askAssessment: string | undefined; + const fragQuestions: QuestionItem[] = []; + for (const content of Object.values(outputs)) { + const side = extractFragmentAskSidecar(content); + if (side.assessment && !askAssessment) askAssessment = side.assessment; + for (const q of side.questions) { + if (!fragQuestions.some((x) => x.prompt === q.prompt)) { + fragQuestions.push(q); + } + } + } + if (fragQuestions.length) { + if (!askUser?.length) { + askUser = fragQuestions; + } else { + for (const q of fragQuestions) { + if (!askUser.some((x) => x.prompt === q.prompt)) askUser.push(q); + } + } + } + const summary = typeof obj.summary === "string" && obj.summary.trim() ? obj.summary.trim() @@ -226,6 +251,7 @@ function parseWorkerResponse( summary, preview, askUser: askUser?.length ? askUser : undefined, + askAssessment, }); } @@ -233,6 +259,7 @@ function parseWorkerResponse( export function sanitizeWorkerSetOutputs(result: WorkerRunResult): WorkerRunResult { const outputs = { ...result.outputs }; const askUser = [...(result.askUser ?? [])]; + const askAssessment = result.askAssessment; let droppedProse = false; for (const tag of [...Object.keys(outputs)]) { @@ -292,6 +319,7 @@ export function sanitizeWorkerSetOutputs(result: WorkerRunResult): WorkerRunResu summary, preview, askUser: askUser.length ? askUser : undefined, + askAssessment: askAssessment?.trim() || undefined, }; } diff --git a/tests/context-fragment.test.ts b/tests/context-fragment.test.ts new file mode 100644 index 0000000..9c9b236 --- /dev/null +++ b/tests/context-fragment.test.ts @@ -0,0 +1,216 @@ +import { describe, expect, it } from "vitest"; +import { + parseContextFragment, + projectFragmentContent, + contextFragmentToView, + extractFragmentAskSidecar, +} from "../src/skills/context-fragment.js"; +import { + parseContextOrder, + contextOrderToSegments, + slotOrderForRef, + applyContextOrderEdit, + mergeContextOrderIntoWorkerSetJson, + serializeContextOrder, +} from "../src/skills/context-order.js"; +import { assembleWorkerContext } from "../src/skills/context-segments.js"; +import { Blackboard } from "../src/blackboard/blackboard.js"; +import { buildDeclaredWorkerSkill } from "../src/skills/declared-worker.js"; +import { parseWorkerSetYaml } from "../src/skills/worker-set-parse.js"; + +describe("context-fragment.v1", () => { + it("parses public header and projects summary", () => { + const frag = parseContextFragment({ + schema: "context-fragment.v1", + 技能: "美学纲领与交互范式", + brief: "特权求生紧张", + mount: ["world-simulator", "narrator"], + 稳变: "stable", + 正文: { 体验: { 内核: "紧张" } }, + 开放问题: [], + })!; + expect(frag.技能).toBe("美学纲领与交互范式"); + expect(projectFragmentContent(JSON.stringify(frag), "summary")).toBe( + "特权求生紧张", + ); + const view = contextFragmentToView(frag); + expect(view.ok).toBe(true); + expect(view.sections.some((s) => s.title === "概要")).toBe(true); + }); + + it("extracts 自评 + 追问 into ask sidecar", () => { + const side = extractFragmentAskSidecar({ + schema: "context-fragment.v1", + brief: "孤立免疫", + 正文: { 美学纲领: { 体验内核: "…" } }, + 自评: { + 维度: [ + { 名: "交互范式", 分数: 70, 说明: "权限尚可" }, + { 名: "美学纲领", 分数: 55, 说明: "边界未锁" }, + ], + 薄弱点: "暴力尺度未定", + }, + 追问: { + 导语: "为贴近质感,还需确认:", + 题目: [ + { + 问: "暴力与血腥如何服务压迫感?", + 建议选项: ["偶发锋利高光", "常态压抑少见血", "其它"], + 示例: "远处尖叫,近处只留锈迹", + }, + ], + }, + }); + expect(side.assessment).toContain("交互范式 70%"); + expect(side.assessment).toContain("薄弱点:暴力尺度未定"); + expect(side.assessment).toContain("为贴近质感"); + expect(side.questions).toHaveLength(1); + expect(side.questions[0]!.prompt).toContain("暴力与血腥"); + expect(side.questions[0]!.prompt).toContain("示例:远处尖叫"); + expect(side.questions[0]!.options?.map((o) => o.label)).toEqual([ + "偶发锋利高光", + "常态压抑少见血", + "其它", + ]); + }); +}); + +describe("context-order.v1", () => { + it("maps flat order to segments (history after stable)", () => { + const doc = parseContextOrder({ + schema: "context-order.v1", + slots: [ + { + ref: "world-simulator", + inserts: [ + { order: 0, ref: "worker.persona", projection: "fixed" }, + { order: 1, ref: "设计.实现机制", projection: "summary" }, + { order: 2, ref: "对话.历史", projection: "summary" }, + { order: 3, ref: "用户.最新输入", projection: "full" }, + ], + }, + ], + })!; + const slot = slotOrderForRef(doc, "world-simulator")!; + const segs = contextOrderToSegments({ + slot, + personaText: "中立主世界层", + }); + expect(segs[0].inline).toContain("中立主世界层"); + expect(segs[1].tags).toEqual(["设计.实现机制"]); + expect(segs[2].tags).toEqual(["对话.历史"]); + expect(segs[3].tier).toBe("dynamic"); + }); + + it("reorders flat list and merges into worker set JSON", () => { + const doc = parseContextOrder({ + schema: "context-order.v1", + slots: [ + { + ref: "narrator", + inserts: [ + { order: 0, ref: "worker.persona", projection: "fixed" }, + { order: 1, ref: "设计.叙事指南", projection: "summary" }, + { order: 2, ref: "设计.美学纲领与交互范式", projection: "summary" }, + { order: 3, ref: "对话.历史", projection: "summary" }, + { order: 4, ref: "运行.本轮.裁决", projection: "full" }, + ], + }, + ], + })!; + const moved = applyContextOrderEdit(doc, { + action: "move", + slotRef: "narrator", + index: 2, + delta: -1, + }); + expect(moved.slots[0]!.inserts.map((i) => i.ref)).toEqual([ + "worker.persona", + "设计.美学纲领与交互范式", + "设计.叙事指南", + "对话.历史", + "运行.本轮.裁决", + ]); + + const afterHist = applyContextOrderEdit(moved, { + action: "set_anchor", + slotRef: "narrator", + index: 1, + anchor: "post_history", + }); + const refs = afterHist.slots[0]!.inserts.map((i) => i.ref); + const histAt = refs.indexOf("对话.历史"); + expect(refs.indexOf("设计.美学纲领与交互范式")).toBeGreaterThan(histAt); + + const merged = mergeContextOrderIntoWorkerSetJson( + JSON.stringify({ + play_slots: { gm: true, narrator: true, perspective: false }, + workers: [], + }), + afterHist, + ); + const again = JSON.parse(merged); + expect(again.context_order.schema).toBe("context-order.v1"); + expect(serializeContextOrder(afterHist)).toContain("对话.历史"); + }); + + it("declared worker prefers context_order segments", () => { + const parsed = parseWorkerSetYaml( + JSON.stringify({ + play_slots: { gm: true, narrator: true, perspective: false }, + context_order: { + schema: "context-order.v1", + slots: [ + { + ref: "world-simulator", + inserts: [ + { order: 0, ref: "worker.persona", projection: "fixed" }, + { order: 1, ref: "对话.历史", projection: "summary" }, + { order: 2, ref: "用户.最新输入", projection: "full" }, + ], + }, + ], + }, + workers: [], + }), + ); + expect(parsed.parseError).toBeUndefined(); + const entry = parsed.workers.find((w) => w.ref === "world-simulator")!; + const { worker } = buildDeclaredWorkerSkill({ + skillPackName: "world-simulator", + entry, + template: { + id: "world-simulator", + label: "主世界层", + duty: "裁决", + suggested_context: { static: ["设计.worker集"], dynamic: ["用户.最新输入"] }, + }, + workerSet: parsed, + }); + expect(worker.contextSegments?.some((s) => s.inline?.includes("裁决"))).toBe( + true, + ); + expect( + worker.contextSegments?.some( + (s) => s.tier === "dynamic" && s.tags.includes("用户.最新输入"), + ), + ).toBe(true); + + const bb = new Blackboard(); + bb.write({ + tag: "用户.最新输入", + content: "推门进去", + source: "test", + }); + const text = assembleWorkerContext({ + inputs: { "用户.最新输入": "推门进去" }, + segments: worker.contextSegments, + blackboard: bb, + workerId: "world-simulator", + workerName: "主世界层", + outputTags: ["运行.本轮.裁决"], + }); + expect(text).toContain("裁决"); + expect(text).toContain("推门进去"); + }); +}); diff --git a/tests/dialogue-history.test.ts b/tests/dialogue-history.test.ts new file mode 100644 index 0000000..7e1fbc1 --- /dev/null +++ b/tests/dialogue-history.test.ts @@ -0,0 +1,140 @@ +import { describe, expect, it } from "vitest"; +import { + appendDialogueHistoryTurn, + projectDialogueHistory, + buildHistoryFallback, + isDialogueHistoryRef, +} from "../src/skills/dialogue-history.js"; +import { assembleWorkerContext } from "../src/skills/context-segments.js"; +import { Blackboard } from "../src/blackboard/blackboard.js"; +import { + contextOrderToSegments, + parseContextOrder, + applyContextOrderEdit, + synthesizeContextOrderFromWorkers, +} from "../src/skills/context-order.js"; + +describe("dialogue history projection", () => { + it("recognizes history refs and projects by level", () => { + expect(isDialogueHistoryRef("对话.历史")).toBe(true); + expect(isDialogueHistoryRef("history")).toBe(true); + const long = Array.from({ length: 30 }, (_, i) => `## 用户\n\n第${i}句`).join( + "\n\n", + ); + const summary = projectDialogueHistory(long, "summary"); + expect(summary).toContain("仅最近"); + expect(projectDialogueHistory(long, "fixed")).toContain("不注入"); + }); + + it("assembles history tag in flat segment order", () => { + const bb = new Blackboard(); + bb.write({ + tag: "对话.历史", + content: appendDialogueHistoryTurn("", { role: "用户", text: "推门" }), + source: "test", + }); + bb.write({ tag: "用户.最新输入", content: "再看一眼", source: "test" }); + const text = assembleWorkerContext({ + inputs: {}, + segments: [ + { + id: "p", + tier: "static", + tags: [], + label: "## persona", + inline: "GM", + projection: "fixed", + }, + { + id: "h", + tier: "static", + tags: ["对话.历史"], + label: "## 对话.历史", + projection: "full", + }, + { + id: "u", + tier: "dynamic", + tags: ["用户.最新输入"], + label: "## 用户.最新输入", + projection: "full", + }, + ], + blackboard: bb, + workerId: "world-simulator", + workerName: "主世界层", + outputTags: [], + }); + const iPersona = text.indexOf("GM"); + const iHist = text.indexOf("推门"); + const iUser = text.indexOf("再看一眼"); + expect(iPersona).toBeGreaterThanOrEqual(0); + expect(iHist).toBeGreaterThan(iPersona); + expect(iUser).toBeGreaterThan(iHist); + }); + + it("falls back to event stream when history empty", () => { + expect( + buildHistoryFallback({ + eventStream: "门开了", + latestUser: "走进去", + }), + ).toContain("门开了"); + }); +}); + +describe("flat context order", () => { + it("synthesizes history tag and moves across it", () => { + const doc = synthesizeContextOrderFromWorkers( + [ + { + ref: "narrator", + name: "叙事转述", + context: { + static: ["设计.叙事指南"], + dynamic: ["运行.本轮.裁决", "用户.最新输入"], + }, + }, + ], + { gm: true, narrator: true, perspective: false }, + )!; + expect(doc.slots[0]!.inserts.some((i) => i.ref === "对话.历史")).toBe(true); + + const histIdx = doc.slots[0]!.inserts.findIndex((i) => i.ref === "对话.历史"); + const moved = applyContextOrderEdit(doc, { + action: "set_anchor", + slotRef: "narrator", + index: doc.slots[0]!.inserts.length - 1, + anchor: "pre_history", + }); + const newHist = moved.slots[0]!.inserts.findIndex((i) => i.ref === "对话.历史"); + const lastUser = moved.slots[0]!.inserts.findIndex( + (i) => i.ref === "用户.最新输入", + ); + expect(lastUser).toBeLessThan(newHist); + expect(histIdx).toBeGreaterThanOrEqual(0); + }); + + it("maps to segments in list order including history", () => { + const doc = parseContextOrder({ + schema: "context-order.v1", + slots: [ + { + ref: "world-simulator", + inserts: [ + { order: 0, ref: "worker.persona", projection: "fixed" }, + { order: 1, ref: "对话.历史", projection: "summary" }, + { order: 2, ref: "用户.最新输入", projection: "full" }, + ], + }, + ], + })!; + const segs = contextOrderToSegments({ + slot: doc.slots[0]!, + personaText: "裁决", + }); + expect(segs[0]!.inline).toContain("裁决"); + expect(segs[1]!.tags).toEqual(["对话.历史"]); + expect(segs[2]!.tags).toEqual(["用户.最新输入"]); + }); +}); diff --git a/tests/play-slots.test.ts b/tests/play-slots.test.ts new file mode 100644 index 0000000..702efe5 --- /dev/null +++ b/tests/play-slots.test.ts @@ -0,0 +1,169 @@ +import { describe, expect, it } from "vitest"; +import { + expandPlaySlotsToWorkers, + mergeWorkersWithPlaySlots, + onDemandRefsFromPlaySlots, + parsePlaySlots, + refsFromPlaySlots, +} from "../src/skills/play-slots.js"; +import { + deriveOnDemandWorkerScope, + deriveRunWorkerScope, + parseWorkerSetYaml, +} from "../src/skills/worker-set-parse.js"; +import { parseSettlementPacket } from "../src/skills/settlement-packet.js"; +import { + executeChance, + parseChanceRequest, +} from "../src/skills/chance-tools.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"]); + const workers = expandPlaySlotsToWorkers(slots); + expect(workers.map((w) => w.ref)).toEqual(["world-simulator", "narrator"]); + expect(workers[0].acceptance).toBe("continue"); + expect(workers[1].acceptance).toBe("review"); + }); + + it("orders perspective before gm", () => { + const slots = parsePlaySlots({ + gm: true, + narrator: true, + perspective: true, + })!; + expect(refsFromPlaySlots(slots)).toEqual([ + "role-decide", + "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 }, + workers: [], + }), + ); + expect(parsed?.play_slots?.gm).toBe(true); + expect(parsed?.workers).toHaveLength(2); + expect(deriveRunWorkerScope(parsed)).toEqual([ + "world-simulator", + "narrator", + ]); + }); + + it("preserves custom context on merge", () => { + const slots = parsePlaySlots({ gm: true, narrator: true })!; + const merged = mergeWorkersWithPlaySlots( + [ + { + ref: "world-simulator", + name: "自定义世界", + context: { dynamic: ["变量.当前"] }, + outputs: ["运行.本轮.裁决"], + }, + ], + slots, + ); + expect(merged[0].name).toBe("自定义世界"); + expect(merged[0].context?.dynamic).toEqual(["变量.当前"]); + expect(merged[1].ref).toBe("narrator"); + }); + + it("chance is on-demand and not in turn pipeline", () => { + const slots = parsePlaySlots({ + gm: true, + narrator: true, + perspective: false, + chance: true, + })!; + expect(refsFromPlaySlots(slots)).toEqual(["world-simulator", "narrator"]); + expect(onDemandRefsFromPlaySlots(slots)).toEqual(["chance"]); + const workers = expandPlaySlotsToWorkers(slots); + expect(workers.map((w) => w.ref)).toEqual([ + "world-simulator", + "narrator", + "chance", + ]); + expect(workers.find((w) => w.ref === "chance")?.invocation).toBe( + "on_demand", + ); + + const parsed = parseWorkerSetYaml( + JSON.stringify({ + play_slots: { + gm: true, + narrator: true, + perspective: false, + chance: true, + }, + workers: [], + }), + ); + expect(deriveRunWorkerScope(parsed)).toEqual([ + "world-simulator", + "narrator", + ]); + expect(deriveOnDemandWorkerScope(parsed)).toEqual(["chance"]); + }); +}); + +describe("chance tools", () => { + it("parses and rolls dice expression", () => { + const req = parseChanceRequest({ op: "roll", expression: "2d6+1" }); + expect(req?.op).toBe("roll"); + const result = executeChance(req!); + expect(result.ok).toBe(true); + expect(result.detail.total).toBeTypeOf("number"); + }); + + it("compares scores", () => { + const result = executeChance({ + op: "compare", + left: 15, + right: 12, + mode: "gte", + }); + expect(result.ok).toBe(true); + expect(result.detail.win).toBe(true); + }); + + it("draws from pool", () => { + const result = executeChance({ + op: "draw", + pool: ["甲", "乙", "丙"], + count: 2, + unique: true, + }); + expect(result.ok).toBe(true); + expect((result.detail.drawn as string[]).length).toBe(2); + }); +}); + +describe("settlement packet", () => { + it("parses settlement.v1 into sections", () => { + const view = parseSettlementPacket( + JSON.stringify({ + schema: "settlement.v1", + player_action: "送画册", + resolved: ["林晚收下"], + visible_now: "短谢,未邀留下", + variable_changes: [{ key: "好感", from: 10, to: 18, delta: 8 }], + do_not_say: ["好感数字"], + }), + ); + expect(view.ok).toBe(true); + expect(view.sections.map((s) => s.title)).toContain("玩家行动"); + expect(view.sections.map((s) => s.title)).toContain("变量变更"); + }); + + it("treats prose as visible_now fallback", () => { + const view = parseSettlementPacket("林晚点头收下了画册。"); + expect(view.ok).toBe(true); + expect(view.packet?.visible_now).toContain("画册"); + }); +}); diff --git a/tests/worker-set-sanitize.test.ts b/tests/worker-set-sanitize.test.ts index 036628f..a8b6c08 100644 --- a/tests/worker-set-sanitize.test.ts +++ b/tests/worker-set-sanitize.test.ts @@ -85,4 +85,38 @@ describe("sanitizeWorkerSetOutputs", () => { expect(result.outputs["设计.worker集.草稿"]).toContain("narrator"); expect(result.askUser).toBeUndefined(); }); + + it("lifts fragment 追问/自评 into askUser + askAssessment", () => { + const result = parseWorkerResponseForTest( + JSON.stringify({ + outputs: { + "设计.美学纲领与交互范式": { + schema: "context-fragment.v1", + brief: "孤立免疫", + 正文: { 美学纲领: { 体验内核: "特权与惊惶" } }, + 自评: { + 维度: [{ 名: "美学纲领", 分数: 60, 说明: "边界未锁" }], + 薄弱点: "尺度", + }, + 追问: { + 导语: "还需确认:", + 题目: [ + { + 问: "血腥如何用?", + 建议选项: ["少而锋利", "常态压抑"], + 示例: "锈迹即可", + }, + ], + }, + }, + }, + summary: "美学纲领 · 孤立免疫", + }), + ["设计.美学纲领与交互范式"], + ); + expect(result.askUser?.length).toBe(1); + expect(result.askUser?.[0]?.options?.length).toBe(2); + expect(result.askAssessment).toContain("美学纲领 60%"); + expect(result.askAssessment).toContain("还需确认"); + }); }); diff --git a/tests/worker-set-view.test.ts b/tests/worker-set-view.test.ts index 4cf4c34..ff7e78d 100644 --- a/tests/worker-set-view.test.ts +++ b/tests/worker-set-view.test.ts @@ -47,7 +47,9 @@ describe("formatWorkerSetForUser", () => { expect(view.headline).toContain("西幻"); expect(view.playModeLabel).toBe("行动–反应循环"); expect(view.workers).toHaveLength(2); - expect(view.workers[0].displayName).toBe("世界模拟"); + expect(view.workers[0].displayName).toBe("主世界层"); + expect(view.playSlots?.some((s) => s.id === "gm" && s.enabled)).toBe(true); + expect(view.contextOrder?.slots?.length).toBeGreaterThan(0); expect(view.creationUnits?.map((u) => u.id)).toEqual( expect.arrayContaining([ "fixed:aesthetics", // sample 含 narrator.presentation @@ -121,9 +123,13 @@ workers: expect(view.interactionParadigm).toContain("旁观"); expect(view.coreWorker).toBe("world-simulator"); expect(view.reasoning).toContain("信息隔绝"); - expect(view.workers[0].roleLabel).toBe("核心"); - expect(view.workers[0].rationale).toContain("裁决"); - expect(view.workers[1].mergeConsidered).toContain("NPC"); + // 推断 play_slots 后顺序为 perspective → gm;角色用槽位中文名 + const gm = view.workers.find((w) => w.id === "world-simulator"); + const perspective = view.workers.find((w) => w.id === "role-decide"); + expect(gm?.roleLabel).toBe("主世界层"); + expect(gm?.rationale).toContain("裁决"); + expect(perspective?.roleLabel).toBe("角色视角"); + expect(perspective?.mergeConsidered).toContain("NPC"); }); it("merges default context when explicit context omitted", () => { @@ -134,9 +140,14 @@ workers: when: 世界机之后 `)!; const view = formatWorkerSetForUser(parsed)!; - expect(view.workers[0].context.staticTags.some((t) => t.tag.includes("变量.目录"))).toBe( - true, - ); + expect( + view.workers[0].context.staticTags.some( + (t) => + t.tag.includes("变量.目录") || + t.tag.includes("变量设计") || + t.tag.includes("设计.变量"), + ), + ).toBe(true); expect(view.workers[0].context.explicit).toBe(false); expect(view.workers[0].writes).toContain("变量.当前"); }); diff --git a/web/agent-ui.js b/web/agent-ui.js index 39361ce..af60fd7 100644 --- a/web/agent-ui.js +++ b/web/agent-ui.js @@ -4,7 +4,7 @@ import { displayWorkerLabel, formatWorkerDisplayTitle } from "./display-labels.j const HIDE_KINDS = new Set(["worker_stub"]); -/** 中间对话区不展示的内部调度消息(仅出现在右侧「历史」) */ +/** 中间对话区不展示的内部调度消息(仅出现在左侧「调度」) */ const FEED_HIDDEN_KINDS = new Set([ "agent_tool", "orchestrator_decision", @@ -352,36 +352,83 @@ export function renderSkillPicker(_view, _onPick) { el.innerHTML = ""; } -let activeAgentTab = "timeline"; -let lastAutoTabReason = null; -let userPinnedAgentTab = false; +let activeRailTab = "books"; +/** 用户手动展开侧栏后,在本会话保持展开,直到再点收起 */ +let railUserExpanded = false; -export function setAgentTab(tab, { user = false } = {}) { - activeAgentTab = tab; - if (user) userPinnedAgentTab = true; - const tabs = document.getElementById("agent-tabs"); - tabs?.querySelectorAll(".agent-tab").forEach((btn) => { - const id = btn.getAttribute("data-tab"); - const active = id === tab && !btn.hidden; +export function setRailTab(tab, { expand = false } = {}) { + activeRailTab = tab === "log" ? "log" : "books"; + if (expand) railUserExpanded = true; + document.querySelectorAll(".rail-tab").forEach((btn) => { + const id = btn.getAttribute("data-rail-tab"); + const active = id === activeRailTab; btn.classList.toggle("active", active); btn.setAttribute("aria-selected", active ? "true" : "false"); }); - for (const id of ["timeline", "review", "progress", "board"]) { - const panel = document.getElementById(`agent-panel-${id}`); - panel?.classList.toggle("active", tab === id); - panel?.toggleAttribute("hidden", tab !== id); + document.querySelectorAll(".rail-tab-panel").forEach((panel) => { + const id = panel.getAttribute("data-rail-panel"); + const active = id === activeRailTab; + panel.classList.toggle("active", active); + panel.toggleAttribute("hidden", !active); + }); + syncRailCollapse(); +} + +function wireRailChrome() { + const tabs = document.getElementById("rail-tabs"); + if (tabs && !tabs.dataset.wired) { + tabs.dataset.wired = "1"; + tabs.addEventListener("click", (e) => { + const btn = e.target.closest(".rail-tab"); + if (!btn) return; + const tab = btn.getAttribute("data-rail-tab") ?? "books"; + const collapsed = document.body.classList.contains("rail-collapsed"); + setRailTab(tab, { expand: collapsed }); + }); + } + const toggle = document.getElementById("btn-rail-toggle"); + if (toggle && !toggle.dataset.wired) { + toggle.dataset.wired = "1"; + toggle.addEventListener("click", () => { + if (document.body.dataset.railCanCollapse !== "1") { + railUserExpanded = true; + } else { + railUserExpanded = !railUserExpanded; + } + syncRailCollapse(); + }); } } -function wireAgentTabs() { - const tabs = document.getElementById("agent-tabs"); - if (!tabs || tabs.dataset.wired) return; - tabs.dataset.wired = "1"; - tabs.addEventListener("click", (e) => { - const btn = e.target.closest(".agent-tab"); - if (!btn || btn.hidden) return; - setAgentTab(btn.getAttribute("data-tab") ?? "timeline", { user: true }); - }); +function hasStartedCreation(view) { + if (!view?.id) return false; + if (view.lifecycleStage === "play") return true; + if (hasUserMessages(view)) return true; + const reason = view.waitingReason?.kind; + if (reason && reason !== "intake") return true; + return Boolean(view.reviewArtifact); +} + +function syncRailCollapse(view) { + if (view) { + document.body.dataset.railCanCollapse = hasStartedCreation(view) ? "1" : "0"; + } + const canCollapse = document.body.dataset.railCanCollapse === "1"; + const collapsed = canCollapse && !railUserExpanded; + document.body.classList.toggle("rail-collapsed", collapsed); + const toggle = document.getElementById("btn-rail-toggle"); + if (toggle) { + toggle.title = collapsed ? "展开侧栏" : "收起侧栏"; + toggle.textContent = collapsed ? "›" : "‹"; + } +} + +export function resetRailChrome() { + railUserExpanded = true; + activeRailTab = "books"; + document.body.dataset.railCanCollapse = "0"; + document.body.classList.remove("rail-collapsed"); + setRailTab("books"); } function hasUserMessages(view) { @@ -421,19 +468,18 @@ function shouldShowInFeed(msg, view) { return true; } -function maybeAutoSwitchTab(view) { - const reason = view.waitingReason?.kind ?? null; - // 创作验收改在主对话展示,不再自动跳到侧栏「验收」Tab - if (reason === "review_artifact") { - if (lastAutoTabReason !== "review_artifact" && activeAgentTab === "review") { - setAgentTab("timeline"); - } - } else if (!userPinnedAgentTab && reason === "worker_questions") { - if (lastAutoTabReason !== "worker_questions" && activeAgentTab === "review") { - setAgentTab("timeline"); - } +function maybeSyncRail(view) { + const started = hasStartedCreation(view); + const wasCollapsible = document.body.dataset.railCanCollapse === "1"; + // 刚进入创作:默认缩进;未开写:展开并停在作品 + if (started && !wasCollapsible) { + railUserExpanded = false; } - lastAutoTabReason = reason; + if (!started) { + railUserExpanded = true; + if (activeRailTab !== "books") setRailTab("books"); + } + syncRailCollapse(view); } export function renderLifecycle(view) { @@ -455,55 +501,9 @@ export function renderLifecycle(view) { }); } -export function renderSkillGuide(view) { - wireAgentTabs(); - const progressTabBtn = document.getElementById("agent-tab-btn-progress"); - const list = document.getElementById("skill-guide-list"); - const workerPanel = document.getElementById("worker-set-user-panel"); - if (!list) return; - - const hasCatalog = - view.lifecycleStage === "design" && (view.skillCatalog?.length ?? 0) > 0; - const hasWorkerView = Boolean( - view.workerSetView?.workers?.length || view.workerSetView?.contextTags?.length, - ); - const hasFlowView = Boolean(view.creationFlowView?.steps?.length); - const show = hasCatalog || hasWorkerView || hasFlowView; - - if (progressTabBtn) { - progressTabBtn.hidden = !show; - } - - if (workerPanel) { - const parts = []; - if (hasFlowView && view.lifecycleStage === "design") { - parts.push(renderCreationFlowView(view.creationFlowView)); - } - if (hasWorkerView && view.lifecycleStage === "design") { - parts.push(renderWorkerSetUserView(view.workerSetView, { compact: true })); - } - workerPanel.innerHTML = parts.join(""); - workerPanel.hidden = !workerPanel.innerHTML; - } - - if (!show) { - list.innerHTML = ""; - if (activeAgentTab === "progress") setAgentTab("timeline"); - return; - } - - list.innerHTML = (view.skillCatalog ?? []) - .map( - (s) => ` -
- -
-
${esc(s.label)} ${esc(s.id)}${s.runCount && s.runCount > 1 ? ` ×${s.runCount}` : ""}
-
${esc(s.purpose)}
-
-
`, - ) - .join(""); +/** @deprecated 设计 Tab 已移除;保留空函数以免外部误调用 */ +export function renderSkillGuide(_view) { + wireRailChrome(); } function renderTagList(tags, emptyLabel) { @@ -535,18 +535,17 @@ function formatFocusBodyHtml(body) { if (typeof body === "string") { const t = body.trim(); return t - ? `
${esc(t)}
` + ? formatArtifactBodyHtml(t) : `

(本块尚无正文,请在下方对话中补充)

`; } try { - const pretty = JSON.stringify(body, null, 2); - return `
${highlightJson(pretty)}
`; + return formatArtifactBodyHtml(JSON.stringify(body)); } catch { return `
${esc(String(body))}
`; } } -function renderFocusUnitBanner(focusUnit) { +function renderFocusUnitBanner(focusUnit, opts = {}) { if (!focusUnit) return ""; const isContext = focusUnit.kind === "fixed" || @@ -558,12 +557,19 @@ function renderFocusUnitBanner(focusUnit) { : focusUnit.kind === "phase" ? "创作阶段" : "填充上下文"; - const hint = isContext - ? "请重点审阅高亮区:这是本次要写入 / 验收的上下文正文。" - : focusUnit.kind === "worker" - ? "请重点审阅当前 Worker 规格;展示名应使用中文。" - : "请重点审阅当前创作单位。"; - const bodyHtml = isContext || focusUnit.kind === "phase" + const showBody = + !opts.omitFocusBody && + (isContext || focusUnit.kind === "phase") && + focusUnit.body != null && + String(focusUnit.body).trim(); + const hint = opts.omitFocusBody + ? "下方卡片为本次待验收产物。" + : isContext + ? "请重点审阅:这是本次要写入 / 验收的上下文。" + : focusUnit.kind === "worker" + ? "请重点审阅当前运行规格。" + : "请重点审阅当前创作单位。"; + const bodyHtml = showBody ? `
${formatFocusBodyHtml(focusUnit.body)}
` : ""; return `
@@ -669,8 +675,10 @@ function renderWorkerSetUserView(userView, opts = {}) { ? focusId.slice("worker:".length) : ""; - // 当前单位横幅:填充上下文时高亮正文;创造 worker 时标明焦点 - const focusBanner = renderFocusUnitBanner(focusUnit); + // 当前单位横幅;验收卡可 omitFocusBody,避免与下方产物卡重复占位 + const focusBanner = renderFocusUnitBanner(focusUnit, { + omitFocusBody: opts.omitFocusBody === true, + }); if (focusBanner) parts.push(focusBanner); if (userView.headline) { @@ -687,10 +695,88 @@ function renderWorkerSetUserView(userView, opts = {}) { if (userView.coreWorker) { parts.push( - `

核心 Worker

${esc(userView.coreWorker)} — 负责推剧情并产出本轮实质内容

`, + `

核心执行单元

${esc(userView.coreWorker)} — 负责推剧情并产出本轮实质内容

`, ); } + if (userView.playSlots?.length) { + const chips = userView.playSlots + .map((s) => { + const on = s.enabled ? "ws-slot-on" : "ws-slot-off"; + const state = s.enabled ? "开" : "关"; + return `${esc(s.label)} · ${state}`; + }) + .join(""); + parts.push( + `

游玩槽位

${chips}
`, + ); + } + + if (userView.contextOrder?.slots?.length) { + const editable = opts.editable === true && Boolean(opts.sessionId); + const orderParts = []; + if (userView.contextOrder.brief) { + orderParts.push(`

${esc(userView.contextOrder.brief)}

`); + } + if (userView.contextOrder.synthesized) { + orderParts.push( + `

尚未写入规格;调整顺序将保存为「上下文投影排序」并合并进 Worker 集(若已有)。

`, + ); + } else if (editable) { + orderParts.push( + `

扁平投影序:↑↓ 调整位置;「对话.历史」是可投影标签(改 projection 裁剪长度);也可移到历史前/后。

`, + ); + } + for (const slot of userView.contextOrder.slots) { + const title = slot.label || slot.ref; + const inserts = Array.isArray(slot.inserts) ? slot.inserts : null; + if (editable && inserts?.length) { + const rows = inserts + .map((ins) => { + const isHist = Boolean(ins.isHistory) || ins.ref === "对话.历史"; + const histBtns = isHist + ? "" + : ` + `; + return `
  • + ${esc(ins.line || "")} + + + + ${histBtns} + + +
  • `; + }) + .join(""); + orderParts.push( + `
    ${esc(title)}
      ${rows}
    `, + ); + } else { + const lis = (slot.lines || []) + .map((l) => `
  • ${esc(l)}
  • `) + .join(""); + if (lis) { + orderParts.push( + `
    ${esc(title)}
      ${lis}
    `, + ); + } + } + } + if (orderParts.length) { + parts.push( + `

    上下文投影排序${editable ? " · 可编排" : ""}

    ${orderParts.join("")}
    `, + ); + } + } + if (!compact && userView.reasoning) { parts.push( `

    分工推理

    ${esc(userView.reasoning)}

    `, @@ -856,43 +942,978 @@ function renderWorkerSetUserView(userView, opts = {}) { return `
    ${parts.join("")}
    `; } -/** 尝试把正文美化为缩进 JSON;失败则原样 pre */ -function formatArtifactBodyHtml(body) { - const trimmed = (body || "").trim(); - if (!trimmed) { - return `

    (无正文)

    `; - } +const ARTIFACT_TITLE_KEYS = [ + "名", + "显示名", + "块id", + "段id", + "rule_id", + "pool_id", + "映射id", + "id", + "key", + "字段名", + "label", + "ref", + "title", +]; - const tryParse = (text) => { +const ARTIFACT_SKIP_KEYS = new Set(["schema", "schema_version"]); + +/** 剥 markdown 代码围栏,便于从验收正文里取出 JSON */ +function stripCodeFences(text) { + let t = String(text || "").trim(); + t = t.replace(/^```(?:json|yaml|yml)?\s*\n?/i, ""); + t = t.replace(/\n?```\s*$/i, ""); + return t.trim(); +} + +/** 验收正文常为 `## tag\\n\\n{json}` 多段拼接 */ +function splitTaggedArtifactSections(text) { + const trimmed = String(text || "").trim(); + if (!trimmed) return []; + const re = /^##\s+(.+?)\s*$/gm; + const hits = []; + let m; + while ((m = re.exec(trimmed))) { + hits.push({ title: m[1].trim(), index: m.index, headerEnd: m.index + m[0].length }); + } + if (!hits.length) return [{ title: "", content: trimmed }]; + return hits.map((h, i) => { + const end = i + 1 < hits.length ? hits[i + 1].index : trimmed.length; + return { title: h.title, content: trimmed.slice(h.headerEnd, end).trim() }; + }); +} + +function tryParseJsonDoc(text) { + const tryParse = (s) => { try { - return JSON.parse(text); + return JSON.parse(s); } catch { return null; } }; - - let parsed = tryParse(trimmed); - if (!parsed) { - const start = trimmed.indexOf("{"); - const end = trimmed.lastIndexOf("}"); - if (start >= 0 && end > start) { - parsed = tryParse(trimmed.slice(start, end + 1)); - } + const cleaned = stripCodeFences(text); + let parsed = tryParse(cleaned); + if (parsed != null) return parsed; + const objStart = cleaned.indexOf("{"); + const objEnd = cleaned.lastIndexOf("}"); + if (objStart >= 0 && objEnd > objStart) { + parsed = tryParse(cleaned.slice(objStart, objEnd + 1)); + if (parsed != null) return parsed; } - if (!parsed) { - const start = trimmed.indexOf("["); - const end = trimmed.lastIndexOf("]"); - if (start >= 0 && end > start) { - parsed = tryParse(trimmed.slice(start, end + 1)); + const arrStart = cleaned.indexOf("["); + const arrEnd = cleaned.lastIndexOf("]"); + if (arrStart >= 0 && arrEnd > arrStart) { + parsed = tryParse(cleaned.slice(arrStart, arrEnd + 1)); + if (parsed != null) return parsed; + } + return null; +} + +function wrapArtifactRawDetails(rawText) { + const pretty = (() => { + const parsed = tryParseJsonDoc(rawText); + if (parsed != null) return JSON.stringify(parsed, null, 2); + return String(rawText || "").trim(); + })(); + const body = pretty.startsWith("{") || pretty.startsWith("[") + ? `${highlightJson(pretty)}` + : esc(pretty); + return `
    原始 JSON
    ${body}
    `; +} + +function clampPreviewText(text, max = 140) { + const t = String(text || "").replace(/\s+/g, " ").trim(); + if (!t) return ""; + return t.length > max ? `${t.slice(0, max)}…` : t; +} + +/** 从产物对象抽出卡片预览:标题 / 摘要 / 徽章 / 分数 pill */ +function summarizeArtifactDoc(doc, tagTitle = "") { + if (Array.isArray(doc)) { + return { + title: tagTitle || "列表产物", + brief: `${doc.length} 条记录`, + chipsHtml: "", + scoresHtml: "", + }; + } + const title = + tagTitle || + (typeof doc.技能 === "string" && doc.技能.trim()) || + (typeof doc.brief === "string" && clampPreviewText(doc.brief, 40)) || + doc.schema || + "产物"; + let brief = ""; + if (typeof doc.brief === "string" && doc.brief.trim()) { + brief = clampPreviewText(doc.brief, 160); + } else if (typeof doc.正文 === "string") { + brief = clampPreviewText(doc.正文, 160); + } else if (doc.正文 && typeof doc.正文 === "object") { + const body = doc.正文; + const kernel = body.美学纲领?.体验内核 || body.体验内核; + if (typeof kernel === "string") brief = clampPreviewText(kernel, 160); + else { + const keys = Object.keys(body).slice(0, 5); + brief = keys.length ? `含:${keys.join("、")}` : ""; } + } else if (Array.isArray(doc.真值)) { + brief = `真值 ${doc.真值.length} 项` + (doc.side_effects ? ` · 副作用 ${doc.side_effects.length}` : ""); + } else if (doc.play_slots) { + brief = "游玩槽位配置"; } + const chips = []; + const stability = doc.稳变 || doc.稳定 || doc.stability; + if (stability) chips.push(`${esc(String(stability))}`); + if (Array.isArray(doc.mount)) { + for (const m of doc.mount.slice(0, 3)) { + chips.push(`${esc(String(m))}`); + } + if (doc.mount.length > 3) { + chips.push(`+${doc.mount.length - 3}`); + } + } + const 自评 = doc.自评; + let scoresHtml = ""; + if (自评 && typeof 自评 === "object" && Array.isArray(自评.维度)) { + const pills = 自评.维度 + .map((d) => { + if (!d || typeof d !== "object") return ""; + const name = d.名 || d.维度 || "?"; + const pct = parseCompleteness(d.分数); + if (pct == null) return ""; + return `${esc(String(name))} ${Math.round(pct)}%`; + }) + .filter(Boolean); + if (pills.length) scoresHtml = pills.join(""); + } + return { + title: String(title), + brief, + chipsHtml: chips.join(""), + scoresHtml, + }; +} + +function renderArtifactCompactCard(doc, tagTitle, rawSlice, opts = {}) { + const meta = summarizeArtifactDoc(doc, tagTitle); + const full = + renderKnownArtifactHtml(doc, { + embed: true, + hideAskSidecar: opts.hideAskSidecar === true, + }) || renderStructuredDocHtml(doc); + const raw = rawSlice ? wrapArtifactRawDetails(rawSlice) : ""; + const metaRow = [meta.chipsHtml, meta.scoresHtml].filter(Boolean).join(""); + const openAttr = opts.open ? " open" : ""; + return `
    + +
    +
    + ${esc(meta.title)} + ${metaRow ? `
    ${metaRow}
    ` : ""} + ${meta.brief ? `

    ${esc(meta.brief)}

    ` : ""} +
    + +
    +
    +
    ${full}${raw}
    +
    `; +} + +function renderPlainCompactCard(title, content, opts = {}) { + const preview = clampPreviewText(content, 160); + const openAttr = opts.open ? " open" : ""; + return `
    + +
    +
    + ${esc(title || "产物")} + ${preview ? `

    ${esc(preview)}

    ` : ""} +
    + +
    +
    +
    ${esc(content)}
    +
    `; +} + +/** 紧凑卡片:预览 + 展开详情;多 tag 各一张。opts.defaultOpen:验收时默认展开吃满阅读区 */ +function formatArtifactBodyHtml(body, opts = {}) { + const trimmed = (body || "").trim(); + if (!trimmed) { + return `

    (无正文)

    `; + } + const cardOpts = { + open: opts.defaultOpen === true, + hideAskSidecar: opts.hideAskSidecar === true, + }; + + const sections = splitTaggedArtifactSections(trimmed); + const hasTagHeaders = sections.length > 1 || (sections.length === 1 && sections[0].title); + + if (hasTagHeaders) { + const cards = sections.map((sec) => { + const parsed = tryParseJsonDoc(sec.content); + if (parsed != null && typeof parsed === "object") { + return renderArtifactCompactCard(parsed, sec.title, sec.content, cardOpts); + } + return renderPlainCompactCard(sec.title || "产物", sec.content, cardOpts); + }); + return `
    ${cards.join("")}
    `; + } + + const parsed = tryParseJsonDoc(trimmed); if (parsed != null && typeof parsed === "object") { - const pretty = JSON.stringify(parsed, null, 2); - return `
    ${highlightJson(pretty)}
    `; + return `
    ${renderArtifactCompactCard(parsed, "", trimmed, cardOpts)}
    `; } - return `
    ${esc(trimmed)}
    `; + return `
    ${renderPlainCompactCard("产物", trimmed, cardOpts)}
    `; +} + +/** 已知产物 → 分节卡片;未知对象走通用结构化渲染 */ +function renderKnownArtifactHtml(doc, opts = {}) { + if (Array.isArray(doc)) { + return `
    ${renderStructuredValueHtml(doc, 0)}
    `; + } + const schema = doc.schema; + if (schema === "settlement.v1" || isSettlementLike(doc)) { + return renderSettlementSectionsHtml(doc); + } + if (schema === "context-fragment.v1" || isContextFragmentLike(doc)) { + return renderContextFragmentHtml(doc, opts); + } + if (schema === "context-order.v1" || Array.isArray(doc.slots)) { + const orderHtml = renderContextOrderHtml(doc); + if (orderHtml) return orderHtml; + } + if (Array.isArray(doc.真值) || Array.isArray(doc.side_effects) || doc.维护语句约定) { + return renderVariableDesignHtml(doc); + } + if (doc.play_slots && typeof doc.play_slots === "object") { + return renderPlaySlotsHtml(doc); + } + return null; +} + +function renderPlaySlotsHtml(doc) { + const slotLabels = { + gm: "主世界层", + "world-simulator": "主世界层", + narrator: "叙事转述", + perspective: "角色视角", + chance: "机遇裁定", + }; + const slots = doc.play_slots; + const keys = Object.keys(slots); + const chips = keys + .map((k) => { + const on = Boolean(slots[k]); + const label = slotLabels[k] || k; + return `${esc(label)}`; + }) + .join(""); + const parts = []; + if (doc.brief) { + parts.push(`

    概要

    ${esc(String(doc.brief))}

    `); + } + parts.push( + `

    游玩槽位

    ${chips}
    `, + ); + const rest = { ...doc }; + delete rest.play_slots; + delete rest.brief; + delete rest.schema; + const extra = Object.keys(rest).filter((k) => rest[k] != null && rest[k] !== ""); + if (extra.length) { + parts.push( + `

    其它

    ${renderStructuredValueHtml(rest, 0)}
    `, + ); + } + return `
    ${parts.join("")}
    `; +} + +function isContextFragmentLike(doc) { + return ( + typeof doc.brief === "string" && + (doc.正文 != null || doc.body != null) && + (typeof doc.技能 === "string" || doc.schema === "context-fragment.v1") + ); +} + +function renderContextFragmentHtml(doc, opts = {}) { + const parts = []; + // 紧凑卡外层已展示 brief/挂载,展开内不再重复头区 + if (!opts.embed) { + const metaChips = []; + if (doc.技能) metaChips.push(`${esc(String(doc.技能))}`); + const stability = doc.稳变 || doc.稳定 || doc.stability; + if (stability) metaChips.push(`${esc(String(stability))}`); + if (Array.isArray(doc.mount) && doc.mount.length) { + for (const m of doc.mount) { + metaChips.push(`${esc(String(m))}`); + } + } + if (doc.brief || metaChips.length) { + parts.push(`
    + ${metaChips.length ? `
    ${metaChips.join("")}
    ` : ""} + ${doc.brief ? `

    ${esc(String(doc.brief))}

    ` : ""} +
    `); + } + } + + const body = doc.正文 != null ? doc.正文 : doc.body; + if (typeof body === "string" && body.trim()) { + parts.push( + `

    正文

    ${renderProseHtml(body)}
    `, + ); + } else if (body && typeof body === "object") { + const specialty = renderSpecialtyBodyHtml(body, doc.技能); + if (specialty) { + parts.push(specialty); + } else { + parts.push( + `

    正文

    ${renderStructuredValueHtml(body, 0)}
    `, + ); + } + } + + // 自评/追问已挂询问卡时:产物内保留评分条,追问不重复展示 + const hideAsk = opts.hideAskSidecar === true; + const scoreHtml = renderSelfScoreHtml(doc.自评); + if (scoreHtml) parts.push(scoreHtml); + + if (!hideAsk) { + const probeHtml = renderProbeHtml(doc.追问); + if (probeHtml) parts.push(probeHtml); + + const openQs = Array.isArray(doc.开放问题) ? doc.开放问题 : []; + if (openQs.length) { + parts.push( + `

    开放问题

      ${openQs + .map((l) => `
    • ${esc(String(l))}
    • `) + .join("")}
    `, + ); + } + } + if (!parts.length) return null; + return `
    ${parts.join("")}
    `; +} + +/** 美学纲领等:顶层板块并排 mosaic,子项用瓷砖网格 */ +function renderSpecialtyBodyHtml(body, skill) { + if (!body || typeof body !== "object" || Array.isArray(body)) return ""; + const looksAesthetics = + body.设定逻辑 != null || + body.交互范式 != null || + body.美学纲领 != null || + (typeof skill === "string" && skill.includes("美学")); + if (!looksAesthetics) return ""; + + const order = ["设定逻辑", "交互范式", "美学纲领"]; + const used = new Set(); + const parts = []; + for (const key of order) { + if (body[key] == null) continue; + used.add(key); + const wide = key === "美学纲领"; + parts.push( + `

    ${esc(key)}

    ${renderAestheticsNodeHtml(body[key], key, 0)}
    `, + ); + } + for (const [k, v] of Object.entries(body)) { + if (used.has(k) || v == null || v === "") continue; + parts.push( + `

    ${esc(k)}

    ${renderStructuredValueHtml(v, 0)}
    `, + ); + } + return parts.length ? `
    ${parts.join("")}
    ` : ""; +} + +function parseCompleteness(v) { + if (v == null) return null; + if (typeof v === "number" && Number.isFinite(v)) { + if (v < 0 || v > 100) return null; + return v; + } + const s = String(v).trim(); + if (!s) return null; + const withPct = s.match(/^(\d+(?:\.\d+)?)\s*%$/); + if (withPct) return Math.max(0, Math.min(100, Number(withPct[1]))); + const bare = s.match(/^(\d+(?:\.\d+)?)$/); + if (bare) { + const n = Number(bare[1]); + if (n >= 0 && n <= 100) return n; + } + return null; +} + +function pctTone(pct) { + if (pct < 40) return "low"; + if (pct < 70) return "mid"; + return "high"; +} + +/** 统一百分比条:对齐 status-dot / badge 色阶 */ +function renderPctMeterHtml(pct, label = "完备度") { + if (pct == null || !Number.isFinite(pct)) return ""; + const n = Math.max(0, Math.min(100, Math.round(pct))); + const tone = pctTone(n); + return `
    +
    + ${esc(label)} + ${n}% +
    +
    +
    `; +} + +/** 瓷砖用迷你百分比(数字 + 细条),省纵向空间 */ +function renderMiniPctHtml(pct) { + if (pct == null || !Number.isFinite(pct)) return ""; + const n = Math.max(0, Math.min(100, Math.round(pct))); + return `${n}`; +} + +function extractNodePct(value) { + if (!value || typeof value !== "object") return null; + return parseCompleteness(value.完备度 ?? value.分数); +} + +/** 短诊断块(结论/完备度/已知待探)适合瓷砖;散文对象必须通栏 */ +function isDiagnosticNode(value) { + return ( + value != null && + typeof value === "object" && + !Array.isArray(value) && + (value.结论 != null || + value.完备度 != null || + value.已知 != null || + value.待探 != null) + ); +} + +function isProseHeavyObject(value) { + if (!value || typeof value !== "object" || Array.isArray(value)) return false; + if (isDiagnosticNode(value)) return false; + const vals = Object.values(value).filter((x) => x != null && x !== ""); + if (!vals.length) return false; + let prose = 0; + for (const x of vals) { + if (typeof x === "string" && (x.length > 36 || x.includes("\n"))) prose += 1; + else if (Array.isArray(x)) prose += 1; + else if (typeof x === "object" && !isDiagnosticNode(x)) prose += 1; + } + return prose >= Math.ceil(vals.length / 2); +} + +function renderAfTile(title, value) { + const pct = extractNodePct(value); + const mini = pct != null ? renderMiniPctHtml(pct) : ""; + let body = ""; + if (value == null) { + body = ``; + } else if (typeof value !== "object") { + body = `

    ${esc(clampPreviewText(String(value), 100))}

    `; + } else if (value.结论 != null || value.完备度 != null) { + const text = value.结论 != null ? String(value.结论) : ""; + const basis = value.依据 ? `${esc(clampPreviewText(String(value.依据), 60))}` : ""; + body = `${text ? `

    ${esc(clampPreviewText(text, 90))}

    ` : ""}${basis}`; + } else if (value.已知 != null || value.待探 != null) { + body = `${value.已知 != null ? `

    ${esc(clampPreviewText(String(value.已知), 80))}

    ` : ""} + ${value.待探 != null && String(value.待探) !== "无" ? `

    ${esc(clampPreviewText(`待探:${value.待探}`, 60))}

    ` : ""}`; + } else { + // 非诊断对象不应进瓷砖;通栏回退 + return `
    ${esc(title)}${mini}
    ${renderAestheticsNodeHtml(value, title, 1)}
    `; + } + return `
    ${esc(title)}${mini}
    ${body}
    `; +} + +function isPctFieldKey(key) { + if (!key) return false; + return /完备度|分数|覆盖度|充分度|克制度|承重度|可维护|必要性|属性妥当|格式准确|契约符合|同真相|可开玩|完整度|进度/.test( + String(key), + ); +} + +function renderScalarOrPctHtml(value, keyHint) { + if (typeof value === "boolean") { + return `${value ? "是" : "否"}`; + } + if (isPctFieldKey(keyHint) || (typeof value === "string" && /^\d+(\.\d+)?\s*%$/.test(value.trim()))) { + const pct = parseCompleteness(value); + if (pct != null) return renderPctMeterHtml(pct, keyHint || "完备度"); + } + if (typeof value === "number" && isPctFieldKey(keyHint)) { + const pct = parseCompleteness(value); + if (pct != null) return renderPctMeterHtml(pct, keyHint); + } + if (typeof value === "string" || typeof value === "number") { + return renderProseHtml(String(value)); + } + return `${esc(String(value))}`; +} + +function renderAestheticsNodeHtml(value, keyHint, depth) { + if (value == null || value === "") return ``; + if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") { + return renderScalarOrPctHtml(value, keyHint); + } + if (Array.isArray(value)) return renderStructuredValueHtml(value, depth); + + // 单点诊断:由上层 af-tile 承载;此处给非网格调用兜底 + if (value.结论 != null || value.完备度 != null) { + return renderAfTile(keyHint || "项", value); + } + if (value.已知 != null || value.待探 != null) { + return renderAfTile(keyHint || "项", value); + } + + // 美学纲领:体验内核通栏;诊断块可瓷砖;呈现要点等散文块通栏堆叠 + if (keyHint === "美学纲领" || value.体验内核 != null) { + const parts = []; + if (value.体验内核 != null) { + parts.push( + `
    体验内核
    ${renderProseHtml(String(value.体验内核))}
    `, + ); + } + const rest = { ...value }; + delete rest.体验内核; + const restEntries = Object.entries(rest).filter(([, v]) => v != null && v !== ""); + if (restEntries.length) { + const tiles = restEntries.filter(([, v]) => isDiagnosticNode(v)); + const prose = restEntries.filter(([, v]) => !isDiagnosticNode(v)); + if (tiles.length) { + parts.push( + `
    ${tiles.map(([k, v]) => renderAfTile(k, v)).join("")}
    `, + ); + } + if (prose.length) { + parts.push( + `
    ${prose + .map(([k, v]) => { + if (v && typeof v === "object" && !Array.isArray(v)) { + return `
    ${esc(k)}
    ${renderAestheticsNodeHtml(v, k, depth + 1)}
    `; + } + return `
    ${esc(k)}
    ${renderScalarOrPctHtml(v, k)}
    `; + }) + .join("")}
    `, + ); + } + } + return parts.join(""); + } + + // 呈现要点等:子键多为长文 → 通栏 KV(标签在上) + if (keyHint === "呈现要点" || isProseHeavyObject(value)) { + return renderObjectKvHtml(value, depth); + } + + if (depth > 4) return renderStructuredValueHtml(value, depth); + + const entries = Object.entries(value).filter(([, v]) => v != null && v !== ""); + if (!entries.length) return `(空)`; + + // 仅同构短诊断块进瓷砖网格 + const allDiagnostic = entries.every(([, v]) => isDiagnosticNode(v)); + if (allDiagnostic && entries.length >= 2) { + return `
    ${entries.map(([k, v]) => renderAfTile(k, v)).join("")}
    `; + } + + // 混合:分组标题 + 内部再网格/kv(通栏) + const nestedGroups = entries.filter( + ([, v]) => + v && + typeof v === "object" && + !Array.isArray(v) && + !isDiagnosticNode(v), + ); + if (nestedGroups.length >= 1 && depth < 3) { + return `
    ${entries + .map(([k, v]) => { + if (isDiagnosticNode(v)) { + return renderAfTile(k, v); + } + if (v && typeof v === "object" && !Array.isArray(v)) { + return `
    ${esc(k)}
    ${renderAestheticsNodeHtml(v, k, depth + 1)}
    `; + } + return `
    ${esc(k)}
    ${renderScalarOrPctHtml(v, k)}
    `; + }) + .join("")}
    `; + } + + const allLeaf = entries.every( + ([, v]) => v == null || typeof v !== "object" || Array.isArray(v), + ); + if (allLeaf) { + return renderObjectKvHtml(value, depth); + } + return renderObjectKvHtml(value, depth); +} + +function renderSelfScoreHtml(自评) { + if (!自评 || typeof 自评 !== "object") return ""; + const dims = Array.isArray(自评.维度) ? 自评.维度 : []; + const bars = dims + .map((d) => { + if (!d || typeof d !== "object") return ""; + const name = d.名 || d.维度 || d.name || "?"; + const pct = parseCompleteness(d.分数); + const note = d.说明 + ? `

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

    ` + : ""; + if (pct == null) { + return `
    ${esc(String(name))}未评分
    ${note}
    `; + } + return `
    ${renderPctMeterHtml(pct, String(name))}${note}
    `; + }) + .filter(Boolean); + let weak = ""; + if (自评.薄弱点) { + weak = `

    薄弱点 ${esc(String(自评.薄弱点))}

    `; + } + if (!bars.length && !weak) return ""; + return `

    自评

    ${bars.join("")}
    ${weak}
    `; +} + +function renderProbeHtml(追问) { + if (!追问 || typeof 追问 !== "object") return ""; + const qs = Array.isArray(追问.题目) ? 追问.题目 : []; + const cards = []; + if (追问.导语) { + cards.push(`

    ${esc(String(追问.导语))}

    `); + } + qs.forEach((q, i) => { + if (!q || typeof q !== "object") return; + const letter = String.fromCharCode(65 + (i % 26)); + const opts = + Array.isArray(q.建议选项) && q.建议选项.length + ? `
      ${q.建议选项 + .map((o, j) => { + const L = String.fromCharCode(65 + (j % 26)); + return `
    • ${esc(String(o))}
    • `; + }) + .join("")}
    ` + : ""; + const ex = q.示例 ? `

    示例:${esc(String(q.示例))}

    ` : ""; + cards.push( + `
    ${letter}${esc(String(q.问 || "?"))}
    ${opts}${ex}
    `, + ); + }); + if (!cards.length) return ""; + return `

    追问

    ${cards.join("")}
    `; +} + +function renderContextOrderHtml(doc) { + if (!Array.isArray(doc.slots) || !doc.slots.length) return null; + const parts = []; + if (doc.brief) { + parts.push( + `

    概要

    ${esc(String(doc.brief))}

    `, + ); + } + for (const slot of doc.slots) { + if (!slot || typeof slot !== "object") continue; + const title = slot.label || slot.ref || "槽"; + const inserts = Array.isArray(slot.inserts) ? slot.inserts : []; + if (!inserts.length && Array.isArray(slot.order)) { + // 扁平序:直接是条目列表 + const rows = slot.order + .map((i, idx) => renderContextInsertRow(i, idx)) + .filter(Boolean); + if (rows.length) { + parts.push( + `

    ${esc(String(title))}

      ${rows.join("")}
    `, + ); + } + continue; + } + const rows = inserts + .slice() + .sort((a, b) => (Number(a?.order) || 0) - (Number(b?.order) || 0)) + .map((i, idx) => renderContextInsertRow(i, idx)) + .filter(Boolean); + if (rows.length) { + parts.push( + `

    ${esc(String(title))}

      ${rows.join("")}
    `, + ); + } + } + // 顶层扁平 slots 即 inserts + if (!parts.length && doc.slots.every((s) => s && (s.ref || s.anchor != null))) { + const rows = doc.slots.map((i, idx) => renderContextInsertRow(i, idx)).filter(Boolean); + if (rows.length) { + parts.push( + `

    投影序

      ${rows.join("")}
    `, + ); + } + } + if (!parts.length) return null; + return `
    ${parts.join("")}
    `; +} + +function renderContextInsertRow(item, idx) { + if (item == null) return ""; + if (typeof item !== "object") { + return `
  • ${idx + 1} ${esc(String(item))}
  • `; + } + const isHistory = item.ref === "对话.历史" || item.ref === "history"; + const anchor = + item.anchor === "post_history" || item.anchor === "历史后" + ? "历史后" + : item.anchor === "pre_history" || item.anchor === "历史前" + ? "历史前" + : item.anchor + ? String(item.anchor) + : ""; + const proj = + item.projection && item.projection !== "full" ? String(item.projection) : ""; + const note = item.note ? String(item.note) : ""; + const ref = item.ref || item.label || "?"; + const meta = [anchor, proj].filter(Boolean).map((t) => `${esc(t)}`).join(""); + return `
  • ${item.order ?? idx + 1}${esc(String(ref))}${meta}${note ? ` — ${esc(note)}` : ""}
  • `; +} + +function renderStructuredDocHtml(doc) { + if (Array.isArray(doc)) { + return `
    ${renderStructuredValueHtml(doc, 0)}
    `; + } + const parts = []; + if (typeof doc.brief === "string" && doc.brief.trim()) { + parts.push( + `

    概要

    ${esc(doc.brief)}

    `, + ); + } + const body = { ...doc }; + delete body.brief; + delete body.schema; + if (body.自评) { + const score = renderSelfScoreHtml(body.自评); + delete body.自评; + const rest = renderObjectSectionsHtml(body, 0); + if (rest) parts.push(rest); + if (score) parts.push(score); + } else { + const rest = renderObjectSectionsHtml(body, 0); + if (rest) parts.push(rest); + } + if (!parts.length) { + parts.push(renderStructuredValueHtml(doc, 0)); + } + return `
    ${parts.join("")}
    `; +} + +function renderObjectSectionsHtml(obj, depth) { + if (!obj || typeof obj !== "object" || Array.isArray(obj)) return ""; + const entries = Object.entries(obj).filter( + ([k, v]) => !ARTIFACT_SKIP_KEYS.has(k) && v != null && v !== "", + ); + if (!entries.length) return ""; + // 浅层:每键一节;深层:kv + if (depth === 0) { + return entries + .map(([k, v]) => { + if (k === "自评") return renderSelfScoreHtml(v); + if (k === "追问") return renderProbeHtml(v); + return `

    ${esc(k)}

    ${renderStructuredValueHtml(v, depth + 1)}
    `; + }) + .join(""); + } + return renderStructuredValueHtml(obj, depth); +} + +function renderStructuredValueHtml(value, depth) { + if (value == null || value === "") return ``; + if (typeof value === "string") return renderProseHtml(value); + if (typeof value === "number" || typeof value === "boolean") { + return `${esc(String(value))}`; + } + if (Array.isArray(value)) { + if (!value.length) return `(空)`; + const allScalar = value.every( + (v) => v == null || ["string", "number", "boolean"].includes(typeof v), + ); + if (allScalar) { + return `
      ${value + .map((v) => `
    • ${esc(String(v))}
    • `) + .join("")}
    `; + } + return `
    ${value + .map((item, i) => renderArrayItemCard(item, i, depth)) + .join("")}
    `; + } + if (typeof value === "object") { + if (depth > 5) { + return `
    嵌套对象
    ${highlightJson(JSON.stringify(value, null, 2))}
    `; + } + return renderObjectKvHtml(value, depth); + } + return `${esc(String(value))}`; +} + +function renderProseHtml(text) { + const t = String(text); + if (!t.trim()) return ``; + if (t.includes("\n") || t.length > 160) { + return `
    ${esc(t).replace(/\n/g, "
    ")}
    `; + } + return `

    ${esc(t)}

    `; +} + +function pickArtifactTitle(obj, fallbackIndex) { + if (!obj || typeof obj !== "object") return `#${fallbackIndex + 1}`; + for (const k of ARTIFACT_TITLE_KEYS) { + if (obj[k] != null && String(obj[k]).trim()) return String(obj[k]); + } + const first = Object.keys(obj)[0]; + if (first && typeof obj[first] !== "object") return `${first}:${obj[first]}`; + return `#${fallbackIndex + 1}`; +} + +function renderArrayItemCard(item, index, depth) { + if (item == null || typeof item !== "object") { + return `
    ${esc(String(item))}
    `; + } + const title = pickArtifactTitle(item, index); + const rest = { ...item }; + for (const k of ARTIFACT_TITLE_KEYS) delete rest[k]; + // 保留标题键若还有其它信息价值:名/类型等同屏展示 + const keepMeta = {}; + for (const k of ["类型", "顺序", "生命周期", "填充方", "内容形态", "用途", "通道"]) { + if (item[k] != null && item[k] !== "") keepMeta[k] = item[k]; + } + const metaBits = Object.entries(keepMeta) + .map(([k, v]) => { + if (typeof v === "object") return ""; + return `${esc(k)}:${esc(String(v))}`; + }) + .filter(Boolean) + .join(""); + for (const k of Object.keys(keepMeta)) delete rest[k]; + const body = Object.keys(rest).length + ? renderObjectKvHtml(rest, depth + 1) + : ""; + return `
    ${esc(title)}
    ${metaBits ? `
    ${metaBits}
    ` : ""}${body}
    `; +} + +function renderObjectKvHtml(obj, depth) { + const entries = Object.entries(obj).filter( + ([k, v]) => !ARTIFACT_SKIP_KEYS.has(k) && v != null && v !== "", + ); + if (!entries.length) return `(空)`; + const rows = entries + .map(([k, v]) => { + const isComplex = v != null && typeof v === "object"; + if (!isComplex) { + const leaf = renderScalarOrPctHtml(v, k); + // 百分比条自带标签,不再并排挤「完备度」键名 + if (isPctFieldKey(k) && parseCompleteness(v) != null) { + return `
    ${leaf}
    `; + } + // 长文:标签在上,吃满横向宽度(避免窄列硬折行) + const longProse = + typeof v === "string" && (v.length > 40 || v.includes("\n")); + if (longProse) { + return `
    ${esc(k)}
    ${leaf}
    `; + } + return `
    ${esc(k)}
    ${leaf}
    `; + } + return `
    ${esc(k)}
    ${renderStructuredValueHtml(v, depth + 1)}
    `; + }) + .join(""); + return `
    ${rows}
    `; +} + +function isSettlementLike(doc) { + const keys = ["player_action", "visible_now", "resolved", "npc_moves", "variable_changes"]; + let hits = 0; + for (const k of keys) if (k in doc) hits += 1; + return hits >= 2; +} + +function renderSettlementSectionsHtml(doc) { + const sections = []; + const push = (title, lines) => { + if (!lines || !lines.length) return; + sections.push( + `

    ${esc(title)}

      ${lines + .map((l) => `
    • ${esc(String(l))}
    • `) + .join("")}
    `, + ); + }; + const pushHtml = (title, html) => { + if (!html) return; + sections.push(`

    ${esc(title)}

    ${html}
    `); + }; + if (doc.player_action) push("玩家行动", [doc.player_action]); + if (Array.isArray(doc.resolved) && doc.resolved.length) push("已落地", doc.resolved); + if (doc.visible_now) push("可见现状", [doc.visible_now]); + if (Array.isArray(doc.npc_moves) && doc.npc_moves.length) { + pushHtml("角色动作", renderStructuredValueHtml(doc.npc_moves, 0)); + } + if (Array.isArray(doc.variable_changes) && doc.variable_changes.length) { + pushHtml("变量变更", renderStructuredValueHtml(doc.variable_changes, 0)); + } + if (doc.tone_hint) push("语气", [doc.tone_hint]); + if (Array.isArray(doc.do_not_say) && doc.do_not_say.length) { + push("勿写入正文", doc.do_not_say); + } + if (Array.isArray(doc.suggested_actions) && doc.suggested_actions.length) { + push("建议行动", doc.suggested_actions); + } + if (!sections.length) return null; + return `
    ${sections.join("")}
    `; +} + +function renderVariableDesignHtml(doc) { + const parts = []; + if (doc.brief) { + parts.push( + `

    概要

    ${esc(String(doc.brief))}

    `, + ); + } + const body = doc.正文 && typeof doc.正文 === "object" ? doc.正文 : doc; + const keys = [ + "依据的体验", + "真值", + "Data映射索引", + "维护语句约定", + "side_effects", + "旁观汇总意图", + "不立变量的理由", + ]; + const used = new Set(); + for (const k of keys) { + if (body[k] == null) continue; + used.add(k); + parts.push( + `

    ${esc(k)}

    ${renderStructuredValueHtml(body[k], 0)}
    `, + ); + } + if (body === doc) { + for (const [k, v] of Object.entries(doc)) { + if (used.has(k) || ARTIFACT_SKIP_KEYS.has(k) || k === "brief" || k === "正文") continue; + if (v == null || v === "") continue; + if (k === "自评") { + const s = renderSelfScoreHtml(v); + if (s) parts.push(s); + continue; + } + parts.push( + `

    ${esc(k)}

    ${renderStructuredValueHtml(v, 0)}
    `, + ); + } + } else { + for (const [k, v] of Object.entries(body)) { + if (used.has(k) || v == null || v === "") continue; + parts.push( + `

    ${esc(k)}

    ${renderStructuredValueHtml(v, 0)}
    `, + ); + } + } + if (!parts.length) return null; + return `
    ${parts.join("")}
    `; } /** 极简 JSON 着色(仅 key / string / 其它) */ @@ -991,17 +2012,83 @@ function formatFlowParams(params) { return parts.length ? parts.join(" · ") : ""; } -function renderReviewFeedCard(review) { +function wireContextOrderEditor(root, sessionId) { + if (!root || !sessionId) return; + const post = (body) => + fetch(`/api/sessions/${encodeURIComponent(sessionId)}/context-order`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }) + .then(async (res) => { + const data = await res.json(); + if (!res.ok) throw new Error(data.error || "编排失败"); + window.dispatchEvent(new CustomEvent("wa:session-updated", { detail: data })); + }) + .catch((err) => alert(err.message || String(err))); + + root.querySelectorAll("[data-ctx-order]").forEach((el) => { + const action = el.getAttribute("data-ctx-order"); + const row = el.closest(".ctx-order-row"); + if (!row || !action) return; + const slotRef = row.getAttribute("data-slot-ref"); + const index = Number(row.getAttribute("data-index")); + if (!slotRef || !Number.isFinite(index)) return; + + if (el.tagName === "SELECT") { + el.addEventListener("change", () => { + void post({ + action: "set_projection", + slotRef, + index, + projection: el.value, + }); + }); + return; + } + + el.addEventListener("click", () => { + if (action === "move") { + const delta = Number(el.getAttribute("data-delta")); + if (delta !== 1 && delta !== -1) return; + void post({ action: "move", slotRef, index, delta }); + return; + } + if (action === "set_anchor") { + const anchor = el.getAttribute("data-anchor"); + if (anchor !== "pre_history" && anchor !== "post_history") return; + void post({ action: "set_anchor", slotRef, index, anchor }); + } + }); + }); +} + +function renderReviewFeedCard(review, opts = {}) { const flowHtml = review.creationFlowView ? renderCreationFlowView(review.creationFlowView) : ""; - const structured = !flowHtml && review.workerSetView - ? renderWorkerSetUserView(review.workerSetView) - : ""; - const jsonHtml = formatArtifactBodyHtml(review.body); + // 模块产物直接出产物卡;完整 Worker 集 / 流程才展开规格视图 + const wsHint = `${review.summary || ""} ${review.workerId || ""}`; + const showFullWorkerSet = + Boolean(review.workerSetView) && + /worker集|refine|worker-spec|游玩拓扑|细化终稿/i.test(wsHint); + let structured = ""; + if (flowHtml) { + structured = flowHtml; + } else if (showFullWorkerSet && review.workerSetView) { + structured = renderWorkerSetUserView(review.workerSetView, { + editable: false, + compact: true, + omitFocusBody: true, + }); + } + const artifactHtml = formatArtifactBodyHtml(review.body, { + defaultOpen: true, + hideAskSidecar: opts.hideAskSidecar === true, + }); const contextId = review.sourceMessageId || review.id || ""; const hasContext = review.contextTrace ? "1" : "0"; - const structuredBlock = flowHtml || structured; + const label = displayWorkerLabel(review.workerId) || "产物"; return `
    @@ -1009,31 +2096,24 @@ function renderReviewFeedCard(review) {
    待验收 - ${esc(displayWorkerLabel(review.workerId) || "产物")} + ${esc(label)}
    - ${review.summary ? `

    ${esc(review.summary)}

    ` : ""} -

    接受后:本单位过程讨论会折叠,产物进入前情。确认请用底栏「接受目前产物」;要改则在底栏输入修改意见后发送。

    - ${structuredBlock ? `
    ${structuredBlock}
    ` : ""} + ${structured ? `
    ${structured}
    ` : ""}
    -

    ${structuredBlock ? "规格 JSON" : "产物正文"}

    - ${jsonHtml} + ${artifactHtml}
    `; } -/** 侧栏验收 Tab 已弃用:验收改在主对话;此处仅隐藏 Tab */ -export function renderReviewPanel(view, _handlers = {}) { - const tabBtn = document.getElementById("agent-tab-btn-review"); - const panel = document.getElementById("review-artifact-panel"); - if (!tabBtn || !panel) return; - tabBtn.hidden = true; - panel.innerHTML = ""; - if (activeAgentTab === "review") setAgentTab("timeline"); -} +/** @deprecated 验收已在主对话;无侧栏面板 */ +export function renderReviewPanel(_view, _handlers = {}) {} export function renderAgentPanel(view, loading) { + wireRailChrome(); + maybeSyncRail(view); + const focusEl = document.getElementById("agent-focus"); const traceEl = document.getElementById("tool-trace"); const timelineEl = document.getElementById("agent-timeline"); @@ -1126,6 +2206,10 @@ export function renderMessageFeed(view, loading, handlers = {}) { return; } + const reviewingNow = + view.waitingReason?.kind === "review_artifact" && Boolean(view.reviewArtifact); + document.body.classList.toggle("is-reviewing", reviewingNow); + const intake = view.waitingReason?.kind === "intake" && view.intake?.fields?.length; const showIntakePanel = intake && hasUserMessages(view); @@ -1204,7 +2288,13 @@ export function renderMessageFeed(view, loading, handlers = {}) { ${!isUser && kind === "worker_questions" ? `
    需要你回答
    ` : ""} ${isUser ? "" : `
    ${headInner}
    `} ${!isUser && msg.thinking ? renderThinkingBlock(msg.thinking) : ""} -
    ${kind === "worker_questions" ? formatQuestionsHtml(body) : esc(body)}
    +
    ${ + kind === "worker_questions" + ? formatQuestionsHtml(body) + : kind === "worker_output" + ? formatArtifactBodyHtml(body) + : esc(body) + }
    ${isUser ? `
    ${headInner}
    ` : ""} `; feed.appendChild(card); @@ -1219,7 +2309,9 @@ export function renderMessageFeed(view, loading, handlers = {}) { !loading ) { const wrap = document.createElement("div"); - wrap.innerHTML = renderReviewFeedCard(view.reviewArtifact); + wrap.innerHTML = renderReviewFeedCard(view.reviewArtifact, { + hideAskSidecar: Boolean(getActiveQuestions(view)), + }); const card = wrap.firstElementChild; if (card) feed.appendChild(card); } @@ -1329,7 +2421,6 @@ export function renderBoardPanel(view) { export function renderWorkspace(view, loading, _onPickSkill, handlers = {}) { renderLifecycle(view); renderSkillPicker(view); - maybeAutoSwitchTab(view); const qHost = document.getElementById("questions-card-host"); if (qHost) { renderQuestionsCard(qHost, view, { @@ -1337,9 +2428,6 @@ export function renderWorkspace(view, loading, _onPickSkill, handlers = {}) { }); } renderMessageFeed(view, loading, handlers); - renderSkillGuide(view); - renderReviewPanel(view, handlers); - renderBoardPanel(view); renderAgentPanel(view, loading); } diff --git a/web/app.js b/web/app.js index 9d0b9d0..3e82862 100644 --- a/web/app.js +++ b/web/app.js @@ -1,4 +1,4 @@ -import { renderWorkspace, updateLiveStreamPanel } from "./agent-ui.js"; +import { renderWorkspace, updateLiveStreamPanel, resetRailChrome } from "./agent-ui.js"; import { downloadMarkdown, sessionToMarkdown } from "./export.js"; import { renderIntakePanel } from "./intake-ui.js"; import { displaySkillPackLabel } from "./display-labels.js"; @@ -780,12 +780,17 @@ function renderEmpty() { $("btn-export").disabled = true; $("message-feed").innerHTML = `

    点击左侧 + 新建作品

    `; $("skill-picker").hidden = true; - $("agent-focus").innerHTML = ""; - $("agent-timeline").innerHTML = ""; - $("skill-guide-list").innerHTML = ""; - const wsp = $("worker-set-user-panel"); - if (wsp) wsp.innerHTML = ""; - $("tool-trace").hidden = true; + const focus = $("agent-focus"); + if (focus) focus.innerHTML = ""; + const timeline = $("agent-timeline"); + if (timeline) timeline.innerHTML = ""; + const trace = $("tool-trace"); + if (trace) { + trace.hidden = true; + trace.innerHTML = ""; + } + document.body.classList.remove("is-reviewing"); + resetRailChrome(); hideBookMenu(); document.body.dataset.lifecycle = "design"; $("composer").innerHTML = `
    暂无打开的作品
    `; diff --git a/web/display-labels.js b/web/display-labels.js index dc0b46e..4112462 100644 --- a/web/display-labels.js +++ b/web/display-labels.js @@ -26,7 +26,8 @@ const WORKER_LABELS = { "agent-burst": "编排器调度", narrator: "叙事转述", "role-decide": "角色决策", - "world-simulator": "世界推演", + "world-simulator": "主世界层", + chance: "机遇裁定", "round-present": "回合呈现", outline: "大纲 / 细纲", "chapter-writer": "章节正文", @@ -35,7 +36,7 @@ const WORKER_LABELS = { const FIXED_TOPIC_LABELS = { "aesthetics-interaction": "美学纲领与交互范式", interaction: "交互范式", - narrative_guide: "叙事指南", + narrative_guide: "叙事指南与故事推进", input_protocol: "输入协议", core_premise: "核心前提", aesthetics: "美学纲领", diff --git a/web/index.html b/web/index.html index eaefcb9..72a8d38 100644 --- a/web/index.html +++ b/web/index.html @@ -4,20 +4,47 @@ Writing Agent + + +
    -
    - - diff --git a/web/questions-ui.js b/web/questions-ui.js index e57f8ff..273ca34 100644 --- a/web/questions-ui.js +++ b/web/questions-ui.js @@ -73,6 +73,10 @@ export function getActiveQuestions(view) { }; } if (wr.kind === "review_artifact" && wr.questions?.length) { + const assessment = + typeof wr.assessment === "string" && wr.assessment.trim() + ? wr.assessment.trim() + : ""; return { questions: normalizeQuestions(wr.questions).map((q) => ({ ...q, @@ -80,6 +84,7 @@ export function getActiveQuestions(view) { })), workerId: view.reviewArtifact?.workerId, pageSize, + assessment, optional: true, skipLabel: "跳过", dismissOnAccept: true, diff --git a/web/styles.css b/web/styles.css index 5f7ac80..356ded0 100644 --- a/web/styles.css +++ b/web/styles.css @@ -11,15 +11,22 @@ --ok: #16a34a; --warn: #ca8a04; --danger: #dc2626; - --books-w: 220px; - --agent-w: 300px; + --books-w: 200px; + --books-w-collapsed: 52px; --head-h: 48px; --composer-min: 72px; - font-family: system-ui, "Segoe UI", "PingFang SC", sans-serif; + --font-ui: "Noto Sans SC", "PingFang SC", "Microsoft YaHei UI", system-ui, sans-serif; + --font-read: "Noto Serif SC", "Songti SC", "STSong", "Source Han Serif SC", Georgia, serif; + font-family: var(--font-ui); font-size: 14px; color: var(--text); } +/* 开创作后缩进左侧栏 */ +body.rail-collapsed { + --books-w: var(--books-w-collapsed); +} + * { box-sizing: border-box; } html, body { height: 100%; @@ -40,13 +47,97 @@ a.link:hover { text-decoration: underline; } } .panel { background: var(--surface); border: 1px solid var(--border); min-height: 0; } -.panel-books { border-right: none; display: flex; flex-direction: column; min-height: 0; overflow: hidden; } +.panel-rail, +.panel-books { + border-right: 1px solid var(--border); + display: flex; + flex-direction: column; + min-height: 0; + overflow: hidden; + transition: width 0.15s ease; +} .panel-head { display: flex; align-items: center; justify-content: space-between; padding: 10px 12px; border-bottom: 1px solid var(--border); font-size: 13px; flex-shrink: 0; } +.rail-head { + gap: 6px; + padding: 8px 8px; + flex-wrap: wrap; +} +.rail-toggle { + flex-shrink: 0; + width: 28px; + padding: 4px 0; + font-size: 14px; + line-height: 1; +} +body.rail-collapsed .rail-toggle { transform: rotate(180deg); } +.rail-tabs { + display: flex; + flex: 1; + gap: 2px; + min-width: 0; +} +.rail-tab { + flex: 1; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 4px; + border: none; + background: transparent; + border-radius: 6px; + padding: 5px 6px; + cursor: pointer; + color: var(--muted); + font-size: 12px; + font-weight: 600; +} +.rail-tab:hover { background: var(--bg); color: var(--text); } +.rail-tab.active { + background: var(--accent-soft); + color: var(--accent); +} +.rail-tab-icon { font-size: 12px; } +.rail-head-actions { display: flex; align-items: center; gap: 4px; } +.rail-panels { + flex: 1; + min-height: 0; + display: flex; + flex-direction: column; + overflow: hidden; +} +.rail-tab-panel { + flex: 1; + min-height: 0; + display: flex; + flex-direction: column; + overflow: hidden; +} +.rail-tab-panel[hidden] { display: none !important; } + +body.rail-collapsed .rail-tab-label, +body.rail-collapsed .path-bar, +body.rail-collapsed .rail-head-actions #btn-new-book, +body.rail-collapsed .burst-badge { display: none !important; } +body.rail-collapsed .rail-head { + flex-direction: column; + align-items: stretch; + padding: 6px 4px; + gap: 4px; +} +body.rail-collapsed .rail-tabs { + flex-direction: column; +} +body.rail-collapsed .rail-tab { + flex-direction: column; + padding: 8px 2px; +} +body.rail-collapsed .rail-panels { display: none; } +body.rail-collapsed .panel-rail { align-items: stretch; } /* composer 按内容自适应;中间区吃掉剩余高度,整页锁在视口内 */ .panel-main { display: grid; @@ -72,7 +163,7 @@ a.link:hover { text-decoration: underline; } .main-body { display: grid; - grid-template-columns: 1fr var(--agent-w); + grid-template-columns: 1fr; min-height: 0; overflow: hidden; } @@ -81,9 +172,7 @@ a.link:hover { text-decoration: underline; } flex-direction: column; min-height: 0; overflow: hidden; - border-right: 1px solid var(--border); } -.panel-agent { display: flex; flex-direction: column; min-height: 0; overflow: hidden; background: #fafafa; } .btn, .btn-sm { border: 1px solid var(--border); @@ -176,7 +265,7 @@ a.link:hover { text-decoration: underline; } } .lifecycle-btn.active { background: var(--accent-soft); color: var(--accent); font-weight: 600; } body[data-lifecycle="play"] { --accent: #16a34a; --accent-soft: #dcfce7; --bg: #f0fdf4; } -body[data-lifecycle="play"] .panel-agent { background: #f7fef9; } +body[data-lifecycle="play"] .panel-rail { background: #f7fef9; } /* Skill picker — primary action when starting */ .skill-picker { @@ -200,13 +289,18 @@ body[data-lifecycle="play"] .panel-agent { background: #f7fef9; } .message-feed { flex: 1; overflow-y: auto; - padding: 16px 20px; + padding: 12px 14px; min-height: 0; display: flex; flex-direction: column; - gap: 14px; + gap: 10px; background: var(--bg); } +body.is-reviewing .message-feed, +body.rail-collapsed .message-feed { + padding: 6px 10px; + gap: 6px; +} .empty { color: var(--muted); text-align: center; padding: 40px 16px; align-self: center; max-width: 100%; } .empty-intake { white-space: pre-wrap; text-align: left; max-width: 560px; margin: 40px auto; line-height: 1.6; } @@ -463,26 +557,304 @@ body[data-lifecycle="play"] .panel-agent { background: #f7fef9; } margin: 0 0 8px; } -/* 主对话验收卡 */ +/* 主对话验收卡:吃满中间栏 */ +.msg-review { + align-self: stretch; + width: 100%; +} .msg-review .msg-review-bubble { - width: 90%; - max-width: 90%; - padding: 14px 16px 16px; + width: 100%; + max-width: 100%; + padding: 8px 10px 10px; + border-radius: 10px; +} +.msg-review .msg-head { padding: 2px 2px 0; } +.msg-review .msg-tag { + font-family: var(--font-ui); + letter-spacing: 0.04em; } .review-feed-summary { - margin: 0 0 8px; - font-size: 14px; - line-height: 1.45; + margin: 2px 0 4px; + font-family: var(--font-read); + font-size: 15px; + font-weight: 600; + line-height: 1.4; color: var(--text, inherit); } -.review-json-block { - margin: 12px 0 14px; +.review-json-block { margin: 4px 0 0; } +.msg-review .review-hint { + margin: 0 0 6px; + font-size: 11px; + line-height: 1.35; } -.review-json-title { - margin: 0 0 8px; - font-size: 12px; +.msg-review .ws-focus-banner { + padding: 6px 8px; + margin-bottom: 6px; +} +.msg-review .ws-focus-hint { margin-bottom: 0; font-size: 11px; } +.msg-review .review-worker-set { margin: 0 0 6px; } + +/* 产物紧凑卡:横向主轴,预览一行 */ +.artifact-compact-list { + display: flex; + flex-direction: column; + gap: 6px; +} +.artifact-compact { + border: 1px solid var(--border); + border-radius: 10px; + background: var(--surface); + overflow: hidden; +} +.artifact-compact-sum { + list-style: none; + cursor: pointer; + padding: 8px 10px; + user-select: none; +} +.artifact-compact-sum::-webkit-details-marker { display: none; } +.artifact-compact-sum::marker { content: ""; } +.artifact-compact-row { + display: flex; + align-items: flex-start; + gap: 10px; +} +.artifact-compact-main { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + gap: 3px; +} +.artifact-compact-title { + font-family: var(--font-ui); + font-size: 14px; + font-weight: 700; + line-height: 1.3; + letter-spacing: 0.01em; +} +.artifact-compact-meta { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 4px 6px; +} +.artifact-compact-toggle { + flex-shrink: 0; + align-self: center; + font-family: var(--font-ui); + font-size: 11px; font-weight: 600; + color: var(--accent); + padding: 4px 10px; + border-radius: 999px; + background: var(--accent-soft); +} +.artifact-compact-toggle::before { content: "展开"; } +.artifact-compact[open] .artifact-compact-toggle::before { content: "收起"; } +.artifact-compact-preview { + margin: 0; + font-family: var(--font-read); + font-size: 13px; + font-weight: 500; + line-height: 1.45; + color: #3f3f46; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.pct-pill { + display: inline-flex; + align-items: center; + padding: 1px 6px; + border-radius: 999px; + font-family: var(--font-ui); + font-size: 10.5px; + font-weight: 700; + font-variant-numeric: tabular-nums; +} +.pct-pill.tone-low { + color: var(--danger); + background: color-mix(in srgb, var(--danger) 12%, var(--surface)); +} +.pct-pill.tone-mid { + color: #a16207; + background: #fef9c3; +} +.pct-pill.tone-high { + color: var(--ok); + background: #dcfce7; +} +.artifact-compact-body { + border-top: 1px solid var(--border); + padding: 8px; + max-height: min(72vh, 640px); + overflow: auto; + background: #fafafa; + font-family: var(--font-ui); +} +.artifact-compact-body .artifact-friendly { gap: 6px; } +.artifact-compact-body .artifact-block, +.artifact-compact-body .artifact-friendly .ws-section { + padding: 6px 8px; +} +.ws-focus-body { + margin-top: 8px; + max-height: none; + padding: 0; + border: none; + background: transparent; +} +.ws-focus-body .artifact-compact-list { margin: 0; } + +/* 展开区:mosaic / 瓷砖网格,吃满横向宽度 */ +.af-mosaic { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 6px; +} +body.is-reviewing .af-mosaic, +body.rail-collapsed .af-mosaic { + grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); +} +.af-panel { + padding: 6px 8px; + border: 1px solid var(--border); + border-radius: 8px; + background: var(--surface); + min-width: 0; +} +.af-panel > h4 { + margin: 0 0 6px; + font-family: var(--font-ui); + font-size: 11px; + font-weight: 700; color: var(--muted); + letter-spacing: 0.06em; +} +.af-panel-wide { grid-column: 1 / -1; } +.af-tile-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(132px, 1fr)); + gap: 5px; +} +.af-tile { + padding: 6px 7px; + border-radius: 7px; + background: var(--bg); + border: 1px solid color-mix(in srgb, var(--border) 70%, transparent); + min-width: 0; +} +.af-tile:hover { border-color: var(--border); background: #fff; } +.af-tile-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 6px; + margin-bottom: 3px; +} +.af-tile-name { + font-family: var(--font-ui); + font-size: 11px; + font-weight: 700; + color: #52525b; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + letter-spacing: 0.02em; +} +.af-tile-text { + margin: 0; + font-family: var(--font-read); + font-size: 12.5px; + font-weight: 500; + line-height: 1.5; + color: #27272a; + display: -webkit-box; + -webkit-line-clamp: 4; + -webkit-box-orient: vertical; + overflow: hidden; + word-break: break-word; +} +.af-tile-sub { + display: block; + margin-top: 2px; + font-family: var(--font-ui); + font-size: 10.5px; + color: var(--muted); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.af-tile-sub.pending { color: #a16207; } +.af-stack { display: flex; flex-direction: column; gap: 6px; width: 100%; } +.af-group { min-width: 0; width: 100%; } +.af-group-wide { + width: 100%; + min-width: 0; + padding: 4px 0; +} +.af-group-wide .artifact-kv, +.af-group-wide .artifact-prose, +.af-group-wide .artifact-inline { + max-width: none; +} +.af-group-title { + font-family: var(--font-ui); + font-size: 10.5px; + font-weight: 700; + color: var(--muted); + margin-bottom: 4px; + letter-spacing: 0.05em; +} +.af-panel-wide .af-stack, +.af-panel-wide .artifact-kv { + width: 100%; +} +.af-score-grid { + display: grid !important; + grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); + gap: 5px; +} +.pct-mini { + display: inline-flex; + align-items: center; + gap: 4px; + flex-shrink: 0; + font-variant-numeric: tabular-nums; +} +.pct-mini > b { + font-size: 11px; + font-weight: 800; + min-width: 1.6em; + text-align: right; +} +.pct-mini > i { + display: block; + width: 28px; + height: 4px; + border-radius: 999px; + background: color-mix(in srgb, var(--border) 80%, transparent); + position: relative; + overflow: hidden; + font-style: normal; +} +.pct-mini > i::after { + content: ""; + position: absolute; + inset: 0 auto 0 0; + width: var(--p, 0%); + border-radius: inherit; + background: currentColor; +} +.pct-mini.tone-low { color: var(--danger); } +.pct-mini.tone-mid { color: var(--warn); } +.pct-mini.tone-high { color: var(--ok); } + +@media (max-width: 720px) { + .af-mosaic { grid-template-columns: 1fr; } + .af-panel-wide { grid-column: auto; } } .json-pretty, .review-feed-plain { @@ -514,14 +886,11 @@ body[data-lifecycle="play"] .panel-agent { background: #f7fef9; } .review-feed-actions .btn { flex: 1; } -.msg-review .review-worker-set { - margin: 8px 0 4px; -} .msg-review .ws-section h4 { - font-size: 13px; + font-size: 12px; } .msg-review .ws-user-view { - gap: 12px; + gap: 8px; } .ws-creation-flow .flow-brief { @@ -838,7 +1207,13 @@ body[data-lifecycle="play"] .panel-agent { background: #f7fef9; } color: var(--muted); line-height: 1.4; } -.qcard-prompt { font-size: 14px; font-weight: 600; margin-bottom: 8px; line-height: 1.45; } +.qcard-prompt { + font-size: 14px; + font-weight: 600; + margin-bottom: 8px; + line-height: 1.45; + white-space: pre-wrap; +} .qcard-options { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 6px; } .qcard-opt { display: grid; @@ -915,6 +1290,290 @@ body[data-lifecycle="play"] .panel-agent { background: #f7fef9; } .ws-user-view { display: flex; flex-direction: column; gap: 10px; } .ws-section h4 { margin: 0 0 6px; font-size: 12px; color: var(--muted); font-weight: 600; } .ws-section p { margin: 0; line-height: 1.5; } +.ws-slot-row { display: flex; flex-wrap: wrap; gap: 6px; } +.ws-slot-chip { + display: inline-flex; align-items: center; padding: 2px 8px; border-radius: 6px; + font-size: 12px; border: 1px solid var(--border); +} +.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; } +/* 产物结构化展示 — 对齐 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 { + display: flex; flex-direction: column; gap: 8px; + padding: 10px 12px; + border-radius: 10px; + border: 1px solid color-mix(in srgb, var(--accent) 35%, var(--border)); + background: color-mix(in srgb, var(--accent) 7%, var(--surface)); +} +.artifact-block, +.artifact-friendly .ws-section { + padding: 6px 8px; + border: 1px solid var(--border); + border-radius: 8px; + background: var(--surface); +} +.artifact-block h4, +.artifact-friendly .ws-section h4 { + margin: 0 0 6px; + font-size: 11px; + font-weight: 700; + color: var(--muted); + letter-spacing: 0.05em; +} +.artifact-chip-row { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; } +.artifact-chip { + display: inline-flex; align-items: center; + padding: 2px 8px; border-radius: 6px; + font-size: 12px; border: 1px solid var(--border); + background: var(--surface-2, rgba(0, 0, 0, 0.03)); +} +.artifact-chip.mount { + background: var(--accent-soft); + border-color: transparent; + color: var(--accent); +} +.artifact-chip.muted { opacity: 0.8; color: var(--muted); } +.artifact-prose, .artifact-inline, .artifact-conclusion { + margin: 0; + font-family: var(--font-read); + font-size: 13.5px; + font-weight: 500; + line-height: 1.6; + color: #27272a; + white-space: pre-wrap; + word-break: break-word; +} +.artifact-prose, .artifact-conclusion { + padding: 8px 10px; + border-radius: 8px; + background: var(--bg); +} +.artifact-brief { + margin: 0; + font-family: var(--font-read); + font-size: 15px; + font-weight: 600; + line-height: 1.5; +} +.artifact-kernel.qcard-assessment, +.artifact-kernel { + font-family: var(--font-read); +} +.artifact-kernel .artifact-prose, +.artifact-kernel .artifact-inline { + font-size: 14px; + line-height: 1.65; +} +.artifact-aside-label, +.af-panel > h4, +.artifact-block h4, +.artifact-friendly .ws-section h4 { + font-family: var(--font-ui); +} +.artifact-scalar { font-variant-numeric: tabular-nums; } +.artifact-cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 6px; } +.artifact-card { + border: 1px solid var(--border); + border-radius: 8px; + padding: 8px 10px; + background: var(--bg); +} +.artifact-card-title { font-weight: 600; font-size: 13px; margin-bottom: 6px; } +.artifact-card-body { font-size: 13px; line-height: 1.45; } +.artifact-kv { display: flex; flex-direction: column; gap: 8px; } +.artifact-kv-row { + display: grid; + grid-template-columns: minmax(4.5rem, 26%) 1fr; + gap: 6px 10px; + align-items: start; +} +.artifact-kv-row.complex { grid-template-columns: 1fr; gap: 4px; } +.artifact-kv-key { + font-size: 11px; font-weight: 600; color: var(--muted); + padding-top: 2px; +} +.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; + gap: 5px; + padding: 8px 10px; + border-radius: 8px; + background: var(--bg); + border: 1px solid var(--border); +} +.pct-meter-head { + display: flex; + justify-content: space-between; + align-items: baseline; + gap: 8px; + font-size: 12px; +} +.pct-meter-label { font-weight: 600; color: var(--muted); } +.pct-meter-value { + font-weight: 700; + font-variant-numeric: tabular-nums; + font-size: 13px; +} +.pct-meter-track { + height: 8px; + border-radius: 999px; + background: color-mix(in srgb, var(--border) 70%, transparent); + overflow: hidden; +} +.pct-meter-track > i { + display: block; + height: 100%; + border-radius: inherit; + transition: width 0.2s ease; +} +.pct-meter.tone-low .pct-meter-value { color: var(--danger); } +.pct-meter.tone-low .pct-meter-track > i { background: var(--danger); } +.pct-meter.tone-mid .pct-meter-value { color: var(--warn); } +.pct-meter.tone-mid .pct-meter-track > i { background: var(--warn); } +.pct-meter.tone-high .pct-meter-value { color: var(--ok); } +.pct-meter.tone-high .pct-meter-track > i { background: var(--ok); } + +.artifact-scores { display: flex; flex-direction: column; gap: 8px; } +.artifact-score-note, .artifact-weak { + margin: 6px 0 0; font-size: 12px; color: var(--muted); line-height: 1.4; +} +.artifact-weak { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; } +.artifact-diag { display: flex; flex-direction: column; gap: 8px; } +.artifact-basis, .artifact-fact { + margin: 0; + font-size: 12.5px; + line-height: 1.5; + padding: 6px 8px; + border-radius: 6px; + background: color-mix(in srgb, var(--surface) 88%, var(--muted) 12%); +} +.artifact-fact.pending { + border-left: 3px solid var(--warn); + background: color-mix(in srgb, #fef9c3 55%, var(--surface)); +} +.artifact-aside-label { + display: inline-block; + margin-right: 6px; + font-size: 11px; + font-weight: 700; + color: var(--muted); +} +.artifact-kernel.qcard-assessment { margin: 0; } +.artifact-subgroups { display: flex; flex-direction: column; gap: 8px; } +.artifact-subgroup { + padding: 8px 10px; + border-radius: 8px; + background: var(--bg); + border: 1px solid transparent; +} +.artifact-subgroup:hover { border-color: var(--border); } +.artifact-subgroup-title { + font-size: 12px; + font-weight: 600; + color: var(--muted); + margin-bottom: 6px; +} +.artifact-body-specialty { display: contents; } +.artifact-probe-lead { + margin: 0 0 10px; + font-size: 13px; + line-height: 1.45; + color: var(--muted); +} +.artifact-probe-q + .artifact-probe-q { margin-top: 14px; } +.artifact-probe-opts { margin-top: 6px; } +.artifact-probe-opts .qcard-opt { pointer-events: none; } +.artifact-q-idx { + display: inline-flex; + align-items: center; + justify-content: center; + width: 22px; + height: 22px; + margin-right: 8px; + border-radius: 6px; + background: var(--accent-soft); + color: var(--accent); + font-size: 11px; + font-weight: 700; + vertical-align: -2px; +} +.artifact-order-list { + list-style: none; margin: 0; padding: 0; + display: flex; flex-direction: column; gap: 6px; +} +.artifact-order-list > li { + display: flex; flex-wrap: wrap; align-items: center; gap: 6px 8px; + padding: 6px 8px; border: 1px solid var(--border); border-radius: 8px; + font-size: 13px; background: var(--bg); +} +.artifact-order-list > li.is-history { + border-color: color-mix(in srgb, var(--accent) 45%, var(--border)); + background: color-mix(in srgb, var(--accent) 8%, var(--surface)); +} +.artifact-order-idx { + display: inline-flex; align-items: center; justify-content: center; + min-width: 1.4rem; height: 1.4rem; padding: 0 4px; + border-radius: 999px; font-size: 11px; font-weight: 700; + background: var(--accent-soft); + color: var(--accent); +} +.artifact-order-ref { font-weight: 600; } +.artifact-raw { + margin-top: 10px; + font-size: 12px; + color: var(--muted); +} +.artifact-raw > summary { + cursor: pointer; user-select: none; padding: 4px 0; +} +.artifact-raw > summary:hover { color: var(--text); } +.artifact-raw .json-pretty { margin-top: 6px; max-height: min(40vh, 360px); } +.artifact-body-root > .artifact-kv, +.artifact-body-root > .artifact-cards { margin-top: 2px; } + +.ws-context-order .ws-order-hint { margin: 0.25rem 0 0.5rem; font-size: 0.85em; } +.ctx-order-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 6px; } +.ctx-order-row { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + gap: 8px; + padding: 6px 8px; + border: 1px solid var(--border, #ddd); + border-radius: 6px; + background: var(--surface-2, transparent); +} +.ctx-order-line { flex: 1 1 12rem; font-size: 0.9em; line-height: 1.4; } +.ctx-order-actions { display: inline-flex; flex-wrap: wrap; align-items: center; gap: 4px; } +.btn-mini { + font: inherit; + font-size: 0.8em; + padding: 2px 8px; + border-radius: 4px; + border: 1px solid var(--border, #ccc); + background: var(--surface, #fff); + cursor: pointer; +} +.btn-mini:hover { border-color: var(--accent, #666); } +.ctx-order-proj { + font: inherit; + font-size: 0.8em; + padding: 2px 4px; + border-radius: 4px; + border: 1px solid var(--border, #ccc); + max-width: 6.5rem; +} +.ctx-order-row.ctx-order-history { + border-color: var(--accent, #888); + background: var(--accent-soft, rgba(0, 0, 0, 0.04)); +} .ws-muted { color: var(--muted); font-size: 11px; } .ws-headline p { font-weight: 500; } .ws-list, .ws-kv { margin: 0; padding-left: 18px; line-height: 1.5; } @@ -1018,14 +1677,6 @@ body[data-lifecycle="play"] .panel-agent { background: #f7fef9; } color: #92400e; line-height: 1.45; } -.ws-focus-body { - border-radius: 8px; - background: #fff; - border: 1px dashed #f59e0b; - padding: 8px 10px; - max-height: 280px; - overflow: auto; -} .ws-focus-body-text, .ws-focus-body-json { margin: 0; @@ -1084,10 +1735,11 @@ body[data-lifecycle="play"] .panel-agent { background: #f7fef9; } .context-menu-danger:hover { background: #fef2f2; } /* Agent panel */ -.agent-focus { padding: 10px 12px; border-bottom: 1px solid var(--border); font-size: 13px; } -.agent-focus-who { font-weight: 600; } -.agent-focus-action { margin-top: 4px; } -.agent-focus-detail { margin-top: 4px; font-size: 12px; color: var(--muted); } +.agent-focus { padding: 8px 10px; border-bottom: 1px solid var(--border); font-size: 12px; flex-shrink: 0; } +.agent-focus-who { font-weight: 600; font-family: var(--font-ui); } +.agent-focus-action { margin-top: 2px; } +.agent-focus-detail { margin-top: 2px; font-size: 11px; color: var(--muted); } +#rail-panel-log .tool-trace { max-height: 30%; overflow: auto; } .burst-badge { font-size: 11px; padding: 2px 6px; border-radius: 4px; background: var(--accent-soft); color: var(--accent); } .tool-trace { padding: 8px 12px; border-bottom: 1px solid var(--border); font-size: 12px; } .tool-trace-item { padding: 4px 0; border-bottom: 1px dashed var(--border); } @@ -1177,9 +1829,10 @@ body[data-lifecycle="play"] .panel-agent { background: #f7fef9; } .dialog-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 14px; } .dialog-head-row { display: flex; justify-content: space-between; align-items: center; } -@media (max-width: 900px) { - .layout { grid-template-columns: 1fr; } - .panel-books { display: none; } - .main-body { grid-template-columns: 1fr; } - .panel-agent { display: none; } +@media (max-width: 720px) { + .layout { grid-template-columns: var(--books-w-collapsed) 1fr; } + body:not(.rail-collapsed) { --books-w: var(--books-w-collapsed); } + body:not(.rail-collapsed) .rail-tab-label, + body:not(.rail-collapsed) .path-bar { display: none !important; } + body:not(.rail-collapsed) .rail-panels { display: none; } }