新建作品改为直接进入对话落盘,按本局创作方案写能力,并把楼层呈现和零层卡分开。
输入栏同时可以按分组切换模型。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,891 @@
|
||||
# 对话落盘单一化 · 执行蓝图
|
||||
|
||||
> 状态:核心方向已确认,按本文依赖顺序实施;非工具节点仍须逐项审阅提示词。
|
||||
> 修订:2026-09-30。
|
||||
> 相关:`docs/play-presentation-shells.md`、`docs/context-fragment-design.md`、`docs/world-simulator-modules.md`。
|
||||
|
||||
## 执行状态(2026-09-28)
|
||||
|
||||
- [x] 基线扫尾:删除新建作品选配方的过时提示。
|
||||
- [x] 工作包 A:本局创作方案、两态注入、能力覆盖校验、旧配方案例化。
|
||||
- [x] 工作包 B:`read_module`、`read_library_entry`、提示版本门禁、按需读取库。
|
||||
- [ ] 工作包 C(进行中):内容节点、普通呈现链和 0 层呈现链均已迁移;`user-requirements` 经审阅决定删除;四个共用运行步骤已确认为自动工具节点,尚待补齐完整确定性编译。
|
||||
- [ ] 工作包 D:游玩投影与维护合同编译,未开始。
|
||||
- [ ] 工作包 E(进行中):楼层/0 层双呈现节点与数据合同已建立;壳参考库已按 7 种区、5 只壳写入 `shell-refs/`;隔离 iframe、持久槽位运行时和重 roll 尚未实现。
|
||||
- [ ] 工作包 F:库条目 schema、本局化与提示归属,部分完成;偏好/文风库按需读取;仓库 `seeds/libraries/*.json` 可 git 备份并 `npm run libraries:restore` 恢复。
|
||||
- [ ] 工作包 G:长程记忆、表维护与固定输入语法,未开始。
|
||||
|
||||
## 0. 已确认结论
|
||||
|
||||
1. 新建作品只填写名称并直接进入对话落盘。这部分已经完成,不再列为工作包;旧工序编排书和后端 DAG 暂留兼容。
|
||||
2. 创作入口与设计清单合并为一份中间产物「本局创作方案」。它根据自然语言生成本局方法(下文称“本局配方”),并给全部能力分档。
|
||||
3. 旧配方不再供用户手动选择。修订后作为本局创作方案的参考案例,不作为固定路径。
|
||||
4. 不写死“先美学、再清单、再逐节点”的统一工作流。本局创作方案描述依赖与重要程度,模型根据当前材料灵活推进,并可在用户改口后整份重写。
|
||||
5. 创作对话保留一次用户输入触发多次 `insert` 的能力。逐节点审阅只约束本次程序改造:修改每份非工具节点提示词前必须给用户过目,不限制产品运行时一次只能写一个产物。
|
||||
6. 壳最终仍是正文组成节点读取的参考库。否定的是当前七个外观壳及其按产品原型换皮的分类,不是否定参考库本身。
|
||||
7. 正文前端采用隔离 iframe。它是显示容器,不提供 `action` 回传;用户仍从主输入框输入。
|
||||
8. 创作源产物、创作交互与评分、游玩投影分开。开玩时不得再把全部 `设计.*` 原样挂给主世界层。
|
||||
9. 长程记忆采用“写大总结”和“清空旧楼层”两个独立操作,不在大总结成功后自动删除历史。
|
||||
10. 输入语法写死:裸写表示上帝视角,单井号包裹的 `#内容#` 表示角色行动,引号包裹的内容表示角色说话。
|
||||
11. 新增“0 层卡”呈现模式:开场白所在气泡是持久前端;正文、指令、表盘、地图等都是其中的槽位,每轮就地更新,不继续向下叠加可见助手楼层。
|
||||
12. 普通楼层与 0 层卡不共用呈现节点;现已复制 `回复呈现`、`正文组成`、`开场白与开场变量` 三个显示强相关节点,其它内容能力继续共用。
|
||||
13. 0 层模式在“更多操作”中提供“重 roll 本轮”,复用本轮消息前 checkpoint;暂不提供任意历史回退。
|
||||
|
||||
|
||||
|
||||
## 1. 当前基线
|
||||
|
||||
以下内容已经落地,不再实施:
|
||||
|
||||
- 新建作品弹窗只保留作品名称。
|
||||
- 新建作品固定 `creationMode: "dictate"`。
|
||||
- 新会话默认进入对话落盘。
|
||||
- 旧工序编排书仍可打开,DAG 后端和旧前端仅作兼容。
|
||||
|
||||
扫尾项(已完成):
|
||||
|
||||
- [x] 删除 `web/agent-ui.js` 中“请先在新建作品时选定配方”的过时提示;旧书展示所需的「工序编排」标签可保留。
|
||||
|
||||
现有但尚未达到本文目标的基础设施:
|
||||
|
||||
- 文风库、偏好库和库注册表已经存在,但对话落盘仍会整库注入。
|
||||
- `play-bind` 已能自动装配部分运行上下文,但仍主要按 tag 分类并给主世界层全量投影。
|
||||
- `auditor` 和 `maintain.v1` 已存在,但默认关闭,也没有本文所需的阻塞、总结和清空交互。
|
||||
- 现有七个壳及预览器仍存在,只作为待替换旧实现。
|
||||
|
||||
|
||||
|
||||
## 2. 目标链路
|
||||
|
||||
```text
|
||||
用户自然描述
|
||||
→ 本局创作方案〔中间产物,可反复重写〕
|
||||
- 生成本局配方:如何把本次需求做出来
|
||||
- 给全部能力分档并说明理由、缺口和建议实例
|
||||
- 只表达必要依赖,不生成固定流水线
|
||||
→ 按当前材料灵活推进
|
||||
- 写某能力前 read_module
|
||||
- 需要库条目时 read_library_entry
|
||||
- 同轮只连续写入这句话已经说清的能力,通常 2~3 个
|
||||
- 每份写入携带独立 self_score
|
||||
→ 回复呈现
|
||||
→ 正文组成读取壳参考库,生成本局模板与数据契约
|
||||
→ 开场白按数据契约生成 present.v1
|
||||
→ 开玩时把源产物编译成各 Worker 的游玩投影
|
||||
```
|
||||
|
||||
这不是硬编码流程:
|
||||
|
||||
- 用户这句话已经说清的能力,同轮可以读取并连续 `insert`,通常 2~3 个。没提到的「必须」和「有必要」按已有材料给 2~3 个选项询问,选定后再写。写开场前用未落项问用户哪些还没设计。
|
||||
- 能力之间只有真实语义依赖。例如正文组成需要知道要呈现什么,开场白通常靠近创作收束;不能因此把所有作品强制塞进同一节点序列。
|
||||
- 文本创作、世界模拟、短局、长局、数据化跑团等只是本局配方案例,不是程序分支。
|
||||
- 用户改变目标时,先更新本局创作方案,再按新方案增删或改写产物。
|
||||
|
||||
|
||||
|
||||
## 3. 工作包 A · 本局创作方案〔已完成〕
|
||||
|
||||
|
||||
|
||||
### 3.1 能力与产物
|
||||
|
||||
新增 `modules/creation-plan/prompt.md`,目录登记:
|
||||
|
||||
```yaml
|
||||
- id: creation-plan
|
||||
name: 本局创作方案
|
||||
layer: intermediate
|
||||
artifact: 设计.本局创作方案
|
||||
```
|
||||
|
||||
该产物同时承担原“创作入口”和“本局设计清单”的职责:
|
||||
|
||||
```json
|
||||
{
|
||||
"schema": "creation-plan.v1",
|
||||
"技能": "本局创作方案",
|
||||
"brief": "一句话说明本局靠什么兑现体验",
|
||||
"recipe": {
|
||||
"primary": "主要创作方法",
|
||||
"secondary_traits": ["可混合的次要体验特征"],
|
||||
"method": ["针对本局的做法,不是固定节点顺序"],
|
||||
"references": ["采用或改造过的旧配方案例 id"]
|
||||
},
|
||||
"items": [
|
||||
{
|
||||
"module_id": "variable-design",
|
||||
"tier": "必须",
|
||||
"why": "任务时限与暴露度必须跨轮保存",
|
||||
"missing": "暴露度达到哪一档会被识破尚未确定",
|
||||
"instances": []
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
`items` 覆盖对话落盘可见的全部内容能力,但排除:
|
||||
|
||||
- 本局创作方案自身。
|
||||
- 变量声明、变量映射、上下文排序等机械工具节点。
|
||||
- 只供旧工序编排使用的节点。
|
||||
|
||||
四档含义:
|
||||
|
||||
|
||||
| 档位 | 含义 | 默认行为 |
|
||||
| ----- | -------------- | --------------- |
|
||||
| 必须 | 缺少后无法运行或核心体验会崩 | 收口前问清。用户没提到时给 2~3 个选项,选定后再写 |
|
||||
| 有必要 | 缺少仍可运行,但体验明显受损 | 同样先问再写;用户明确不要时可降档 |
|
||||
| 有一定效果 | 有了更好 | 用户提及时做;可顺带询问一次 |
|
||||
| 没有意义 | 本局用不上 | 不主动推进 |
|
||||
|
||||
|
||||
四档是收口前的核对清单,不是本轮待办,也不是 DAG。程序不按照数组顺序强制执行节点。本轮通常只写用户这句话已经说清的 2~3 个能力。
|
||||
|
||||
### 3.2 旧配方改为案例
|
||||
|
||||
- 删除新建阶段和创作页中的手动配方选择。
|
||||
- 修订现有对话落盘配方,只保留各自有辨识度的方法、适用信号和能力档位倾向。
|
||||
- 本局创作方案可以引用、混合或改写多个案例;引用结果写入 `recipe.references`。
|
||||
- 案例不能复制总提示的公共规则,也不能恢复成隐藏的固定工作流。
|
||||
|
||||
|
||||
|
||||
### 3.3 注入
|
||||
|
||||
`buildDictateSystemPrompt` 分两态:
|
||||
|
||||
- 没有方案:提供能力目录的短声明、使用边界和配方案例目录,要求根据用户当前材料形成方案。
|
||||
- 已有方案:注入完整本局配方;能力按档分组,显示已落/未落和最低自评分。前三档附带该能力的何时用 / 何时不用(方案改写或补全后仍保留;可反复能力已落一条也保留),供本轮选型。不附带执行规格。「没有意义」只列名称。
|
||||
|
||||
用户改口、新增方向,或现有方案明显不再成立时,允许覆盖 `设计.本局创作方案`。
|
||||
|
||||
### 3.4 程序约束与验收
|
||||
|
||||
- 写入方案时校验能力覆盖;漏项则拒绝并列出 id。
|
||||
- 插入开场白时,如仍有“必须”项未落,只提醒,不拒绝。
|
||||
- 不要求体验锚定达到固定计数,也不要求先经过美学节点。
|
||||
- 方案落盘后系统提示应明显短于全目录注入。
|
||||
- 测试:`tests/creation-plan.test.ts`。
|
||||
|
||||
|
||||
|
||||
## 4. 工作包 B · 写产物前读取能力与参考〔已完成〕
|
||||
|
||||
|
||||
|
||||
### 4.1 `read_module`
|
||||
|
||||
`src/dictate/tools.ts` 新增:##
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "read_module",
|
||||
"parameters": {
|
||||
"module_id": "能力 id,也接受中文名",
|
||||
"reason": "可选,一句话"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
返回该能力的:
|
||||
|
||||
1. `task`、`principles`、`output`、`score`。
|
||||
2. 绑定库的条目目录与短说明,不返回整库正文。
|
||||
3. 正文组成所需的壳参考目录、正文安全子集和 iframe 数据契约说明。
|
||||
4. 该能力适用的程序约束。
|
||||
|
||||
会话记录已读取的能力及其提示版本。写某能力产物前未读该版本时,`insert` 拒绝并提示调用 `read_module`。同轮可以先读用户这句已经说清的能力(通常 2~3 个),再连续执行这些 `insert`。还没提到的先问,不先读后写。
|
||||
|
||||
机械工具产物不受此限制。
|
||||
|
||||
### 4.2 `read_library_entry`
|
||||
|
||||
仅返回库目录不足以完成选择,因此新增独立工具:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "read_library_entry",
|
||||
"parameters": {
|
||||
"library_id": "库 id",
|
||||
"entry_ids": ["本轮选中的 1~2 个条目 id"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
约束:
|
||||
|
||||
- 只能读取当前能力绑定库中的条目。
|
||||
- 默认最多读取两个;确需更多时必须给出原因。
|
||||
- 本局源产物记录条目引用和本局微调;未选条目不进入后续提示。
|
||||
- 用户提出库外新取向时,可先形成本局临时条目;是否回写全局库另行确认。
|
||||
|
||||
|
||||
|
||||
### 4.3 验收
|
||||
|
||||
- 未读取能力就写对应产物会被拒绝。
|
||||
- 读取正文组成时能看到壳目录和 iframe 契约。
|
||||
- 对话落盘不再默认注入文风库、偏好库全文。
|
||||
- 测试:`tests/dictate-read-module.test.ts`、`tests/dictate-read-library-entry.test.ts`。
|
||||
|
||||
|
||||
|
||||
## 5. 工作包 C · 产物分层、评分与自然回复〔进行中〕
|
||||
|
||||
|
||||
|
||||
### 5.1 四类数据
|
||||
|
||||
1. **中间产物**:只服务创作决策,不进游玩。目前包括 `设计.本局创作方案`,以及 `lifecycle_intent = seed_only` 的生成规则。
|
||||
2. **源产物**:能力形成的完整定稿,保留在黑板供编辑、验收和重新编译。
|
||||
3. **游玩投影**:从源产物确定性提取、按 Worker 分发的运行时内容。
|
||||
4. **创作交互与评分元数据**:回应、评价、启发、追问、缺口和分数;留在消息或元数据,不进入游玩投影。
|
||||
|
||||
`DictateProduct` 和黑板元数据新增 `layer`。目录的 `layer` 是默认值;生成规则根据产物中的 `lifecycle_intent` 覆盖默认值。
|
||||
|
||||
### 5.2 自评分
|
||||
|
||||
每个能力 `prompt.md` 新增 `score` fence,每个维度给出 5 / 3 / 1 分锚点。`insert` 新增:
|
||||
|
||||
```json
|
||||
{
|
||||
"self_score": {
|
||||
"dims": [
|
||||
{
|
||||
"name": "维度名",
|
||||
"score": 3,
|
||||
"gap": "当前缺口",
|
||||
"options": ["涉及用户偏好时提供的具体选项"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- 分数为 1~5。
|
||||
- `score <= 3` 必须写 `gap`;涉及用户选择时给 2~3 个 `options`。
|
||||
- 维度名必须与该能力 `score` fence 一致。
|
||||
- 分数存入黑板元数据,不再写进产物正文。
|
||||
- `追问`、`开放问题`、完备度诊断同样不进入游玩投影。
|
||||
|
||||
迁移按 §9 逐节点审阅进行。过渡期兼容规则:
|
||||
|
||||
- 已有 `score` fence 的能力使用新参数并严格校验。
|
||||
- 尚未迁移的能力继续接受旧 `output` 形状,不因缺少 `score` fence 阻塞写入。
|
||||
- 全部内容节点迁移完成后,统一删除旧自评解析和兼容分支。
|
||||
|
||||
|
||||
|
||||
### 5.3 用户可见回复
|
||||
|
||||
回复不使用固定标题、固定句数或逐模块仪式。按当轮内容自然覆盖:
|
||||
|
||||
1. 回应用户输入中的有效判断、矛盾或值得保留之处。
|
||||
2. 说明本轮形成或改写了什么;完整 JSON 由产物卡展示。
|
||||
3. 用户没提到的「必须」或「有必要」,给 2~3 个根据原话推出的选项来问。这句话已经说清并写入的,不再强行追问。
|
||||
4. 简短说明强项和缺口。
|
||||
|
||||
写开场或结束创作前,用仍未落的「必须」和「有必要」问用户哪些还没设计。同轮写入多个已说清产物时综合回应整轮,不逐模块重复模板。
|
||||
|
||||
写正文组成或 0 层正文组成时,回复里直接给出一份隔离内容供预览:与本局前端同一套骨架,示例已经写进去。完整 JSON 仍由产物卡展示。
|
||||
|
||||
## 6. 工作包 D · 游玩投影编译〔未开始〕
|
||||
|
||||
“游玩投影”指:创作时保存的完整产物不能直接塞进模型;开玩时先抽出每个 Worker 真正需要的字段,再分别装配。
|
||||
|
||||
### 6.1 编译入口
|
||||
|
||||
在 `src/dictate/play-bind.ts` 增加明确的编译阶段:
|
||||
|
||||
```text
|
||||
黑板源产物
|
||||
→ resolve module / layer
|
||||
→ intermediate 与 seed_only 跳过
|
||||
→ compileDictateProjection
|
||||
→ 按 mount 分发给 world-simulator / narrator / auditor
|
||||
→ 各自按 context_order 装配
|
||||
```
|
||||
|
||||
不要再用 tag 正则把几乎全部 `设计.*` 挂给主世界层。
|
||||
|
||||
### 6.2 投影契约
|
||||
|
||||
能力目录声明默认 `mount`;每个最终能力提供确定性的投影器或字段允许列表:
|
||||
|
||||
- 主世界层:事实推进、角色扮演、输入解释、权限边界。
|
||||
- 转述:本局选中的文风、呈现规则、正文数据契约。
|
||||
- 副 Worker:表结构、当前值、维护规则及人物一致性所需的少量设定。
|
||||
|
||||
所有投影统一剥离:
|
||||
|
||||
- 创作期回应与评价。
|
||||
- `self_score`、追问、开放问题、完备度和诊断依据。
|
||||
- 本局创作方案状态。
|
||||
- 不属于目标 Worker 的字段。
|
||||
|
||||
纯模型阅读内容优先生成有身份标题的 Markdown;变量、映射、数据契约和 `present.v1` 保持结构化。
|
||||
|
||||
这解决两个具体问题:
|
||||
|
||||
1. `layer` 只回答“整份产物是否进入游玩”,不足以回答最终产物中哪些字段能进入。
|
||||
2. `mount` 只回答“给谁”,不足以回答“给这个 Worker 看哪些字段”。因此必须同时有分层、投影和分发。
|
||||
|
||||
|
||||
|
||||
### 6.3 验收
|
||||
|
||||
- 本局创作方案和 `seed_only` 规则不出现在游玩请求中。
|
||||
- 主世界层、转述、副 Worker 得到不同且最小的投影。
|
||||
- 请求日志中不存在创作回应、评分、追问和诊断字段。
|
||||
- 具体实例暂时全量稳定注入,不引入关键词检索或 RAG。
|
||||
- 扩展 `tests/dictate-play-bind.test.ts`,覆盖分层、字段剥离和按 Worker 分发。
|
||||
|
||||
### 6.4 维护方式分流与条件式编译工具节点
|
||||
|
||||
维护不固定走副 LLM。创作收口时先按复杂度选择一种运行策略:
|
||||
|
||||
1. `none`:没有跨轮可变内容,不挂维护能力。
|
||||
2. `inline_tool`:只有 1~2 个简单独立变量,或少量可由主世界层明确决定的标量更新。给主世界层挂原生 `update_state` tool call;正文仍输出普通 Markdown。
|
||||
3. `inline_structured`:模型不支持工具调用时,用 `{ body, state_ops }` 结构化整包,程序只显示 `body`。这会牺牲自然流式体验,因此不是首选。
|
||||
4. `auditor_bundle`:变量较多、存在交叉依赖,或同时维护拓扑、补实例、其它备忘录时,运行 `maintenance-compile` 并启用统一维护副 Worker。
|
||||
|
||||
数量不是唯一判据:一个带扩展规则的关系图也应走 `auditor_bundle`;三个彼此独立、更新完全确定的计数器仍可走程序或 tool call。
|
||||
|
||||
`update_state` 只接受设计阶段声明过的变量、操作和 revision。主世界层先决定本轮事实并调用工具,程序校验、提交并返回结果,再输出用户可见正文。相比正文隐藏标签,它能在生成过程中报告拒绝或冲突;相比把长正文包进 JSON,它保留自然文本与流式输出。当前 GM harness 已有 `chance` tool loop,可沿用同一机制。
|
||||
|
||||
API 能力选路复用 profile 已有探测结果,但使用独立策略:
|
||||
|
||||
- `tools=ok`:使用可选 tool call;不能用 `forcedTool` 强迫每轮更新,因为“本轮无变化”是正常结果。
|
||||
- `strictTool=ok`:动态工具带 `strict: true`;若供应商对实际 schema 拒绝,则降为普通 tool,并继续由本地校验全部参数。
|
||||
- tools 不可用而 `jsonSchema / forcedTool / jsonObject` 任一可用:通过现有 `completeStructured` 选路提交 `{ body, state_ops }`。
|
||||
- 只有纯文本能力:可请求 JSON 并严格解析、失败后重试;解析失败则不写状态。新实现不使用正文隐藏标签或正则维护变量。
|
||||
|
||||
动态结构随请求生成,不需要向 API 预注册。`inline_tool` 只用于少量变量,因此优先为每个变量生成一个类型明确、结构简单的小工具:工具名使用稳定安全 id,description 携带该变量的更新条件,parameters 只含该类型允许的 `set / delta`、值、`expected_rev` 与简短依据。避免为所有变量动态拼一棵带大量 `oneOf` 的复杂 schema。
|
||||
|
||||
现有能力探测验证协议是否支持 tools、强制 tool、strict tool 与各类 JSON 输出;它不能证明供应商支持任意复杂 schema 方言。因此实现时需增加一个与状态更新工具同形的代表性探测,运行请求仍保留“strict 被拒绝 → 非 strict + 本地校验”的降级。
|
||||
|
||||
`maintenance-compile` 是条件式自动工具节点,不调用 LLM 创作内容。仅在策略为 `auditor_bundle` 时扫描所有最终产物,把分散的跨轮维护要求编译成一份 `设计.维护合同`:
|
||||
|
||||
```json
|
||||
{
|
||||
"schema": "maintenance-manifest.v1",
|
||||
"targets": [
|
||||
{
|
||||
"id": "目标短 id",
|
||||
"kind": "table | topology | generation | memo",
|
||||
"state_ref": "变量.当前 | 运行.拓扑#id | …",
|
||||
"rule_refs": ["来源产物及字段路径"],
|
||||
"allowed_ops": ["set", "delta"],
|
||||
"timing": "pre_narrative | post_narrative",
|
||||
"writers": ["program", "gm", "auditor", "user"],
|
||||
"visibility": "public | hidden",
|
||||
"rules": ["给维护者执行的短规则"],
|
||||
"invariants": ["不可破坏的约束"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
主要来源包括:
|
||||
|
||||
- `设计.变量设计与更新规则` 的真值、写入源、更新时机、钳制和 side effects。
|
||||
- `设计.拓扑图谱#*` 的动态模式、允许操作、写入者和一致性约束。
|
||||
- `设计.生成规则#*` 中游玩期按需补实例的触发条件。
|
||||
- 后续明确声明“游玩期可修改”的其它备忘录;纯静态设定不进入维护合同。
|
||||
|
||||
编译规则:
|
||||
|
||||
- 不复制整份创作产物,只提取维护所需字段并保留来源引用。
|
||||
- `none / inline_tool / inline_structured` 不生成聚合合同,也不启用 `auditor`。
|
||||
- 一旦启用 `auditor`,应把本轮所有需要其判断的维护目标、当前状态和更新规则一次性投影给它,不能让它只看到变量而漏掉拓扑或补实例规则。
|
||||
- `inline_tool` 只给主世界层简短工具 schema,不注入完整维护文档。
|
||||
- `auditor_bundle` 下正文生成者不承担隐藏维护清单;需要从正文事实推断的更新统一由回合末 `auditor` 处理。必须在正文前完成的机械更新单列为 `pre_narrative`,由程序或专门的前置调用处理。
|
||||
|
||||
运行期由程序从维护合同生成精简的 `maintenance-context.v1`,只包含本轮相关目标、当前状态、用户输入和最终正文。旁观输出升级为 `maintain.v2`,至少可携带:
|
||||
|
||||
- `table_ops`
|
||||
- `topology_patches`
|
||||
- `generation_requests`
|
||||
- `memo_ops`
|
||||
- `notes`
|
||||
|
||||
旧 `maintain.v1` 继续作为仅含 `table_ops / need_generate` 的兼容输入。
|
||||
|
||||
验收:
|
||||
|
||||
- 单个“用户存款”变量只挂 `update_state`,不启动 `maintenance-compile` 或副 LLM。
|
||||
- 工具调用成功后只显示正文;工具参数、结果和隐藏状态不混入气泡。
|
||||
- 工具不可用时结构化整包可正确分离正文与状态;纯文本解析失败不写状态。
|
||||
- 运行期变量维护不依赖正则;SillyTavern 正则只作为外部预设导入兼容能力,不能写权威状态。
|
||||
- 同一局既有变量、关系图和按需实例时,旁观一次收到完整合同并可在一份维护包中分别更新。
|
||||
- 主世界层提示不再塞入仅供维护使用的完整更新规则。
|
||||
- 每项操作都有目标 id、来源规则和 revision;未知目标或越权操作被拒绝。
|
||||
- 多数无变化回合仍可返回空维护包,不为“检查过”制造写入。
|
||||
|
||||
|
||||
|
||||
## 7. 工作包 E · 壳参考库与隔离 iframe〔未开始〕
|
||||
|
||||
|
||||
|
||||
### 7.1 壳参考库
|
||||
|
||||
壳是正文组成读取的参考。创作时选一只最接近的,按本局启用的区删掉多余区并重排,生成一次前端后固定。游玩只输出各区内容,由程序装配。
|
||||
|
||||
可见区有 6 种。隐藏的变量更新不参与分类,也不进壳。
|
||||
|
||||
| 区 | 里面是什么 |
|
||||
| --- | --- |
|
||||
| 抬头 | 页首一行短内容,例如标题、日期、天气、地点;条目一多或当前值一长,改用数据卡 |
|
||||
| 正文 | 这一轮的主叙事 |
|
||||
| 小故事 | 论坛上的一段,或正文地点以外其他重要角色的一小段;同一轮只写一种 |
|
||||
| 数据卡 | 一组变量的卡片或表;只露出点名的列和行 |
|
||||
| 结构 | 地图、关系或要对着看的长表 |
|
||||
| 文字选项 | 几条下一步,只显示文字,始终直接可见,不放进标签或折叠 |
|
||||
|
||||
参考壳按主面来,写在 `skills/dialogue/world-simulator/shell-refs/`:
|
||||
|
||||
| 称呼 | id | 什么时候用 |
|
||||
| --- | --- | --- |
|
||||
| 阅读 | `reading` | 只有正文、页首一行和选项 |
|
||||
| 正文为主 | `dual-text` | 正文占满一栏。短文、地图用标签或折叠,点开才看。选项始终直接可见 |
|
||||
| 数据常驻 | `data-cards` | 数据一直铺开,不替换正文。左栏是数百字正文,右栏是另一路短内容。选项始终直接可见 |
|
||||
| 成稿 | `piece` | 一页一份能单独拿走的成品。内部小节在创作时按范例重排。选项在成稿下面 |
|
||||
|
||||
每只壳的备注只写两类:不替换的前端结构,以及可按本局用途替换的部位。备注不点名某一种应用场景。
|
||||
|
||||
`alongside`、`paged` 不再单独作为参考。另一篇长文不与正文左右对半。右栏是短内容时,可以和长正文并排。
|
||||
|
||||
美化用配色、字体层级、边框、间距,以及 CSS 或简单内联 SVG,按本次能写稳的程度完成。
|
||||
|
||||
### 7.2 正文组成产物
|
||||
|
||||
`设计.正文组成` 保存:
|
||||
|
||||
```json
|
||||
{
|
||||
"参考壳": ["按正文体验命名的参考 id"],
|
||||
"为何": "它如何服务本局的阅读体验",
|
||||
"数据契约": {
|
||||
"blocks": {
|
||||
"body": { "类型": "markdown", "职责": "主读正文" },
|
||||
"任意本局键": { "类型": "kv | list | markdown", "职责": "用途" }
|
||||
}
|
||||
},
|
||||
"frontend": {
|
||||
"html": "<div id=\"app\"></div>",
|
||||
"css": "",
|
||||
"js": "present.onData(render)"
|
||||
},
|
||||
"示例灌数": {
|
||||
"schema": "present.v1",
|
||||
"blocks": {}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- 创作期生成一次模板并固定。游玩每轮只生成内容块,由程序装配进该模板,不再生成壳。
|
||||
- 美化用配色、字体层级、边框、间距,以及 CSS 或简单内联 SVG,按本次能写稳的程度完成。
|
||||
- 壳只包含本局实际启用的区。参考壳多出来的区在创作时删掉并重排,不留到游玩再空着。
|
||||
- `blocks` 允许自定义键,前后端 normalization 不再吞掉未知键。
|
||||
- `html + css + js` 合计上限 60 KB。
|
||||
|
||||
|
||||
|
||||
### 7.3 最小隔离
|
||||
|
||||
新增 `web/present-frame.js`:
|
||||
|
||||
- 使用 `<iframe sandbox="allow-scripts">`,不加 `allow-same-origin`。
|
||||
- 禁止网络、表单、弹窗和顶层导航。
|
||||
- 只提供 `present.onData(cb)`、安全 Markdown 渲染和自动高度回报。
|
||||
- 不提供 `present.action`,也没有“直接发送”设置。
|
||||
- 模板报错或 3 秒内未 ready 时,回退为安全的纯文本/基础块渲染;不回退到已否决的旧七壳。
|
||||
|
||||
这里是低成本故障隔离,不把自创模板当作敌对第三方脚本设计复杂权限系统。
|
||||
|
||||
### 7.4 接入与验收
|
||||
|
||||
- 游玩终稿、开场候选、正文组成预览接入 iframe。
|
||||
- `SessionView` 下发 `presentationFrontend`。
|
||||
- 流式期间先显示纯文本,完成后灌入 iframe。
|
||||
- chat 型与长文阅读型参考必须肉眼显著不同。
|
||||
- 模板损坏时消息仍可读。
|
||||
- 测试:`tests/present-frame.test.ts`,覆盖 srcdoc、消息来源、ready/错误回退和自定义 blocks。
|
||||
|
||||
### 7.5 双呈现链
|
||||
|
||||
内容节点继续共用;只复制与显示形态直接耦合的三个节点:
|
||||
|
||||
- `回复呈现`:普通楼层版继续定义每轮消息区域;0 层版定义持久卡槽位及各槽内容规则。
|
||||
- `正文组成`:普通楼层版生成每条消息的前端;0 层版生成开场时一次建立的持久前端、Tab 与槽位渲染器。
|
||||
- `开场白与开场变量`:普通楼层版生成第一条消息;0 层版同时初始化持久前端和全部槽位初值。
|
||||
|
||||
当前实现:
|
||||
|
||||
- 已建立 `zero-layer-status / 设计.0层回复呈现`、`zero-layer-reply-format / 设计.0层正文组成`、`zero-layer-opening-setup / 设计.0层开场白与初态`。
|
||||
- `本局创作方案.presentation_mode` 必须在 `message / zero_layer` 中选一种;`items` 只包含所选模式的三项呈现能力,不把另一套写成“没有意义”占位。
|
||||
- 0 层更新合同使用 `zero-layer.card.v1` 与 `zero-layer.update.v1`;更新方式为 `replace / append / data`,并以 `base_revision` 原子提交。
|
||||
- 主世界层与转述不复制能力节点,只按呈现模式切换最终输出合同。
|
||||
|
||||
### 7.6 0 层卡、槽位更新与重 roll
|
||||
|
||||
0 层卡是开场白所在气泡内的持久前端,不是额外的上下文深度:
|
||||
|
||||
- 正文也是一个槽位,可与“公司指令”“监控表盘”“地图”等槽位并列。
|
||||
- 每轮助手结果仍写入内部对话历史,但前端不新增可见助手楼层;程序把本轮更新应用到 0 层卡。
|
||||
- 槽位声明更新方式:`replace`(整块替换)、`append`(受上限约束的槽内日志)、`data`(结构化数据驱动渲染)。
|
||||
- 0 层初始状态与每轮槽位更新均持久化;刷新、续作、run 快照和重 roll 后可恢复。
|
||||
- 规范路径使用结构化 `slot_updates`;新增受限正则提取/替换兼容层,用于导入 SillyTavern 标签和显式声明的 `<公司指令>…</公司指令>` 一类输出,不以任意正则直接修改权威状态。
|
||||
- iframe 只接收规范化后的槽位数据;LLM 不直接生成或修改运行中的 DOM。
|
||||
|
||||
“更多操作”菜单新增:
|
||||
|
||||
- **重 roll 本轮**:恢复最新一轮助手输出前的 checkpoint,重新生成该轮并重新应用槽位更新。
|
||||
- 暂不提供任意历史回退:缺少稳定用户锚点前,不暴露多步撤销。
|
||||
- 既有消息分支 / swipe 数据仍保留;0 层前端只显示当前活动分支重放后的状态。
|
||||
|
||||
验收:
|
||||
|
||||
- 连续多轮后仍只有一个可见 0 层前端,正文与其它槽位按合同更新。
|
||||
- 重 roll 不残留被废弃版本写入的槽位值。
|
||||
- 模板损坏时可查看本轮纯文本与结构化槽位数据。
|
||||
- 正则适配器失败不修改权威槽位状态。
|
||||
|
||||
|
||||
|
||||
## 8. 工作包 F · 库、本局化与提示归属〔部分完成〕
|
||||
|
||||
|
||||
|
||||
### 8.1 偏好库与文风库
|
||||
|
||||
- 偏好库:输入用法、推进节奏、是否代演玩家、停笔方式、常用禁区。
|
||||
- 文风库:遣词、段落节奏、笔墨焦点、写法档和示范片段。
|
||||
- 故事推进和叙事指南改为“选择 + 本局化”节点,各读取最相关的 1~2 项,解决冲突并记录微调。
|
||||
- 未选条目不进入后续提示。
|
||||
|
||||
|
||||
|
||||
### 8.2 规则归属
|
||||
|
||||
- 公共运行原则放总提示。
|
||||
- 配方案例只写有辨识度的方法和档位倾向。
|
||||
- 能力提示写使用边界、产物规格和评分。
|
||||
- 可程序校验的格式与依赖只在程序实现。
|
||||
- 删除面向用户的“落盘、收口、下一刀、模块”等内部流程腔。
|
||||
- 重复长例只留在一个权威位置,其余位置引用。
|
||||
|
||||
|
||||
|
||||
## 9. 非工具节点逐项审阅
|
||||
|
||||
本节只约束实施改造,不约束创作运行时的 `insert` 次数。
|
||||
|
||||
每份非工具节点提示词按以下流程处理:
|
||||
|
||||
1. 一次讨论一个节点。
|
||||
2. 修改前完整展示审阅表。
|
||||
3. 用户批准后才修改该节点的 `prompt.md`、直接关联目录声明和测试。
|
||||
4. 修改后展示实际变化与验收结果,再进入下一节点。
|
||||
|
||||
审阅表:
|
||||
|
||||
|
||||
| 项 | 内容 |
|
||||
| ------ | --------------------- |
|
||||
| 节点 | id / 中文名 |
|
||||
| 产物层 | intermediate / final |
|
||||
| 本局职责 | 为什么存在,服务创作还是游玩 |
|
||||
| 当前问题 | 现有提示、产物或消费链路的问题 |
|
||||
| 使用边界 | 何时有用、何时没有意义、与相邻能力如何分工 |
|
||||
| 产物形状 | 保留、删除、新增哪些字段 |
|
||||
| 评分规则 | 维度及 5 / 3 / 1 锚点 |
|
||||
| 用户可见交互 | 如何确认,何时根据缺口询问 |
|
||||
| 程序消费 | 谁读取,怎样投影,是否进入游玩 |
|
||||
| 待用户决定 | 尚有分歧的选项 |
|
||||
| 审阅结论 | 未审 / 批准 / 修改后再审 / 否决 |
|
||||
|
||||
|
||||
机械工具节点可以集中修改和测试,但不能借此改变内容节点的职责、语义或评分标准。
|
||||
|
||||
## 10. 工作包 G · 长程记忆、表维护与输入通道〔未开始〕
|
||||
|
||||
|
||||
|
||||
### 10.1 上下文拼装顺序
|
||||
|
||||
为尽量保留稳定前缀缓存,顺序调整为:
|
||||
|
||||
```text
|
||||
稳定系统提示
|
||||
→ 静态游玩投影
|
||||
→ 本局数据契约与工具说明
|
||||
→ 可变记忆表 / 变量当前值 / 最新总结
|
||||
→ 清空后保留的最近对话楼层
|
||||
→ 当前用户输入
|
||||
```
|
||||
|
||||
静态内容在前,可变表和对话放后。表格每轮变化时只破坏其后的缓存,不让整个稳定前缀失效。
|
||||
|
||||
### 10.2 副 Worker 维护
|
||||
|
||||
```text
|
||||
主世界层写出事实正文
|
||||
→ 转述 / 呈现(若启用)
|
||||
→ 副 Worker 根据已发生事实维护变量与记忆表
|
||||
→ 维护完成后允许下一轮用户输入
|
||||
```
|
||||
|
||||
- 默认启用独立副 Worker;多数轮允许空操作。
|
||||
- 更新期间禁用发送,在输入区显示“正在更新记忆表…”。
|
||||
- 成功后恢复输入;失败时保留旧表并提供重试,不让半成品进入下一轮。
|
||||
- 表很少且规则简单时,未来可显式选择主世界层隐藏维护段;本阶段不做自动阈值切换。
|
||||
- 正文组成只决定如何显示,不修改变量和记忆。
|
||||
|
||||
|
||||
|
||||
### 10.3 动态二维表
|
||||
|
||||
按本局需要建立少而承重的表:
|
||||
|
||||
- 事件账本:时间、地点、人物、事件、直接后果。
|
||||
- 承诺与债务:双方、内容、状态、到期或触发条件。
|
||||
- 人物关系:当前态度、公开关系、关键变化。
|
||||
- 线索与未决事项:内容、来源、可信度、解决状态。
|
||||
- 阶段事实:位置、目标、重要资源。
|
||||
|
||||
本局创作方案和变量设计决定需要哪些表,不要求每局全建。
|
||||
|
||||
### 10.4 大总结与清空
|
||||
|
||||
在输入框左侧“更多”菜单增加两个独立按钮:
|
||||
|
||||
**写大总结**
|
||||
|
||||
- 总结 Worker 读取当前保留历史和记忆表。
|
||||
- 把仍有后续价值的事实更新进动态表。
|
||||
- 在消息流最新处插入一层用户可见的总结层。
|
||||
- 不删除任何旧楼层。
|
||||
- 总结期间与表维护一样阻止发送;失败时不插入总结层,也不修改表。
|
||||
|
||||
**清空旧楼层**
|
||||
|
||||
- 只保留最新三层游玩消息,通常是最近用户输入、助手输出和总结层。
|
||||
- 不要求这三层具有固定角色;按时间保留最后三层。
|
||||
- 不删除动态表、源产物、静态游玩投影、开场实例或已保存快照。
|
||||
- 属于显式不可逆操作,执行前显示将删除的楼层范围并二次确认。
|
||||
|
||||
用户通常先写大总结、确认内容,再手动清空。两步不做自动绑定,允许少量信息重复。
|
||||
|
||||
### 10.5 固定输入语法
|
||||
|
||||
输入语法固定,不放进偏好库,也不允许按局反转:
|
||||
|
||||
- 裸写表示上帝视角,用于提出推进、镜头、节奏和改写要求。
|
||||
- 单井号成对包裹的 `#内容#` 表示角色行动。
|
||||
- 中文或英文引号包裹的内容表示角色说话。
|
||||
- 同一条输入可以混合三种内容。
|
||||
|
||||
示例:
|
||||
|
||||
```text
|
||||
下一段加快节奏。#我推门进去#“你们在谈什么?”
|
||||
```
|
||||
|
||||
程序拆成:
|
||||
|
||||
1. 上帝视角:`下一段加快节奏。`
|
||||
2. 角色行动:`我推门进去`
|
||||
3. 角色说话:`你们在谈什么?`
|
||||
|
||||
只有首尾配对的单井号才构成行动标记;未配对井号按普通裸写处理。`()`、`()`、`[]`、`【】` 不承担系统语义。
|
||||
|
||||
### 10.6 动态拓扑状态
|
||||
|
||||
`拓扑图谱` 用于“只生成一个、但普通无规则实例不足以表达”的结构对象,例如一张迷宫地图、一所学校的固定空间、一棵升级树或一条人物关系链。
|
||||
|
||||
它不是 0 层卡专属产物,而是开场后仍可修改的运行备忘录。普通楼层和 0 层卡共享同一份权威拓扑状态,只采用不同的显示入口。
|
||||
|
||||
创作期:
|
||||
|
||||
- 每个 `设计.拓扑图谱#id` 只定义一张图;能力可反复建立不同图。
|
||||
- 产物定义节点/连接字段、初始结构、扩展规则,以及 `static | record | expand` 动态模式。
|
||||
- 图数据与显示分离:拓扑节点只管权威结构,不要求提前实现渲染;是否显示、显示在哪里及采用何种形式,全部留给后续「正文组成」决定。
|
||||
|
||||
游玩期:
|
||||
|
||||
- 从设计图初始化 `运行.拓扑#id`,保存节点、连接、当前位置/当前焦点、发现状态与 `revision`。
|
||||
- 新增 `topology.patch.v1`:支持增改删节点与连接、移动当前位置、揭示隐藏内容;带 `base_revision`,整批原子校验,禁止悬空连接。
|
||||
- 设计产物声明允许的写入者和可编辑字段;程序、维护副 Worker 或用户都必须通过同一 patch 与 revision 冲突检查修改,不能直接覆盖整张图。
|
||||
- 主世界层负责写正文事实,不在用户可见正文中夹带拓扑维护块。由正文事实产生的探索、关系等变化,在回合末交给统一维护副 Worker 记录。
|
||||
- 确实必须先改状态再写正文的机械操作标为 `pre_narrative`;其余默认 `post_narrative`,不得为了拓扑一律增加前置调用。
|
||||
- 语义位置属于图真相;纯显示坐标、缩放和折叠状态另存 `运行.拓扑布局#id`。
|
||||
- 若「正文组成」决定展示,渲染器与壳一样在创作期生成一次并固定。游玩只把 `运行.拓扑` 的节点与连接灌入该渲染器出图。模型不输出 SVG,壳也不写死一份地点名单。普通楼层放在辅助区或标签页,0 层卡投影到 `data` 槽。未决定展示时只保存备忘录。
|
||||
|
||||
当前已有的“表格”只支持 `变量.当前` 的扁平键值格和逐格 revision,不支持多张任意表、节点/连接或拓扑 patch。因此不能直接把现有表格当成拓扑实现;可复用其 revision、用户编辑和冲突保护思路,但只先新增独立的拓扑存储与更新接口。显示入口等「正文组成」实际选用后再实现。
|
||||
|
||||
验收:
|
||||
|
||||
- 已访问节点和连接跨轮、刷新与 run 快照保持稳定。
|
||||
- 返回旧地点时只能沿已持久化连接,不重新生成冲突结构。
|
||||
- 旧 revision patch 被拒绝;非法 patch 不产生半次更新。
|
||||
- 固定学校地图保持稳定相对位置;动态迷宫只追加已探索部分。
|
||||
- 是否展示不影响存储;切换呈现模式不迁移或复制图数据。
|
||||
|
||||
### 10.7 随机数就是隐藏变量
|
||||
|
||||
随机数不另建表,也不另做协议。它是现有 `变量.当前` 里的行,玩家默认不可见,行名用「名称.随机数」,例如 `敏捷.随机数`、`攻击.随机数`。
|
||||
|
||||
这一行的值是还没用的数字。用掉第一个,就改写这一行;只剩 1 个时补到预备个数。程序出数,模型不编。重 roll 跟着这张表现有的检查点恢复。
|
||||
|
||||
与呈现模式的关系:
|
||||
|
||||
- **0 层卡**:创作与开场不写依赖随机数才能成立的玩法。开场落库时不应出现「没有随机数可用」的阻塞。
|
||||
- **普通楼层**:随机行在第一次正式游玩回复前由程序按合同填入;若开场未预填,第一次回复也会正常补齐,不要求开场模型写出具体骰面。
|
||||
|
||||
## 11. 能力提示同步清单
|
||||
|
||||
|
||||
| 文件 | 改动 |
|
||||
| --------------------------------------- | -------------------------------------------------- |
|
||||
| `modules/catalog.yaml` | 新增 `creation-plan`;增加默认 `layer`、`mount`;绑定库保持目录级声明 |
|
||||
| `modules/creation-plan/prompt.md` | 新建本局配方 + 能力分档产物 |
|
||||
| `modules/reply-format/prompt.md` | 读取新壳参考,生成前端与数据契约;删除渲染字段默认空 |
|
||||
| `modules/opening-setup/prompt.md` | 按正文组成的数据契约输出 `present.v1` |
|
||||
| `modules/narrative-guide/prompt.md` | 从文风库选 1~2 项并本局化 |
|
||||
| `modules/story-progression/prompt.md` | 从偏好库选 1~2 项,钉输入用法与停笔边界 |
|
||||
| `modules/variable-design/prompt.md` | 增加按需建立记忆表及维护方式 |
|
||||
| `modules/*/prompt.md` | 按 §9 逐节点确认,迁移 `score` fence |
|
||||
| `src/dictate/prompt.ts` | 方案驱动、读取工具、自然回复,删除固定路径与重复禁令 |
|
||||
| `src/dictate/insert-feedback.ts` | 只提供关注点、缺口和启发,不生成固定回复模板 |
|
||||
| 旧对话落盘配方 | 修订为本局创作方案可读取的案例 |
|
||||
| `src/dictate/play-bind.ts` | 编译游玩投影,跳过中间层,按 Worker 分发 |
|
||||
| `worker-templates/world-simulator.yaml` | 按固定输入语法解释裸写、`#行动#` 与引号;只读取自己的投影 |
|
||||
| `worker-templates/narrator.yaml` | 按数据契约输出各块,只读本局文风选择 |
|
||||
| `worker-templates/auditor.yaml` | 按统一维护合同处理变量、拓扑、补实例与可变备忘录;默认空操作 |
|
||||
|
||||
|
||||
写提示词时遵循 `prompt-writing` 技能口径。
|
||||
|
||||
## 12. 实施顺序
|
||||
|
||||
```text
|
||||
扫尾旧配方提示
|
||||
→ 本局创作方案 + 旧配方案例化
|
||||
→ read_module / read_library_entry
|
||||
→ layer / self_score / 自然回复
|
||||
→ 剩余非工具节点逐项审阅
|
||||
→ 按复杂度选择维护策略;仅 auditor_bundle 运行 maintenance-compile
|
||||
→ 游玩投影编译
|
||||
|
||||
正文体验分类讨论
|
||||
→ 壳参考库重建
|
||||
→ 源「回复呈现 / 正文组成 / 开场白」审阅完成
|
||||
→ 复制 0 层呈现链
|
||||
→ iframe、普通 present.v1 与 0 层 slot_updates
|
||||
→ 0 层卡持久化、正则兼容层与“重 roll 本轮”
|
||||
|
||||
动态表规格
|
||||
→ topology.patch.v1 与运行拓扑
|
||||
→ 副 Worker 默认维护与输入阻塞
|
||||
→ 上下文顺序调整
|
||||
→ 大总结 / 清空旧楼层
|
||||
→ 固定输入语法解析
|
||||
```
|
||||
|
||||
实施约束:
|
||||
|
||||
- 非工具节点严格执行 §9;程序基础设施可以按工作包集中实现。
|
||||
- 先完成游玩投影再允许新产物直接开玩,避免继续污染运行上下文。
|
||||
- 具体实例当前保持全量注入;本阶段不做 RAG。
|
||||
- 旧七壳在新参考库和安全回退渲染就绪后删除。
|
||||
|
||||
|
||||
|
||||
## 13. 决策状态
|
||||
|
||||
- [x] 本局创作方案引用 4 份已修订案例:快穿短局、数据化跑团体验、文本生成器、交互式长文生成器;删除“正文组成”案例。
|
||||
- [x] 删除独立的“用户需求”节点;偏好库、文风库改由相关节点绑定并通过 `read_library_entry` 按需读取。
|
||||
- [x] 生成规则:名称/用途→字段→池或提示→填写工作流;两份以上,或一份但字段多、需要先审字段与工作流时使用;一条字段很少的具名内容通常不建规则也不落实例;一产物一条规则;`只预生成` 为中间层。不因开场后是否继续生成而改道。
|
||||
- [x] 具体实例:按规则填一条记录,一份产物一条;写入多条时程序按记录拆开。无规则只留下用户点名的一条字段很少的内容。给人读的键和专名用中文,英文专名音译。
|
||||
- [x] 叙事指南:读文风库 1~2(最多 3)并本局化;产物用风格推荐与要求,少禁多做;推进仍归故事推进+偏好库。
|
||||
- [x] 故事推进:全局唯一输入用法;一次输出单轮目标字数及扩写/续写定义、占比和字数预算;含 OOC 处理策略;其它节点不重复定义篇幅。
|
||||
- [x] 实现机制:一个产物可含多条共同作用的稳定因果;用于推演事件与人物反应;缺失后果只作内部移除检验。
|
||||
- [x] 库种子:`seeds/libraries/*.json` 进 git;`libraries:restore` / `libraries:export`。
|
||||
- [x] 0 层卡:开场气泡内的持久前端;正文也是槽位,每轮就地更新;内部历史仍逐轮保存。
|
||||
- [x] 双呈现链:所有源节点审阅完成后,只复制 `回复呈现 / 正文组成 / 开场白与开场变量`;内容能力共用。
|
||||
- [x] 动态拓扑方向:一产物一张图,运行期用版本化 patch 持久扩展;它是呈现无关的可变备忘录,是否渲染完全由正文组成决定;节点提示已迁移。
|
||||
- [x] 维护分流:简单状态优先原生 `update_state` tool call;工具不可用时按 profile 能力走结构化整包;运行期变量维护不依赖正则;复杂维护才编译合同并启用副 LLM。
|
||||
- [x] 普通回复呈现:只定义实际启用的语义区域、出现条件、权威来源与内容要求;内联变量更新作为隐藏语义区,具体投递由 API 能力选路;篇幅只归故事推进。
|
||||
- [x] 随机数:现有变量表里的隐藏行,行名为「名称.随机数」;用掉一个就改行,只剩 1 个时补。
|
||||
- [x] 普通正文组成:把回复呈现的可见区域落成完整 HTML/CSS/JS、`present.v1` 数据契约和区域渲染;隐藏维护区不进 DOM,权威数据只由程序投影。
|
||||
- [x] 普通开场白:默认 1 条(用户要求时 2~3);全部保存,默认条开玩;每条自带初态与可选角色;不写随机数具体值。
|
||||
- [x] 0 层与随机:0 层不写依赖随机的玩法;随机行在第一次正式回复时由程序补齐。
|
||||
- [x] 0 层“重 roll 本轮”复用消息前 checkpoint;暂不实现任意历史回退。
|
||||
- [x] 壳按展示的区分类。参考壳三种称呼:阅读、正文为主、数据常驻。正文为主把短文和地图收进标签或折叠;数据常驻把数据一直摊开。选项始终直接可见。隐藏变量更新不进壳。
|
||||
- [ ] 各最终能力的 `mount` 和字段投影允许列表。
|
||||
- [ ] 文风库、偏好库条目 schema,以及临时条目回写全局库的权限。
|
||||
- [ ] “清空旧楼层”是否提供快照恢复;当前方案按不可逆二次确认处理。
|
||||
- [ ] 生图任务的独立输出契约和插入时机;不纳入本轮执行范围。
|
||||
|
||||
## 14. 剩余执行清单
|
||||
|
||||
按依赖顺序继续:
|
||||
|
||||
1. **完成内容节点审阅与迁移**
|
||||
- [x] `topology`:一产物一张图、扩展合同和五维评分。
|
||||
- [x] `variable-design`:真值、更新规则与维护复杂度判据。
|
||||
- [x] `status-bar`:普通楼层版语义区域、权威来源与隐藏更新区。
|
||||
- [x] `random-range`:变量表隐藏随机行、预备个数与补数规则。
|
||||
- [x] `reply-format`:普通楼层版前端、区域落点与数据契约。
|
||||
- [x] `opening-setup`:普通楼层开场候选、初态、角色与收口。
|
||||
|
||||
2. **完成共用运行工具审阅**
|
||||
- 运行时固定顺序:用户需要填写的内容节点全部完成 → 单独调用 `prepare_opening` → 信息可见范围 → 游玩执行方式 → 上下文排序 → 运行配置组装 → 下一步才生成并写入开场白。开场候选选定后只落初态并重投影,不首次创建运行配置。
|
||||
- [x] `variable-context / 信息可见范围`:程序从上游公开性、秘密与揭示声明编译谁能看到什么;不另造状态摘要。
|
||||
- [x] `worker-spec / 游玩执行方式`:程序整理前面已经确认的独立转述、复杂维护、秘密隔离或写手路径需求,只选择固定执行单元。
|
||||
- [x] `context-order / 上下文排序`:程序先按变化频率分组,再在同组内按重要性排列;稳定前缀在前,历史居中,当前状态与本轮输入靠后。
|
||||
- [x] `refine / 运行配置组装`:把执行方式、上下文排序、变量表与所选呈现合同打包成运行期读取的 `设计.worker集`;不承担正文润色。
|
||||
- [x] `narrative` 仅服务旧工序编排,不属于本轮对话落盘迁移。
|
||||
|
||||
3. **完成 0 层呈现链**
|
||||
- [x] 为 `回复呈现 / 正文组成 / 开场白与开场变量` 建 0 层版本,确定 id、artifact tag、目录声明和测试。
|
||||
- [x] 扩展本局创作方案,使普通楼层与 0 层卡互斥选型。
|
||||
- [x] 定义 `zero-layer.card.v1`、`zero-layer.update.v1`、`slot_updates`、三种更新方式和初始槽位状态。
|
||||
|
||||
4. **实现游玩投影与运行合同**
|
||||
- [ ] 把四个共用运行工具全部改为确定性程序调用;旧工序路径不得再用 LLM 猜执行槽、上下文顺序或最终运行配置。
|
||||
- [ ] 实现 `none / inline_tool / inline_structured / auditor_bundle` 维护策略选择,并复用 profile 能力探测。
|
||||
- [ ] 扩展代表性状态工具探测;实际 strict schema 被拒绝时降级为非 strict tool + 本地校验。
|
||||
- [ ] 为简单变量新增受白名单、操作范围和 revision 约束的 `update_state` GM tool。
|
||||
- [ ] 新增条件式 `maintenance-compile`,只为 `auditor_bundle` 产出 `maintenance-manifest.v1`。
|
||||
- [ ] 编译最终产物的字段白名单和 Worker 分发,剥离评分、追问与中间层。
|
||||
- [ ] 运行期生成精简 `maintenance-context.v1`,扩展 `maintain.v2` 的表、拓扑、补实例和备忘录操作。
|
||||
- [ ] 普通楼层输出 `present.v1`;0 层模式输出槽位更新。
|
||||
- [ ] 保持旧书、旧多规则产物和旧呈现数据可读。
|
||||
|
||||
5. **实现动态拓扑**
|
||||
- [ ] `topology.v1`、`topology.patch.v1`、revision 与原子校验。
|
||||
- [ ] 运行图、写入者权限、用户编辑,以及默认由回合末副 Worker 维护。
|
||||
- [ ] 本阶段不预做图谱 UI;后续正文组成选择展示时,再实现所需入口和渲染器。
|
||||
|
||||
6. **实现前端与重 roll**
|
||||
- [x] 重建壳参考库:`shell-refs/` 五只壳供正文组成与 0 层正文组成读取。隔离 iframe 与自定义 blocks 运行时尚未接入。
|
||||
- [ ] 0 层持久前端、Tab、槽位重放与正则兼容适配器。
|
||||
- [ ] “更多操作”增加“重 roll 本轮”;恢复 checkpoint 后重新生成,不提供任意历史回退。
|
||||
- [ ] 新前端和安全回退就绪后删除旧七壳。
|
||||
|
||||
7. **实现长程运行能力**
|
||||
- [ ] 动态二维表与副 Worker 维护阻塞。
|
||||
- [ ] 随机数行:在 `变量.当前` 增加「名称.随机数」隐藏行;用掉队首就改行;只剩 1 个时补数。
|
||||
- [ ] 稳定前缀、可变表和对话的上下文顺序。
|
||||
- [ ] 写大总结、清空旧楼层及二次确认。
|
||||
- [ ] 固定输入解析:裸写 OOC / 上帝视角、`#行动#`、引号对话。
|
||||
|
||||
8. **最终验收**
|
||||
- [ ] 普通楼层与 0 层模式端到端测试。
|
||||
- [ ] 重 roll、续作、run 快照、分支和拓扑持久化测试。
|
||||
- [ ] 全量测试、lint、构建基线与迁移文档。
|
||||
|
||||
+1
-1
@@ -135,7 +135,7 @@
|
||||
| `design-flow` | **创作 · 流程编排** | 以已选【配方】为起点,编排/增量修订工作流计划(可变 DAG) |
|
||||
| `design-step` | **创作 · 执行步骤** | 按工作流计划执行当前【技能】 |
|
||||
| `opening-generator` | **开局 · 开场白** | 无 `opening-setup` 时的兜底落库;有则选定开场白节点即收口 |
|
||||
| `opening-setup`(技能) | **开场白与开场变量** | 工序路径:1~多条候选 swipe。对话落盘用 `设计.开场白[#短码]` 增殖;游玩辅助 Tab「开场」点选 |
|
||||
| `opening-setup`(技能) | **开场白与开场变量** | 可增殖节点:每条 `设计.开场白#短码`。游玩时开场消息左右切换,选中的一条作为 0 层 |
|
||||
| `protagonist`(技能) | **主角设定** | `设计.主角设定[#短码]`;与开场白同短码即绑定该开局。挂在当前选用的用户角色卡下 |
|
||||
| `worker-spec`(技能) | **游玩拓扑** | 勾选固定槽;旧称「Worker 规格」,勿再当自由发明演员 |
|
||||
| `design-core` 等 | (已废弃) | 旧分步 skill;勿再调度 |
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
# 美学纲领与交互范式
|
||||
|
||||
> **技能标准形范例**(其它中段技能对齐本结构)。
|
||||
> 程序只切割下方 **fence 块**(`meta` / `opening` / `task` / …);`##` 标题仅供人读。
|
||||
> 写法:`docs/world-simulator-modules.md`;产物公共头:`docs/context-fragment-design.md`。
|
||||
|
||||
## meta
|
||||
|
||||
```meta
|
||||
@@ -11,81 +7,42 @@ name: 美学纲领与交互范式
|
||||
id: aesthetics-interaction
|
||||
artifact: 设计.美学纲领与交互范式
|
||||
declaration: >
|
||||
钉清用户会在意的体验承诺:站位、反复感到什么、轮转何时停。询问相近故合为一步。
|
||||
提取用户最想反复获得的核心体验,以及让它成立的背景与规则内核;同时钉清参与和停笔边界。
|
||||
when: |
|
||||
还不能回答这三件:代入还是旁观?最想反复感到什么?轮转到哪必须停等用户?
|
||||
无上游,通常作第一步。
|
||||
用户要的是叙事、角色扮演或持续互动,但还不能明确说出:最想反复获得什么体验,以及什么承重前提让它成立。
|
||||
用户主动改变核心体验、代入站位或关键权限时也应重写。
|
||||
when_not: |
|
||||
这三件已有等价定稿、用户未要求重谈 → 不重复排入。
|
||||
只改文风 → 「叙事指南」(工序编排:「叙事指南与故事推进」);只改推进或输入用法 → 「故事推进」(工序编排同上);只改每轮看什么 → 「回复呈现」;只改排版/壳 → 「正文组成」。
|
||||
只改舞台、规则、真值、槽位 → 对应下游技能。与「给玩家什么体验」无关 → 不排。
|
||||
本局不依赖体验契约的纯转换、资料整理或机械格式任务,可不使用。
|
||||
只改遣词和具体写法 → 叙事指南;只改推进与输入用法 → 故事推进;只改舞台详情或硬法则 → 对应能力。
|
||||
boundary: |
|
||||
本技能:体验是什么、怎么发生在用户身上、轮转怎么停。产出供后续编排阅读的开头节点。
|
||||
叙事指南:把感觉落成遣词/笔墨/禁忌(对话落盘)——本步只钉感觉与轮转。工序编排二者并入「叙事指南与故事推进」。
|
||||
故事推进:怎么往前、输入用法(对话落盘)——本步只钉感觉与轮转。
|
||||
舞台骨架 / 具体实例 / 生成规则:舞台与可执行规则——本步不定。
|
||||
实现机制 / 变量* / 回复呈现 / 正文组成 / 游玩拓扑 / 上下文投影排序 / 细化终稿:落到可运行结构——本步不定。
|
||||
本能力只交核心体验、体验成立的背景与规则内核、参与边界。
|
||||
背景与规则只提取承重内核,不扩写百科;舞台细节归舞台骨架,可执行硬法则归实现机制。
|
||||
feeds: design_only
|
||||
```
|
||||
|
||||
## opening
|
||||
|
||||
```opening
|
||||
先用几句话抓住你要的「变造世界」和体验核心(想到什么写什么,不必整齐):
|
||||
|
||||
1. 原型世界是什么?变在哪里?
|
||||
例:丧尸世界,但只有我不会被感染;现代都市,但灵异是真的;也可以只说「现代都市」,变点稍后补。
|
||||
|
||||
2. 你代入吗?
|
||||
· 完全代入(我就是那个人,TA 的遭遇≈我的遭遇)
|
||||
· 部分代入 / 跟着体验(有距离)
|
||||
· 操控或旁观(角色是棋子/观察对象)
|
||||
· 其它(请写明)
|
||||
|
||||
3. 最想反复感受到的是什么?
|
||||
一句话即可——权力、求生紧张、关系拉扯、解谜、荣耀、禁忌刺激……用你自己的词。
|
||||
先说你最想在这局里反复获得什么体验,以及你希望自己站在什么位置。可以直接给一个场景、关系或感觉,不必先整理设定。
|
||||
```
|
||||
|
||||
## task
|
||||
|
||||
```task
|
||||
与用户一起钉清一份「体验契约」:给玩家什么感受、如何参与、正文如何呈现、何时等待用户。
|
||||
从用户材料中提取一份体验契约,写入 `设计.美学纲领与交互范式`。产物只回答三件事:用户最想反复获得什么核心体验;什么最小背景或规则内核让它成立;模型与用户各自能决定什么、何时必须停笔。
|
||||
|
||||
产物必须是 **context-fragment.v1** JSON,写入 tag「设计.美学纲领与交互范式」。
|
||||
业务字段全部放在「正文」内(三柱为**对象**,不要写成一段散文);须含 schema / 技能 / brief;建议填 mount 与稳变(本步不写数字 order)。
|
||||
由运行时按模型能力做 schema/tool 投递,不要再口头包一层 outputs 字符串。
|
||||
|
||||
一次 design-step、一场对话、一次验收。参与方式与美学内容在同一步内交叉追问,不要先交「半份交互」再开「半份美学」。
|
||||
用户已答过追问:把答复写入正文三块,`追问.题目` 优先 `[]`;禁止在「交互范式」与「美学纲领」之间轮流补洞拖验收。核心体验与权限边界够用即可收。
|
||||
|
||||
若程序已发出默认问题:用户首答在「用户.worker答复」,开场白在「创作.能力开场白」。禁止重复同一开场;在首答基础上补洞、追问、产出。黑板「用户.需求」里已有的句子当作补充材料采纳,不代替本步引导作答。
|
||||
保留用户自己的重点和尺度。材料不足时少写,把真正影响核心体验或背景内核的缺口交给 `self_score.gap`;产物正文不放评分、诊断、追问或开放问题。
|
||||
```
|
||||
|
||||
## principles
|
||||
|
||||
```principles
|
||||
1. 核心是用户想要的体验;其余询问都是为了碰到这个核心。
|
||||
2. 信息不足时宁可少写,不要强行填充。
|
||||
3. 做好随时修改的准备;探测时大胆,用户可以说不。
|
||||
4. 完善设定时不要忘掉原本的核心体验。
|
||||
5. 用户常说不清想要什么 → 展示优于提问、大胆优于保守、去道德化/常态化、具体化;负面信号同样有效。
|
||||
6. 变造世界:先抓「原型 + 变在哪里」;核心体验往往来自变种,不要写成设定百科。
|
||||
7. 同一设定不同用户要的东西可以完全不同(角斗士:荣耀 / 自由 / 血与酒)——找到核心与禁忌,不是补全世界观。
|
||||
8. 本步是上下文片段,不是执行单元清单;禁止输出 Worker 列表或最终插入 order。
|
||||
```
|
||||
|
||||
## probe
|
||||
|
||||
```probe
|
||||
本步只钉三件:站位(代入 / 镜头看哪 / 爽从哪来)、核心感觉(反复感受到什么)、轮转边界(何时等用户)。
|
||||
|
||||
用户首句或黑板已有的直接采纳。默认问题已覆盖变造、代入、核心感觉。已答清的不重问。
|
||||
|
||||
缺其中一件:在产物「追问」写 1~2 点(建议选项 + 短场景);顶层 askUser 为 null。三件够验收:`追问.题目` 为 `[]`,内容维度等其余键写「未定」或省略。
|
||||
|
||||
用户讲人物、舞台、规则:只抽取能改这三件的一句写入正文,其余写入「开放问题」,本步不追问细节。
|
||||
|
||||
用两种站位或两种感觉的短场景让用户选或改,不追问开放式「你还想要什么」。
|
||||
1. 核心体验不是题材、剧情简介或情绪标签;要写用户如何参与、体验怎样反复发生。
|
||||
2. 背景与规则寻找最小承重内核:可以是关键关系、社会前提、世界规则或作用机制。删掉它会使核心体验不成立。
|
||||
3. 角色人文关系、描写焦点和文风信号可作为核心体验的组成材料,但不为评分硬拆成独立维度。
|
||||
4. 本步不扩写世界百科,不制定完整硬机制,不细化遣词句式,不决定故事推进方法。
|
||||
5. 参与边界写可执行权限:模型可补哪些反应与行动,哪些决定必须留给用户,何时停笔。
|
||||
6. 用户改口导致体验内核改变时整份重写,不在旧契约后追加互相冲突的补丁。
|
||||
```
|
||||
|
||||
## output
|
||||
@@ -94,136 +51,63 @@ feeds: design_only
|
||||
{
|
||||
"schema": "context-fragment.v1",
|
||||
"技能": "美学纲领与交互范式",
|
||||
"brief": "一句话点题:用户最想反复感受到什么",
|
||||
"brief": "一句话点出用户最想反复获得的核心体验",
|
||||
"体验锚定": {
|
||||
"站位": "用户与当前角色/作品的关系",
|
||||
"核心感觉": "最想反复获得的体验",
|
||||
"轮转停点": "何时必须停笔等待用户"
|
||||
},
|
||||
"mount": ["world-simulator", "narrator"],
|
||||
"稳变": "stable",
|
||||
"正文": {
|
||||
"设定逻辑": {
|
||||
"变造": { "原型": "…", "变点": "…" },
|
||||
"参与方式": {
|
||||
"用户与user关系": { "结论": "…", "完备度": "…%", "依据": "…" },
|
||||
"焦点位置": { "结论": "…", "完备度": "…%", "依据": "…" },
|
||||
"满足来源": { "结论": "…", "完备度": "…%", "依据": "…" }
|
||||
},
|
||||
"内容维度": {
|
||||
"核心感觉": { "完备度": "…%", "已知": "…", "待探": "无|…" },
|
||||
"人物与关系": { "完备度": "…%", "已知": "…", "待探": "无|…" },
|
||||
"身体与感官": { "完备度": "…%", "已知": "…", "待探": "无|…" },
|
||||
"背景与规则": { "完备度": "…%", "已知": "…", "待探": "无|…" },
|
||||
"意义与主题": { "完备度": "…%", "已知": "…", "待探": "无|…" }
|
||||
},
|
||||
"特殊概念": "无|短列",
|
||||
"区域化诊断": {
|
||||
"是否需要": false,
|
||||
"判断依据": "…",
|
||||
"区域识别": [],
|
||||
"边界类型": "无|空间|阶段|…",
|
||||
"转换机制": "…"
|
||||
},
|
||||
"复杂性识别": {
|
||||
"内容复杂性": "简单|可结构化复杂|…",
|
||||
"体验者位置复杂性": "简单|…",
|
||||
"判断依据": "…"
|
||||
},
|
||||
"完备度判断": {
|
||||
"核心状态": "核心明确|仍混沌|…",
|
||||
"判断依据": "…",
|
||||
"薄弱点": "…"
|
||||
}
|
||||
"核心体验": {
|
||||
"体验主张": "用户最想反复获得什么",
|
||||
"发生方式": "它通过哪些关系、处境、感官或选择反复发生",
|
||||
"角色人文": "关键人物关系中的权力、尊严、亲疏或人性张力;无则省略",
|
||||
"描写信号": "应把注意力放在哪里、呈现什么质感;具体写法交给叙事指南"
|
||||
},
|
||||
"交互范式": {
|
||||
"前置配置": {
|
||||
"用户与user关系": "…",
|
||||
"人称": "第二人称称「你」|…",
|
||||
"元叙事": "不得跳出故事直接问卷;决策用情节呈现|…"
|
||||
},
|
||||
"叙事视角": {
|
||||
"主要视角": "…",
|
||||
"视角切换": "禁止|例外…",
|
||||
"可写内心": "仅 user|…",
|
||||
"不呈现的信息": "…"
|
||||
},
|
||||
"描写权限": {
|
||||
"思考与抉择": "何时可写 / 何时必须停等用户",
|
||||
"言语": "…",
|
||||
"主动行动": "…",
|
||||
"情绪与身体反应": "…",
|
||||
"习惯与既有技能": "…",
|
||||
"外在状态": "…",
|
||||
"用户输入后": "只确认 / 补细节 / 连带后果…"
|
||||
},
|
||||
"后果与叙事控制": {
|
||||
"行为后果判定": "…",
|
||||
"世界对user的影响": "…",
|
||||
"时间与场景": "…",
|
||||
"引入新内容": "…",
|
||||
"设计困境": "…"
|
||||
},
|
||||
"区域差异": "无|按区域写权限差异(与设定逻辑.区域化诊断对齐)"
|
||||
"背景与规则": {
|
||||
"核心内核": "让体验成立的最小承重前提",
|
||||
"作用方式": "它如何约束人物、选择与后果",
|
||||
"非必要设定": "明确本步不展开的装饰信息;无则省略"
|
||||
},
|
||||
"美学纲领": {
|
||||
"体验内核": "给用户读的纲领正文(可多段;写清要反复感到什么、权力/关系如何成立)",
|
||||
"呈现要点": {
|
||||
"氛围基调": "…",
|
||||
"边界与尺度": "色情/暴力/血腥等:服务体验的用法与禁忌",
|
||||
"其它要点": ["权力日常化|反差|…"]
|
||||
}
|
||||
"参与边界": {
|
||||
"用户站位": "代入、部分代入、旁观、操控或本局自定义描述",
|
||||
"模型可代演": "可补写的行动、反应和既有习惯",
|
||||
"用户保留权": "不得替用户决定的事项",
|
||||
"世界反馈": "模型可直接裁定和推进的外部后果",
|
||||
"停笔时机": "必须等用户输入的明确时机"
|
||||
}
|
||||
},
|
||||
"自评": {
|
||||
"维度": [
|
||||
{ "名": "交互范式", "分数": 8, "说明": "权限边界是否锁住核心体验" },
|
||||
{ "名": "美学纲领", "分数": 8, "说明": "体验内核是否点题、有质感" },
|
||||
{ "名": "整体协调", "分数": 8, "说明": "范式是否在保护纲领" }
|
||||
],
|
||||
"薄弱点": "一句话"
|
||||
},
|
||||
"追问": {
|
||||
"导语": "为贴近你要的质感,还需确认:",
|
||||
"题目": [
|
||||
{
|
||||
"问": "…",
|
||||
"建议选项": ["选项A(可带短场景)", "选项B", "其它(请写明)"],
|
||||
"示例": "可选:一句示范句或短钩子"
|
||||
}
|
||||
]
|
||||
},
|
||||
"开放问题": []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
硬规则(公共三段 + 本技正文):
|
||||
1. 合法 JSON;必含 `schema` / `技能` / `brief` / `正文` / `自评` / `追问`(题目可 `[]`)。
|
||||
2. **公共三段**:`正文`=产物主体;`自评`=打分(**分数 0–10 十分制,禁止百分数**);`追问`=导语+题目(建议选项/示例)。禁止用散文代替这三段。
|
||||
3. **正文深度**:三柱(设定逻辑 / 交互范式 / 美学纲领)为对象。柱下到「条目」为止:条目是短字段、string[],或诊断三元组 `{结论, 依据, 完备度}` / `{已知, 待探, 完备度}`。诊断里的结论/依据/已知/待探必须是字符串。禁止把整柱写成一段 text;禁止在诊断或短字段里再套对象。
|
||||
4. **本技三柱**:`设定逻辑`(变造 + 参与方式 + 内容维度 + 区域/复杂性/完备度)+ `交互范式`(前置配置/视角/权限/后果)+ `美学纲领`(体验内核 + 呈现要点)。未知省略或「未定」,禁止编造百科。
|
||||
5. `mount` 建议挂主世界层+转述;**禁止**写数字 `order`。`稳变` 定稿后多为 `stable`。
|
||||
6. `追问.题目` 一次宜 1~3 题;有建议选项与示例;允许「其它」。已答清的不要再问。题目只写在「追问」,askUser 为 null。
|
||||
7. summary:`美学纲领与交互范式 · {brief 缩略}`。
|
||||
硬规则:
|
||||
1. 输出合法 JSON;保留 `体验锚定` 仅为兼容,不做计数门槛。
|
||||
2. `正文` 只含 `核心体验`、`背景与规则`、`参与边界`;未知字段省略,不用“完备度”或诊断对象占位。
|
||||
3. 不在产物正文写 `自评`、`追问`、`开放问题`、评分依据或用户回复。
|
||||
4. `mount` 固定包含 `world-simulator` 与 `narrator`;后续投影器分别裁剪字段。
|
||||
|
||||
## score
|
||||
|
||||
```score
|
||||
核心体验:
|
||||
- 5:准确提取用户最想反复获得的体验,写清用户站位、体验如何发生及何时停笔;后续能力可据此取舍。
|
||||
- 3:大方向正确,但仍偏题材或情绪概括,或体验如何反复发生尚不明确。
|
||||
- 1:误读用户重点,只有剧情简介、设定标签或泛化氛围。
|
||||
|
||||
背景与规则:
|
||||
- 5:找到让核心体验成立的最小内核——关键关系、背景前提或作用规则,并说明它怎样持续制造该体验。
|
||||
- 3:找到相关前提,但内核过宽、因果不清,或混有较多装饰设定。
|
||||
- 1:没找到承重内核;只有百科堆砌,或规则与核心体验无关。
|
||||
```
|
||||
|
||||
## checklist
|
||||
|
||||
```checklist
|
||||
- [ ] 含 schema / brief / 正文 / 自评 / 追问 三段外壳?
|
||||
- [ ] 正文三柱是对象(非一段散文)?设定逻辑里诊断有结论/依据/完备度?自评有交互/美学/协调维度?
|
||||
- [ ] 未写 order、未输出执行单元列表?
|
||||
- [ ] brief 能点题核心体验?变造/代入/满足来源清楚或已标未定?
|
||||
- [ ] 交互范式权限与美学纲领互相保护、不矛盾?
|
||||
- [ ] 追问只打真正薄弱点,带建议选项?已答过则题目为空、准备验收?
|
||||
- [ ] 有没有为「完整」强行填充百科或偏离用户核心?
|
||||
```
|
||||
|
||||
## examples
|
||||
|
||||
```examples
|
||||
好:
|
||||
- brief 点题「垄断生存资源的绝对掌控感」;正文三柱为对象(设定逻辑含诊断三元组、交互范式锁权限、美学纲领写体验内核);自评打分;追问带建议选项打薄弱点。
|
||||
- 内容质感可参考「权力孤岛」类长描写,但必须落在上述 JSON 键里,不要输出 XML/自定义标签壳,也不要把三柱收成一段 text。
|
||||
|
||||
坏:
|
||||
- 只有散文纲领,无 JSON / 无自评 / 无追问结构
|
||||
- 三柱是 sections[].text 一段话,验收卡无法切开诊断与契约
|
||||
- 百科式背景堆砌,无满足来源与权限边界
|
||||
- 输出 workers[] 或 order 表
|
||||
- 追问空洞「你想要什么体验?」无选项
|
||||
- [ ] 核心体验写的是用户反复获得什么,而非题材或剧情简介?
|
||||
- [ ] 背景与规则是删掉就会破坏体验的最小内核?
|
||||
- [ ] 参与边界明确模型可代演、用户保留权和停笔时机?
|
||||
- [ ] 未扩写舞台百科、硬机制、具体文风或推进方案?
|
||||
- [ ] 产物正文不含评分、诊断、追问和开放问题?
|
||||
```
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
# name = 固定中文名(流程 JSON 的 name;同能力可多次时靠 step.id 区分)
|
||||
# declaration = 给人看/兜底的短声明(规划选型以 prompt.md ```meta 为准并覆盖本表)
|
||||
# artifact = 执行期产物 tag(程序映射;不写进流程 JSON)
|
||||
# layer = 默认产物层(intermediate / final)
|
||||
# mount = 默认游玩挂载目标;具体字段仍由投影器裁剪
|
||||
# repeatable = true 时允许同能力多次**新建**编入增量 DAG(如生成规则、具体实例)。
|
||||
# 回头修改已完成节点不是 repeatable:用户在图上点该节点重进,不要排 mode=revise。
|
||||
# closer = true 时为创作终节点:选定后程序落库、关 DAG、保存定稿
|
||||
@@ -29,9 +31,21 @@
|
||||
# 〔先验产物〕例外:params 是规划产物,可空;「写什么」在步内钉。
|
||||
|
||||
modules:
|
||||
# —— 本局决策(仅对话落盘) ——
|
||||
- id: creation-plan
|
||||
name: 本局创作方案
|
||||
intake: dictate
|
||||
layer: intermediate
|
||||
declaration: >-
|
||||
先抽出用户要的核心体验,再主动找出有帮助的内容能力并分档,
|
||||
形成本局专用创作方法;指导后续取舍,但不规定固定流程
|
||||
artifact: 设计.本局创作方案
|
||||
|
||||
# —— 体验契约 ——
|
||||
- id: aesthetics-interaction
|
||||
name: 美学纲领与交互范式
|
||||
layer: final
|
||||
mount: [world-simulator, narrator]
|
||||
declaration: >-
|
||||
钉清用户会在意的体验承诺:站位、反复感到什么、轮转何时停。
|
||||
询问相近故合为一步。
|
||||
@@ -40,63 +54,64 @@ modules:
|
||||
- id: protagonist
|
||||
name: 主角设定
|
||||
repeatable: true
|
||||
layer: final
|
||||
mount: [world-simulator]
|
||||
declaration: >-
|
||||
钉清 @玩家 扮演的那个人:名字、背景、特殊设定(身份/金手指等)。
|
||||
代入有名片时排本步;可 设计.主角设定#短码 与开场白同槽绑定;禁止把名字/简介写进开场白正文。
|
||||
钉清 @玩家 扮演的那个人:名字、背景、特殊设定(身份/金手指等);
|
||||
自动写入时加性格。代入有名片时排本步;可 设计.主角设定#短码 与开场白同槽绑定;禁止把名字/简介写进开场白正文。
|
||||
artifact: 设计.主角设定
|
||||
|
||||
- id: user-requirements
|
||||
name: 用户需求
|
||||
libraries: [preferences]
|
||||
declaration: >-
|
||||
把跨局偏好库里对本局生效的硬约束(禁区/纠偏/默认取向)落成可执行条目;
|
||||
不重做文风问卷,也不钉体验站位
|
||||
artifact: 设计.用户需求
|
||||
|
||||
# —— 世界与内容 ——
|
||||
- id: world-blueprint
|
||||
name: 舞台骨架
|
||||
layer: final
|
||||
mount: [world-simulator]
|
||||
declaration: >-
|
||||
本局舞台备案:划出玩家实际游玩会换区活动的主要区域并写细;
|
||||
尺度与详细度随体验变。代入后几乎不移动、只在一处待着则不排
|
||||
保存会在游玩中反复影响世界反馈的外部舞台:
|
||||
熟悉基底、必要变造、核心运转和主要活动场所
|
||||
artifact: 设计.舞台骨架
|
||||
|
||||
- id: generation-rules
|
||||
name: 生成规则
|
||||
kind: prior-artifact
|
||||
repeatable: true
|
||||
layer: final
|
||||
mount: [world-simulator, auditor]
|
||||
declaration: >-
|
||||
要精准格式,或本局原创导致知识中无可参考对象、或难以在无指导情况下生成足够可信内容且故事里还要生成时:写出该对象族怎么生成;
|
||||
创意字段用方向池,不堆枚举词;不填最终实例;可按对象反复
|
||||
为需要统一生成的一类对象写出合同:名称、用途、字段、池或提示、填写工作流;
|
||||
不填最终实例;可按对象反复。两份以上,或一份但字段多、需要先审工作流时使用
|
||||
artifact: 设计.生成规则
|
||||
params:
|
||||
- key: target
|
||||
label: 生成对象
|
||||
hint: 规划字段;编排器已知写哪一类就填。空则步内钉
|
||||
- key: rule_id
|
||||
label: 规则 id
|
||||
hint: 英文 kebab-case,须与 target 对应(女租客→female-tenants);改对象时禁止沿用旧 id
|
||||
label: 规则名
|
||||
hint: 中文短名,须与 target 对应(女租客→女租客);改对象时禁止沿用旧名
|
||||
- key: lifecycle_intent
|
||||
label: 生命周期意图
|
||||
hint: runtime_only | seed_only | seed_and_runtime;不明则步内钉
|
||||
hint: 仅游玩期 | 只预生成 | 预生成并游玩期;不明则步内钉;只预生成=中间层,其余=最终层
|
||||
|
||||
- id: concrete-instances
|
||||
name: 具体实例
|
||||
kind: prior-artifact
|
||||
repeatable: true
|
||||
layer: final
|
||||
mount: [world-simulator]
|
||||
declaration: >-
|
||||
已有预生成规则时:按该规则原样交出 records;不设计、不改合同;可按规则/批次反复
|
||||
按已有生成规则填写一条记录,单独落成一份产物;
|
||||
没有规则时只留下用户点名的一条字段很少的具名内容
|
||||
artifact: 设计.具体实例
|
||||
params:
|
||||
- key: rule_id
|
||||
label: 调用规则
|
||||
hint: 规划字段;须来自已验收生成规则中 seed_only/seed_and_runtime 的 rule_id。空则步内钉
|
||||
hint: 规划字段;填已有生成规则的中文规则名。无规则的单条极简路径可空
|
||||
- key: batch_goal
|
||||
label: 批次目标
|
||||
hint: 本批要锚定什么(如开局重要NPC)
|
||||
label: 这一条
|
||||
hint: 本条要锚定什么(如开局某位重要人物)
|
||||
- key: count
|
||||
label: 数量
|
||||
hint: 同批时本批条数;规则「一次生成」为单条时忽略(本步只交 1 条)
|
||||
hint: 每条实例一份产物;一次写入多条记录时程序按条拆开
|
||||
|
||||
- id: narrative
|
||||
name: 叙事指南与故事推进
|
||||
@@ -111,99 +126,155 @@ modules:
|
||||
name: 叙事指南
|
||||
intake: dictate
|
||||
libraries: [style-packs]
|
||||
layer: final
|
||||
mount: [narrator, world-simulator]
|
||||
declaration: >-
|
||||
美学已有方向时:把感觉落成可执行的遣词、笔墨落点、禁忌与写法档;
|
||||
不重做体验问卷,也不钉故事怎么往前
|
||||
从文风库选取本局写法(默认 1~2,确需可到 3),
|
||||
落成风格推荐与要求、笔墨落点、少量禁忌与可选写法档;不钉推进
|
||||
artifact: 设计.叙事指南
|
||||
|
||||
- id: story-progression
|
||||
name: 故事推进
|
||||
intake: dictate
|
||||
libraries: [preferences]
|
||||
layer: final
|
||||
mount: [world-simulator, narrator]
|
||||
declaration: >-
|
||||
美学已有方向时:钉故事怎么往前、用户输入用法(大纲扩写或扮演停笔);
|
||||
不重做文风问卷
|
||||
从偏好库选取约束,只选定一种用户输入用法,
|
||||
并写清扩写/续写定义、比例与 OOC 处理;不写遣词
|
||||
artifact: 设计.故事推进
|
||||
|
||||
# —— 机制与数据 ——
|
||||
- id: mechanism
|
||||
name: 实现机制
|
||||
layer: final
|
||||
mount: [world-simulator]
|
||||
declaration: >-
|
||||
整体感觉已钉,故事里有必须当作既成事实的变造法则时:
|
||||
点出它们如何约束故事,不解释为何存在
|
||||
保存世界中稳定成立的因果法则,供主世界层推演事件后果、
|
||||
剧情变化和人物反应;一次可写多条共同作用的机制
|
||||
artifact: 设计.实现机制
|
||||
|
||||
- id: topology
|
||||
name: 拓扑图谱
|
||||
intake: dictate
|
||||
layer: final
|
||||
mount: [world-simulator, auditor]
|
||||
repeatable: true
|
||||
declaration: >-
|
||||
必须生成、但不适合走「生成规则→具体实例」时:用拓扑结构写出来
|
||||
(升级路径、地图、人物关系等)
|
||||
为一个独有但结构复杂的对象定义节点、连接、初始图与开场后的维护规则;
|
||||
一份产物一张图,可按地图、关系链、升级树等对象反复
|
||||
artifact: 设计.拓扑图谱
|
||||
|
||||
- id: variable-design
|
||||
name: 变量设计与更新规则
|
||||
intake: dictate
|
||||
layer: final
|
||||
mount: [world-simulator, auditor]
|
||||
declaration: >-
|
||||
体验必须跨轮记住少数状态时:钉真值、谁可写、如何变、维护语句,
|
||||
以及按值换文案的映射索引
|
||||
定义必须跨轮保存的最少真值、合法更新条件、权限与派生映射,
|
||||
供程序选择简单状态工具或复杂维护副 Worker
|
||||
artifact: 设计.变量设计与更新规则
|
||||
|
||||
- id: variable-context
|
||||
name: 变量控制上下文
|
||||
name: 信息可见范围
|
||||
declaration: >-
|
||||
已有真值或映射时:钉谁看见什么(主世界层/转述/旁观)、未解锁不注入;
|
||||
无信息差则可不排
|
||||
程序根据变量、拓扑和秘密声明整理谁能看到什么、何时可以看到;
|
||||
无特殊信息差时直接采用默认范围
|
||||
artifact: 设计.变量控制上下文
|
||||
|
||||
# —— 呈现 ——
|
||||
- id: status-bar
|
||||
name: 回复呈现
|
||||
intake: dictate
|
||||
layer: final
|
||||
mount: [world-simulator, narrator]
|
||||
declaration: >-
|
||||
几乎每局都要:钉每轮终稿的必选/可选区域,以及各区域的简单内容规则
|
||||
(字数、段落、选项怎么组成等);不排版
|
||||
定义普通楼层每轮实际启用的语义区域、出现条件、权威来源与内容要求;
|
||||
可含隐藏更新语义区,不决定前端排版和渲染
|
||||
artifact: 设计.监控栏
|
||||
|
||||
- id: zero-layer-status
|
||||
name: 0层回复呈现
|
||||
intake: dictate
|
||||
layer: final
|
||||
mount: [world-simulator, narrator]
|
||||
declaration: >-
|
||||
定义持久 0 层卡实际启用的语义槽位、更新触发、更新方式与权威来源;
|
||||
不决定 HTML 和排版
|
||||
artifact: 设计.0层回复呈现
|
||||
|
||||
- id: random-range
|
||||
name: 随机范围整理
|
||||
intake: dictate
|
||||
layer: final
|
||||
mount: [world-simulator]
|
||||
declaration: >-
|
||||
上游已有检定/对抗/掷骰需求时:汇总全部要用的随机项与取值范围(含表达式),
|
||||
供程序每轮插入备用随机数;排在正文组成之前
|
||||
把已定的检定和抽签收成「变量.当前」里的隐藏行,行名用「名称.随机数」
|
||||
artifact: 设计.随机范围整理
|
||||
|
||||
- id: reply-format
|
||||
name: 正文组成
|
||||
intake: dictate
|
||||
layer: final
|
||||
mount: [world-simulator, narrator]
|
||||
declaration: >-
|
||||
把「回复呈现」已钉的内容展示给用户:选壳、美化排版,可写 CSS/HTML/JS;
|
||||
排在回复呈现之后、开场白之前;有随机表则一并接到隐藏备用区
|
||||
把普通楼层「回复呈现」的语义区域落成每条消息使用的前端模板:
|
||||
定义区域位置、HTML/CSS/JS、数据绑定与特殊渲染
|
||||
artifact: 设计.正文组成
|
||||
|
||||
- id: zero-layer-reply-format
|
||||
name: 0层正文组成
|
||||
intake: dictate
|
||||
layer: final
|
||||
mount: [world-simulator, narrator]
|
||||
declaration: >-
|
||||
把 0 层语义槽位落成跨轮持久前端,定义固定外壳、槽位位置、
|
||||
Tab 与结构化 slot_updates 渲染合同
|
||||
artifact: 设计.0层正文组成
|
||||
|
||||
# —— 开局(创作偏晚) ——
|
||||
- id: opening-setup
|
||||
name: 开场白与开场变量
|
||||
closer: true
|
||||
repeatable: true
|
||||
intake: dictate
|
||||
layer: final
|
||||
mount: [world-simulator, narrator]
|
||||
declaration: >-
|
||||
创作终节点:写出 1~多条开场(每条 = meta 用户角色 + 正文;须含字面 @玩家)并钉同真相初值;
|
||||
选定后程序落库、结束创作并保存定稿
|
||||
普通楼层创作终节点,且可增殖:每条开场各占一个节点(设计.开场白#场景短码)。
|
||||
默认 1 条;还要入口就再增殖。全部保存。游玩时左右切换,选中的一条作为 0 层
|
||||
artifact: 设计.开场白与开场变量
|
||||
|
||||
- id: zero-layer-opening-setup
|
||||
name: 0层开场白与初态
|
||||
closer: true
|
||||
repeatable: true
|
||||
intake: dictate
|
||||
layer: final
|
||||
mount: [world-simulator, narrator]
|
||||
declaration: >-
|
||||
0 层创作终节点,且可增殖:每条开场各占一个节点(设计.0层开场白与初态#场景短码),
|
||||
自带持久卡第一屏、槽位初值和同真相运行初态。游玩时左右切换,选中的一条作为 0 层
|
||||
artifact: 设计.0层开场白与初态
|
||||
|
||||
# —— 收成 / 钉声明(共用;世界模拟器按需) ——
|
||||
- id: worker-spec
|
||||
name: 游玩拓扑
|
||||
name: 游玩执行方式
|
||||
repeatable: false
|
||||
declaration: >-
|
||||
不能吃默认槽时才勾选:主世界层必开;有变量/表才开旁观;转述默认开;
|
||||
强秘密才开视角;写手则大纲+章节;禁止发明执行单元与机遇槽
|
||||
程序根据前面已经确认的写作、维护和秘密隔离需求,整理本局需要启用的固定执行单元
|
||||
artifact: 设计.worker规格
|
||||
|
||||
- id: context-order
|
||||
name: 上下文投影排序
|
||||
name: 上下文排序
|
||||
repeatable: false
|
||||
auto: true
|
||||
declaration: >-
|
||||
收成前给已启用槽排扁平投影序(含对话.历史);
|
||||
不发明槽、不重写长文、不演情节选择题
|
||||
程序按变化频率与重要性排列各执行单元的上下文,并选择完整、摘要或字段投影
|
||||
artifact: 设计.上下文投影排序
|
||||
|
||||
- id: refine
|
||||
name: 细化终稿
|
||||
name: 运行配置组装
|
||||
declaration: >-
|
||||
按已勾选槽(或默认槽)与投影排序表收成运行规格;不发明新执行单元
|
||||
程序把执行方式、上下文排序、变量表和输出合同组装成可直接开玩的运行配置
|
||||
artifact: 设计.worker集
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
# 具体实例
|
||||
|
||||
> 技能文档。程序只切割下方 **fence 块**;`##` 标题仅供人读。
|
||||
> 产物外壳:`docs/context-fragment-design.md`。
|
||||
> 本步极简:严格按已验收的「生成规则」执行,交出实际产物。
|
||||
|
||||
## meta
|
||||
|
||||
```meta
|
||||
@@ -11,72 +7,47 @@ name: 具体实例
|
||||
id: concrete-instances
|
||||
artifact: 设计.具体实例
|
||||
declaration: >
|
||||
已有预生成规则时:按该规则原样交出 records;不设计、不改合同;可按规则/批次反复
|
||||
按已有生成规则填写一条记录,单独落成一份产物。没有规则时,只留下用户点名的一条字段很少的具名内容。
|
||||
when: |
|
||||
「设计.生成规则」里已有 `seed_only` 或 `seed_and_runtime` 的规则,且现在需要实际名片/记录(开局 NPC、种子事件等)。
|
||||
已有生成规则,要按该规则写入一条种子或开局记录时使用。每条记录单独一份产物。
|
||||
还没有规则时,只在用户明确要求留下一条字段很少的具名内容时使用。
|
||||
when_not: |
|
||||
尚无规则或规则不完整 → 先「生成规则」。想改格式/方法/池 → 回「生成规则」,本步不改合同。
|
||||
规则是 `runtime_only` → 不排,游玩期再生成。
|
||||
只要规则不要预写实例 → 不排。
|
||||
还没有规则,而对象有两份以上,或字段较多、需要先审字段和填写工作流 → 先写生成规则。
|
||||
内容不重要、游玩时现写就够 → 不落本产物。
|
||||
一张有节点和连接的结构图 → 拓扑图谱。
|
||||
规则写明创作期不预填、只在游玩期生成 → 本步不写记录。
|
||||
boundary: |
|
||||
本技能只做一件事:按指定规则生成 records。规则说怎么写、什么格式、如何用池、填到多厚,就照做。
|
||||
生成规则:合同制定方——本步不重论证、不修改。
|
||||
本能力只交一条记录。有规则则严格按字段、池或提示、填写工作流填写;无规则则只写用户给出的少量简单字段。
|
||||
不重议合同,不写世界百科,不改变量公式。一次写入多条时,程序按记录拆成多份产物。
|
||||
feeds: gm
|
||||
```
|
||||
|
||||
## opening
|
||||
|
||||
```opening
|
||||
先认定本批按哪条规则生成:
|
||||
- 若【本步对象】已有 rule_id,以此为起点;要换规则就在本步改(须仍是已验收的预生成规则)。
|
||||
- 若规划为空,从已验收生成规则里选出一条预生成规则。
|
||||
|
||||
读规则「一次生成」:同批则本步一次交齐互有关系的多条;单条则本步只交 1 条。有批次目标可一句带过。
|
||||
先说这条记录是谁、依哪条生成规则。没有规则、又只是用户点名的一条简单内容,直接写这一条。
|
||||
```
|
||||
|
||||
## task
|
||||
|
||||
```task
|
||||
你正在执行「具体实例」。产物必须是 **context-fragment.v1** JSON,写入「设计.具体实例」。
|
||||
写出一条具体实例,写入 `设计.具体实例#中文名`。一份产物只含一条记录。用户一次给出多条时,每条各写一份;若交来的是记录数组,程序会按条拆开。
|
||||
|
||||
本步 = 规则执行器。禁止重议对象族、生命周期、schema 或描写方法。
|
||||
有生成规则时:认定规则的中文名,按规则的字段、池或提示、填写工作流生成这一条;用户已写明的专名与结果只扩写不改写,未写字段从已有材料往下补满。无规则且只是一条字段很少的具名内容时:直接写用户给出的字段。
|
||||
|
||||
本步是〔先验产物〕:先定调用哪条规则,再生成 records。读【本步对象】与【原型建议】;在本步钉「写什么」。
|
||||
|
||||
执行顺序:
|
||||
1. 认定 `rule_id`:规划有则采用(用户要换则以最新为准,须仍是已验收的预生成规则);空则先从依赖产物里选出。不要停下来让编排器补参。非预生成生命周期或缺信息 → 停止并指出缺口。
|
||||
2. 条数:先读规则「一次生成」。
|
||||
- `单条`:本批 `records` 恰好 1 条;规划 `count` 即使写成多条也忽略。
|
||||
- `同批`:一次交齐互有关系的多条。条数:规划 `count` 有数字用数字,否则用规则「数量」。生成时先对齐「一次生成.关系」(各条如何互指、对立、同场),再填各条字段。
|
||||
`batch_goal` 只作不违反规则的筛选。缺「一次生成」时按「数量」/ `count` 交 `records[]`(可多条)。
|
||||
3. 完全按规则的「生成与描写」+「产物格式」+「池」+「分级表现」生成 `records`(始终是数组,单条也是长度为 1 的数组;键=schema 顶层字段)。
|
||||
池的用法以规则 `池型` 为准:
|
||||
- **方向**:以方向为主。粒度是词条/方向时,字段值可以就是方向名。粒度是句子时,按该方向写开(方向名可作标签,后面仍要达到地板)。方向说明里的专名不是选项库,不要按专名点菜。
|
||||
- **元素**:从 `条目` 里抽取或必选,写入绑定字段。
|
||||
有 **配方**:按配方从各组件取方向,拼进目标字段那一格。
|
||||
有 **分级表现**:实例只填档位短值;不要把整段表象复制进 record,除非 schema 单独设了表象字段。叙事时按规则展开。
|
||||
无池的描写字段按 `粒度`/`地板`/`表现`/`工作流` 写到够厚。
|
||||
4. 按规则自检(含地板:句子级不得只用一个词交差);不合格内部重做。已有产物则追加批次(除非用户要求替换)。
|
||||
5. 输出 JSON;追问通常 `[]`。
|
||||
|
||||
summary:`具体实例 · {对象} · {n}条`
|
||||
材料足够便直接成稿。缺的是合同本身时,先回到生成规则,不在这里补合同。产物正文不放评分、诊断、追问或开放问题。
|
||||
```
|
||||
|
||||
## principles
|
||||
|
||||
```principles
|
||||
1. 只按规则来:方法、配方、表现、分级表现、格式、池型、数量、一次生成、约束、地板——规则有什么用什么;没有的不发明。
|
||||
2. 同批先钉记录间关系再填各条;单条不虚构同场关系。始终交 `records[]`。
|
||||
3. 只交 records:单层 JSON;不交解释、草稿、schema 副本、整池粘贴。方向以方向为主;说明里的专名不是选项。
|
||||
4. 句子级描写必须达到该字段 `地板`;词条/档位/数字可以短。句子级不要只丢一个词交差;方向名可以当选项或标签。
|
||||
5. 要改合同 → 回「生成规则」,本步不加字段、不改枚举、不改池型。
|
||||
```
|
||||
|
||||
## probe
|
||||
|
||||
```probe
|
||||
通常不追问。仅当 contextual 数量算不出、或同批「关系」写了但本批对象对不齐时,问一句。
|
||||
用户要 schema 外内容 → 提示回「生成规则」。
|
||||
1. 有规则:字段名、取值方式、工作流阶段与依赖都以规则为准;不增删字段,不改池。
|
||||
2. 池选取按合理度选择,不是程序抽签;提示约束按那句话的范围写开。
|
||||
3. 按填写工作流推进:先骨架后血肉;阶段依赖与自检能核对的,生成后要能过。
|
||||
4. 一份产物一条记录。多条各自成稿,用中文名称区分。
|
||||
5. 无规则只写用户点名的那一条,字段保持很少;不借机发明完整人设合同。
|
||||
6. 用户已写明的专名、关系、结果保持原样,只做厚度扩写;未写字段从这些材料延伸。
|
||||
7. 给人读的键和专名用中文。材料里的英文名音译成中文,不把英文原名留在字段值里。外壳的 schema、brief、mount 保持既有键。
|
||||
```
|
||||
|
||||
## output
|
||||
@@ -85,63 +56,42 @@ summary:`具体实例 · {对象} · {n}条`
|
||||
{
|
||||
"schema": "context-fragment.v1",
|
||||
"技能": "具体实例",
|
||||
"brief": "rule_id + 本批条数",
|
||||
"brief": "这一条是谁、依哪条规则",
|
||||
"mount": ["world-simulator"],
|
||||
"稳变": "stable",
|
||||
"正文": {
|
||||
"rule_id": "与本步认定一致(规划有则对齐)",
|
||||
"本批数量": 3,
|
||||
"批次条件": ["规划 batch_goal 等;无则 []"],
|
||||
"batch_id": "英文 kebab-case",
|
||||
"records": [
|
||||
{
|
||||
"键来自规则产物格式 schema": "值"
|
||||
}
|
||||
],
|
||||
"增量说明": "新建|追加|替换某 batch_id"
|
||||
},
|
||||
"自评": {
|
||||
"维度": [
|
||||
{
|
||||
"名": "合规模",
|
||||
"分数": 8,
|
||||
"说明": "是否按方法/配方/表现/单层格式/池型(方向为主,元素=抽取)/分级表现/地板生成;句子级是否达到粒度"
|
||||
}
|
||||
],
|
||||
"薄弱点": "一句话;无则空字符串"
|
||||
},
|
||||
"追问": {
|
||||
"导语": "",
|
||||
"题目": []
|
||||
},
|
||||
"开放问题": []
|
||||
"规则": "所依生成规则的中文名;无规则时省略",
|
||||
"名称": "这条记录的中文名",
|
||||
"记录": {
|
||||
"字段名": "按规则字段或极简内容填写的值"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
硬规则:
|
||||
1. 合法 JSON;含 `schema` / `技能` / `brief` / `正文` / `自评` / `追问`。
|
||||
2. 正文键固定:`rule_id` / `本批数量` / `批次条件` / `batch_id` / `records` / `增量说明`。
|
||||
3. `records` 始终为数组;每条必须是规则约定的单层 JSON;`本批数量 === records.length`。规则「一次生成」为单条时长度为 1;为同批时 ≥2 且关系对齐。
|
||||
4. 自评仅「合规模」一维。禁止改规则、写 `order`、输出执行单元列表。
|
||||
5. summary:`具体实例 · {对象} · {n}条`。
|
||||
1. 输出合法 JSON。`记录` 是单个对象。`名称` 用中文,并与这条记录的主名称一致。
|
||||
2. 有规则时 `记录` 的键必须来自该规则字段,不得增删。
|
||||
3. 无规则时省略 `规则`,`记录` 只含用户给出的少量简单字段。
|
||||
4. 字段名和专名用中文;英文专名音译为中文。
|
||||
5. 不在产物正文写自评、追问、开放问题或合同草稿。
|
||||
6. `mount` 固定为 `["world-simulator"]`。
|
||||
|
||||
## score
|
||||
|
||||
```score
|
||||
工作流符合度:
|
||||
- 5:严格按规则填写工作流推进;字段、池选取、依赖与自检都落实;无规则路径则字段简单够用且不越权发明合同。
|
||||
- 3:大体按工作流,但有一处阶段跳跃、依赖未咬合,或某字段明显偏薄。
|
||||
- 1:忽略工作流;乱增字段;或把用户已定内容改掉。
|
||||
```
|
||||
|
||||
## checklist
|
||||
|
||||
```checklist
|
||||
- [ ] 按本步认定的 rule_id(规划或步内钉)的预生成规则执行?
|
||||
- [ ] 一次生成已遵守(单条=1 条;同批=多条且关系对齐)?records 始终是数组?
|
||||
- [ ] records 单层、键值符合产物格式与全部约束?句子级是否达到地板?有配方是否组装进目标字段?方向是否以方向为主(专名不当选项库)?
|
||||
- [ ] 自评为合规模?未改合同?
|
||||
```
|
||||
|
||||
## examples
|
||||
|
||||
```examples
|
||||
好:规则一次生成=同批、关系=开局仇敌二人 → 本批 2 条,对立在两条里对齐。
|
||||
好:规则一次生成=单条 → 本批 1 条;还要更多就再点原型。
|
||||
好:方向「冷焰」作标签,能力描述按该方向写开到可感知;或粒度=方向时字段值就是「冷焰」。
|
||||
好:阶层字段填「金丹」,表象留在规则的分级表现里,不把整段对照复制进每条 record。
|
||||
坏:同批规则却只交 1 条,或分多步把有关系的人拆开写。
|
||||
坏:加规则没有的字段;输出一段「我是怎么想的」却没有 records;本步改 schema。
|
||||
坏:句子级只填「阴郁」;或按方向说明里那个专名点菜交差。
|
||||
- [ ] 有规则则按字段与填写工作流执行;无规则则只是一条字段很少的具名内容?
|
||||
- [ ] 一份产物只有一条记录,名称是中文?
|
||||
- [ ] 记录键值合法,用户已定专名未改写,英文专名已音译?
|
||||
- [ ] 未改合同?
|
||||
- [ ] 产物正文不含评分、诊断、追问?
|
||||
```
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# 上下文投影排序
|
||||
# 上下文排序
|
||||
|
||||
> 技能文档。程序只切割下方 **fence 块**;`##` 标题仅供人读。
|
||||
> **先抄程序钉死的执行单元名册,再给每个已启用者分配上下文与顺序。**
|
||||
@@ -8,25 +8,18 @@
|
||||
## meta
|
||||
|
||||
```meta
|
||||
name: 上下文投影排序
|
||||
name: 上下文排序
|
||||
id: context-order
|
||||
artifact: 设计.上下文投影排序
|
||||
declaration: >
|
||||
收成前给已启用槽排扁平投影序(含对话.历史);不发明槽、不重写长文、不演情节选择题
|
||||
程序按变化频率与重要性排列各执行单元的上下文,并选择完整、摘要或字段投影
|
||||
when: |
|
||||
游玩槽已确认(或可按默认),且主要上下文块已有可摘要产物,需要决定各执行单元看见什么、以何顺序插入。
|
||||
本步为〔程序步〕:上下文已多、即将收成时排入。
|
||||
落档或开玩时自动执行;执行方式与可用产物已经确定。
|
||||
when_not: |
|
||||
体验/站位仍混沌 → 先「美学纲领与交互范式」。
|
||||
槽位未定且用户拒绝默认 → 先「游玩拓扑」。
|
||||
上游上下文仍大量空洞、无法判断稳/变 → 先补缺口,勿空排。
|
||||
只需勾选槽 → 「游玩拓扑」;上下文很少、只需合并终稿 → 「细化终稿」可按默认序退化。
|
||||
没有任何创作产物时仍生成最小顺序,不向用户提问。
|
||||
boundary: |
|
||||
本技能:抄名册 → 凭各块 brief/挂载/稳变 给每槽 inserts[]。
|
||||
名册权威在程序(及已勾选的游玩拓扑);本步不增白名单外 id。
|
||||
中段技能可带 mount 与稳/变;最终 order 以本步为准。
|
||||
细化终稿:把本步排序表收进运行规格 context_order;本步不输出完整 设计.worker集。
|
||||
禁止把依赖产物里的游玩示例(迎上去/推开/反客为主 等)当成当前要问的题。
|
||||
变化频率决定缓存分组:稳定内容在前,历史居中,当前状态与本轮输入在后。
|
||||
同一频率内,低重要性在前、高重要性靠近使用位置;本程序不改写上游内容。
|
||||
feeds: design_only
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
# 本局创作方案
|
||||
|
||||
## meta
|
||||
|
||||
```meta
|
||||
name: 本局创作方案
|
||||
id: creation-plan
|
||||
artifact: 设计.本局创作方案
|
||||
declaration: >
|
||||
先抽出用户要的核心体验,再主动找出有帮助的内容能力并分档,形成本局专用创作方法;它指导取舍,不规定固定流程。
|
||||
when: |
|
||||
尚无本局创作方案,且用户已经给出有效创作方向;或用户改口、新增方向,导致现有方案明显不再成立。
|
||||
when_not: |
|
||||
只是在既定方向内补充局部材料,现有方法与能力分档仍成立时,不重复改写。
|
||||
boundary: |
|
||||
本能力只决定本局怎么做、哪些内容能力重要以及还缺什么;不代替任何能力生成正文,不安排 DAG,不进入游玩上下文。
|
||||
feeds: design_only
|
||||
```
|
||||
|
||||
## task
|
||||
|
||||
```task
|
||||
根据用户当前材料生成或整份重写「本局创作方案」,写入 `设计.本局创作方案`。方案用于指导后续创作取舍:先定本局要兑现的体验,再形成针对这份体验的方法,并把程序提供的全部内容能力各分到一个档位。
|
||||
|
||||
先抽出核心体验,再扫能力目录找有帮助的能力,最后才写方法和分档。核心体验是用户想反复感到、扮演或达成的那一件事;从角色、限制、例子和题材里抽,即使用户没把它叫做体验。写进 `brief`。不要停在原话点到的题材词和能力名上。
|
||||
|
||||
然后用这份体验逐项看能力目录。用户没点名、但缺了会使体验立不住、持续不了或跑不起来的能力,标成「必须」或「有必要」,把还没选定的具体内容写进 `missing`。用户点到、却不承载这份体验的能力,降到「有一定效果」或「没有意义」。只写真实语义依赖;`items` 的排列不表示执行顺序,也不是本轮要写完的清单。缺口留给后续询问,不在生成方案的同一轮起草那些能力。
|
||||
|
||||
必须原样覆盖能力目录中的每个 `module_id` 各一次。目录外 id、本能力、机械工具节点和旧工序节点都不纳入。用户改变目标且旧方案不再成立时,按新的核心体验重写完整方案,不在旧 JSON 上追加补丁。
|
||||
```
|
||||
|
||||
## principles
|
||||
|
||||
```principles
|
||||
1. 配方必须针对抽出的核心体验:说明靠什么方法兑现它,不复述通用创作常识,也不复述用户点到的词表。
|
||||
2. 四档按对这份体验和可运行性的贡献分,不按原话有没有出现这个能力:
|
||||
- 必须:缺少后无法运行,或核心体验会崩。用户没点名、但体验直接压在上面的能力也放这里。
|
||||
- 有必要:缺少仍可运行,但体验明显变弱。主动把有帮助、用户没提到的能力放这里。
|
||||
- 有一定效果:对体验有增益,缺了核心仍在。可顺带询问一次。
|
||||
- 没有意义:对这份核心体验没有帮助。不是「用户没提到」。
|
||||
3. `why` 写该能力怎样服务抽出的核心体验;`missing` 只写仍会影响实施的具体缺口,没有则写空字符串。前三档里还没选定的做法,用能直接拿去问的具体选择写入 `missing`。
|
||||
4. `instances` 只列已经能判断的具体实例需求;生成规则、具体实例等可反复能力可以列多项,其余通常为空数组。
|
||||
5. 旧配方只能作为方法案例引用、混合或改写;公共规则不从案例复制进本局配方。案例里的档位倾向只供对照,与本局体验冲突时以本局体验为准。
|
||||
6. 材料少时仍形成方案:从已有材料推断核心体验,不编造世界设定。推断出的有帮助能力照样上档,还没定的具体内容写进对应 `missing`。这些缺口在后续对话里按用户原话和已抽出的体验给 2~3 个选项询问,选定后再写对应能力。
|
||||
```
|
||||
|
||||
## output
|
||||
|
||||
```output
|
||||
{
|
||||
"schema": "creation-plan.v1",
|
||||
"技能": "本局创作方案",
|
||||
"brief": "一句话写出抽出的核心体验,以及靠什么兑现它",
|
||||
"recipe": {
|
||||
"primary": "主要创作方法",
|
||||
"secondary_traits": ["可混合的次要体验特征"],
|
||||
"method": ["针对本局的做法,不是固定节点顺序"],
|
||||
"references": ["采用或改造过的旧配方案例 id"]
|
||||
},
|
||||
"items": [
|
||||
{
|
||||
"module_id": "variable-design",
|
||||
"tier": "必须",
|
||||
"why": "任务时限与暴露度必须跨轮保存",
|
||||
"missing": "暴露度达到哪一档会被识破尚未确定",
|
||||
"instances": []
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
硬规则:
|
||||
1. 输出合法 JSON,`schema` 固定为 `creation-plan.v1`,`技能` 固定为 `本局创作方案`。
|
||||
2. `recipe.method` 描述本局做法,不写节点序列、阶段编号或统一流水线。
|
||||
3. `items` 必须覆盖程序给出的全部能力各一次;`tier` 只能是四档之一。
|
||||
4. `why`、`missing`、`instances` 每项都必须存在;没有缺口时 `missing` 写空字符串。
|
||||
5. 产物中不写自评分、追问、回复话术或游玩正文。
|
||||
|
||||
## score
|
||||
|
||||
```score
|
||||
能力覆盖:
|
||||
- 5:全部应覆盖能力恰好一次,排除项正确,档位合法。
|
||||
- 3:主体齐全,但有边界能力误分、重复或理由过于笼统。
|
||||
- 1:明显漏项,或纳入机械工具与旧工序节点。
|
||||
|
||||
配方针对性:
|
||||
- 5:`brief` 里的核心体验能解释主要方法;用户没点名但承载体验的能力已在「必须」或「有必要」,换一个作品就不成立。
|
||||
- 3:体验可辨,但分档仍主要跟着原话点到的词走。
|
||||
- 1:只有用户原话的复述或泛泛原则,没有主动找出有帮助的能力。
|
||||
|
||||
依赖克制:
|
||||
- 5:只表达真实语义依赖,不暗示固定执行顺序。
|
||||
- 3:大体灵活,但夹有弱顺序或阶段暗示。
|
||||
- 1:恢复成统一流水线或 DAG。
|
||||
|
||||
缺口可行动性:
|
||||
- 5:前三档缺口具体,涉及选择时能给出明确实例方向。
|
||||
- 3:部分缺口笼统,仍需再次解释才能行动。
|
||||
- 1:缺口缺失、虚构,或变成无差别问卷。
|
||||
```
|
||||
|
||||
## checklist
|
||||
|
||||
```checklist
|
||||
- [ ] brief 先写出抽出的核心体验,而不是复述用户点到的题材词?
|
||||
- [ ] recipe 是兑现这份体验的本局方法,而不是固定节点流程?
|
||||
- [ ] 用户没点名、但缺了体验会明显变差的能力,已在「必须」或「有必要」,并写了具体 missing?
|
||||
- [ ] 「没有意义」表示对这份体验没有帮助,而不是「用户没提到」?
|
||||
- [ ] 能力目录中的每个 module_id 恰好出现一次,且没有目录外 id?
|
||||
- [ ] missing 具体且不重复 why,没有缺口时为空字符串?
|
||||
- [ ] 产物不含评分、追问、回复话术或游玩正文?
|
||||
```
|
||||
@@ -1,9 +1,5 @@
|
||||
# 生成规则
|
||||
|
||||
> 技能文档。程序只切割下方 **fence 块**;`##` 标题仅供人读。
|
||||
> 产物外壳:`docs/context-fragment-design.md`;范例:`aesthetics-interaction` / `mechanism`。
|
||||
> 正文高度自定义(描写文案、schema 字段、池随对象族而变);外壳与规则键必须严。
|
||||
|
||||
## meta
|
||||
|
||||
```meta
|
||||
@@ -11,116 +7,47 @@ name: 生成规则
|
||||
id: generation-rules
|
||||
artifact: 设计.生成规则
|
||||
declaration: >
|
||||
要精准格式,或本局原创听了会雾且故事里还要生成时:写出该对象族怎么生成(描写/格式/可选池);
|
||||
创意字段用方向池给创作范围,不堆枚举词;不填最终实例;可按对象反复
|
||||
为需要统一生成的一类对象写出合同:名称、用途、字段、池或提示约束、填写工作流;不填最终实例;可按对象反复。
|
||||
when: |
|
||||
已有「美学纲领与交互范式」或等价整体感觉,且下面两条满足一条就排(可按对象族反复):
|
||||
a. 生成物必须有精准格式。例:武功典籍要有名字、效果、阶层。
|
||||
b. 本局原创、听了会一头雾水的东西,故事里还要随机/反复生成。例:人人能绽放的「心之彩」;当成本局原创的斩魄刀。
|
||||
要落两份或以上同类对象时使用。
|
||||
只落一份,但字段多、结构重要,或需要先审字段和填写工作流时也使用。
|
||||
不因开场后是否继续按同一合同生成而改道。每个产物只写一条规则。
|
||||
when_not: |
|
||||
整体感觉未钉 → 先「美学纲领与交互范式」。
|
||||
常识能推、也不需要精准格式(现实名车、西幻常见种族)→ 不排。
|
||||
只要几张具名名片、不必按规则生成 → 不排;已有规则再要名片 → 「具体实例」。
|
||||
跨轮记住什么 → 「变量设计与更新规则」。故事怎么往前写 → 「故事推进」(工序编排:「叙事指南与故事推进」)。
|
||||
只有一条、字段很少、也不承载玩法的具名内容,才不建规则。这种内容通常也不必落具体实例。
|
||||
一张有节点和连接的结构(地图、升级路径、关系网)→ 拓扑图谱。
|
||||
跨轮真值 → 变量设计;舞台格局与硬法则 → 舞台骨架 / 实现机制。
|
||||
boundary: |
|
||||
本技能:一条规则=怎么描写/生成 + 单层 JSON 产物格式 + 可选池(方向池优先;元素池仅封闭集合)。
|
||||
具体实例:按规则填最终 records;本步不写最终实例(池≠实例)。
|
||||
舞台骨架 / 实现机制:上游真值只引用——`依据`/`硬约束`写短引用句或最短填写条款;社会格局、世界状况、既成法则正文不在本步重写。
|
||||
缺上游真值时:本步仍只写填写合同时必须守的最短条款;格局/法则长文交给舞台骨架或实现机制,本步不代写百科、不发明「共通前提」迷你世界书。
|
||||
变量*:游玩期状态更新——本步只约束生成时的类型与合法初值范围。
|
||||
随机范围整理:把本步(及其他上游)里需要的掷骰/检定范围收成插入表——本步可写合法区间,不代替那份汇总。
|
||||
旁观维护:可读合同(rule_id、产物格式、何时可 need_generate);程序抽样只抽元素池;方向池由填写方按规则自创。
|
||||
本能力只交生成合同:名称、用途、字段、池或提示、填写工作流。
|
||||
不填最终 records,不重写世界百科,不改变量公式与剧情推进。
|
||||
feeds: gm,auditor
|
||||
```
|
||||
|
||||
## opening
|
||||
|
||||
```opening
|
||||
先认定本轮规则写哪一类内容:
|
||||
- 若【本步对象】/【原型建议】已有对象提示,以此为起点;要改对象就在本步改。
|
||||
- 若规划为空,先钉对象族,再谈规则。
|
||||
|
||||
然后谈:
|
||||
1. 生成出来必须钉死哪些字段吗?(如武功典籍的名字、效果、阶层)没有就说没有。
|
||||
2. 这是不是本局原创、听了会一头雾水、还要在故事里随机生成的东西?
|
||||
3. 更需要:开局先钉一批、游玩中再随机补,还是只在游玩期动态生成?
|
||||
4. 这类实例彼此有没有必须同场钉死的关系(如互为亲属、对立、同场搭档)?有 → 一次同批,关系用一两句钉互指即可;彼此独立 → 一次一条。台上格局、世界律、前史不在这里展开。
|
||||
5. 有没有必须全员一致、能数完的标签(阶层、强度档、武器种类)?名字、能力、气质、外貌这类写不完的,后面用「方向」划范围(一句话概括即可,少举例)。阶层这类光看名字不懂的,后面会写成能看见/听见的对照,不要只留阶名。
|
||||
|
||||
想到什么写什么;完整规则在正式产物里钉。
|
||||
先说这套规则生成什么、用在什么场合。只有一条字段很少的具名内容,通常不必建规则,也不必落具体实例。
|
||||
```
|
||||
|
||||
## task
|
||||
|
||||
```task
|
||||
你正在执行「生成规则」。产物必须是 **context-fragment.v1** JSON,写入「设计.生成规则」。
|
||||
为单一对象族写出一份生成合同,写入 `设计.生成规则#中文规则名`。先写名称与用途,再按用途推出字段与每项填充要求;需要封闭选取的字段绑池,更适合一句话约束的字段写提示;最后写填写工作流(含依赖、自检、反模式),供具体实例与游玩期动态生成共用。
|
||||
|
||||
本步写出的最终物是**规则**,不是实例。规则是创造这类东西的方法:空白表格 + 怎么填厚 + 必要时给方向。一条规则必须说清两件事,并可附第三件:
|
||||
1. **生成与描写**:按什么顺序写、多方向如何拼进一格、每格填到多厚、分级如何被感知。
|
||||
2. **产物格式**:生成结果必须遵循的可校验格式(schema)。描写字段要有粒度和地板。
|
||||
3. **池**(可选,默认无):优先 **方向池**(方向名 + 一句概括;默认不举例,至多点 1 个专名);**元素池**只给能数完、必须全员一致的封闭集合。无则 `池: []`。
|
||||
|
||||
本步是〔先验产物〕:先定本轮对象族,再写规则。读【本步对象】与【原型建议】;在本步钉「写什么」。禁止顺手扩到其它族。
|
||||
|
||||
执行顺序:
|
||||
1. 认定对象族:规划有 `target` 则采用(用户要改则以最新为准);规划为空则先钉对象。不要停下来让编排器补参。
|
||||
2. 对照时机 a/b:必须有精准格式,或本局原创听了会雾且故事里还要生成。两条都不成立 → 结论「无需专门规则」,`rules: []`。
|
||||
3. 两项成立时,生命周期三选一(规划里 `lifecycle_intent` 合理则采用):
|
||||
- `runtime_only`:仅游玩期动态生成;不要求「具体实例」。
|
||||
- `seed_only`:由「具体实例」预生成;规则用完可丢。
|
||||
- `seed_and_runtime`:先种子实例;规则与实例都保留,游玩期仍可按同规则生成。
|
||||
4. 钉「数量」与「一次生成」(两件不同的事):
|
||||
- **数量**:这类对象总共要有几条(fixed / range / contextual)。
|
||||
- **一次生成**:点一次「具体实例」交几条。彼此有必须同场对齐的关系 → `同批`;彼此独立 → `单条`。产物形状始终是 `records[]`。
|
||||
5. 写字段前先做设计判断(写入该 rule 的 `设计判断`):
|
||||
- 列出 **不可穷举项**(姓名、绰号、能力概念、外貌手感等理论上写不完的)→ 这些用方向池或自由写,禁止做成条目枚举。
|
||||
- 列出 **封闭集合**(阶层、强度档、有限类型名)→ 这些才用 `allowed_values` 或一个元素池。
|
||||
- 标签本身看不懂的分级(境界、品阶、强度档、好感档等)→ 必须另写 `分级表现`,把每档写成可感知的详细表象,不要只留字母/阶名。
|
||||
- 从本对象族真正用得上的面挑 3~6 个正交字段(是什么 / 能做什么 / 和谁关联 / 怎么被感知),不要摊通用名片。
|
||||
- 外貌、服装、仪式等由几面拼成一格的 → 写 `配方`(多方向组装进同一个顶层字段),不要拆成嵌套对象,也不要为每一面各建元素池。
|
||||
6. 写满规则:生成与描写(含配方、表现、工作流、反模式)+ 产物格式(**单层 JSON**)+ 池(需要才填)+ 需要时的分级表现。
|
||||
每个描写类 string 字段必须有 `粒度`(句子)和 `地板`(填到什么才算够)。`示例形状` 必须达到地板,禁止用单标签当示例。
|
||||
7. 逐字段过筛:每个属性都要能回答「对本对象族是否必要」。冗余字段删掉(如女主规则里的「性别」);玩法接口不得漏(如 D&D 怪物的等级、攻击骰)。
|
||||
8. `rule_id`:规划已给则用;否则稳定 kebab-case。已有产物按 rule_id 增量。
|
||||
若本步对象与规划不一致,以最新对象为准并重钉对应英文 rule_id,禁止沿用旧 id。
|
||||
9. 按自评四维打分并填追问;输出 JSON。禁止夹带最终实例 records。
|
||||
|
||||
若程序已发默认问题:禁止重复开场;在首答上补洞。
|
||||
summary:建立 → `生成规则 · {对象} · {仅动态|仅预生成|预生成并动态} · {单条|同批}`;跳过 → `生成规则 · {对象} · 无需专门规则`。
|
||||
每个产物只含一条规则。材料足够便直接成稿;真正会改合同的缺口(生命周期、必填字段、单条/同批关系)交给 `self_score.gap`。产物正文不放评分、诊断、追问或开放问题。
|
||||
```
|
||||
|
||||
## principles
|
||||
|
||||
```principles
|
||||
1. 整条规则先问必要性:时机 a(精准格式)或时机 b(原创会雾且还要生成)是否成立?两条都不成立则跳过,勿硬造表。
|
||||
2. 一调用一对象族;格式或用途不同则分次编排。
|
||||
3. 规则 = 描写/生成方法 + 产物格式。池不是必填第三设计;默认 `池: []`。宁少池,勿为每个字段挂一套词表。
|
||||
4. 属性逐个过筛:每个字段必须对本对象族有意义;禁塞通用名片字段,也禁漏掉玩法接口字段。
|
||||
5. 产物格式 = **单层 JSON**:一条产物是一层扁平对象;值仅为标量或「标量数组」。
|
||||
禁止嵌套 object、禁止 object 数组。此格式要对齐动态表格列、旁观者→主世界层生成、以及投影取字段。
|
||||
类型/必填/枚举/上下界必须可校验;禁止只给示例让下游猜类型。
|
||||
6. 池分两种,先判断能不能数完:
|
||||
- **方向池**(默认):选项理论上无限。给 4~8 个方向;每条以**方向名 + 一句概括**为主。默认不举例;需要手感时至多点 1 个专名,禁止把方向写成作品名/例句清单。填写以方向为主:方向名可以当选项或标签;句子级字段按该方向写开。程序不抽方向池。
|
||||
- **元素池**:选项有限且必须全员一致(阶层、武器种类、强度档字母)。条目可穷举;旁观 `need_generate` 只抽这类池。
|
||||
姓名、绰号、能力概念、外貌、气质(作描写时)禁止元素池,也禁止用 `allowed_values` 假穷举。同一封闭集合不要既写枚举又再建元素池。
|
||||
7. 填值要有地板。骰子、是否可破坏可以短。外貌、能力现象、限制、互动手感必须写到可感知。`示例形状` 是地板的样板,不是「阴郁」这种标签。
|
||||
分级标签本身看不懂时:实例字段仍填短档位;详细对照写在 `分级表现`(每档 = 一句概括 + 一段可感知表象,相邻档必须能分清)。不要只丢「A / 金丹」让人猜。
|
||||
8. 几面拼成一格用 **配方**:点名目标字段、用到哪些方向组件、怎么拼进这一格。表现类要点用 **表现**:一句概括 + 至多 1 条例子;例子是密度样板,不是选项库。
|
||||
9. 生命周期先行;未定前不假装可执行「具体实例」。
|
||||
10. **一次生成**看关系,不看「想不想一次多写」:有必须同场钉死的关系 → `同批`;彼此独立 → `单条`。`同批` 至少 2 条。不要把「总共要几个」和「这一次交几条」混成一件事。
|
||||
11. **上游真值引用,不重述**:舞台格局、世界状况、既成法则已有 → `依据`/`硬约束`点名引用;没有 → 只写最短填写条款,并在追问或开放问题标「应落舞台骨架/实现机制」。禁止用「共通前提」类长文重写世界书。
|
||||
12. **同批关系只钉互指**:`一次生成.关系`与字段间约束写清谁对谁(对立/亲属/席位互指等);不借关系段落写前史、社会律、权力百科。
|
||||
13. 不写变量更新公式、不写剧情推进、不填最终实例。
|
||||
```
|
||||
|
||||
## probe
|
||||
|
||||
```probe
|
||||
每轮 1~2 点。规划已钉且用户未改对象时,禁止重问 target / rule_id。规划为空或用户要换对象时,先问本轮生成什么。
|
||||
|
||||
优先:整条规则是否必要;实例彼此有无必须同场对齐的关系(只追互指,不追世界书);某字段该不该进表;不可穷举项有没有被错做成枚举词表;分级标签够不够让人看懂;描写字段地板是否够用。
|
||||
若时机 a/b 都不成立 → 建议跳过,停追问字段与格式。
|
||||
不追问:最终实例正文、变量增减公式、其它对象族、完整世界百科、台上格局/既成法则长文(→ 舞台骨架/实现机制)、再给哪个字段加一套枚举池。
|
||||
1. 用途决定字段:每个字段都要能回答「对这个用途是否必要」;女 NPC 不写性别;药水要写清效果参数,不要只写外观。
|
||||
2. 字段不多不少、互不重叠:资产与存款这类同义字段只留一个。
|
||||
3. 取值分三类:自由生成;从池中选取(填写方按合理度选,不是伪随机程序);提示约束(如「现代都市中正常的职业」)。默认优先提示约束;只有需要封闭一致或分阶段选型时才建池。
|
||||
4. 填写工作流按重要性推进:先锚定会约束后续的选项,再填血肉;阶段、依赖、自检、反模式都服务于「怎么填对」,写短写准。
|
||||
5. 生命周期三选一:`仅游玩期`(创作期不预填)、`只预生成`(预填完即可丢掉规则)、`预生成并游玩期`(既写种子,开场后仍可再生成)。选哪一档不改变「要不要先写这份合同」。
|
||||
6. `只预生成` 为中间层;另两类为最终层,挂主世界层与旁观。
|
||||
7. 上游舞台/机制只短引用;不在本步发明共通前提百科。
|
||||
8. 不输出最终实例记录。
|
||||
9. 给人读的键和专名用中文。材料里的英文名音译成中文。外壳的 schema、brief、mount 保持既有键。
|
||||
```
|
||||
|
||||
## output
|
||||
@@ -129,224 +56,103 @@ summary:建立 → `生成规则 · {对象} · {仅动态|仅预生成|预生
|
||||
{
|
||||
"schema": "context-fragment.v1",
|
||||
"技能": "生成规则",
|
||||
"brief": "一句话:对象族 + 建立规则|无需专门规则 + 生命周期(若建立)+ 方向池|元素池|无池",
|
||||
"brief": "一句话:对象 + 用途 + 生命周期",
|
||||
"mount": ["world-simulator", "auditor"],
|
||||
"稳变": "stable",
|
||||
"正文": {
|
||||
"本步参数": {
|
||||
"target": "与本步认定的对象一致(规划有则对齐,步内改则以最新为准)",
|
||||
"rule_id": "已钉或本步拟定;跳过时可空字符串",
|
||||
"lifecycle_intent": "规划原值、本步钉死值或空"
|
||||
"名称": "人话规则名,如「末世可征服 NPC」",
|
||||
"用途": "这套规则在本局用来生成什么、服务哪类游玩反馈",
|
||||
"规则名": "中文短名,与名称对应且稳定",
|
||||
"生命周期": "仅游玩期|只预生成|预生成并游玩期",
|
||||
"数量": {
|
||||
"模式": "固定|区间|看情况",
|
||||
"值": 1,
|
||||
"最小": 1,
|
||||
"最大": 1,
|
||||
"决定规则": "contextual 时说明如何决定条数"
|
||||
},
|
||||
"必要性判断": {
|
||||
"生成对象": "本轮对象族",
|
||||
"对游玩的重要性": "时机 a:必须钉死哪些字段;无则写「不需精准格式」",
|
||||
"基础生成的不足": "时机 b:为何听了会雾、现有常识推不出;能推则写清",
|
||||
"结论": "建立专门规则|无需专门规则"
|
||||
"一次生成": {
|
||||
"模式": "单条|同批",
|
||||
"关系": "同批必填:本批必须一起钉死的互指;单条写彼此独立"
|
||||
},
|
||||
"rules": [
|
||||
"字段": [
|
||||
{
|
||||
"rule_id": "英文 kebab-case",
|
||||
"对象": "该规则生成什么",
|
||||
"生命周期": "runtime_only|seed_only|seed_and_runtime",
|
||||
"上下文策略": {
|
||||
"预生成实例": true,
|
||||
"保留规则到游玩期": false,
|
||||
"保留实例到游玩期": true
|
||||
},
|
||||
"数量": {
|
||||
"模式": "fixed|range|contextual",
|
||||
"值": 1,
|
||||
"最小": 1,
|
||||
"最大": 1,
|
||||
"决定规则": "contextual 时必填,可较长"
|
||||
},
|
||||
"一次生成": {
|
||||
"模式": "单条|同批",
|
||||
"关系": "同批必填:本批记录之间必须一起钉的联系;单条写「彼此独立」"
|
||||
},
|
||||
"设计判断": {
|
||||
"不可穷举项": ["姓名", "能力概念", "外貌"],
|
||||
"封闭集合": ["类型", "阶层"],
|
||||
"正交字段依据": "一句话:为何是这几个字段而不是通用名片"
|
||||
},
|
||||
"生成与描写": {
|
||||
"依据": ["可较长;引用上游产物或用户要求"],
|
||||
"配方": [
|
||||
{
|
||||
"目标字段": "外貌",
|
||||
"组件": ["轮廓方向", "质感方向"],
|
||||
"组装": "各取一个方向,拼进外貌这一格;不要拆成嵌套对象"
|
||||
}
|
||||
],
|
||||
"表现": [
|
||||
{
|
||||
"针对": "能力现象",
|
||||
"概括": "写到旁人能看见或听见的变化,以及对手能抓住的空隙",
|
||||
"例子": ["至多 1 条密度样板;没有就不写"]
|
||||
}
|
||||
],
|
||||
"工作流": ["按配方取方向并组装", "封闭档位对照分级表现", "按地板写描写格"],
|
||||
"方法": ["描写口径;方向为主(方向名可作选项/标签,句子级按该方向写开);元素池如何抽取"],
|
||||
"硬约束": ["任何产物不得违反"],
|
||||
"字段间约束": ["有就写;无则 []"],
|
||||
"变化维度": ["允许随机或变化的部分;无则 []"],
|
||||
"禁止项": ["不得出现的组合或内容"],
|
||||
"去重规则": ["同批/跨批"],
|
||||
"反模式": ["为名字/能力/外貌建枚举条目池", "描写字段只填一个词", "方向池堆作品名或例句当选项", "分级只写阶名没有表象"],
|
||||
"校验": ["类型、枚举、边界、地板、配方是否组装进目标字段、分级表现是否可分档"]
|
||||
},
|
||||
"分级表现": [
|
||||
{
|
||||
"字段": "阶层",
|
||||
"说明": "实例只填档位值;叙事按本表展开。无此类分级则 []",
|
||||
"档位": [
|
||||
{
|
||||
"值": "档位名",
|
||||
"概括": "一句:这档能做到什么量级",
|
||||
"表象": "可感知的详细对照(看见/听见/碰到什么;须能和上下档分清)"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"产物格式": {
|
||||
"格式": "单层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
|
||||
}
|
||||
},
|
||||
"示例形状": {
|
||||
"名称": "达到地板的名字,不是「某某」",
|
||||
"阶层": "金丹",
|
||||
"外貌": "按配方拼好的一段可感知外貌,不是「黑发高个」",
|
||||
"能力描述": "写清现象与可被抓住的空隙;禁止一个词交差"
|
||||
}
|
||||
},
|
||||
"池": [
|
||||
{
|
||||
"pool_id": "英文 kebab-case;同规则内唯一",
|
||||
"名称": "人话名",
|
||||
"池型": "方向|元素",
|
||||
"绑定字段": "单层 schema 的顶层字段名",
|
||||
"用途": "方向:方向为主的创作范围|元素:随机抽取或必选",
|
||||
"说明": "该字段如何用本池;多池绑同一字段时看配方",
|
||||
"方向": {
|
||||
"方向名": "一句概括。默认到此为止;至多再点 1 个专名。仅池型=方向时使用"
|
||||
},
|
||||
"条目": [
|
||||
{
|
||||
"id": "可选稳定 id",
|
||||
"内容": "仅池型=元素:可穷举的标签或短词",
|
||||
"权重": 1,
|
||||
"标签": ["可选分类标签"]
|
||||
}
|
||||
],
|
||||
"抽取或组装规则": "方向池写「以方向为主;句子级按该方向写开」;多池同字段看配方;元素池写抽取方式,默认均匀随机"
|
||||
}
|
||||
]
|
||||
"名": "顶层字段名",
|
||||
"填充要求": "填什么、填到多厚、为何本用途需要它",
|
||||
"取值方式": "自由|池选取|提示约束",
|
||||
"绑定池": "池选取时填写池名称;其它省略",
|
||||
"提示": "提示约束时写可执行的一句范围;其它省略"
|
||||
}
|
||||
],
|
||||
"增量说明": "相对已有规则新增或修改了什么;首条可写「新建」"
|
||||
},
|
||||
"自评": {
|
||||
"维度": [
|
||||
"池": [
|
||||
{
|
||||
"名": "必要性",
|
||||
"分数": 8,
|
||||
"说明": "时机 a 或 b 是否成立;跳过时理由是否充分"
|
||||
},
|
||||
{
|
||||
"名": "属性妥当",
|
||||
"分数": 8,
|
||||
"说明": "逐字段:有无冗余或缺失玩法接口;跳过时写 N/A"
|
||||
},
|
||||
{
|
||||
"名": "材料得当",
|
||||
"分数": 8,
|
||||
"说明": "不可穷举项是否做成方向池/自由而非假穷举;方向是否以概括为主、例子极少;元素池是否仅封闭集合;该细的分级是否有表象;描写字段地板是否够密;跳过时写 N/A"
|
||||
},
|
||||
{
|
||||
"名": "格式准确",
|
||||
"分数": 8,
|
||||
"说明": "是否严格单层 JSON、类型/必填可对接动态表格·旁观者生成·投影;跳过时写 N/A"
|
||||
"名称": "与字段绑定的人话名",
|
||||
"说明": "何时从中选取、选取口径",
|
||||
"条目": ["封闭可选项;只列必须一致的有限集合"]
|
||||
}
|
||||
],
|
||||
"薄弱点": "一句话"
|
||||
},
|
||||
"追问": {
|
||||
"导语": "为钉准生成规则,还需确认:",
|
||||
"题目": [
|
||||
{
|
||||
"问": "…",
|
||||
"建议选项": ["选项A", "选项B", "其它(请写明)"],
|
||||
"示例": "可选"
|
||||
}
|
||||
]
|
||||
},
|
||||
"开放问题": []
|
||||
"填写工作流": {
|
||||
"工作流": {
|
||||
"阶段1 骨架锚定": "先选哪些池/字段、定下什么内核",
|
||||
"阶段2 灵魂注入": "下一步推什么,如何与上一阶段咬合",
|
||||
"阶段3 血肉填充": "姓名、外貌等如何服务已定内核",
|
||||
"阶段4 立场定调": "可选;最后定关系或立场类字段"
|
||||
},
|
||||
"依赖关系": [
|
||||
"字段A → 字段B:约束说明"
|
||||
],
|
||||
"自检清单": [
|
||||
"填完后必须能回答的关键问题"
|
||||
],
|
||||
"反模式": [
|
||||
{
|
||||
"名": "短名",
|
||||
"说明": "为什么错",
|
||||
"反例": "可选一句"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
硬规则(公共三段 + 本技正文):
|
||||
1. 合法 JSON;必含 `schema` / `技能` / `brief` / `正文` / `自评` / `追问`(题目可 `[]`)。
|
||||
2. **公共三段**:正文=规则主体;自评=必要性/属性妥当/材料得当/格式准确(**分数 0–10 十分制**);追问=导语+建议选项。
|
||||
3. **正文键固定**:本步参数、必要性判断、rules、增量说明。
|
||||
4. **每条 rule 必含**:设计判断、生成与描写、产物格式、数量、一次生成;**池可空数组**。`生成与描写.配方` / `表现` 无则 `[]`。标签看不懂的分级必须有 `分级表现`(每档概括+表象,相邻可分清);无此类则 `分级表现: []`。有池时 `绑定字段` 必须是单层 schema 的顶层键,且必须标 `池型`。多个方向池绑同一字段时配方必须写清怎么拼进该格。`一次生成.模式` 为同批时 `关系` 必填且本批至少 2 条;为单条时一次只交 1 条。
|
||||
5. **产物格式强制单层**:`schema` 的 `type` 不得为 `object`;`array` 的 `items` 不得为 object。禁止 `外貌.发色` 这类嵌套路径。描写类 string 字段必须有 `粒度` 与 `地板`;`示例形状` 必须达到地板。
|
||||
6. **池型**:`方向` 必须有 `方向`(对象:方向名→一句概括,4~8 条;默认不举例,至多 1 个专名),`条目` 可 `[]` 或省略。`元素` 必须有可穷举 `条目`,不要写 `方向`。不可穷举项禁止 `池型=元素`,也禁止用 `allowed_values` 冒充。
|
||||
7. 结论为「无需专门规则」时:`rules` 必须 `[]`。上下文策略布尔须与生命周期一致。
|
||||
8. 池≠最终实例;禁止输出具体实例 `records`、变量更新规则、执行单元列表。
|
||||
9. `mount` 默认主世界层 + 旁观维护(旁观只需合同键:rule_id/格式/必要性;描写长文可在投影时 summary);**禁止**写 `order`。summary 见 task。
|
||||
硬规则:
|
||||
1. 输出合法 JSON;每个产物只描述一条规则,禁止 `rules` 数组堆多条。
|
||||
2. `字段` 由用途推出;可省略的字段不写;重叠字段合并。
|
||||
3. `池` 可 `[]`;池选取字段必须能对应到某个池;提示约束字段必须有 `提示`。
|
||||
4. `填写工作流` 必含 `工作流`;`依赖关系` / `自检清单` / `反模式` 为子字段,无则 `[]`,不要拆成正文顶层并列大段。
|
||||
5. `一次生成` 为同批时关系必填且本批至少 2 条;为单条时一次只交 1 条。
|
||||
6. 字段值为扁平可填内容;不要嵌套对象当字段值契约。
|
||||
7. 不在产物正文写自评、追问、开放问题或最终记录。
|
||||
8. `只预生成` 时程序记为中间层;`仅游玩期` / `预生成并游玩期` 记为最终层。
|
||||
9. 字段名和专名用中文;英文专名音译为中文。
|
||||
|
||||
## score
|
||||
|
||||
```score
|
||||
字段妥当:
|
||||
- 5:字段由用途推出,不多不少、不重叠;该有的玩法/效果接口齐全,该省略的已省略。
|
||||
- 3:大体可用,但有 1 个冗余、重叠或明显缺失字段。
|
||||
- 1:通用名片乱塞,或关键效果/接口字段缺失。
|
||||
|
||||
池与提示:
|
||||
- 5:该用池的用了,不该用的改成一句提示约束;池精炼可选取;提示具体可执行。
|
||||
- 3:大体合理,但个别字段误用/漏用池,或池过泛、提示含糊。
|
||||
- 1:大段假穷举,或本该一句话提示却做成巨型词表。
|
||||
|
||||
填写工作流:
|
||||
- 5:阶段简洁有力,依赖与自检/反模式能引导按重要性推进,不臃肿。
|
||||
- 3:有阶段但顺序或依赖不清,或清单偏长却抓不住关键咬合。
|
||||
- 1:无工作流,或与字段脱节,无法指导动态生成。
|
||||
```
|
||||
|
||||
## checklist
|
||||
|
||||
```checklist
|
||||
- [ ] 含 schema / brief / 正文 / 自评 / 追问?
|
||||
- [ ] 自评为必要性 / 属性妥当 / 材料得当 / 格式准确?
|
||||
- [ ] 整条规则必要性有举证?不成立时 rules=[]?
|
||||
- [ ] 建立时:有设计判断(不可穷举 vs 封闭集合)?逐字段无冗余无漏接口?产物为单层 JSON?数量与一次生成已分清?
|
||||
- [ ] 描写字段有粒度+地板,示例形状不是单标签?
|
||||
- [ ] 该细的分级有概括+可感知表象,不是只留阶名?
|
||||
- [ ] 多方向拼一格时有配方?表现是概括+极少例子?
|
||||
- [ ] 池:无则 [];有则标池型;方向池以概括为主、例子极少;元素池仅封闭集合;未把名字/能力做成枚举词表?
|
||||
- [ ] 未夹带最终实例、变量公式、其它对象族、百科?
|
||||
```
|
||||
|
||||
## examples
|
||||
|
||||
```examples
|
||||
好:
|
||||
- 武功典籍:单层含 名字/效果/阶层。阶层短填 + `分级表现` 把每阶写成可感知对照;名字与效果用方向池(方向名+一句概括)。
|
||||
- 外貌:轮廓方向 × 质感方向,配方拼进 `外貌` 一格,不要 `{发色:{}}`。
|
||||
- 「心之彩」:颜色/性格用方向池(如「冷焰:静、刺、余烬不散」),不要列出 20 种色号,也不要堆作品名。
|
||||
- D&D 怪物:名称/等级/攻击骰齐全;等级若只写数字看不懂,补分级表现;气质用方向池+句子地板。
|
||||
- 女主规则:不设「性别」;池可空;内在冲突自由写并给地板。
|
||||
- 互为仇敌的两名开局 NPC:一次生成=同批;可互换的酒馆路人:一次生成=单条。
|
||||
- 现实名车、西幻常见种族 → 无需专门规则。
|
||||
|
||||
坏:
|
||||
- 为气质/花招/绰号/能力各建一个 8~20 词的元素池,填出来全是标签。
|
||||
- 方向池每条挂 3 个作品名或例句,变成假选项库。
|
||||
- 产物写成 `{ "外貌": { "发色": "…" } }` → 非单层。
|
||||
- `示例形状` 里 `能力描述: "时空斩"` 或 `气质: "阴郁"` —— 地板被示例教坏。
|
||||
- 阶层/强度只有「A」「金丹」,没有表象,叙事时没法写。
|
||||
- 女主规则塞「性别」;或怪物规则只有「名字+性格」没有等级/攻击骰。
|
||||
- 有亲属关系的三人却标成单条;或把「总共要 5 个」写成同批但彼此无必须同场的关系。
|
||||
- 把写好的 NPC 整份塞进池当最终实例。
|
||||
- 把社会格局、前史、既成律写成「共通前提」长文塞进规则;应引用舞台/机制,或只留最短填写硬约束。
|
||||
- [ ] 名称与用途已写清?
|
||||
- [ ] 字段由用途推出,无冗余重叠、无关键缺失?
|
||||
- [ ] 池与提示分工正确?
|
||||
- [ ] 填写工作流含子字段:工作流 / 依赖关系 / 自检清单 / 反模式?
|
||||
- [ ] 生命周期与一次生成已钉?
|
||||
- [ ] 未输出最终实例、百科长文、评分或追问?
|
||||
```
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
# 实现机制
|
||||
|
||||
> 技能文档。程序只切割下方 **fence 块**;`##` 标题仅供人读。
|
||||
> 产物外壳对齐:`docs/context-fragment-design.md`;范例:`aesthetics-interaction`。
|
||||
|
||||
## meta
|
||||
|
||||
```meta
|
||||
@@ -10,156 +7,44 @@ name: 实现机制
|
||||
id: mechanism
|
||||
artifact: 设计.实现机制
|
||||
declaration: >
|
||||
整体感觉已钉,故事里有必须当作既成事实的变造法则(科幻意义上的第一初设定)时:点出它们如何约束故事,不解释为何存在
|
||||
保存世界中稳定成立的因果法则,供主世界层推演事件后果、剧情变化和人物反应;一次可写多条共同作用的机制。
|
||||
when: |
|
||||
已有「美学纲领与交互范式」或等价整体感觉,且用户表述或美学卡里有必须当作既成事实的变造法则。
|
||||
例:现代都市但已繁荣一统、存在修炼体系;三体黑暗森林;机器人三定律。
|
||||
特征:故事不必纠结它为什么存在,拿掉则故事骨架会换。
|
||||
本局存在拿掉后会改变故事骨架的既成法则,且后续需要据此推演环境变化、事件后果或人物反应时使用。
|
||||
when_not: |
|
||||
整体感觉未钉 → 先「美学纲领与交互范式」。
|
||||
没有特殊变造,或「特殊」只是身份/开场处境、能被「舞台骨架」「具体实例」「开场白与开场变量」覆盖(如现实世界模拟普通大学生)→ 不排。
|
||||
只有熟悉常识,没有特殊因果法则 → 不使用。
|
||||
地点、势力与社会格局 → 舞台骨架;多份人物或要先审字段的名片 → 生成规则,单条字段很少的具名内容才归具体实例;字段合同 → 生成规则。
|
||||
boundary: |
|
||||
本技能:点名故事当作既成事实的变造法则,写清如何约束后续;不补起源、不写舞台百科、不写人物名片。
|
||||
美学纲领与交互范式:体验与轮转——本步不重定。
|
||||
舞台骨架:环境与社会格局(谁在台上、状况如何转)——本步只钉缺了会换骨架的既成律。
|
||||
具体实例 / 开场白与开场变量:名片与第一屏(身份特殊走它们)。
|
||||
生成规则:可引用本步法则作填写硬约束;本步不写对象族 schema/池,也不把法则扩成名片填写圣经。
|
||||
变量* / 叙事指南 / 故事推进:跨轮真值、遣词、推进(工序编排后二者合为「叙事指南与故事推进」)——本步只钉前提法则。
|
||||
本能力交稳定因果,不预写唯一剧情,也不把所有人物钉成同一种反应。
|
||||
不解释起源,不扩写世界百科,不制定推进节奏、变量表或检定公式。
|
||||
feeds: gm
|
||||
```
|
||||
|
||||
## opening
|
||||
|
||||
```opening
|
||||
整体感觉已有方向时,把故事里「必须当作已经成立」的变造法则点出来(想到什么写什么;点名即可,不必解释它为什么存在)。
|
||||
|
||||
1. 这局有哪些特殊变造,后续情节都得认它?
|
||||
例:现代都市,但世界已繁荣一统;存在修炼体系;黑暗森林法则;机器人三定律。
|
||||
|
||||
2. 拿掉或改弱哪一条,故事骨架会换成另一个?
|
||||
一句话——只点「缺了就不是这个故事」的。
|
||||
|
||||
3. 若只是某人身份特殊、开场处境特殊,先别写在这里(那是舞台 / 人物 / 开场白)。
|
||||
说说这个世界有哪些已经成立、会持续改变事件后果的法则。重点是它如何起作用,以及不同处境和人物会因此面对什么;多条机制可以一起写清。
|
||||
```
|
||||
|
||||
## task
|
||||
|
||||
```task
|
||||
你正在执行「实现机制」。产物必须是 **context-fragment.v1** JSON,写入「设计.实现机制」。
|
||||
把本局承重的既成因果法则整理为实现机制,写入 `设计.实现机制`。一个产物可以包含多条共同作用的机制;分别写清法则内容、生效方式、因果影响和推演口径,再说明机制之间如何共同作用。
|
||||
|
||||
本步钉的是故事的第一初设定:已被承认、不必解释来源、却会决定后续能不能这么写的变造法则。不是科学原理,不是舞台百科,也不是人物名片。
|
||||
先在内部对每条候选做移除检验:拿掉后故事骨架是否改变。`缺失后果`只用于筛选,不得写进实际产物。机制应让主世界层能够推演事件怎样发展、不同人物可能怎样反应,但不得指定唯一剧情或统一人物反应。
|
||||
|
||||
从已确认的整体感觉出发,点出这些法则,写清它们如何约束故事、拿掉会怎样。用户没有这类特殊变造:覆盖检验写明「无特殊法则」,追问确认是否跳过;身份/开场类特殊交给「舞台骨架」「具体实例」「开场白与开场变量」。
|
||||
|
||||
执行顺序:
|
||||
1. 读依赖产物与用户表述,忠实提取整体感觉与已点名的变造;不重做美学/交互。
|
||||
2. 思考阶段判断基准世界可能性范围(只校验,不写入产物)。
|
||||
3. 列出必须当作既成事实的变造法则,检查拿掉后故事骨架是否会换。
|
||||
4. 对每条候选做移除检验。
|
||||
5. 只保留缺了会换骨架的法则;常识、装饰、人物名片、开场处境、后续技能展开对象不纳入。
|
||||
6. 写入正文:依据体验、支撑点(每条法则)、支撑点关系、覆盖判断。
|
||||
7. 填自评与追问(薄弱点才问;题目可空数组)。
|
||||
8. 输出符合 output 契约的 JSON。
|
||||
|
||||
工作姿态——「点出已经成立之物」:不另造体验;不补起源史;不设设定菜单;不题材套件;可整理用户未命名的法则并请修正;二选一先问清再写。
|
||||
|
||||
依赖含混时不越权重做上一步。无法在本步解决的写入开放问题或追问。
|
||||
|
||||
若程序已发默认问题:禁止重复开场;在首答与依赖上补洞。
|
||||
summary:`实现机制 · …`(点题主要支撑)。
|
||||
材料足够便直接成稿;真正会改变法则内容的缺口交给 `self_score.gap`。产物正文不放评分、诊断、追问或开放问题。
|
||||
```
|
||||
|
||||
## principles
|
||||
|
||||
```principles
|
||||
1. 从整体感觉正推到既成法则
|
||||
始终保持:整体感觉 → 故事必须承认的变造法则 → 该法则如何约束后续。
|
||||
题材标签不能代替「这条法则缺了故事会换」。
|
||||
|
||||
2. 法则不是完整设定
|
||||
同一设定可有很多面向,本步只写「当作已经成立、后续必须认」的那一面。钉的就是这类既成事实整句,不顺着去补体系百科:
|
||||
这个世界没有花里胡哨的魔法,只有繁衍到极致的斗气。
|
||||
这个西幻世界里,男尊女卑是常态。
|
||||
这里以三国杀高手为尊。
|
||||
|
||||
3. 以缺失后果检验必要性
|
||||
每条法则至少能回答:拿掉以后,故事骨架会换成哪一个?只能回答「更丰富」「以后可能用得上」的,不纳入。
|
||||
|
||||
4. 本步写的是第一初设定,不是人物或舞台
|
||||
可以是:被直接承认的超常前提、已完成的世界变造(繁荣一统)、约束性公理(黑暗森林、三定律)、体系性硬规则(修炼境界)。
|
||||
人物个性、身份特殊、开场处境、地理社会展开,交给舞台 / 实例 / 开场白。
|
||||
|
||||
5. 基准世界只作内部校验
|
||||
思考阶段判断「什么可能存在」,不要写入产物。最小充分继承:
|
||||
- 现实地球:默认现实物理、心理、社会规律。
|
||||
- 变造地球:继承现实基础,只在用户已表达或体验确实依赖处承认变造。
|
||||
- 具体作品世界:优先最近一层作品设定;原作未定义处再参考其底层世界。
|
||||
- 类型世界:只继承与当前体验有关的类型约定,不带入类型全部常见元素。
|
||||
- 从头创造:显式设定优先;未定义处用最少量常识基础。
|
||||
- 混合或二创:只处理与当前法则有关的继承与变造,不整理完整谱系。
|
||||
候选超出已知可能性时先确认变造是否本在用户设想中,不暗中加入。
|
||||
|
||||
6. 法则可以不解释来源
|
||||
写清「它怎样约束故事」,不要求「宇宙为什么会有它」。
|
||||
不为显得严谨而补起源、发明者、历史沿革或伪科学理论,除非「追查来源」本身就是这条法则要支撑的体验。
|
||||
|
||||
7. 按真实复杂度描述
|
||||
一句话能说清就用一句话;否则才按过程、张力、层次、构成、循环或关系网展开。
|
||||
多支点关系简单时用自然语言(共同支撑 / 前提 / 反衬 / 制衡);关系确实复杂才展开,不为形式制造图谱。
|
||||
|
||||
8. 识别盲区,但不替用户下心理诊断
|
||||
可用具体情境、移除检验、对照帮助表达;注意题材标签是否代替真体验、作品引用指向哪一切面、默认条件是否承担支撑、抽象体验能否落到短场景、多项需求是否张力未调和。
|
||||
不能把拒绝解释成隐藏欲望,不能把犹豫诊断为羞耻或自我欺骗。用户明确拒绝即边界;陈述与反馈不一致时只中性复述差异并请求确认。
|
||||
|
||||
9. 宁少勿多
|
||||
支撑点无预设数量。主要支撑已覆盖、剩余只是常识/装饰/后续展开对象时立即停止。
|
||||
不为显得完整而增加人物、势力、地点、规则或冲突。
|
||||
```
|
||||
|
||||
## probe
|
||||
|
||||
```probe
|
||||
只追问会改变核心支点、支撑切面或支撑关系的缺口。每轮 1~2 点。依赖产物或用户已答清的禁止重问。
|
||||
|
||||
优先方式:
|
||||
|
||||
1. 移除检验
|
||||
暂时拿掉疑似支点,问核心感觉是否仍成立。
|
||||
例:「如果他不是在主动隐瞒,而是真的无法识别异常,这种紧张感还是你要的吗?」
|
||||
|
||||
2. 短场景落地
|
||||
抽象感觉 → 紧贴现有材料的短场景请修正。
|
||||
例:「更接近哪种瞬间:证据已经出现却被他自行合理化,还是他其实察觉了但选择不追问?」
|
||||
|
||||
3. 支撑关系核对
|
||||
已识别元素但作用不清 → 核对缺失后果。
|
||||
例:「这个身份差距主要制造不可抗拒的吸引,还是让暴露后的代价变得更高?」
|
||||
|
||||
4. 张力辨认
|
||||
两项体验可能由相反条件支撑 → 放进同一具体描述确认。
|
||||
例:「你既要长期安全,又要濒临暴露;是否意味着真正需要的是『客观上有保护,但角色主观上始终无法确信安全』?」
|
||||
|
||||
5. 引用拆解
|
||||
引用作品/类型 → 只问与支撑机制有关的切面。
|
||||
例:「你提到这部作品,主要是想保留其中的信息差、人物关系,还是那个无法撤销的超常前提?」
|
||||
|
||||
提问给出可直接采用或微调的完整句子,不罗列大批名词选项。对照只用于暴露差异,不变成设定菜单。
|
||||
|
||||
能稳妥推断时:先写成暂定支点并复述,请用户校正;不要要求用户从零发明专业术语。
|
||||
|
||||
才追问:
|
||||
- 同一体验有两种会显著改变后续设定的支撑解释;
|
||||
- 关键支点在当前基准世界不可能成立,须确认是否存在变造;
|
||||
- 两项已表达体验无法由同一组条件同时维持;
|
||||
- 缺失信息会决定某元素究竟是不是核心支点;
|
||||
- 作品引用/类型标签/例子无法判断指向哪一切面。
|
||||
|
||||
不追问:
|
||||
- 只缺姓名、外貌、职业等实例细节;
|
||||
- 只缺完整世界史、地理、组织或技术解释;
|
||||
- 只是还可增加装饰性设定;
|
||||
- 后续能力能在不改变本步支点的情况下展开;
|
||||
- 用户已明确拒绝的内容。
|
||||
1. 机制是稳定因果:写世界承认什么、何时生效、会带来什么后果。
|
||||
2. 内部使用移除检验筛选承重法则;只能回答“更丰富”或“以后也许有用”的候选不纳入。缺失后果不输出。
|
||||
3. 因果影响写行动可能性、代价、资源与关系怎样改变;推演口径写推演情节和人物反应时必须考虑什么。
|
||||
4. 人物反应保持开放:同一机制下,人物可因认知、利益、身份和性格不同而隐藏、合作、利用、抵抗或误判。
|
||||
5. 多条机制可一次生成。分别写清后,再说明它们是前提、叠加、制衡还是反馈循环;没有关系则不硬连。
|
||||
6. 机制不等于题材标签。“灵气复苏”“全民直播求生”必须拆成可推演的生效方式与因果链。
|
||||
7. 一句话够用便停止;不补起源史、体系百科、人物卡、剧情大纲或推进节奏。
|
||||
```
|
||||
|
||||
## output
|
||||
@@ -168,121 +53,87 @@ summary:`实现机制 · …`(点题主要支撑)。
|
||||
{
|
||||
"schema": "context-fragment.v1",
|
||||
"技能": "实现机制",
|
||||
"brief": "一句话:主要靠什么撑住核心体验",
|
||||
"brief": "一句话说明哪些稳定因果在改变本局世界",
|
||||
"mount": ["world-simulator"],
|
||||
"稳变": "stable",
|
||||
"正文": {
|
||||
"依据的核心体验": [
|
||||
"从依赖产物忠实提取的体验锚点;此处不重做美学纲领"
|
||||
],
|
||||
"支撑点": [
|
||||
"既成机制": [
|
||||
{
|
||||
"名称": "简短、稳定、对人可读",
|
||||
"归属元素": "角色 / 关系 / 群体 / 世界条件 / 规则 / 情境…",
|
||||
"支撑切面": "该元素中真正相关的那一面",
|
||||
"切面形态": {
|
||||
"结构": "一句话|过程|张力|层次|构成|循环|关系网",
|
||||
"概述": "最短充分描述",
|
||||
"展开": [{ "部分": "仅必要时", "描述": "…" }]
|
||||
},
|
||||
"对应体验": ["直接服务的体验锚点"],
|
||||
"如何支撑": "条件 / 限制 / 对照 / 张力如何让体验成立",
|
||||
"缺失后果": "拿掉或改弱后具体损失什么(禁止只写「变差」)",
|
||||
"完备度": "…%"
|
||||
"名称": "简短稳定的人话名",
|
||||
"法则内容": "世界中稳定成立什么",
|
||||
"生效方式": [
|
||||
"对谁、在什么条件下、以什么方式起作用"
|
||||
],
|
||||
"因果影响": [
|
||||
"它怎样改变行动可能性、代价、资源、关系或事件后果"
|
||||
],
|
||||
"推演口径": [
|
||||
"推演剧情发展和人物反应时必须结合哪些认知、利益、身份或处境"
|
||||
],
|
||||
"边界或例外": [
|
||||
"确有必要才写;无则省略"
|
||||
]
|
||||
}
|
||||
],
|
||||
"支撑点关系": [
|
||||
"共同作用": [
|
||||
{
|
||||
"涉及": ["支撑点名称"],
|
||||
"关系": "前提|共同支撑|反衬|制衡|递进|循环|…",
|
||||
"体验作用": "为何必须保留这段关系"
|
||||
}
|
||||
],
|
||||
"覆盖检验": {
|
||||
"主要支撑是否覆盖核心体验": "是|否|部分",
|
||||
"说明": "还有哪块体验没有支点、或支点过密",
|
||||
"宁少勿多说明": "为何到此停止"
|
||||
}
|
||||
},
|
||||
"自评": {
|
||||
"维度": [
|
||||
{
|
||||
"名": "覆盖度",
|
||||
"分数": 8,
|
||||
"说明": "能否完全覆盖用户已表达的规则/前提/硬条件(未漏掉用户点名的承重约束)"
|
||||
},
|
||||
{
|
||||
"名": "充分度",
|
||||
"分数": 8,
|
||||
"说明": "支撑点合起来能否充分展示用户想要的核心体验(缺了是否撑不住感觉)"
|
||||
},
|
||||
{
|
||||
"名": "克制度",
|
||||
"分数": 8,
|
||||
"说明": "是否精确到支撑切面;无多余百科、人物卡、起源史或题材默认拓展"
|
||||
}
|
||||
],
|
||||
"薄弱点": "一句话"
|
||||
},
|
||||
"追问": {
|
||||
"导语": "为钉准支撑切面,还需确认:",
|
||||
"题目": [
|
||||
{
|
||||
"问": "移除检验或短场景落地题…",
|
||||
"建议选项": ["选项A(可带短场景)", "选项B", "其它(请写明)"],
|
||||
"示例": "可选示范句"
|
||||
"涉及机制": ["机制 A", "机制 B"],
|
||||
"关系": "前提|叠加|制衡|反馈循环",
|
||||
"推演影响": "共同作用时会怎样改变事件与人物选择空间"
|
||||
}
|
||||
]
|
||||
},
|
||||
"开放问题": []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
硬规则(公共三段 + 本技正文):
|
||||
1. 合法 JSON;必含 `schema` / `技能` / `brief` / `正文` / `自评` / `追问`(题目可 `[]`)。
|
||||
2. **公共三段**:`正文`=产物主体;`自评`=打分(**分数 0–10 十分制**);`追问`=导语+建议选项。禁止整份散文壳。
|
||||
3. **本技正文**:`依据的核心体验` + `支撑点[]` + `支撑点关系[]` + `覆盖检验`。基准世界/推理过程/淘汰候选不得写入。
|
||||
4. 每个支撑点须有归属元素、支撑切面、如何支撑、缺失后果;简单支点结构=「一句话」、展开=`[]`。
|
||||
5. `mount` 默认主世界层(`world-simulator`);**禁止**写 `order`。承重规则进 GM,不进转述全文。
|
||||
6. `稳变` 多为 `stable`(支点定稿后少改);若用户明确机制会阶段性改换可标 `semi`。
|
||||
7. 不要输出角色卡、世界百科、剧情大纲、叙事文风、变量表、执行单元列表。
|
||||
8. summary:`实现机制 · {brief 缩略}`。
|
||||
硬规则:
|
||||
1. 输出合法 JSON;`既成机制` 可含多条承重机制。
|
||||
2. `缺失后果`、移除检验和被淘汰候选只用于内部推理,不得进入正文。
|
||||
3. 每条机制必须写 `法则内容 / 生效方式 / 因果影响 / 推演口径`。
|
||||
4. `共同作用` 只写真实存在的机制关系;无则 `[]`。
|
||||
5. 推演口径提供因果依据,不指定唯一剧情,也不规定所有人物作同一种反应。
|
||||
6. 不在产物正文写自评、追问、开放问题、起源百科或检定公式。
|
||||
7. `mount` 固定为 `["world-simulator"]`。
|
||||
|
||||
本步完成:主要支撑已覆盖;每点过「如何支撑/缺了怎样」;无暗中变造;剩余归常识或下游技能。
|
||||
## score
|
||||
|
||||
```score
|
||||
骨架必要度:
|
||||
- 5:每条都是拿掉后会改变故事骨架的承重法则,没有常识、装饰或下游细节。
|
||||
- 3:主体承重,但夹有一条作用较弱或可交给其它能力的内容。
|
||||
- 1:只有题材标签或装饰设定,没有稳定承重法则。
|
||||
|
||||
因果清晰度:
|
||||
- 5:生效条件与影响链清楚,能据此推演环境、事件和关系怎样变化。
|
||||
- 3:法则方向正确,但生效方式或一段关键因果仍需猜测。
|
||||
- 1:只说“存在某设定”,无法推出后果。
|
||||
|
||||
推演开放度:
|
||||
- 5:能支持人物按不同认知、利益、身份和性格作出多种合理反应,而不失去法则约束。
|
||||
- 3:提供了反应依据,但部分表述暗含单一路线。
|
||||
- 1:直接预写唯一剧情,或把所有人物规定成同一种反应。
|
||||
```
|
||||
|
||||
## checklist
|
||||
|
||||
```checklist
|
||||
- [ ] 含 schema / brief / 正文 / 自评 / 追问?
|
||||
- [ ] 依据的核心体验忠实来自上游,非本步新设计?
|
||||
- [ ] 每个支撑点有归属元素、切面、如何支撑、缺失后果?
|
||||
- [ ] 自评含覆盖度 / 充分度 / 克制度?
|
||||
- [ ] 未把身份/开场处境写成法则?未写成设定菜单、人物卡、起源史、order 表?
|
||||
- [ ] 宁少勿多;支撑点关系仅保留有体验作用的项(可空)?
|
||||
- [ ] 追问只打会改变支点的缺口,带建议选项?
|
||||
- [ ] 每条候选已在内部通过移除检验,且缺失后果未写入产物?
|
||||
- [ ] 法则内容、生效方式、因果影响与推演口径均可执行?
|
||||
- [ ] 多条机制的共同作用已写清,或明确无须关联?
|
||||
- [ ] 能支持不同人物作出不同但合理的反应?
|
||||
- [ ] 未写成题材标签、世界百科、剧情大纲或裁定表?
|
||||
- [ ] 产物正文不含评分、诊断、追问?
|
||||
```
|
||||
|
||||
## examples
|
||||
|
||||
```examples
|
||||
好:
|
||||
- brief「现代都市认修炼境界与繁荣一统」;正文有依据体验 + 支撑点(修炼可越级、世界已一统)过移除检验;自评三维;无宗门百科。
|
||||
- 公理支点「黑暗森林」:一句话钉「猜到即打击」;不写宇宙社会学论文。
|
||||
- 整份落在 context-fragment.v1;支撑点切片示例:
|
||||
灵气复苏:
|
||||
- 灵气重新进入环境,生物可吸收并强化、异变;浓度、接触方式与修炼方法影响变化速度和可控程度。
|
||||
- 因果上会改变资源价值和既有力量关系;推演人物时结合其认知、资源、身份得失与控制能力,不能默认人人都追求修炼。
|
||||
|
||||
{
|
||||
"名称": "黑暗森林",
|
||||
"归属元素": "宇宙公理",
|
||||
"支撑切面": "暴露坐标即招来打击,文明默认隐藏",
|
||||
"切面形态": { "结构": "一句话", "概述": "被承认的既成法则,不解释为何如此。", "展开": [] },
|
||||
"对应体验": ["猜疑链下的生存紧张"],
|
||||
"如何支撑": "后续情节不能靠「对外广播求援」轻松收场。",
|
||||
"缺失后果": "若可公开求援而不被打击,故事从藏匿改成外交。",
|
||||
"完备度": "90%"
|
||||
}
|
||||
|
||||
坏:
|
||||
- 设定菜单:「可选诅咒/寄生物/邪神…」
|
||||
- 把普通大学生的身份特殊写成法则;起源史、人物卡、舞台百科
|
||||
- 只有散文支点、无 schema / 自评 / 追问
|
||||
全民直播求生:
|
||||
- 可拆为“强制进入求生场”“关键行为公开直播”“观众反馈可进入场内(若本局采用)”等多条机制。
|
||||
- 公开评价与即时生存会共同改变选择代价;人物是否表演、隐藏或利用镜头,应由其是否在乎声誉、是否相信能返回现实等条件推出。
|
||||
- 观众若只能观看,就不能推演出打赏资源;边界会直接改变因果链。
|
||||
```
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
# 叙事指南
|
||||
|
||||
> 对话落盘技能。程序只切割下方 **fence 块**;`##` 标题仅供人读。
|
||||
> 产物外壳:`docs/context-fragment-design.md`。
|
||||
> 工序编排仍用「叙事指南与故事推进」;本步只钉怎么写,不钉故事怎么往前。
|
||||
|
||||
## meta
|
||||
|
||||
```meta
|
||||
@@ -11,75 +7,46 @@ name: 叙事指南
|
||||
id: narrative-guide
|
||||
artifact: 设计.叙事指南
|
||||
declaration: >
|
||||
美学已有方向时:把感觉落成可执行的遣词、笔墨落点、禁忌与写法档;不重做体验问卷,也不钉故事怎么往前
|
||||
从文风库选取本局写法,落成可执行的遣词要求、笔墨落点、少量禁忌与可选写法档;不钉故事怎么往前。
|
||||
when: |
|
||||
已有美学纲领(或等价),但游玩正文仍不知道怎么写:遣词什么味道、笔墨落谁身上、场合换哪一档写法、绝对不能写什么。
|
||||
用户主动谈文风、措辞禁忌、写法档(官能/爽文/恐怖/情色等)也排。
|
||||
交互范式或故事推进已钉停笔/轮转,不构成不排本步的理由。
|
||||
已有美学纲领(或等价),但游玩正文仍不知道怎么写:遣词什么要求、笔墨落谁身上、场合是否要换写法档、有哪些绝对不能写。
|
||||
用户主动谈文风、措辞、写法档时也使用。
|
||||
when_not: |
|
||||
本技能只给对话落盘。工序编排不排本步(编排池是「叙事指南与故事推进」)。
|
||||
美学未钉 → 先「美学纲领与交互范式」。
|
||||
用户只要感觉/站位、不要文风细则 → 不排,沿用美学。
|
||||
只谈故事怎么往前、用户输入用法 → 「故事推进」。
|
||||
版式/壳 → 「正文组成」;每轮看什么(字数/变量/日期)→ 「回复呈现」。
|
||||
用户明确说本局不用文风细则,且美学里已有可执行遣词方向 → 可不使用。
|
||||
只谈推进、输入用法 → 「故事推进」。版式 → 「正文组成」。字数与可见区域 → 「回复呈现」。
|
||||
boundary: |
|
||||
本技能:一份 context-fragment,写清怎么写(遣词、笔墨、禁忌、写法档)。
|
||||
故事推进:怎么往前、输入用法——本步不定。
|
||||
美学:感觉与轮转。生成规则/变量*:合同与表。
|
||||
本能力只交怎么写:选用文风、遣词要求、笔墨落点、少量绝对禁忌、可选写法档。
|
||||
推进与输入用法归故事推进;体验契约归美学纲领。
|
||||
feeds: narrator,gm
|
||||
```
|
||||
|
||||
## opening
|
||||
|
||||
```opening
|
||||
美学里的感觉已有方向时,请谈「正文怎么写」(想到什么写什么):
|
||||
|
||||
1. 像什么文风?遣词什么味道?笔墨花在谁身上?有什么绝对不能写?
|
||||
2. 正文若有几种写法档要切换(官能 / 爽文 / 恐怖 / 情色这类),点名即可;没有就说没有。
|
||||
说说这局正文希望读起来像什么。可以直接点文风方向,或说不同场合要不要换一种写法。重点写“要怎么写”,只有真正碰不得的再提禁忌。
|
||||
```
|
||||
|
||||
## task
|
||||
|
||||
```task
|
||||
你正在执行「叙事指南」。产物必须是 **context-fragment.v1** JSON,写入「设计.叙事指南」。
|
||||
把本局写法整理为叙事指南,写入 `设计.叙事指南`。先用 `read_library_entry` 从绑定的文风库读取最相关条目(默认 1~2;确需第三种时须写 reason 并标明适用场合),再按本局体验本局化,写成可执行的风格推荐与要求。
|
||||
|
||||
把美学里的感觉落成可执行的写法。作用对象是本局游玩正文(转述与主世界层都读这份)。
|
||||
产物回答:选了哪些文风、各自用在什么场合;遣词与笔墨要怎么做;只有少数绝对禁忌。除禁忌外,少写“不要怎样”,多写“要怎样”。不写示范段落,不写推进与输入用法。
|
||||
|
||||
执行顺序:
|
||||
1. 若提示中有【文风库 · 可选用】且用户点名/对上某包:优先按该包落成「怎么写」,再按本局美学微调;不要重做文风问卷。
|
||||
2. 否则继承美学体验与禁忌,改写成做法向句子。
|
||||
3. 写纲领、遣词(含示范)、笔墨焦点、禁忌/不偏好。
|
||||
遣词用户未提段落形态时,默认写入:完整长自然段叙述,不要单句成段(篇幅数字交给「回复呈现」)。
|
||||
4. 情境备用 = 写法档(体裁/调性容器),不是具体桥段。有切换则每条写:名字=档名(如「官能小说」「网文爽文」「展示恐怖」「展示情色」),用法=该档怎么遣词、笔墨落哪、何时切进来。无切换则 `[]`。用户若报出「当众折辱」「独处一室」这类场合,升一档写进对应写法档,场合本身不单列。
|
||||
5. 自评 + 追问。故事怎么往前、用户输入用法交给「故事推进」,本步不写。
|
||||
|
||||
summary:`叙事指南 · …`
|
||||
材料足够便直接成稿;真正会改写法的缺口交给 `self_score.gap`。产物正文不放评分、诊断、追问或开放问题。
|
||||
```
|
||||
|
||||
## principles
|
||||
|
||||
```principles
|
||||
1. 感觉 vs 做法:美学写要什么爽;本步写怎么遣词、笔墨落哪。
|
||||
2. 遣词与禁忌要可执行;绝对禁忌 ≠ 不偏好。
|
||||
用户未提及时:完整长自然段,不要单句成段。
|
||||
3. 情境备用是写法档:名字用「官能小说」「网文爽文」「展示恐怖」「展示情色」同级;用法写该档遣词与何时切入。无切换则空数组。
|
||||
4. 字数区间归「回复呈现」;推进与输入用法归「故事推进」。
|
||||
```
|
||||
|
||||
## probe
|
||||
|
||||
```probe
|
||||
本步只钉三件:遣词可执行、笔墨落点、绝对禁忌(含写法档有无切换)。
|
||||
|
||||
已在美学写清的感觉改写成做法向句子,不重问「想感受到什么」。
|
||||
用户未提段落形态时:写入长自然段默认并简短确认即可。
|
||||
|
||||
缺遣词/笔墨/禁忌其中一件:在产物「追问」写 1~2 点(建议选项 + 短句示例)。三件够验收:`追问.题目` 为 `[]`。
|
||||
|
||||
用户谈到推进、停笔、大纲扩写:写入「开放问题」,交给「故事推进」。
|
||||
用户谈到变量增减、生成规则、舞台百科:只抽取能改遣词 / 禁忌的一句,其余写入「开放问题」。
|
||||
用户提到要换写法:收成写法档写入「情境备用」(官能 / 爽文 / 恐怖 / 情色这类),不追问具体场合怎么摆;没提则 `[]`。
|
||||
字数问「回复呈现」;本步不追问字数。
|
||||
1. 文风库是参考,不是照抄:读取后改成本局可执行要求,并记下适用场合与微调。
|
||||
2. 优先 1~2 种文风;确需第三种时,每种都必须有清晰切换场合,禁止无差别堆叠。
|
||||
3. 写法用“风格推荐 + 要求”表达:对象、节奏、注意力落点、句段形态都写成要做什么。示范段落不写。
|
||||
4. 绝对禁忌只留真正碰不得的少数几条;其余偏好都改成正向做法。
|
||||
5. 写法档是多文风/多调性容器:有切换才写,无切换整项省略。场合名要能指导何时切入,不是具体剧情桥段清单。
|
||||
6. 不写故事推进、输入用法、字数区间或版式壳。
|
||||
```
|
||||
|
||||
## output
|
||||
@@ -88,74 +55,77 @@ summary:`叙事指南 · …`
|
||||
{
|
||||
"schema": "context-fragment.v1",
|
||||
"技能": "叙事指南",
|
||||
"brief": "一句话:体裁 + 笔墨重心",
|
||||
"brief": "一句话:本局正文怎么写",
|
||||
"mount": ["narrator", "world-simulator"],
|
||||
"稳变": "stable",
|
||||
"正文": {
|
||||
"依据的体验": ["…"],
|
||||
"叙事纲领": {
|
||||
"核心定位": "…",
|
||||
"叙事使命": "…",
|
||||
"叙事追求": ["…"]
|
||||
},
|
||||
"风格与遣词": {
|
||||
"叙述者人格": "…",
|
||||
"整体基调": "…",
|
||||
"遣词造句": ["原则 - 做法 → 目的"],
|
||||
"示范": [{ "情境": "常态", "段落": "…" }]
|
||||
},
|
||||
"笔墨焦点": {
|
||||
"观众与代入取向": "…",
|
||||
"详写优先": ["…"],
|
||||
"可略写": ["…"]
|
||||
},
|
||||
"禁忌与不偏好": {
|
||||
"绝对禁忌": ["…"],
|
||||
"不偏好": ["…"]
|
||||
},
|
||||
"情境备用": [
|
||||
"选用文风": [
|
||||
{
|
||||
"名字": "展示情色",
|
||||
"用法": "切到身体与权力交换时:感官词加重、节奏放慢、对话变短;切回日常则收。"
|
||||
},
|
||||
{
|
||||
"名字": "网文爽文",
|
||||
"用法": "打脸/反转时:可复述对方原话、结果写满、过程可略。"
|
||||
"库条目": "style-packs 中的 id 或名称",
|
||||
"适用场合": "默认全文 / 亲密场面 / 战斗反转 等",
|
||||
"本局要求": "从该条目提炼并改写后的可执行写法要求"
|
||||
}
|
||||
],
|
||||
"内容与表达": {
|
||||
"视角执行": "…",
|
||||
"段落形态": "默认:完整长自然段叙述,不要单句成段;用户另定则改写",
|
||||
"场景详略映射": ["…"]
|
||||
}
|
||||
},
|
||||
"自评": {
|
||||
"维度": [
|
||||
{ "名": "写法可执行", "分数": 8, "说明": "…" },
|
||||
{ "名": "与美学对齐", "分数": 8, "说明": "…" }
|
||||
],
|
||||
"薄弱点": ""
|
||||
},
|
||||
"追问": { "导语": "", "题目": [] },
|
||||
"开放问题": []
|
||||
"遣词": {
|
||||
"整体取向": "本局叙述应呈现的风格方向",
|
||||
"风格推荐": ["优先采用的节奏、用词层级、句段形态"],
|
||||
"写法要求": ["要写到什么密度、如何处理对话与叙述、注意力放哪"]
|
||||
},
|
||||
"笔墨焦点": {
|
||||
"详写": ["应花笔墨的对象与感受"],
|
||||
"可略": ["可一带而过的部分"]
|
||||
},
|
||||
"禁忌": ["仅少数绝对不能写的内容;无则省略"],
|
||||
"写法档": [
|
||||
{
|
||||
"名字": "档名,如展示情色 / 网文爽文",
|
||||
"适用场合": "何时切到这一档",
|
||||
"写法要求": "这一档要怎么写、笔墨落哪、如何切回"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
硬规则:合法 JSON;示范至少 1 段;禁忌与不偏好都要有;禁止 order;summary 见 task。
|
||||
情境备用若有:名字必须是写法档(官能小说 / 网文爽文 / 展示恐怖 / 展示情色 同级),用法写该档的遣词与切入口;不是「当众折辱」「物资告急」这类具体桥段。无切换则 `[]`。
|
||||
不写「推进与决策」。
|
||||
硬规则:
|
||||
1. 输出合法 JSON;`选用文风` 默认 1~2 项,最多 3 项,每项必须有适用场合与本局要求。
|
||||
2. 不写示范段落、示例正文或仿写样章。
|
||||
3. 除 `禁忌` 外,正文以正向要求为主;空话氛围词改成可执行做法。
|
||||
4. `写法档` 无切换时整项省略;有则每档写清场合与写法要求。
|
||||
5. 不在产物正文写自评、追问、开放问题或推进/输入用法。
|
||||
6. `mount` 固定包含 `narrator` 与 `world-simulator`。
|
||||
|
||||
## score
|
||||
|
||||
```score
|
||||
遣词可执行:
|
||||
- 5:风格推荐与要求具体到能直接落笔,无需再猜味道。
|
||||
- 3:大方向清楚,但仍有几处停留在氛围概括。
|
||||
- 1:只有题材或情绪标签,无法指导遣词。
|
||||
|
||||
笔墨与禁忌:
|
||||
- 5:详略落点清楚;绝对禁忌少而可执行;其余多为正向要求。
|
||||
- 3:落点大体有,但禁忌偏多或详略仍含糊。
|
||||
- 1:几乎不谈笔墨,或堆满否定句却无正向做法。
|
||||
|
||||
文风选用:
|
||||
- 5:1~2(偶发 3)条贴合本局;多档时切换场合清楚,无重复堆叠。
|
||||
- 3:选用相关,但场合区分弱,或有一条不够贴。
|
||||
- 1:未读库硬编,或多种文风无差别并列。
|
||||
|
||||
本局化:
|
||||
- 5:库内容已改成本局说法与要求,能看出针对本局体验的取舍。
|
||||
- 3:有微调,但仍有整段近乎原文搬用。
|
||||
- 1:基本照抄库条目,或与本局体验脱节。
|
||||
```
|
||||
|
||||
## checklist
|
||||
|
||||
```checklist
|
||||
- [ ] 遣词+示范+禁忌够用?
|
||||
- [ ] 情境备用若有,是否是写法档而非具体桥段?无切换是否为 `[]`?
|
||||
- [ ] 未把推进/输入用法写进本步?
|
||||
```
|
||||
|
||||
## examples
|
||||
|
||||
```examples
|
||||
好:一次钉清遣词 + 笔墨落点 + 禁忌;情境备用是「展示情色」「网文爽文」两档及切换时笔墨。
|
||||
坏:追问好感公式 / 事件池 / 大纲扩写还是扮演停笔;或把「当众折辱」「独处一室」当成情境备用。
|
||||
- [ ] 已按需读取文风库并本局化?
|
||||
- [ ] 选用文风 1~2(最多 3),各有适用场合?
|
||||
- [ ] 遣词是风格推荐与要求,不是示范段落?
|
||||
- [ ] 除少数禁忌外,多为“要怎样”?
|
||||
- [ ] 未写入推进、输入用法、字数或版式?
|
||||
- [ ] 产物正文不含评分、诊断、追问?
|
||||
```
|
||||
|
||||
@@ -36,9 +36,10 @@ feeds: narrator,gm
|
||||
|
||||
1. 像什么文风?遣词什么味道?笔墨花在谁身上?有什么绝对不能写?
|
||||
2. 故事往前靠什么:场面自己推、等用户决策,还是无专门推进?正文若有几种写法档要切换(官能 / 爽文 / 恐怖 / 情色这类),点名即可;没有就说没有。
|
||||
3. 用户每轮输入怎么用?未提及时按体验二选一写入(可简短确认,不必当缺口追问):
|
||||
- **大纲扩写**(写手/成稿向):输入=待扩写为事实的大纲;细化完全后写正文;不超出大纲未涉及的剧情,完整演绎大纲。
|
||||
- **扮演停笔**(代入扮演、等用户决策):用户本轮行为与对话作正文开头并扩写填满;不可写用户角色未给出的其它行为/对话;不可替用户角色做决定;正文停在其他角色等用户角色回应之刻。
|
||||
3. 用户每轮输入怎么用?未提及时按体验三选一写入(可简短确认,不必当缺口追问):
|
||||
- **大纲扩写**(写手/成稿向):输入占比高=待演成场面的大纲。节拍上的人、事、话、结果不动;节拍之间补具体动作把处境演出来。拓写宜充分;续写段宜短。
|
||||
- **续写**(代入扮演、等用户决策):输入往往较短;拓写跟已声明内容;续写多于大纲扩写、短于自动写入。
|
||||
- **自动写入**(短指令驱动):输入=短需求(meta 或极简扮演);续写宜长;可按主角设定代写用户角色。
|
||||
```
|
||||
|
||||
## task
|
||||
@@ -57,9 +58,10 @@ mount:`narrator` 与 `world-simulator` 都挂同一份全文——不要为省
|
||||
4. 情境备用 = 写法档(体裁/调性容器),不是具体桥段。有切换则每条写:名字=档名(如「官能小说」「网文爽文」「展示恐怖」「展示情色」),用法=该档怎么遣词、笔墨落哪、何时切进来。无切换则 `[]`。用户若报出「当众折辱」「独处一室」这类场合,升一档写进对应写法档,场合本身不单列。
|
||||
5. 写推进与决策(可写「无专门推进」);变量只写钩子。
|
||||
「用户输入用法」未提及时:按美学/交互范式选一种写入产物——
|
||||
- 写手/大纲成稿 → **大纲扩写**(输入=大纲→细化成正文;不越界、完整演绎);
|
||||
- 代入扮演、轮转等用户决策 → **扮演停笔**(输入的行为与对话作正文开头并扩写填满;不写用户角色未给出的行为/对话;不替用户角色做决定;停在其他角色等回应之刻)。
|
||||
两种都像则写进追问让用户点一次;不要混成半套规则。
|
||||
- 写手/大纲成稿 → **大纲扩写**;
|
||||
- 代入扮演、轮转等用户决策 → **续写**;
|
||||
- 短指令/meta、希望 AI 代演用户角色 → **自动写入**。
|
||||
多种都像则写进追问让用户点一次;不要混成半套规则。产物须装载对应准则(见 principles)。
|
||||
6. 自评 + 追问。
|
||||
|
||||
summary:`叙事指南与故事推进 · …`
|
||||
@@ -72,9 +74,17 @@ summary:`叙事指南与故事推进 · …`
|
||||
2. 遣词与禁忌要可执行;绝对禁忌 ≠ 不偏好。
|
||||
用户未提及时:完整长自然段,不要单句成段。
|
||||
3. 推进可轻可无;表规格交给变量/生成规则。
|
||||
用户输入用法两套可选默认(未提及时择一,勿混用):
|
||||
- 大纲扩写:输入=大纲→扩写为事实→细化后正文;不发明大纲未涉及的剧情,完整演绎大纲。
|
||||
- 扮演停笔:用户本轮行为与对话→正文开头并扩写填满;禁止描述用户角色未输入的其它行为/对话;禁止替用户角色做决定;正文停在其他角色需要用户角色回应之刻。
|
||||
用户输入用法三选一(未提及时择一,勿混用)。差别在输入量、拓写/续写篇幅与是否代演用户角色。
|
||||
|
||||
【拓写 / 续写准则】(大纲扩写、续写 必装;自动写入不装「禁代演」)
|
||||
- 拓写(大纲扩写):输入是待演成场面的大纲,不是终稿句子。已写明的人、事、话、结果保持不变,每个节拍都要演到。节拍之间补具体动作、物件和当场的小纠正,把场合里已有的处境演出来;大纲没写这些动作也要写。例:大纲「我去相亲、坐了下来,她说要婚后自由出轨权,我拒绝了」——坐下前写先要一杯咖啡,又急忙改成两杯,显出局促;她的要求和拒绝的结果不动。只加形容词、比喻或气氛、不增加动作,不算拓写。另起新冲突、新目标,或改掉已定结果,才算出界。
|
||||
- 拓写(续写):只写用户本轮已声明的行为与对话;可补该动作怎么发生、他人怎么接;不另写用户没声明的新决定。
|
||||
- 续写:不可再写用户角色的实际行动/说话/带倾向抉择;最多被动占位(望去/让开);禁止替用户角色做决定。整轮停在其他角色等用户角色行动抉择之刻。
|
||||
- 篇幅:大纲扩写→拓写充分、续写短;续写用法→续写多于大纲扩写、短于自动写入。
|
||||
|
||||
- **大纲扩写**:输入=大纲→按上列大纲拓写演成正文,节拍之间补具体动作;装载上列准则。主角设定偏好:客观为主、少性格。
|
||||
- **续写**(旧称扮演停笔):行为与对话作拓写起点;装载上列准则;主角设定偏好同大纲扩写。
|
||||
- **自动写入**:短 meta/极简扮演→长续写;允许按主角设定代写用户角色;倾向详钉性格。
|
||||
4. 情境备用是写法档:名字用「官能小说」「网文爽文」「展示恐怖」「展示情色」同级;用法写该档遣词与何时切入。无切换则空数组。
|
||||
5. 工序编排保持本步一份产物;对话落盘拆成「叙事指南」与「故事推进」,不要在本步执行时再拆技能。
|
||||
6. 字数区间归「回复呈现」;本步管怎么写与怎么推,不重钉字数。
|
||||
@@ -88,7 +98,7 @@ summary:`叙事指南与故事推进 · …`
|
||||
已在美学写清的感觉改写成做法向句子,不重问「想感受到什么」。
|
||||
|
||||
用户未提段落形态时:写入长自然段默认并简短确认即可。
|
||||
用户未提输入用法时:按体验选「大纲扩写」或「扮演停笔」写入产物并简短确认;两者都像才追问二选一,不必当成大缺口。
|
||||
用户未提输入用法时:按体验选「大纲扩写」「续写」或「自动写入」写入产物并简短确认;多种都像才追问点选,不必当成大缺口。
|
||||
|
||||
缺遣词/笔墨/禁忌/推进其中一件:在产物「追问」写 1~2 点(建议选项 + 短句示例)。四件够验收:`追问.题目` 为 `[]`。推进方式不明则正文写「无专门推进」,不问好感公式、进度表或事件池。
|
||||
|
||||
@@ -141,10 +151,11 @@ summary:`叙事指南与故事推进 · …`
|
||||
"推进与决策": {
|
||||
"有无专门推进": "有|无",
|
||||
"驱动力来源": "…",
|
||||
"用户输入用法": "择一写入。大纲扩写:输入=待扩写为事实的大纲;细化后写正文;不超出大纲未涉及处,完整演绎。扮演停笔:用户本轮行为与对话作正文开头并扩写填满;不可写用户角色未给出的其它行为/对话;不可替用户角色做决定;停在其他角色等用户角色回应之刻。用户另定则改写",
|
||||
"用户输入用法": "择一写入完整规格。大纲扩写:节拍上的人事物结果不动,节拍之间补具体动作把处境演出来(相亲坐下前先点一杯又改两杯显局促;只加形容词不算拓写);续写段短。续写:拓写跟已声明行为、不另写新决定;续写多于大纲扩写。自动写入:短 meta/极简扮演→长续写;可按主角设定代演用户角色。用户另定则改写",
|
||||
"叙事速度与详略": "…",
|
||||
"成败与难度": "…",
|
||||
"变量门控钩子": []
|
||||
"变量门控钩子": [],
|
||||
"主角设定偏好": "大纲扩写|续写→客观为主、少性格;自动写入→详性格;无主角卡则空字符串"
|
||||
},
|
||||
"内容与表达": {
|
||||
"视角执行": "…",
|
||||
@@ -180,6 +191,6 @@ summary:`叙事指南与故事推进 · …`
|
||||
## examples
|
||||
|
||||
```examples
|
||||
好:一次钉清遣词 + 笔墨落点 + 禁忌;推进写「等用户决策」或「无专门推进」;输入用法按体验钉「大纲扩写」或「扮演停笔」之一;情境备用是「展示情色」「网文爽文」两档及切换时笔墨。
|
||||
坏:拆成「文风技能」和「推进技能」两步;把两套输入用法混成半套;追问好感公式 / 事件池;或把「当众折辱」「独处一室」当成情境备用。
|
||||
好:一次钉清遣词 + 笔墨落点 + 禁忌;推进写「等用户决策」或「无专门推进」;输入用法按体验钉「大纲扩写」「续写」或「自动写入」之一;大纲扩写写明节拍之间补具体动作、只加形容词不算拓写;情境备用是「展示情色」「网文爽文」两档及切换时笔墨。
|
||||
坏:拆成「文风技能」和「推进技能」两步;把多种输入用法混成半套;续写用法下仍代玩家做决定;自动写入却禁止一切扮演玩家;追问好感公式 / 事件池;或把「当众折辱」「独处一室」当成情境备用。
|
||||
```
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
# 开场白与开场变量
|
||||
|
||||
> 技能文档。程序只切割下方 **fence 块**;`##` 标题仅供人读。
|
||||
> 产物外壳:`docs/context-fragment-design.md`。
|
||||
> **创作终节点**:规格与呈现契约已齐。写出 1~多条开场(每条 = meta 初值/角色 + present.v1 正文)供选定;选定后程序落库、关闭工作流、保存定稿。
|
||||
> 不必再调磁盘 worker `opening-generator`(无本步时它仍可作兜底落库)。
|
||||
|
||||
## meta
|
||||
|
||||
```meta
|
||||
@@ -12,86 +7,68 @@ name: 开场白与开场变量
|
||||
id: opening-setup
|
||||
artifact: 设计.开场白与开场变量
|
||||
declaration: >
|
||||
创作终节点:写出 1~多条开场(每条 = meta 开场变量初值 + 正文 present.v1,壳与正文组成一致;须含字面 @玩家);选定后落库、结束创作并保存定稿。用户代入名片走「主角设定」,勿把名字/简介写进开场正文。
|
||||
普通楼层创作终节点,且可增殖:每条开场是一个节点,落成 `设计.开场白#场景短码`。
|
||||
默认写 1 条;用户要多个入口时再增殖。每条自带 present.v1、同真相初态与可选用户角色。
|
||||
全部保存。游玩时左右切换,选中的一条作为 0 层。
|
||||
when: |
|
||||
收成链已齐(至少体验契约可引用,且「细化终稿」已产出或可立即收成运行规格),需要可开玩的第一段剧情与同真相初值。
|
||||
排在收成之后作最后一步。用户要进游玩但还没有第一屏 → 排。
|
||||
回复呈现、正文组成和其它本局内容能力均已完成,且程序已依次编译信息可见范围、游玩执行方式、上下文排序与运行配置后,需要可开玩的第一屏时使用。
|
||||
when_not: |
|
||||
体验/呈现仍混沌 → 先「美学纲领与交互范式」与「回复呈现」「正文组成」等。
|
||||
只要改文风不要开场 → 「叙事指南」(工序编排:「叙事指南与故事推进」)。
|
||||
只要改真值规则不要开场文 → 「变量设计与更新规则」。
|
||||
运行规格未齐 → 先「细化终稿」。尚未选定开场前若要补前序节点(如 NPC)→ 插在本步之前,勿以「收口已排入」拒绝追加。
|
||||
语义区域或前端模板未钉 → 先回复呈现、正文组成。
|
||||
`设计.worker集` 尚未由程序组装 → 先运行收口工具链。
|
||||
只改文风或真值规则 → 叙事指南 / 变量设计。
|
||||
要做持久 0 层开场 → 0层开场白与初态,不在本步兼顾。
|
||||
boundary: |
|
||||
本技能是创作终节点:1~多条开场。每条 = meta(开场变量初值)+ 正文(present.v1,shell 与「设计.正文组成」一致)。
|
||||
用户代入名片优先「设计.主角设定」;本步可不写用户角色。旧稿若在 meta 带了用户角色仍可读,但不要再往开场正文夹名字/简介。
|
||||
初值与该条叙述同真相。必须遵守已验收的叙事指南、回复呈现、正文组成、变量设计;不重做这些契约。正文不得只交一段散文。
|
||||
具体实例:开场可引用已有实例名片,不在本步新造百科。
|
||||
选定后由程序写入 输出.开场白 / 运行.初始变量 / 运行.开场用户角色 / 变量.当前,关闭 DAG 并保存定稿;不要在本步之后再排其它能力。
|
||||
feeds: narrator
|
||||
本能力只写开场候选与各候选初态。不重开篇幅、文风、区域清单或前端模板。
|
||||
只填正文组成声明为模型生成的 blocks;状态与拓扑由程序按候选初态投影。
|
||||
随机数是变量表隐藏行:本步不写具体数字。0 层玩法不依赖随机;普通楼层在第一次正式回复时由程序补齐随机行。
|
||||
每条开场单独落盘。游玩时左右切换,选中的一条作为 0 层;创作里不把入口收成一道先选定的题。
|
||||
feeds: gm,narrator
|
||||
```
|
||||
|
||||
## opening
|
||||
|
||||
```opening
|
||||
设定已齐时,用几句话定开场第一拍:
|
||||
|
||||
1. 玩家睁眼/进场时在哪、正发生什么?
|
||||
2. 开场结束时,TA 显然可以做什么?
|
||||
3. 此时各真值大概是多少?(能推断的直接说;推不出的标「要问」)
|
||||
设定已齐时,用几句话说清第一拍:玩家进场时在哪、正发生什么、结束后显然能做什么。默认写一条开场。还要别的入口时,每条再增殖一个开场节点;进游玩后左右切换,选中的一条作为 0 层。
|
||||
```
|
||||
|
||||
## task
|
||||
|
||||
```task
|
||||
你正在执行「开场白与开场变量」。这是创作最后一步。产物必须是 **context-fragment.v1** JSON,写入「设计.开场白与开场变量」。
|
||||
读取程序已组装的 `设计.worker集` 和全部最终层产物,写入 `设计.开场白与开场变量`。这是普通楼层创作终节点;不得在本步首次整理执行单元或上下文顺序。
|
||||
|
||||
主产物:1~3 条可开玩开场(选定后落库结束创作)。每条必须是 **meta + 正文** 两截,不是一段散文。
|
||||
默认只生成 1 条开场。用户要多个入口时,每条再增殖一份产物,对话落盘写入 `设计.开场白#场景短码`(如 dorm、gate);改某一条则覆盖同一完整 tag。不要把多条入口收成聊天里的选择题。每条候选各自携带:
|
||||
- 稳定 id 与一句区分说明
|
||||
- 可选用户角色名片(不同候选可不同)
|
||||
- 与该条叙述同真相的变量初值
|
||||
- 若本局有拓扑:当前位置/焦点与已揭示范围
|
||||
- 符合正文组成数据契约的 `present.v1`(只填模型生成区)
|
||||
|
||||
**正文 = 正文组成的输出格式(强制):**
|
||||
- 读「设计.正文组成」的 `呈现壳.shell_id` 与可见区域。
|
||||
- 每条 `正文` 必须是 **present.v1 对象**(与游玩终稿、正文组成「示例.灌数」同形)。`shell` 必须等于该 shell_id。只填该壳已有区域;没有的键省略或空字符串。
|
||||
- 用户主读写在 `blocks.body`;监控栏初值写在 `blocks.monitor`(短键值,勿长文);建议行动写在 `正文.meta.suggested_actions`。
|
||||
- prose 壳也要包成 present.v1(`blocks.body`),不要只交裸 Markdown。
|
||||
- 开场比一章短:愿点进游玩即可,不必凑游玩默认 1000~2000 字。
|
||||
全部候选都保存。每条正文只写在该候选的 `正文` 里,不要再抄一份全文或初值。`默认候选id` 指向第一次进入 0 层的那条;程序按这个 id 读取它的正文和初值。游玩时左右切换,选中的一条作为 0 层,并整套换该条正文、初态与角色。
|
||||
|
||||
**meta = 这条开场的初始设定:**
|
||||
- `开场变量`:与该条叙述同真相的初值(名与变量设计一致)。顶层再抄一份推荐条的初值,供程序落库。
|
||||
- `用户角色`:可选;优先已由「主角设定」钉好。若本条必须自带名片且尚无主角设定,可填名字+简介(程序会抬到主角设定节点)。禁止把名字/简介写进 `开场白全文` / 候选正文。
|
||||
硬规格:
|
||||
1. 每条可见正文至少出现一次字面 `@玩家`;不写死具体姓名顶名字位。
|
||||
2. `present.v1` 的区域只覆盖正文组成声明的模型生成 blocks;程序投影区留空给程序灌。
|
||||
3. 不输出 `名称.随机数` 的具体数组;随机行由程序在第一次正式回复前或回复时补齐。
|
||||
4. 开场宜短,愿点进游玩即可,不抄故事推进的单轮篇幅预算当第一章。
|
||||
|
||||
**用户角色占位(产物规格,不是可选项):**
|
||||
- `开场白全文`、每条候选 `正文`(含 `blocks.body` 及其它有字区域)里,凡指称用户扮演角色,第三人称点名处必须写字面量 `@玩家`(六个字符:`@` + `玩家`)。
|
||||
- 每条候选正文至少出现一次 `@玩家`。禁止写死具体姓名(如林晚、阿泽);禁止用「用户」「主角」顶名字位。
|
||||
- 「你」可与 `@玩家` 并用;若全文只有「你」、零个 `@玩家`,视为不合格,改写后再交。
|
||||
- 进游玩时程序把 `@玩家` 换成当前用户角色名;创作稿必须保留字面 `@玩家`。
|
||||
|
||||
执行顺序:
|
||||
1. 读依赖:worker 集/美学/叙事/**正文组成(先抄 shell_id)**/监控栏/变量设计/舞台与实例/主角设定。
|
||||
2. 写 1~3 条候选:每条 `meta`(开场变量;用户角色仅在缺主角设定时补)+ `正文`(present.v1)。同真相、第一拍可不同。
|
||||
3. 推荐条的 present.v1 同时写入 `开场白全文`(对象,不是字符串)。
|
||||
4. 顶层 `开场变量` = 推荐条 meta 里那份初值。冲突以叙述为准。
|
||||
5. 交稿前自检:每条 `正文.shell` 与正文组成一致;JSON 搜索 `@玩家`;正文无「用户角色.名字」行;再自评 + 追问;输出 JSON。
|
||||
|
||||
缺上游契约时:能写则按已有材料写开场并标开放问题;不能成立第一拍则追问 1~2 点,勿交空壳。
|
||||
|
||||
summary:`开场白与开场变量 · …`(点题场景)。
|
||||
材料足够便直接成稿;会改变第一拍成立的缺口交给 `self_score.gap`。产物正文不放评分、诊断、追问或开放问题。
|
||||
```
|
||||
|
||||
## principles
|
||||
|
||||
```principles
|
||||
1. 开场为主,表为配套。
|
||||
2. 正文 = present.v1,壳与「设计.正文组成」一致;文风跟叙事指南。尚未钉壳时用 prose 包一层 present.v1,并标开放问题。
|
||||
3. 同真相:该条 meta.开场变量 与该条叙述一致。
|
||||
4. 可行动:第一拍结束有下一步空间;写入 suggested_actions(该壳开行动时)。
|
||||
5. 短:愿点进游玩,非第一章。
|
||||
6. 不剧透隐藏档。
|
||||
7. 用户角色名位 = 字面 `@玩家`(见 task 规格);不合格则改写。
|
||||
1. 开场为主,初态配套。叙述与该条变量、拓扑初态必须同真相。
|
||||
2. 默认一条。多条是不同入口,每条一个可增殖节点,不是同文换皮,也不是先让用户选唯一开场。
|
||||
3. 用户角色可按候选不同;没有名片时仍用 `@玩家` 占位,进游玩再替换。
|
||||
4. 只写模型生成区。状态芯片、地图等权威显示由程序投影,不在开场正文里重编一份。
|
||||
5. 不剧透隐藏档,不写成第一章,不在本步改前端模板。
|
||||
6. 本步结束后创作收口;不要再排后续内容能力。
|
||||
```
|
||||
|
||||
## probe
|
||||
|
||||
```probe
|
||||
开场地点/冲突不清;某真值开场值推不出;壳区域是否都要填;候选是否漏写 `@玩家` 或写成了裸散文。一次 1~2 点。
|
||||
每轮 1~2 点。优先确认地点与冲突、某真值开场值是否推得出、多条时入口差异是否成立、每条是否含 `@玩家`。不追问前端重设计或随机骰面。
|
||||
```
|
||||
|
||||
## output
|
||||
@@ -100,131 +77,103 @@ summary:`开场白与开场变量 · …`(点题场景)。
|
||||
{
|
||||
"schema": "context-fragment.v1",
|
||||
"技能": "开场白与开场变量",
|
||||
"brief": "一句话:开场场景 + 关键初值",
|
||||
"mount": ["narrator"],
|
||||
"brief": "一句话:默认开场场景 + 有几条候选",
|
||||
"mount": ["world-simulator", "narrator"],
|
||||
"稳变": "stable",
|
||||
"正文": {
|
||||
"依据契约": [
|
||||
"正文组成 / 叙事指南 / 监控栏 / 变量设计 等已遵循要点;写出所用 shell_id"
|
||||
],
|
||||
"用户角色": {
|
||||
"名字": "沈聿",
|
||||
"简介": "本地放贷人;与这对母女相识但不熟。"
|
||||
},
|
||||
"开场白全文": {
|
||||
"schema": "present.v1",
|
||||
"shell": "与设计.正文组成.呈现壳.shell_id 一致",
|
||||
"blocks": {
|
||||
"monitor": { "地点": "堂屋", "债额": "三两" },
|
||||
"body": "清晨,@玩家推开堂屋门,债契还摊在桌上。……"
|
||||
},
|
||||
"meta": { "suggested_actions": ["问债", "先坐下"] }
|
||||
},
|
||||
"默认候选id": "opening-1",
|
||||
"开场白候选": [
|
||||
{
|
||||
"id": "opening-1",
|
||||
"区分": "这条入口与其它条差在哪;仅一条时可写「默认」",
|
||||
"meta": {
|
||||
"用户角色": { "名字": "沈聿", "简介": "本地放贷人;与这对母女相识但不熟。" },
|
||||
"用户角色": {
|
||||
"名字": "可选;无则省略整段",
|
||||
"简介": "可选"
|
||||
},
|
||||
"开场变量": [
|
||||
{ "名": "债额", "值": 3, "依据": "桌上债契" }
|
||||
{
|
||||
"名": "与变量设计真值名一致",
|
||||
"值": "初值",
|
||||
"依据": "开场事实"
|
||||
}
|
||||
],
|
||||
"拓扑初态": [
|
||||
{
|
||||
"topology_id": "有拓扑时填写",
|
||||
"当前位置": "节点 id",
|
||||
"已揭示": ["节点或连接 id"]
|
||||
}
|
||||
]
|
||||
},
|
||||
"正文": {
|
||||
"schema": "present.v1",
|
||||
"shell": "与开场白全文.shell 相同",
|
||||
"blocks": {
|
||||
"monitor": { "地点": "堂屋", "债额": "三两" },
|
||||
"body": "清晨,@玩家推开堂屋门,债契还摊在桌上。……"
|
||||
"body": "清晨,@玩家推开门。……"
|
||||
},
|
||||
"meta": { "suggested_actions": ["问债", "先坐下"] }
|
||||
"meta": {
|
||||
"suggested_actions": ["若正文组成启用文字选项则给短建议;否则 []"]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"开场变量": [
|
||||
{
|
||||
"名": "与变量设计真值名一致",
|
||||
"值": "初值",
|
||||
"依据": "开场事实/设定推断"
|
||||
}
|
||||
],
|
||||
"隐藏段开场": "通常空;若正文组成钉了隐藏段且开场必须写出,写入 present.v1 blocks.hidden",
|
||||
"落库提示": {
|
||||
"输出.开场白": "推荐条 正文(present.v1 JSON)",
|
||||
"运行.初始变量": "该条 meta.开场变量 → 表格式",
|
||||
"运行.开场用户角色": "该条 meta.用户角色 → 名字+简介",
|
||||
"变量.当前": "与初始变量对齐"
|
||||
},
|
||||
"用户可行动空间": "开场结束后玩家显然能尝试什么"
|
||||
},
|
||||
"自评": {
|
||||
"维度": [
|
||||
{
|
||||
"名": "契约符合",
|
||||
"分数": 8,
|
||||
"说明": "是否 present.v1、shell 与正文组成一致、叙事/变量同真相"
|
||||
},
|
||||
{
|
||||
"名": "同真相",
|
||||
"分数": 8,
|
||||
"说明": "开场叙述与开场变量是否一致"
|
||||
},
|
||||
{
|
||||
"名": "可开玩",
|
||||
"分数": 8,
|
||||
"说明": "是否短、可感、有下一步行动空间"
|
||||
},
|
||||
{
|
||||
"名": "玩家占位",
|
||||
"分数": 8,
|
||||
"说明": "每条候选是否含字面 @玩家、无写死姓名"
|
||||
},
|
||||
{
|
||||
"名": "版式符合",
|
||||
"分数": 8,
|
||||
"说明": "是否 present.v1,shell 与正文组成一致,区域未超出该壳"
|
||||
}
|
||||
],
|
||||
"薄弱点": "一句话"
|
||||
},
|
||||
"追问": {
|
||||
"导语": "为钉准开场,还需确认:",
|
||||
"题目": [
|
||||
{
|
||||
"问": "…",
|
||||
"建议选项": ["选项A", "选项B", "其它(请写明)"],
|
||||
"示例": "可选"
|
||||
}
|
||||
]
|
||||
},
|
||||
"开放问题": []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
硬规则:
|
||||
1. context-fragment.v1;自评含「玩家占位」与「版式符合」。
|
||||
2. `开场白全文` 与每条候选 `正文` 必须是 present.v1 对象(含 `schema`/`shell`/`blocks.body`);`shell` 与「设计.正文组成」一致。禁止只交散文字符串。
|
||||
3. `开场白候选` 1~3 条,每条含 `meta`(开场变量与该条同真相;用户角色可选,优先主角设定)+ `正文`。顶层 `开场变量` 必填(推荐条初值)。
|
||||
4. 每条正文(含 body)必须含字面 `@玩家`;不发明新真值名。
|
||||
5. summary:`开场白与开场变量 · {brief 缩略}`。
|
||||
1. 输出合法 JSON;`开场白候选` 默认 1 条,仅用户明确要求时 2~3 条。
|
||||
2. `默认候选id` 必须指向候选之一。正文和初值只写在该候选上,不另抄全文或开场变量。
|
||||
3. 每条 `正文` 必须是 `present.v1`,只含正文组成允许模型生成的 blocks;每条可见正文须含字面 `@玩家`。
|
||||
4. 不同候选可有不同用户角色与初态;切换时整套替换,不混用。
|
||||
5. 不输出随机数具体值,不重写前端,不把程序投影区填成模型编造状态。
|
||||
6. 产物正文不含自评、追问或开放问题。
|
||||
7. `mount` 固定为 `["world-simulator", "narrator"]`。
|
||||
|
||||
## score
|
||||
|
||||
```score
|
||||
开场必要:
|
||||
- 5:第一拍清楚、有行动空间,短到愿意开玩。
|
||||
- 3:能开玩,但入口或下一步略含糊。
|
||||
- 1:像第一章摘要,或缺冲突与下一步。
|
||||
|
||||
前端符合:
|
||||
- 5:present.v1 只填模型生成区,且与正文组成契约一致。
|
||||
- 3:主体符合,有一个次要区域或空态处理含糊。
|
||||
- 1:裸散文,或把程序投影区填成模型重编数据。
|
||||
|
||||
状态自洽:
|
||||
- 5:每条候选的变量与拓扑初态都与该条叙述同真相,互不串味。
|
||||
- 3:默认条自洽,备选条有一处初态含糊。
|
||||
- 1:叙述与初值冲突,或多条共用一套却声称不同入口。
|
||||
|
||||
角色占位:
|
||||
- 5:每条含字面 `@玩家`;有名片时不把姓名写进正文名字位。
|
||||
- 3:占位大体正确,有一条名片与正文关系略乱。
|
||||
- 1:写死姓名,或全文没有 `@玩家`。
|
||||
|
||||
收口可切换:
|
||||
- 5:默认条明确,全部候选可保存;多条时入口差异成立。
|
||||
- 3:能收口,但多条差异偏弱。
|
||||
- 1:默认不清,或未要求多条却堆出重复候选。
|
||||
```
|
||||
|
||||
## checklist
|
||||
|
||||
```checklist
|
||||
- [ ] 1~3 条候选,推荐条 正文 = `开场白全文`(均为 present.v1)?
|
||||
- [ ] `shell` 与「设计.正文组成」一致?只填该壳已有区域?不是裸散文?
|
||||
- [ ] 每条候选可搜到 `@玩家`?无写死姓名?
|
||||
- [ ] 该条 meta 初值与叙述同真相?有行动空间?
|
||||
- [ ] 未剧透隐藏档、未写成第一章?
|
||||
- [ ] 默认 1 条;仅用户要求时才有 2~3 条?
|
||||
- [ ] `默认候选id` 指向一条候选,且该条正文、初值没有在外面再抄一份?
|
||||
- [ ] 每条含 `@玩家`,present.v1 只含模型生成区?
|
||||
- [ ] 各候选初态自洽,未填写随机数具体值?
|
||||
- [ ] 未重开前端、篇幅或区域清单?
|
||||
```
|
||||
|
||||
## examples
|
||||
|
||||
```examples
|
||||
好:正文为 present.v1,shell 与正文组成相同;body 里有字面 @玩家;meta 带开场变量;名片在主角设定。
|
||||
好:prose 壳也包成 {"schema":"present.v1","shell":"prose","blocks":{"body":"冷风灌进来时,@玩家还握着空水瓶。"}}。
|
||||
好:「你(@玩家)站在闸机前,票夹是空的。」写在 blocks.body。
|
||||
坏:`开场白全文` 只是一段字符串,没有 present.v1。
|
||||
坏:shell 与正文组成不一致,或灌了该壳没有的区域。
|
||||
坏:全文只有「你推开门」——零个 `@玩家`。
|
||||
坏:「林晚推开门」——写死姓名。
|
||||
坏:在正文或开场白全文里写「用户角色.名字: …」——名片应落「主角设定」。
|
||||
- 单条:堂屋债契,body 含 `@玩家`,开场变量写债额初值;状态栏由程序投影。
|
||||
- 用户要两条入口:一条放贷人名片、一条社畜穿越名片;各带自己的初态,默认指定其中一条开玩。
|
||||
- 不合适:开场里填写 `敏捷.随机数=[…]`;或把地图权威数据写成模型散文却不给拓扑初态。
|
||||
```
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
# 主角设定
|
||||
|
||||
> 技能文档。程序只切割下方 **fence 块**。
|
||||
> 产物外壳:`docs/context-fragment-design.md`。
|
||||
> 落盘后挂在当前选用的用户角色卡下(横线下方),不进全局列表。关掉这局就不加载。不改其它技能正文。
|
||||
|
||||
## meta
|
||||
|
||||
```meta
|
||||
@@ -11,58 +7,41 @@ name: 主角设定
|
||||
id: protagonist
|
||||
artifact: 设计.主角设定
|
||||
declaration: >
|
||||
钉清 @玩家 扮演的那个人:名字、背景、特殊设定(身份/金手指等)。落盘后挂在当前选用的用户角色卡下。
|
||||
保存本局用户所扮演角色的身份名片,以及核心体验和已授权代演真正需要的最少信息。
|
||||
when: |
|
||||
用户代入,且这局角色有名字、身份或金手指(快穿短局几乎总有一张脸),尚无「设计.主角设定」定稿。
|
||||
用户代入一个有独立身份、背景、关系或特殊能力的角色,且这些信息会影响核心体验或世界反应。
|
||||
LLM 获得较多推进权,而现有材料不足以稳定代演该角色时也使用。
|
||||
when_not: |
|
||||
旁观或把角色当棋子、不要代入身份 → 不排。
|
||||
只要体验感觉、不要名片 → 「美学纲领与交互范式」。
|
||||
NPC 名片 → 「具体实例」。开场第一拍(叙事)→ 「开场白与开场变量」;名片本身仍走本步,勿写进开场白。
|
||||
旁观、无固定主角或纯作者视角时不使用。刻意无名的自我投射不因缺名字而强制使用。
|
||||
NPC 有多份或需要统一字段时归生成规则,要留下的单条再写具体实例;世界硬法则归实现机制;第一幕归开场白。
|
||||
boundary: |
|
||||
本技能:@玩家 扮演的那一张名片(名字、背景、特殊设定)。
|
||||
美学纲领与交互范式:体验与轮转——本步不重定。
|
||||
实现机制:世界法则——金手指写在本步「它能做什么」,法则来源不写。
|
||||
开场白:第一屏怎么开始——本步不定时间地点冲突;也不把名字/简介塞进开场正文。
|
||||
本能力只交用户角色名片和必要行为依据。体验与权限来自美学纲领,输入及推进方式来自故事推进,本步据此控制详略。
|
||||
feeds: gm
|
||||
```
|
||||
|
||||
## opening
|
||||
|
||||
```opening
|
||||
这局你代入谁?(想到什么写什么)
|
||||
|
||||
1. 名字:别人怎么叫你?不要写 @玩家。
|
||||
2. 背景:一两句来历或处境即可,不写世界百科。
|
||||
3. 特殊设定:身份、金手指、随身之物;没有就说没有。
|
||||
说说这局里的“你”是谁。名字可以留作 `@玩家`;重点写会影响核心体验或世界反应的身份、背景、关系和特殊能力。只有需要 LLM 多代演时,才补性格与默认反应。
|
||||
```
|
||||
|
||||
## task
|
||||
|
||||
```task
|
||||
把用户原话收成 @玩家 扮演的那一张名片,写入 tag「设计.主角设定」。作用对象是本局黑板;程序读取后挂在当前选用的用户角色卡下,不写进全局列表。
|
||||
把用户材料整理为本局用户角色名片,写入 `设计.主角设定`。先保留承载核心体验的身份、能力、关系与限制,再按 LLM 获得的推进权决定是否需要行为依据。
|
||||
|
||||
产物必须是 **context-fragment.v1** JSON。正文只含:名字、背景、特殊设定(身份 / 金手指 / 其它)。须含 schema / 技能 / brief;mount 主世界层;稳变多为 stable。
|
||||
|
||||
名字:本局称呼,进游玩替换 @玩家。禁止写成「@玩家」「用户」「主角」。缺名字 → 追问 1 题,不交半张卡。
|
||||
背景:一两句。没有就写「未定」,不编百科。
|
||||
特殊设定:有则写身份、金手指(能做什么、限制一句)、其它;没有就各写「无」。
|
||||
|
||||
一次对话、一次验收。不写世界、不写文风、不写开场第一拍。
|
||||
用户自己扮演或提供详细大纲时,身份背景够用即可,不替用户设计人格与选择。LLM 代演越多,性格、处事倾向和默认反应越需要具体可执行。产物正文不放评分、追问或诊断。
|
||||
```
|
||||
|
||||
## principles
|
||||
|
||||
```principles
|
||||
1. 一张脸:删掉仍知道「我是谁」的句子才落盘。
|
||||
2. 金手指写能力与限制,不写它为何存在。
|
||||
3. 材料不够:名字必须问出;背景和特殊设定可「未定」或「无」。
|
||||
4. 规格走 JSON;追问一次 1 题。
|
||||
```
|
||||
|
||||
## probe
|
||||
|
||||
```probe
|
||||
只追问挡住成卡的缺口。缺名字:给 2 个称呼选项 + 其它。背景/金手指用户说没有 → 写「无」,不问第三轮。
|
||||
1. 只设计用户已明确给出、核心体验依赖或运行确实需要的内容。
|
||||
2. “首富之子”“唯我独法”等若是核心体验成立条件,属于必要设定,不算过度设计。
|
||||
3. 特殊能力写“能做什么 + 限制”;世界为何如此由实现机制处理。
|
||||
4. `称呼` 可写固定名字,也可写 `@玩家` 表示刻意留白;不要因无名阻塞成卡。
|
||||
5. 行为依据随推进权增减:用户控制越多越简;LLM 代演越多越具体。
|
||||
6. 不写 NPC 名片、世界百科、文风规则或开场事件。
|
||||
```
|
||||
|
||||
## output
|
||||
@@ -71,52 +50,54 @@ feeds: gm
|
||||
{
|
||||
"schema": "context-fragment.v1",
|
||||
"技能": "主角设定",
|
||||
"brief": "一句话:谁、带着什么特殊",
|
||||
"brief": "一句话说明此人是谁,以及什么设定最承载核心体验",
|
||||
"mount": ["world-simulator"],
|
||||
"稳变": "stable",
|
||||
"正文": {
|
||||
"名字": "林晚",
|
||||
"背景": "原剧情里被退婚的庶女,刚穿过来那天正被当众羞辱。",
|
||||
"特殊设定": {
|
||||
"身份": "侯府庶女 / 原女主的垫脚石",
|
||||
"金手指": "能看见别人对她的好感数字;别人看不见。",
|
||||
"其它": "无"
|
||||
}
|
||||
},
|
||||
"自评": {
|
||||
"维度": [
|
||||
{ "名": "可代入", "分数": 8, "说明": "名字能替换 @玩家,身份清楚" },
|
||||
{ "名": "够用", "分数": 8, "说明": "没有世界百科,特殊设定能约束正文" }
|
||||
"称呼": "@玩家 或用户明确给出的固定名字",
|
||||
"身份与背景": "只写影响体验或世界反应的来历、处境与关系",
|
||||
"特殊设定": [
|
||||
{
|
||||
"设定": "身份、能力、关系或随身之物",
|
||||
"作用": "它怎样承载核心体验或影响世界反应",
|
||||
"限制": "无则省略"
|
||||
}
|
||||
],
|
||||
"薄弱点": "一句话"
|
||||
},
|
||||
"追问": {
|
||||
"导语": "名片还缺:",
|
||||
"题目": []
|
||||
},
|
||||
"开放问题": []
|
||||
"行为依据": {
|
||||
"适用条件": "仅在 LLM 获得相应代演权限时存在",
|
||||
"处事倾向": "可执行的一两句",
|
||||
"默认反应": "不替用户决定关键选择"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
硬规则:
|
||||
1. 合法 JSON;必含 schema / 技能 / brief / 正文 / 自评 / 追问。
|
||||
2. 正文是对象,键为 名字 / 背景 / 特殊设定;特殊设定为对象(身份、金手指、其它),不是一段散文。
|
||||
3. 名字不是 @玩家 / 用户 / 主角。
|
||||
4. 禁止写数字 order、禁止输出执行单元列表。
|
||||
5. summary:`主角设定 · {名字}`。
|
||||
1. 输出合法 JSON;`行为依据` 不需要时整项省略。
|
||||
2. 不写产物内自评、追问、开放问题或完备度。
|
||||
3. 不把核心体验所依赖的身份、能力或关系误删为“过度设计”。
|
||||
4. 不替用户补未授权的人格、目标、情绪和关键决定。
|
||||
|
||||
## score
|
||||
|
||||
```score
|
||||
宽松度:
|
||||
- 5:只设计本局运行确实需要、核心体验依赖或用户已明确给出的部分;用户控制越多,留白越充分。
|
||||
- 3:有少量未经确认、也非体验承重的性格或行为倾向,可能影响用户扮演但容易改写。
|
||||
- 1:大量替用户设计人格、目标、情绪或行动逻辑,明显挤占用户控制权。
|
||||
|
||||
必要度:
|
||||
- 5:核心体验所需的身份、能力、关系完整;代演信息量与 LLM 获得的推进权匹配。
|
||||
- 3:缺一项会削弱核心体验或导致代演漂移的信息。
|
||||
- 1:漏掉“我是谁、我凭什么获得该体验”的承重设定,或 LLM 需要代演却没有行为依据。
|
||||
```
|
||||
|
||||
## checklist
|
||||
|
||||
```checklist
|
||||
- [ ] 含 schema / brief / 正文 / 自评 / 追问?
|
||||
- [ ] 名字可替换 @玩家?不是占位词?
|
||||
- [ ] 背景一两句或「未定」?特殊设定有则写能力与限制,无则「无」?
|
||||
- [ ] 没有世界百科、没有开场第一拍?
|
||||
```
|
||||
|
||||
## examples
|
||||
|
||||
```examples
|
||||
好:名字「林晚」;背景一句穿来当日处境;金手指「看见好感数字」+限制「别人看不见」。
|
||||
坏:名字写成 @玩家;把侯府地理、朝堂势力写进背景;把开场羞辱戏全文写进名片。
|
||||
- [ ] 核心体验依赖的身份、能力和关系都已保留?
|
||||
- [ ] 非必要、未经确认的性格与行为设计已删除?
|
||||
- [ ] 行为依据的详细度与 LLM 推进权匹配?
|
||||
- [ ] 称呼允许固定名字或 `@玩家` 留白?
|
||||
- [ ] 未写 NPC、世界百科、文风或开场事件?
|
||||
```
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
# 随机范围整理
|
||||
|
||||
> 技能文档。程序只切割下方 **fence 块**;`##` 标题仅供人读。
|
||||
> 产物外壳:`docs/context-fragment-design.md`。
|
||||
> 本步把上游已定的检定/对抗/掷骰需求,收成一份**程序可插入的随机值范围表**;不发明新裁定法则。
|
||||
> 游玩期:程序按本表每轮往消息里塞备用随机数(前端折叠隐藏、用户看不见,发给 AI 时仍在上下文);主世界层直接用表中本次数,不再单独「机遇裁定」槽。
|
||||
|
||||
## meta
|
||||
|
||||
```meta
|
||||
@@ -12,67 +7,49 @@ name: 随机范围整理
|
||||
id: random-range
|
||||
artifact: 设计.随机范围整理
|
||||
declaration: >
|
||||
上游已有检定/对抗/掷骰需求时:汇总全部要用的随机项与取值范围(含表达式),供程序每轮插入备用随机数;排在正文组成之前
|
||||
把已定的检定和抽签收成「变量.当前」里的隐藏行,行名用「名称.随机数」;
|
||||
值是剩余数字,用掉一个就改这一行,只剩 1 个时补上。
|
||||
when: |
|
||||
机制/生成规则/变量/叙事等上游已出现需要随机数的内容(对抗、检定、抽签、伤害区间等),且尚未收成一份范围表。
|
||||
排在「正文组成」之前(通常紧挨其前;回复呈现可并行或略前)。
|
||||
机制、变量或生成规则已出现必须用随机数才能裁定的点时使用。
|
||||
when_not: |
|
||||
全程不靠随机数推进 → 不排。
|
||||
还在定「怎么裁定 / 公式本身」→ 先「实现机制」「生成规则」或「变量设计与更新规则」;本步只汇总已定需求。
|
||||
只谈前端怎么藏、选壳美化 → 「正文组成」。
|
||||
只谈每轮可见区域与字数 → 「回复呈现」。
|
||||
全程不靠随机推进 → 不使用。
|
||||
公式本身还没钉死 → 先实现机制、生成规则或变量设计。
|
||||
按合理度从池里选,而不是抽签 → 不列入。
|
||||
boundary: |
|
||||
本技能:一份清单——每项随机用途、范围或表达式(如力量*5~*10、敏捷检定 1~100)、本轮建议备用条数。
|
||||
不写新裁定法则、不写骰子剧情;法则来自上游,此处只整理成程序可读范围。
|
||||
游玩拓扑:不再开「机遇裁定」槽;真随机靠本表 + 程序插入上下文。
|
||||
正文组成:负责把隐藏备用区接到壳/消息里——本步只交「要插什么数」。
|
||||
feeds: gm,narrator
|
||||
不新建表,不发骰面。数字是普通隐藏变量,默认玩家看不见。
|
||||
0 层玩法不依赖随机。普通楼层在第一次正式回复时由程序补齐随机行,开场不必写出具体数字。
|
||||
feeds: gm
|
||||
```
|
||||
|
||||
## opening
|
||||
|
||||
```opening
|
||||
把本局所有要用的随机数收成一张表(想到什么写什么):
|
||||
|
||||
1. 上游里哪些地方要掷 / 检定 / 抽区间?(对抗、伤害、事件抽签……)
|
||||
2. 每一项的取值范围或表达式?(例:敏捷检定 1~100;攻击伤害=力量×5~×10)
|
||||
3. 每轮消息大概要预先塞几条备用?(够用即可,不要灌爆)
|
||||
本局哪些地方必须抽个数才能定?每项用哪一行保存剩余数字,以及怎么掷。
|
||||
```
|
||||
|
||||
## task
|
||||
|
||||
```task
|
||||
你正在执行「随机范围整理」。产物必须是 **context-fragment.v1** JSON,写入「设计.随机范围整理」。
|
||||
写入 `设计.随机范围整理`。每项是 `变量.当前` 的一行,行名写成「名称.随机数」,玩家默认不可见。值是剩余数字组成的数组,下一个可用的是第一个。
|
||||
|
||||
本步只做一件事:把上游已定、游玩时会用到的随机需求,整理成程序可插入的范围表。
|
||||
禁止重议「要不要检定」「公式对不对」——缺则追问上游或写进开放问题;本步不发明新法则。
|
||||
用掉第一个后,程序改写这一行,去掉那个数。改完后如果只剩 1 个,就按范围补到预备个数,不要等这一行空了。模型不自己编数字。
|
||||
|
||||
执行顺序:
|
||||
1. 扫机制 / 生成规则 / 变量 / 叙事 / 美学:列出所有「需要随机数才能裁定」的点。
|
||||
2. 每项写清:用途短名、范围或表达式(相对变量/表字段时写清引用)、单位(整数 d100 / 闭区间等)。
|
||||
3. 钉本轮建议备用条数(按项或全局);够本轮最坏情况即可。
|
||||
4. 无随机需求则本步不应被排;若误进:正文写空清单并说明「无需随机」。
|
||||
5. 自评 + 追问;输出 JSON。
|
||||
|
||||
summary:`随机范围整理 · {项数或主要用途}`。
|
||||
无随机需求则本步不排。公式还没在上游钉死时,把缺口交给 `self_score.gap`。产物正文不放评分和追问。
|
||||
```
|
||||
|
||||
## principles
|
||||
|
||||
```principles
|
||||
1. 只汇总,不定新法。范围与表达式必须能追溯到上游已验收产物。
|
||||
2. 表达式写给人与程序都能读:`力量*5~*10`、`1~100`、`池权重抽 1`——忌散文。
|
||||
3. 一项一类用途;不要把「战斗」糊成一条却含三种掷法。
|
||||
4. 备用条数服务「本轮上下文预插入」;宁少可补,勿一次塞上百。
|
||||
5. 不输出机遇裁定槽、不写 chance tool 调用说明。
|
||||
1. 只登记上游已经成立的掷法,不新写法则。
|
||||
2. 一行一项,行名是「名称.随机数」,例如 `敏捷.随机数`。
|
||||
3. 和别的隐藏变量一样存在现有表里:能改、有版本;不对玩家默认展示。
|
||||
4. 预备个数够连用即可,少了靠「只剩 1 个就补」,不要一次放很多。
|
||||
```
|
||||
|
||||
## probe
|
||||
|
||||
```probe
|
||||
每轮 1~2 点:某项范围是否仍是散文;表达式引用的变量/表是否未在上游出现;有无漏掉已声明的检定。
|
||||
不追问:壳怎么藏、CSS、文风、要不要主世界层。
|
||||
全程明显无随机:确认不排即可,不必硬凑表。
|
||||
每轮 1~2 点。范围是否还能算、行名是否会撞、有没有漏掉已经声明的检定。
|
||||
```
|
||||
|
||||
## output
|
||||
@@ -81,58 +58,63 @@ summary:`随机范围整理 · {项数或主要用途}`。
|
||||
{
|
||||
"schema": "context-fragment.v1",
|
||||
"技能": "随机范围整理",
|
||||
"brief": "一句话:收了哪些随机项",
|
||||
"mount": ["world-simulator", "narrator"],
|
||||
"brief": "登记了哪些隐藏随机行",
|
||||
"mount": ["world-simulator"],
|
||||
"稳变": "stable",
|
||||
"正文": {
|
||||
"依据": ["上游机制/生成规则/变量/叙事中与随机相关的要点"],
|
||||
"插入意图": "程序按本表每轮往消息隐藏区插入备用随机数;裁决时只用上下文已有数,不另开机遇槽",
|
||||
"项": [
|
||||
{
|
||||
"id": "agility-check",
|
||||
"行名": "敏捷.随机数",
|
||||
"用途": "敏捷检定",
|
||||
"范围": "1~100",
|
||||
"表达式": null,
|
||||
"来源": "实现机制·… / 变量·…",
|
||||
"每轮备用条数": 3
|
||||
},
|
||||
{
|
||||
"id": "attack-damage",
|
||||
"用途": "攻击伤害",
|
||||
"范围": null,
|
||||
"表达式": "力量*5~*10",
|
||||
"来源": "变量设计·力量 / 机制·对抗",
|
||||
"每轮备用条数": 2
|
||||
"表达式": "无表达式时省略",
|
||||
"依赖真值": [],
|
||||
"预备个数": 3,
|
||||
"来源": "上游短引用"
|
||||
}
|
||||
],
|
||||
"全局备用上限": "可选;防止灌爆上下文的软上限"
|
||||
},
|
||||
"自评": {
|
||||
"维度": [
|
||||
{ "名": "覆盖", "分数": 8, "说明": "上游随机点是否收齐" },
|
||||
{ "名": "可执行", "分数": 8, "说明": "范围/表达式是否可直接插数" }
|
||||
],
|
||||
"薄弱点": ""
|
||||
},
|
||||
"追问": { "导语": "", "题目": [] },
|
||||
"开放问题": []
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
硬规则:合法 JSON;`项` 可 `[]`(仅当误排且确认无随机);有项则每项须有用途 +(范围或表达式至少一者);禁止 order;summary 见 task。
|
||||
硬规则:
|
||||
1. 合法 JSON;有项则每项有行名、用途、预备个数,以及范围或表达式。
|
||||
2. 行名以 `.随机数` 结尾,彼此不重复。
|
||||
3. 不输出具体骰面,不另建表,不写票 id。
|
||||
4. 产物正文不含自评和追问。
|
||||
5. `mount` 为 `["world-simulator"]`。
|
||||
|
||||
## score
|
||||
|
||||
```score
|
||||
覆盖度:
|
||||
- 5:该随机的点都有独立行。
|
||||
- 3:漏了一项次要抽签。
|
||||
- 1:明显漏项,或没有随机却硬凑。
|
||||
|
||||
可执行性:
|
||||
- 5:行名、范围或表达式、预备个数都能直接写成隐藏变量。
|
||||
- 3:大体能写,有一处范围含糊。
|
||||
- 1:只有用途,程序无法出数。
|
||||
|
||||
克制:
|
||||
- 5:就是现有表上的隐藏行,用一个改一行,只剩 1 个再补。
|
||||
- 3:行名或隐藏方式略绕。
|
||||
- 1:另建存储、票证或让模型自己编数。
|
||||
```
|
||||
|
||||
## checklist
|
||||
|
||||
```checklist
|
||||
- [ ] 每项都能对上上游,没有本步新发明的裁定?
|
||||
- [ ] 范围/表达式短且可插数?
|
||||
- [ ] 未要求机遇裁定槽或 chance tool?
|
||||
- [ ] 排在正文组成之前的意图是否写进 brief/插入意图?
|
||||
- [ ] 每项是「名称.随机数」,玩家默认不可见?
|
||||
- [ ] 范围或表达式能算出数?
|
||||
- [ ] 用掉一个就改这一行,只剩 1 个时补?
|
||||
- [ ] 没有新表、票 id 或具体骰面?
|
||||
```
|
||||
|
||||
## examples
|
||||
|
||||
```examples
|
||||
好:两项——敏捷 1~100(备用 3);攻击伤害 力量*5~*10(备用 2);插入意图写清隐藏备用区。
|
||||
坏:在本步新写一套战斗规则;或只写「战斗要掷骰」无范围;或指引去开 play_slots.chance。
|
||||
- `敏捷.随机数` 的值是 [47, 12, 83]。用了 47,这一行改成 [12, 83]。再使用一次后只剩 [83],随即补到 3 个。
|
||||
- `攻击.随机数` 按力量算出区间,仍是同一张表里的另一行。
|
||||
```
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# 细化终稿
|
||||
# 运行配置组装
|
||||
|
||||
> 能力文档。程序只切割下方 **fence 块**;`##` 标题仅供人读。
|
||||
> 产物 tag:**`设计.worker集`**(JSON)。
|
||||
@@ -7,25 +7,18 @@
|
||||
## meta
|
||||
|
||||
```meta
|
||||
name: 细化终稿
|
||||
name: 运行配置组装
|
||||
id: refine
|
||||
artifact: 设计.worker集
|
||||
declaration: >
|
||||
按已勾选槽(或默认槽)与投影排序表收成运行规格;不发明新执行单元
|
||||
程序把执行方式、上下文排序、变量表和输出合同组装成可直接开玩的运行配置
|
||||
when: |
|
||||
至少体验契约可引用;槽已勾选或可按默认;若上下文块已多则宜先有投影排序表。
|
||||
需要输出「设计.worker集」才能进游玩时排;编排应将流程导向 closed。
|
||||
落档或开玩时自动执行;运行期需要统一的「设计.worker集」快照。
|
||||
when_not: |
|
||||
体验站位未定 → 先「美学纲领与交互范式」。
|
||||
不能吃默认槽且尚未勾选 → 先「游玩拓扑」。
|
||||
上下文块已多且稳/变未排序 → 先「上下文投影排序」。
|
||||
只要改某一块设定长文 → 回头修改对应上游节点,不要用本步重写。
|
||||
前置内容尚未齐全时仍可生成最小可运行配置,并在后续落档时重新组装。
|
||||
boundary: |
|
||||
本能力:合并上游 → 完整运行规格 JSON(interaction、play_slots、context_order/inserts、
|
||||
workers 由槽展开、resident_context、tables)。
|
||||
游玩拓扑:槽位权威;上下文投影排序:插入序与投影位权威;本步不新增白名单外的 ref。
|
||||
变量设计:side_effects / 真值吸入 tables;不创建 variable-update 执行单元。
|
||||
进 play 由用户手动决定。
|
||||
本程序只合并已确认产物并执行校验,不润色正文、不补世界观,也不发明执行单元。
|
||||
普通楼层与 0 层使用同一组装器,由所选呈现产物决定输出合同。
|
||||
```
|
||||
|
||||
## opening
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
# 正文组成
|
||||
|
||||
> 技能文档。程序只切割下方 **fence 块**;`##` 标题仅供人读。
|
||||
> **创作定位:把「回复呈现」已钉的内容展示给用户**(选壳、分区、用安全子集美化)。壳与【正文安全子集】由程序动态装载。
|
||||
> 产物外壳:`docs/context-fragment-design.md`。
|
||||
> 旧称:设计回复格式。
|
||||
|
||||
## meta
|
||||
|
||||
```meta
|
||||
@@ -12,87 +7,62 @@ name: 正文组成
|
||||
id: reply-format
|
||||
artifact: 设计.正文组成
|
||||
declaration: >
|
||||
把「回复呈现」已钉的内容展示给用户:选壳、分区、按【正文安全子集】做折叠/表格等美化;有「随机范围整理」则把隐藏备用随机区接到消息;排在二者之后、开场白之前
|
||||
把普通楼层「回复呈现」的语义区域落成每条消息使用的前端模板:
|
||||
定义区域位置、HTML/CSS/JS、数据绑定与特殊渲染。
|
||||
when: |
|
||||
已有「回复呈现」(或等价:每轮终稿看什么已钉)后排。几乎每局都要。
|
||||
若本局有随机需求:须已有「随机范围整理」(或确认无随机)后再排本步。
|
||||
必须在「开场白与开场变量」之前。
|
||||
已有普通楼层「回复呈现」,需要决定每个语义区域在气泡内如何显示时使用。
|
||||
排在回复呈现之后、开场白与开场变量之前。
|
||||
when_not: |
|
||||
还不知道每轮给用户看什么 → 先「回复呈现」。
|
||||
已定要检定/掷骰但范围表未收 → 先「随机范围整理」。
|
||||
不要排在开场白之后。几乎不跳过。
|
||||
只谈文风遣词 → 「叙事指南」(工序编排:「叙事指南与故事推进」)。只改呈现区域与内容规则(字数/选项槽/监控键)→ 「回复呈现」。
|
||||
还不知道回复包含哪些语义内容 → 先回复呈现。
|
||||
要设计持久 0 层卡、槽位更新或跨轮 Tab → 使用 0层正文组成,不在本节点兼顾。
|
||||
boundary: |
|
||||
本技能:前端展示——从【可选呈现壳】选壳、把回复呈现的各区域落到版式;正文语法以【正文安全子集】为准(折叠、表格等)。
|
||||
回复呈现:必选/可选区域 + 各区简单内容规则——本步不重议清单与规则,只负责怎么显示。
|
||||
随机范围整理:要插哪些数、范围是什么——本步不改范围表,只负责怎么藏进消息。
|
||||
叙事指南:文风。变量设计:真值规则。开场白:第一屏——本步先定展示。
|
||||
feeds: narrator
|
||||
回复呈现决定“有什么、何时出现、来自哪里”;本能力决定“在前端哪里、如何渲染”。
|
||||
状态和拓扑数据由程序投影,本能力只写绑定与渲染器;不得让正文模型重写权威数据。
|
||||
隐藏维护区不进 DOM。随机数是变量表隐藏行,也与本能力无关。
|
||||
壳参考库只提供布局与交互方法参考,不是必须照抄的固定产品模板。
|
||||
feeds: gm,narrator
|
||||
```
|
||||
|
||||
## opening
|
||||
|
||||
```opening
|
||||
本局把「回复呈现」里已钉的内容展示给用户(选壳、分区、安全子集美化)。壳与安全子集由程序动态装载,不要背死。
|
||||
|
||||
1. 对照【可选呈现壳】:哪一个最适合放下已钉的那些东西?
|
||||
2. 各项落到壳的哪一区?显示名叫什么?渲染提示用纯文本还是 Markdown?
|
||||
3. 需要折叠/表格等时,对照【正文安全子集】(一般够用;html/css/js 默认空)。
|
||||
4. 定稿时会用前面情节架空一幕灌进壳里,专供你看版式/美化(内容随便,不当正史)。
|
||||
把回复呈现里已启用的语义区域做成普通消息气泡内的前端。先决定阅读主次和区域落点,再按需设计 Tab、表格、拓扑图等渲染;隐藏维护区不显示。
|
||||
```
|
||||
|
||||
## task
|
||||
|
||||
```task
|
||||
你正在执行「正文组成」。产物必须是 **context-fragment.v1** JSON,写入「设计.正文组成」。
|
||||
|
||||
本步 = **前端展示**:按「回复呈现」的清单,从【可选呈现壳】里选一个,把内容落到区域。
|
||||
正文美化优先【正文安全子集】(用户开 Markdown 渲染时生效)。日常折叠、表格等走子集即可;「渲染」里 html/css/js 默认空字符串。
|
||||
禁止重议「看什么、正文多少字」;禁止自造目录没有的 shell_id;禁止发明子集没有的语法。
|
||||
为普通楼层生成一份可复用前端,写入 `设计.正文组成`。每条助手消息仍各自形成一个气泡;程序把当轮 `present.v1` 灌入同一模板。
|
||||
|
||||
执行顺序:
|
||||
1. 读「回复呈现」与美学/叙事:清单是权威;不重开「看什么」问卷。
|
||||
2. **选 shell_id**(必须是【可选呈现壳】里的 id);说明为何这个壳放得下清单里的项。
|
||||
3. 可见块:把清单各项挂到该壳已有区域;写显示名与职责;渲染提示选「纯文本」或「Markdown」。
|
||||
4. 「渲染」html/css/js:默认全空;仅当用户点名要超出安全子集的交互时再议(本步通常不写)。
|
||||
5. 隐藏段(若清单要求):定界与用途。
|
||||
6. 填「呈现壳.微调」;分区必须与壳一致。
|
||||
7. **示例**:用前面已定情节架空一幕灌进 `present.v1`;body 可演示子集语法(如折叠/表格);字数以回复呈现为准。
|
||||
8. 自评 + 追问;禁止写 context `order`。
|
||||
1. 读取回复呈现,只处理其中实际启用的语义区域。
|
||||
2. 从【壳参考库】选一只与本局已启用区域最接近的壳。按该壳备注改写样本里的名称、栏目、配色和内部小节。本局没有的区从样本骨架删除并重排,不留空位。
|
||||
3. 为每个用户可见区域定义 block、位置、显示名和渲染方式。模型生成区读取对应 block;程序投影区直接绑定权威数据。
|
||||
4. 隐藏 `state_ops` 等维护区列入非渲染处理,不创建 DOM 节点。
|
||||
5. 写完整 HTML/CSS/JS,创作时一次生成并固定;游玩只灌数据,不再重写前端。美化用配色、字体层级、边框、间距,以及 CSS 或简单内联 SVG,按本次能写稳的程度完成。JS 通过 `present.onData(render)` 接收数据;地图所需的 SVG 从拓扑数据绘制,游玩模型不输出 SVG。
|
||||
6. 给一份非正史 `present.v1` 预览数据,覆盖所有可见区域与空态。
|
||||
|
||||
summary:`正文组成 · 适配 {shell_id} · …`。
|
||||
模板总量不超过 60 KB。材料足够便直接成稿;会改变阅读结构的缺口交给 `self_score.gap`。产物正文不放评分、诊断、追问或开放问题。
|
||||
```
|
||||
|
||||
## principles
|
||||
|
||||
```principles
|
||||
1. 创作 = 把「回复呈现」的清单展示出来
|
||||
选壳、分区、显示名、安全子集语法都为那份清单服务。本步验收重点是「看起来对不对」。
|
||||
「按意见修改」:以最新产物为底;用户要求按先后都有效,冲突时以较新的为准。用户说整体可以、或只提美化 → 不换 shell_id。换壳必须用户点名。
|
||||
|
||||
2. 清单权威在「回复呈现」
|
||||
正文篇幅、要不要日期、展示哪些变量,以那份产物为准。本步只决定怎么显示。
|
||||
|
||||
3. 壳是底,子集是语法
|
||||
从【可选呈现壳】选底;Markdown 区只使用【正文安全子集】。不要发明新后端协议、目录没有的壳、或子集没有的语法。
|
||||
|
||||
4. 隐藏段服务维护,不污染阅读
|
||||
|
||||
5. 与转述、变量分工
|
||||
文风归叙事指南;真值归变量设计;看什么归回复呈现;本步只交展示契约。
|
||||
|
||||
6. 示例 = 壳与美化预览,非正史
|
||||
用已定材料随便架空一幕,把各区域/监控/行动坞撑满,让用户看见版式。
|
||||
禁止把示例当成开场白或游玩 canon;禁止为写好看而新造设定;文笔及格即可,勿打磨成章。
|
||||
1. 语义区域必须全部有去处;未启用区域不补齐,隐藏维护区不渲染。
|
||||
2. 正文是主阅读面。辅助状态、线索、地图和文字选项按用途确定层级,不平均分割注意力。
|
||||
3. 程序投影区只消费权威 block:变量、拓扑和程序结果不要求模型重复填写。
|
||||
4. HTML 只放稳定骨架;数据由 `present.onData` 注入。缺 block 时按合同省略或显示空态。
|
||||
5. Tab、折叠、缩放等纯本地交互可以使用;不得联网、提交表单、弹窗、顶层导航或向宿主发送用户行动。
|
||||
6. Markdown 使用宿主提供的安全渲染能力;动态文字使用安全 API 或转义后写入,不能拼接未清洗 HTML。
|
||||
7. 地图/关系图由 JS 根据结构化拓扑绘制;显示坐标只属于布局,不回写世界真相。
|
||||
8. 预览只用于验收前端,不写成开场白或正史。
|
||||
9. 美化落在配色、字体层级、边框、间距和简单内联 SVG 上,写进 CSS,按本次能写稳的程度完成。
|
||||
```
|
||||
|
||||
## probe
|
||||
|
||||
```probe
|
||||
每轮 1~2 点。
|
||||
|
||||
优先:壳能否放下回复呈现的各项;分区与显示名;渲染提示纯文本还是 Markdown;要不要子集里的折叠/表格。
|
||||
不追问:重议字数/变量清单、事件池 schema、文风遣词、示例文笔好坏、本局任意 CSS/JS。
|
||||
每轮 1~2 点。优先确认阅读主面、辅助区域是否需要并列或折叠、拓扑采用何种可读形式。语义内容、篇幅、变量更新规则不在这里重议。
|
||||
```
|
||||
|
||||
## output
|
||||
@@ -101,168 +71,130 @@ summary:`正文组成 · 适配 {shell_id} · …`。
|
||||
{
|
||||
"schema": "context-fragment.v1",
|
||||
"技能": "正文组成",
|
||||
"brief": "一句话:选了哪壳、关键区怎么挂",
|
||||
"mount": ["narrator"],
|
||||
"brief": "一句话说明普通消息的阅读结构与特殊渲染",
|
||||
"mount": ["world-simulator", "narrator"],
|
||||
"稳变": "stable",
|
||||
"正文": {
|
||||
"依据的体验与呈现": {
|
||||
"回复呈现要点": "从设计.监控栏摘:必选区 + 字数等",
|
||||
"美学或叙事可映到版式的一句": "可空"
|
||||
},
|
||||
"呈现壳": {
|
||||
"shell_id": "必须是【可选呈现壳】里的 id",
|
||||
"为何选它": "对照回复呈现要同时看见什么",
|
||||
"微调": {
|
||||
"monitor_fields": [],
|
||||
"block_labels": {},
|
||||
"show_suggested_actions": true,
|
||||
"tone_chrome": "default|messenger|book|terminal",
|
||||
"empty_states": {}
|
||||
}
|
||||
},
|
||||
"版式隐喻": "可空;一句气质,不是新壳",
|
||||
"可见块": [
|
||||
"参考壳": [
|
||||
{
|
||||
"块id": "与回复呈现一致的短名",
|
||||
"挂到区域": "monitor|header|body|footer|aside",
|
||||
"显示名": "可空",
|
||||
"职责": "一句",
|
||||
"来源提示": "转述填|程序填|混合"
|
||||
"id": "壳参考库中的方法 id",
|
||||
"采用": "借用什么布局、阅读节奏或局部交互方法",
|
||||
"本局化": "为本局语义区域作了什么调整"
|
||||
}
|
||||
],
|
||||
"监控栏锚点": {
|
||||
"启用": true,
|
||||
"字段": ["与回复呈现一致;无则 []"]
|
||||
"阅读结构": {
|
||||
"主阅读面": "body",
|
||||
"辅助信息关系": "并列|折叠|标签页|正文前后|本局描述",
|
||||
"响应式": "窄屏与宽屏如何保持正文优先"
|
||||
},
|
||||
"隐藏段": [
|
||||
{
|
||||
"段id": "variable-delta",
|
||||
"用途": "变量维护语句,供程序拆进变量.当前",
|
||||
"定界或正则约定": "例:<<<VARS>>>...<<<END>>>",
|
||||
"用户界面": "默认隐藏|折叠可见|调试可见",
|
||||
"模型必须输出": true,
|
||||
"内容形状": "短键值/JSON行;非 chance toolcall",
|
||||
"示例": "可选"
|
||||
}
|
||||
],
|
||||
"前端拆分与美化": {
|
||||
"需要前端分区": "是|否",
|
||||
"区域": [
|
||||
{
|
||||
"区域id": "monitor|header|body|footer|aside",
|
||||
"对应块id": ["…"],
|
||||
"意图": "固定顶栏|主滚动|折叠文末|…",
|
||||
"渲染提示": "纯文本|Markdown"
|
||||
}
|
||||
],
|
||||
"渲染": {
|
||||
"html": "",
|
||||
"css": "",
|
||||
"js": ""
|
||||
},
|
||||
"说明": "Markdown 区语法见【正文安全子集】;渲染三字段默认空。无分区则写「单流渲染」(通常 prose)"
|
||||
},
|
||||
"拼装与终稿": {
|
||||
"终稿tag": "通常 输出.用户展示",
|
||||
"拼装方": "叙事转述一次写出各块|程序按块拼接|混合",
|
||||
"推荐灌数": "分块壳优先 present.v1;prose 可为纯 Markdown(安全子集)",
|
||||
"单轮正文篇幅": "以回复呈现的字数为准,本步不重钉",
|
||||
"与正文关系": "转述读主世界「输出.用户展示」再填各块;可补残稿与版式,不改剧情;隐藏段可供旁观解析维护语句"
|
||||
},
|
||||
"示例": {
|
||||
"性质": "验收预览|非正史|仅看壳与美化",
|
||||
"依据": ["前面已定情节/设定要点,一两句即可"],
|
||||
"用来说明": "这幕只为撑开版式(分区、监控、行动坞、显示名…),内容可糙",
|
||||
"灌数": {
|
||||
"schema": "present.v1",
|
||||
"shell": "与正文.呈现壳.shell_id 一致",
|
||||
"blocks": {
|
||||
"monitor": "若该壳有 monitor:短键值;否则省略",
|
||||
"header": "若有则短字;无则省略",
|
||||
"body": "架空主读文;需要时可演示 :::details / 表格(短段即可)",
|
||||
"aside": "若有则短线索/备注;无则省略",
|
||||
"footer": "若有则可空或一句"
|
||||
"数据契约": {
|
||||
"schema": "present.v1",
|
||||
"blocks": {
|
||||
"body": {
|
||||
"类型": "markdown",
|
||||
"来源": "model_generated",
|
||||
"职责": "主读正文",
|
||||
"空值": "error"
|
||||
},
|
||||
"meta": {
|
||||
"suggested_actions": ["若该壳开建议行动则给 2~3 个占位选项;否则 []"]
|
||||
"本局其它可见区域 id": {
|
||||
"类型": "text|markdown|kv|list|table|topology",
|
||||
"来源": "model_generated|program_projection",
|
||||
"职责": "从回复呈现继承",
|
||||
"空值": "omit|empty_state|error"
|
||||
}
|
||||
},
|
||||
"不渲染": [
|
||||
{
|
||||
"id": "内联维护区 id;无则省略",
|
||||
"交给": "运行时维护解析器"
|
||||
}
|
||||
]
|
||||
},
|
||||
"区域落点": [
|
||||
{
|
||||
"block_id": "与回复呈现一致",
|
||||
"位置": "DOM 区域或 Tab id",
|
||||
"显示名": "需要标题时填写;不需要则空字符串",
|
||||
"渲染": "safe_markdown|text|kv|list|table|topology_svg",
|
||||
"交互": "无|本地切换|折叠|缩放",
|
||||
"空态": "省略或显示什么"
|
||||
}
|
||||
],
|
||||
"frontend": {
|
||||
"html": "<div id=\"app\">完整稳定骨架</div>",
|
||||
"css": "完整样式;正文优先;含窄屏规则",
|
||||
"js": "使用 present.onData(render) 的完整渲染代码"
|
||||
},
|
||||
"示例灌数": {
|
||||
"schema": "present.v1",
|
||||
"blocks": {
|
||||
"body": "非正史短占位正文",
|
||||
"本局其它可见区域 id": "与其类型相符的非正史数据"
|
||||
}
|
||||
}
|
||||
},
|
||||
"自评": {
|
||||
"维度": [
|
||||
{
|
||||
"名": "可见完备",
|
||||
"分数": 8,
|
||||
"说明": "用户该看见的块是否齐、顺序是否服务体验"
|
||||
},
|
||||
{
|
||||
"名": "清单落实",
|
||||
"分数": 8,
|
||||
"说明": "回复呈现的各项是否都落到壳区域"
|
||||
},
|
||||
{
|
||||
"名": "可实现",
|
||||
"分数": 8,
|
||||
"说明": "shell_id 是否在【可选呈现壳】内;Markdown 是否只用【正文安全子集】"
|
||||
},
|
||||
{
|
||||
"名": "版式可感",
|
||||
"分数": 8,
|
||||
"说明": "示例灌数是否按壳撑开各区,使人一眼能验收美化(不论文笔)"
|
||||
}
|
||||
],
|
||||
"薄弱点": "一句话"
|
||||
},
|
||||
"追问": {
|
||||
"导语": "为钉准用户看见的版式,还需确认:",
|
||||
"题目": [
|
||||
{
|
||||
"问": "…",
|
||||
"建议选项": ["选项A", "选项B", "其它(请写明)"],
|
||||
"示例": "可选"
|
||||
}
|
||||
]
|
||||
},
|
||||
"开放问题": []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
硬规则:
|
||||
1. 合法 JSON;含公共三段外壳。
|
||||
2. 正文须含 `呈现壳.shell_id`(须在【可选呈现壳】内)与 `微调`;可见块只挂壳已有区域。
|
||||
3. `可见块` 至少 1 条(通常含 body);禁止自造区域 id / 目录没有的壳。
|
||||
4. 正文须含 `示例`:`性质` 标明非正史;`灌数` 为 `present.v1`,`shell` 与本步 shell_id 一致;只填该壳已有区域。
|
||||
5. 示例内容可糙:只为验收壳与美化;禁止当开场白/正史;禁止为示例新造设定。
|
||||
6. 禁止程序 API schema、context `order`。Markdown 区只使用【正文安全子集】;渲染 html/css/js 默认空。
|
||||
7. 自评:可见完备 / 清单落实 / 可实现 / 版式可感。
|
||||
8. summary:`正文组成 · 适配 {shell_id} · {brief 缩略}`。
|
||||
9. 兼容旧称产物 tag「设计.回复格式」。
|
||||
1. 输出合法 JSON;`数据契约.blocks` 必须覆盖回复呈现的全部用户可见区域,id 保持一致。
|
||||
2. 程序投影区不得改成模型生成;隐藏维护区只能进入 `不渲染`,不得出现在 DOM 或预览 blocks。
|
||||
3. `frontend.html/css/js` 均须给出可执行内容;合计不超过 60 KB。JS 必须调用 `present.onData(render)`。
|
||||
4. HTML 不嵌入本轮数据;动态文字安全渲染。禁止网络、表单、弹窗、顶层导航和宿主行动回传。
|
||||
5. topology 渲染读取结构化节点与连接;不得要求游玩模型输出 SVG。
|
||||
6. `示例灌数` 是非正史预览,schema 固定 `present.v1`,只含用户可见 blocks。
|
||||
7. 不重定义语义区域、篇幅、文风、变量规则、随机数或 0 层槽位更新。
|
||||
8. 产物正文不含自评、追问或开放问题。
|
||||
9. `mount` 固定为 `["world-simulator", "narrator"]`;兼容旧称 tag `设计.回复格式`。
|
||||
|
||||
## score
|
||||
|
||||
```score
|
||||
区域落实:
|
||||
- 5:每个可见语义区域都有 block、落点和渲染器;隐藏区明确不渲染。
|
||||
- 3:主体完整,但有一个辅助区落点或空态含糊。
|
||||
- 1:漏区,或擅自添加回复呈现没有的内容。
|
||||
|
||||
阅读结构:
|
||||
- 5:正文始终是主阅读面,辅助信息层级与响应式布局服务本局体验。
|
||||
- 3:总体可读,但某个并列区略抢正文或窄屏处理不足。
|
||||
- 1:区域平均堆叠,阅读焦点混乱。
|
||||
|
||||
权威绑定:
|
||||
- 5:变量、拓扑和程序结果只读权威投影,模型生成区边界清楚。
|
||||
- 3:主要绑定正确,但有一个权威字段仍可能被模型重复填写。
|
||||
- 1:地图或状态每轮由模型重编。
|
||||
|
||||
实现完整:
|
||||
- 5:HTML/CSS/JS 与 present.v1 合同闭合,空态、安全渲染和故障降级均可实现。
|
||||
- 3:主体可运行,但有一处事件、空态或数据类型未闭合。
|
||||
- 1:只有外观描述或空代码,无法执行。
|
||||
|
||||
本局化:
|
||||
- 5:参考壳的方法被消化成本局布局,没有固定产品换皮感。
|
||||
- 3:适配基本成立,但仍明显照搬单一参考。
|
||||
- 1:只选一个壳名,没有根据语义区域设计。
|
||||
```
|
||||
|
||||
## checklist
|
||||
|
||||
```checklist
|
||||
- [ ] 是把「回复呈现」的清单展示出来,而不是重议看什么?
|
||||
- [ ] shell_id 在【可选呈现壳】内?可见块未超出该壳分区?
|
||||
- [ ] 清单各项已落到区域?字数未在本步另定一套?
|
||||
- [ ] Markdown 区语法落在【正文安全子集】内?渲染三字段默认空?
|
||||
- [ ] 有示例灌数(present.v1),壳一致、各区撑开,且标明非正史?
|
||||
- [ ] 未把示例写成开场白或打磨成长文?
|
||||
- [ ] 回复呈现的全部可见区域均有 block、落点与渲染方式?
|
||||
- [ ] 隐藏维护区未进入 DOM 或预览?
|
||||
- [ ] 程序投影的变量和拓扑没有要求模型重写?
|
||||
- [ ] HTML/CSS/JS 完整,JS 使用 present.onData(render),总量不超 60 KB?
|
||||
- [ ] 美化已用配色、字体层级、边框、间距或简单 SVG 写进 CSS,程度限于本次能写稳的部分?
|
||||
- [ ] 本地交互不联网、不回传行动,动态内容安全渲染?
|
||||
- [ ] present.v1 预览覆盖可见区域并明确非正史?
|
||||
```
|
||||
|
||||
## examples
|
||||
|
||||
```examples
|
||||
好:
|
||||
- 回复呈现要正文+好感:选 chat_monitor,好感进 monitor,正文进 body;渲染提示 Markdown。
|
||||
- 需要折叠说明:body 用 :::details 标题 … :::(见安全子集)。
|
||||
- 需要对照表:body 用 Markdown 表格。
|
||||
- 回复呈现只要正文:选 prose;渲染三字段空。
|
||||
|
||||
坏:
|
||||
- 本步重钉「正文 300 字」而回复呈现已经写了 800~1500。
|
||||
- 不写 shell_id,只写「要好看的 UI」。
|
||||
- 示例写成第一章正史,或为写好看新造未定设定。
|
||||
- 示例 shell 与呈现壳不一致,或灌了壳没有的区域。
|
||||
- 写本局任意 CSS/JS,却只是要折叠或表格。
|
||||
- 只有正文:生成克制的阅读模板,body 用安全 Markdown 渲染,不添加无用侧栏。
|
||||
- 正文 + 状态:状态 block 绑定程序投影,可放正文上方或紧凑侧区;正文模型不输出状态副本。
|
||||
- 正文 + 地图:JS 根据 topology block 绘制 SVG;节点名称安全转义,窄屏可切到列表降级。
|
||||
- 多个辅助区:确有必要时使用本地 Tab;切换只改变当前气泡显示,不向宿主发送消息。
|
||||
- 武侠阅读:纸色、墨色、细线框和一条简单 SVG 纹样;只保留本局启用的区。
|
||||
- 不合适:把隐藏 state_ops 做成折叠面板;只写“机械风界面”却不给代码;让模型每轮输出地图 SVG。
|
||||
```
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
# 回复呈现
|
||||
|
||||
> 技能文档。程序只切割下方 **fence 块**;`##` 标题仅供人读。
|
||||
> 产物外壳:`docs/context-fragment-design.md`。
|
||||
> 旧称:设计监控栏 / 设计状态栏。
|
||||
> 本步钉:**每轮终稿有哪些区域(必选/可选)**,以及各区域的**简单内容规则**;不排版、不选壳。
|
||||
|
||||
## meta
|
||||
|
||||
```meta
|
||||
@@ -12,81 +7,63 @@ name: 回复呈现
|
||||
id: status-bar
|
||||
artifact: 设计.监控栏
|
||||
declaration: >
|
||||
几乎每局都要:钉每轮终稿的必选/可选区域,以及各区域的简单内容规则(字数、段落、选项怎么组成等);不排版
|
||||
定义普通楼层每轮回复实际启用的语义区域、出现条件、权威来源与内容要求;不决定前端排版和渲染。
|
||||
when: |
|
||||
舞台/世界/变量等上游大致齐后排。几乎每局都要。
|
||||
排在「正文组成」之前:先钉有什么区域、各区怎么写,再交给正文组成去展示。
|
||||
已有故事推进及本局需要展示的变量、拓扑或其它内容后使用;排在正文组成与开场白之前。
|
||||
when_not: |
|
||||
上游还没齐 → 先那些。
|
||||
只谈怎么排版、选壳、CSS/HTML/JS → 「正文组成」。
|
||||
只谈文风遣词 → 「叙事指南」(工序编排:「叙事指南与故事推进」)。
|
||||
检定/掷骰范围汇总 → 「随机范围整理」。
|
||||
变量如何增减 → 「变量设计与更新规则」(本步只定展示哪些、展示规则)。
|
||||
boundary: >
|
||||
本技能:区域清单(必选/可选)+ 各区域简单内容规则(字数、自然段、选项槽位与提示口径等)。
|
||||
正文组成:如何把这些区域展示给用户(壳、美化、CSS/HTML/JS)——本步不定版式。
|
||||
随机范围整理:隐藏备用随机数表——本步不收范围。
|
||||
变量设计:真值与更新——本步可点名要展示的键与监控规则,不写增减公式。
|
||||
feeds: narrator
|
||||
每轮只需普通正文且语义合同已由上游明确、无需额外区域时可使用最小正文区域,不扩展其它块。
|
||||
只讨论 HTML、CSS、Tab、分区或具体渲染 → 正文组成。
|
||||
boundary: |
|
||||
本能力决定回复由哪些可见或隐藏语义区域组成、何时出现、内容来自哪里、怎样才算填对。
|
||||
正文组成负责把这些区域放到前端的具体位置并实现 HTML/CSS/JS、Tab 和特殊渲染。
|
||||
故事推进是单轮字数及扩写/续写预算的唯一来源;本能力不重复定义篇幅。
|
||||
变量设计与拓扑图谱定义权威数据和更新规则;本能力只决定回复是否包含其展示区或隐藏更新区。
|
||||
feeds: gm,narrator
|
||||
```
|
||||
|
||||
## opening
|
||||
|
||||
```opening
|
||||
每轮最终回复要分哪些区域?(想到什么写什么)
|
||||
|
||||
1. 必选:正文——字数、段落形态?未提及时默认:约 1000~2000 字;完整长自然段,不要单句成段。
|
||||
2. 要不要状态/变量监控(如开头日期、好感)?盯哪些键?
|
||||
3. 要不要选项栏?若要:几个槽、各自什么气质(如 A 冒险 / B 稳重),生成选项时遵守什么提示?
|
||||
4. 要不要可选块:小故事旁支、线索条等?没有就说没有。
|
||||
每轮回复除正文外,还需要哪些语义内容?逐项说明它何时出现、由模型生成还是由程序投影,以及用户是否可见。若有地图或状态展示,请指出权威数据来源;若维护策略要求结构化隐藏更新,也把它列为隐藏区域。
|
||||
```
|
||||
|
||||
## task
|
||||
|
||||
```task
|
||||
你正在执行「回复呈现」。产物必须是 **context-fragment.v1** JSON,写入「设计.监控栏」。
|
||||
为普通楼层定义每轮回复的语义区域合同,写入 `设计.监控栏`。只写本局实际启用的区域;未启用的区域直接省略。
|
||||
|
||||
本步做两件事:
|
||||
1. **区域**:每轮终稿有哪些块——哪些必选、哪些可选。
|
||||
2. **简单规则**:每个启用区域怎么生成/填写(字数、自然段、选项槽与提示口径、监控键等)。
|
||||
每个区域写清:
|
||||
- 用途与用户可见性。
|
||||
- 每轮出现或满足条件才出现。
|
||||
- 来源是模型生成、程序权威投影,还是维护通道。
|
||||
- 对应的上游引用与正向内容要求。
|
||||
- 没有内容时是省略、给空态,还是仍须输出。
|
||||
|
||||
不选壳、不写 CSS/HTML/JS、不排版。展示交给「正文组成」。
|
||||
正文区域只引用 `设计.故事推进` 的单轮篇幅与扩写/续写预算,不复制数值。状态、地图等权威内容使用程序投影,正文模型不重复生成。选项若启用,只定义文字建议的组成规则,不声明点击交互。
|
||||
|
||||
默认必选:`正文`。用户未点名正文规格时,采用默认规则:
|
||||
- 字数约 **1000~2000**;
|
||||
- **完整长自然段叙述,不要单句成段**。
|
||||
用户另有要求则以用户为准。
|
||||
简单变量采用 `inline_tool / inline_structured` 内联维护时,回复合同包含用户不可见的变量更新语义区;程序优先把它投递为 tool call,工具不可用时改为结构化 `state_ops`。`auditor_bundle` 由副 Worker 维护,不建立主回复更新区。
|
||||
|
||||
常见可选:`状态监控`(日期、变量芯片等)、`选项栏`、`小故事`(旁支短文)等——按体验增减,不要堆满。
|
||||
|
||||
执行顺序:
|
||||
1. 读美学/叙事/变量/舞台:继承体验与已有真值名。
|
||||
2. 钉必选区域:至少含正文;写清内容规则。未提及时用默认(1000~2000 字;长自然段、勿单句成段)。
|
||||
3. 逐个判定可选区域:要则写规则;不要则不进清单(可在「刻意不启用」说明)。
|
||||
4. 若开选项栏:钉槽位数、各槽气质/组成规则、生成时的短提示口径(供转述遵守)。
|
||||
5. 若开状态监控:点名键、形态、刷新意图;不写变量增减公式。
|
||||
6. 自评 + 追问;输出 JSON。
|
||||
|
||||
summary:`回复呈现 · …`(点题必选+主要可选)。
|
||||
材料足够便直接成稿;真正会改变区域合同的缺口交给 `self_score.gap`。产物正文不放评分、诊断、追问或开放问题。
|
||||
```
|
||||
|
||||
## principles
|
||||
|
||||
```principles
|
||||
1. 区域 = 用户每轮会看见的内容槽;规则 = 槽里怎么写。两者都在本步钉死。
|
||||
2. 正文默认必选。其它区域默认可选:有体验需要才开。
|
||||
3. 规则要可执行、宜短:字数、自然段、选项槽气质、监控键名——够转述与正文组成接上即可。
|
||||
正文规格用户未提及时:字数 1000~2000;完整长自然段,不要单句成段。
|
||||
4. 选项栏的「选项由什么组成 / 各槽提示词」在本步规定,不推到叙事指南或正文组成。
|
||||
5. 不问「长什么样」。壳、分区、CSS/HTML/JS 交给「正文组成」。
|
||||
6. 展示用的变量是子集;增减仍归变量设计。
|
||||
1. 语义区域回答“回复里有什么”,正文组成回答“在前端哪里、怎么显示”。
|
||||
2. 只写实际启用区域。固定每轮出现用 `every_turn`;条件出现须给可判定条件。
|
||||
3. 模型生成区写内容要求;程序投影区写权威 ref 与字段选择。程序已有的数据不让模型复述一份。
|
||||
4. 正文篇幅、扩写和续写预算只引用故事推进;文风只引用叙事指南。
|
||||
5. 状态与拓扑的真相来自运行数据。可见区域只选择投影字段,不在这里修改变量或拓扑。
|
||||
6. 简单变量的隐藏更新也是语义区域;运行期可投递为 tool call 或结构化 `state_ops`。副 Worker 路径不在主回复伪造空区域。
|
||||
7. 选项是文本建议。是否有交互、点击后做什么,只有正文组成和宿主前端真正实现时才声明。
|
||||
8. 区域要求使用正向规格;没有特殊要求便保持短,不为完整感堆区域。
|
||||
9. 每个可见区属于一种:抬头、正文、小故事、数据卡、结构、文字选项。抬头是页首一行短内容;条目一多或当前值一长则改用数据卡。数据卡和结构要写出露出的列或字段。隐藏的变量更新不占这些区。
|
||||
```
|
||||
|
||||
## probe
|
||||
|
||||
```probe
|
||||
每轮 1~2 点:必选是否只剩正文;某可选区开了却没规则;选项槽气质是否空;正文段落/字数是否离谱。
|
||||
不追问:选哪个壳、CSS、文风遣词、变量更新公式。
|
||||
每轮 1~2 点。优先确认某区域是否真的需要、何时出现、由谁生成、权威数据来自哪里。排版、HTML、Tab 和具体渲染留给正文组成。
|
||||
```
|
||||
|
||||
## output
|
||||
@@ -95,130 +72,116 @@ summary:`回复呈现 · …`(点题必选+主要可选)。
|
||||
{
|
||||
"schema": "context-fragment.v1",
|
||||
"技能": "回复呈现",
|
||||
"brief": "一句话:必选哪些区 + 开了哪些可选区",
|
||||
"mount": ["narrator"],
|
||||
"brief": "一句话说明每轮实际包含哪些语义区域及主要权威来源",
|
||||
"mount": ["world-simulator", "narrator"],
|
||||
"稳变": "stable",
|
||||
"正文": {
|
||||
"依据": ["美学/叙事/变量/舞台里与「每轮看见什么」相关的要点"],
|
||||
"必选区域": [
|
||||
"语义区域": [
|
||||
{
|
||||
"id": "body",
|
||||
"名称": "正文",
|
||||
"规则": {
|
||||
"字数": "默认 1000~2000;用户另定则从其",
|
||||
"自然段": "完整长自然段叙述,不要单句成段",
|
||||
"其它": "可选:人称、是否含对白密度等一句"
|
||||
}
|
||||
}
|
||||
],
|
||||
"可选区域": [
|
||||
{
|
||||
"id": "status",
|
||||
"名称": "状态监控",
|
||||
"启用": true,
|
||||
"规则": {
|
||||
"字段": [
|
||||
{
|
||||
"名": "日期|好感|…",
|
||||
"来源": "真值键|投影|临场",
|
||||
"形态": "短文本|数字|枚举"
|
||||
}
|
||||
],
|
||||
"刷新": "每轮|事件后"
|
||||
}
|
||||
"用途": "承载本轮叙事正文",
|
||||
"用户可见": true,
|
||||
"出现": {
|
||||
"方式": "every_turn",
|
||||
"条件": ""
|
||||
},
|
||||
"内容类型": "markdown",
|
||||
"区域种类": "正文",
|
||||
"来源": {
|
||||
"方式": "model_generated",
|
||||
"ref": "设计.故事推进"
|
||||
},
|
||||
"内容要求": [
|
||||
"遵守故事推进中的单轮篇幅与扩写/续写预算",
|
||||
"遵守叙事指南;在本轮自然停笔点结束"
|
||||
],
|
||||
"空值处理": "error"
|
||||
},
|
||||
{
|
||||
"id": "choices",
|
||||
"名称": "选项栏",
|
||||
"启用": true,
|
||||
"规则": {
|
||||
"槽数": 2,
|
||||
"槽": [
|
||||
{
|
||||
"槽id": "A",
|
||||
"气质": "冒险",
|
||||
"组成": "推进风险/探索的行动一句",
|
||||
"生成提示": "写给转述:本槽选项须偏冒险、短、可点"
|
||||
},
|
||||
{
|
||||
"槽id": "B",
|
||||
"气质": "稳重",
|
||||
"组成": "保守/观察/缓和的行动一句",
|
||||
"生成提示": "写给转述:本槽选项须偏稳重、短、可点"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "side_story",
|
||||
"名称": "小故事",
|
||||
"启用": false,
|
||||
"规则": {
|
||||
"字数": "无则空",
|
||||
"与正文关系": "旁支;不抢主线"
|
||||
}
|
||||
}
|
||||
],
|
||||
"刻意不启用": [
|
||||
"本局不开的区域及原因"
|
||||
]
|
||||
},
|
||||
"自评": {
|
||||
"维度": [
|
||||
{
|
||||
"名": "区域清楚",
|
||||
"分数": 8,
|
||||
"说明": "必选/可选是否分明;下游能否直接按区展示与灌数"
|
||||
},
|
||||
{
|
||||
"名": "规则可执行",
|
||||
"分数": 8,
|
||||
"说明": "字数/段落/选项槽/监控键是否够转述遵守"
|
||||
}
|
||||
],
|
||||
"薄弱点": "一句话"
|
||||
},
|
||||
"追问": {
|
||||
"导语": "为钉准每轮区域与规则,还需确认:",
|
||||
"题目": [
|
||||
{
|
||||
"问": "…",
|
||||
"建议选项": ["选项A", "选项B", "其它(请写明)"],
|
||||
"示例": "可选"
|
||||
"id": "本局实际启用的其它区域 id",
|
||||
"名称": "状态、地图、线索、文字选项或本局自定义名称",
|
||||
"用途": "这一语义区域帮助用户获得什么信息",
|
||||
"用户可见": true,
|
||||
"出现": {
|
||||
"方式": "every_turn|conditional",
|
||||
"条件": "conditional 时写程序或模型可判断的条件"
|
||||
},
|
||||
"内容类型": "text|markdown|kv|list|table|topology|state_ops",
|
||||
"区域种类": "小故事|数据卡|结构|文字选项",
|
||||
"来源": {
|
||||
"方式": "model_generated|program_projection|maintenance_channel",
|
||||
"ref": "上游产物、运行状态或维护策略的稳定引用",
|
||||
"字段": ["程序投影时只选择实际需要的字段"],
|
||||
"投递": "仅维护区使用:runtime_selects_tool_or_structured"
|
||||
},
|
||||
"内容要求": [
|
||||
"正向说明该区域应该提供什么"
|
||||
],
|
||||
"空值处理": "omit|empty_state|error"
|
||||
}
|
||||
]
|
||||
},
|
||||
"开放问题": []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
硬规则:
|
||||
1. 合法 JSON;公共三段外壳。
|
||||
2. 正文须含:必选区域(至少正文)、可选区域、刻意不启用。
|
||||
3. 每个启用区域须有可执行的 `规则`;选项栏须含槽气质与生成提示。
|
||||
4. 禁止选壳、写 CSS/HTML/JS、发明前端分区。
|
||||
5. summary:`回复呈现 · {brief 缩略}`。
|
||||
6. 产物 tag 仍为「设计.监控栏」。
|
||||
1. 输出合法 JSON;`语义区域` 只含实际启用项,不写 `启用:false` 或未启用清单。
|
||||
2. 普通楼层至少含 `body`;正文只引用故事推进,不复制目标字数、占比或字数预算。
|
||||
3. 每个区域必须声明出现方式、内容类型、区域种类、来源、内容要求和空值处理。可见区的区域种类只能是抬头、正文、小故事、数据卡、结构、文字选项。
|
||||
4. `program_projection` 必须给稳定 ref;状态和拓扑数据不得标为临场或要求模型重新生成。
|
||||
5. 内联变量更新区必须 `用户可见:false`、内容类型为 `state_ops`、来源为 `maintenance_channel`;程序按 API 能力选择 tool call 或结构化区域。`auditor_bundle` 不建立该区。
|
||||
6. 文字选项只规定数量、差异维度和内容要求;不写可点击、按钮动作或回传协议。
|
||||
7. 不选壳,不写布局、HTML、CSS、JS、Tab、SVG 或槽位替换;这些属于正文组成。
|
||||
8. 产物正文不含自评、追问或开放问题。
|
||||
9. `mount` 固定为 `["world-simulator", "narrator"]`;artifact tag 暂时保留 `设计.监控栏`。
|
||||
|
||||
## score
|
||||
|
||||
```score
|
||||
区域必要性:
|
||||
- 5:每个区域都服务明确体验或运行需要,没有为完整感添加空壳。
|
||||
- 3:主体必要,但有一个区域价值偏弱或可并入其它区域。
|
||||
- 1:堆满状态、线索、选项等区域,却说不清用途。
|
||||
|
||||
来源一致性:
|
||||
- 5:模型生成、程序投影与维护通道区分准确;权威数据都有稳定 ref。
|
||||
- 3:主要来源正确,但有一个字段仍可能被模型重复生成。
|
||||
- 1:状态或拓扑没有权威来源,每轮靠正文模型重编。
|
||||
|
||||
内容可执行性:
|
||||
- 5:出现条件、内容类型、正向要求和空值处理足以让下游直接生产。
|
||||
- 3:区域可理解,但条件或填写要求仍有一处含糊。
|
||||
- 1:只有区域名称,不知道何时出现或应填什么。
|
||||
|
||||
信息负担:
|
||||
- 5:每轮只出现有用内容,条件区能省略,程序已有数据不重复占用模型输出。
|
||||
- 3:整体克制,但有轻微重复或一个区域出现过频。
|
||||
- 1:正文外长期堆积大量无关信息。
|
||||
|
||||
职责边界:
|
||||
- 5:只定义语义内容,并把渲染、维护真相和篇幅分别交给正确节点。
|
||||
- 3:主体边界正确,但夹有少量排版或重复篇幅要求。
|
||||
- 1:在本步写 HTML、变量公式或重新定义推进篇幅。
|
||||
```
|
||||
|
||||
## checklist
|
||||
|
||||
```checklist
|
||||
- [ ] 正文在必选区,且有字数/自然段等规则?
|
||||
- [ ] 可选区逐项启用或刻意不启用?
|
||||
- [ ] 选项栏若开:槽气质与生成提示已写?
|
||||
- [ ] 状态监控若开:字段与来源已写?
|
||||
- [ ] 未写排版或 CSS/HTML/JS?
|
||||
- [ ] 只列实际启用区域,普通楼层含正文?
|
||||
- [ ] 每个区域的出现条件、来源、要求与空值处理完整?
|
||||
- [ ] 状态、地图等权威数据均由程序投影,模型没有重复生成?
|
||||
- [ ] 内联维护的隐藏 state_ops 已声明运行期选路,副 Worker 路径未建立该区?
|
||||
- [ ] 正文只引用故事推进,没有重复字数和比例?
|
||||
- [ ] 未写布局、HTML/CSS/JS、Tab、SVG 或交互动作?
|
||||
```
|
||||
|
||||
## examples
|
||||
|
||||
```examples
|
||||
好:
|
||||
- 必选正文默认 1000~2000 字、长自然段勿单句成段;可选状态监控=日期+好感;选项栏 A 冒险 / B 稳重,各有生成提示。
|
||||
- 必选正文;不开选项、不开小故事;状态只盯体力。用户要求极短时字数可低于默认。
|
||||
|
||||
坏:
|
||||
- 只写「要选项」却不定 A/B 气质与生成提示。
|
||||
- 正文无字数/段落规则。
|
||||
- 在本步选壳或写 CSS。
|
||||
- 只有正文:只列 body,并引用故事推进和叙事指南。
|
||||
- 展示状态:新增 kv 区域,来源指向变量当前值及明确字段;内容由程序投影。
|
||||
- 展示地图:新增 topology 区域,说明用户应看到哪些节点、连接或当前位置;具体 SVG、表格或 Tab 留给正文组成。
|
||||
- 简单变量内联维护:新增用户不可见的 state_ops 语义区域;运行期优先投递为 tool call,工具不可用时进入结构化整包。
|
||||
- 文字选项:写数量、彼此差异和应提供的行动方向;不声明点击行为。
|
||||
```
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
# 故事推进
|
||||
|
||||
> 对话落盘技能。程序只切割下方 **fence 块**;`##` 标题仅供人读。
|
||||
> 产物外壳:`docs/context-fragment-design.md`。
|
||||
> 工序编排仍用「叙事指南与故事推进」;本步只钉怎么往前与用户输入用法,不钉遣词。
|
||||
|
||||
## meta
|
||||
|
||||
```meta
|
||||
@@ -11,76 +7,50 @@ name: 故事推进
|
||||
id: story-progression
|
||||
artifact: 设计.故事推进
|
||||
declaration: >
|
||||
美学已有方向时:钉故事怎么往前、用户输入用法(大纲扩写或扮演停笔);不重做文风问卷
|
||||
从偏好库选取本局约束,只选定一种用户输入用法,并写清单轮目标字数、扩写与续写的定义和篇幅分配、OOC 处理;不写遣词。
|
||||
when: |
|
||||
已有美学纲领(或等价),但尚未钉用户输入用法(大纲扩写 / 扮演停笔),或还不知道故事靠场面自己推、等用户、还是无专门推进。
|
||||
用户主动谈推进、节奏、停笔、大纲扩写也排。
|
||||
遣词未钉、叙事指南未落,不构成不排本步的理由。
|
||||
已有美学纲领(或等价),但尚未钉死唯一的用户输入用法,或扩写/续写边界与比例仍不清。
|
||||
用户主动谈推进、节奏、停笔、大纲扩写、续写、自动写入时也使用。
|
||||
when_not: |
|
||||
本技能只给对话落盘。工序编排不排本步(编排池是「叙事指南与故事推进」)。
|
||||
美学未钉 → 先「美学纲领与交互范式」。
|
||||
只改遣词、笔墨、写法档 → 「叙事指南」。
|
||||
事件池 schema、真值增减 → 「生成规则」/「变量设计与更新规则」。
|
||||
固定输入标记(裸写 / `#行动#` / 引号)由程序解析,不在本步重定义或按局反转。
|
||||
boundary: |
|
||||
本技能:一份 context-fragment,写清怎么往前 + 用户输入用法(大纲扩写或扮演停笔,勿混成半套)。
|
||||
叙事指南:遣词/笔墨/禁忌/写法档——本步不定。
|
||||
美学 / 交互范式:感觉与轮转权限。本步把「输入怎么用」写成游玩可执行的一种。
|
||||
生成规则/变量*:合同与表——推进只写钩子,不写公式。
|
||||
本能力只交推进合同:唯一输入用法、单轮目标字数、扩写要求、续写要求、篇幅分配、OOC 处理策略。
|
||||
叙事指南管怎么写;变量与生成规则管表与合同;主角详略只写偏好钩子。
|
||||
feeds: gm,narrator
|
||||
```
|
||||
|
||||
## opening
|
||||
|
||||
```opening
|
||||
美学里的轮转已有方向时,请谈「故事怎么往前」(想到什么写什么):
|
||||
|
||||
1. 故事往前靠什么:场面自己推、等用户决策,还是无专门推进?
|
||||
2. 用户每轮输入怎么用?未提及时按体验二选一写入(可简短确认,不必当缺口追问):
|
||||
- **大纲扩写**(写手/成稿向):输入=待扩写为事实的大纲;细化完全后写正文;不超出大纲未涉及的剧情,完整演绎大纲。
|
||||
- **扮演停笔**(代入扮演、等用户决策):用户本轮行为与对话作正文开头并扩写填满;不可写用户角色未给出的其它行为/对话;不可替用户角色做决定;正文停在其他角色等用户角色回应之刻。
|
||||
先选定这局唯一的输入用法:大纲扩写、续写,或自动写入。再说清楚单轮大约写多长、扩写和续写各做什么及各占多少;上帝视角/OOC 指令准备怎么处理。
|
||||
```
|
||||
|
||||
## task
|
||||
|
||||
```task
|
||||
你正在执行「故事推进」。产物必须是 **context-fragment.v1** JSON,写入「设计.故事推进」。
|
||||
把本局推进合同写入 `设计.故事推进`。先用 `read_library_entry` 从绑定的偏好库读取最相关 1~2 条并本局化,再只选定一种用户输入用法,按该用法写清单轮目标字数、扩写定义、续写定义、两者各自篇幅预算,以及裸写(上帝视角/OOC)指令如何处理。
|
||||
|
||||
钉清本局故事怎么往前、用户每轮输入怎么用。作用对象是主世界层与转述的游玩正文。
|
||||
禁止混用两种用法或写成半套规则。固定标记怎么识别交给程序;本步只写识别之后怎么服从与落笔。不写遣词、示范段落或版式。
|
||||
|
||||
执行顺序:
|
||||
1. 继承美学/交互范式里的轮转(何时等用户),改写成推进做法。
|
||||
2. 写推进与决策(可写「无专门推进」);变量只写钩子。
|
||||
「用户输入用法」未提及时:按美学/交互范式选一种写入产物——
|
||||
- 写手/大纲成稿 → **大纲扩写**(输入=大纲→细化成正文;不越界、完整演绎);
|
||||
- 代入扮演、轮转等用户决策 → **扮演停笔**(输入的行为与对话作正文开头并扩写填满;不写用户角色未给出的行为/对话;不替用户角色做决定;停在其他角色等回应之刻)。
|
||||
两种都像则写进追问让用户点一次;不要混成半套规则。
|
||||
3. 自评 + 追问。遣词、笔墨、写法档交给「叙事指南」,本步不写。
|
||||
|
||||
summary:`故事推进 · …`
|
||||
材料足够便直接成稿;多种用法都像、必须用户点选时,把缺口交给 `self_score.gap`。产物正文不放评分、诊断、追问或开放问题。
|
||||
```
|
||||
|
||||
## principles
|
||||
|
||||
```principles
|
||||
1. 推进可轻可无;表规格交给变量/生成规则。
|
||||
2. 用户输入用法两套可选默认(未提及时择一,勿混用):
|
||||
- 大纲扩写:输入=大纲→扩写为事实→细化后正文;不发明大纲未涉及的剧情,完整演绎大纲。
|
||||
- 扮演停笔:用户本轮行为与对话→正文开头并扩写填满;禁止描述用户角色未输入的其它行为/对话;禁止替用户角色做决定;正文停在其他角色需要用户角色回应之刻。
|
||||
3. 交互范式已有停笔权限时:把它收成上面两种之一写入产物,不要因为权限已有就不落本步。
|
||||
```
|
||||
|
||||
## probe
|
||||
|
||||
```probe
|
||||
本步只钉两件:推进方式(场面推进 / 用户决策 / 无专门推进)、用户输入用法(大纲扩写 / 扮演停笔)。
|
||||
|
||||
用户未提输入用法时:按体验选一种写入产物并简短确认;两者都像才追问二选一,不必当成大缺口。
|
||||
推进方式不明则正文写「无专门推进」,不问好感公式、进度表或事件池。
|
||||
|
||||
缺其中一件:在产物「追问」写 1~2 点。两件够验收:`追问.题目` 为 `[]`。
|
||||
|
||||
用户谈到遣词、笔墨、写法档:写入「开放问题」,交给「叙事指南」。
|
||||
用户谈到变量增减、生成规则:只抽取能改推进方式的一句,其余写入「开放问题」。
|
||||
1. 全局只允许一种用户输入用法。切分依据是扩写与续写的定义,以及两者在本局的职责和篇幅预算。
|
||||
2. 三种用法互斥,写入时只保留一个完整合同:
|
||||
- 大纲扩写:输入是待演成场面的大纲。扩写要把每个节拍演满,节拍上的人、事、话、结果保持不变,节拍之间补具体动作把处境写出来;续写短,且不代用户做新决定。
|
||||
- 续写:扩写只跟用户本轮已声明的行为与对话;续写为主,停在其他角色等待用户抉择处,不替用户做态度或推进性决定。
|
||||
- 自动写入:输入多为短 meta 或极简扮演;续写宜长;可按主角设定代演用户角色言行。
|
||||
3. 多写“要怎样完成扩写/续写”,少写否定清单;只有会破坏该用法边界的禁点才保留。
|
||||
4. OOC/上帝视角内容由程序从裸写识别;本步写处理策略:服从到什么程度、是否打断当前场面、如何与既定扩写/续写比例兼容。
|
||||
5. 偏好库是参考:读取后改成本局可执行要求,不照抄。
|
||||
6. 单轮篇幅在这里一次钉死:给目标字数或窄区间,并把扩写、续写的占比与字数预算对应起来;其它节点直接消费,不重复定义。
|
||||
7. 不写遣词、写法档、版式或事件池公式。
|
||||
```
|
||||
|
||||
## output
|
||||
@@ -89,46 +59,68 @@ summary:`故事推进 · …`
|
||||
{
|
||||
"schema": "context-fragment.v1",
|
||||
"技能": "故事推进",
|
||||
"brief": "一句话:推进有无 + 输入用法",
|
||||
"brief": "一句话:唯一输入用法 + 单轮篇幅与扩写/续写分配",
|
||||
"mount": ["world-simulator", "narrator"],
|
||||
"稳变": "stable",
|
||||
"正文": {
|
||||
"依据的体验": ["…"],
|
||||
"推进与决策": {
|
||||
"有无专门推进": "有|无",
|
||||
"驱动力来源": "…",
|
||||
"用户输入用法": "择一写入。大纲扩写:输入=待扩写为事实的大纲;细化后写正文;不超出大纲未涉及处,完整演绎。扮演停笔:用户本轮行为与对话作正文开头并扩写填满;不可写用户角色未给出的其它行为/对话;不可替用户角色做决定;停在其他角色等用户角色回应之刻。用户另定则改写",
|
||||
"叙事速度与详略": "…",
|
||||
"成败与难度": "…",
|
||||
"变量门控钩子": []
|
||||
}
|
||||
},
|
||||
"自评": {
|
||||
"维度": [
|
||||
{ "名": "推进可执行", "分数": 8, "说明": "…" },
|
||||
{ "名": "输入用法钉清", "分数": 8, "说明": "…" }
|
||||
"选用偏好": [
|
||||
{
|
||||
"库条目": "preferences 中的 id",
|
||||
"本局要求": "本局化后的可执行约束"
|
||||
}
|
||||
],
|
||||
"薄弱点": ""
|
||||
},
|
||||
"追问": { "导语": "", "题目": [] },
|
||||
"开放问题": []
|
||||
"推进方式": "场面自推 / 等用户决策 / 无专门推进",
|
||||
"用户输入用法": "大纲扩写 | 续写 | 自动写入",
|
||||
"扩写要求": ["该用法下扩写要做什么、守住什么结果"],
|
||||
"续写要求": ["该用法下续写写到哪里、何时停笔"],
|
||||
"单轮篇幅": {
|
||||
"目标字数": "明确数字或窄区间",
|
||||
"扩写占比": "百分比",
|
||||
"扩写字数预算": "与目标字数及占比一致",
|
||||
"续写占比": "百分比",
|
||||
"续写字数预算": "与目标字数及占比一致",
|
||||
"自然浮动条件": "为完成当前句、动作或必要停笔点允许怎样小幅浮动"
|
||||
},
|
||||
"OOC处理": "裸写/上帝视角指令如何服从、是否打断场面、如何与比例兼容",
|
||||
"主角设定偏好": "大纲扩写或续写→客观设定为主;自动写入→详性格等主观面;无主角则省略"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
硬规则:合法 JSON;输入用法必须是大纲扩写或扮演停笔之一(或用户另定的完整写法),禁止半套;禁止 order;summary 见 task。
|
||||
不写遣词示范、写法档、禁忌清单。
|
||||
硬规则:
|
||||
1. 输出合法 JSON;`用户输入用法` 只能是三者之一,不得并列或混装半套。
|
||||
2. `扩写要求`、`续写要求`、`单轮篇幅` 必须与所选用法一致;两项占比合计 100%,字数预算与目标字数对应。
|
||||
3. `选用偏好` 默认 1~2 项;无合适条目时可 `[]`,但仍须钉死唯一用法。
|
||||
4. 不重定义裸写 / `#行动#` / 引号的识别规则;只写 OOC 处理策略。
|
||||
5. 不在产物正文写自评、追问、开放问题或遣词示范。
|
||||
6. `mount` 固定包含 `world-simulator` 与 `narrator`。
|
||||
|
||||
## score
|
||||
|
||||
```score
|
||||
唯一性与切分:
|
||||
- 5:只选定一种用法;扩写/续写定义、目标字数和预算切分清楚,无第二种用法残留。
|
||||
- 3:主用法明确,但仍有一处边界、占比或字数预算含糊。
|
||||
- 1:混用多种用法,或扩写/续写职责无法区分。
|
||||
|
||||
可执行性:
|
||||
- 5:按该合同可直接开玩,扩写、续写、停笔与 OOC 处理都能执行。
|
||||
- 3:大方向可用,但停笔或 OOC 处理仍需猜测。
|
||||
- 1:只有用法名称,没有可执行要求。
|
||||
|
||||
偏好本局化:
|
||||
- 5:已读偏好并改成本局要求,与唯一用法咬合。
|
||||
- 3:有选用,但微调不足或与用法略冲突。
|
||||
- 1:未读库却声称偏好,或照抄且与本局脱节。
|
||||
```
|
||||
|
||||
## checklist
|
||||
|
||||
```checklist
|
||||
- [ ] 推进清楚或明确无?输入用法是完整的一种?
|
||||
- [ ] 未把遣词/写法档写进本步?
|
||||
```
|
||||
|
||||
## examples
|
||||
|
||||
```examples
|
||||
好:推进写「等用户决策」;输入用法钉「扮演停笔」。
|
||||
好:推进写「无专门推进」;输入用法钉「大纲扩写」,并写清不越界、一次成篇。
|
||||
坏:把两套输入用法混成半套;追问好感公式 / 事件池;或在本步写遣词示范。
|
||||
- [ ] 只选定一种用户输入用法?
|
||||
- [ ] 单轮目标字数、扩写/续写定义、占比与字数预算已写清且互相一致?
|
||||
- [ ] OOC/裸写处理策略已写,且未重定义固定标记语法?
|
||||
- [ ] 偏好已按需读取并本局化?
|
||||
- [ ] 未写入遣词、写法档或半套混用规则?
|
||||
- [ ] 产物正文不含评分、诊断、追问?
|
||||
```
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
# 拓扑图谱
|
||||
|
||||
> 技能文档。程序只切割下方 **fence 块**;`##` 标题仅供人读。
|
||||
> 产物外壳:`docs/context-fragment-design.md`。
|
||||
> 必须生成、但不适合走「生成规则→具体实例」时:用拓扑结构写出来。
|
||||
|
||||
## meta
|
||||
|
||||
```meta
|
||||
@@ -11,70 +7,60 @@ name: 拓扑图谱
|
||||
id: topology
|
||||
artifact: 设计.拓扑图谱
|
||||
declaration: >
|
||||
有些东西必须生成,但不需要或不适合走「生成规则→具体实例」时:用拓扑结构写出来(升级路径、地图、人物关系等)
|
||||
为一个独有但结构复杂的对象定义节点、连接、初始图与开场后的维护规则;可用于地图、关系链、升级树等可变备忘录。
|
||||
when: |
|
||||
已有「美学纲领与交互范式」或等价整体感觉,且同时满足:
|
||||
这些东西必须生成;不需要或不适合走「生成规则→具体实例」;能用拓扑结构描述。
|
||||
很泛:升级路径、地图、人物关系等等都可以。
|
||||
单个对象要用节点和连接才能写清时使用,例如地图、升级路径、关系网。
|
||||
一张图一份产物。不因开场后会不会再长节点而改用别的能力。
|
||||
when_not: |
|
||||
整体感觉未钉 → 先「美学纲领与交互范式」。
|
||||
需要精准格式,或本局原创听了会雾且还要按规则生成 → 「生成规则」;已有规则再要名片 → 「具体实例」。
|
||||
只是活动区域写细、没有拓扑可描述 → 「舞台骨架」。
|
||||
跨轮真值 → 「变量设计与更新规则」。执行单元勾选 → 「游玩拓扑」。
|
||||
没有必须生成的拓扑结构 → 不排。
|
||||
没有节点和连接,只是同类对象的多份记录,或一份要先审字段的合同 → 生成规则,再按需填具体实例。
|
||||
只有一条字段很少的具名内容,且用户明确要留下 → 具体实例。
|
||||
只有地点、势力或社会格局的叙述,没有可定义的连接 → 舞台骨架。
|
||||
只需保存少量数值或枚举 → 变量设计。
|
||||
boundary: |
|
||||
本技能:用拓扑结构写出必须存在、又不走生成规则合同的内容(节点、连接、方向/层级)。
|
||||
生成规则 / 具体实例:复杂合同与 records——本步不写生成格式、不填名片表。
|
||||
舞台骨架:社会结构与活动区(谁在台上、区怎么划)——有节点与连接才走本步;仅格局叙述、无边可画 → 舞台骨架。
|
||||
游玩拓扑:固定槽勾选——本步不描述执行单元谁读谁写。
|
||||
变量*:跨轮状态——本步不设计变量表。
|
||||
feeds: gm
|
||||
本能力定义一张图的结构真相与维护合同,不负责界面、SVG、排版或是否展示。
|
||||
「正文组成」决定是否渲染以及显示入口;维护合同编译工具负责把本产物的更新要求交给运行期维护者。
|
||||
本能力不设计 Worker 拓扑,不把图拆成生成规则,也不把节点扩写成人物卡或地点百科。
|
||||
feeds: gm,auditor
|
||||
```
|
||||
|
||||
## opening
|
||||
|
||||
```opening
|
||||
先说要写哪块拓扑(想到什么写什么):
|
||||
|
||||
1. 有什么必须生成、又不适合走「生成规则→具体实例」?
|
||||
2. 它的拓扑是什么:升级路径、地图、人物关系,还是别的连接?
|
||||
例:家门→小区门→花园↑;谁连谁;功法从哪一阶接到哪一阶。
|
||||
说说要保存的这张图是什么、游玩时用它记住什么,以及开场后它应保持固定、只记录状态,还是允许继续长出节点和连接。
|
||||
```
|
||||
|
||||
## task
|
||||
|
||||
```task
|
||||
你正在执行「拓扑图谱」。产物必须是 **context-fragment.v1** JSON,写入「设计.拓扑图谱」。
|
||||
为一个独有的结构对象设计拓扑图谱,写入 `设计.拓扑图谱#图名`。每份产物只定义一张图:先写名称与用途,再定义节点字段、连接字段和初始图,最后写开场后的动态模式与维护规则。
|
||||
|
||||
本步写出必须生成、但不需要或不适合走「生成规则→具体实例」的内容。用拓扑结构描述即可:升级路径、地图、人物关系等都很泛,能连起来就写。
|
||||
动态模式只选一种:
|
||||
- `static`:节点、连接和状态均固定。
|
||||
- `record`:节点与连接固定,但可记录发现、备注或节点/连接状态。
|
||||
- `expand`:允许按规则新增、修改或删除节点与连接。
|
||||
|
||||
执行顺序:
|
||||
1. 认定本步写哪几块拓扑(名称 + 为何必须生成、为何不走生成规则)。
|
||||
2. 每块列出节点与连接(从哪到哪、什么关系、有无方向或层级)。
|
||||
3. 路径写清顺序或岔路;关系写清称谓或从属;升级写清阶与阶怎么接。点保持短名,不写成百科或名片表。
|
||||
4. 填齐正文 + 自评 + 追问,输出 JSON。
|
||||
拓扑是呈现无关的运行备忘录。只说明存什么、怎样合法修改;不生成 SVG、坐标、前端、Tab 或正文展示格式。需要从正文事实推断的变化交给统一维护副 Worker;必须在正文前完成的机械更新才标为 `pre_narrative`。
|
||||
|
||||
若程序已发默认问题:禁止重复开场;在首答上补洞。
|
||||
summary:`拓扑图谱 · {名称或类型}`。
|
||||
材料足够便直接成稿;真正会改变结构合同的缺口交给 `self_score.gap`。产物正文不放评分、诊断、追问或开放问题。
|
||||
```
|
||||
|
||||
## principles
|
||||
|
||||
```principles
|
||||
1. 先问:必须生成吗?走「生成规则→具体实例」是不是过重或不合适?能用拓扑结构描述吗?三问成立才写。
|
||||
2. 拓扑很泛:升级路径、地图、人物关系都是同一类产物,不要只当成探索地图。
|
||||
3. 节点短、连接准。点不是名片,连接不是散文,也不是生成合同。
|
||||
4. 该用规则反复生成的对象族不要塞进本步。
|
||||
5. 仅有「谁在台上、如何分层」的格局叙述、画不出边 → 交给舞台骨架,本步不写。
|
||||
1. 一产物一张图。不同地图、关系网或升级树分别调用本能力,不能塞进同一 `拓扑` 数组。
|
||||
2. 用途决定字段。节点与连接只保留识别、推演和维护所需信息;字段不重叠,不把节点写成完整名片。
|
||||
3. 节点 id 稳定且唯一;连接只能引用已存在节点,并明确方向、关系和必要属性。
|
||||
4. 动态模式决定维护权限:`static` 无运行写入;`record` 只改允许的状态字段;`expand` 才能改变图结构。
|
||||
5. 维护规则写触发事实、允许操作、执行时机、写入者和不变量。多数由正文事实引起的变化使用 `post_narrative`。
|
||||
6. 图数据与显示分离。相对位置若是世界真相可作为节点/连接字段;屏幕坐标、缩放、颜色与排版不进入本产物。
|
||||
7. 初始图只写已确定内容;可扩展不等于预先填满所有未知节点。
|
||||
8. 给人读的键和专名用中文。材料里的英文名音译成中文。外壳的 schema、brief、mount 保持既有键。
|
||||
```
|
||||
|
||||
## probe
|
||||
|
||||
```probe
|
||||
每轮 1~2 点。已答清的禁止重问。
|
||||
|
||||
优先:为何不走生成规则;这块拓扑是路径、地图还是关系;缺了哪条连接会断。
|
||||
不追问:执行单元读写、生成格式与池、变量公式、舞台百科、具体名片字段。
|
||||
每轮 1~2 点。优先确认用途、节点/连接各需哪些字段、动态模式,以及哪些变化必须跨轮记住。展示方式留给正文组成,不在这里追问。
|
||||
```
|
||||
|
||||
## output
|
||||
@@ -83,85 +69,136 @@ summary:`拓扑图谱 · {名称或类型}`。
|
||||
{
|
||||
"schema": "context-fragment.v1",
|
||||
"技能": "拓扑图谱",
|
||||
"brief": "一句话:写了哪几块拓扑",
|
||||
"mount": ["world-simulator"],
|
||||
"brief": "一句话说明这张图保存什么,以及开场后如何维护",
|
||||
"mount": ["world-simulator", "auditor"],
|
||||
"稳变": "stable",
|
||||
"正文": {
|
||||
"拓扑": [
|
||||
"图名": "这张图的中文短名,稳定且唯一",
|
||||
"名称": "这张地图 / 关系链 / 升级树的名称",
|
||||
"用途": "为什么需要保存它,后续据此推演或记忆什么",
|
||||
"类型": "map|relation|path|tree|network|other",
|
||||
"动态模式": "static|record|expand",
|
||||
"节点字段": [
|
||||
{
|
||||
"名称": "升级路径 / 地图 / 人物关系 / …",
|
||||
"类型": "路径|地图|关系|其它",
|
||||
"用途": "为何必须生成、游玩里怎么用",
|
||||
"节点": [
|
||||
{
|
||||
"id": "英文或短码",
|
||||
"名称": "家门 / 某阶 / 某人短名",
|
||||
"是什么": "一句;不是名片"
|
||||
}
|
||||
],
|
||||
"连接": [
|
||||
{
|
||||
"from": "节点 id",
|
||||
"to": "节点 id",
|
||||
"关系": "通往 / 下一阶 / 父女 / …",
|
||||
"方向": "单向|双向",
|
||||
"备注": "如 ↑ 上楼;无则空字符串"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"自评": {
|
||||
"维度": [
|
||||
{
|
||||
"名": "时机合适",
|
||||
"分数": 8,
|
||||
"说明": "是否必须生成、且不该走生成规则→具体实例"
|
||||
},
|
||||
{
|
||||
"名": "结构清楚",
|
||||
"分数": 8,
|
||||
"说明": "拓扑是否写清、有无断连"
|
||||
"键": "名称",
|
||||
"类型": "string|number|boolean|enum|set",
|
||||
"填写要求": "此字段保存什么、何时必须填写",
|
||||
"可更新": false
|
||||
}
|
||||
],
|
||||
"薄弱点": "一句话"
|
||||
},
|
||||
"追问": {
|
||||
"导语": "为钉准这块拓扑,还需确认:",
|
||||
"题目": [
|
||||
"连接字段": [
|
||||
{
|
||||
"问": "…",
|
||||
"建议选项": ["选项A", "选项B", "其它(请写明)"],
|
||||
"示例": "可选"
|
||||
"键": "关系",
|
||||
"类型": "string|number|boolean|enum|set",
|
||||
"填写要求": "此字段保存什么、何时必须填写",
|
||||
"可更新": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"开放问题": []
|
||||
],
|
||||
"初始图": {
|
||||
"节点": [
|
||||
{
|
||||
"id": "稳定节点 id",
|
||||
"字段": {
|
||||
"名称": "节点短名"
|
||||
}
|
||||
}
|
||||
],
|
||||
"连接": [
|
||||
{
|
||||
"id": "稳定连接 id",
|
||||
"from": "起点 id",
|
||||
"to": "终点 id",
|
||||
"directed": true,
|
||||
"字段": {
|
||||
"关系": "连接含义"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"维护规则": {
|
||||
"允许操作": ["update_node_state"],
|
||||
"默认时机": "post_narrative",
|
||||
"写入者": ["auditor", "user"],
|
||||
"触发与更新": [
|
||||
{
|
||||
"当": "正文中出现什么已成立事实",
|
||||
"做": "对哪个目标执行什么操作",
|
||||
"要求": "操作需要满足的条件"
|
||||
}
|
||||
],
|
||||
"不变量": [
|
||||
"节点 id 不复用;连接端点必须存在"
|
||||
],
|
||||
"用户可编辑": [
|
||||
"允许用户直接修正的字段;无则空数组"
|
||||
]
|
||||
},
|
||||
"维护摘要": {
|
||||
"给统一维护合同": [
|
||||
"需要投影给维护副 Worker 的短规则"
|
||||
],
|
||||
"当前状态引用": "运行.拓扑#图名"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
硬规则:
|
||||
1. 合法 JSON;必含 `schema` / `技能` / `brief` / `正文` / `自评` / `追问`(题目可 `[]`)。
|
||||
2. 正文含 `拓扑` 数组;每块含名称、类型、节点、连接。
|
||||
3. `mount` 默认主世界层;**禁止**写 `order`。不写执行单元槽位、不交 records、不写生成 schema。
|
||||
4. summary:`拓扑图谱 · {brief 缩略}`。
|
||||
1. 输出合法 JSON;一份产物只含一张图,并提供稳定中文 `图名`。
|
||||
2. `节点字段 / 连接字段` 必须说明类型、填写要求和是否可更新;字段应足够且不重叠。
|
||||
3. 初始连接引用的节点必须存在;节点与连接 id 各自唯一。
|
||||
4. `static` 的允许操作与触发更新必须为空;`record` 不得允许新增或删除结构;只有 `expand` 可改变节点与连接。
|
||||
5. 维护规则必须写默认时机、写入者和不变量;不得把正文生成者要求为在可见正文中输出隐藏维护块。
|
||||
6. 不输出 SVG、屏幕坐标、CSS、前端或展示入口;这些属于正文组成。
|
||||
7. 产物正文不含自评、追问、开放问题或多张图。
|
||||
8. `mount` 固定为 `["world-simulator", "auditor"]`。
|
||||
|
||||
## score
|
||||
|
||||
```score
|
||||
对象适配:
|
||||
- 5:确实是一张独有的关系结构,普通实例或扁平变量不足以表达。
|
||||
- 3:存在真实连接,但部分内容可移交舞台、实例或变量。
|
||||
- 1:只是清单、名片或背景散文,没有拓扑必要性。
|
||||
|
||||
字段合同:
|
||||
- 5:节点与连接字段由用途推出,数量恰当、互不重叠,更新权限明确。
|
||||
- 3:主体可用,但有字段含义含混、缺少必要状态或略有重复。
|
||||
- 1:只有通用名称/备注,或堆入整份人物卡与地点百科。
|
||||
|
||||
初始图有效性:
|
||||
- 5:id 稳定唯一,连接均有有效端点,方向与关系足以支持后续推演。
|
||||
- 3:结构基本成立,但有一处连接、方向或字段需要猜测。
|
||||
- 1:存在断边、重复 id,或无法判断节点怎样相连。
|
||||
|
||||
维护可执行性:
|
||||
- 5:动态模式、触发事实、允许操作、时机、写入者和不变量彼此一致。
|
||||
- 3:更新方向正确,但触发条件或操作边界仍不够明确。
|
||||
- 1:只说“后续更新”,维护者无法据此生成合法 patch。
|
||||
|
||||
长期一致性:
|
||||
- 5:返回旧节点、追加新结构和并发修正时都有明确边界,不会重造既有事实。
|
||||
- 3:普通更新稳定,但扩展或冲突处理仍有模糊处。
|
||||
- 1:每轮都可能重写整张图,无法保证跨轮一致。
|
||||
```
|
||||
|
||||
## checklist
|
||||
|
||||
```checklist
|
||||
- [ ] 含 schema / brief / 正文 / 自评 / 追问?
|
||||
- [ ] 每块拓扑有节点和连接?路径/地图/关系能认?
|
||||
- [ ] 确是必须生成且不适合走生成规则→具体实例?
|
||||
- [ ] 这一产物只定义一张确有拓扑必要性的图?
|
||||
- [ ] 节点与连接字段足够、不重叠,并声明可否更新?
|
||||
- [ ] 初始图无重复 id、断边和含糊方向?
|
||||
- [ ] 动态模式与允许操作一致?
|
||||
- [ ] 维护触发、时机、写入者和不变量足以编译进统一维护合同?
|
||||
- [ ] 未提前设计渲染、界面或可见正文维护块?
|
||||
```
|
||||
|
||||
## examples
|
||||
|
||||
```examples
|
||||
好:家门→小区门→花园↑——地图拓扑,连接带方向。
|
||||
好:家族谁连谁——关系拓扑,连接是称谓。
|
||||
好:功法一阶接到二阶、旁支接到某秘术——升级路径。
|
||||
坏:其实该按规则反复生成的对象族(如心之彩)硬写成拓扑。
|
||||
坏:把舞台活动区再抄一遍,没有连接。
|
||||
坏:只有「谁在台上、如何分层」的格局散文、画不出边 → 舞台骨架,不是本步。
|
||||
坏:描述主世界层谁读谁写 → 游玩拓扑,不是本步。
|
||||
- 固定空间:节点与通路固定;若只需记录“已发现/封锁”,使用 record,不因探索重新生成建筑结构。
|
||||
- 关系网络:人物节点固定或可扩展,关系边可记录类型、强度与公开状态;关系变化必须由正文中的已成立事实触发。
|
||||
- 升级树:阶段和前置连接固定时用 static;若允许游玩中发现新分支才用 expand,并写清新增分支必须满足的前置关系。
|
||||
- 不合适:把若干互不相连的人物简介装进节点;应改用生成规则,需要留下的单条再写具体实例。
|
||||
- 不合适:在本产物中指定 SVG 坐标、颜色和 Tab;展示应由正文组成决定。
|
||||
```
|
||||
|
||||
@@ -1,118 +0,0 @@
|
||||
# 用户需求
|
||||
|
||||
> 对话落盘与工序编排共用。程序只切割下方 **fence 块**;`##` 标题仅供人读。
|
||||
> 产物外壳:`docs/context-fragment-design.md`。
|
||||
> 从全局【偏好库】选用硬约束写入本局;不管文风遣词(那是叙事指南)。
|
||||
|
||||
## meta
|
||||
|
||||
```meta
|
||||
name: 用户需求
|
||||
id: user-requirements
|
||||
artifact: 设计.用户需求
|
||||
declaration: >
|
||||
把跨局偏好库里对本局生效的硬约束(禁区/纠偏/默认取向)落成可执行条目;
|
||||
不重做文风问卷,也不钉体验站位
|
||||
when: |
|
||||
提示中有【偏好库 · 可选用】且库非空,或用户主动谈跨局禁区、绝对不要写什么、默认取向要进本局。
|
||||
开局常见:库里已有起步条目且本局尚无「设计.用户需求」。
|
||||
when_not: |
|
||||
本局已有等价「设计.用户需求」且用户未要求改偏好 → 不重复排。
|
||||
只谈文风遣词/示范 → 「叙事指南」(工序编排:「叙事指南与故事推进」)。
|
||||
只谈体验站位/核心感觉 → 「美学纲领与交互范式」。
|
||||
库空且用户未提硬约束 → 不排。
|
||||
boundary: |
|
||||
本技能:一份 context-fragment,写清本局必须遵守的硬约束列表。
|
||||
文风:叙事指南。体验:美学。推进:故事推进。
|
||||
feeds: narrator,gm
|
||||
```
|
||||
|
||||
## opening
|
||||
|
||||
```opening
|
||||
偏好库里已有跨局硬约束时,请确认本局要带哪些(想到什么写什么):
|
||||
|
||||
1. 哪些条目本局生效?有没有要改写、收紧或放宽的?
|
||||
2. 有没有库外、但本局一定要遵守的禁区/纠偏?(一句话即可)
|
||||
```
|
||||
|
||||
## task
|
||||
|
||||
```task
|
||||
你正在执行「用户需求」。产物必须是 **context-fragment.v1** JSON,写入「设计.用户需求」。
|
||||
|
||||
作用对象是本局游玩固定上下文(主世界层与转述都读)。只写硬约束,不写遣词示范。
|
||||
|
||||
执行顺序:
|
||||
1. 若提示中有【偏好库 · 可选用】:默认把 active 条目纳入,再按用户点名增删/改写;勿整库无脑粘贴原文若用户只要子集。
|
||||
2. 用户本局补充的禁区/纠偏:写成可执行短句,与库条并列。
|
||||
3. 自评 + 追问。文风交给叙事指南;体验站位交给美学。
|
||||
|
||||
summary:`用户需求 · …`
|
||||
```
|
||||
|
||||
## principles
|
||||
|
||||
```principles
|
||||
1. 硬约束 ≠ 文风:禁区/取向/纠偏归本步;遣词与示范归叙事指南。
|
||||
2. 条目要短、可执行;写「不要 X,改做 Y」优于空泛「注意分寸」。
|
||||
3. 库空且用户无补充 → 可不排本步;有库则优先落盘,勿装作没有。
|
||||
4. 与本局世界观许可冲突时,以本步产物为准(游玩侧已约定)。
|
||||
```
|
||||
|
||||
## probe
|
||||
|
||||
```probe
|
||||
本步只钉:本局生效的硬约束列表是否够用、有无漏掉用户点名的禁区。
|
||||
|
||||
库条目用户未否决的默认纳入;用户只要子集则只写子集。
|
||||
缺关键禁区且用户话里有信号:在「追问」写 1 点。够用:`追问.题目` 为 `[]`。
|
||||
|
||||
用户谈到文风遣词:写入「开放问题」,交给叙事指南。
|
||||
用户谈到站位/核心感觉:写入「开放问题」,交给美学。
|
||||
```
|
||||
|
||||
## output
|
||||
|
||||
```output
|
||||
{
|
||||
"schema": "context-fragment.v1",
|
||||
"技能": "用户需求",
|
||||
"brief": "一句话:本局硬约束重心",
|
||||
"mount": ["world-simulator", "narrator"],
|
||||
"稳变": "stable",
|
||||
"正文": {
|
||||
"来源说明": "偏好库选用 + 本局补充",
|
||||
"约束": [
|
||||
"短、可执行的一条",
|
||||
"…"
|
||||
]
|
||||
},
|
||||
"自评": {
|
||||
"维度": [
|
||||
{ "名": "可执行", "分数": 8, "说明": "…" },
|
||||
{ "名": "覆盖用户点名", "分数": 8, "说明": "…" }
|
||||
],
|
||||
"薄弱点": ""
|
||||
},
|
||||
"追问": { "导语": "", "题目": [] },
|
||||
"开放问题": []
|
||||
}
|
||||
```
|
||||
|
||||
硬规则:合法 JSON;`约束` 至少 1 条(若用户明确「本局不要任何跨局偏好」则可写一条「本局不套用偏好库」);禁止 order。
|
||||
|
||||
## checklist
|
||||
|
||||
```checklist
|
||||
- [ ] 约束短且可执行?
|
||||
- [ ] 未把文风遣词/体验站位写进本步?
|
||||
- [ ] 用户点名的禁区已覆盖或已追问?
|
||||
```
|
||||
|
||||
## examples
|
||||
|
||||
```examples
|
||||
好:纳入偏好库 NTR/取向两条,并加一条「不要反复报精确数字」。
|
||||
坏:写成叙事遣词示范;或整库粘贴却无视用户「只要取向那条」。
|
||||
```
|
||||
@@ -1,4 +1,4 @@
|
||||
# 变量控制上下文
|
||||
# 信息可见范围
|
||||
|
||||
> 技能文档。程序只切割下方 **fence 块**;`##` 标题仅供人读。
|
||||
> 上游:`variable-design`;方法:`docs/progressive-data-design.md`。
|
||||
@@ -6,23 +6,18 @@
|
||||
## meta
|
||||
|
||||
```meta
|
||||
name: 变量控制上下文
|
||||
name: 信息可见范围
|
||||
id: variable-context
|
||||
artifact: 设计.变量控制上下文
|
||||
declaration: >
|
||||
已有真值或映射时:钉谁看见什么(主世界层/转述/旁观)、未解锁不注入;无信息差则可不排
|
||||
程序根据变量、拓扑和秘密声明整理谁能看到什么、何时可以看到;不让模型另造一份状态摘要
|
||||
when: |
|
||||
变量设计已有真值或 Data 映射,且存在信息差:玩家不该看见但主世界层/旁观要知道,或到某档才允许提起,或旁观需要「当前档摘要」。
|
||||
落档或开玩时自动执行;读取已有公开性、写入权限、揭示条件和固定执行单元。
|
||||
when_not: |
|
||||
尚无变量且无投影 → 不排。
|
||||
仅内部记账、永不进任何提示词 → 跳过,在变量设计备注即可。
|
||||
只改字段与更新规则 → 「变量设计与更新规则」。
|
||||
真值对所有槽同样可见、无剧透边界 → 可不排。
|
||||
无变量、拓扑、秘密或其它可变状态时输出空规则即可,不向用户提问。
|
||||
boundary: |
|
||||
本技能:挂载表、剧透边界、旁观汇总视野、与维护语句/投影 tag 对齐。
|
||||
变量设计:字段、映射索引、维护语句形状、side_effects。
|
||||
具体实例:表行正文——本步不重生成,只声明「旁观读当前档/哪几个摘要键」。
|
||||
回复呈现 / 正文组成:用户可见层(看什么 / 怎么展示);本步管提示词视野。
|
||||
上游内容节点决定公开性、秘密与揭示条件;本程序只编译读取范围,不发明语义。
|
||||
回复呈现决定展示哪些安全投影;正文组成决定如何渲染。
|
||||
feeds: gm,auditor
|
||||
```
|
||||
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
# 变量设计与更新规则
|
||||
|
||||
> 技能文档。程序只切割下方 **fence 块**;`##` 标题仅供人读。
|
||||
> 方法:`docs/progressive-data-design.md`;外壳:`docs/context-fragment-design.md`。
|
||||
> 与「正文组成」隐藏段、生成规则/具体实例的 Data 表、旁观汇总对齐。
|
||||
|
||||
## meta
|
||||
|
||||
```meta
|
||||
@@ -11,78 +7,59 @@ name: 变量设计与更新规则
|
||||
id: variable-design
|
||||
artifact: 设计.变量设计与更新规则
|
||||
declaration: >
|
||||
体验必须跨轮记住少数状态时:钉真值、谁可写、如何变、维护语句,以及按值换文案的映射索引
|
||||
定义必须跨轮保存的最少真值、合法更新条件、权限与派生映射,供程序选择简单状态工具或复杂维护副 Worker。
|
||||
when: |
|
||||
体验需要跨轮记住少数状态,否则会糊:必须可查询、门控、防重复,或到某值换一套态度/文案/章纲。
|
||||
用户点名要好感/资源/阶段数字、分档换性格、进度门控时排。
|
||||
某项状态若不持久保存,就会导致数值、阶段、资源、门控或已发生事实跨轮漂移时使用。
|
||||
when_not: |
|
||||
无状态纯对话,或状态只临场描写、从不程序化 → 不排。
|
||||
只要大段设定不要门控 → 「舞台骨架」/「生成规则」。
|
||||
只规定谁的提示词看见什么 → 「变量控制上下文」。
|
||||
只规定每轮给用户看哪些(正文/字数/变量/日期)→ 「回复呈现」。
|
||||
状态只需从最近正文理解、无需查询或门控 → 不使用。
|
||||
节点与连接构成的地图、关系或路径 → 拓扑图谱。
|
||||
大段设定或同类对象合同 → 舞台骨架 / 生成规则。
|
||||
boundary: |
|
||||
本技能:真值清单、更新规则、维护语句约定、Data 映射索引、side_effects 草案。
|
||||
分档长文/性格切片正文:由生成规则定格式、具体实例填表;本步做索引与门控,不重写整表。
|
||||
正文组成:隐藏段定界符——本步钉「维护语句写什么」;定界样式与之对齐。
|
||||
随机范围整理:检定/掷骰取值范围汇总(程序插备用数)——不是变量表维护手段;本步不收随机表。
|
||||
变量控制上下文:挂载对象与旁观汇总视野。
|
||||
细化终稿:收成 tables.side_effects。
|
||||
feeds: gm
|
||||
本能力定义什么是真值、什么事实允许它改变,以及改变后的约束;不决定 API 投递方式。
|
||||
少量简单变量由程序生成主世界层状态工具;复杂变量与拓扑等由维护合同编译工具汇总。
|
||||
变量控制上下文负责详细可见范围;正文组成负责展示。本能力不写隐藏段、正则、maintain 包或界面格式。
|
||||
feeds: gm,auditor
|
||||
```
|
||||
|
||||
## opening
|
||||
|
||||
```opening
|
||||
先说「玩的时候必须记住、否则体验会糊」的东西:
|
||||
|
||||
1. 数字/阶段?(好感、章节、资源…)一次性门?
|
||||
2. 有没有「到了某值换一套态度/大纲」的表?(表正文可已在具体实例里)
|
||||
3. 每轮如何声明变量变了?(旁观维护 maintain.v1 / 正文隐藏段键值 / 两者)
|
||||
4. 旁观或主世界层是否需要一份「当前映射摘要」(如好感档→性格要点)?
|
||||
说说哪些状态必须跨轮准确记住,以及什么已经发生的事实才能让它们改变。只列不保存就会出错的真值;临时描写不用变量化。
|
||||
```
|
||||
|
||||
## task
|
||||
|
||||
```task
|
||||
你正在执行「变量设计与更新规则」。产物必须是 **context-fragment.v1** JSON,写入「设计.变量设计与更新规则」。
|
||||
为本局定义跨轮真值及其更新规则,写入 `设计.变量设计与更新规则`。先做必要性检验,再为每个真值写稳定 id、类型、初始化要求、公开性、允许写入者、允许操作、范围和基于事实的更新规则。
|
||||
|
||||
三分:
|
||||
- **真值**:跨轮必须记住的少数字段(好感、章节…)
|
||||
- **Data 映射**:阈值→切片大表(性格档、章纲、事件池)——格式归生成规则,行归具体实例;本步建索引并指向实例/规则 id
|
||||
- **Progressive**:side_effects 按真值边沿投影当前切片;旁观/主世界层读投影或汇总,不整表常驻
|
||||
区分两类判断:
|
||||
- `deterministic`:程序能从明确指令、公式或工具结果直接计算。
|
||||
- `narrative_inference`:必须结合本轮正文中已经成立的事实判断;须写证据要求,不能凭倾向猜测。
|
||||
|
||||
**维护语句**:规定如何声明变更(供程序拆进 `变量.当前`)。主路径是旁观维护 `maintain.v1`(读本轮正文推断 table_ops);可选正文隐藏段键值。主世界层不交 settlement 裁决包。
|
||||
这不是掷骰;随机数走「随机范围整理」+ 程序插入的备用数。隐藏段定界与「正文组成」对齐。
|
||||
若真值只用于选择分档文本或实例切片,保存真值并引用映射来源,不把派生结果建立成第二个可写真值。若跨越阈值会触发一次性或边沿效果,写成触发规则。
|
||||
|
||||
执行顺序:
|
||||
1. 读美学/机制/生成规则/具体实例/正文组成;不重开体验。
|
||||
2. 立真值(承重才建);写写入源与更新规则。
|
||||
3. 建 Data 映射索引:每张表写清键依哪个真值、材料来自哪条 rule_id/batch、给旁观/投影用的摘要字段。
|
||||
4. 写维护语句约定:形状、必填键、与隐藏段/旁观维护的关系、示例一句。
|
||||
5. 写 side_effects(once/every_edge → replace_tag 等)。
|
||||
6. 自评 + 追问;输出 JSON。
|
||||
最后根据变量数量、相互依赖、判断方式和触发复杂度给出维护复杂度事实,供程序选择 `none / inline_tool / inline_structured / auditor_bundle`;这是建议,不绑定 API 能力。
|
||||
|
||||
summary:`变量设计 · …`(点题主真值 + 有无映射表)。
|
||||
材料足够便直接成稿;真正会改变变量合同的缺口交给 `self_score.gap`。产物正文不放评分、诊断、追问或开放问题。
|
||||
```
|
||||
|
||||
## principles
|
||||
|
||||
```principles
|
||||
1. 承重才立真值;派生性格不落第二可写格。
|
||||
2. 映射内容:具体实例生成 → 本步索引汇总 → side_effects/变量控制上下文供给旁观与主世界层。
|
||||
3. 维护靠「声明 + 程序合并」,不靠模型直接改库:
|
||||
- 主路径:旁观维护 maintain.v1(读本轮正文)和/或正文隐藏段(正则/定界)→ Runtime 合并进 变量.当前
|
||||
- 边沿:side_effects 换投影 tag
|
||||
- 用户手改;chance 只出随机数,不替代维护语句
|
||||
4. 维护语句必须可生成、可校验:固定键名、短值、禁止散文糊弄。
|
||||
5. 旁观要看的是「当前档摘要」,不是整份实例库;汇总句或投影 tag 即可。
|
||||
6. 宁少勿多;与监控栏字段可交集但职责不同(监控=用户看见;真值=程序真相)。
|
||||
1. 承重才立真值:必须可查询、计算、门控或防止重复;仅为描写方便的状态不落库。
|
||||
2. 字段不多不少且互不重叠。余额与存款只留一个;由好感推得的态度档不另设可写字段。
|
||||
3. 更新规则以已发生事实为证据,写清条件、操作和边界。模型可以判断事实,程序负责校验与提交。
|
||||
4. 类型决定操作:number/integer 可 `set|delta`;enum/string/boolean 可 `set`;set 可 `add|remove`。不用散文值绕过类型。
|
||||
5. 公开性只区分用户可见与隐藏;更细的主世界层、转述、旁观投影交给变量控制上下文。
|
||||
6. 阈值映射只保存引用和选取条件;长文内容仍在生成规则/具体实例中。
|
||||
7. 维护复杂度按实际依赖判断,不机械按数量:一个简单余额可走 inline tool;一个可扩展关系图不属于简单变量。
|
||||
8. 不在正文中设计隐藏标签、正则或维护 JSON;投递方式由程序根据 API 能力选择。
|
||||
```
|
||||
|
||||
## probe
|
||||
|
||||
```probe
|
||||
每轮 1~2 点:主真值;连续数字还是阶段枚举;维护走旁观 maintain / 隐藏段 / 两者;映射表是否已有具体实例可索引;旁观要不要当前档摘要。
|
||||
每轮 1~2 点。优先确认不保存会出什么错、变量类型与初值、哪些已成立事实允许更新,以及更新是程序可确定还是需要正文推断。
|
||||
```
|
||||
|
||||
## output
|
||||
@@ -91,136 +68,138 @@ summary:`变量设计 · …`(点题主真值 + 有无映射表)。
|
||||
{
|
||||
"schema": "context-fragment.v1",
|
||||
"技能": "变量设计与更新规则",
|
||||
"brief": "一句话:主真值 + 映射/维护方式",
|
||||
"mount": ["world-simulator"],
|
||||
"brief": "一句话说明必须记住哪些真值,以及更新判断的复杂度",
|
||||
"mount": ["world-simulator", "auditor"],
|
||||
"稳变": "stable",
|
||||
"正文": {
|
||||
"依据的体验": ["从机制/美学提取的状态化点"],
|
||||
"真值": [
|
||||
{
|
||||
"名": "好感",
|
||||
"类型": "number|enum|boolean|string|set",
|
||||
"初值": 0,
|
||||
"对用户可见": true,
|
||||
"对主世界层可见": true,
|
||||
"写入源": ["llm", "用户"],
|
||||
"更新": {
|
||||
"时机": "…",
|
||||
"方式": "delta|绝对值|枚举切换",
|
||||
"钳制": "0..100|未定",
|
||||
"执行单元": "world-simulator"
|
||||
"id": "稳定安全 id",
|
||||
"名称": "用户可读名称",
|
||||
"用途": "不保存它会造成什么跨轮错误",
|
||||
"类型": "number|integer|enum|string|boolean|set",
|
||||
"初始化": {
|
||||
"默认值": 0,
|
||||
"开场确定规则": "开场可覆盖默认值时写条件;否则写固定"
|
||||
},
|
||||
"备注": ""
|
||||
"公开性": "public|hidden",
|
||||
"允许写入者": ["从 program|gm_tool|auditor|user 中选择实际允许项"],
|
||||
"允许操作": ["按类型从 set|delta|add|remove 中选择"],
|
||||
"约束": {
|
||||
"最小值": 0,
|
||||
"最大值": 100,
|
||||
"枚举值": [],
|
||||
"其它不变量": []
|
||||
},
|
||||
"更新规则": [
|
||||
{
|
||||
"当": "什么已发生事实允许更新",
|
||||
"判断方式": "deterministic|narrative_inference",
|
||||
"证据要求": "程序输入或正文中必须能指出什么",
|
||||
"操作": "set|delta|add|remove",
|
||||
"值规则": "固定值、公式或允许的推断范围",
|
||||
"时机": "pre_narrative|post_narrative"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"Data映射索引": [
|
||||
"派生映射": [
|
||||
{
|
||||
"映射id": "affinity-personality",
|
||||
"用途": "好感分档性格|章大纲|场景包|事件池|…",
|
||||
"键依真值": "好感",
|
||||
"键规则": "例:区间 [0,30)/[30,60)/[60,100]",
|
||||
"结构摘要": "阈值 → 态度/行为切片字段…",
|
||||
"生成规则rule_id": "来自设计.生成规则;无则待补",
|
||||
"具体实例batch_id": "来自设计.具体实例;无则待补",
|
||||
"旁观与投影要用的摘要键": ["档名", "态度要点", "禁触"],
|
||||
"说明": "长文在实例表;本索引只为查表与汇总"
|
||||
"id": "稳定映射 id",
|
||||
"用途": "真值变化后选择什么只读切片",
|
||||
"依据变量": ["变量 id"],
|
||||
"选取规则": "区间、枚举匹配或组合条件",
|
||||
"来源": {
|
||||
"规则名": "设计.生成规则#中文规则名;无则省略",
|
||||
"名称": "设计.具体实例#中文名;无则省略"
|
||||
},
|
||||
"读取字段": ["需要投影的摘要字段"]
|
||||
}
|
||||
],
|
||||
"维护语句约定": {
|
||||
"通道": ["旁观maintain.v1", "隐藏段", "二者都要"],
|
||||
"与正文组成对齐": "隐藏段段id / 定界约定;无隐藏段则写「仅旁观维护」",
|
||||
"语句形状": {
|
||||
"格式": "maintain.v1 table_ops|隐藏段键值列表",
|
||||
"必填键": ["key", "delta|to"],
|
||||
"禁止": ["无键散文", "改未立真值", "主世界层交 settlement JSON"]
|
||||
},
|
||||
"生成要求": "旁观维护按本轮正文推断变更(多数轮可空);主世界层专注故事正文",
|
||||
"示例": [
|
||||
{ "key": "好感", "delta": 8, "reason": "送礼成功" }
|
||||
],
|
||||
"程序侧": "Runtime 合并 maintain.v1 → 变量.当前;再跑 side_effects;非 chance toolcall"
|
||||
},
|
||||
"side_effects": [
|
||||
"触发规则": [
|
||||
{
|
||||
"id": "affinity-romance",
|
||||
"field": "好感",
|
||||
"op": "gte",
|
||||
"value": 60,
|
||||
"mode": "once",
|
||||
"id": "稳定触发 id",
|
||||
"条件": {
|
||||
"变量": "变量 id",
|
||||
"比较": "eq|neq|gte|lte|gt|lt|changed",
|
||||
"值": 0
|
||||
},
|
||||
"模式": "once|every_edge",
|
||||
"action": {
|
||||
"type": "replace_tag",
|
||||
"tag": "上下文.角色态度",
|
||||
"content": "恋爱档要点或「按映射id查当前档摘要」"
|
||||
"类型": "project_mapping|write_memo|queue_generation",
|
||||
"目标": "映射 id、备忘录 id 或规则 id",
|
||||
"要求": "触发后程序应做什么"
|
||||
}
|
||||
}
|
||||
],
|
||||
"旁观汇总意图": {
|
||||
"需要": "是|否",
|
||||
"汇总什么": "例:当前好感档性格要点 + 已触发 once 门",
|
||||
"建议tag": "上下文.旁观.状态摘要",
|
||||
"材料来自": ["Data映射索引", "投影tag", "真值字段"]
|
||||
},
|
||||
"不立变量的理由": ["…"]
|
||||
},
|
||||
"自评": {
|
||||
"维度": [
|
||||
{
|
||||
"名": "承重度",
|
||||
"分数": 8,
|
||||
"说明": "真值是否只覆盖必须跨轮记住的状态"
|
||||
},
|
||||
{
|
||||
"名": "可维护",
|
||||
"分数": 8,
|
||||
"说明": "维护语句是否可解析;映射索引是否接得上实例/规则"
|
||||
},
|
||||
{
|
||||
"名": "克制度",
|
||||
"分数": 8,
|
||||
"说明": "无派生可写格膨胀;无用 chance 冒充改表"
|
||||
}
|
||||
],
|
||||
"薄弱点": "一句话"
|
||||
},
|
||||
"追问": {
|
||||
"导语": "为钉准变量与维护,还需确认:",
|
||||
"题目": [
|
||||
{
|
||||
"问": "…",
|
||||
"建议选项": ["选项A", "选项B", "其它(请写明)"],
|
||||
"示例": "可选"
|
||||
}
|
||||
]
|
||||
},
|
||||
"开放问题": []
|
||||
"维护复杂度": {
|
||||
"建议": "inline_tool|auditor_bundle",
|
||||
"依据": [
|
||||
"变量数量、交叉依赖、narrative_inference、触发规则与其它可变产物"
|
||||
],
|
||||
"可合并更新": [
|
||||
"必须原子提交的一组变量 id;无则空数组"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
硬规则:
|
||||
1. context-fragment.v1 外壳;自评承重度/可维护/克制度。
|
||||
2. Data 映射索引可指向尚未生成的实例(标待补),但键与用途必须清。
|
||||
3. 维护语句约定必填;须声明通道与示例。
|
||||
4. side_effects 可 `[]`。禁止输出完整 workers[]。
|
||||
5. summary:`变量设计 · {brief 缩略}`。
|
||||
1. 输出合法 JSON;每个真值必须有稳定 `id`、用途、类型、初始化、权限、操作、约束和更新规则。
|
||||
2. 类型与允许操作必须相容;number/integer 的边界、enum 的枚举值必须明确;不适用的约束字段省略。
|
||||
3. `narrative_inference` 必须写证据要求;正文没有该证据时不更新。
|
||||
4. 派生映射与触发规则可为空;存在时必须引用已声明变量,且派生结果不得成为第二个可写真值。
|
||||
5. `维护复杂度.建议` 只在 `inline_tool|auditor_bundle` 中选择;程序仍按 API 能力决定具体投递方式。
|
||||
6. 不输出隐藏段、正则、`maintain.v1/v2`、tool schema、Worker 提示或界面字段。
|
||||
7. 产物正文不含自评、追问或开放问题。
|
||||
8. `mount` 固定为 `["world-simulator", "auditor"]`。
|
||||
|
||||
## score
|
||||
|
||||
```score
|
||||
真值必要性:
|
||||
- 5:每个字段不保存都会造成明确跨轮错误,没有临时描写或可派生冗余。
|
||||
- 3:主体必要,但夹有一个可从其它字段或最近正文推出的状态。
|
||||
- 1:把人物感受、场景描述或大段设定全部变量化。
|
||||
|
||||
字段妥当性:
|
||||
- 5:类型、初值、操作和约束彼此一致,字段数量恰当且不重叠。
|
||||
- 3:基本可用,但有边界、枚举或初始化要求不够明确。
|
||||
- 1:类型与操作冲突,或存在余额/存款一类重复真值。
|
||||
|
||||
更新可执行性:
|
||||
- 5:每条规则都有已发生事实、判断方式、证据、操作、值规则和时机。
|
||||
- 3:更新方向明确,但证据或值范围仍需猜测。
|
||||
- 1:只说“随剧情变化”,无法生成或校验更新。
|
||||
|
||||
权限与公开性:
|
||||
- 5:写入者与公开性符合用途,隐藏真值不会因显示或用户误改而泄露、漂移。
|
||||
- 3:大体合理,但有一项写入者或公开边界含混。
|
||||
- 1:任何执行单元都能任意改,或秘密状态被默认公开。
|
||||
|
||||
映射与触发一致性:
|
||||
- 5:映射、阈值触发和原子更新都引用有效变量,且没有建立重复可写真值。
|
||||
- 3:主体能接通,但有来源引用或边沿语义不清。
|
||||
- 1:派生结果反向覆盖真值,或触发引用不存在字段。
|
||||
```
|
||||
|
||||
## checklist
|
||||
|
||||
```checklist
|
||||
- [ ] 真值有写入源与更新时机?
|
||||
- [ ] 映射索引指向规则/实例或标待补?旁观摘要键清楚?
|
||||
- [ ] 维护语句可解析且与正文组成/旁观维护对齐?
|
||||
- [ ] 未把分档长文做成可写真值?未用 chance 当改表?
|
||||
- [ ] 每个真值都通过“不保存会出错”的必要性检验?
|
||||
- [ ] 类型、初值、允许操作、约束和写入者互相一致?
|
||||
- [ ] 每条更新规则都有事实证据和可执行的值规则?
|
||||
- [ ] 派生映射未被重复建成可写真值?
|
||||
- [ ] 维护复杂度依据实际依赖,而不是只按变量数量?
|
||||
- [ ] 未写隐藏标签、正则、维护包或界面格式?
|
||||
```
|
||||
|
||||
## examples
|
||||
|
||||
```examples
|
||||
好:
|
||||
- 好感真值 + 映射索引指向 concrete batch「分档性格」+ side_effect 投影态度 + 维护语句走 variable_changes。
|
||||
- 旁观汇总意图:只挂当前档「态度要点」,不挂整表。
|
||||
|
||||
坏:
|
||||
- 每轮让模型重写「当前性格」长字符串当真值。
|
||||
- 只写「好感会变」无维护语句形状。
|
||||
- 用 run_worker chance 来改好感。
|
||||
- 单个数值真值、只有 set/delta 和上下界、无交叉依赖:建议 inline_tool;程序按 API 探测生成类型明确的小工具。
|
||||
- 多个真值需要结合正文共同判断,且阈值会触发映射或补实例:建议 auditor_bundle;维护编译工具统一收集。
|
||||
- 当前态度由好感区间映射得到:只保存好感,态度作为派生映射,不建立第二个可写“当前态度”。
|
||||
- 不合适:每轮重写一段“当前性格”长字符串,或只写“随剧情变化”而没有证据与操作。
|
||||
```
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# 游玩拓扑
|
||||
# 游玩执行方式
|
||||
|
||||
> 能力文档。程序只切割下方 **fence 块**;`##` 标题仅供人读。
|
||||
> **禁止自由发明执行单元。** 只从固定槽勾选;程序按 `play_slots` 展开 workers。
|
||||
@@ -8,27 +8,18 @@
|
||||
## meta
|
||||
|
||||
```meta
|
||||
name: 游玩拓扑
|
||||
name: 游玩执行方式
|
||||
id: worker-spec
|
||||
artifact: 设计.worker规格
|
||||
declaration: >
|
||||
不能吃默认槽时才勾选:主世界层必开;转述默认关(要独立文风层才开);有变量/表维护才开旁观;强秘密才开视角;写手则大纲+章节;禁止发明执行单元与机遇槽
|
||||
程序根据前面已经确认的写作、维护和秘密隔离需求,整理本局需要启用的固定执行单元
|
||||
when: |
|
||||
体验契约已大致清楚,且不能吃默认槽:要开转述、开旁观、开角色视角(强秘密不能进主世界层),或走写手大纲+章节。
|
||||
或要修订已勾选槽(开/关 auditor、perspective、narrator 等)。
|
||||
落档或开玩时自动执行;从叙事、维护复杂度、信息隔离和写作路径中推导固定槽位。
|
||||
when_not: |
|
||||
体验/站位仍混沌 → 先「美学纲领与交互范式」。
|
||||
世界模拟可吃默认(仅主世界层;转述/旁观/视角关)→ 可不排,「细化终稿」按默认收成。
|
||||
只需输出完整运行规格 → 「细化终稿」(本步只交槽位勾选)。
|
||||
检定/掷骰范围汇总 → 「随机范围整理」;不要为本步加「机遇裁定」槽。
|
||||
不要用本步发明世界观/性格/变量专用执行单元。
|
||||
前置语义尚未确定时沿用安全默认值,不向用户重复询问。
|
||||
boundary: |
|
||||
本能力:输出 play_slots(及写手路径的 writing_slots),可选覆盖挂载说明;不写完整 设计.worker集。
|
||||
主世界层(gm)几乎总要。转述默认关:主世界层直接交用户可见原文。旁观维护:有变量/表/规则要程序化盯时才开。
|
||||
不再设机遇裁定(chance)槽:真随机走「随机范围整理」+ 程序插入上下文备用数。
|
||||
细化终稿:按本步勾选展开 workers、合并常驻与 tables。
|
||||
变量设计 / 变量控制上下文:真值与投影,不是推理槽。
|
||||
叙事指南:默认挂主世界层;若开了转述可挂转述。故事推进挂主世界层(有转述可兼挂)。工序编排二者合为「叙事指南与故事推进」。世界/机制:挂到主世界层——本步只点名槽。
|
||||
本程序只选择白名单中的固定执行单元,不创作内容、不发明专用 Worker。
|
||||
随机数由程序和变量表维护,不建立机遇裁定 LLM。
|
||||
```
|
||||
|
||||
## opening
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
# 舞台骨架
|
||||
|
||||
> 技能文档。程序只切割下方 **fence 块**;`##` 标题仅供人读。
|
||||
> 产物外壳:`docs/context-fragment-design.md`;范例:`aesthetics-interaction` / `mechanism`。
|
||||
|
||||
## meta
|
||||
|
||||
```meta
|
||||
@@ -10,137 +7,42 @@ name: 舞台骨架
|
||||
id: world-blueprint
|
||||
artifact: 设计.舞台骨架
|
||||
declaration: >
|
||||
本局舞台备案:划出玩家实际游玩会换区活动的主要区域并写细;尺度与详细度随体验变。代入后几乎不移动、只在一处待着则不排
|
||||
保存会在游玩中反复影响世界反馈的外部舞台:熟悉基底、必要变造、核心运转和主要活动场所。
|
||||
when: |
|
||||
已有「美学纲领与交互范式」或等价整体感觉,且玩家实际游玩会在划分的活动区域上发生(会换区、会移动)。
|
||||
变造世界与现实世界都要排:只是详细度有所不同。
|
||||
玩家实际游玩过程中的主要活动区域必须写细,但如果本身世界变造不多、或特殊设定较少(如西幻就足以概括的世界),就不需要多写。
|
||||
核心体验依赖外部环境、社会力量或世界状况持续作出一致反馈,而现有材料还不足以支撑这些反馈时使用。
|
||||
是否换区不是判断标准:跨越多地可以需要舞台骨架;一室之内的朝堂、审讯或家庭关系同样可以需要。
|
||||
when_not: |
|
||||
整体感觉未钉 → 先「美学纲领与交互范式」。
|
||||
既少换区、社会格局与世界状况也不需要备案才能撑体验(如网聊、门卫室)→ 不排。
|
||||
虽少换区,但台上势力/运转状况仍决定体验(如一室朝堂博弈)→ 仍排;活动区可极简成一处。
|
||||
可点名名片 → 「具体实例」另排。对象族怎么生成 → 「生成规则」。
|
||||
熟悉现实基底已经够用,地点和外部格局不会改变核心体验或世界反馈时不使用。
|
||||
既成硬法则归实现机制;多份人物或要先审字段的名片归生成规则,单条字段很少的具名内容才归具体实例;重点在节点与连接本身时归拓扑图谱。
|
||||
boundary: |
|
||||
本技能:舞台备案——范围、基底+变造、主要活动区域(写细)、社会结构、世界状况。
|
||||
多张名片/同批实例共享的台上格局与世界状况写在这里,供下游引用。
|
||||
美学纲领与交互范式:体验与轮转——本步不重定。
|
||||
实现机制:既成变造法则——本步把法则落到活动区域上长什么样,不重钉法则来源。
|
||||
具体实例 / 生成规则 / 叙事指南:名片、生成合同(schema/池/描写法)、遣词——本步不定。故事推进(工序编排并入「叙事指南与故事推进」)也不定。
|
||||
本能力只交会上台并反复有用的外部事实,其余沿用熟悉常识或明确留白。
|
||||
不重写核心体验,不解释硬法则起源,不制作人物名片、完整地图、历史或百科。
|
||||
feeds: gm
|
||||
```
|
||||
|
||||
## opening
|
||||
|
||||
```opening
|
||||
先框住「玩家实际会待着玩的地方」(想到什么写什么):
|
||||
|
||||
1. 舞台有多大?熟悉基底是什么?变在哪里?
|
||||
例:一座城 / 一所学校几个系 / 「魔族与人族对峙的前线」(可抽象,不必真地图)。
|
||||
|
||||
2. 社会结构与世界状况里,什么在撑故事?
|
||||
例:人类与丧尸如何相处;每年寒灾;南极通往怪物世界的出口;村中活人祭祀……
|
||||
只点会影响核心运转、会反复碰到的。
|
||||
|
||||
3. 玩家实际游玩的主要活动区域是哪几块?
|
||||
这些区域要写细——不是点名就走。舞台外可以留黑。
|
||||
说说这局里会反复影响你的外部舞台:它以什么熟悉世界为基底,哪里不同,哪些场所、关系或世界状况会持续参与反馈。只写真正会上台的部分即可。
|
||||
```
|
||||
|
||||
## task
|
||||
|
||||
```task
|
||||
你正在执行「舞台骨架」。产物必须是 **context-fragment.v1** JSON,写入「设计.舞台骨架」。
|
||||
把用户材料整理为一份可供主世界层稳定反馈的舞台骨架,写入 `设计.舞台骨架`。先确定最短的熟悉基底与必要变造,再提取会反复影响玩家的社会关系、环境条件和主要场所;体验碰不到的范围明确留白。
|
||||
|
||||
本步是舞台**备案**:变造世界与现实世界都有划分的舞台。钉的是玩家实际游玩过程中的**主要活动区域**——这些区域要写细。范围、基底与变造、社会结构、世界状况沿用现有字段。
|
||||
|
||||
尺度由本局体验决定。同一「现代都市」基底下:
|
||||
- 「体验作为韩国顶级财阀」→ 舞台往往是地球级势力、国家与资本网络,活动区写到财团/宴会/听证层;
|
||||
- 「体验普通大学生活」→ 舞台可能只是一座城或一所学校,活动区写到系馆与社团层。详细度不同,不是跳过本步。
|
||||
|
||||
核心操作:在已确认体验(及可选的既成法则)之上做备案;玩家主要活动区域写细;舞台外留黑。
|
||||
|
||||
执行顺序:
|
||||
1. 读依赖与用户表述,忠实继承体验与既成法则;不重做美学/机制。
|
||||
2. 判定舞台尺度:以体验会碰到的边界为准。
|
||||
3. 选定熟悉基底,写清变点。
|
||||
4. 钉「社会结构」:台上有哪些种类/势力/阶层在互动(种类级与格局级,非名片)。
|
||||
5. 钉「世界状况」:灾变、通道、祭祀、资源命脉、禁忌等——须能回答如何影响核心运转;软性日常条件也归此。
|
||||
6. 玩家主要活动区域写细;舞台外「刻意留黑」。
|
||||
7. 类型滤镜仅作体裁/氛围参照;禁止题材套件勾选。
|
||||
8. 填齐正文 + 自评 + 追问,输出 JSON。
|
||||
|
||||
工作姿态——「备案活动区,不写百科」:问玩家会反复待在哪、碰到什么运转部件;舞台外不补全图。
|
||||
|
||||
若程序已发默认问题:禁止重复开场;在首答与依赖上补洞。
|
||||
summary:`舞台骨架 · …`(点题尺度 / 社会结构或世界状况)。
|
||||
材料足够便直接成稿。若两种舞台尺度或运转条件会导致明显不同的世界反馈,把这一处缺口交给 `self_score.gap`;产物正文不放评分、诊断、追问或开放问题。
|
||||
```
|
||||
|
||||
## principles
|
||||
|
||||
```principles
|
||||
1. 舞台服务于体验
|
||||
尺度、格局、社会结构、世界状况都必须能回答:它让哪段已确认体验得以发生或被感觉到?
|
||||
不能从题材标签反推「这类故事通常有完整大陆/完整国别」。
|
||||
|
||||
2. 骨架 ≠ 地图 ≠ 百科;社会与状况同等重要
|
||||
地理只是一类钩子。社会结构(谁在台上、如何分层互动)与世界状况(寒灾、异界出口、活人祭祀、命脉资源等)
|
||||
往往比「哪条河接哪座山」更能决定故事如何运转——必须写,但不能写成物种志/民俗全书。
|
||||
可以是抽象格局(对峙、割据、阶层天井)。不要求接壤比例尺、全史年表。
|
||||
下游要生成多张名片时:共享的台上格局与状况仍写在本步;生成规则只引用,不在规则里重抄世界书。
|
||||
下游需要点名条目时交给「具体实例」;本步给格局级与状况级引用钩子。
|
||||
|
||||
3. 变造基底,再按活动区写细
|
||||
优先路径:大家都有印象的基底世界 → 点明变在哪里 → 玩家主要活动区域写细,并写清会撑故事的社会/状况条目。
|
||||
详细度跟体验走:财阀博弈可写到国家/财团层;校园日常写到系馆与社团层。这是备案详细度,不是跳过本步。
|
||||
|
||||
4. 类型滤镜是叠加在基底上的体裁/氛围参照,不是必选题单
|
||||
可用于澄清「故事以何种体裁被讲述」,从而影响冲突模式与背景气质。
|
||||
禁止:列出大菜单请用户勾选;禁止因选了滤镜就自动塞满该类型常见地理与势力。
|
||||
|
||||
5. 与实现机制的分工
|
||||
机制已钉既成变造法则时:本步把法则落到主要活动区域上长什么样,不重钉「为何存在」。
|
||||
机制未排入时:仍从体验直接做舞台备案。
|
||||
|
||||
6. 活动区写细,舞台外留黑
|
||||
玩家主要活动区域写细即可停止。舞台外、体验碰不到的部分默认不写。
|
||||
「未展开范围」写明刻意省略,避免下游把留黑当成缺口去补百科。
|
||||
|
||||
7. 正推,禁止否定式路由
|
||||
写「需要跨国资本压迫感 → 舞台升到国家/财团层」,
|
||||
不写「因为是校园文所以不要国际政治」(除非用户明确不要)。
|
||||
|
||||
8. 继承优先于发明
|
||||
基底已蕴含的常识默认成立;只在变点与体验依赖处显式改写。
|
||||
不要为显得严谨而补起源史、伪科学或全套神话谱系,除非它们本身会上台。
|
||||
```
|
||||
|
||||
## probe
|
||||
|
||||
```probe
|
||||
只追问会改变尺度、变造、社会结构、世界状况或台上格局的缺口。每轮 1~2 点。已答清的禁止重问。
|
||||
|
||||
【默认问题已覆盖】尺度、基底+变点、社会结构/世界状况、玩家主要活动区域。首答后按缺口补。
|
||||
|
||||
优先方式:
|
||||
|
||||
1. 尺度对照
|
||||
例:「同是现代都市——更接近『全国财阀与国家势力都在台上』,还是『基本不出这座城/这所学校』?」
|
||||
|
||||
2. 变造落句
|
||||
例:「是否可以写成:现代都市基底,但国家边界弱化到几乎无国别,冲突主要在公司与城市场景里发生?」
|
||||
|
||||
3. 社会与状况核对
|
||||
例:「台上互动方是否就是『人类 + 无攻击欲的丧尸』?还有没有别的智能方会影响你的掌控?」
|
||||
例:「『每年寒灾』是全局运转的硬时钟,还是只在边陲传说?祭祀是真要死人,还是可被你改写的习俗?」
|
||||
|
||||
4. 主要活动区域
|
||||
例:「真正会反复待着玩的,是总部—宴会—监管听证,还是还要经常切到海外子公司现场?这些区要写细。」
|
||||
|
||||
5. 滤镜澄清 / 机制落地(可选)
|
||||
体裁气质会改变冲突模式时才问滤镜;机制已点世界侧切面时,问它在台上长什么样,不重做支撑检验。
|
||||
|
||||
才追问:两种尺度会显著改变后续;变点/社会/状况不清会改核心运转;主要活动区域范围决定要否某类势力;类型标签分不清氛围与硬设定。
|
||||
|
||||
不追问:具体名片(→ 具体实例);完整物种志/民俗志/世界史/全地图;生成/变量/文风;用户已拒绝展开的背景;基底可默认继承的常识。
|
||||
1. 舞台以世界怎样回应玩家为准,不以是否换区、地图大小或题材类型为准。
|
||||
2. 熟悉常识默认继承,只写相对基底的必要变造,以及会反复参与体验的外部事实。
|
||||
3. 核心运转可以是社会力量、阶层关系、周期事件、资源条件、禁忌或日常秩序;只说明它如何作用于玩家和主要舞台。
|
||||
4. 主要舞台可以是空间、机构、社会层或一间房;详细度以能稳定反馈为止。
|
||||
5. 既成硬法则只引用其结果,规则本身交给实现机制;多份或要先审字段的具名人物交给生成规则,单条字段很少的具名内容才交给具体实例。
|
||||
6. 用户未触及的地区、历史、物种、民俗和势力不补全,在留白边界中说明即可。
|
||||
```
|
||||
|
||||
## output
|
||||
@@ -149,129 +51,61 @@ summary:`舞台骨架 · …`(点题尺度 / 社会结构或世界状况)
|
||||
{
|
||||
"schema": "context-fragment.v1",
|
||||
"技能": "舞台骨架",
|
||||
"brief": "一句话:舞台多大 + 社会/状况如何撑故事",
|
||||
"brief": "一句话说明什么外部舞台在支撑核心体验",
|
||||
"mount": ["world-simulator"],
|
||||
"稳变": "stable",
|
||||
"正文": {
|
||||
"依据的体验": [
|
||||
"从依赖产物忠实提取;可附带与舞台相关的机制支点名"
|
||||
],
|
||||
"舞台尺度": {
|
||||
"范围": "地球级 / 国家 / 城市 / 机构内部 / 抽象割据带 / …",
|
||||
"为何如此": "与核心体验的关系",
|
||||
"玩家常活动边界": "反复碰到的空间或社会边界(可抽象)"
|
||||
"舞台基底": {
|
||||
"熟悉基底": "现实或常见类型中可直接继承的最短描述",
|
||||
"必要变造": ["只列会改变世界反馈的差异;无则省略"]
|
||||
},
|
||||
"基底与变造": {
|
||||
"基底": "大家都有印象的原型(现代都市、西方高奇幻常见格局…)",
|
||||
"变点": ["相对基底改了什么;每条宜短、可引用"],
|
||||
"类型滤镜": "可选体裁/氛围参照名;无则空字符串",
|
||||
"默认可继承": "基底下可默认成立、本步不写的常识范围(一句话)"
|
||||
},
|
||||
"社会结构": [
|
||||
"核心运转": [
|
||||
{
|
||||
"名称": "种类/势力/阶层/阵营等格局级称呼——不是具体名片",
|
||||
"性质": "智慧生灵种类|国家/财团|院系|帮派|阵营|阶层|其它",
|
||||
"在舞台上的位置": "玩家如何碰到;相对权力/危险/依赖/互动方式",
|
||||
"服务体验": "它让什么感觉或规则成立",
|
||||
"细化程度": "点到为止|需要下游实例化|本步已够用"
|
||||
"要素": "社会力量、关系、环境条件或世界状况",
|
||||
"如何作用": "它怎样持续影响玩家、选择或后果",
|
||||
"作用范围": "涉及哪些主要舞台;无需区分时省略"
|
||||
}
|
||||
],
|
||||
"世界状况": [
|
||||
"主要舞台": [
|
||||
{
|
||||
"名称": "影响核心运转或台上氛围的状况条目",
|
||||
"性质": "周期性灾变|异界/通道|祭祀习俗|资源命脉|禁忌律法|日常节奏|话语禁忌|其它",
|
||||
"是什么": "最短可引用描述(例:每年寒灾;南极通往怪物世界的出口;村中活人祭祀)",
|
||||
"如何影响运转": "它怎样推动、限制或框住故事与体验;软条件也可写「塑造氛围/日常感」",
|
||||
"节奏或触发": "年周期 / 可往返 / 日常默认…;不明则「未定」",
|
||||
"作用范围": "全局|仅某舞台区",
|
||||
"服务体验": "对应哪段体验或机制支点"
|
||||
"名称": "会反复上台的空间、机构或社会层",
|
||||
"玩家如何接触": "玩家在这里做什么、受到什么反馈",
|
||||
"关键运转": ["仅引用与此处直接相关的核心运转"]
|
||||
}
|
||||
],
|
||||
"关键舞台区": [
|
||||
{
|
||||
"名称": "会反复上台的区/层/格局名",
|
||||
"是什么": "空间、社会层或抽象舞台的最短描述",
|
||||
"为何需要": "服务哪段体验或哪个机制支点",
|
||||
"格局要点": ["与社会结构/世界状况如何交汇——只写台上用得上的"]
|
||||
}
|
||||
],
|
||||
"未展开范围": [
|
||||
"刻意留黑或仅继承常识;禁止下游当缺口补百科"
|
||||
],
|
||||
"覆盖检验": {
|
||||
"关键骨架是否够用": "是|否|部分",
|
||||
"说明": "玩家会反复碰到的社会与状况是否已有钩子",
|
||||
"宁少勿多说明": "为何到此停止"
|
||||
}
|
||||
},
|
||||
"自评": {
|
||||
"维度": [
|
||||
{
|
||||
"名": "覆盖度",
|
||||
"分数": 8,
|
||||
"说明": "能否覆盖用户已表达的舞台/社会/状况/变造硬要求"
|
||||
},
|
||||
{
|
||||
"名": "充分度",
|
||||
"分数": 8,
|
||||
"说明": "骨架是否够下游引用,并撑起用户想要的体验发生与运转"
|
||||
},
|
||||
{
|
||||
"名": "克制度",
|
||||
"分数": 8,
|
||||
"说明": "是否只写会上台部分;无地图百科、物种志、民俗全书膨胀"
|
||||
}
|
||||
],
|
||||
"薄弱点": "一句话"
|
||||
},
|
||||
"追问": {
|
||||
"导语": "为钉准舞台骨架,还需确认:",
|
||||
"题目": [
|
||||
{
|
||||
"问": "…",
|
||||
"建议选项": ["选项A(可带短场景)", "选项B", "其它(请写明)"],
|
||||
"示例": "可选示范句"
|
||||
}
|
||||
]
|
||||
},
|
||||
"开放问题": []
|
||||
"留白边界": ["本局不会上台、只继承常识或刻意不展开的范围"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
硬规则(公共三段 + 本技正文):
|
||||
1. 合法 JSON;必含 `schema` / `技能` / `brief` / `正文` / `自评` / `追问`(题目可 `[]`)。
|
||||
2. **公共三段**:正文=舞台主体;自评=覆盖度/充分度/克制度(**分数 0–10 十分制**);追问=导语+建议选项。
|
||||
3. **本技正文**须含:依据体验、舞台尺度、基底与变造、**社会结构**、**世界状况**、关键舞台区、未展开范围、覆盖检验。
|
||||
4. 社会结构=谁在台上互动(种类/势力/阶层,格局级);世界状况=世界如何运转(灾变/通道/习俗等硬部件 + 日常软条件)。无则空数组,但须在覆盖检验说明「为何无需」。
|
||||
5. `mount` 默认主世界层;**禁止**写 `order`。`稳变` 多为 `stable`。
|
||||
6. 不要输出具体实例名片、物种大全、民俗全书、生成规则、变量表、执行单元列表。
|
||||
7. summary:`舞台骨架 · {brief 缩略}`。
|
||||
硬规则:
|
||||
1. 输出合法 JSON;未知或无内容的可选字段直接省略,不写“未定”占位。
|
||||
2. `核心运转` 统一容纳必要的社会结构与世界状况,不为分类而补内容。
|
||||
3. `主要舞台` 只写会反复上台且需要稳定反馈的部分;单一房间也可成为完整舞台。
|
||||
4. 不在产物正文写自评、追问、开放问题、覆盖检验或设定诊断。
|
||||
5. `mount` 固定为 `["world-simulator"]`。
|
||||
|
||||
## score
|
||||
|
||||
```score
|
||||
支撑度:
|
||||
- 5:核心体验依赖的舞台、关系和运转条件均足以支持稳定世界反馈。
|
||||
- 3:方向正确,但缺少一项会反复影响体验的环境或社会条件,或其作用尚不明确。
|
||||
- 1:只有题材或地点标签,无法据此判断世界如何回应玩家。
|
||||
|
||||
克制度:
|
||||
- 5:只写会上台且反复有用的内容,熟悉常识默认继承,其余明确留白。
|
||||
- 3:夹带少量暂时不会影响游玩的地图、历史或设定。
|
||||
- 1:写成完整世界百科,核心舞台反而不突出。
|
||||
```
|
||||
|
||||
## checklist
|
||||
|
||||
```checklist
|
||||
- [ ] 含 schema / brief / 正文 / 自评 / 追问?
|
||||
- [ ] 自评为覆盖度 / 充分度 / 克制度?
|
||||
- [ ] 尺度由体验决定?基底+变造可引用?
|
||||
- [ ] 社会结构与世界状况已写(或空数组且说明无需)?硬状况能说清如何影响运转?
|
||||
- [ ] 玩家主要活动区域已写细,且有未展开范围?
|
||||
- [ ] 未写成地图百科、物种志、民俗全书、类型勾选菜单、具体名片?
|
||||
- [ ] 追问只打尺度/社会/状况/变造缺口,带建议选项?
|
||||
```
|
||||
|
||||
## examples
|
||||
|
||||
```examples
|
||||
好:
|
||||
- 体验「普通大学生活」→ 仍要备案;舞台=校园几系(活动区写细);世界状况可轻;未展开=国家政治。
|
||||
- 体验「垄断粮食的末日支配」→ 社会结构=人类 + 无攻击欲丧尸 + 楼内依赖关系;世界状况=外出采集命脉;舞台区=楼内安全区 / 楼外采集区。
|
||||
- 世界状况示例:每年寒灾;南极通往怪物世界的出口;村中活人祭祀。
|
||||
- 抽象:「魔族与人族对峙割据」——社会结构两种阵营;不画大陆全图。
|
||||
|
||||
坏:
|
||||
- 只写山脉河流,不写祭祀/灾变/异界口等用户已点名的运转部件。
|
||||
- 「先写完整七大国、五十种魔族生态、三万年神话…」→ 百科。
|
||||
- 「地点1:XX咖啡馆,店主叫…」→ 具体实例,不是骨架。
|
||||
- 把多张名片共享的台上格局丢给生成规则去写「共通前提」→ 本步该写格局钩子,规则只引用。
|
||||
- 整份只有散文无三段外壳。
|
||||
- [ ] 舞台内容足以让主世界层稳定回应玩家?
|
||||
- [ ] 判断依据是外部反馈,而不是换区数量或地图大小?
|
||||
- [ ] 核心运转只含会反复作用的关系与状况?
|
||||
- [ ] 主要舞台详细度够用即止,未触及范围已留白?
|
||||
- [ ] 未重写体验、硬法则、人物名片、完整历史或百科?
|
||||
- [ ] 产物正文不含评分、诊断、追问和开放问题?
|
||||
```
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
# 0 层开场白与初态
|
||||
|
||||
## meta
|
||||
|
||||
```meta
|
||||
name: 0层开场白与初态
|
||||
id: zero-layer-opening-setup
|
||||
artifact: 设计.0层开场白与初态
|
||||
declaration: >
|
||||
0 层创作终节点,且可增殖:每条开场是一个节点,落成 `设计.0层开场白与初态#场景短码`。
|
||||
每条自带持久卡第一屏、槽位初值和同真相运行初态。默认 1 条;还要入口就再增殖。
|
||||
游玩时左右切换,选中的一条作为 0 层。
|
||||
when: |
|
||||
0层回复呈现、0层正文组成和其它内容能力均已完成,程序也已组装运行配置,需要创建持久卡第一屏时使用。
|
||||
when_not: |
|
||||
本局使用普通消息楼层 → 开场白与开场变量。
|
||||
槽位或持久外壳未定 → 先 0层回复呈现、0层正文组成。
|
||||
`设计.worker集` 尚未由程序组装 → 先运行 prepare_opening 收口工具链。
|
||||
boundary: |
|
||||
本能力只写候选第一屏、卡槽初值与同真相变量/拓扑初态;不重写外壳和更新合同。
|
||||
开场不依赖随机数,不填写任何随机数行的具体值。
|
||||
feeds: gm,narrator
|
||||
```
|
||||
|
||||
## opening
|
||||
|
||||
```opening
|
||||
用持久卡的第一屏建立第一拍:玩家进场时在哪、哪些槽位一开始有内容、结束后显然能做什么。默认一条。还要别的入口时,每条再增殖一个开场节点;进游玩后左右切换,选中的一条作为 0 层。
|
||||
```
|
||||
|
||||
## task
|
||||
|
||||
```task
|
||||
读取程序已组装的 `设计.worker集`、`设计.0层回复呈现`、`设计.0层正文组成` 和其它最终层产物,写入 `设计.0层开场白与初态`。
|
||||
|
||||
默认生成 1 条。用户要多个入口时,每条再增殖一份产物,写入 `设计.0层开场白与初态#场景短码`;改某一条则覆盖同一完整 tag。不要把多条入口收成聊天里的选择题。每条候选必须携带一套不可混用的初态:
|
||||
- 稳定候选 id 与一句区分。
|
||||
- 可选用户角色名片。
|
||||
- `zero-layer.card.v1` 初始卡:revision=0,包含所有可见槽位的初值或合同允许的空态。
|
||||
- 与第一屏同真相的变量初值。
|
||||
- 若有拓扑:当前位置、焦点和已揭示范围。
|
||||
|
||||
卡内模型生成槽位由本步填写;程序投影槽位只声明初始绑定,由程序从变量和拓扑初态灌入。正文槽的可见内容至少出现一次字面 `@玩家`。开场不依赖随机数,因此不输出任何 `名称.随机数` 的具体数组。
|
||||
|
||||
全部候选保存。游玩时左右切换,选中的一条作为 0 层:程序整套替换用户角色、卡状态、变量和拓扑初态,之后每轮只应用 `zero-layer.update.v1`,不新增可见助手楼层。
|
||||
|
||||
材料足够便直接成稿;会改变第一屏成立的缺口交给 `self_score.gap`。产物正文不放评分、诊断、追问或开放问题。
|
||||
```
|
||||
|
||||
## principles
|
||||
|
||||
```principles
|
||||
1. 第一屏、卡槽、变量和拓扑初态必须是同一个事实版本。
|
||||
2. 默认一条。多条是不同入口,每条一个可增殖节点,各自一整套初态,不共享半套状态。
|
||||
3. 所有可见槽位都必须有初值、允许的空态或程序投影绑定。
|
||||
4. 只填模型生成槽;状态、地图和表盘从权威初态投影。
|
||||
5. 用户角色没有名片时仍使用 `@玩家`;有名片时也不把姓名硬写进占位位置。
|
||||
6. 不写 HTML/CSS/JS,不复制固定外壳,不在开场里模拟后续 slot_updates。
|
||||
7. 本步结束后创作收口;后续变化进入持久卡更新协议。
|
||||
```
|
||||
|
||||
## probe
|
||||
|
||||
```probe
|
||||
每轮 1~2 点。优先确认第一拍、初始活动视图、模型槽初值、某项权威状态的开场值,以及多入口时整套初态差异。
|
||||
```
|
||||
|
||||
## output
|
||||
|
||||
```output
|
||||
{
|
||||
"schema": "context-fragment.v1",
|
||||
"技能": "0层开场白与初态",
|
||||
"brief": "一句话说明默认第一屏与候选数量",
|
||||
"mount": ["world-simulator", "narrator"],
|
||||
"稳变": "stable",
|
||||
"正文": {
|
||||
"默认候选id": "zero-opening-1",
|
||||
"候选": [
|
||||
{
|
||||
"id": "zero-opening-1",
|
||||
"区分": "默认;多条时说明入口差异",
|
||||
"用户角色": {
|
||||
"名字": "可选;无则省略整段",
|
||||
"简介": "可选"
|
||||
},
|
||||
"初始卡": {
|
||||
"schema": "zero-layer.card.v1",
|
||||
"card_id": "本局稳定卡 id",
|
||||
"revision": 0,
|
||||
"active_view": "初始主工作面或 Tab id",
|
||||
"slots": {
|
||||
"body": {
|
||||
"mode": "replace|append",
|
||||
"content": "包含字面 @玩家 的第一拍"
|
||||
},
|
||||
"其它模型生成槽位": {
|
||||
"mode": "与0层回复呈现一致",
|
||||
"content": "初值或允许的空态"
|
||||
},
|
||||
"程序投影槽位": {
|
||||
"mode": "data",
|
||||
"projection_ref": "变量、拓扑或备忘录的稳定引用"
|
||||
}
|
||||
}
|
||||
},
|
||||
"变量初态": [
|
||||
{
|
||||
"名": "与变量设计真值名一致",
|
||||
"值": "开场值",
|
||||
"依据": "第一屏事实"
|
||||
}
|
||||
],
|
||||
"拓扑初态": [
|
||||
{
|
||||
"topology_id": "有拓扑时填写",
|
||||
"当前位置": "节点 id",
|
||||
"焦点": "可选节点或连接 id",
|
||||
"已揭示": ["节点或连接 id"]
|
||||
}
|
||||
],
|
||||
"用户可行动空间": "第一屏结束后显然能尝试什么"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
硬规则:
|
||||
1. 输出合法 JSON;候选默认 1 条,仅用户明确要求时 2~3 条。
|
||||
2. `默认候选id` 必须指向候选之一;每条都是完整独立初态。
|
||||
3. 初始卡 schema 固定为 `zero-layer.card.v1`,revision 固定为 0。
|
||||
4. 初始卡覆盖全部可见槽位;模型生成槽给初值或空态,程序投影槽给稳定 `projection_ref`。
|
||||
5. 每条正文可见内容包含字面 `@玩家`;不写死用户角色姓名替代占位。
|
||||
6. 不输出随机数具体值,不填后续 slot_updates,不复制前端代码。
|
||||
7. 变量、拓扑与第一屏同真相;切换候选时整套替换。
|
||||
8. 产物正文不含自评、追问或开放问题。
|
||||
9. `mount` 固定为 `["world-simulator", "narrator"]`。
|
||||
|
||||
## score
|
||||
|
||||
```score
|
||||
第一屏可玩:
|
||||
- 5:第一拍、当前信息和行动空间清楚,短到愿意立即开玩。
|
||||
- 3:可以开玩,但当前目标或下一步略含糊。
|
||||
- 1:像长篇序章,或看不出玩家能做什么。
|
||||
|
||||
卡合同符合:
|
||||
- 5:全部可见槽位都有正确初值、空态或程序投影绑定,模式与0层合同一致。
|
||||
- 3:主体符合,一个辅助槽位初值含糊。
|
||||
- 1:漏槽、模式冲突或直接复制整页 HTML。
|
||||
|
||||
状态自洽:
|
||||
- 5:第一屏、变量、拓扑和程序投影引用完全同真相。
|
||||
- 3:主体一致,但一项次要初态依据不足。
|
||||
- 1:叙述与初态冲突或程序槽由模型编造。
|
||||
|
||||
持久起点:
|
||||
- 5:card_id 稳定、revision=0、初始视图明确,后续可直接应用 slot_updates。
|
||||
- 3:可初始化,但初始视图或某槽持久语义略含糊。
|
||||
- 1:仍按普通消息设计,无法形成持久卡。
|
||||
|
||||
候选切换:
|
||||
- 5:默认明确;多条时每条入口与整套初态差异成立。
|
||||
- 3:可切换,但多条差异偏弱。
|
||||
- 1:候选共享冲突状态,或未要求却堆重复入口。
|
||||
```
|
||||
|
||||
## checklist
|
||||
|
||||
```checklist
|
||||
- [ ] 默认一条,默认 id 有效?
|
||||
- [ ] 每条卡覆盖全部可见槽位,revision=0?
|
||||
- [ ] 第一屏含 @玩家,变量与拓扑初态同真相?
|
||||
- [ ] 程序投影槽只写 projection_ref,未编造权威数据?
|
||||
- [ ] 未写随机数、前端代码或后续 slot_updates?
|
||||
```
|
||||
|
||||
## examples
|
||||
|
||||
```examples
|
||||
- 终端操作师:初始活动视图为公司指令,正文写 @玩家 登入后的第一项任务;表盘和地图只给程序投影引用。
|
||||
- 固定学校:正文建立第一拍,地图槽绑定学校拓扑,线索槽使用允许的空态。
|
||||
- 不合适:把后续每轮更新预写进开场;输出随机数组;每个候选只换正文却共用冲突变量。
|
||||
```
|
||||
@@ -0,0 +1,212 @@
|
||||
# 0 层正文组成
|
||||
|
||||
## meta
|
||||
|
||||
```meta
|
||||
name: 0层正文组成
|
||||
id: zero-layer-reply-format
|
||||
artifact: 设计.0层正文组成
|
||||
declaration: >
|
||||
把 0 层语义槽位落成一张跨轮持久前端:定义固定外壳、槽位位置、Tab 与 slot_updates 渲染合同。
|
||||
when: |
|
||||
已有 0层回复呈现,需要决定持久卡如何显示及怎样原地应用槽位更新时使用。
|
||||
when_not: |
|
||||
本局使用普通消息楼层 → 正文组成。
|
||||
还不知道卡内有哪些语义槽位 → 先 0层回复呈现。
|
||||
boundary: |
|
||||
0层回复呈现决定槽位内容和更新语义;本能力决定固定外壳、位置、渲染与更新应用。
|
||||
权威状态由程序投影;本能力只消费数据,不让正文模型重复生成。
|
||||
feeds: gm,narrator
|
||||
```
|
||||
|
||||
## opening
|
||||
|
||||
```opening
|
||||
把已启用槽位做成一张持久卡。先确定主工作面、Tab 或辅助区关系,再为 replace、append、data 三种更新写稳定渲染入口。
|
||||
```
|
||||
|
||||
## task
|
||||
|
||||
```task
|
||||
为 0 层模式生成持久前端,写入 `设计.0层正文组成`。开场只创建一次外壳;后续助手回合不新增可见气泡,而是提交 `zero-layer.update.v1` 更新现有槽位。
|
||||
|
||||
执行顺序:
|
||||
1. 读取 0层回复呈现,只处理实际启用的可见槽位。
|
||||
2. 从【壳参考库】选一只与本局已启用槽位最接近的壳。按该壳备注改写样本里的名称、栏目、配色和内部小节。本局没有的区从样本骨架删除并重排,不留空位。槽位更新方式仍以 0层回复呈现为准。
|
||||
3. 为每个槽位定义 DOM 落点、显示名、渲染器和空态。Tab、折叠、缩放只做本地查看。
|
||||
4. 定义 `zero-layer.card.v1` 初始状态和 `zero-layer.update.v1` 更新合同:
|
||||
- `replace`:替换目标槽当前内容。
|
||||
- `append`:按顺序追加记录,由程序执行上限或压缩规则。
|
||||
- `data`:对程序权威数据应用结构化投影,不接收模型伪造的完整状态。
|
||||
5. 隐藏维护通道不创建 DOM。模型只提交允许它生成的槽位;状态和拓扑更新由程序投影。
|
||||
6. 写完整 HTML/CSS/JS,创作时一次生成并固定。美化用配色、字体层级、边框、间距,以及 CSS 或简单内联 SVG,按本次能写稳的程度完成。JS 使用宿主提供的 `zeroLayer.onInit(renderCard)` 与 `zeroLayer.onPatch(applyPatch)`;不得依赖正则替换。地图 SVG 只从结构化数据绘制。
|
||||
7. 给一份非正史初始卡和一次更新预览,覆盖所有可见槽位与空态。
|
||||
|
||||
HTML、CSS、JS 合计不超过 60 KB。材料足够便直接成稿;会改变主工作面的缺口交给 `self_score.gap`。产物正文不放评分、诊断、追问或开放问题。
|
||||
```
|
||||
|
||||
## principles
|
||||
|
||||
```principles
|
||||
1. 固定外壳只创建一次;每轮只更新目标槽位,不重发整页代码。
|
||||
2. 正文或核心工作面保持最高阅读优先级,辅助 Tab 不平均争夺注意力。
|
||||
3. 更新模式来自 0层回复呈现,前端不能擅自把 replace 改成 append。
|
||||
4. 程序投影槽只消费权威数据;正文模型不输出状态副本、地图 SVG 或完整卡快照。
|
||||
5. 每次更新带目标槽位和模式;程序校验允许列表与 revision 后原子提交。
|
||||
6. 动态文字安全渲染;Markdown 使用宿主安全能力,不能拼接未清洗 HTML。
|
||||
7. 允许本地 Tab、折叠、缩放和 SVG 图谱;禁止联网、表单、弹窗、顶层导航和宿主行动回传。
|
||||
8. 正规路径使用结构化 slot_updates;正则只属于外部兼容适配器,不写进本产物。
|
||||
9. 美化落在配色、字体层级、边框、间距和简单内联 SVG 上,写进 CSS,按本次能写稳的程度完成。
|
||||
```
|
||||
|
||||
## probe
|
||||
|
||||
```probe
|
||||
每轮 1~2 点。优先确认主工作面、哪些槽位放入 Tab、正文是替换还是卡内日志,以及结构化地图需要哪种可读降级。
|
||||
```
|
||||
|
||||
## output
|
||||
|
||||
```output
|
||||
{
|
||||
"schema": "context-fragment.v1",
|
||||
"技能": "0层正文组成",
|
||||
"brief": "一句话说明持久卡的主工作面、辅助导航与更新方式",
|
||||
"mount": ["world-simulator", "narrator"],
|
||||
"稳变": "stable",
|
||||
"正文": {
|
||||
"参考壳": [
|
||||
{
|
||||
"id": "壳参考库方法 id",
|
||||
"采用": "借用的布局或局部交互方法",
|
||||
"本局化": "怎样适配本局槽位"
|
||||
}
|
||||
],
|
||||
"阅读结构": {
|
||||
"主工作面": "body 或本局核心槽位",
|
||||
"导航": "无|tabs|本局描述",
|
||||
"响应式": "窄屏与宽屏如何保持主工作面优先"
|
||||
},
|
||||
"卡合同": {
|
||||
"schema": "zero-layer.card.v1",
|
||||
"字段": {
|
||||
"card_id": "稳定卡 id",
|
||||
"revision": "从 0 开始的程序 revision",
|
||||
"active_view": "当前本地视图;不写入世界真相",
|
||||
"slots": "以槽位 id 为键的持久状态"
|
||||
}
|
||||
},
|
||||
"更新合同": {
|
||||
"schema": "zero-layer.update.v1",
|
||||
"字段": {
|
||||
"base_revision": "更新所依据的 revision",
|
||||
"slot_updates": [
|
||||
{
|
||||
"slot_id": "目标槽位 id",
|
||||
"mode": "replace|append|data",
|
||||
"content": "replace/append 使用;类型与槽位一致",
|
||||
"data": "data 使用;由程序权威投影提供"
|
||||
}
|
||||
]
|
||||
},
|
||||
"提交": "程序校验槽位、模式、来源和 revision 后原子应用并持久化"
|
||||
},
|
||||
"槽位落点": [
|
||||
{
|
||||
"slot_id": "与0层回复呈现一致",
|
||||
"位置": "DOM 区域或 Tab id",
|
||||
"显示名": "需要标题时填写",
|
||||
"渲染": "safe_markdown|text|kv|list|table|topology_svg",
|
||||
"允许模式": "replace|append|data",
|
||||
"空态": "首次无内容时显示什么",
|
||||
"降级": "结构化渲染失败时的可读形式"
|
||||
}
|
||||
],
|
||||
"不渲染": [
|
||||
{
|
||||
"id": "隐藏维护通道 id;无则省略",
|
||||
"交给": "运行时维护解析器"
|
||||
}
|
||||
],
|
||||
"frontend": {
|
||||
"html": "<div id=\"zero-layer-app\">完整稳定外壳</div>",
|
||||
"css": "完整样式与响应式规则",
|
||||
"js": "使用 zeroLayer.onInit(renderCard) 与 zeroLayer.onPatch(applyPatch) 的完整代码"
|
||||
},
|
||||
"预览": {
|
||||
"initial_card": {
|
||||
"schema": "zero-layer.card.v1",
|
||||
"card_id": "preview",
|
||||
"revision": 0,
|
||||
"active_view": "main",
|
||||
"slots": {}
|
||||
},
|
||||
"one_update": {
|
||||
"schema": "zero-layer.update.v1",
|
||||
"base_revision": 0,
|
||||
"slot_updates": []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
硬规则:
|
||||
1. 输出合法 JSON;槽位落点覆盖 0层回复呈现的全部可见槽位,id 与更新模式一致。
|
||||
2. `frontend.html/css/js` 完整且总量不超过 60 KB;JS 必须实现初始化与增量更新入口。
|
||||
3. 后续回合只提交 `zero-layer.update.v1`,不重发 HTML 或完整卡。
|
||||
4. `data` 槽只接受程序权威投影;隐藏维护通道不进入 DOM。
|
||||
5. 更新必须带 `base_revision`,由程序校验后原子提交;失败时保持旧卡。
|
||||
6. 禁止正则作为正规更新协议;禁止网络、表单、弹窗、顶层导航和行动回传。
|
||||
7. topology 读取结构化节点与连接绘制 SVG,并提供列表降级。
|
||||
8. 预览是非正史验收数据,不写成开场。
|
||||
9. 产物正文不含自评、追问或开放问题。
|
||||
10. `mount` 固定为 `["world-simulator", "narrator"]`。
|
||||
|
||||
## score
|
||||
|
||||
```score
|
||||
槽位落实:
|
||||
- 5:每个可见槽位都有落点、允许模式、渲染器、空态和降级。
|
||||
- 3:主体完整,但一个辅助槽位的空态或降级含糊。
|
||||
- 1:漏槽或模式与语义合同冲突。
|
||||
|
||||
持久更新:
|
||||
- 5:初始化、revision、原子 slot_updates 与失败保持旧值形成闭环。
|
||||
- 3:可更新,但 revision 或失败处理有一处不清。
|
||||
- 1:仍要求每轮重发整张页面或依赖文本替换。
|
||||
|
||||
权威绑定:
|
||||
- 5:模型槽与程序投影槽边界清楚,状态和拓扑没有双写。
|
||||
- 3:主要正确,但一个数据槽来源含糊。
|
||||
- 1:模型每轮重编权威状态。
|
||||
|
||||
界面结构:
|
||||
- 5:主工作面明确,Tab 和辅助区服务本局体验,窄屏仍可用。
|
||||
- 3:总体可读,但一个辅助区抢占注意力。
|
||||
- 1:机械地堆满 Tab,没有主次。
|
||||
|
||||
实现与安全:
|
||||
- 5:代码完整、安全渲染、无联网与回传,结构化区域有降级。
|
||||
- 3:主体可运行,但一处安全或降级细节不完整。
|
||||
- 1:只有外观描述、空代码或不安全 HTML 拼接。
|
||||
```
|
||||
|
||||
## checklist
|
||||
|
||||
```checklist
|
||||
- [ ] 所有可见槽位都有落点,模式与语义合同一致?
|
||||
- [ ] 外壳只初始化一次,后续只应用 slot_updates?
|
||||
- [ ] revision、原子提交与失败保持旧值完整?
|
||||
- [ ] 权威状态没有被模型重复生成,隐藏维护区未进 DOM?
|
||||
- [ ] 前端完整、安全、无正则依赖与外部联网?
|
||||
- [ ] 美化已用配色、字体层级、边框、间距或简单 SVG 写进 CSS,程度限于本次能写稳的部分?
|
||||
```
|
||||
|
||||
## examples
|
||||
|
||||
```examples
|
||||
- 机械终端:正文为主工作面;公司指令、监控表盘、地图分 Tab;指令 replace、日志 append、表盘和地图 data。
|
||||
- 学校舞台:正文 replace;地图 data;已知线索 append 并有上限;切 Tab 只改变本地视图。
|
||||
- 不合适:让模型每轮输出完整 HTML;用正则替换 `<公司指令>`;把按钮点击直接回传为角色行动。
|
||||
```
|
||||
@@ -0,0 +1,189 @@
|
||||
# 0 层回复呈现
|
||||
|
||||
## meta
|
||||
|
||||
```meta
|
||||
name: 0层回复呈现
|
||||
id: zero-layer-status
|
||||
artifact: 设计.0层回复呈现
|
||||
declaration: >
|
||||
定义持久 0 层卡实际启用的语义槽位、更新触发、更新方式、权威来源与内容要求;不决定 HTML 和排版。
|
||||
when: |
|
||||
本局选择 0 层模式,开场卡会在后续回合原地更新,而不是持续增加新的可见助手楼层时使用。
|
||||
when_not: |
|
||||
本局使用普通消息楼层 → 回复呈现。
|
||||
只讨论 HTML、Tab、布局或具体渲染 → 0层正文组成。
|
||||
boundary: |
|
||||
本能力决定持久卡里有什么语义槽位,以及每轮怎样更新。
|
||||
0层正文组成负责固定外壳、槽位位置和渲染;变量与拓扑仍由其权威产物维护。
|
||||
feeds: gm,narrator
|
||||
```
|
||||
|
||||
## opening
|
||||
|
||||
```opening
|
||||
这张持久卡需要保留哪些区域?逐项说明它在什么情况下更新、是替换当前内容、追加记录,还是由程序更新结构化数据。
|
||||
```
|
||||
|
||||
## task
|
||||
|
||||
```task
|
||||
为 0 层模式定义持久卡语义合同,写入 `设计.0层回复呈现`。只列本局实际启用的槽位;未启用的槽位直接省略。
|
||||
|
||||
每个可见槽位写清:
|
||||
- 用途、内容类型和用户能看到什么。
|
||||
- 更新触发:每轮,或可由程序判断的条件。
|
||||
- 更新方式只选一种:`replace` 替换当前内容、`append` 追加有界记录、`data` 更新结构化数据。
|
||||
- 来源是模型生成还是程序权威投影,并引用稳定上游。
|
||||
- 没有新内容时保持原值、显示空态还是清空。
|
||||
|
||||
正文同样是槽位。它可以替换为本轮正文,也可以追加到卡内日志,但不能同时使用两种语义。`append` 必须给记录上限或压缩规则,避免 0 层卡无限增长。
|
||||
|
||||
变量、拓扑和备忘录使用 `program_projection`,正文模型不重复生成。简单变量需要内联维护时,另列用户不可见的维护通道;程序优先使用 tool call,工具不可用时使用结构化 `state_ops`。副 Worker 维护不建立主回复维护通道。
|
||||
|
||||
材料足够便直接成稿;真正改变槽位合同的缺口交给 `self_score.gap`。产物正文不放评分、诊断、追问或开放问题。
|
||||
```
|
||||
|
||||
## principles
|
||||
|
||||
```principles
|
||||
1. 0 层是同一张卡持续更新,不是把普通回复列表藏起来。
|
||||
2. 一个槽位只选一种更新方式;需要不同语义时拆成两个有明确用途的槽位。
|
||||
3. `replace` 保留最新值;`append` 保留有界记录;`data` 由程序按结构化字段更新。
|
||||
4. 程序已有的状态、拓扑和计算结果只投影,不让正文模型复述。
|
||||
5. 槽位更新条件必须可判断;没有更新时默认保持原值,不用空内容覆盖。
|
||||
6. 隐藏维护通道不是可见槽位,不进入 DOM。
|
||||
7. 本能力只定义语义,不写 HTML、CSS、JS、Tab、SVG 或正则。
|
||||
8. 每个可见槽位属于一种:抬头、正文、小故事、数据卡、结构、文字选项。抬头是页首一行短内容。隐藏维护通道不占这些区。
|
||||
```
|
||||
|
||||
## probe
|
||||
|
||||
```probe
|
||||
每轮 1~2 点。优先确认哪些内容必须持久可见、正文是替换还是追加、追加内容保留多久,以及某槽位应由模型还是程序更新。
|
||||
```
|
||||
|
||||
## output
|
||||
|
||||
```output
|
||||
{
|
||||
"schema": "context-fragment.v1",
|
||||
"技能": "0层回复呈现",
|
||||
"brief": "一句话说明持久卡有哪些槽位及主要更新方式",
|
||||
"mount": ["world-simulator", "narrator"],
|
||||
"稳变": "stable",
|
||||
"正文": {
|
||||
"模式": "zero_layer",
|
||||
"语义槽位": [
|
||||
{
|
||||
"id": "body",
|
||||
"名称": "正文",
|
||||
"用途": "承载当前场面或卡内叙事记录",
|
||||
"用户可见": true,
|
||||
"内容类型": "markdown",
|
||||
"区域种类": "正文",
|
||||
"更新": {
|
||||
"方式": "replace|append",
|
||||
"触发": "every_turn",
|
||||
"无新内容": "keep",
|
||||
"记录上限": "仅 append 时填写条数或压缩规则"
|
||||
},
|
||||
"来源": {
|
||||
"方式": "model_generated",
|
||||
"ref": "设计.故事推进"
|
||||
},
|
||||
"内容要求": [
|
||||
"遵守故事推进与叙事指南",
|
||||
"只生成该槽位本次需要新增或替换的内容"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "本局实际启用的其它槽位 id",
|
||||
"名称": "公司指令、监控表盘、地图、关系或其它本局名称",
|
||||
"用途": "该槽位持续帮助用户看到什么",
|
||||
"用户可见": true,
|
||||
"内容类型": "text|markdown|kv|list|table|topology",
|
||||
"区域种类": "小故事|数据卡|结构|文字选项",
|
||||
"更新": {
|
||||
"方式": "replace|append|data",
|
||||
"触发": "every_turn|conditional",
|
||||
"条件": "conditional 时填写可判断条件",
|
||||
"无新内容": "keep|empty_state|clear",
|
||||
"记录上限": "仅 append 时填写"
|
||||
},
|
||||
"来源": {
|
||||
"方式": "model_generated|program_projection",
|
||||
"ref": "稳定上游或运行状态引用",
|
||||
"字段": ["程序投影时只选择实际展示字段"]
|
||||
},
|
||||
"内容要求": ["正向说明这个槽位每次应提供什么"]
|
||||
}
|
||||
],
|
||||
"隐藏维护通道": [
|
||||
{
|
||||
"id": "简单变量内联维护时填写;否则省略",
|
||||
"内容类型": "state_ops",
|
||||
"投递": "runtime_selects_tool_or_structured",
|
||||
"用户可见": false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
硬规则:
|
||||
1. 输出合法 JSON;只列实际启用槽位,至少包含 `body`。
|
||||
2. 每个槽位必须有稳定 id、区域种类、更新方式、触发、无新内容处理、来源和内容要求。可见槽位的区域种类只能是抬头、正文、小故事、数据卡、结构、文字选项。
|
||||
3. `append` 必须有记录上限或压缩规则;`data` 必须来自程序权威投影。
|
||||
4. 没有更新时保持原值;除非合同明确要求,不用空值清槽。
|
||||
5. 隐藏维护通道用户不可见,不属于语义槽位。
|
||||
6. 不写 HTML、CSS、JS、Tab、SVG、正则或槽位选择器。
|
||||
7. 产物正文不含自评、追问或开放问题。
|
||||
8. `mount` 固定为 `["world-simulator", "narrator"]`。
|
||||
|
||||
## score
|
||||
|
||||
```score
|
||||
槽位必要性:
|
||||
- 5:每个槽位都服务明确体验或运行需要,没有为了像应用而堆空 Tab。
|
||||
- 3:主体必要,但有一个槽位可以合并或价值偏弱。
|
||||
- 1:大量槽位没有稳定用途。
|
||||
|
||||
更新语义:
|
||||
- 5:每个槽位的 replace、append 或 data 选择准确,触发和无更新处理完整。
|
||||
- 3:主体清楚,但有一个槽位的更新方式或触发略含糊。
|
||||
- 1:只说“刷新”,无法判断怎样修改持久内容。
|
||||
|
||||
权威来源:
|
||||
- 5:模型内容与程序投影边界清楚,变量和拓扑没有被重复生成。
|
||||
- 3:主要来源正确,但有一个字段存在双写风险。
|
||||
- 1:权威状态由正文模型每轮重编。
|
||||
|
||||
长期负担:
|
||||
- 5:追加槽有上限或压缩规则,未更新槽保持原值,长期运行不会无限膨胀。
|
||||
- 3:整体可控,但一个追加区缺少明确上限。
|
||||
- 1:所有历史都无限追加或每轮重写整张卡。
|
||||
|
||||
职责边界:
|
||||
- 5:只定义语义槽位,把渲染、数据维护和篇幅交给正确节点。
|
||||
- 3:主体正确,夹有少量布局描述。
|
||||
- 1:在本步直接写前端代码或变量公式。
|
||||
```
|
||||
|
||||
## checklist
|
||||
|
||||
```checklist
|
||||
- [ ] 只列实际启用槽位,且包含 body?
|
||||
- [ ] 每个槽位只选一种更新方式,触发与无更新处理完整?
|
||||
- [ ] append 有上限,data 有权威来源?
|
||||
- [ ] 隐藏维护通道未混入可见槽位?
|
||||
- [ ] 未写前端、正则或重复权威数据?
|
||||
```
|
||||
|
||||
## examples
|
||||
|
||||
```examples
|
||||
- 终端体验:正文 replace;公司指令 replace;操作日志 append 且只保留最近若干条;监控表盘 data。
|
||||
- 固定学校:正文 replace;地图 data,读取权威拓扑;线索板 append 并按章节压缩。
|
||||
- 不合适:每轮输出整张地图文本并覆盖权威图;所有槽位都 append 且没有上限。
|
||||
```
|
||||
@@ -13,7 +13,7 @@ core: >-
|
||||
process:
|
||||
- 开始通常先做「美学纲领与交互范式」。
|
||||
- 对照【能力 · 可选工序】的「何时用 / 何时不用」选型;调用条件以技能 meta 为准。
|
||||
- 收成前若不能吃写手默认槽(大纲+章节),再排「游玩拓扑」勾选 →「细化终稿」并 closed。
|
||||
- 收成时由程序根据已确认需求整理「游玩执行方式」,再用「运行配置组装」生成可执行规格并 closed。
|
||||
- 舞台、机制、变量、监控栏等:仅当对应技能「何时用」成立时再选,勿套世界模拟全套。
|
||||
|
||||
principles:
|
||||
|
||||
@@ -8,23 +8,23 @@ when: 回合互动、可扩展的世界推进、角色扮演、沉浸推演类
|
||||
core: >-
|
||||
先钉清用户如何参与、正文如何呈现、核心体验与禁忌(美学卡上用户会在意的部分);
|
||||
再按那些承诺选出要兑现的上下文技能,并标明挂哪些固定槽(默认:只开主世界层,直接交可见原文;转述可选);
|
||||
挂载归属可早定,插入顺序必须晚定——收成前用「上下文投影排序」按各块概括排出数字序与双锚点投影;
|
||||
再细化终稿收成运行规格。执行单元只用固定槽(主世界层必开 / 叙事转述默认关 / 可选角色视角;旁观按变量需要),不为表/百科再拆槽,不开机遇裁定槽。
|
||||
挂载归属可早定,插入顺序由程序在落档时按变化频率与重要性自动整理;
|
||||
再由「运行配置组装」生成可执行规格。执行单元只用固定槽(主世界层必开 / 叙事转述默认关 / 可选角色视角;旁观按变量需要),不为表/百科再拆槽,不开机遇裁定槽。
|
||||
|
||||
process:
|
||||
- 开始通常先做「美学纲领与交互范式」。
|
||||
- 读美学卡上用户会在意的部分,对照【能力 · 可选工序】的何时用 / 何时不用,把兑现那些承诺所需的节点写进 steps(需要几步排几步)。各技能调用条件以 meta 为准,本表不重复。
|
||||
- 世界模拟默认槽:仅主世界层(转述关、旁观关、视角关)。能吃默认则不必排「游玩拓扑」。要独立文风层再开转述;有变量/表再开旁观。
|
||||
- 中段按承诺写上下文;每块只标挂谁(mount)与稳/变,order 留给「上下文投影排序」。挂载归属:世界/机制/生成/叙事指南 → 主世界层(无转述时叙事也挂主世界层);开了转述则叙事指南可挂转述;美学进常驻。
|
||||
- 世界模拟默认槽:仅主世界层(转述关、旁观关、视角关)。「游玩执行方式」由程序从独立文风层、变量维护和秘密隔离需求中整理。
|
||||
- 中段按承诺写上下文;每块只标挂谁(mount)与稳/变,具体顺序留给自动「上下文排序」。挂载归属:世界/机制/生成/叙事指南 → 主世界层(无转述时叙事也挂主世界层);开了转述则叙事指南可挂转述;美学进常驻。
|
||||
- 「回复呈现」几乎都排:先钉每轮看什么。有检定/对抗/掷骰则排「随机范围整理」收范围表。二者都在「正文组成」之前;正文组成把清单(及隐藏备用随机区)展示出来,开场白之前。
|
||||
- 收成链顺序固定:上游上下文大致齐 →「回复呈现」→(若有随机)「随机范围整理」→「正文组成」→「上下文投影排序」→「细化终稿」→ 需要开场则「开场白与开场变量」(〔收口〕)。选定开场后结束创作并保存。
|
||||
- 收成链顺序固定:上游上下文大致齐 →「回复呈现」→(若有随机)「随机范围整理」→「正文组成」→ 程序自动整理执行方式、上下文并组装运行配置 → 需要开场则「开场白与开场变量」(〔收口〕)。选定开场后结束创作并保存。
|
||||
|
||||
principles:
|
||||
- 正推:美学卡上用户会在意的部分 → 缺口 → 技能。同一题材换核就换图,不按题材套世界模拟全套。
|
||||
- 技能选型以技能 meta 为准;本配方不代替各技能写调用条件。
|
||||
- 执行单元只用固定槽(主世界层 / 可选叙事转述 / 可选角色视角;旁观按需)。不开机遇裁定;真随机=随机范围整理 + 程序插备用数。
|
||||
- 默认少槽:只开主世界层;转述默认关(主世界层原文即终稿);有变量/表开旁观;仅强信息隔离才开角色视角。
|
||||
- 挂谁可早、排第几须晚:插入序与投影位只由「上下文投影排序」产出;中段技能写 mount 与稳/变。
|
||||
- 挂谁可早、排第几须晚:插入序与投影位只由自动「上下文排序」产出;中段技能写 mount 与稳/变。
|
||||
- 投影序扁平:「对话.历史」按 projection 动态裁剪;少变靠前、易变靠后是软规则,不是硬分区。
|
||||
- 真值 / Data / Progressive 三分;派生性格与章细纲不落可写真值。
|
||||
- 有编排参数的步骤:进执行前钉齐 params(askUser 选项+其它);缺参时先问再进 design-step。
|
||||
|
||||
@@ -1,20 +1,16 @@
|
||||
# 对话落盘 · 配方目录(与工序编排配方不共享)
|
||||
# id = recipes/转述/{id}/ 文件夹;对用户展示用 name(中文)
|
||||
# 选定后写入黑板 tag 创作.选用配方;对话落盘不种节点 DAG
|
||||
# 对话落盘 · 本局创作方案参考案例目录(不供用户手动选择)
|
||||
# id = recipes/转述/{id}/ 文件夹;本局方案可引用、混合或改写多个案例
|
||||
|
||||
recipes:
|
||||
- id: 快穿短局
|
||||
name: 快穿短局
|
||||
declaration: 围着一个核心设定快速代入;难扩展、不适合长期经营;至多简单数值
|
||||
declaration: 用一个立刻生效的体验核形成短反馈环;需要时可混入长连续或硬机制
|
||||
- id: 数据化跑团体验
|
||||
name: 数据化跑团体验
|
||||
declaration: AIRP / 交互式小说;设定为主,可选跨轮真值与分档映射
|
||||
declaration: 用硬判断机制与表格/数值真值,让选择、资源和后果可核对地持续生效
|
||||
- id: 文本生成器
|
||||
name: 文本生成器
|
||||
declaration: 创作收范例与模仿面;开玩后主世界出正文、转述出文风/壳
|
||||
declaration: 把每次输入转成相对独立的成稿,并稳定复现范例与写法约束
|
||||
- id: 交互式长文生成器
|
||||
name: 交互式长文生成器
|
||||
declaration: 多轮钉需求与文风;连贯长文/章段(无数值跑团)
|
||||
- id: 正文组成
|
||||
name: 正文组成
|
||||
declaration: 钉呈现格式(壳与分区);开场白按该格式生成
|
||||
declaration: 以长连续承接事实、人物弧线和未决承诺;可有变量,也可混入硬机制
|
||||
|
||||
@@ -1,25 +1,28 @@
|
||||
# 交互式长文生成器 · 对话落盘配方
|
||||
# 原「文本生成器」长文路径;多轮互动钉规格,再生成连贯长文。
|
||||
# 本局创作方案参考案例;可以与硬机制等案例混合,不以有无变量作区分。
|
||||
|
||||
when: >-
|
||||
用户要写较长、可分段推进的正文(章段、连载向、润色扩写);
|
||||
需要多轮确认需求/文风/结构;不要世界推演、不要跨轮数值状态机
|
||||
brief: 让多轮生成共享长连续,在章节与段落推进中守住事实、人物弧线和未决承诺
|
||||
|
||||
core: >-
|
||||
把用户原话收成可生成长文的规格:需求、文风、结构与禁忌用 insert。
|
||||
默认无变量、无旁观、无主世界层。正文格式(设计.正文组成)与开场白按需另钉。
|
||||
signals:
|
||||
- 用户要章段、连载或持续多轮推进的长正文
|
||||
- 后文必须承接早先事实、伏笔、人物变化与章节目标
|
||||
- 用户会在生成过程中改方向、局部重写或控制节奏
|
||||
|
||||
process:
|
||||
- 听清写什么、给谁看、气质与禁忌 → insert 用户.需求 / 设计.文风 等(order 偏小)。本轮材料已够时可同轮多落;缺关键信息就问一句。
|
||||
- 需要版式 → insert 设计.正文组成;要开篇样例 → 按格式 insert 设计.开场白。上游已齐时可与上列同轮一并落。
|
||||
- 不要搭跑团数值、舞台骨架、declare_variable。
|
||||
- 长章叙事落产物,不要只留在聊天确认段。
|
||||
method:
|
||||
- 把长期承诺与本段目标分开:稳定保存不能漂移的事实,同时允许当前段落灵活改写
|
||||
- 用少而承重的变量或动态表维护连续性;变量可以存在,但不必形成硬判定系统
|
||||
- 每轮既推进当前章段,也更新仍对后文有价值的关系、伏笔和未决事项
|
||||
- 若成功失败、阈值或资源消耗需要可核对裁决,同时混入数据化跑团案例
|
||||
|
||||
principles:
|
||||
- 正推长文交付规格,勿套数据化跑团。
|
||||
- 保留用户原意;规格只走 toolcall。
|
||||
- 格式与开场白分清:正文组成 ≠ 开场白。
|
||||
|
||||
brief: 交互式长文:多轮钉需求与文风 → 可分段生成
|
||||
|
||||
steps: []
|
||||
tier_tendencies:
|
||||
- module_id: story-progression
|
||||
tier: 必须
|
||||
why: 要钉清每轮如何承接、推进、停笔和接受用户改向
|
||||
- module_id: narrative-guide
|
||||
tier: 有必要
|
||||
why: 长文本需要跨段稳定的遣词、节奏与笔墨焦点
|
||||
- module_id: variable-design
|
||||
tier: 有必要
|
||||
why: 变量或动态表可维护人物变化、伏笔和未决事项,但不必天然数值化
|
||||
- module_id: mechanism
|
||||
tier: 有一定效果
|
||||
why: 只有本局存在需要一致裁决的硬规则时才升档
|
||||
|
||||
@@ -1,36 +1,27 @@
|
||||
# 快穿短局 · 对话落盘配方
|
||||
# 注入系统提示「本局配方」;不种节点 DAG。
|
||||
# 各能力何时落盘 → 【能力 · 何时落盘】;本配方只给骨架,并覆盖「舞台/生成/拓扑」即使何时用成立也不落。
|
||||
# 本局创作方案参考案例;可以与其它案例混合或被整份改写,不是固定路径。
|
||||
|
||||
when: >-
|
||||
用户要快速代入一小段体验:快穿小世界、一次性副本、单钩子情景、短任务。
|
||||
核依赖跨轮记住时,至多两三个数字(时限、任务进度、好感)。
|
||||
brief: 用一个立刻生效的体验核形成短反馈环,让少量材料尽快变成可玩的局面
|
||||
|
||||
core: >-
|
||||
把用户原话收成可开玩的一核短局规格,写入本局黑板;开玩后主世界层读产物写正文,默认原文即终稿。
|
||||
必交:美学纲领(这一核是什么、怎么代入、轮转到哪停)。
|
||||
用户代入有名字/简介 → insert 设计.主角设定(名字+背景);不要写进开场白。
|
||||
核是拿掉会换故事的变造法则 → 再交实现机制。
|
||||
核依赖跨轮记住 → 再 declare 至多两三个真值。核可玩 → 交开场白(只要叙事,勿夹用户角色 meta)。
|
||||
某一能力能不能落对照【能力 · 何时落盘】。材料只够围着这一核开玩 → 收口开场白。
|
||||
用户改口要铺可经营世界 → 本局仍把已说的收成这一核,并告知完整世界用「数据化跑团体验」或工序编排「回合推演」。
|
||||
signals:
|
||||
- 用户要一次性副本、单钩子情景、短任务或快穿小世界
|
||||
- 乐趣集中在一个处境、关系或变造法则,删掉它整局就失去意义
|
||||
- 用户希望快速进入局面,而不是先建设可长期经营的广阔世界
|
||||
|
||||
process:
|
||||
- 每轮对照【能力 · 何时落盘】与已有产物:条件已成立的可同轮多落;核还糊 → 问挡住的 1 点。
|
||||
- 【1】insert 设计.美学纲领与交互范式:用用户原话写清这一核、站位、何时等用户。缺哪件问哪件。
|
||||
- 【1b】代入且有名字/简介 → insert 设计.主角设定(或 设计.主角设定#短码,与开场同槽)。禁止写进开场白 meta。
|
||||
- 【2】核=拿掉会换故事的变造法则(如任务失败即送走)→ insert 设计.实现机制,写短。只是身份或开场处境 → 写进主角设定,跳过机制。
|
||||
- 用户点名文风/禁忌/写法档 → insert 叙事指南。点名推进/大纲/停笔 → insert 故事推进。二者条件独立,不要因为没谈推进就不落文风。用户要分区或壳 → 回复呈现再正文组成。未点名文风与推进 → 跳过二者,开玩吃默认散文。材料已齐且条件成立时可与上列同轮一并落。
|
||||
- 核必须跨轮记住的数(时限/进度/好感,至多三个)→ declare_variable;到某值换文案再 declare_map。没有门控 → 不立真值。
|
||||
- 核已可玩 → insert 设计.开场白(须含 @玩家)。多开局用 设计.开场白#短码,互不覆盖;改某一局 insert 同一完整 tag。禁止夹用户角色.名字/简介。
|
||||
- 舞台骨架 / 生成规则 / 拓扑即使「何时用」成立也不 insert:地点与规则折进美学核或开场白。
|
||||
method:
|
||||
- 把最承重的处境、关系或法则压成一核,其余设定只保留能让这一核马上运转的部分
|
||||
- 让每轮反馈都回到这一核;需要状态时只保留真正改变选择或后果的少量状态
|
||||
- 用户后来要求扩展经营时,可以混入长连续或硬机制案例,不把“短局”当永久限制
|
||||
|
||||
principles:
|
||||
- 落盘只留删掉会让「这局玩什么」变掉的内容;其余当随口补充,问一句要不要并进核,用户没要就不 insert。
|
||||
- 交得出可玩开场即停。地图、分册、生成一族 NPC 留给「数据化跑团体验」或「回合推演」。
|
||||
- 有门控才 declare 真值。
|
||||
- 默认只开主世界层;有真值时旁观由落档程序开。规格只走 toolcall。
|
||||
|
||||
brief: 一核短局:钉体验核 → 主角名片 → 按需一条机制/两三个数 → 开场开玩
|
||||
|
||||
steps: []
|
||||
tier_tendencies:
|
||||
- module_id: aesthetics-interaction
|
||||
tier: 必须
|
||||
why: 必须知道短局反复兑现的核心感受和停笔边界
|
||||
- module_id: opening-setup
|
||||
tier: 必须
|
||||
why: 短局需要让体验核立即进入可玩的具体局面
|
||||
- module_id: world-blueprint
|
||||
tier: 有一定效果
|
||||
why: 只有体验核依赖换区、探索或空间选择时才需要展开
|
||||
- module_id: variable-design
|
||||
tier: 有一定效果
|
||||
why: 只有状态会跨轮改变选择或后果时才升档
|
||||
|
||||
@@ -1,35 +1,27 @@
|
||||
# 数据化跑团体验 · 对话落盘配方
|
||||
# 注入转述总助手「本局配方」;不种节点 DAG。
|
||||
# 名字带「数据化」,内核仍是 AIRP / 交互式小说。
|
||||
# 各能力何时落盘 → 系统提示【能力 · 何时落盘】(modules meta);本配方只给顺序骨架。
|
||||
# 本局创作方案参考案例;可以与长连续等案例混合,不以篇幅长短作区分。
|
||||
|
||||
when: >-
|
||||
用户要回合制角色扮演、沉浸推演或交互式小说(AIRP / IF);
|
||||
在普通设定之外,还可能需要跨轮记住少量状态,或按阈值换态度/大纲/场景文案
|
||||
brief: 用硬判断机制和结构化状态让选择、资源与后果可核对地持续生效
|
||||
|
||||
core: >-
|
||||
理论上总是先设计美学纲领与体验核心,用来锚定「用户会在意什么」;
|
||||
再按需用核心规则(实现机制)钉下那套特殊故事设计;然后才往外兑现舞台、
|
||||
叙事、真值、映射、版式与开场。作用对象是本局黑板;开玩后主世界层读投影与
|
||||
真值写正文,默认只开主世界层、原文即终稿。
|
||||
具体能不能落:对照【能力 · 何时落盘】的何时用 / 何时不用;本配方不重复罗列。
|
||||
signals:
|
||||
- 用户在意成功失败、资源消耗、阈值、检定或明确触发条件
|
||||
- 状态必须跨轮保持真值,不能只靠叙事语气近似表达
|
||||
- 表格或数值的变化本身会改变可做选择、世界反应或结局
|
||||
|
||||
process:
|
||||
- 每轮对照【能力 · 何时落盘】与已有产物:凡「何时用」已成立的可同轮多次 insert / declare_*;条件含糊或未到 → 本轮不落该能力,短确认或问挡住的 1 点。不抢跑未齐条件的下游。
|
||||
- 【骨架序 · 1】美学纲领与体验核心(能力表里通常总是先做)→ insert 设计.美学纲领与交互范式。模糊则细化;不确定则询问。未锚定前不建议下游。
|
||||
- 【骨架序 · 2】实现机制(仅当能力「何时用」成立:有拿掉就换骨架的变造法则)→ insert 设计.实现机制。何时不用成立则跳过,不追问要不要加规则。
|
||||
- 【骨架序 · 3+】其余能力(主角设定、舞台、生成规则、叙事、变量、回复呈现、正文组成、开场白…)各自对照何时用再落;变量走 declare_variable / declare_map。本轮材料已够且条件齐的可一并落。
|
||||
用户代入且有名字/身份/金手指时落主角设定(insert 设计.主角设定)。
|
||||
- 聊完一轮:按 reply_module 分段确认本轮所落模块 + 建议「条件已近」的下一刀(勿推荐尚未满足何时用的能力)。
|
||||
method:
|
||||
- 只把会参与判断的事实结构化,明确真值、写入者、变化条件和冲突时的裁决来源
|
||||
- 把规则结果转成世界中的可感后果;数值服务体验,不用数字替代叙事
|
||||
- 长短与连续性另行决定;长连续跑团可同时引用交互式长文案例
|
||||
|
||||
principles:
|
||||
- 锚定优先:美学不定稳,不做下游;完善设定时不要忘掉核心体验。
|
||||
- 何时落盘以能力 meta 为准;配方顺序是骨架,不是许可抢跑。
|
||||
- 正推:美学卡上用户会在意的部分 → 缺口 → 再落对应产物;换核就换补什么。
|
||||
- 承重才立真值;派生文案 = f(真值),只走映射。真值是兑现体验的加层,不是主戏。
|
||||
- 默认无独立叙事转述、无旁观;用户点名再议。
|
||||
- 保留用户原意;规格只走 toolcall。
|
||||
|
||||
brief: 美学锚定 → 按需机制 → 对照能力何时落盘再兑现;可选真值+映射
|
||||
|
||||
steps: []
|
||||
tier_tendencies:
|
||||
- module_id: mechanism
|
||||
tier: 必须
|
||||
why: 硬判断需要可重复执行且结果一致的规则
|
||||
- module_id: variable-design
|
||||
tier: 必须
|
||||
why: 参与判断的状态必须有真值、更新条件和维护方式
|
||||
- module_id: story-progression
|
||||
tier: 有必要
|
||||
why: 需要说明用户输入怎样触发判断,以及何时停下等待选择
|
||||
- module_id: topology
|
||||
tier: 有一定效果
|
||||
why: 只有升级、地图或关系本身构成可计算结构时才增加价值
|
||||
|
||||
@@ -1,32 +1,27 @@
|
||||
# 文本生成器 · 对话落盘配方
|
||||
# 创作期写出游玩要用的固定上下文;成文在游玩:主世界出正文,转述出文风/壳。
|
||||
# 本局创作方案参考案例;可以与其它案例混合或被整份改写,不是固定路径。
|
||||
|
||||
when: >-
|
||||
用户要短篇/短文批量,且要强模仿给定范例;
|
||||
篇与篇不必剧情承接;成文走游玩管线(主世界 + 叙事转述)
|
||||
brief: 把每次输入转成相对独立的成稿,并让范例与写法约束稳定复现
|
||||
|
||||
core: >-
|
||||
创作进料必须 insert 齐游玩上下文:模仿范例、模仿要点、故事推进(大纲扩写)、可选叙事指南与轻量需求。
|
||||
落档只挂载这些创作期产物,不另造用法。开玩后主世界写故事正文(Markdown),转述按范例改写/镶壳。
|
||||
刚需开转述;用户输入用法以创作期故事推进为准。
|
||||
signals:
|
||||
- 用户提供范例,要求仿写、扩写或批量产出
|
||||
- 每份成稿可以独立收束,不依赖长期世界状态
|
||||
- 评价重点是成稿质量与风格复现,而非回合判定
|
||||
|
||||
process:
|
||||
- 要齐:① insert 设计.模仿范例;② insert 设计.模仿要点(用户点名要学的面)。缺则问这两项。
|
||||
- 必 insert 设计.故事推进(创作期产物,开玩直接挂载)。正文至少写清:
|
||||
「用户输入用法:大纲扩写。本轮输入=短文大纲/场面要点;一次成完整篇并收束。禁止扮演停笔、禁止等用户行动。主世界交故事正文;有转述时转述负责文风与呈现,可补残稿但不改剧情。」
|
||||
- 文风细则对照「叙事指南」何时用再 insert 设计.叙事指南。
|
||||
- 可选 insert 用户.需求(题材/禁忌/篇幅)。
|
||||
- 齐料清单缺故事推进 / 范例 / 要点任一 → 先补 insert,不要说可以开玩。
|
||||
- 材料齐了:告知「可以落档/开玩」;禁止在创作期写成稿短文。
|
||||
- 开玩:用户本轮=本篇大纲;读创作期已挂上下文推进。对话一长 clear_dialogue,留产物。
|
||||
method:
|
||||
- 区分范例中要复现的写法与只属于原文的内容,形成少量可执行约束
|
||||
- 把用户每次输入解释为本篇素材或大纲,保留明确的人、事、话与结果,再补足具体场面
|
||||
- 每份输出自行收束;只有用户明确要求跨篇承接时才混入长连续方法
|
||||
|
||||
principles:
|
||||
- 游玩只消费创作期产物;用法/文风/模仿面都在 insert 里,不靠落档程序补种。
|
||||
- 创作≠生产:创作收上下文;生产在游玩。
|
||||
- 分工:主世界 = 故事正文;转述 = 文风/壳(对照范例+要点;可补残稿)。
|
||||
- 输入用法由创作期故事推进钉死为大纲扩写。
|
||||
- 一篇一世界;少问约束;无 declare_variable。
|
||||
|
||||
brief: 文本生成:创作期钉范例+要点+大纲扩写推进 → 开玩挂载成文
|
||||
|
||||
steps: []
|
||||
tier_tendencies:
|
||||
- module_id: narrative-guide
|
||||
tier: 必须
|
||||
why: 范例的遣词、节奏和笔墨焦点需要稳定复现
|
||||
- module_id: story-progression
|
||||
tier: 必须
|
||||
why: 必须钉清输入是素材、大纲、续写还是改写要求
|
||||
- module_id: variable-design
|
||||
tier: 没有意义
|
||||
why: 独立成稿通常不需要跨篇真值;若要连续承接则按本局重新升档
|
||||
- module_id: reply-format
|
||||
tier: 有一定效果
|
||||
why: 只有成稿需要特殊分区或局部渲染时才增加价值
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
# 正文组成 · 转述进料配方
|
||||
# 只钉「怎么给用户看」的格式;开场白按此格式另生成。
|
||||
|
||||
when: >-
|
||||
已大致知道要什么体验,需要定版式:选壳、分区、各区职责;
|
||||
或用户在谈呈现/版式/要不要选项坞/监控栏
|
||||
|
||||
core: >-
|
||||
写入设计.正文组成(格式契约)。
|
||||
这不是开场白、不是故事正文;开场白必须按本格式的可见块生成。
|
||||
|
||||
process:
|
||||
- 对照设定产物,选 shell_id(文本沉浸默认 prose)。
|
||||
- 写可见块挂到壳区域(至少 body)。
|
||||
- 需要时写微调(tone_chrome、block_labels、是否显示建议行动)。
|
||||
- 用户要第一幕时:再按本格式生成设计.开场白(内容),勿把格式 JSON 当开场白展示。
|
||||
|
||||
principles:
|
||||
- 格式权威在设计.正文组成;开场白服从格式。
|
||||
- 不重议文风遣词(归叙事/需求);本步只交展示契约。
|
||||
- 产物自洽:只读设计.正文组成就能知道开场/每轮终稿怎么灌数。
|
||||
|
||||
brief: 正文组成:钉呈现壳与可见块(格式)→ 开场白按格式生成
|
||||
|
||||
steps: []
|
||||
@@ -0,0 +1,124 @@
|
||||
# 对照
|
||||
|
||||
## intro
|
||||
|
||||
```intro
|
||||
正文和一块结构同时在屏上。谁占主面积按本局定。抬头可贴在这一行的上沿。
|
||||
```
|
||||
|
||||
## layout
|
||||
|
||||
```layout
|
||||
id: alongside
|
||||
主区: 正文与结构,并排
|
||||
可加窄带: 抬头、文字选项
|
||||
摆放:
|
||||
抬头: 最上的一行短内容
|
||||
正文: 一栏
|
||||
结构: 另一栏。创作时写一次绘图渲染器并固定;游玩把运行拓扑的节点与连接灌进去出图。不把节点写成清单。折叠标题用本局这块内容的短名
|
||||
文字选项: 最下,始终直接可见,不放进标签或折叠
|
||||
窄屏改为上下排列。未启用的区不留空位。
|
||||
```
|
||||
|
||||
## note
|
||||
|
||||
```note
|
||||
不替换:正文和结构同屏。窄屏改为上下。选项在最下,直接可见。结构用创作时固定的绘图器,游玩灌入拓扑。
|
||||
可替换:两栏谁更宽、抬头文字、结构块的短名、配色和字体。本局没有结构时改用阅读或正文为主。
|
||||
```
|
||||
|
||||
## sample
|
||||
|
||||
```sample
|
||||
html:
|
||||
<div id="app">
|
||||
<p id="heading"></p>
|
||||
<div class="pair">
|
||||
<article id="body"></article>
|
||||
<section id="structure">
|
||||
<div id="graph"></div>
|
||||
</section>
|
||||
</div>
|
||||
<ol id="options"></ol>
|
||||
</div>
|
||||
|
||||
css:
|
||||
:root { color: #1c1915; background: #f7f3ea; }
|
||||
#app { max-width: 56rem; margin: 0 auto; padding: 1.25rem; font: 16px/1.6 "Palatino Linotype", "Songti SC", serif; }
|
||||
.pair { display: grid; grid-template-columns: 1.2fr 0.8fr; gap: 1rem; }
|
||||
#structure { border: 1px solid #d5cbb8; border-radius: 6px; padding: 0.8rem; background: #fffdf8; }
|
||||
#graph { min-height: 8rem; }
|
||||
#options { margin: 1rem 0 0; padding-left: 1.2rem; }
|
||||
@media (max-width: 720px) { .pair { grid-template-columns: 1fr; } }
|
||||
|
||||
js:
|
||||
present.onData(function (data) {
|
||||
var blocks = (data && data.blocks) || {};
|
||||
putText("heading", blocks.heading);
|
||||
putText("body", blocks.body);
|
||||
putGraph("graph", blocks.structure);
|
||||
putList("options", blocks.options);
|
||||
});
|
||||
function putText(id, value) {
|
||||
var el = document.getElementById(id);
|
||||
if (!el) return;
|
||||
if (value == null || value === "") { el.remove(); return; }
|
||||
el.textContent = String(value);
|
||||
}
|
||||
function putGraph(id, value) {
|
||||
var el = document.getElementById(id);
|
||||
if (!el) return;
|
||||
el.replaceChildren();
|
||||
var nodes = value && Array.isArray(value.nodes) ? value.nodes : [];
|
||||
var links = value && Array.isArray(value.links) ? value.links : [];
|
||||
if (!nodes.length) { el.remove(); return; }
|
||||
var step = 120;
|
||||
var svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
||||
svg.setAttribute("viewBox", "0 0 " + Math.max(nodes.length * step, step) + " 92");
|
||||
svg.setAttribute("width", "100%");
|
||||
svg.setAttribute("height", "92");
|
||||
var at = {};
|
||||
nodes.forEach(function (node, i) { at[node.id] = i; });
|
||||
links.forEach(function (link) {
|
||||
var a = at[link.from];
|
||||
var b = at[link.to];
|
||||
if (a == null || b == null) return;
|
||||
var line = document.createElementNS(svg.namespaceURI, "line");
|
||||
line.setAttribute("x1", 48 + a * step);
|
||||
line.setAttribute("y1", 34);
|
||||
line.setAttribute("x2", 48 + b * step);
|
||||
line.setAttribute("y2", 34);
|
||||
line.setAttribute("stroke", "#8a5a2b");
|
||||
svg.appendChild(line);
|
||||
});
|
||||
nodes.forEach(function (node, i) {
|
||||
var x = 48 + i * step;
|
||||
var circle = document.createElementNS(svg.namespaceURI, "circle");
|
||||
circle.setAttribute("cx", x);
|
||||
circle.setAttribute("cy", 34);
|
||||
circle.setAttribute("r", 14);
|
||||
circle.setAttribute("fill", node.current ? "#1c1915" : "#fffdf8");
|
||||
circle.setAttribute("stroke", "#1c1915");
|
||||
var label = document.createElementNS(svg.namespaceURI, "text");
|
||||
label.setAttribute("x", x);
|
||||
label.setAttribute("y", 70);
|
||||
label.setAttribute("text-anchor", "middle");
|
||||
label.setAttribute("font-size", "12");
|
||||
label.textContent = String(node.name || node.id || "");
|
||||
svg.appendChild(circle);
|
||||
svg.appendChild(label);
|
||||
});
|
||||
el.appendChild(svg);
|
||||
}
|
||||
function putList(id, value) {
|
||||
var el = document.getElementById(id);
|
||||
if (!el) return;
|
||||
el.replaceChildren();
|
||||
if (!Array.isArray(value) || !value.length) { el.remove(); return; }
|
||||
value.forEach(function (line) {
|
||||
var li = document.createElement("li");
|
||||
li.textContent = String(line);
|
||||
el.appendChild(li);
|
||||
});
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,16 @@
|
||||
# 壳参考库。正文组成与 0 层正文组成读取后,按本局启用的区改一版并固定。
|
||||
# 旧 present-shells 只留给现有渲染器,不作为生成样本。
|
||||
|
||||
shells:
|
||||
- id: reading
|
||||
name: 阅读
|
||||
- id: dual-text
|
||||
name: 正文为主
|
||||
- id: data-cards
|
||||
name: 数据常驻
|
||||
- id: alongside
|
||||
name: 对照
|
||||
- id: paged
|
||||
name: 分时
|
||||
- id: piece
|
||||
name: 成稿
|
||||
@@ -0,0 +1,125 @@
|
||||
# 数据常驻
|
||||
|
||||
## intro
|
||||
|
||||
```intro
|
||||
数据一直铺在面上,不折叠,也不替换正文。正文仍是数百字的主文,占较宽的一栏。另一路内容占较窄的一栏。选项始终直接可见。
|
||||
```
|
||||
|
||||
## layout
|
||||
|
||||
```layout
|
||||
id: data-cards
|
||||
主区: 数据常驻
|
||||
附区: 正文(数百字)、窄栏
|
||||
可加窄带: 抬头、文字选项
|
||||
摆放:
|
||||
抬头: 最上的一行
|
||||
数据: 一直展开,紧挨抬头。当前值都短则并排;任一条较长则整组改为上下排列
|
||||
正文: 数据之下较宽的一栏,不因数据常驻而缩成一句
|
||||
窄栏: 较窄的一栏。栏顶标题按本局这块内容填写。窄屏改为正文在上、窄栏在下
|
||||
文字选项: 最下,始终直接可见
|
||||
本局没有窄栏时删掉窄栏,正文占回整宽。两三条短内容用阅读壳的抬头一行,不用本壳。未启用的区不留空位。
|
||||
```
|
||||
|
||||
## note
|
||||
|
||||
```note
|
||||
不替换:数据区在抬头下一直展开。正文在其下较宽的一栏,篇幅仍是数百字。窄栏在较窄的一栏。选项在两栏之下,直接可见。窄屏时宽栏在上。
|
||||
可替换:数据项的名称、条数和当前值,窄栏的标题和条目,配色和字体。本局没有窄栏时删掉窄栏,正文占回整宽。
|
||||
```
|
||||
|
||||
## sample
|
||||
|
||||
```sample
|
||||
html:
|
||||
<div id="app">
|
||||
<p id="heading"></p>
|
||||
<div id="cards"></div>
|
||||
<div class="pair">
|
||||
<article id="body"></article>
|
||||
<aside class="side">
|
||||
<p id="side-title"></p>
|
||||
<div id="side"></div>
|
||||
</aside>
|
||||
</div>
|
||||
<ol id="options"></ol>
|
||||
</div>
|
||||
|
||||
css:
|
||||
:root { color: #1c2420; background: #d5ddd4; }
|
||||
#app { max-width: 52rem; margin: 0 auto; padding: 1rem; font: 15px/1.75 "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif; }
|
||||
#heading { margin: 0 0 0.6rem; font-size: 13px; }
|
||||
#cards { display: grid; grid-template-columns: repeat(4, 1fr); gap: 0.45rem; margin-bottom: 0.75rem; }
|
||||
#cards.stack { grid-template-columns: 1fr; }
|
||||
.card { padding: 0.4rem 0.55rem; border-radius: 4px; background: rgba(255,255,255,0.72); }
|
||||
.card b { display: block; font-size: 12px; font-weight: 600; color: #5c6b62; }
|
||||
.card span { font-size: 1.05rem; font-weight: 650; }
|
||||
.pair { display: grid; grid-template-columns: 1.65fr 0.72fr; gap: 0.7rem; align-items: start; }
|
||||
#body { margin: 0; padding: 0.85rem 1rem; border-radius: 6px; background: #f7faf6; }
|
||||
.side { padding: 0.55rem 0.65rem; border-radius: 6px; background: rgba(22,28,26,0.88); color: #f2f4f2; font-size: 13px; line-height: 1.45; }
|
||||
#side-title { margin: 0 0 0.15rem; padding-bottom: 0.35rem; border-bottom: 1px solid rgba(255,255,255,0.16); font-weight: 700; }
|
||||
#options { margin: 0.75rem 0 0; padding: 0; list-style: none; }
|
||||
#options li { margin-top: 0.35rem; padding: 0.35rem 0.7rem; border-radius: 4px; background: rgba(255,255,255,0.72); }
|
||||
@media (max-width: 720px) { .pair, #cards { grid-template-columns: 1fr; } }
|
||||
|
||||
js:
|
||||
present.onData(function (data) {
|
||||
var blocks = (data && data.blocks) || {};
|
||||
putText("heading", blocks.heading);
|
||||
putText("side-title", blocks.side_title);
|
||||
putText("body", blocks.body);
|
||||
putCards("cards", blocks.cards);
|
||||
putLines("side", blocks.side);
|
||||
putList("options", blocks.options);
|
||||
});
|
||||
function putText(id, value) {
|
||||
var el = document.getElementById(id);
|
||||
if (!el) return;
|
||||
if (value == null || value === "") { el.remove(); return; }
|
||||
el.textContent = String(value);
|
||||
}
|
||||
function putCards(id, value) {
|
||||
var el = document.getElementById(id);
|
||||
if (!el) return;
|
||||
el.replaceChildren();
|
||||
var rows = Array.isArray(value) ? value : [];
|
||||
if (!rows.length) { el.remove(); return; }
|
||||
var long = rows.some(function (row) { return String(row.value ?? "").length > 8; });
|
||||
el.classList.toggle("stack", long);
|
||||
rows.forEach(function (row) {
|
||||
var card = document.createElement("div");
|
||||
card.className = "card";
|
||||
var name = document.createElement("b");
|
||||
var num = document.createElement("span");
|
||||
name.textContent = String(row.name || "");
|
||||
num.textContent = String(row.value ?? "");
|
||||
card.appendChild(name);
|
||||
card.appendChild(num);
|
||||
el.appendChild(card);
|
||||
});
|
||||
}
|
||||
function putLines(id, value) {
|
||||
var el = document.getElementById(id);
|
||||
if (!el) return;
|
||||
el.replaceChildren();
|
||||
var rows = Array.isArray(value) ? value : [];
|
||||
if (!rows.length) { el.remove(); return; }
|
||||
rows.forEach(function (line) {
|
||||
var p = document.createElement("p");
|
||||
p.textContent = String(line && line.text != null ? line.text : line);
|
||||
el.appendChild(p);
|
||||
});
|
||||
}
|
||||
function putList(id, value) {
|
||||
var el = document.getElementById(id);
|
||||
if (!el) return;
|
||||
el.replaceChildren();
|
||||
if (!Array.isArray(value) || !value.length) { el.remove(); return; }
|
||||
value.forEach(function (line) {
|
||||
var li = document.createElement("li");
|
||||
li.textContent = String(line);
|
||||
el.appendChild(li);
|
||||
});
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,79 @@
|
||||
# 双文
|
||||
|
||||
## intro
|
||||
|
||||
```intro
|
||||
正文占满阅读宽度。短文和结构用标签或折叠,点开才看。选项始终直接可见。
|
||||
```
|
||||
|
||||
## layout
|
||||
|
||||
```layout
|
||||
id: dual-text
|
||||
主区: 正文
|
||||
可加: 短文、结构、数据、抬头、文字选项
|
||||
摆放:
|
||||
抬头: 最上的一行短内容
|
||||
正文: 占满阅读宽度,按数百字来写
|
||||
短文、结构: 标签或折叠,点开才看。标签用短显示名
|
||||
数据: 若有,默认可收起
|
||||
文字选项: 始终直接可见,不放进标签或折叠
|
||||
未启用的区不留空位。
|
||||
```
|
||||
|
||||
## note
|
||||
|
||||
```note
|
||||
不替换:正文占满阅读宽度。其余块收在标签或折叠里,默认不展开。选项在这些控件之外,直接可见。
|
||||
可替换:标签和折叠的短名、收起块里的文字、配色和字体。本局没有的块删掉。数据要一直展开时改用数据常驻。
|
||||
```
|
||||
|
||||
## sample
|
||||
|
||||
```sample
|
||||
html:
|
||||
<div id="app">
|
||||
<p id="heading"></p>
|
||||
<article id="body"></article>
|
||||
<details>
|
||||
<summary id="vignette-title"></summary>
|
||||
<div id="vignette"></div>
|
||||
</details>
|
||||
<ol id="options"></ol>
|
||||
</div>
|
||||
|
||||
css:
|
||||
:root { color: #1c1915; background: #f7f3ea; }
|
||||
#app { max-width: 40rem; margin: 0 auto; padding: 1.25rem; font: 17px/1.75 "Palatino Linotype", "Songti SC", serif; }
|
||||
#heading { margin: 0 0 0.8rem; font-size: 0.95rem; }
|
||||
#vignette { font-size: 0.92rem; margin-top: 0.6rem; }
|
||||
#options { margin: 0.8rem 0 0; padding-left: 1.2rem; }
|
||||
summary { cursor: pointer; }
|
||||
|
||||
js:
|
||||
present.onData(function (data) {
|
||||
var blocks = (data && data.blocks) || {};
|
||||
putText("heading", blocks.heading);
|
||||
putText("body", blocks.body);
|
||||
putText("vignette-title", blocks.vignette_title);
|
||||
putText("vignette", blocks.vignette);
|
||||
putList("options", blocks.options);
|
||||
});
|
||||
function putText(id, value) {
|
||||
var el = document.getElementById(id);
|
||||
if (!el) return;
|
||||
if (value == null || value === "") { el.remove(); return; }
|
||||
el.textContent = String(value);
|
||||
}
|
||||
function putList(id, value) {
|
||||
var el = document.getElementById(id);
|
||||
if (!el) return;
|
||||
el.replaceChildren();
|
||||
if (!Array.isArray(value) || !value.length) { el.remove(); return; }
|
||||
value.forEach(function (line) {
|
||||
var li = document.createElement("li");
|
||||
li.textContent = String(line);
|
||||
el.appendChild(li);
|
||||
});
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,84 @@
|
||||
# 分时
|
||||
|
||||
## intro
|
||||
|
||||
```intro
|
||||
正文、小故事、数据卡、结构里有三块以上都要单独看。一次只显示一块,其余用标签切换。切换只改变当前视图。
|
||||
```
|
||||
|
||||
## layout
|
||||
|
||||
```layout
|
||||
id: paged
|
||||
主区: 当前这一页
|
||||
分页: 正文、小故事、数据卡、结构中本局启用且需要单独看的区
|
||||
可加窄带: 抬头留在标签之外,始终可见
|
||||
摆放:
|
||||
抬头: 标签之上
|
||||
标签: 一行,当前页加下划线。标签用本局该页的短名,不写区种类名
|
||||
页内: 按该区自己的读法排。数据卡用卡片。结构调用已固定的拓扑绘图渲染器,不输出节点清单。
|
||||
未启用的区不建标签。
|
||||
```
|
||||
|
||||
## note
|
||||
|
||||
```note
|
||||
不替换:同时只显示一页。页用一行标签切换。抬头在标签之外。
|
||||
可替换:每个标签的短名、页内文字和数据项、配色和字体。本局未启用的页不建标签。
|
||||
```
|
||||
|
||||
## sample
|
||||
|
||||
```sample
|
||||
html:
|
||||
<div id="app">
|
||||
<p id="heading"></p>
|
||||
<div id="tabs"></div>
|
||||
<div id="page"></div>
|
||||
</div>
|
||||
|
||||
css:
|
||||
:root { color: #1c1915; background: #f7f3ea; }
|
||||
#app { max-width: 44rem; margin: 0 auto; padding: 1.25rem; font: 16px/1.65 "Segoe UI", "Source Han Sans SC", sans-serif; }
|
||||
#tabs { display: flex; gap: 0.4rem; margin-bottom: 0.9rem; }
|
||||
#tabs button { border: 0; border-bottom: 2px solid transparent; background: transparent; padding: 0.35rem 0.6rem; font: inherit; color: inherit; }
|
||||
#tabs button[aria-current="true"] { border-bottom-color: #8a5a2b; }
|
||||
|
||||
js:
|
||||
var pages = [];
|
||||
var current = 0;
|
||||
present.onData(function (data) {
|
||||
var blocks = (data && data.blocks) || {};
|
||||
putText("heading", blocks.heading);
|
||||
pages = Array.isArray(blocks.pages) ? blocks.pages : [];
|
||||
current = 0;
|
||||
drawTabs();
|
||||
drawPage();
|
||||
});
|
||||
function drawTabs() {
|
||||
var el = document.getElementById("tabs");
|
||||
el.replaceChildren();
|
||||
pages.forEach(function (page, index) {
|
||||
var button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.textContent = page.name;
|
||||
if (index === current) button.setAttribute("aria-current", "true");
|
||||
button.addEventListener("click", function () {
|
||||
current = index;
|
||||
drawTabs();
|
||||
drawPage();
|
||||
});
|
||||
el.appendChild(button);
|
||||
});
|
||||
}
|
||||
function drawPage() {
|
||||
var el = document.getElementById("page");
|
||||
el.textContent = pages[current] ? pages[current].text : "";
|
||||
}
|
||||
function putText(id, value) {
|
||||
var el = document.getElementById(id);
|
||||
if (!el) return;
|
||||
if (value == null || value === "") { el.remove(); return; }
|
||||
el.textContent = String(value);
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,66 @@
|
||||
# 成稿
|
||||
|
||||
## intro
|
||||
|
||||
```intro
|
||||
一次只放一份能单独拿走的成稿。成稿内部的小节在创作时按这一局的范例重排。选项贴在成稿下面,始终直接可见。
|
||||
```
|
||||
|
||||
## layout
|
||||
|
||||
```layout
|
||||
id: piece
|
||||
主区: 一份成稿
|
||||
可加窄带: 文字选项
|
||||
摆放:
|
||||
成稿: 居中。内部小节在创作时按范例重写
|
||||
文字选项: 成稿之后,始终直接可见
|
||||
未启用的区不留空位。
|
||||
```
|
||||
|
||||
## note
|
||||
|
||||
```note
|
||||
不替换:一份成稿容器居中。选项在容器之后,直接可见。
|
||||
可替换:容器内部的标记、分段、配色和字体。按本局这一份成品重写内部。样本内部与本局不同时整段替换,保留容器和选项的前后关系。
|
||||
```
|
||||
|
||||
## sample
|
||||
|
||||
```sample
|
||||
html:
|
||||
<div id="app">
|
||||
<article id="piece"></article>
|
||||
<ol id="options"></ol>
|
||||
</div>
|
||||
|
||||
css:
|
||||
:root { color: #1c1916; background: #ebe6dc; }
|
||||
#app { max-width: 34rem; margin: 0 auto; padding: 1.6rem 1.25rem 2rem; font: 16px/1.65 "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif; }
|
||||
#piece { margin: 0; }
|
||||
#options { margin: 1rem 0 0; padding-left: 1.15rem; }
|
||||
|
||||
js:
|
||||
present.onData(function (data) {
|
||||
var blocks = (data && data.blocks) || {};
|
||||
putText("piece", blocks.piece);
|
||||
putList("options", blocks.options);
|
||||
});
|
||||
function putText(id, value) {
|
||||
var el = document.getElementById(id);
|
||||
if (!el) return;
|
||||
if (value == null || value === "") { el.remove(); return; }
|
||||
el.textContent = String(value);
|
||||
}
|
||||
function putList(id, value) {
|
||||
var el = document.getElementById(id);
|
||||
if (!el) return;
|
||||
el.replaceChildren();
|
||||
if (!Array.isArray(value) || !value.length) { el.remove(); return; }
|
||||
value.forEach(function (line) {
|
||||
var li = document.createElement("li");
|
||||
li.textContent = String(line);
|
||||
el.appendChild(li);
|
||||
});
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,83 @@
|
||||
# 阅读
|
||||
|
||||
## intro
|
||||
|
||||
```intro
|
||||
只展示正文。抬头、文字选项需要时贴在正文前后。本局没有的区从骨架删除。
|
||||
```
|
||||
|
||||
## layout
|
||||
|
||||
```layout
|
||||
id: reading
|
||||
主区: 正文
|
||||
可加窄带: 抬头、文字选项
|
||||
摆放:
|
||||
抬头: 正文之前的一行短内容
|
||||
正文: 阅读柱,占主要宽度
|
||||
文字选项: 正文之后,始终直接可见,不放进标签或折叠。不显示「文字选项」这个区名
|
||||
未启用的区不留空位。
|
||||
```
|
||||
|
||||
## note
|
||||
|
||||
```note
|
||||
不替换:单栏阅读柱。抬头在正文前,独占一行。选项在正文后,直接可见。
|
||||
可替换:抬头文字、正文文字、配色和字体。本局没有抬头时删掉抬头。
|
||||
```
|
||||
|
||||
## sample
|
||||
|
||||
```sample
|
||||
html:
|
||||
<div id="app">
|
||||
<p id="heading"></p>
|
||||
<article id="body"></article>
|
||||
<ol id="options"></ol>
|
||||
</div>
|
||||
|
||||
css:
|
||||
:root { color: #1c1915; background: #f7f3ea; }
|
||||
#app { max-width: 38rem; margin: 0 auto; padding: 1.5rem 1.25rem 2.5rem; font: 17px/1.75 "Palatino Linotype", "Songti SC", serif; }
|
||||
#heading { margin: 0 0 0.85rem; font-size: 0.95rem; letter-spacing: 0.04em; }
|
||||
#body { font-size: 1.05rem; }
|
||||
#options { margin: 1.25rem 0 0; padding-left: 1.2rem; }
|
||||
|
||||
js:
|
||||
present.onData(function (data) {
|
||||
var blocks = (data && data.blocks) || {};
|
||||
putHeading("heading", blocks.heading);
|
||||
putText("body", blocks.body);
|
||||
putList("options", blocks.options);
|
||||
});
|
||||
function putHeading(id, value) {
|
||||
var el = document.getElementById(id);
|
||||
if (!el) return;
|
||||
el.replaceChildren();
|
||||
if (value == null || value === "") { el.remove(); return; }
|
||||
if (typeof value === "string") { el.textContent = value; return; }
|
||||
Object.keys(value).forEach(function (key) {
|
||||
var item = document.createElement("span");
|
||||
item.textContent = key + " " + String(value[key]);
|
||||
item.style.marginRight = "1rem";
|
||||
el.appendChild(item);
|
||||
});
|
||||
}
|
||||
function putText(id, value) {
|
||||
var el = document.getElementById(id);
|
||||
if (!el) return;
|
||||
if (value == null || value === "") { el.remove(); return; }
|
||||
el.textContent = String(value);
|
||||
}
|
||||
function putList(id, value) {
|
||||
var el = document.getElementById(id);
|
||||
if (!el) return;
|
||||
el.replaceChildren();
|
||||
if (!Array.isArray(value) || !value.length) { el.remove(); return; }
|
||||
value.forEach(function (line) {
|
||||
var li = document.createElement("li");
|
||||
li.textContent = String(line);
|
||||
el.appendChild(li);
|
||||
});
|
||||
}
|
||||
```
|
||||
@@ -22,6 +22,6 @@ presentation_hints:
|
||||
prompt_excerpt: |
|
||||
你是章节写手。只写本轮要求的一段/一章,对照大纲节点兑现承诺。
|
||||
输出进 正文.当前段;若需归档到 正文.已完成 按声明 outputs 执行。
|
||||
用户本轮输入视为待扩写为事实的大纲:细化完全后写成正文;剧情不超出大纲未涉及处,完整演绎大纲内容。
|
||||
用户本轮输入是待演成场面的大纲,不是终稿句子。已写明的人、事、话、结果保持不变,每个节拍都要演到。节拍之间补具体动作、物件和当场的小纠正,把场合里已有的处境演出来;大纲没写这些动作也要写。例:「我去相亲、坐了下来」——坐下前写先要一杯咖啡,又急忙改成两杯,显出局促。只加形容词、比喻或气氛、不增加动作,不算扩写。另起大纲没有的冲突、新目标,或改掉已定结果,才算出界。
|
||||
正文采用完整长自然段叙述,不要单句成段;篇幅默认约 1000~2000 字(用户或回复呈现另有规定则从其)。
|
||||
不要重写整本;不要发明与大纲冲突的主线转折,除非用户本轮明确要求。
|
||||
|
||||
@@ -42,8 +42,9 @@ prompt_excerpt: |
|
||||
用户主读正文(body)默认 1000~2000 字(汉字);以「设计.监控栏」回复呈现已钉字数为准。
|
||||
采用完整长自然段叙述,不要单句成段;不要几句短信就结束。
|
||||
用户输入用法以「设计.故事推进」或「设计.叙事指南与故事推进」为准:
|
||||
- 大纲扩写:把主世界正文当事实底稿,细化文风后写完整终稿;不超出底稿未涉及的剧情;一次成篇,勿停等用户行动。
|
||||
- 扮演停笔:仅当故事推进(或合并技能)明确钉了此用法时——终稿停在其他角色等用户角色回应之刻;不可写用户角色未给出的其它行为/对话。
|
||||
- 未钉故事推进/叙事指南时:按大纲扩写,禁止默认扮演停笔。
|
||||
- 大纲扩写:把主世界正文当事实底稿写终稿。底稿里节拍之间的具体动作原样保留;文风只改遣词与节奏,不要把这些动作收成形容词。不另补底稿没有的动作,不改底稿已有的人、事、话、结果。续写短;用户角色实操仅保留底稿已有,续写不加代控。
|
||||
- 续写:仅当故事推进(或合并技能)明确钉了此用法时——终稿停在其他角色等用户角色回应之刻;不可写用户角色未给出的其它行为/对话。
|
||||
- 自动写入:续写宜长;可按主角设定保留/补全用户角色言行(与底稿、人设一致)。
|
||||
- 未钉故事推进/叙事指南时:按大纲扩写,禁止默认续写或自动写入。
|
||||
禁止在正文里写出 tag 名(「输出.用户展示」)、压缩摘要或过程日志。
|
||||
禁止编造主世界正文中没有的事实。
|
||||
|
||||
@@ -31,9 +31,10 @@ prompt_excerpt: |
|
||||
若本局启用了叙事转述:你仍写完整(或尽量完整)的场面正文;文风润色、残稿补全、前端壳由转述处理。不要改成「只交事实包」。
|
||||
真值/表字段变更不靠你交结构化包:有旁观维护时由副 LLM 根据本轮正文改表;若正文组成钉了隐藏段维护语句,可按约定写短键值,否则不要为维护打断叙述。
|
||||
用户输入用法以「设计.故事推进」或「设计.叙事指南与故事推进」为准:
|
||||
- 钉了「大纲扩写」:本轮输入=待扩写大纲/场面要点;一次交完整篇正文,勿停在等用户行动处。
|
||||
- 钉了「扮演停笔」:才只推进用户已声明行为/对话,并停在需用户回应之刻。
|
||||
- 未钉故事推进/叙事指南时:按大纲扩写处理,禁止默认扮演停笔。
|
||||
- 钉了「大纲扩写」:本轮输入是待演成场面的大纲,不是终稿句子。已写明的人、事、话、结果保持不变,每个节拍都要演到。节拍之间补具体动作、物件和当场的小纠正,把场合里已有的处境演出来;大纲没写这些动作也要写。例:「我去相亲、坐了下来」——坐下前可以写先跟服务员要一杯咖啡,话出口又急忙改成两杯,用来显出局促;后面她提出的要求和拒绝的结果不动。只加形容词、比喻或气氛、不增加动作,不算拓写。另起大纲没有的冲突、新目标,或改掉已定结果,才算出界。设计文若写「不发明未涉及的剧情」,指的是这一种出界,不是禁止节拍之间的具体动作。续写段宜短且不可代写用户角色行动/说话;演绎后停在抉择点(配方另钉一次收束则从其配方)。
|
||||
- 钉了「续写」:拓写跟用户已声明行为/对话;续写可推进场面但用户角色仅被动占位;禁止替用户角色做决定;停在需用户回应之刻。
|
||||
- 钉了「自动写入」:短需求驱动;续写宜长;可按主角设定代写用户角色言行。
|
||||
- 未钉故事推进/叙事指南时:按大纲扩写处理,禁止默认续写或自动写入。
|
||||
需要随机数时:只用本轮上下文里程序已插入的备用随机数(见「设计.随机范围整理」);禁止口算编造。
|
||||
不要把未投影的分档长文整表塞进输出;只写本轮可见后果。
|
||||
禁止在正文里写出 tag 名、压缩摘要或过程日志。
|
||||
|
||||
@@ -0,0 +1,381 @@
|
||||
import {
|
||||
catalogModulesForIntake,
|
||||
type ModuleCatalog,
|
||||
type ModuleCatalogEntry,
|
||||
} from "../skills/creation-flow.js";
|
||||
import type { DictateProduct } from "./types.js";
|
||||
|
||||
export const CREATION_PLAN_TAG = "设计.本局创作方案";
|
||||
export const CREATION_PLAN_SCHEMA = "creation-plan.v1";
|
||||
|
||||
export const CREATION_PLAN_TIERS = [
|
||||
"必须",
|
||||
"有必要",
|
||||
"有一定效果",
|
||||
"没有意义",
|
||||
] as const;
|
||||
|
||||
export type CreationPlanTier = (typeof CREATION_PLAN_TIERS)[number];
|
||||
export type CreationPresentationMode = "message" | "zero_layer";
|
||||
|
||||
const PRESENTATION_MODULE_IDS: Record<
|
||||
CreationPresentationMode,
|
||||
readonly string[]
|
||||
> = {
|
||||
message: ["status-bar", "reply-format", "opening-setup"],
|
||||
zero_layer: [
|
||||
"zero-layer-status",
|
||||
"zero-layer-reply-format",
|
||||
"zero-layer-opening-setup",
|
||||
],
|
||||
};
|
||||
const ALL_PRESENTATION_MODULE_IDS = new Set(
|
||||
Object.values(PRESENTATION_MODULE_IDS).flat(),
|
||||
);
|
||||
|
||||
export type CreationPlanItem = {
|
||||
module_id: string;
|
||||
tier: CreationPlanTier;
|
||||
why: string;
|
||||
missing: string;
|
||||
instances: unknown[];
|
||||
};
|
||||
|
||||
export type CreationPlan = {
|
||||
schema: typeof CREATION_PLAN_SCHEMA;
|
||||
技能: string;
|
||||
brief: string;
|
||||
/** 缺省兼容旧方案,按普通消息楼层处理。 */
|
||||
presentation_mode?: CreationPresentationMode;
|
||||
recipe: {
|
||||
primary: string;
|
||||
secondary_traits: string[];
|
||||
method: string[];
|
||||
references: string[];
|
||||
};
|
||||
items: CreationPlanItem[];
|
||||
};
|
||||
|
||||
/**
|
||||
* 本局方案只覆盖对话落盘中的内容能力。旧 DAG 收成节点、机械映射节点和方案自身不参与。
|
||||
* 新内容能力默认纳入;程序步(auto)默认排除。
|
||||
*/
|
||||
export const CREATION_PLAN_EXCLUDED_MODULE_IDS = new Set([
|
||||
"creation-plan",
|
||||
"narrative",
|
||||
"variable-context",
|
||||
"context-order",
|
||||
"worker-spec",
|
||||
"refine",
|
||||
]);
|
||||
|
||||
export function creationPlanModules(
|
||||
catalog: ModuleCatalog | null | undefined,
|
||||
presentationMode: CreationPresentationMode = "message",
|
||||
): ModuleCatalogEntry[] {
|
||||
if (!catalog) return [];
|
||||
const selectedPresentation = new Set(
|
||||
PRESENTATION_MODULE_IDS[presentationMode],
|
||||
);
|
||||
return catalogModulesForIntake(catalog, "dictate").filter(
|
||||
(module) =>
|
||||
!module.auto &&
|
||||
!CREATION_PLAN_EXCLUDED_MODULE_IDS.has(module.id) &&
|
||||
(!ALL_PRESENTATION_MODULE_IDS.has(module.id) ||
|
||||
selectedPresentation.has(module.id)),
|
||||
);
|
||||
}
|
||||
|
||||
function presentationModeOf(
|
||||
plan: Pick<CreationPlan, "presentation_mode">,
|
||||
): CreationPresentationMode {
|
||||
return plan.presentation_mode === "zero_layer" ? "zero_layer" : "message";
|
||||
}
|
||||
|
||||
export function parseCreationPlan(raw: string | undefined | null): CreationPlan | null {
|
||||
if (!raw?.trim()) return null;
|
||||
try {
|
||||
const doc = JSON.parse(raw) as unknown;
|
||||
if (!doc || typeof doc !== "object" || Array.isArray(doc)) return null;
|
||||
const row = doc as Record<string, unknown>;
|
||||
if (row.schema !== CREATION_PLAN_SCHEMA || !Array.isArray(row.items)) {
|
||||
return null;
|
||||
}
|
||||
const recipe = row.recipe;
|
||||
if (
|
||||
typeof row.技能 !== "string" ||
|
||||
typeof row.brief !== "string" ||
|
||||
(row.presentation_mode !== undefined &&
|
||||
row.presentation_mode !== "message" &&
|
||||
row.presentation_mode !== "zero_layer") ||
|
||||
!recipe ||
|
||||
typeof recipe !== "object" ||
|
||||
Array.isArray(recipe)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const recipeRow = recipe as Record<string, unknown>;
|
||||
if (
|
||||
typeof recipeRow.primary !== "string" ||
|
||||
!isStringArray(recipeRow.secondary_traits) ||
|
||||
!isStringArray(recipeRow.method) ||
|
||||
!isStringArray(recipeRow.references)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const validItems = row.items.every((item) => {
|
||||
if (!item || typeof item !== "object" || Array.isArray(item)) return false;
|
||||
const value = item as Record<string, unknown>;
|
||||
return (
|
||||
typeof value.module_id === "string" &&
|
||||
CREATION_PLAN_TIERS.includes(value.tier as CreationPlanTier) &&
|
||||
typeof value.why === "string" &&
|
||||
typeof value.missing === "string" &&
|
||||
Array.isArray(value.instances)
|
||||
);
|
||||
});
|
||||
if (!validItems) return null;
|
||||
return row as CreationPlan;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export type CreationPlanValidation = {
|
||||
ok: boolean;
|
||||
missingIds: string[];
|
||||
duplicateIds: string[];
|
||||
unknownIds: string[];
|
||||
error?: string;
|
||||
plan?: CreationPlan;
|
||||
};
|
||||
|
||||
export function validateCreationPlan(
|
||||
raw: string,
|
||||
catalog: ModuleCatalog | null | undefined,
|
||||
): CreationPlanValidation {
|
||||
const plan = parseCreationPlan(raw);
|
||||
if (!plan) {
|
||||
return {
|
||||
ok: false,
|
||||
missingIds: creationPlanModules(catalog).map((m) => m.id),
|
||||
duplicateIds: [],
|
||||
unknownIds: [],
|
||||
error: `本局创作方案必须是合法的 ${CREATION_PLAN_SCHEMA} JSON,且含 items 数组`,
|
||||
};
|
||||
}
|
||||
|
||||
const presentationMode = presentationModeOf(plan);
|
||||
const required = creationPlanModules(catalog, presentationMode).map((m) => m.id);
|
||||
if (required.length === 0) {
|
||||
return {
|
||||
ok: false,
|
||||
missingIds: [],
|
||||
duplicateIds: [],
|
||||
unknownIds: [],
|
||||
error: "能力目录不可用,无法校验本局创作方案",
|
||||
};
|
||||
}
|
||||
|
||||
const ids = plan.items
|
||||
.map((item) => String(item?.module_id ?? "").trim())
|
||||
.filter(Boolean);
|
||||
const counts = new Map<string, number>();
|
||||
for (const id of ids) counts.set(id, (counts.get(id) ?? 0) + 1);
|
||||
|
||||
const allowed = new Set(required);
|
||||
const missingIds = required.filter((id) => !counts.has(id));
|
||||
const duplicateIds = [...counts]
|
||||
.filter(([, count]) => count > 1)
|
||||
.map(([id]) => id);
|
||||
const unknownIds = [...counts.keys()].filter((id) => !allowed.has(id));
|
||||
const invalidTierIds = plan.items
|
||||
.filter(
|
||||
(item) =>
|
||||
!CREATION_PLAN_TIERS.includes(item?.tier as CreationPlanTier),
|
||||
)
|
||||
.map((item) => String(item?.module_id ?? "(空 id)"));
|
||||
|
||||
const problems: string[] = [];
|
||||
if (missingIds.length) problems.push(`漏项:${missingIds.join("、")}`);
|
||||
if (duplicateIds.length) problems.push(`重复:${duplicateIds.join("、")}`);
|
||||
if (unknownIds.length) problems.push(`未知:${unknownIds.join("、")}`);
|
||||
if (invalidTierIds.length) {
|
||||
problems.push(`档位无效:${invalidTierIds.join("、")}`);
|
||||
}
|
||||
|
||||
return {
|
||||
ok: problems.length === 0,
|
||||
missingIds,
|
||||
duplicateIds,
|
||||
unknownIds,
|
||||
...(problems.length ? { error: `本局创作方案能力覆盖校验失败;${problems.join(";")}` } : {}),
|
||||
plan,
|
||||
};
|
||||
}
|
||||
|
||||
export function formatCreationPlanCapabilityCatalog(
|
||||
catalog: ModuleCatalog | null | undefined,
|
||||
): string {
|
||||
const allContent = catalogModulesForIntake(catalog ?? { modules: [] }, "dictate").filter(
|
||||
(module) =>
|
||||
!module.auto && !CREATION_PLAN_EXCLUDED_MODULE_IDS.has(module.id),
|
||||
);
|
||||
const formatModule = (module: ModuleCatalogEntry) => {
|
||||
const boundary = compact(module.boundary ?? "", 180);
|
||||
return [
|
||||
`- ${module.id}(${module.name}):${compact(module.declaration, 120)}`,
|
||||
...(boundary ? [` 边界:${boundary}`] : []),
|
||||
].join("\n");
|
||||
};
|
||||
const commonLines = allContent
|
||||
.filter((module) => !ALL_PRESENTATION_MODULE_IDS.has(module.id))
|
||||
.map(formatModule);
|
||||
const byId = new Map(allContent.map((module) => [module.id, module]));
|
||||
const modeLines = (mode: CreationPresentationMode) =>
|
||||
PRESENTATION_MODULE_IDS[mode]
|
||||
.map((id) => byId.get(id))
|
||||
.filter((module): module is ModuleCatalogEntry => Boolean(module))
|
||||
.map(formatModule);
|
||||
if (commonLines.length === 0) return "";
|
||||
return [
|
||||
"【本局创作方案 · 能力目录】",
|
||||
"先选择 presentation_mode=`message|zero_layer`。items 必须覆盖全部公共能力,并且只覆盖所选呈现模式的三项能力;不得同时选两套。分档看能力对抽出的核心体验和可运行性的贡献:用户没点名但缺了体验会明显变差的,标进「必须」或「有必要」。数组顺序不代表执行顺序。",
|
||||
"【公共能力】",
|
||||
commonLines.join("\n"),
|
||||
"【message · 普通消息楼层】",
|
||||
modeLines("message").join("\n"),
|
||||
"【zero_layer · 持久 0 层卡】",
|
||||
modeLines("zero_layer").join("\n"),
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
export function formatCreationPlanProgress(params: {
|
||||
plan: CreationPlan;
|
||||
catalog: ModuleCatalog | null | undefined;
|
||||
products: readonly DictateProduct[];
|
||||
}): string {
|
||||
const modules = new Map(
|
||||
creationPlanModules(
|
||||
params.catalog,
|
||||
presentationModeOf(params.plan),
|
||||
).map((module) => [module.id, module]),
|
||||
);
|
||||
const productTags = params.products.map((product) => product.tag);
|
||||
const productByTag = new Map(params.products.map((product) => [product.tag, product]));
|
||||
const groups = new Map<CreationPlanTier, string[]>(
|
||||
CREATION_PLAN_TIERS.map((tier) => [tier, []]),
|
||||
);
|
||||
|
||||
for (const item of params.plan.items) {
|
||||
const module = modules.get(item.module_id);
|
||||
if (!module || !CREATION_PLAN_TIERS.includes(item.tier)) continue;
|
||||
const landedTags = productTags.filter(
|
||||
(tag) => tag === module.artifact || tag.startsWith(`${module.artifact}#`),
|
||||
);
|
||||
const scores = landedTags
|
||||
.map((tag) => {
|
||||
const product = productByTag.get(tag);
|
||||
const metadataScores = product?.selfScore?.dims.map((dim) => dim.score) ?? [];
|
||||
return metadataScores.length
|
||||
? Math.min(...metadataScores)
|
||||
: lowestSelfScore(product?.content);
|
||||
})
|
||||
.filter((score): score is number => score !== undefined);
|
||||
const scoreText = scores.length ? `,最低自评分 ${Math.min(...scores)}` : "";
|
||||
const state = landedTags.length ? `已落${scoreText}` : "未落";
|
||||
const missing = item.missing?.trim() ? `;缺口:${item.missing.trim()}` : "";
|
||||
const lines = [
|
||||
`- ${item.module_id}(${module.name})〔${state}〕:${item.why}${missing}`,
|
||||
];
|
||||
if (item.tier !== "没有意义") {
|
||||
const when = oneLine(module.when ?? "");
|
||||
const whenNot = oneLine(module.when_not ?? "");
|
||||
if (when) lines.push(` 何时用:${when}`);
|
||||
if (whenNot) lines.push(` 何时不用:${whenNot}`);
|
||||
}
|
||||
groups.get(item.tier)!.push(...lines);
|
||||
}
|
||||
|
||||
return [
|
||||
"【本局创作方案 · 当前决策】",
|
||||
JSON.stringify(
|
||||
{
|
||||
schema: params.plan.schema,
|
||||
brief: params.plan.brief,
|
||||
presentation_mode: presentationModeOf(params.plan),
|
||||
recipe: params.plan.recipe,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"",
|
||||
"【能力状态】未落的「必须」和「有必要」是收口前的核对清单,不是本轮待办。前三档附带何时用 / 何时不用,方案改写或补全后仍对照它们决定本轮写哪项;可反复能力已落一条也保留。「没有意义」只列名称,不附使用条件。",
|
||||
...CREATION_PLAN_TIERS.flatMap((tier) => [
|
||||
`### ${tier}`,
|
||||
...(groups.get(tier)?.length ? groups.get(tier)! : ["- (无)"]),
|
||||
]),
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
export function requiredCreationPlanGaps(params: {
|
||||
planRaw: string | undefined;
|
||||
catalog: ModuleCatalog | null | undefined;
|
||||
products: readonly DictateProduct[];
|
||||
}): string[] {
|
||||
const plan = parseCreationPlan(params.planRaw);
|
||||
if (!plan) return [];
|
||||
const modules = new Map(
|
||||
creationPlanModules(
|
||||
params.catalog,
|
||||
presentationModeOf(plan),
|
||||
).map((module) => [module.id, module]),
|
||||
);
|
||||
return plan.items
|
||||
.filter((item) => item.tier === "必须")
|
||||
.filter((item) => {
|
||||
const artifact = modules.get(item.module_id)?.artifact;
|
||||
return (
|
||||
!artifact ||
|
||||
!params.products.some(
|
||||
(product) =>
|
||||
product.tag === artifact || product.tag.startsWith(`${artifact}#`),
|
||||
)
|
||||
);
|
||||
})
|
||||
.map((item) => item.module_id);
|
||||
}
|
||||
|
||||
function lowestSelfScore(raw: string | undefined): number | undefined {
|
||||
if (!raw?.trim()) return undefined;
|
||||
try {
|
||||
const doc = JSON.parse(raw) as Record<string, unknown>;
|
||||
const modern = (doc.self_score as { dims?: Array<{ score?: unknown }> } | undefined)
|
||||
?.dims;
|
||||
const legacy = (doc.自评 as { 维度?: Array<{ 分数?: unknown }> } | undefined)
|
||||
?.维度;
|
||||
const values = [
|
||||
...(modern ?? []).map((dim) => dim.score),
|
||||
...(legacy ?? []).map((dim) => dim.分数),
|
||||
]
|
||||
.map(Number)
|
||||
.filter(Number.isFinite);
|
||||
return values.length ? Math.min(...values) : undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function oneLine(text: string): string {
|
||||
return text.replace(/\s+/g, " ").trim();
|
||||
}
|
||||
|
||||
function compact(text: string, max: number): string {
|
||||
const line = oneLine(text);
|
||||
if (line.length <= max) return line;
|
||||
return `${line.slice(0, max - 1)}…`;
|
||||
}
|
||||
|
||||
function isStringArray(value: unknown): value is string[] {
|
||||
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
/**
|
||||
* 体验是否锚定:程序按三件打分,不读模型自评 / 完备度。
|
||||
* 权威字段是产物里的「体验锚定」;缺则回退到美学正文里对应的短句。
|
||||
*/
|
||||
|
||||
import { tryParseJsonDoc } from "../parse/json-doc.js";
|
||||
|
||||
export const AESTHETICS_PRODUCT_TAG = "设计.美学纲领与交互范式";
|
||||
|
||||
export const EXPERIENCE_ANCHOR_RECIPE_NAMES = ["快穿短局", "数据化跑团体验"] as const;
|
||||
|
||||
const STANCES = ["完全代入", "部分代入", "旁观", "操控", "跟着体验"] as const;
|
||||
|
||||
const PLACEHOLDER =
|
||||
/^(未定|待定|无|没有|不知道|不详|未知|待补|待探|仍混沌|核心明确|稍后|以后再说|待写|…+|\.{2,}|TBD|tbd|—+|-+|null|undefined)$/i;
|
||||
|
||||
export type AnchorItemId = "站位" | "核心感觉" | "轮转停点";
|
||||
|
||||
export type ExperienceAnchorItem = {
|
||||
id: AnchorItemId;
|
||||
ok: boolean;
|
||||
/** 通过时记下程序采信的短句 */
|
||||
value?: string;
|
||||
reason: string;
|
||||
};
|
||||
|
||||
export type ExperienceAnchorScore = {
|
||||
total: 3;
|
||||
passed: number;
|
||||
anchored: boolean;
|
||||
items: ExperienceAnchorItem[];
|
||||
};
|
||||
|
||||
export function recipeRequiresExperienceAnchor(recipeName?: string): boolean {
|
||||
const name = recipeName?.trim() ?? "";
|
||||
return (EXPERIENCE_ANCHOR_RECIPE_NAMES as readonly string[]).includes(name);
|
||||
}
|
||||
|
||||
export function isDictateOpeningTag(tag: string): boolean {
|
||||
const t = tag.trim();
|
||||
return (
|
||||
t === "设计.开场白" ||
|
||||
t.startsWith("设计.开场白#") ||
|
||||
t === "设计.开场白与开场变量" ||
|
||||
t === "设计.0层开场白与初态"
|
||||
);
|
||||
}
|
||||
|
||||
export function scoreExperienceAnchor(content: string | undefined | null): ExperienceAnchorScore {
|
||||
const raw = content?.trim() ?? "";
|
||||
const doc = raw ? asRecord(tryParseJsonDoc(raw)) : undefined;
|
||||
const anchor = asRecord(doc?.体验锚定) ?? asRecord(asRecord(doc?.正文)?.体验锚定);
|
||||
const body = asRecord(doc?.正文);
|
||||
|
||||
const stance = scoreStance(
|
||||
firstText(
|
||||
anchor?.站位,
|
||||
dig(body, ["设定逻辑", "参与方式", "用户与user关系"]),
|
||||
dig(body, ["交互范式", "前置配置", "用户与user关系"]),
|
||||
labeled(raw, "站位"),
|
||||
),
|
||||
);
|
||||
const feeling = scoreFeeling(
|
||||
firstText(
|
||||
anchor?.核心感觉,
|
||||
dig(body, ["设定逻辑", "内容维度", "核心感觉", "已知"]),
|
||||
dig(body, ["美学纲领", "体验内核"]),
|
||||
labeled(raw, "核心感觉"),
|
||||
),
|
||||
);
|
||||
const turn = scoreTurnStop(
|
||||
firstText(
|
||||
anchor?.轮转停点,
|
||||
dig(body, ["交互范式", "描写权限", "思考与抉择"]),
|
||||
labeled(raw, "轮转停点"),
|
||||
),
|
||||
);
|
||||
|
||||
const items = [stance, feeling, turn];
|
||||
const passed = items.filter((item) => item.ok).length;
|
||||
return { total: 3, passed, anchored: passed === 3, items };
|
||||
}
|
||||
|
||||
export function formatExperienceAnchorBlock(score: ExperienceAnchorScore): string {
|
||||
const lines = [
|
||||
"## 体验锚定(程序打分,不采用模型自评)",
|
||||
`分数:${score.passed}/${score.total}`,
|
||||
`已锚定:${score.anchored ? "是" : "否"}`,
|
||||
];
|
||||
for (const item of score.items) {
|
||||
const shown = item.ok && item.value ? `(${clip(item.value)})` : `(${item.reason})`;
|
||||
lines.push(`- ${item.id}:${item.ok ? "通过" : "未通过"}${shown}`);
|
||||
}
|
||||
lines.push(
|
||||
score.anchored
|
||||
? "3/3:各能力何时用里的「已有美学」成立。开场白仍须等其它何时用已成立的能力落完。"
|
||||
: "未满 3/3:「已有美学」不成立。本轮只把缺口写进「设计.美学纲领与交互范式」的「体验锚定」(站位、核心感觉、轮转停点)。不得宣称已锚定,不得 insert 开场白。",
|
||||
);
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
export function experienceAnchorToolPayload(score: ExperienceAnchorScore): {
|
||||
passed: number;
|
||||
total: 3;
|
||||
anchored: boolean;
|
||||
missing: AnchorItemId[];
|
||||
} {
|
||||
return {
|
||||
passed: score.passed,
|
||||
total: 3,
|
||||
anchored: score.anchored,
|
||||
missing: score.items.filter((item) => !item.ok).map((item) => item.id),
|
||||
};
|
||||
}
|
||||
|
||||
export function openingBlockedByAnchorMessage(score: ExperienceAnchorScore): string {
|
||||
const missing = score.items.filter((item) => !item.ok).map((item) => item.id);
|
||||
return `拒绝写入开场白:体验锚定 ${score.passed}/${score.total},缺 ${missing.join("、") || "三件"}。先补「设计.美学纲领与交互范式」的「体验锚定」,再写开场。`;
|
||||
}
|
||||
|
||||
function scoreStance(raw: string | undefined): ExperienceAnchorItem {
|
||||
const id: AnchorItemId = "站位";
|
||||
const t = clean(raw);
|
||||
if (!t) return { id, ok: false, reason: "空或未定" };
|
||||
if (/还是|或者/.test(t)) return { id, ok: false, reason: "还是问句,没有选定" };
|
||||
const hit = STANCES.find((s) => t === s || t.startsWith(s));
|
||||
if (hit) return { id, ok: true, value: hit, reason: "选定" };
|
||||
if (t.includes("棋子") || t.includes("观察")) {
|
||||
return { id, ok: true, value: "旁观", reason: "选定" };
|
||||
}
|
||||
return { id, ok: false, reason: "须是完全代入、部分代入、旁观、操控之一" };
|
||||
}
|
||||
|
||||
function scoreFeeling(raw: string | undefined): ExperienceAnchorItem {
|
||||
const id: AnchorItemId = "核心感觉";
|
||||
const t = clean(raw);
|
||||
if (!t || t.length < 4) return { id, ok: false, reason: "空、未定,或短于一句" };
|
||||
return { id, ok: true, value: t, reason: "有一句" };
|
||||
}
|
||||
|
||||
function scoreTurnStop(raw: string | undefined): ExperienceAnchorItem {
|
||||
const id: AnchorItemId = "轮转停点";
|
||||
const t = clean(raw);
|
||||
if (!t || t.length < 8) return { id, ok: false, reason: "空、未定,或没写清何时停" };
|
||||
if (!/停|等/.test(t)) return { id, ok: false, reason: "没写停笔或等用户" };
|
||||
return { id, ok: true, value: t, reason: "写了何时停" };
|
||||
}
|
||||
|
||||
function clean(raw: string | undefined): string {
|
||||
const t = raw?.trim().replace(/\s+/g, " ") ?? "";
|
||||
if (!t || PLACEHOLDER.test(t)) return "";
|
||||
return t;
|
||||
}
|
||||
|
||||
function firstText(...candidates: unknown[]): string | undefined {
|
||||
for (const candidate of candidates) {
|
||||
const text = textOf(candidate);
|
||||
if (text) return text;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function textOf(value: unknown): string | undefined {
|
||||
if (typeof value === "string") {
|
||||
const t = value.trim();
|
||||
return t || undefined;
|
||||
}
|
||||
const row = asRecord(value);
|
||||
if (!row) return undefined;
|
||||
for (const key of ["结论", "已知", "体验内核"]) {
|
||||
if (typeof row[key] === "string" && row[key].trim()) return row[key].trim();
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function labeled(raw: string, label: string): string | undefined {
|
||||
if (!raw || raw.startsWith("{") || raw.startsWith("[")) return undefined;
|
||||
const re = new RegExp(`(?:^|\\n)\\s*${label}\\s*[::]\\s*([^\\n]+)`);
|
||||
return re.exec(raw)?.[1]?.trim();
|
||||
}
|
||||
|
||||
function dig(root: Record<string, unknown> | undefined, path: string[]): unknown {
|
||||
let cur: unknown = root;
|
||||
for (const key of path) {
|
||||
const row = asRecord(cur);
|
||||
if (!row) return undefined;
|
||||
cur = row[key];
|
||||
}
|
||||
return cur;
|
||||
}
|
||||
|
||||
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
||||
return value as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function clip(text: string): string {
|
||||
const t = text.trim();
|
||||
return t.length > 36 ? `${t.slice(0, 36)}…` : t;
|
||||
}
|
||||
@@ -2,7 +2,9 @@ export { buildDictateSystemPrompt, DICTATE_SYSTEM_PROMPT } from "./prompt.js";
|
||||
export {
|
||||
CREATION_INTAKE_MODE_TAG,
|
||||
DICTATE_MODE_VALUE,
|
||||
DICTATE_LAYER_META_KEY,
|
||||
DICTATE_ORDER_META_KEY,
|
||||
DICTATE_SELF_SCORE_META_KEY,
|
||||
isDictateModeValue,
|
||||
isAllowedProductTag,
|
||||
defaultDictateOrder,
|
||||
@@ -11,6 +13,7 @@ export {
|
||||
sortDictateProducts,
|
||||
parseDictateOrder,
|
||||
type DictateProduct,
|
||||
type DictateSelfScore,
|
||||
type DictateChatTurn,
|
||||
} from "./types.js";
|
||||
export {
|
||||
@@ -19,6 +22,40 @@ export {
|
||||
} from "./context.js";
|
||||
export { DICTATE_TOOL_DEFINITIONS } from "./tools.js";
|
||||
export { runDictateTurn, type DictateTurnResult } from "./turn.js";
|
||||
export {
|
||||
CREATION_PLAN_TAG,
|
||||
CREATION_PLAN_SCHEMA,
|
||||
CREATION_PLAN_TIERS,
|
||||
CREATION_PLAN_EXCLUDED_MODULE_IDS,
|
||||
creationPlanModules,
|
||||
parseCreationPlan,
|
||||
validateCreationPlan,
|
||||
formatCreationPlanCapabilityCatalog,
|
||||
formatCreationPlanProgress,
|
||||
requiredCreationPlanGaps,
|
||||
type CreationPlan,
|
||||
type CreationPlanItem,
|
||||
type CreationPresentationMode,
|
||||
type CreationPlanTier,
|
||||
type CreationPlanValidation,
|
||||
} from "./creation-plan.js";
|
||||
export {
|
||||
DICTATE_MODULE_READ_STATE_TAG,
|
||||
buildDictateModuleReadIndex,
|
||||
parseDictateModuleReadState,
|
||||
serializeDictateModuleReadState,
|
||||
readLibraryEntries,
|
||||
type DictateModuleReadPayload,
|
||||
type DictateModuleReadState,
|
||||
} from "./read-module.js";
|
||||
export {
|
||||
AESTHETICS_PRODUCT_TAG,
|
||||
scoreExperienceAnchor,
|
||||
formatExperienceAnchorBlock,
|
||||
recipeRequiresExperienceAnchor,
|
||||
isDictateOpeningTag,
|
||||
type ExperienceAnchorScore,
|
||||
} from "./experience-anchor.js";
|
||||
export {
|
||||
DICTATE_INSERT_TAG_ALIASES,
|
||||
DICTATE_MULTI_MODULE_REPLY_HINT,
|
||||
@@ -48,6 +85,7 @@ export {
|
||||
mergeGenerationRulesArtifacts,
|
||||
resolveRepeatableInsertTag,
|
||||
sanitizeRepeatableSlot,
|
||||
splitConcreteInstanceContents,
|
||||
} from "./repeatable-tags.js";
|
||||
export {
|
||||
VARIABLE_CATALOG_TAG,
|
||||
|
||||
@@ -22,7 +22,7 @@ export const DICTATE_INSERT_TAG_ALIASES: Readonly<Record<string, string>> = {
|
||||
|
||||
/** 多模块同轮:写进每条 insert tool result 的 reply_hint */
|
||||
export const DICTATE_MULTI_MODULE_REPLY_HINT =
|
||||
"本轮带回了多个 reply_module。末尾可见回复:每个模块各写一块 markdown,标题用 ## 模块 · {名称}(与该 reply_module 一致);块间空一行;每块只写该模块的确认 / 薄弱点 / 追问。全部模块写完后,可另起一段写 1 条「建议下一刀」(条件须已接近成立)。缺某一块则补上后再结束本轮。";
|
||||
"本轮写入了多个产物。末尾用一段自然回复综合说明本轮变化、共同强项与关键缺口;不要逐模块套标题或重复模板。只有缺口确需用户决定时才提问。";
|
||||
|
||||
const MAX_PROBE_CHARS = 1200;
|
||||
const MAX_PRINCIPLES_CHARS = 900;
|
||||
@@ -73,6 +73,15 @@ export function extractSelfScoreDimensionNames(outputFence: string): string[] {
|
||||
return names.slice(0, 8);
|
||||
}
|
||||
|
||||
function extractScoreFenceDimensionNames(scoreFence: string): string[] {
|
||||
const names: string[] = [];
|
||||
for (const match of scoreFence.matchAll(/^([^#\-\s][^:\n]{0,30}):\s*$/gm)) {
|
||||
const name = match[1]?.trim();
|
||||
if (name && !names.includes(name)) names.push(name);
|
||||
}
|
||||
return names.slice(0, 8);
|
||||
}
|
||||
|
||||
function clip(text: string, max: number): string {
|
||||
const t = text.trim();
|
||||
if (t.length <= max) return t;
|
||||
@@ -91,7 +100,12 @@ export function formatDictateUserFacingBrief(params: {
|
||||
const probe = getModuleSection(sections, "probe");
|
||||
const principles = getModuleSection(sections, "principles");
|
||||
const output = getModuleSection(sections, "output");
|
||||
const dims = output ? extractSelfScoreDimensionNames(output) : [];
|
||||
const score = getModuleSection(sections, "score");
|
||||
const dims = score
|
||||
? extractScoreFenceDimensionNames(score)
|
||||
: output
|
||||
? extractSelfScoreDimensionNames(output)
|
||||
: [];
|
||||
|
||||
if (!probe && !principles && dims.length === 0) return "";
|
||||
|
||||
@@ -101,19 +115,19 @@ export function formatDictateUserFacingBrief(params: {
|
||||
if (dims.length) includes.push(`自评关注(${dims.join("、")})`);
|
||||
|
||||
const parts: string[] = [
|
||||
`# 模块 · ${params.module.name}`,
|
||||
`# 本轮关注 · ${params.module.name}`,
|
||||
"",
|
||||
`回答时请附带本模块(落盘 tag:\`${params.tag}\`)。`,
|
||||
`用于组织写入 \`${params.tag}\` 后的自然回复,不要求固定标题或独立模块段。`,
|
||||
"",
|
||||
"本模块内容包括:",
|
||||
...includes.map((x) => `- ${x}`),
|
||||
"",
|
||||
"## 写入用户回复时做什么",
|
||||
`1. 用二级标题 \`## 模块 · ${params.module.name}\` 开一段(多模块时本段只写本模块)。`,
|
||||
"2. 确认:2~4 句复述本模块已落要点,让用户能判断对不对。",
|
||||
"3. 薄弱点:对照下方「自评关注」口头点 1 处不够硬的地方(不必交打分表或 JSON)。",
|
||||
"4. 追问:1~2 点;每点给建议选项或短场景,用户可直接采用或改写。材料已够则追问可空,改写「建议下一刀」。",
|
||||
"5. 不粘贴产物全文;不复述 tool JSON。",
|
||||
"1. 自然回应用户本轮值得保留、需要纠正或存在矛盾的内容。",
|
||||
"2. 简要说明形成或改写了什么;完整 JSON 由产物卡展示。",
|
||||
"3. 结合 self_score 说明最强处与关键缺口,不复述整张评分表。",
|
||||
"4. 只有低分缺口确需用户决定时才提问,并给具体选项或短场景。",
|
||||
"5. 同轮多个产物时合并回应,不逐模块套模板;不粘贴产物全文或 tool JSON。",
|
||||
];
|
||||
|
||||
if (probe) {
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
type ContextOrderProjection,
|
||||
type ContextOrderSlot,
|
||||
} from "../skills/context-order.js";
|
||||
import { parseContextFragment } from "../skills/context-fragment.js";
|
||||
import {
|
||||
WORKER_SET_DRAFT_TAG,
|
||||
WORKER_SET_FINAL_TAG,
|
||||
@@ -22,7 +23,6 @@ import {
|
||||
import {
|
||||
defaultPlaySlots,
|
||||
enabledPlaySlotIds,
|
||||
parsePlaySlots,
|
||||
refForSlot,
|
||||
type PlaySlotsConfig,
|
||||
} from "../skills/play-slots.js";
|
||||
@@ -49,6 +49,7 @@ import {
|
||||
const SKIP_BIND_TAGS = new Set([
|
||||
"设计.开场白",
|
||||
"设计.开场白与开场变量",
|
||||
"设计.0层开场白与初态",
|
||||
"设计.worker集",
|
||||
"设计.worker集.草稿",
|
||||
CONTEXT_ORDER_TAG,
|
||||
@@ -71,7 +72,7 @@ function shouldSkipBindTag(tag: string): boolean {
|
||||
|
||||
/** 偏文风/呈现/硬约束:有叙事转述时挂转述;否则仍挂主世界层 */
|
||||
const STYLE_TAG_RE =
|
||||
/叙事指南|故事推进|文风|美学|纲领|禁忌|用户约束|用户需求|示例|模仿|正文组成|回复格式|监控栏|篇幅|结构/;
|
||||
/叙事指南|故事推进|文风|美学|纲领|禁忌|用户约束|用户需求|示例|模仿|正文组成|回复呈现|回复格式|监控栏|篇幅|结构/;
|
||||
|
||||
/** 这些配方开玩时刚需叙事转述(主世界出内容、转述出文风) */
|
||||
const RECIPES_REQUIRE_NARRATOR = new Set(["文本生成器"]);
|
||||
@@ -87,6 +88,11 @@ function isStyleProductTag(tag: string): boolean {
|
||||
return STYLE_TAG_RE.test(tag);
|
||||
}
|
||||
|
||||
function isFrequentlyChangingProduct(product: DictateProduct): boolean {
|
||||
const stability = parseContextFragment(product.content)?.稳变;
|
||||
return stability === "semi" || stability === "volatile";
|
||||
}
|
||||
|
||||
/** 从黑板收集对话落盘产物(有正文的 用户.* / 设计.*) */
|
||||
export function collectDictateBindProducts(board: Blackboard): DictateProduct[] {
|
||||
const products: DictateProduct[] = [];
|
||||
@@ -157,20 +163,34 @@ function buildGmInserts(params: {
|
||||
insert(0, WORKER_PERSONA_REF, "fixed", "槽位人设"),
|
||||
];
|
||||
let o = 1;
|
||||
for (const p of params.products) {
|
||||
if (!params.includeStyle && isStyleProductTag(p.tag)) continue;
|
||||
const included = params.products.filter(
|
||||
(p) => params.includeStyle || !isStyleProductTag(p.tag),
|
||||
);
|
||||
const stable = included.filter((p) => !isFrequentlyChangingProduct(p));
|
||||
const changing = included.filter(isFrequentlyChangingProduct);
|
||||
for (const p of stable) {
|
||||
inserts.push(
|
||||
insert(
|
||||
o++,
|
||||
p.tag,
|
||||
"full",
|
||||
`对话落盘固定产物〔order=${effectiveDictateOrder(p)}〕`,
|
||||
`稳定产物;同频率按重要性序〔order=${effectiveDictateOrder(p)}〕`,
|
||||
),
|
||||
);
|
||||
}
|
||||
inserts.push(
|
||||
insert(o++, DIALOGUE_HISTORY_TAG, "summary", "对话历史(按投影裁剪)"),
|
||||
);
|
||||
for (const p of changing) {
|
||||
inserts.push(
|
||||
insert(
|
||||
o++,
|
||||
p.tag,
|
||||
"full",
|
||||
`较常变化产物;置于历史后〔order=${effectiveDictateOrder(p)}〕`,
|
||||
),
|
||||
);
|
||||
}
|
||||
for (const tag of params.runtimeTags) {
|
||||
inserts.push(
|
||||
insert(
|
||||
@@ -280,7 +300,8 @@ export function buildDictateContextOrder(params: {
|
||||
|
||||
return renumberContextOrder({
|
||||
schema: CONTEXT_ORDER_SCHEMA,
|
||||
brief: "对话落盘落档自动挂载:固定产物 → 对话.历史 → 已有真值/投影 → 本轮输入",
|
||||
brief:
|
||||
"程序按变化频率分组,并在同组内按重要性排列:稳定产物 → 对话.历史 → 较常变化产物与当前状态 → 本轮输入",
|
||||
play_slots: params.playSlots,
|
||||
slots,
|
||||
});
|
||||
|
||||
+42
-80
@@ -6,45 +6,55 @@
|
||||
*/
|
||||
|
||||
export function buildDictateSystemPrompt(opts?: {
|
||||
/** 已有方案 + 分档状态;存在时不再注入全能力目录 */
|
||||
creationPlanState?: string;
|
||||
/** 尚无方案时使用的短能力目录 */
|
||||
capabilityCatalog?: string;
|
||||
/** 尚无方案时可引用的旧配方案例目录 */
|
||||
recipeExamples?: string;
|
||||
/** @deprecated 仅兼容旧调用,作为案例而非已选路径 */
|
||||
recipeName?: string;
|
||||
/** @deprecated 仅兼容旧调用,作为案例而非已选路径 */
|
||||
recipeBrief?: string;
|
||||
/** 来自 modules catalog meta:何时落盘 / insert 目标 */
|
||||
/** @deprecated 使用 capabilityCatalog */
|
||||
moduleGuide?: string;
|
||||
}): string {
|
||||
const recipeBlock =
|
||||
const legacyExample =
|
||||
opts?.recipeName || opts?.recipeBrief
|
||||
? `
|
||||
## 本局配方
|
||||
名称:${opts.recipeName ?? "(未命名)"}
|
||||
${opts.recipeBrief ? `要点:\n${opts.recipeBrief}` : ""}
|
||||
按上述适用与原则整理产物;材料只够做本配方时,就只做本配方范围内的事。
|
||||
能力「何时落盘」以【能力 · 何时落盘】为准;本配方只给顺序骨架,不代替各能力调用条件。
|
||||
【旧配方案例 · 兼容参考】
|
||||
${opts.recipeName ? `- ${opts.recipeName}` : ""}
|
||||
${opts.recipeBrief ?? ""}
|
||||
它只提供可改造的方法,不是用户已选路径,也不规定能力顺序。
|
||||
`
|
||||
: "";
|
||||
const decisionBlock = opts?.creationPlanState?.trim()
|
||||
? `${opts.creationPlanState.trim()}
|
||||
|
||||
const moduleBlock = opts?.moduleGuide?.trim()
|
||||
? `
|
||||
${opts.moduleGuide.trim()}
|
||||
`
|
||||
: "";
|
||||
方案是收口前的核对清单,不是本轮待办。写哪项先对照该能力旁的何时用 / 何时不用;档位只说明值不值得做。条件成立,且用户这句话已经说清的,通常 2~3 个:先 read_module,再连续 insert。说清指用户已经给出这份产物要写的内容,不是能从题材推演出一整份。标成「必须」或「有必要」、但这句话没覆盖到或使用条件不成立的,本轮不写,按已有材料给 2~3 个选项来问,选定或改写后再写入。写开场或结束创作前,列出仍未落的「必须」和「有必要」,问用户哪些还没设计、要补哪几项或明确不要。用户改口或方案明显失效时,先整份覆盖 \`设计.本局创作方案\`;使用条件仍留在能力旁。`
|
||||
: `【当前状态 · 尚无本局创作方案】
|
||||
先抽出用户要的核心体验,再对照能力目录主动标出有帮助的能力,生成 \`creation-plan.v1\`,用 insert 写入 \`设计.本局创作方案\`。先写 \`presentation_mode: "message"|"zero_layer"\`,再覆盖公共能力与该模式对应的三项呈现能力;两套呈现不得同时纳入。分档看能力对这份体验和可运行性的贡献:用户没点名、但缺了体验会明显变差的,标进「必须」或「有必要」,具体还没定的选择写进 missing。方案只表达本局方法、能力档位、理由与缺口,不生成固定流水线。
|
||||
|
||||
return `你是对话落盘创作助手:把用户原话整理成可落盘规格,并用可见回复让用户确认、补洞、选下一刀。作用对象是本局创作上下文(对话可清空,产物保留)。
|
||||
${opts?.capabilityCatalog?.trim() || opts?.moduleGuide?.trim() || "(能力目录暂不可用;目录恢复前不要猜造方案。)"}
|
||||
${opts?.recipeExamples?.trim() ? `\n\n【旧配方案例目录】\n${opts.recipeExamples.trim()}` : ""}
|
||||
${legacyExample}`;
|
||||
|
||||
return `你是对话落盘创作助手:把用户自然描述整理为可编辑的本局创作产物。作用对象是创作黑板;开玩后由游玩 Worker 接棒。
|
||||
|
||||
**本提示只用于创作。** 开玩后叙事由游玩 worker 执行;创作期只落规格与引导,不写游玩回合正文。
|
||||
|
||||
## 落盘节奏(先读)
|
||||
1. 对照【能力 · 何时落盘】与已有产物:条件成立才落对应能力;未成立 → 本轮不 insert / 不 declare 该能力产物,改为短确认或问挡住的 1 点。
|
||||
2. **同轮可多次 insert / declare_***(一步并行或多个 tool 轮次都行):用户本轮材料已够、且多个能力的「何时用」都成立时,把这些都落掉;每个成功的 insert 各回一份 \`reply_module\`,可见回复按模块分段附带。
|
||||
3. **不抢跑未齐条件的下游**(这才是「别一口气铺满」的意思):条件未成立的能力本轮不落,写进「建议下一刀」等用户补料或上游齐了再落。配方顺序是骨架;能不能落以各能力「何时用 / 何时不用」为准。
|
||||
${decisionBlock}
|
||||
|
||||
## 落盘(仅允许 toolcall)
|
||||
## 写入规则
|
||||
设定 / 变量 / 映射必须经 tool 写入;聊天只做确认与引导,不当落盘载体。
|
||||
|
||||
### insert — 写入/覆盖固定产物
|
||||
- position:\`用户.*\` / \`设计.*\`(优先用【能力 · 何时落盘】里的落盘 tag)
|
||||
- content:正文;order 可选(越小越靠前;越常改越大)
|
||||
- position:\`用户.*\` / \`设计.*\`
|
||||
- content:正文;order 可选(同一变化频率组内,基础材料用较小值,越接近生成时使用的合同越大)
|
||||
- 同 position 再 insert = 整份覆盖改写;变量目录与映射改用 declare_variable / declare_map
|
||||
- **〔可反复〕须拆分**:生成规则 / 具体实例等每条增殖用 \`基名#唯一id\`(如 \`设计.生成规则#ex-girlfriend-card\`、\`设计.具体实例#lei-ying\`)。id 用 rule_id / batch_id / 姓名 slug。禁止反复 insert 基名覆盖旧条;改某一条才 insert 同一完整 tag。
|
||||
- 可反复能力每个实例用 \`基名#唯一id\`;改某实例时覆盖同一完整 tag
|
||||
- 只有方案中该填的内容都完成后才写开场白;先单独调用 prepare_opening,让程序编译信息可见范围、游玩执行方式、上下文排序和运行配置;工具成功后的下一步再生成并 insert 开场白
|
||||
- 开场白与生成规则、具体实例一样可增殖:每条入口 insert \`设计.开场白#场景短码\`(0 层卡用 \`设计.0层开场白与初态#场景短码\`)。改某一条则覆盖同一完整 tag。全部保存;游玩时左右切换,选中的一条作为 0 层。不要在回复里把多条入口收成一道先选定的题
|
||||
|
||||
### delete — 删除固定产物
|
||||
- position:已写入的 \`用户.*\` / \`设计.*\` tag,整份移除
|
||||
@@ -54,7 +64,6 @@ ${opts.moduleGuide.trim()}
|
||||
### declare_variable — 跨轮真值
|
||||
- key / type / initial / user_visible(用户是否可见)
|
||||
- 只立必须跨轮记住的状态;派生文案不要立成可写变量
|
||||
- 时机对照「变量设计与更新规则」的何时用;美学未锚定前不 declare
|
||||
- 移除字段 → undeclare_variable
|
||||
|
||||
### declare_map — 真值→投影上下文
|
||||
@@ -63,65 +72,18 @@ ${opts.moduleGuide.trim()}
|
||||
- 槽位固定、内容随真值换档;旧档离开上下文
|
||||
- 移除一条 → remove_map(同 id 再 declare_map = 覆盖改写)
|
||||
|
||||
## 对用户可见回复
|
||||
tool 跑完后写给用户看的 markdown。作用:确认本轮落了什么、点出薄弱处、用可选追问引导补洞或下一刀。
|
||||
## 推进原则
|
||||
1. 本局创作方案是决策信息,不是 DAG;不要按 items 顺序逐项执行。有方案时,写哪项先对照该能力的何时用 / 何时不用。
|
||||
2. 写任何内容能力前先调用 read_module 读取当前提示版本;需要库条目时,再用 read_library_entry 读取当前能力绑定库条目(默认 1~2;确需更多须给 reason)。不要凭目录短说明猜产物。
|
||||
3. 本轮只写入用户这句话已经说清的能力,通常 2~3 个。对这些能力先 read_module,再 insert / declare_*。每次 insert 都是独立产物写入。
|
||||
4. 写某能力前以 read_module 返回的 task / principles / output / score 为准;尚未读到专用提示时,不猜造其复杂结构。能力提示里的「材料足够便直接成稿」只用于这句已经说清的能力。
|
||||
5. 「必须」和「有必要」要补齐:即使用户没点过能力名,也根据已抽出的核心体验和已有输入给 2~3 个选项来问,选定或改写后再写入。不要因为原话没出现就把有帮助的能力降成不推进。用户明确不要的「有必要」可以降档。「有一定效果」可顺带问一次。「没有意义」表示对这份体验没有帮助,不推进。未落的「必须」和「有必要」留到写开场或结束创作前,问用户哪些还没设计。
|
||||
6. 用户改口、新增方向或现有方案明显不成立时,先覆盖本局创作方案,再据此增删或改写其它产物。
|
||||
|
||||
执行顺序:
|
||||
1. 先完成本轮需要的 insert / declare_*。
|
||||
2. 收集各次 insert 成功返回的 \`reply_module\`(若有;每个模块一份独立 markdown)。
|
||||
3. 按下列规格写自然语言(粘贴产物全文或 tool JSON 都算失败):
|
||||
|
||||
**有 1 个 reply_module**
|
||||
- 用二级标题 \`## 模块 · {名称}\`(名称与该 reply_module 标题一致)
|
||||
- 段内按该模块的「写入用户回复时做什么」写满:确认要点 → 口头薄弱点 → 追问 1~2 点(每点含建议选项或短场景)
|
||||
|
||||
**有 N 个 reply_module(N≥2)**
|
||||
- 同轮多次 insert,或一步并行多个,都会各回一份 \`reply_module\`
|
||||
- 每个模块各写一块,标题互不相同的 \`## 模块 · {名称}\`;块与块之间空一行
|
||||
- 每一块只服务该模块;全部模块写完后,可另起一段写「建议下一刀」(仅 1 条、且条件已接近成立)
|
||||
|
||||
**没有 reply_module**
|
||||
- 用 2~4 句确认本轮写入的 tag 要点,并建议条件已近的下一刀;若落盘被挡住,只问挡住的 1 点
|
||||
|
||||
## 分清落盘目标
|
||||
- 正文组成(格式)→ insert \`设计.正文组成\`(须「回复呈现」等上游条件已满足,见能力表)
|
||||
- 开场白(内容)→ insert \`设计.开场白\` 或 \`设计.开场白#场景短码\`(多开场互不覆盖;改某一条则 insert 同一完整 tag);落档/开玩同步 \`输出.开场白\`
|
||||
- 代入名片 → insert \`设计.主角设定\` 或 \`设计.主角设定#同短码\`(与开场白同槽即绑定该开局;无槽则全局默认)
|
||||
- 多篇无关联短文(生产在游玩)→ 创作期钉 \`设计.模仿范例\` / \`设计.模仿要点\` / \`设计.故事推进\`(大纲扩写);文风细则另 insert \`设计.叙事指南\`
|
||||
- 好感分档性格等 → declare_variable + declare_map;分档文案走映射,不 insert 整表散文
|
||||
${recipeBlock}${moduleBlock}
|
||||
## 按用户本轮意图择一为主(仍须过「何时用」)
|
||||
落盘后的可见回复一律按「对用户可见回复」写;下列各节只约定本轮落什么。
|
||||
|
||||
### A · 钉体验 / 设定
|
||||
优先美学纲领与体验核心(条件总是先做);其它设定类 insert 仅当对应能力何时用成立。
|
||||
|
||||
### B · 钉正文格式
|
||||
「回复呈现」等上游已齐时才 insert \`设计.正文组成\`。
|
||||
|
||||
### C · 生成开场白
|
||||
收口:体验已锚定。首条可用 \`设计.开场白\`;再写别的开局用 \`设计.开场白#场景短码\`(与生成规则同款:同 tag 覆盖,新短码新建)。禁止用 \`## 岔路\` 塞进同一份。
|
||||
**形状:** 只要叙事正文。禁止把 \`用户角色.名字\` / \`用户角色.简介\` 写进开场白。代入名片 → \`设计.主角设定\` 或 \`设计.主角设定#同短码\`。
|
||||
**正文格式:** 已有 \`设计.正文组成\` 时,正文必须是 present.v1 JSON(\`shell\` 与正文组成一致,主读写 \`blocks.body\`),不要只交裸 Markdown;尚未钉正文组成时才用 Markdown。
|
||||
**content 规格:** 正文至少一处字面 \`@玩家\` 作用户角色名位;姓名位用 \`@玩家\`,不写死具体姓名;全文不能只有「你」而零个 \`@玩家\`。缺则改写后再 insert。进游玩时程序替换 \`@玩家\`。
|
||||
进玩时在游玩辅助「开场」Tab 点选开局;有绑定名片则可切到该预设。
|
||||
|
||||
### D · 钉变量与映射
|
||||
仅当「变量设计」何时用成立(体验须跨轮记住状态等):declare_variable → 有分档换文再 declare_map。配方名带「数据化」也不自动开跑 D。
|
||||
|
||||
### E · 文本生成器(创作收料 → 开玩挂载)
|
||||
必须用 insert 写齐游玩上下文:\`设计.模仿范例\`、\`设计.模仿要点\`、\`设计.故事推进\`(用户输入用法:大纲扩写;扮演停笔关掉)。文风细则对照「叙事指南」何时用再 insert \`设计.叙事指南\`。缺范例/要点/故事推进任一就先补产物,再说可以开玩。创作期不写完整短文。开玩后程序只挂载这些产物:主世界出内容,转述出文风。
|
||||
|
||||
### F · 快穿短局
|
||||
仅当本局配方是快穿短局,或用户只要一核短局。先 insert 美学纲领(这一核、怎么代入、何时等用户);代入有名字/简介 → insert \`设计.主角设定\`(不要写进开场白)。核是变造法则再 insert 实现机制;有门控才 declare 至多三个真值。点名文风 → insert 叙事指南;点名推进/停笔 → insert 故事推进(条件独立)。核可玩即 insert 开场白:用户未点名分区/壳则跳过正文组成(覆盖 C),开玩吃默认散文。舞台骨架/生成规则/拓扑即使「何时用」成立也不 insert,把地点与规则折进美学核或开场白。用户改口要铺可经营世界 → 本局仍只收这一核,并告知改用「数据化跑团体验」或「回合推演」。
|
||||
|
||||
## 常用序感
|
||||
- 需求/美学等不变设定:靠前(负)
|
||||
- 正文组成:中间(0)
|
||||
- 开场白:靠后(正)
|
||||
|
||||
## 设计.正文组成
|
||||
JSON:\`正文.呈现壳.shell_id\`、\`正文.可见块[]\`。文本局默认 prose;至少一块 body。
|
||||
## 用户可见回复
|
||||
自然回应用户本轮有效判断、矛盾和值得保留之处,再简要说明形成或改写了什么。完整 JSON 由产物卡展示,不粘贴到回复。
|
||||
写正文组成或 0 层正文组成时,在回复里直接给出一份隔离内容供预览:与写入的 frontend 同一套骨架,示例内容已经放进去,单独可看。产物仍只保存 frontend 与数据契约。
|
||||
这句话没覆盖到的「必须」或「有必要」,用 2~3 个从用户原话推出的选项询问,不先写成产物。已经说清并写入的,说明形成了什么,不再另找问题。同轮多个已说清产物只做一次综合回应,不逐模块套固定标题。
|
||||
|
||||
## 其它
|
||||
对话明显过长时 clear_dialogue(产物与变量/映射保留)。
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import {
|
||||
getModuleSection,
|
||||
loadModulePrompt,
|
||||
parseModulePromptSections,
|
||||
type ModuleCatalog,
|
||||
type ModuleCatalogEntry,
|
||||
} from "../skills/creation-flow.js";
|
||||
import {
|
||||
loadShellRefCatalog,
|
||||
type ShellRefEntry,
|
||||
} from "../skills/shell-ref-catalog.js";
|
||||
import {
|
||||
formatMarkdownSafeSubsetForPrompt,
|
||||
loadMarkdownSafeSubsetCatalog,
|
||||
} from "../skills/markdown-safe-subset.js";
|
||||
import { getLibrary } from "../libraries/registry.js";
|
||||
import {
|
||||
getPreference,
|
||||
listActivePreferences,
|
||||
} from "../preference/store.js";
|
||||
import {
|
||||
getStylePack,
|
||||
listActiveStylePacks,
|
||||
} from "../style-pack/store.js";
|
||||
|
||||
export const DICTATE_MODULE_READ_STATE_TAG = "运行.对话落盘.已读能力";
|
||||
|
||||
export type DictateModuleReadPayload = {
|
||||
module_id: string;
|
||||
name: string;
|
||||
artifact: string;
|
||||
version: string;
|
||||
task: string;
|
||||
principles: string;
|
||||
output: string;
|
||||
score: string;
|
||||
score_dimensions: string[];
|
||||
libraries: Array<{
|
||||
library_id: string;
|
||||
label: string;
|
||||
entries: Array<{ id: string; name?: string; summary: string }>;
|
||||
}>;
|
||||
constraints: string[];
|
||||
presentation_references?: {
|
||||
shells: ShellRefEntry[];
|
||||
markdown_safe_subset: string;
|
||||
iframe_contract: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type DictateModuleReadState = {
|
||||
schema: "dictate-module-read.v1";
|
||||
modules: Record<string, string>;
|
||||
active_module_id?: string;
|
||||
};
|
||||
|
||||
export async function buildDictateModuleReadIndex(params: {
|
||||
skillPackRoot: string;
|
||||
catalog: ModuleCatalog;
|
||||
}): Promise<Map<string, DictateModuleReadPayload>> {
|
||||
const index = new Map<string, DictateModuleReadPayload>();
|
||||
const [shells, markdown] = await Promise.all([
|
||||
loadShellRefCatalog(params.skillPackRoot),
|
||||
loadMarkdownSafeSubsetCatalog(params.skillPackRoot),
|
||||
]);
|
||||
for (const module of params.catalog.modules) {
|
||||
const prompt = await loadModulePrompt(params.skillPackRoot, module.id);
|
||||
if (!prompt) continue;
|
||||
const sections = parseModulePromptSections(prompt);
|
||||
const payload: DictateModuleReadPayload = {
|
||||
module_id: module.id,
|
||||
name: module.name,
|
||||
artifact: module.artifact,
|
||||
version: modulePromptVersion(sections.blocks),
|
||||
task: getModuleSection(sections, "task") ?? "",
|
||||
principles: getModuleSection(sections, "principles") ?? "",
|
||||
output: getModuleSection(sections, "output") ?? "",
|
||||
score: getModuleSection(sections, "score") ?? "",
|
||||
score_dimensions: extractScoreDimensions(
|
||||
getModuleSection(sections, "score") ?? "",
|
||||
),
|
||||
libraries: libraryDirectories(module),
|
||||
constraints: moduleConstraints(module),
|
||||
...(module.id === "reply-format" || module.id === "zero-layer-reply-format"
|
||||
? {
|
||||
presentation_references: {
|
||||
shells,
|
||||
markdown_safe_subset: markdown
|
||||
? formatMarkdownSafeSubsetForPrompt(markdown)
|
||||
: "",
|
||||
iframe_contract:
|
||||
module.id === "reply-format"
|
||||
? "正文组成生成一次 frontend 模板与数据契约;游玩每轮只传 present.v1 blocks,由程序装进已固定的壳。iframe 使用 sandbox=\"allow-scripts\",不含 allow-same-origin;模板只通过 present.onData(cb) 收数据,不提供 action 回传。html+css+js 合计不超过 60 KB;未知 blocks 键必须保留。"
|
||||
: "0层正文组成生成一次持久前端;之后每轮只提交 slot_updates。模板通过 zeroLayer.onInit 与 zeroLayer.onPatch 收数据,不提供 action 回传。html+css+js 合计不超过 60 KB。",
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
index.set(module.id, payload);
|
||||
index.set(module.name, payload);
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
export function parseDictateModuleReadState(
|
||||
raw: string | undefined | null,
|
||||
): DictateModuleReadState {
|
||||
if (raw?.trim()) {
|
||||
try {
|
||||
const row = JSON.parse(raw) as DictateModuleReadState;
|
||||
if (
|
||||
row?.schema === "dictate-module-read.v1" &&
|
||||
row.modules &&
|
||||
typeof row.modules === "object"
|
||||
) {
|
||||
return row;
|
||||
}
|
||||
} catch {
|
||||
// Reset malformed internal state.
|
||||
}
|
||||
}
|
||||
return { schema: "dictate-module-read.v1", modules: {} };
|
||||
}
|
||||
|
||||
export function serializeDictateModuleReadState(
|
||||
state: DictateModuleReadState,
|
||||
): string {
|
||||
return JSON.stringify(state);
|
||||
}
|
||||
|
||||
export function readLibraryEntries(params: {
|
||||
module: DictateModuleReadPayload;
|
||||
libraryId: string;
|
||||
entryIds: string[];
|
||||
reason?: string;
|
||||
}): { entries?: unknown[]; error?: string } {
|
||||
const libraryId = params.libraryId.trim();
|
||||
const bound = params.module.libraries.some(
|
||||
(library) => library.library_id === libraryId,
|
||||
);
|
||||
if (!bound) {
|
||||
return { error: `能力 ${params.module.module_id} 未绑定库 ${libraryId}` };
|
||||
}
|
||||
const ids = [...new Set(params.entryIds.map((id) => id.trim()).filter(Boolean))];
|
||||
if (ids.length === 0) return { error: "entry_ids 不能为空" };
|
||||
if (ids.length > 2 && !params.reason?.trim()) {
|
||||
return { error: "默认最多读取两个库条目;读取更多时必须提供 reason" };
|
||||
}
|
||||
if (libraryId === "preferences") {
|
||||
const entries = ids.map(getPreference);
|
||||
const missing = ids.filter((_, index) => !entries[index]);
|
||||
if (missing.length) return { error: `偏好库无条目:${missing.join("、")}` };
|
||||
return {
|
||||
entries: entries.map((entry) => ({
|
||||
id: entry!.id,
|
||||
content: entry!.content,
|
||||
})),
|
||||
};
|
||||
}
|
||||
if (libraryId === "style-packs") {
|
||||
const entries = ids.map(getStylePack);
|
||||
const missing = ids.filter((_, index) => !entries[index]);
|
||||
if (missing.length) return { error: `文风库无条目:${missing.join("、")}` };
|
||||
return {
|
||||
entries: entries.map((entry) => ({
|
||||
id: entry!.id,
|
||||
name: entry!.name,
|
||||
content: entry!.content,
|
||||
...(entry!.samples ? { samples: entry!.samples } : {}),
|
||||
})),
|
||||
};
|
||||
}
|
||||
return { error: `库 ${libraryId} 尚不支持按条目读取` };
|
||||
}
|
||||
|
||||
function modulePromptVersion(blocks: Record<string, string>): string {
|
||||
const source = ["task", "principles", "output", "score"]
|
||||
.map((id) => `${id}\n${blocks[id]?.trim() ?? ""}`)
|
||||
.join("\n---\n");
|
||||
return createHash("sha256").update(source).digest("hex").slice(0, 16);
|
||||
}
|
||||
|
||||
function libraryDirectories(module: ModuleCatalogEntry): DictateModuleReadPayload["libraries"] {
|
||||
return (module.libraries ?? []).map((id) => {
|
||||
const provider = getLibrary(id);
|
||||
if (id === "preferences") {
|
||||
return {
|
||||
library_id: id,
|
||||
label: provider?.label ?? id,
|
||||
entries: listActivePreferences().map((entry) => ({
|
||||
id: entry.id,
|
||||
summary: clip(entry.content),
|
||||
})),
|
||||
};
|
||||
}
|
||||
if (id === "style-packs") {
|
||||
return {
|
||||
library_id: id,
|
||||
label: provider?.label ?? id,
|
||||
entries: listActiveStylePacks().map((entry) => ({
|
||||
id: entry.id,
|
||||
name: entry.name,
|
||||
summary: clip(entry.content),
|
||||
})),
|
||||
};
|
||||
}
|
||||
return { library_id: id, label: provider?.label ?? id, entries: [] };
|
||||
});
|
||||
}
|
||||
|
||||
function repeatableLandLine(module: ModuleCatalogEntry): string {
|
||||
if (module.id === "opening-setup") {
|
||||
return "产物可反复:每条开场使用 设计.开场白#场景短码;同完整 tag 才覆盖。游玩时左右切换,选中的一条作为 0 层";
|
||||
}
|
||||
if (module.id === "zero-layer-opening-setup") {
|
||||
return "产物可反复:每条开场使用 设计.0层开场白与初态#场景短码;同完整 tag 才覆盖。游玩时左右切换,选中的一条作为 0 层";
|
||||
}
|
||||
if (module.id === "generation-rules") {
|
||||
return "产物可反复:每条规则单独落盘,使用 设计.生成规则#规则名;同完整 tag 才覆盖";
|
||||
}
|
||||
if (module.id === "concrete-instances") {
|
||||
return "产物可反复:每条实例单独落盘,使用 设计.具体实例#名称;同完整 tag 才覆盖";
|
||||
}
|
||||
if (module.repeatable) {
|
||||
return `产物可反复:每条单独落盘,使用 ${module.artifact}#唯一id;同完整 tag 才覆盖`;
|
||||
}
|
||||
return `产物 tag:${module.artifact}`;
|
||||
}
|
||||
|
||||
function moduleConstraints(module: ModuleCatalogEntry): string[] {
|
||||
const constraints = [repeatableLandLine(module)];
|
||||
if (module.id === "generation-rules") {
|
||||
constraints.push(
|
||||
"产物层由生命周期决定:只预生成=intermediate;仅游玩期 / 预生成并游玩期=final",
|
||||
);
|
||||
constraints.push("每个产物只写一条规则");
|
||||
} else if (module.layer) {
|
||||
constraints.push(`默认产物层:${module.layer}`);
|
||||
}
|
||||
if (module.mount?.length) constraints.push(`默认挂载:${module.mount.join("、")}`);
|
||||
if (module.auto) constraints.push("机械程序节点,不受读取门禁");
|
||||
return constraints;
|
||||
}
|
||||
|
||||
function clip(text: string, max = 140): string {
|
||||
const compact = text.replace(/\s+/g, " ").trim();
|
||||
return compact.length <= max ? compact : `${compact.slice(0, max - 1)}…`;
|
||||
}
|
||||
|
||||
function extractScoreDimensions(score: string): string[] {
|
||||
const names: string[] = [];
|
||||
for (const match of score.matchAll(/^([^#\-\s][^:\n]{0,30}):\s*$/gm)) {
|
||||
const name = match[1]?.trim();
|
||||
if (name && !names.includes(name)) names.push(name);
|
||||
}
|
||||
return names;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* 对话落盘 · 可增殖产物 tag 拆分
|
||||
*
|
||||
* catalog 标 repeatable 的能力(生成规则 / 具体实例)共用同一 artifact 基名。
|
||||
* catalog 标 repeatable 的能力(生成规则 / 具体实例 / 拓扑图谱等)共用同一 artifact 基名。
|
||||
* 对话里若反复 insert 基 tag,后写会整份覆盖先写。
|
||||
* 约定:基名#槽位(如 设计.具体实例#lei-ying),每条增殖各占一 tag。
|
||||
*/
|
||||
@@ -48,6 +48,85 @@ export function buildRepeatableProductTag(
|
||||
return s ? `${base}${REPEATABLE_TAG_SEP}${s}` : base;
|
||||
}
|
||||
|
||||
function nameFromRecord(row: unknown): string | undefined {
|
||||
if (!row || typeof row !== "object" || Array.isArray(row)) return undefined;
|
||||
const record = row as Record<string, unknown>;
|
||||
return (
|
||||
asString(record.姓名) ??
|
||||
asString(record.名称) ??
|
||||
asString(record.名字) ??
|
||||
asString(record.name) ??
|
||||
asString(record.id)
|
||||
);
|
||||
}
|
||||
|
||||
function concreteInstanceName(body: Record<string, unknown>): string | undefined {
|
||||
const direct =
|
||||
asString(body.名称) ??
|
||||
asString(body.实例名) ??
|
||||
asString(body.batch_id) ??
|
||||
asString(body.批次id) ??
|
||||
asString(body.id);
|
||||
if (direct) return direct;
|
||||
const fromRecord = nameFromRecord(body.记录);
|
||||
if (fromRecord) return fromRecord;
|
||||
const rows = Array.isArray(body.records)
|
||||
? body.records
|
||||
: Array.isArray(body.记录)
|
||||
? body.记录
|
||||
: [];
|
||||
for (const row of rows) {
|
||||
const name = nameFromRecord(row);
|
||||
if (name) return name;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function concreteRecordRows(body: Record<string, unknown>): unknown[] | null {
|
||||
if (Array.isArray(body.records) && body.records.length > 1) return body.records;
|
||||
if (Array.isArray(body.记录) && body.记录.length > 1) return body.记录;
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 一条具体实例正文里若含多条记录,拆成每条一份。不足两条时原样返回。 */
|
||||
export function splitConcreteInstanceContents(content: string): string[] {
|
||||
const parsed = tryParseJsonDoc(content);
|
||||
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
||||
return [content];
|
||||
}
|
||||
const doc = parsed as Record<string, unknown>;
|
||||
const bodyRaw = doc.正文;
|
||||
if (!bodyRaw || typeof bodyRaw !== "object" || Array.isArray(bodyRaw)) {
|
||||
return [content];
|
||||
}
|
||||
const body = bodyRaw as Record<string, unknown>;
|
||||
const rows = concreteRecordRows(body);
|
||||
if (!rows) return [content];
|
||||
|
||||
const rule =
|
||||
asString(body.规则) ?? asString(body.规则名) ?? asString(body.rule_id);
|
||||
const used = new Set<string>();
|
||||
return rows.map((row, index) => {
|
||||
const base = nameFromRecord(row) ?? `第${index + 1}条`;
|
||||
let name = base;
|
||||
let n = 2;
|
||||
while (used.has(name)) {
|
||||
name = `${base}-${n}`;
|
||||
n += 1;
|
||||
}
|
||||
used.add(name);
|
||||
const nextBody: Record<string, unknown> = {
|
||||
...(rule ? { 规则: rule } : {}),
|
||||
名称: name,
|
||||
记录: row && typeof row === "object" && !Array.isArray(row) ? row : { 内容: String(row) },
|
||||
};
|
||||
if (Array.isArray(body.批次条件) && body.批次条件.length) {
|
||||
nextBody.批次条件 = body.批次条件;
|
||||
}
|
||||
return JSON.stringify({ ...doc, brief: name, 正文: nextBody }, null, 2);
|
||||
});
|
||||
}
|
||||
|
||||
function asString(v: unknown): string | undefined {
|
||||
if (typeof v !== "string") return undefined;
|
||||
const t = v.trim();
|
||||
@@ -65,7 +144,7 @@ function bodyOfFragment(content: string): Record<string, unknown> | null {
|
||||
}
|
||||
|
||||
/**
|
||||
* 从正文抽出建议槽位:生成规则 → rule_id;具体实例 → batch_id / 首条姓名。
|
||||
* 从正文抽出建议槽位:生成规则 → 规则名;具体实例 → 名称 / 记录里的姓名。
|
||||
*/
|
||||
export function extractRepeatableSlotFromContent(
|
||||
family: string,
|
||||
@@ -77,7 +156,11 @@ export function extractRepeatableSlotFromContent(
|
||||
|
||||
if (base === "设计.生成规则" || /生成规则$/.test(base)) {
|
||||
const top =
|
||||
asString(body.rule_id) ?? asString(body.id) ?? asString(body.规则id);
|
||||
asString(body.规则名) ??
|
||||
asString(body.rule_id) ??
|
||||
asString(body.id) ??
|
||||
asString(body.规则id) ??
|
||||
asString(body.名称);
|
||||
if (top) return sanitizeRepeatableSlot(top) || undefined;
|
||||
const rules = body.rules;
|
||||
if (Array.isArray(rules)) {
|
||||
@@ -85,7 +168,10 @@ export function extractRepeatableSlotFromContent(
|
||||
if (!row || typeof row !== "object" || Array.isArray(row)) continue;
|
||||
const r = row as Record<string, unknown>;
|
||||
const id =
|
||||
asString(r.rule_id) ?? asString(r.id) ?? asString(r.规则id);
|
||||
asString(r.规则名) ??
|
||||
asString(r.rule_id) ??
|
||||
asString(r.id) ??
|
||||
asString(r.规则id);
|
||||
if (id) return sanitizeRepeatableSlot(id) || undefined;
|
||||
}
|
||||
}
|
||||
@@ -95,27 +181,12 @@ export function extractRepeatableSlotFromContent(
|
||||
}
|
||||
|
||||
if (base === "设计.具体实例" || /具体实例$/.test(base)) {
|
||||
const batch =
|
||||
asString(body.batch_id) ??
|
||||
asString(body.批次id) ??
|
||||
asString(body.id);
|
||||
if (batch) return sanitizeRepeatableSlot(batch) || undefined;
|
||||
const records = body.records;
|
||||
if (Array.isArray(records)) {
|
||||
for (const row of records) {
|
||||
if (!row || typeof row !== "object" || Array.isArray(row)) continue;
|
||||
const r = row as Record<string, unknown>;
|
||||
const name =
|
||||
asString(r.姓名) ??
|
||||
asString(r.name) ??
|
||||
asString(r.名称) ??
|
||||
asString(r.id);
|
||||
if (name) return sanitizeRepeatableSlot(name) || undefined;
|
||||
}
|
||||
}
|
||||
const ruleId = asString(body.rule_id);
|
||||
const named = concreteInstanceName(body);
|
||||
if (named) return sanitizeRepeatableSlot(named) || undefined;
|
||||
const ruleId =
|
||||
asString(body.规则) ?? asString(body.规则名) ?? asString(body.rule_id);
|
||||
if (ruleId) {
|
||||
return sanitizeRepeatableSlot(`${ruleId}-batch`) || undefined;
|
||||
return sanitizeRepeatableSlot(`${ruleId}-1`) || undefined;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
@@ -157,7 +228,7 @@ export function resolveRepeatableInsertTag(params: {
|
||||
const otherSiblings = siblings.filter((t) => t !== family);
|
||||
if (otherSiblings.length > 0 && raw === family) {
|
||||
return {
|
||||
error: `「${family}」是可增殖产物,追加请用「${family}#唯一id」(生成规则用 rule_id,具体实例用 batch_id/姓名,开场白/主角设定用场景短码),不要覆盖基 tag。改某一条则 insert 同一完整 tag。`,
|
||||
error: `「${family}」是可增殖产物,追加请用「${family}#唯一id」(生成规则用规则名,具体实例用名称,开场白/主角设定用场景短码),不要覆盖基 tag。改某一条则 insert 同一完整 tag。`,
|
||||
};
|
||||
}
|
||||
return { tag: family };
|
||||
|
||||
+91
-1
@@ -4,6 +4,70 @@ import type { ToolDefinition } from "../llm/client.js";
|
||||
* 对话落盘创作期工具:产物 / 变量 / 映射均须 toolcall;聊天只做确认与建议。
|
||||
*/
|
||||
export const DICTATE_TOOL_DEFINITIONS: ToolDefinition[] = [
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "read_module",
|
||||
description:
|
||||
"读取一个内容能力当前版本的任务、原则、产物规格、评分规则、绑定库目录与程序约束。写该能力产物前必须先读取当前版本。",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
module_id: {
|
||||
type: "string",
|
||||
description: "能力 id 或中文名",
|
||||
},
|
||||
reason: {
|
||||
type: "string",
|
||||
description: "本轮为何读取(一句话,可选)",
|
||||
},
|
||||
},
|
||||
required: ["module_id"],
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "read_library_entry",
|
||||
description:
|
||||
"读取当前能力绑定库中本轮选中的条目。默认 1~2;叙事指南等多文风确需第三项时须提供 reason 并说明适用场合。",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
library_id: {
|
||||
type: "string",
|
||||
description: "当前能力绑定的库 id",
|
||||
},
|
||||
entry_ids: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
description: "本轮选中的条目 id,默认最多两个",
|
||||
},
|
||||
reason: {
|
||||
type: "string",
|
||||
description: "读取超过两个条目时必填",
|
||||
},
|
||||
},
|
||||
required: ["library_id", "entry_ids"],
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "prepare_opening",
|
||||
description:
|
||||
"用户该填的内容均已完成后、生成开场白之前单独调用。程序依次编译信息可见范围、游玩执行方式、上下文排序和运行配置;返回成功后再在下一步生成并 insert 开场白。",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {},
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
@@ -26,7 +90,33 @@ export const DICTATE_TOOL_DEFINITIONS: ToolDefinition[] = [
|
||||
order: {
|
||||
type: "number",
|
||||
description:
|
||||
"相对顺序(可负)。越小越靠前。越常改越大。省略则沿用原序或默认。",
|
||||
"同一变化频率组内的重要性顺序(可负)。基础材料用较小值,越接近生成时使用的合同越大;省略则沿用原序或默认。",
|
||||
},
|
||||
self_score: {
|
||||
type: "object",
|
||||
description:
|
||||
"能力已有 score 规则时必填;维度名须完全一致,1~5 分,分数不高于 3 时必须写 gap。",
|
||||
properties: {
|
||||
dims: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "object",
|
||||
properties: {
|
||||
name: { type: "string" },
|
||||
score: { type: "number" },
|
||||
gap: { type: "string" },
|
||||
options: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
},
|
||||
},
|
||||
required: ["name", "score"],
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
required: ["dims"],
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
required: ["position", "content"],
|
||||
|
||||
+556
-44
@@ -1,6 +1,7 @@
|
||||
import type { LlmProvider } from "../llm/client.js";
|
||||
import {
|
||||
createLocalLlmDriver,
|
||||
DriverIterationLimitError,
|
||||
type AgentDriver,
|
||||
} from "../runtime/driver.js";
|
||||
import { buildDictateMessages } from "./context.js";
|
||||
@@ -9,11 +10,31 @@ import { DICTATE_TOOL_DEFINITIONS } from "./tools.js";
|
||||
import {
|
||||
isAllowedProductTag,
|
||||
parseDictateOrder,
|
||||
type DictateSelfScore,
|
||||
type DictateChatTurn,
|
||||
type DictateProduct,
|
||||
} from "./types.js";
|
||||
import { DICTATE_MULTI_MODULE_REPLY_HINT } from "./insert-feedback.js";
|
||||
import { resolveRepeatableInsertTag } from "./repeatable-tags.js";
|
||||
import {
|
||||
DICTATE_MULTI_MODULE_REPLY_HINT,
|
||||
resolveModuleForInsertTag,
|
||||
} from "./insert-feedback.js";
|
||||
import { dictateProductFamily, resolveRepeatableInsertTag, splitConcreteInstanceContents } from "./repeatable-tags.js";
|
||||
import type { ModuleCatalog } from "../skills/creation-flow.js";
|
||||
import {
|
||||
CREATION_PLAN_TAG,
|
||||
formatCreationPlanCapabilityCatalog,
|
||||
formatCreationPlanProgress,
|
||||
parseCreationPlan,
|
||||
requiredCreationPlanGaps,
|
||||
validateCreationPlan,
|
||||
} from "./creation-plan.js";
|
||||
import {
|
||||
DICTATE_MODULE_READ_STATE_TAG,
|
||||
parseDictateModuleReadState,
|
||||
readLibraryEntries,
|
||||
serializeDictateModuleReadState,
|
||||
type DictateModuleReadPayload,
|
||||
} from "./read-module.js";
|
||||
import {
|
||||
VARIABLE_CATALOG_TAG,
|
||||
parseVariableCatalog,
|
||||
@@ -34,11 +55,18 @@ import {
|
||||
parseTableDoc,
|
||||
stringifyTableDoc,
|
||||
} from "../blackboard/table-cells.js";
|
||||
import { isDictateOpeningTag } from "./experience-anchor.js";
|
||||
import { canonicalizeInsertedContent } from "../parse/json-doc.js";
|
||||
|
||||
export type DictateTurnHandlers = {
|
||||
listProducts: () => DictateProduct[];
|
||||
/** order 省略时由实现沿用原序或默认 */
|
||||
writeProduct: (tag: string, content: string, order?: number) => void;
|
||||
writeProduct: (
|
||||
tag: string,
|
||||
content: string,
|
||||
order?: number,
|
||||
metadata?: { layer?: "intermediate" | "final"; selfScore?: DictateSelfScore },
|
||||
) => void;
|
||||
/** 删除固定产物 tag;返回是否曾存在 */
|
||||
deleteProduct: (tag: string) => boolean;
|
||||
/** 读/写变量目录与映射(程序校验后的 JSON) */
|
||||
@@ -46,6 +74,8 @@ export type DictateTurnHandlers = {
|
||||
writeTag: (tag: string, content: string) => void;
|
||||
/** 删除任意黑板 tag(投影 / 变量.当前 等) */
|
||||
deleteTag: (tag: string) => boolean;
|
||||
/** 写入开场白前运行确定性收口工具,保证运行配置先于开场落盘。 */
|
||||
prepareOpening?: () => void;
|
||||
clearDialogue: () => void;
|
||||
onToolCall?: (name: string, detail: string) => void;
|
||||
onThinkingDelta?: (delta: string) => void;
|
||||
@@ -65,6 +95,16 @@ export type DictateTurnResult = {
|
||||
clearedDialogue: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* 一轮对话落盘的工具上限。
|
||||
* 串行模型往往一轮只调一个工具:读能力再写入,方案里十余项,再加上收尾回复。
|
||||
*/
|
||||
export const DICTATE_DRIVER_MAX_ITERATIONS = 36;
|
||||
|
||||
/** 接近上限时写进工具结果,让模型停下来回复用户。 */
|
||||
export const DICTATE_TURN_LIMIT_HINT =
|
||||
"本轮工具次数已接近上限。停止调用工具,直接回复用户。已写入的保留;未写的能力下一轮继续。";
|
||||
|
||||
/**
|
||||
* 跑一轮对话落盘整理:任务 messages 由本模块拼;preset 夹心由 LLM 包装层统一装配。
|
||||
*/
|
||||
@@ -77,6 +117,12 @@ export async function runDictateTurn(params: {
|
||||
recipeBrief?: string;
|
||||
/** modules catalog 的何时落盘块(formatModuleCatalogForDictate) */
|
||||
moduleGuide?: string;
|
||||
/** 尚无本局方案时显示的旧配方案例目录 */
|
||||
recipeExamples?: string;
|
||||
/** 用于本局方案覆盖校验及方案态能力状态 */
|
||||
moduleCatalog?: ModuleCatalog | null;
|
||||
/** 当前能力提示的可读内容与版本;存在时启用写前读取门禁 */
|
||||
moduleReadIndex?: ReadonlyMap<string, DictateModuleReadPayload>;
|
||||
/** catalog 中 repeatable 能力的 artifact 基名(设计.生成规则 等) */
|
||||
repeatableFamilies?: ReadonlySet<string> | readonly string[];
|
||||
driver?: AgentDriver;
|
||||
@@ -85,6 +131,7 @@ export async function runDictateTurn(params: {
|
||||
const wroteTags: string[] = [];
|
||||
const deletedTags: string[] = [];
|
||||
let clearedDialogue = false;
|
||||
let openingPrepared = false;
|
||||
let dialogue = [...params.dialogue];
|
||||
const repeatableFamilies = new Set(
|
||||
[...(params.repeatableFamilies ?? [])].map((t) => String(t).trim()).filter(Boolean),
|
||||
@@ -92,33 +139,146 @@ export async function runDictateTurn(params: {
|
||||
|
||||
const systemPrompt =
|
||||
params.systemPrompt ??
|
||||
buildDictateSystemPrompt({
|
||||
recipeName: params.recipeName,
|
||||
recipeBrief: params.recipeBrief,
|
||||
moduleGuide: params.moduleGuide,
|
||||
});
|
||||
(() => {
|
||||
const products = params.handlers.listProducts();
|
||||
const plan = parseCreationPlan(
|
||||
products.find((product) => product.tag === CREATION_PLAN_TAG)?.content,
|
||||
);
|
||||
return buildDictateSystemPrompt({
|
||||
...(plan
|
||||
? {
|
||||
creationPlanState: formatCreationPlanProgress({
|
||||
plan,
|
||||
catalog: params.moduleCatalog,
|
||||
products,
|
||||
}),
|
||||
}
|
||||
: {
|
||||
capabilityCatalog:
|
||||
formatCreationPlanCapabilityCatalog(params.moduleCatalog) ||
|
||||
params.moduleGuide,
|
||||
recipeExamples: params.recipeExamples,
|
||||
recipeName: params.recipeName,
|
||||
recipeBrief: params.recipeBrief,
|
||||
}),
|
||||
});
|
||||
})();
|
||||
const assembled = buildDictateMessages({
|
||||
systemPrompt,
|
||||
products: params.handlers.listProducts(),
|
||||
dialogue,
|
||||
});
|
||||
const userBlock = assembled.filter((m) => m.role !== "system");
|
||||
let toolSteps = 0;
|
||||
|
||||
const run = await driver.run({
|
||||
let run;
|
||||
try {
|
||||
run = await driver.run({
|
||||
system: systemPrompt,
|
||||
messages: userBlock,
|
||||
tools: DICTATE_TOOL_DEFINITIONS,
|
||||
caller: "dictate_agent",
|
||||
label: "对话落盘",
|
||||
maxIterations: DICTATE_DRIVER_MAX_ITERATIONS,
|
||||
onThinkingDelta: params.handlers.onThinkingDelta,
|
||||
onThinkingDone: params.handlers.onThinkingDone,
|
||||
handleStep: (calls) => {
|
||||
toolSteps += 1;
|
||||
const moduleNamesThisStep: string[] = [];
|
||||
const results = calls.map((call) => {
|
||||
const args = safeParseArgs(call.arguments);
|
||||
if (
|
||||
call.name === "declare_variable" ||
|
||||
call.name === "declare_map" ||
|
||||
call.name === "delete" ||
|
||||
call.name === "undeclare_variable" ||
|
||||
call.name === "remove_map"
|
||||
) {
|
||||
openingPrepared = false;
|
||||
}
|
||||
if (call.name === "read_module") {
|
||||
const ref = String(args.module_id ?? "").trim();
|
||||
const module = params.moduleReadIndex?.get(ref);
|
||||
if (!module) {
|
||||
const err = `未找到能力:${ref || "空"}`;
|
||||
params.handlers.onToolCall?.(call.name, err);
|
||||
return { callId: call.id, content: JSON.stringify({ error: err }) };
|
||||
}
|
||||
const state = parseDictateModuleReadState(
|
||||
params.handlers.readTag(DICTATE_MODULE_READ_STATE_TAG),
|
||||
);
|
||||
state.modules[module.module_id] = module.version;
|
||||
state.active_module_id = module.module_id;
|
||||
params.handlers.writeTag(
|
||||
DICTATE_MODULE_READ_STATE_TAG,
|
||||
serializeDictateModuleReadState(state),
|
||||
);
|
||||
params.handlers.onToolCall?.(
|
||||
call.name,
|
||||
`${module.module_id}@${module.version}`,
|
||||
);
|
||||
return {
|
||||
callId: call.id,
|
||||
content: JSON.stringify({ ok: true, ...module }),
|
||||
};
|
||||
}
|
||||
if (call.name === "read_library_entry") {
|
||||
const state = parseDictateModuleReadState(
|
||||
params.handlers.readTag(DICTATE_MODULE_READ_STATE_TAG),
|
||||
);
|
||||
const module = state.active_module_id
|
||||
? params.moduleReadIndex?.get(state.active_module_id)
|
||||
: undefined;
|
||||
if (!module) {
|
||||
const err = "请先 read_module 读取要使用该库的能力";
|
||||
params.handlers.onToolCall?.(call.name, err);
|
||||
return { callId: call.id, content: JSON.stringify({ error: err }) };
|
||||
}
|
||||
const result = readLibraryEntries({
|
||||
module,
|
||||
libraryId: String(args.library_id ?? ""),
|
||||
entryIds: Array.isArray(args.entry_ids)
|
||||
? args.entry_ids.map(String)
|
||||
: [],
|
||||
reason: typeof args.reason === "string" ? args.reason : undefined,
|
||||
});
|
||||
params.handlers.onToolCall?.(
|
||||
call.name,
|
||||
result.error ?? `${args.library_id}: ${result.entries?.length ?? 0}`,
|
||||
);
|
||||
return {
|
||||
callId: call.id,
|
||||
content: JSON.stringify(
|
||||
result.error
|
||||
? { error: result.error }
|
||||
: {
|
||||
ok: true,
|
||||
module_id: module.module_id,
|
||||
library_id: String(args.library_id ?? ""),
|
||||
entries: result.entries,
|
||||
},
|
||||
),
|
||||
};
|
||||
}
|
||||
if (call.name === "prepare_opening") {
|
||||
params.handlers.prepareOpening?.();
|
||||
openingPrepared = true;
|
||||
params.handlers.onToolCall?.(
|
||||
call.name,
|
||||
"信息可见范围 → 游玩执行方式 → 上下文排序 → 运行配置组装",
|
||||
);
|
||||
return {
|
||||
callId: call.id,
|
||||
content: JSON.stringify({
|
||||
ok: true,
|
||||
runtime_prepared: true,
|
||||
next: "现在生成并 insert 开场白",
|
||||
}),
|
||||
};
|
||||
}
|
||||
if (call.name === "insert" || call.name === "write_product") {
|
||||
let tag = String(args.position ?? args.tag ?? "").trim();
|
||||
const content = String(args.content ?? "");
|
||||
const content = canonicalizeInsertedContent(String(args.content ?? ""));
|
||||
if (tag === VARIABLE_CATALOG_TAG || tag === VALUE_MAP_TAG) {
|
||||
const err = `请用 declare_variable / declare_map,不要 insert ${tag}`;
|
||||
params.handlers.onToolCall?.(call.name, err);
|
||||
@@ -134,24 +294,193 @@ export async function runDictateTurn(params: {
|
||||
params.handlers.onToolCall?.(call.name, err);
|
||||
return { callId: call.id, content: JSON.stringify({ error: err }) };
|
||||
}
|
||||
if (repeatableFamilies.size) {
|
||||
const resolved = resolveRepeatableInsertTag({
|
||||
tag,
|
||||
content,
|
||||
existingTags: params.handlers.listProducts().map((p) => p.tag),
|
||||
repeatableFamilies,
|
||||
});
|
||||
if ("error" in resolved) {
|
||||
params.handlers.onToolCall?.(call.name, resolved.error);
|
||||
const targetModule = resolveModuleForInsertTag(
|
||||
tag,
|
||||
params.moduleCatalog,
|
||||
);
|
||||
if (
|
||||
targetModule &&
|
||||
params.moduleReadIndex &&
|
||||
requiresModuleRead(targetModule.id, targetModule.auto)
|
||||
) {
|
||||
const current = params.moduleReadIndex.get(targetModule.id);
|
||||
const state = parseDictateModuleReadState(
|
||||
params.handlers.readTag(DICTATE_MODULE_READ_STATE_TAG),
|
||||
);
|
||||
if (!current || state.modules[targetModule.id] !== current.version) {
|
||||
const err = `写入 ${tag} 前必须先 read_module("${targetModule.id}") 读取当前提示版本`;
|
||||
params.handlers.onToolCall?.(call.name, err);
|
||||
return {
|
||||
callId: call.id,
|
||||
content: JSON.stringify({ error: resolved.error }),
|
||||
content: JSON.stringify({
|
||||
error: err,
|
||||
required_module_id: targetModule.id,
|
||||
required_version: current?.version,
|
||||
}),
|
||||
};
|
||||
}
|
||||
tag = resolved.tag;
|
||||
}
|
||||
const currentModule = targetModule
|
||||
? params.moduleReadIndex?.get(targetModule.id)
|
||||
: undefined;
|
||||
const selfScore = parseAndValidateSelfScore(
|
||||
args.self_score,
|
||||
currentModule?.score_dimensions ?? [],
|
||||
);
|
||||
if ("error" in selfScore) {
|
||||
params.handlers.onToolCall?.(call.name, selfScore.error);
|
||||
return {
|
||||
callId: call.id,
|
||||
content: JSON.stringify({ error: selfScore.error }),
|
||||
};
|
||||
}
|
||||
if (tag === CREATION_PLAN_TAG) {
|
||||
const validation = validateCreationPlan(content, params.moduleCatalog);
|
||||
if (!validation.ok) {
|
||||
const err = validation.error ?? "本局创作方案校验失败";
|
||||
params.handlers.onToolCall?.(call.name, err);
|
||||
return {
|
||||
callId: call.id,
|
||||
content: JSON.stringify({
|
||||
error: err,
|
||||
missing_ids: validation.missingIds,
|
||||
duplicate_ids: validation.duplicateIds,
|
||||
unknown_ids: validation.unknownIds,
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
if (repeatableFamilies.size) {
|
||||
const family = dictateProductFamily(tag);
|
||||
const pieces =
|
||||
targetModule?.id === "concrete-instances" ||
|
||||
family === "设计.具体实例"
|
||||
? splitConcreteInstanceContents(content)
|
||||
: [content];
|
||||
const baseTag = pieces.length > 1 ? "设计.具体实例" : tag;
|
||||
const positions: string[] = [];
|
||||
const resolvedPieces: Array<{ tag: string; content: string }> = [];
|
||||
for (const piece of pieces) {
|
||||
const resolved = resolveRepeatableInsertTag({
|
||||
tag: baseTag,
|
||||
content: piece,
|
||||
existingTags: [
|
||||
...params.handlers.listProducts().map((p) => p.tag),
|
||||
...resolvedPieces.map((item) => item.tag),
|
||||
],
|
||||
repeatableFamilies,
|
||||
});
|
||||
if ("error" in resolved) {
|
||||
params.handlers.onToolCall?.(call.name, resolved.error);
|
||||
return {
|
||||
callId: call.id,
|
||||
content: JSON.stringify({ error: resolved.error }),
|
||||
};
|
||||
}
|
||||
resolvedPieces.push({ tag: resolved.tag, content: piece });
|
||||
}
|
||||
let replyModule: string | undefined;
|
||||
for (const [index, piece] of resolvedPieces.entries()) {
|
||||
const pieceTag = piece.tag;
|
||||
const order = parseDictateOrder(args.order);
|
||||
const productsBeforeWrite = params.handlers.listProducts();
|
||||
const isOpening = isDictateOpeningTag(pieceTag);
|
||||
const explicitlyPrepared = openingPrepared;
|
||||
if (isOpening && !explicitlyPrepared) {
|
||||
params.handlers.prepareOpening?.();
|
||||
}
|
||||
params.handlers.writeProduct(pieceTag, piece.content, order, {
|
||||
...(targetModule
|
||||
? {
|
||||
layer: resolveInsertLayer({
|
||||
moduleId: targetModule.id,
|
||||
catalogLayer: targetModule.layer,
|
||||
content: piece.content,
|
||||
}),
|
||||
}
|
||||
: {}),
|
||||
...(selfScore.value ? { selfScore: selfScore.value } : {}),
|
||||
});
|
||||
if (!isOpening) openingPrepared = false;
|
||||
wroteTags.push(pieceTag);
|
||||
positions.push(pieceTag);
|
||||
const detail =
|
||||
order === undefined ? pieceTag : `${pieceTag} order=${order}`;
|
||||
params.handlers.onToolCall?.(call.name, detail);
|
||||
replyModule =
|
||||
params.handlers.lookupInsertFeedback?.(pieceTag)?.trim() ||
|
||||
replyModule;
|
||||
const requiredGaps = isDictateOpeningTag(pieceTag)
|
||||
? requiredCreationPlanGaps({
|
||||
planRaw: productsBeforeWrite.find(
|
||||
(product) => product.tag === CREATION_PLAN_TAG,
|
||||
)?.content,
|
||||
catalog: params.moduleCatalog,
|
||||
products: [
|
||||
...productsBeforeWrite.filter((product) => product.tag !== pieceTag),
|
||||
{ tag: pieceTag, content: piece.content, order },
|
||||
],
|
||||
})
|
||||
: [];
|
||||
if (index === resolvedPieces.length - 1) {
|
||||
if (replyModule) {
|
||||
const title =
|
||||
replyModule.match(/^#\s*(?:模块|本轮关注)\s*·\s*(.+)$/m)?.[1]?.trim() ||
|
||||
pieceTag;
|
||||
moduleNamesThisStep.push(title);
|
||||
}
|
||||
return {
|
||||
callId: call.id,
|
||||
content: JSON.stringify({
|
||||
ok: true,
|
||||
position: positions[0],
|
||||
...(positions.length > 1 ? { positions, split: positions.length } : {}),
|
||||
...(isOpening
|
||||
? {
|
||||
runtime_prepared_before_opening: true,
|
||||
preparation_mode: explicitlyPrepared
|
||||
? "prepare_opening"
|
||||
: "implicit_fallback",
|
||||
}
|
||||
: {}),
|
||||
...(order !== undefined ? { order } : {}),
|
||||
...(requiredGaps.length
|
||||
? {
|
||||
warning: `仍有“必须”能力未落:${requiredGaps.join("、")}。开场白已写入,请在准备开玩前提醒用户补齐或调整方案。`,
|
||||
missing_required_module_ids: requiredGaps,
|
||||
}
|
||||
: {}),
|
||||
...(replyModule
|
||||
? {
|
||||
reply_module: replyModule,
|
||||
reply_hint: DICTATE_MULTI_MODULE_REPLY_HINT,
|
||||
}
|
||||
: {}),
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
const order = parseDictateOrder(args.order);
|
||||
params.handlers.writeProduct(tag, content, order);
|
||||
const productsBeforeWrite = params.handlers.listProducts();
|
||||
const isOpening = isDictateOpeningTag(tag);
|
||||
const explicitlyPrepared = openingPrepared;
|
||||
if (isOpening && !explicitlyPrepared) {
|
||||
params.handlers.prepareOpening?.();
|
||||
}
|
||||
params.handlers.writeProduct(tag, content, order, {
|
||||
...(targetModule
|
||||
? {
|
||||
layer: resolveInsertLayer({
|
||||
moduleId: targetModule.id,
|
||||
catalogLayer: targetModule.layer,
|
||||
content,
|
||||
}),
|
||||
}
|
||||
: {}),
|
||||
...(selfScore.value ? { selfScore: selfScore.value } : {}),
|
||||
});
|
||||
if (!isOpening) openingPrepared = false;
|
||||
wroteTags.push(tag);
|
||||
const detail =
|
||||
order === undefined ? tag : `${tag} order=${order}`;
|
||||
@@ -160,15 +489,41 @@ export async function runDictateTurn(params: {
|
||||
params.handlers.lookupInsertFeedback?.(tag)?.trim() || undefined;
|
||||
if (replyModule) {
|
||||
const title =
|
||||
replyModule.match(/^#\s*模块\s*·\s*(.+)$/m)?.[1]?.trim() || tag;
|
||||
replyModule.match(/^#\s*(?:模块|本轮关注)\s*·\s*(.+)$/m)?.[1]?.trim() || tag;
|
||||
moduleNamesThisStep.push(title);
|
||||
}
|
||||
const requiredGaps = isDictateOpeningTag(tag)
|
||||
? requiredCreationPlanGaps({
|
||||
planRaw: productsBeforeWrite.find(
|
||||
(product) => product.tag === CREATION_PLAN_TAG,
|
||||
)?.content,
|
||||
catalog: params.moduleCatalog,
|
||||
products: [
|
||||
...productsBeforeWrite.filter((product) => product.tag !== tag),
|
||||
{ tag, content, order },
|
||||
],
|
||||
})
|
||||
: [];
|
||||
return {
|
||||
callId: call.id,
|
||||
content: JSON.stringify({
|
||||
ok: true,
|
||||
position: tag,
|
||||
...(isOpening
|
||||
? {
|
||||
runtime_prepared_before_opening: true,
|
||||
preparation_mode: explicitlyPrepared
|
||||
? "prepare_opening"
|
||||
: "implicit_fallback",
|
||||
}
|
||||
: {}),
|
||||
...(order !== undefined ? { order } : {}),
|
||||
...(requiredGaps.length
|
||||
? {
|
||||
warning: `仍有“必须”能力未落:${requiredGaps.join("、")}。开场白已写入,请在准备开玩前提醒用户补齐或调整方案。`,
|
||||
missing_required_module_ids: requiredGaps,
|
||||
}
|
||||
: {}),
|
||||
...(replyModule
|
||||
? {
|
||||
reply_module: replyModule,
|
||||
@@ -376,33 +731,36 @@ export async function runDictateTurn(params: {
|
||||
}
|
||||
}
|
||||
|
||||
if (toolSteps >= DICTATE_DRIVER_MAX_ITERATIONS - 2) {
|
||||
noteTurnLimit(results);
|
||||
}
|
||||
|
||||
return { kind: "continue" as const, results };
|
||||
},
|
||||
});
|
||||
|
||||
let reply = "";
|
||||
if (run.stop.kind === "text") {
|
||||
reply = run.stop.content.trim();
|
||||
}
|
||||
if (!reply) {
|
||||
if (wroteTags.length || deletedTags.length) {
|
||||
const parts: string[] = [];
|
||||
if (wroteTags.length) {
|
||||
parts.push(`已写入:${[...new Set(wroteTags)].join("、")}`);
|
||||
}
|
||||
if (deletedTags.length) {
|
||||
parts.push(`已删除:${[...new Set(deletedTags)].join("、")}`);
|
||||
}
|
||||
reply = `${parts.join(";")}。还有要补充的吗?`;
|
||||
} else if (clearedDialogue) {
|
||||
reply = "对话已清空,产物都还在。可以继续说。";
|
||||
} else {
|
||||
reply = "我在听,请继续说你想要的体验或设定。";
|
||||
}
|
||||
} catch (err) {
|
||||
if (!(err instanceof DriverIterationLimitError)) throw err;
|
||||
return {
|
||||
reply: composeDictateReply({
|
||||
wroteTags,
|
||||
deletedTags,
|
||||
clearedDialogue,
|
||||
capped: true,
|
||||
}),
|
||||
iterations: err.iterations,
|
||||
wroteTags: [...new Set(wroteTags)],
|
||||
deletedTags: [...new Set(deletedTags)],
|
||||
clearedDialogue,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
reply,
|
||||
reply: composeDictateReply({
|
||||
stopText: run.stop.kind === "text" ? run.stop.content : "",
|
||||
wroteTags,
|
||||
deletedTags,
|
||||
clearedDialogue,
|
||||
}),
|
||||
iterations: run.iterations,
|
||||
wroteTags: [...new Set(wroteTags)],
|
||||
deletedTags: [...new Set(deletedTags)],
|
||||
@@ -410,6 +768,52 @@ export async function runDictateTurn(params: {
|
||||
};
|
||||
}
|
||||
|
||||
function noteTurnLimit(
|
||||
results: Array<{ callId: string; content: string }>,
|
||||
): void {
|
||||
const last = results.at(-1);
|
||||
if (!last) return;
|
||||
try {
|
||||
const parsed = JSON.parse(last.content) as Record<string, unknown>;
|
||||
parsed.turn_limit = DICTATE_TURN_LIMIT_HINT;
|
||||
last.content = JSON.stringify(parsed);
|
||||
} catch {
|
||||
last.content = `${last.content}\n${DICTATE_TURN_LIMIT_HINT}`;
|
||||
}
|
||||
}
|
||||
|
||||
function composeDictateReply(params: {
|
||||
stopText?: string;
|
||||
wroteTags: string[];
|
||||
deletedTags: string[];
|
||||
clearedDialogue: boolean;
|
||||
capped?: boolean;
|
||||
}): string {
|
||||
let reply = params.stopText?.trim() ?? "";
|
||||
if (!reply) {
|
||||
if (params.wroteTags.length || params.deletedTags.length) {
|
||||
const parts: string[] = [];
|
||||
if (params.wroteTags.length) {
|
||||
parts.push(`已写入:${[...new Set(params.wroteTags)].join("、")}`);
|
||||
}
|
||||
if (params.deletedTags.length) {
|
||||
parts.push(`已删除:${[...new Set(params.deletedTags)].join("、")}`);
|
||||
}
|
||||
reply = `${parts.join(";")}。还有要补充的吗?`;
|
||||
} else if (params.clearedDialogue) {
|
||||
reply = "对话已清空,产物都还在。可以继续说。";
|
||||
} else if (params.capped) {
|
||||
reply = "这轮工具次数已用完,还没有新的产物。可以说要先补哪一块。";
|
||||
} else {
|
||||
reply = "我在听,请继续说你想要的体验或设定。";
|
||||
}
|
||||
}
|
||||
if (params.capped && !reply.includes("工具次数已用完")) {
|
||||
reply = `${reply} 这轮工具次数已用完,其余下一轮继续。`;
|
||||
}
|
||||
return reply;
|
||||
}
|
||||
|
||||
function safeParseArgs(raw: string): Record<string, unknown> {
|
||||
try {
|
||||
const parsed = JSON.parse(raw || "{}");
|
||||
@@ -421,3 +825,111 @@ function safeParseArgs(raw: string): Record<string, unknown> {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function requiresModuleRead(moduleId: string, auto?: boolean): boolean {
|
||||
if (auto) return false;
|
||||
return !new Set([
|
||||
"narrative",
|
||||
"variable-context",
|
||||
"context-order",
|
||||
"worker-spec",
|
||||
"refine",
|
||||
]).has(moduleId);
|
||||
}
|
||||
|
||||
function resolveInsertLayer(params: {
|
||||
moduleId: string;
|
||||
catalogLayer?: "intermediate" | "final";
|
||||
content: string;
|
||||
}): "intermediate" | "final" {
|
||||
if (params.moduleId === "generation-rules") {
|
||||
const lifecycle = normalizeGenerationRuleLifecycle(
|
||||
extractGenerationRuleLifecycle(params.content),
|
||||
);
|
||||
if (lifecycle === "seed_only") return "intermediate";
|
||||
if (lifecycle === "runtime_only" || lifecycle === "seed_and_runtime") {
|
||||
return "final";
|
||||
}
|
||||
}
|
||||
return params.catalogLayer ?? "final";
|
||||
}
|
||||
|
||||
function extractGenerationRuleLifecycle(
|
||||
content: string,
|
||||
): string | undefined {
|
||||
try {
|
||||
const parsed = JSON.parse(content) as {
|
||||
正文?: { 生命周期?: unknown };
|
||||
生命周期?: unknown;
|
||||
};
|
||||
const raw = parsed.正文?.生命周期 ?? parsed.生命周期;
|
||||
return typeof raw === "string" ? raw.trim() : undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeGenerationRuleLifecycle(
|
||||
raw: string | undefined,
|
||||
): string | undefined {
|
||||
if (!raw) return undefined;
|
||||
if (raw === "只预生成" || raw === "seed_only") return "seed_only";
|
||||
if (raw === "仅游玩期" || raw === "runtime_only") return "runtime_only";
|
||||
if (raw === "预生成并游玩期" || raw === "seed_and_runtime") {
|
||||
return "seed_and_runtime";
|
||||
}
|
||||
return raw;
|
||||
}
|
||||
|
||||
function parseAndValidateSelfScore(
|
||||
raw: unknown,
|
||||
expectedNames: readonly string[],
|
||||
): { value?: DictateSelfScore } | { error: string } {
|
||||
if (expectedNames.length === 0) return {};
|
||||
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
||||
return { error: `self_score 必填;维度:${expectedNames.join("、")}` };
|
||||
}
|
||||
const dims = (raw as { dims?: unknown }).dims;
|
||||
if (!Array.isArray(dims)) {
|
||||
return { error: "self_score.dims 必须是数组" };
|
||||
}
|
||||
const parsed: DictateSelfScore["dims"] = [];
|
||||
for (const item of dims) {
|
||||
if (!item || typeof item !== "object" || Array.isArray(item)) {
|
||||
return { error: "self_score.dims 含无效条目" };
|
||||
}
|
||||
const row = item as Record<string, unknown>;
|
||||
const name = typeof row.name === "string" ? row.name.trim() : "";
|
||||
const score = Number(row.score);
|
||||
const gap = typeof row.gap === "string" ? row.gap.trim() : "";
|
||||
const options = Array.isArray(row.options)
|
||||
? row.options.map(String).map((option) => option.trim()).filter(Boolean)
|
||||
: [];
|
||||
if (!name || !Number.isInteger(score) || score < 1 || score > 5) {
|
||||
return { error: "self_score 每项须含合法 name 与 1~5 整数 score" };
|
||||
}
|
||||
if (score <= 3 && !gap) {
|
||||
return { error: `self_score「${name}」不高于 3 分时必须写 gap` };
|
||||
}
|
||||
if (options.length > 0 && (options.length < 2 || options.length > 3)) {
|
||||
return { error: `self_score「${name}」的 options 必须为 2~3 项` };
|
||||
}
|
||||
parsed.push({
|
||||
name,
|
||||
score,
|
||||
...(gap ? { gap } : {}),
|
||||
...(options.length ? { options } : {}),
|
||||
});
|
||||
}
|
||||
const actual = parsed.map((dim) => dim.name);
|
||||
if (
|
||||
actual.length !== expectedNames.length ||
|
||||
expectedNames.some((name) => !actual.includes(name)) ||
|
||||
actual.some((name) => !expectedNames.includes(name))
|
||||
) {
|
||||
return {
|
||||
error: `self_score 维度必须与能力 score 一致:${expectedNames.join("、")}`,
|
||||
};
|
||||
}
|
||||
return { value: { dims: parsed } };
|
||||
}
|
||||
|
||||
+19
-3
@@ -5,11 +5,25 @@ export const DICTATE_MODE_VALUE = "dictate";
|
||||
|
||||
/** 产物相对序写在黑板 item.metadata 里 */
|
||||
export const DICTATE_ORDER_META_KEY = "dictateOrder";
|
||||
export const DICTATE_LAYER_META_KEY = "dictateLayer";
|
||||
export const DICTATE_SELF_SCORE_META_KEY = "dictateSelfScore";
|
||||
|
||||
export type DictateSelfScore = {
|
||||
dims: Array<{
|
||||
name: string;
|
||||
score: number;
|
||||
gap?: string;
|
||||
options?: string[];
|
||||
}>;
|
||||
};
|
||||
|
||||
export type DictateProduct = {
|
||||
tag: string;
|
||||
content: string;
|
||||
/** 相对顺序:负数靠前,正数靠后;缺省时按 tag 启发式 */
|
||||
/** 产物用途层;目录默认值可被具体产物生命周期覆盖 */
|
||||
layer?: "intermediate" | "final";
|
||||
selfScore?: DictateSelfScore;
|
||||
/** 同一变化频率组内的重要性顺序:负数靠前,正数更靠近实际使用位置 */
|
||||
order?: number;
|
||||
};
|
||||
|
||||
@@ -31,13 +45,15 @@ export function isAllowedProductTag(tag: string): boolean {
|
||||
}
|
||||
|
||||
/**
|
||||
* 未显式给 order 时的默认相对序。
|
||||
* 越稳定/重要越靠前(更负);越常改越靠后(更正)。
|
||||
* 未显式给 order 时的默认重要性序。
|
||||
* 变化频率由产物的「稳变」另行分组;同组内基础材料在前,
|
||||
* 越接近本轮生成合同的内容越靠后(更正)。
|
||||
* 可增殖拆分 tag(基名#槽)按基名归类。
|
||||
*/
|
||||
export function defaultDictateOrder(tag: string): number {
|
||||
const t = tag.trim();
|
||||
const family = t.includes("#") ? t.slice(0, t.indexOf("#")).trim() : t;
|
||||
if (family === "设计.本局创作方案") return -50;
|
||||
if (family === "用户.需求" || t === "用户.需求") return -40;
|
||||
if (family === "设计.变量目录" || family === "设计.变量映射") return -25;
|
||||
if (family === "设计.模仿范例" || family === "设计.模仿要点") return -28;
|
||||
|
||||
@@ -136,11 +136,63 @@ export function looksLikeProductJson(value: unknown): boolean {
|
||||
return PRODUCT_HINT_KEYS.some((k) => k in row);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根对象被提前写成 `} , "下一键"` 时丢掉这个 `}`。
|
||||
* 只在 JSON.parse 失败后使用;合法对象不会出现这种根级衔接。
|
||||
*/
|
||||
export function repairPrematureRootClose(s: string): string {
|
||||
const text = String(s || "");
|
||||
let out = "";
|
||||
let depth = 0;
|
||||
let inString = false;
|
||||
let escaped = false;
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const c = text[i]!;
|
||||
if (inString) {
|
||||
out += c;
|
||||
if (escaped) escaped = false;
|
||||
else if (c === "\\") escaped = true;
|
||||
else if (c === '"') inString = false;
|
||||
continue;
|
||||
}
|
||||
if (c === '"') {
|
||||
inString = true;
|
||||
out += c;
|
||||
continue;
|
||||
}
|
||||
if (c === "{") {
|
||||
depth += 1;
|
||||
out += c;
|
||||
continue;
|
||||
}
|
||||
if (c === "}") {
|
||||
if (depth === 1 && continuesAsSiblingKey(text, i + 1)) continue;
|
||||
if (depth > 0) depth -= 1;
|
||||
out += c;
|
||||
continue;
|
||||
}
|
||||
out += c;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function continuesAsSiblingKey(text: string, from: number): boolean {
|
||||
let j = from;
|
||||
while (j < text.length && /\s/.test(text[j]!)) j += 1;
|
||||
if (text[j] !== ",") return false;
|
||||
j += 1;
|
||||
while (j < text.length && /\s/.test(text[j]!)) j += 1;
|
||||
return text[j] === '"';
|
||||
}
|
||||
|
||||
function parseSlice(slice: string): unknown {
|
||||
const repaired = repairPrematureRootClose(slice);
|
||||
return (
|
||||
tryParse(slice) ??
|
||||
tryParse(softenJsonText(slice)) ??
|
||||
tryParse(softenJsonText(convertStructuralSmartQuotes(slice))) ??
|
||||
tryParse(repaired) ??
|
||||
tryParse(softenJsonText(repaired)) ??
|
||||
tryParse(repairTruncatedJsonObject(slice)) ??
|
||||
tryParse(softenJsonText(repairTruncatedJsonObject(slice)))
|
||||
);
|
||||
@@ -224,3 +276,16 @@ export function selectJsonPayload(content: string, reasoning?: string): string {
|
||||
if (thinkOk) return think;
|
||||
return body || think;
|
||||
}
|
||||
|
||||
/** insert 的 content 若是 JSON 文本,抽成规范对象再落盘。散文原样保留。 */
|
||||
export function canonicalizeInsertedContent(content: string): string {
|
||||
const trimmed = content.trim();
|
||||
if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return content;
|
||||
const parsed = tryParseJsonDoc(trimmed);
|
||||
if (!parsed || typeof parsed !== "object") return content;
|
||||
if (!Array.isArray(parsed)) {
|
||||
const row = parsed as Record<string, unknown>;
|
||||
if (row.schema === "context-fragment-v1") row.schema = "context-fragment.v1";
|
||||
}
|
||||
return JSON.stringify(parsed, null, 2);
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@ function buildSystemPrompt(params: {
|
||||
|
||||
|
||||
|
||||
任务:只写短、可执行的禁区/纠偏/默认取向;不写文风遣词、不写世界观与剧情。产物供「用户需求」节点选用。
|
||||
任务:只写短、可执行的禁区/纠偏/默认取向;不写文风遣词、不写世界观与剧情。产物供绑定偏好库的能力按需选用。
|
||||
|
||||
|
||||
|
||||
|
||||
+12
-1
@@ -58,6 +58,17 @@ export type AgentDriver = {
|
||||
|
||||
export const DEFAULT_DRIVER_MAX_ITERATIONS = 12;
|
||||
|
||||
/** 工具循环用尽轮次仍未收口。 */
|
||||
export class DriverIterationLimitError extends Error {
|
||||
readonly iterations: number;
|
||||
|
||||
constructor(maxIterations: number) {
|
||||
super(`Driver exceeded ${maxIterations} iterations`);
|
||||
this.name = "DriverIterationLimitError";
|
||||
this.iterations = maxIterations;
|
||||
}
|
||||
}
|
||||
|
||||
function assistantMessageFromToolCalls(
|
||||
content: string | null,
|
||||
toolCalls: DriverToolCall[],
|
||||
@@ -158,7 +169,7 @@ export function createLocalLlmDriver(llm: LlmProvider): AgentDriver {
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Driver exceeded ${maxIterations} iterations`);
|
||||
throw new DriverIterationLimitError(maxIterations);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -341,9 +341,9 @@ export async function handleBooksApi(
|
||||
title?: string;
|
||||
/** 编排器包 id(registry name) */
|
||||
orchestratorId?: string;
|
||||
/** 用户手动选定的配方 id(内部 recipe) */
|
||||
/** 旧客户端可传;新建作品改由创作对话判断路径 */
|
||||
recipeId?: string;
|
||||
/** recipe=配方流(默认);dictate=转述整理直填 */
|
||||
/** 旧客户端兼容;新建作品统一使用 dictate */
|
||||
creationMode?: "recipe" | "dictate";
|
||||
};
|
||||
const skills = await listSkills();
|
||||
@@ -363,16 +363,11 @@ export async function handleBooksApi(
|
||||
orchestratorId: director.name,
|
||||
orchestratorName: director.name,
|
||||
});
|
||||
const creationMode =
|
||||
body.creationMode === "dictate" ? "dictate" : "recipe";
|
||||
if (!body.recipeId?.trim()) {
|
||||
json(res, 400, { error: "请选择配方" });
|
||||
return true;
|
||||
}
|
||||
const creationMode = "dictate";
|
||||
const session = await sessionManager.createForBook(
|
||||
book.id,
|
||||
director.name,
|
||||
body.recipeId.trim(),
|
||||
body.recipeId?.trim() || undefined,
|
||||
{ creationMode },
|
||||
);
|
||||
json(res, 201, { book: getBook(book.id) ?? book, session });
|
||||
|
||||
+102
-17
@@ -133,6 +133,7 @@ import {
|
||||
findRecipeCatalogEntry,
|
||||
formatCreationFlowForUser,
|
||||
formatDictateRecipeBrief,
|
||||
formatRecipeExamplesForCreationPlan,
|
||||
formatModuleCatalogForDictate,
|
||||
findModuleByName,
|
||||
findStepByRef,
|
||||
@@ -167,7 +168,11 @@ import {
|
||||
import {
|
||||
CREATION_INTAKE_MODE_TAG,
|
||||
DICTATE_MODE_VALUE,
|
||||
DICTATE_LAYER_META_KEY,
|
||||
DICTATE_ORDER_META_KEY,
|
||||
DICTATE_SELF_SCORE_META_KEY,
|
||||
applyDictatePlayBind,
|
||||
buildDictateModuleReadIndex,
|
||||
buildDictateInsertFeedbackIndex,
|
||||
defaultDictateOrder,
|
||||
extractDictateDialogue,
|
||||
@@ -559,9 +564,9 @@ export class SessionManager {
|
||||
content: expanded,
|
||||
source: "user",
|
||||
});
|
||||
const first = s.messages[0];
|
||||
if (first?.title === "开场白") {
|
||||
s.messages[0] = this.playOpeningMessage(expanded);
|
||||
const openingAt = s.messages.findIndex((m) => m.title === "开场白");
|
||||
if (openingAt >= 0) {
|
||||
s.messages[openingAt] = this.playOpeningMessage(expanded);
|
||||
}
|
||||
if (s.bookId) this.persist(s);
|
||||
return this.toView(sessionId);
|
||||
@@ -788,7 +793,17 @@ export class SessionManager {
|
||||
typeof rawOrder === "number" && Number.isFinite(rawOrder)
|
||||
? rawOrder
|
||||
: undefined;
|
||||
products.push({ tag: e.tag, content, order });
|
||||
const layer = item?.metadata?.[DICTATE_LAYER_META_KEY];
|
||||
const selfScore = item?.metadata?.[DICTATE_SELF_SCORE_META_KEY];
|
||||
products.push({
|
||||
tag: e.tag,
|
||||
content,
|
||||
order,
|
||||
...(layer === "intermediate" || layer === "final" ? { layer } : {}),
|
||||
...(selfScore && typeof selfScore === "object"
|
||||
? { selfScore: selfScore as DictateProduct["selfScore"] }
|
||||
: {}),
|
||||
});
|
||||
}
|
||||
return sortDictateProducts(products);
|
||||
}
|
||||
@@ -837,20 +852,32 @@ export class SessionManager {
|
||||
recipeView,
|
||||
recipeOptions: s.recipeOptions,
|
||||
});
|
||||
const moduleGuide = await resolveDictateModuleGuideForSession({
|
||||
const moduleCatalog = await resolveDictateModuleCatalogForSession({
|
||||
skillName: s.runtime.getActiveSkill()?.name,
|
||||
moduleCatalog: s.moduleCatalog,
|
||||
});
|
||||
if (moduleCatalog) s.moduleCatalog = moduleCatalog;
|
||||
const moduleGuide = moduleCatalog
|
||||
? formatModuleCatalogForDictate(moduleCatalog)
|
||||
: undefined;
|
||||
const insertFeedbackIndex = await resolveDictateInsertFeedbackIndexForSession({
|
||||
skillName: s.runtime.getActiveSkill()?.name,
|
||||
moduleCatalog: s.moduleCatalog,
|
||||
moduleCatalog,
|
||||
});
|
||||
const repeatableFamilies = [
|
||||
...(s.moduleCatalog?.modules ?? [])
|
||||
...(moduleCatalog?.modules ?? [])
|
||||
.filter((m) => m.repeatable === true && m.artifact?.trim())
|
||||
.map((m) => m.artifact!.trim()),
|
||||
...DICTATE_OPENING_REPEATABLE_FAMILIES,
|
||||
];
|
||||
const recipeExamples = await resolveDictateRecipeExamplesForSession({
|
||||
skillName: s.runtime.getActiveSkill()?.name,
|
||||
recipeOptions: s.recipeOptions,
|
||||
});
|
||||
const moduleReadIndex = await resolveDictateModuleReadIndexForSession({
|
||||
skillName: s.runtime.getActiveSkill()?.name,
|
||||
moduleCatalog,
|
||||
});
|
||||
|
||||
const result = await runDictateTurn({
|
||||
llm,
|
||||
@@ -858,10 +885,13 @@ export class SessionManager {
|
||||
recipeName: recipeView?.name,
|
||||
recipeBrief,
|
||||
moduleGuide,
|
||||
recipeExamples,
|
||||
moduleCatalog,
|
||||
moduleReadIndex,
|
||||
repeatableFamilies,
|
||||
handlers: {
|
||||
listProducts: () => this.listDictateProducts(s),
|
||||
writeProduct: (tag, content, order) => {
|
||||
writeProduct: (tag, content, order, metadata) => {
|
||||
const board = s.runtime.getBlackboard();
|
||||
let writeContent = content;
|
||||
if (dictateProductFamily(tag) === OPENING_PRODUCT_FAMILY) {
|
||||
@@ -894,6 +924,12 @@ export class SessionManager {
|
||||
metadata: {
|
||||
...(existing?.metadata ?? {}),
|
||||
[DICTATE_ORDER_META_KEY]: resolved,
|
||||
...(metadata?.layer
|
||||
? { [DICTATE_LAYER_META_KEY]: metadata.layer }
|
||||
: {}),
|
||||
...(metadata?.selfScore
|
||||
? { [DICTATE_SELF_SCORE_META_KEY]: metadata.selfScore }
|
||||
: {}),
|
||||
},
|
||||
});
|
||||
},
|
||||
@@ -912,6 +948,9 @@ export class SessionManager {
|
||||
});
|
||||
},
|
||||
deleteTag: (tag) => s.runtime.getBlackboard().deleteByTag(tag),
|
||||
prepareOpening: () => {
|
||||
applyDictatePlayBind(s.runtime.getBlackboard());
|
||||
},
|
||||
clearDialogue: () => {
|
||||
const kept = this.listDictateProducts(s);
|
||||
const lastUser = [...s.messages]
|
||||
@@ -962,7 +1001,9 @@ export class SessionManager {
|
||||
},
|
||||
});
|
||||
|
||||
this.clearAgentThinking(s.runtime.getSession().id);
|
||||
const sessionId = s.runtime.getSession().id;
|
||||
const thinking = this.agentThinkingLive.get(sessionId)?.trim() || undefined;
|
||||
this.clearAgentThinking(sessionId);
|
||||
recordPreMessageCheckpoint(
|
||||
s.branchState,
|
||||
s.messages.length,
|
||||
@@ -977,6 +1018,7 @@ export class SessionManager {
|
||||
actor: "orchestrator",
|
||||
title: "转述整理",
|
||||
body: result.reply,
|
||||
...(thinking ? { thinking } : {}),
|
||||
});
|
||||
s.runtime.enterDictateIntake();
|
||||
}
|
||||
@@ -4346,27 +4388,70 @@ async function resolveDictateRecipeBriefForSession(params: {
|
||||
return view.declaration?.trim() || undefined;
|
||||
}
|
||||
|
||||
/** 对话落盘:注入能力何时落盘(modules meta);优先会话已载 catalog */
|
||||
async function resolveDictateModuleGuideForSession(params: {
|
||||
/** 对话落盘:加载可混合的参考案例,不把任何案例当作已选路径。 */
|
||||
async function resolveDictateRecipeExamplesForSession(params: {
|
||||
skillName?: string | null;
|
||||
recipeOptions?: RecipeCatalogEntry[];
|
||||
}): Promise<string | undefined> {
|
||||
const entries = (params.recipeOptions ?? []).filter(
|
||||
(entry) => entry.family === "dictate",
|
||||
);
|
||||
const skillName = params.skillName?.trim();
|
||||
if (!skillName || entries.length === 0) return undefined;
|
||||
try {
|
||||
const skill = await loadSkill(skillName);
|
||||
const packRoot = skill.skillPackRoot?.trim();
|
||||
if (!packRoot) return undefined;
|
||||
const details = await Promise.all(
|
||||
entries.map((entry) => loadRecipeDetail(packRoot, entry)),
|
||||
);
|
||||
return formatRecipeExamplesForCreationPlan(details) || undefined;
|
||||
} catch (err) {
|
||||
console.error("[会话] 加载本局创作方案参考案例失败", err);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/** 对话落盘:取得含 prompt meta 的能力目录;优先复用会话已载目录 */
|
||||
async function resolveDictateModuleCatalogForSession(params: {
|
||||
skillName?: string | null;
|
||||
moduleCatalog?: ModuleCatalog | null;
|
||||
}): Promise<string | undefined> {
|
||||
}): Promise<ModuleCatalog | null> {
|
||||
let catalog = params.moduleCatalog ?? null;
|
||||
if (!catalog?.modules?.length) {
|
||||
const skillName = params.skillName?.trim();
|
||||
if (!skillName) return undefined;
|
||||
if (!skillName) return null;
|
||||
try {
|
||||
const skill = await loadSkill(skillName);
|
||||
const packRoot = skill.skillPackRoot?.trim();
|
||||
if (!packRoot) return undefined;
|
||||
if (!packRoot) return null;
|
||||
catalog = await loadModuleCatalog(packRoot);
|
||||
} catch (err) {
|
||||
console.error("[会话] 加载转述能力目录失败", err);
|
||||
return undefined;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (!catalog?.modules?.length) return undefined;
|
||||
return formatModuleCatalogForDictate(catalog);
|
||||
return catalog?.modules?.length ? catalog : null;
|
||||
}
|
||||
|
||||
async function resolveDictateModuleReadIndexForSession(params: {
|
||||
skillName?: string | null;
|
||||
moduleCatalog?: ModuleCatalog | null;
|
||||
}) {
|
||||
const skillName = params.skillName?.trim();
|
||||
if (!skillName || !params.moduleCatalog?.modules.length) return undefined;
|
||||
try {
|
||||
const skill = await loadSkill(skillName);
|
||||
const packRoot = skill.skillPackRoot?.trim();
|
||||
if (!packRoot) return undefined;
|
||||
return await buildDictateModuleReadIndex({
|
||||
skillPackRoot: packRoot,
|
||||
catalog: params.moduleCatalog,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("[会话] 构建能力读取索引失败", err);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/** 对话落盘:insert(tag) → 用户可见规范索引 */
|
||||
|
||||
+179
-24
@@ -11,9 +11,9 @@ import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { parse as parseYaml } from "yaml";
|
||||
import {
|
||||
collectLibraryIdsFromModules,
|
||||
formatBoundLibrariesForPrompt,
|
||||
} from "../libraries/index.js";
|
||||
buildRepeatableProductTag,
|
||||
dictateProductSlot,
|
||||
} from "../dictate/repeatable-tags.js";
|
||||
import { tryParseJsonDoc } from "../parse/json-doc.js";
|
||||
|
||||
export const CREATION_FLOW_TAG = "设计.创作流程";
|
||||
@@ -179,6 +179,10 @@ export type ModuleCatalogEntry = {
|
||||
declaration: string;
|
||||
/** 执行期产物 tag(流程 JSON 不写;程序映射) */
|
||||
artifact: string;
|
||||
/** 默认产物层;生成规则可在运行时按 lifecycle_intent 覆盖 */
|
||||
layer?: "intermediate" | "final";
|
||||
/** 默认游玩挂载目标;具体字段仍由投影器裁剪 */
|
||||
mount?: string[];
|
||||
/**
|
||||
* 可选:默认可反复编排进流程(如生成规则、具体实例)。
|
||||
* 程序不硬拦;给编排与校验提示。
|
||||
@@ -286,6 +290,16 @@ export type RecipeDetail = {
|
||||
brief?: string;
|
||||
/** 适用什么体验/任务 */
|
||||
when?: string;
|
||||
/** 对话落盘案例:可识别的用户信号 */
|
||||
signals?: string[];
|
||||
/** 对话落盘案例:可混合、可改写的方法,不表示步骤顺序 */
|
||||
method?: string[];
|
||||
/** 对话落盘案例:能力档位的默认倾向,不覆盖本局判断 */
|
||||
tierTendencies?: Array<{
|
||||
module_id: string;
|
||||
tier: "必须" | "有必要" | "有一定效果" | "没有意义";
|
||||
why: string;
|
||||
}>;
|
||||
/** 整套设计方法的核心思路与最终目标 */
|
||||
core?: string;
|
||||
/** 设计流程:如何增量选型、何时收成(字符串或条目列表) */
|
||||
@@ -961,6 +975,11 @@ export function parseModuleCatalog(raw: string): ModuleCatalog | null {
|
||||
const params = parseModuleParamSpecs(m.params);
|
||||
const intake = parseModuleIntake(m.intake);
|
||||
const libraries = parseModuleLibraries(m.libraries);
|
||||
const layer =
|
||||
m.layer === "intermediate" || m.layer === "final"
|
||||
? m.layer
|
||||
: undefined;
|
||||
const mount = parseModuleLibraries(m.mount);
|
||||
modules.push({
|
||||
id,
|
||||
name,
|
||||
@@ -974,6 +993,8 @@ export function parseModuleCatalog(raw: string): ModuleCatalog | null {
|
||||
...(params ? { params } : {}),
|
||||
...(intake ? { intake } : {}),
|
||||
...(libraries ? { libraries } : {}),
|
||||
...(layer ? { layer } : {}),
|
||||
...(mount ? { mount } : {}),
|
||||
});
|
||||
}
|
||||
if (modules.length === 0) return null;
|
||||
@@ -1027,6 +1048,7 @@ export const MODULE_SECTION_IDS = [
|
||||
"principles",
|
||||
"probe",
|
||||
"output",
|
||||
"score",
|
||||
"checklist",
|
||||
"examples",
|
||||
] as const;
|
||||
@@ -1158,6 +1180,7 @@ export function formatModulePromptForLlm(promptMd: string): string {
|
||||
"principles",
|
||||
"probe",
|
||||
"output",
|
||||
"score",
|
||||
"checklist",
|
||||
"examples",
|
||||
];
|
||||
@@ -1303,18 +1326,25 @@ function slugFromName(name: string): string {
|
||||
具体实例: "concrete-instances",
|
||||
拓扑图谱: "topology",
|
||||
回复呈现: "status-bar",
|
||||
"0层回复呈现": "zero-layer-status",
|
||||
设计状态栏: "status-bar",
|
||||
设计监控栏: "status-bar",
|
||||
变量设计与更新规则: "variable-design",
|
||||
信息可见范围: "variable-context",
|
||||
变量控制上下文: "variable-context",
|
||||
设计回复格式: "reply-format",
|
||||
正文组成: "reply-format",
|
||||
"0层正文组成": "zero-layer-reply-format",
|
||||
随机范围整理: "random-range",
|
||||
游玩执行方式: "worker-spec",
|
||||
游玩拓扑: "worker-spec",
|
||||
"Worker 规格": "worker-spec", // 旧称,等同游玩拓扑
|
||||
上下文排序: "context-order",
|
||||
运行配置组装: "refine",
|
||||
细化终稿: "refine",
|
||||
开场白与开场变量: "opening-setup",
|
||||
开场白: "opening-setup",
|
||||
"0层开场白与初态": "zero-layer-opening-setup",
|
||||
};
|
||||
return map[name] ?? name;
|
||||
}
|
||||
@@ -1387,6 +1417,7 @@ export function formatModuleCatalogForAgent(catalog: ModuleCatalog): string {
|
||||
|
||||
/** 对话落盘不经 DAG:这些能力由落档/开玩程序处理,创作对话一般不 insert */
|
||||
const DICTATE_PROGRAM_SIDE_MODULE_IDS = new Set([
|
||||
"variable-context",
|
||||
"worker-spec",
|
||||
"context-order",
|
||||
"refine",
|
||||
@@ -1420,19 +1451,17 @@ export function formatModuleCatalogForDictate(catalog: ModuleCatalog): string {
|
||||
});
|
||||
const head = [
|
||||
"【能力 · 何时落盘】",
|
||||
"对照下方「何时用 / 何时不用 / 边界」与已有产物判定:条件成立才 insert(或 declare_*);含糊或未到时机 → 本轮不落该能力,改为短确认或问挡住的 1 点。",
|
||||
"同轮可多次落盘:凡本轮材料已够且「何时用」成立的能力,都可 insert / declare_*(可并行);每个 insert 各回 reply_module,可见回复分段附带。",
|
||||
"不抢跑:条件未成立的下游本轮不落,放进「建议下一刀」。",
|
||||
"对照下方「何时用 / 何时不用 / 边界」与已有产物:用户这句已经说清、条件也成立的能力才 insert(或 declare_*),一轮通常 2~3 个。说清指用户已经给出要写的内容,不是能从题材推演出来。",
|
||||
"「必须」和「有必要」里用户还没提到的:本轮不落盘,按已有材料给 2~3 个选项询问,选定后再写。含糊或未到时机的下游同样先问,不先写。",
|
||||
"同轮写入的每份产物各回 reply_module,可见回复综合成一次。",
|
||||
"标〔落档程序〕的:创作对话一般不 insert,开玩/落档时由程序处理。",
|
||||
"标〔收口〕的开场白:美学与必要上游齐后再写;未齐则先补上游。",
|
||||
"标〔可反复〕的(生成规则/具体实例等):每条增殖用「基名#唯一id」拆分落盘(如 设计.具体实例#lei-ying);禁止反复 insert 基名覆盖旧条。改某一条则 insert 同一完整 tag。",
|
||||
"标〔收口〕的开场:写开场前,列出仍未落的「必须」和「有必要」,问用户这些还没设计要补哪几项或明确不要。用户这句已经说清的再写入。普通模式使用回复呈现/正文组成,0 层模式使用对应 0 层节点,不得混用。用户确认可以收口后,程序依次编译信息可见范围、游玩执行方式、上下文排序与运行配置,再写所选模式的开场产物。",
|
||||
"标〔可反复〕的(生成规则、具体实例、开场白、拓扑等):每条单独落盘,用「基名#唯一id」(生成规则用规则名,具体实例用名称,开场白用场景短码)。同完整 tag 才覆盖,禁止反复写基名盖掉别的条。开场全部保留;游玩时左右切换,选中的一条作为 0 层。",
|
||||
"能力执行全文不进聊天;规格只走 toolcall。insert 成功后按返回的 reply_module 写可见回复(见系统提示「对用户可见回复」)。",
|
||||
lines.join("\n"),
|
||||
].join("\n");
|
||||
|
||||
const libIds = collectLibraryIdsFromModules(dictateModules);
|
||||
const libBlock = formatBoundLibrariesForPrompt(libIds).trim();
|
||||
return libBlock ? `${head}\n\n${libBlock}` : head;
|
||||
return head;
|
||||
}
|
||||
|
||||
function dictateLandHint(m: ModuleCatalogEntry): string {
|
||||
@@ -1443,17 +1472,20 @@ function dictateLandHint(m: ModuleCatalogEntry): string {
|
||||
return "declare_variable / declare_map;长文规则可另 insert 「设计.变量设计与更新规则」";
|
||||
}
|
||||
if (m.id === "opening-setup") {
|
||||
return "insert 「设计.开场白」(对话落盘简化 tag;初值与开场同真相)";
|
||||
return "insert 「设计.开场白#场景短码」——可增殖:每条开场各占一 tag;同完整 tag 才覆盖。游玩时左右切换,选中的一条作为 0 层";
|
||||
}
|
||||
if (m.id === "zero-layer-opening-setup") {
|
||||
return "insert 「设计.0层开场白与初态#场景短码」——可增殖:每条开场各占一 tag;同完整 tag 才覆盖。游玩时左右切换,选中的一条作为 0 层";
|
||||
}
|
||||
if (m.artifact?.trim() && m.repeatable) {
|
||||
const art = m.artifact.trim();
|
||||
const slotHint =
|
||||
m.id === "generation-rules"
|
||||
? "rule_id"
|
||||
? "规则名"
|
||||
: m.id === "concrete-instances"
|
||||
? "batch_id或姓名"
|
||||
? "名称"
|
||||
: "唯一id";
|
||||
return `insert 「${art}#${slotHint}」——可增殖:每条对象/批次各占一 tag;同完整 tag 才覆盖改写,禁止反复写基名「${art}」覆盖`;
|
||||
return `insert 「${art}#${slotHint}」——每条单独落盘;同完整 tag 才覆盖,禁止反复写基名「${art}」盖掉别的条`;
|
||||
}
|
||||
if (m.artifact?.trim()) {
|
||||
return `insert 「${m.artifact.trim()}」`;
|
||||
@@ -1555,6 +1587,9 @@ export function parseRecipeYaml(
|
||||
const core = coerceYamlTextField(row.core);
|
||||
const process = normalizeRecipeListOrText(row.process);
|
||||
const principles = normalizeRecipeListOrText(row.principles);
|
||||
const signals = normalizeStringList(row.signals);
|
||||
const method = normalizeStringList(row.method);
|
||||
const tierTendencies = parseRecipeTierTendencies(row.tier_tendencies);
|
||||
const hint =
|
||||
typeof row.hint === "string" && row.hint.trim()
|
||||
? row.hint.trim()
|
||||
@@ -1581,6 +1616,9 @@ export function parseRecipeYaml(
|
||||
declaration: meta.declaration,
|
||||
...(brief ? { brief } : {}),
|
||||
when,
|
||||
...(signals ? { signals } : {}),
|
||||
...(method ? { method } : {}),
|
||||
...(tierTendencies ? { tierTendencies } : {}),
|
||||
...(core ? { core } : {}),
|
||||
...(process ? { process } : {}),
|
||||
...(principles ? { principles } : {}),
|
||||
@@ -1603,6 +1641,37 @@ function normalizeRecipeListOrText(
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function normalizeStringList(raw: unknown): string[] | undefined {
|
||||
if (!Array.isArray(raw)) return undefined;
|
||||
const items = raw
|
||||
.map((item) => (typeof item === "string" ? item.trim() : ""))
|
||||
.filter(Boolean);
|
||||
return items.length ? items : undefined;
|
||||
}
|
||||
|
||||
function parseRecipeTierTendencies(
|
||||
raw: unknown,
|
||||
): RecipeDetail["tierTendencies"] | undefined {
|
||||
if (!Array.isArray(raw)) return undefined;
|
||||
const tiers = new Set(["必须", "有必要", "有一定效果", "没有意义"]);
|
||||
const out: NonNullable<RecipeDetail["tierTendencies"]> = [];
|
||||
for (const item of raw) {
|
||||
if (!item || typeof item !== "object" || Array.isArray(item)) continue;
|
||||
const row = item as Record<string, unknown>;
|
||||
const moduleId =
|
||||
typeof row.module_id === "string" ? row.module_id.trim() : "";
|
||||
const tier = typeof row.tier === "string" ? row.tier.trim() : "";
|
||||
const why = typeof row.why === "string" ? row.why.trim() : "";
|
||||
if (!moduleId || !tiers.has(tier) || !why) continue;
|
||||
out.push({
|
||||
module_id: moduleId,
|
||||
tier: tier as NonNullable<RecipeDetail["tierTendencies"]>[number]["tier"],
|
||||
why,
|
||||
});
|
||||
}
|
||||
return out.length ? out : undefined;
|
||||
}
|
||||
|
||||
export async function loadRecipeDetail(
|
||||
skillPackRoot: string,
|
||||
entry: RecipeCatalogEntry,
|
||||
@@ -1719,6 +1788,42 @@ export function formatDictateRecipeBrief(detail: RecipeDetail): string {
|
||||
return lines.join("\n").trim();
|
||||
}
|
||||
|
||||
/** 本局创作方案使用的旧配方案例目录:只给识别信号、方法与档位倾向,不提供固定流程。 */
|
||||
export function formatRecipeExamplesForCreationPlan(
|
||||
details: readonly RecipeDetail[],
|
||||
): string {
|
||||
return details
|
||||
.filter(
|
||||
(detail) =>
|
||||
detail.signals?.length ||
|
||||
detail.method?.length ||
|
||||
detail.tierTendencies?.length,
|
||||
)
|
||||
.map((detail) => {
|
||||
const lines = [
|
||||
`- ${detail.id}(${detail.name}):${detail.brief ?? detail.declaration}`,
|
||||
];
|
||||
if (detail.signals?.length) {
|
||||
lines.push(` 适用信号:${detail.signals.join(";")}`);
|
||||
}
|
||||
if (detail.method?.length) {
|
||||
lines.push(` 方法:${detail.method.join(";")}`);
|
||||
}
|
||||
if (detail.tierTendencies?.length) {
|
||||
lines.push(
|
||||
` 档位倾向:${detail.tierTendencies
|
||||
.map(
|
||||
(item) =>
|
||||
`${item.module_id}=${item.tier}(${item.why})`,
|
||||
)
|
||||
.join(";")}`,
|
||||
);
|
||||
}
|
||||
return lines.join("\n");
|
||||
})
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
/** 注入 design-flow:用户已选配方(方法论 + 开局起点) */
|
||||
export function formatSelectedRecipeForAgent(detail: RecipeDetail): string {
|
||||
const lines: string[] = [
|
||||
@@ -1915,6 +2020,7 @@ export function formatFlowProgressForAgent(params: {
|
||||
const closerPending =
|
||||
catalog && flow
|
||||
? flow.steps.filter((step) => {
|
||||
if (isPrototypeStep(step)) return false;
|
||||
const mod = catalog.modules.find((m) => m.name === step.name);
|
||||
return Boolean(mod?.closer) && !isStepAccepted(step, accepted);
|
||||
})
|
||||
@@ -2146,6 +2252,39 @@ export function artifactTagForStep(
|
||||
return catalog?.modules.find((m) => m.name === name)?.artifact ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 可增殖实例的落盘 tag。生成规则、具体实例、开场白、拓扑各自占一条,
|
||||
* 不写基名,避免后写的实例盖掉先写的。
|
||||
* 槽位优先用步骤 id 的 #后缀(稳定),否则用规则名 / 这一条。
|
||||
*/
|
||||
export function repeatableInstanceProductTag(input: {
|
||||
module: Pick<ModuleCatalogEntry, "id" | "name" | "artifact" | "repeatable">;
|
||||
step: Pick<CreationFlowStep, "id" | "role" | "params">;
|
||||
}): string | null {
|
||||
if (input.step.role === "prototype") return null;
|
||||
const repeatable =
|
||||
input.module.repeatable === true ||
|
||||
input.module.id === "opening-setup" ||
|
||||
input.module.id === "zero-layer-opening-setup";
|
||||
if (!repeatable) return null;
|
||||
const family =
|
||||
input.module.id === "opening-setup"
|
||||
? "设计.开场白"
|
||||
: input.module.id === "zero-layer-opening-setup"
|
||||
? "设计.0层开场白与初态"
|
||||
: input.module.artifact?.trim() || "";
|
||||
if (!family) return null;
|
||||
const slot =
|
||||
dictateProductSlot(input.step.id) ||
|
||||
flowParamText(input.step.params, "rule_id") ||
|
||||
flowParamText(input.step.params, "batch_goal") ||
|
||||
(input.step.id.trim() && input.step.id.trim() !== input.module.name
|
||||
? input.step.id.trim()
|
||||
: "");
|
||||
if (!slot) return null;
|
||||
return buildRepeatableProductTag(family, slot);
|
||||
}
|
||||
|
||||
export function finalizeCreationFlow(
|
||||
flow: CreationFlow,
|
||||
catalog: ModuleCatalog | null | undefined,
|
||||
@@ -2239,6 +2378,9 @@ function titleFromAcceptedContent(content: unknown): string | undefined {
|
||||
const row = asTitleRecord(content);
|
||||
if (!row) return undefined;
|
||||
const body = asTitleRecord(row.正文) ?? row;
|
||||
if (typeof body.名称 === "string" && body.名称.trim()) {
|
||||
return clampFlowTitle(body.名称);
|
||||
}
|
||||
const pinned = asTitleRecord(body.本步参数);
|
||||
if (typeof pinned?.target === "string" && pinned.target.trim()) {
|
||||
return clampFlowTitle(pinned.target);
|
||||
@@ -2253,6 +2395,9 @@ function titleFromAcceptedContent(content: unknown): string | undefined {
|
||||
if (typeof rec?.对象 === "string" && rec.对象.trim()) {
|
||||
return clampFlowTitle(rec.对象);
|
||||
}
|
||||
if (typeof rec?.名称 === "string" && rec.名称.trim()) {
|
||||
return clampFlowTitle(rec.名称);
|
||||
}
|
||||
}
|
||||
if (typeof row.brief === "string" && row.brief.trim() && row.brief.trim().length <= FLOW_TITLE_MAX) {
|
||||
return row.brief.trim();
|
||||
@@ -2674,7 +2819,7 @@ export function listRepeatableSpawns(
|
||||
acceptedStepIds: readonly string[],
|
||||
): CreationFlowSpawnView[] {
|
||||
return catalogModulesForIntake(catalog, "recipe")
|
||||
.filter((m) => m.repeatable === true && !m.closer)
|
||||
.filter((m) => m.repeatable === true)
|
||||
.filter(
|
||||
(m) => !flow.steps.some((s) => s.name === m.name && isPrototypeStep(s)),
|
||||
)
|
||||
@@ -2776,7 +2921,8 @@ export function spawnInstanceFromPrototype(params: {
|
||||
{ ...params.flow, steps },
|
||||
params.catalog,
|
||||
);
|
||||
if (closerAt >= 0) {
|
||||
// 开场自身是可增殖收口:新实例跟在原型后面,不要写回原型的 depends_on
|
||||
if (closerAt >= 0 && steps[closerAt]!.id !== prototype.id) {
|
||||
const closer = steps[closerAt]!;
|
||||
if (!closer.depends_on.includes(id)) {
|
||||
steps[closerAt] = {
|
||||
@@ -2958,8 +3104,11 @@ export function spawnRepeatableCreationStep(params: {
|
||||
|
||||
const mod = findModuleByName(params.catalog, name);
|
||||
if (!mod) return { error: `未知能力:${name}` };
|
||||
if (mod.repeatable !== true) return { error: `「${name}」不能反复新开` };
|
||||
if (isCloserModule(mod, name)) return { error: `「${name}」是收口,不能增殖` };
|
||||
if (mod.repeatable !== true) {
|
||||
return isCloserModule(mod, name)
|
||||
? { error: `「${name}」是收口,不能增殖` }
|
||||
: { error: `「${name}」不能反复新开` };
|
||||
}
|
||||
const blocked = repeatableSpawnBlockedReason(
|
||||
params.flow,
|
||||
params.catalog,
|
||||
@@ -3065,10 +3214,14 @@ export function dependencyArtifactTags(
|
||||
if (!catalog) return [];
|
||||
const tags: string[] = [];
|
||||
for (const dep of step.depends_on) {
|
||||
const depName = flow
|
||||
? findStepByRef(flow, dep)?.name ?? dep
|
||||
: dep;
|
||||
const art = artifactTagForStep(depName, catalog);
|
||||
const depStep = flow ? findStepByRef(flow, dep) : undefined;
|
||||
const depName = depStep?.name ?? dep;
|
||||
const mod = findModuleByName(catalog, depName);
|
||||
const slotted =
|
||||
depStep && mod
|
||||
? repeatableInstanceProductTag({ module: mod, step: depStep })
|
||||
: null;
|
||||
const art = slotted || artifactTagForStep(depName, catalog);
|
||||
if (art) tags.push(art);
|
||||
}
|
||||
return [...new Set(tags)];
|
||||
@@ -3130,7 +3283,9 @@ export async function resolveDesignStepBinding(params: {
|
||||
|
||||
const inherit =
|
||||
Boolean(params.inheritExisting) || isReviseStep(step);
|
||||
const inheritTag = inherit ? module.artifact : null;
|
||||
const inheritTag = inherit
|
||||
? repeatableInstanceProductTag({ module, step }) ?? module.artifact ?? null
|
||||
: null;
|
||||
const opening =
|
||||
inherit || module.auto
|
||||
? null
|
||||
|
||||
+16
-8
@@ -515,6 +515,7 @@ export async function loadWorkerSkillWithContext(
|
||||
if (workerId.trim() === "design-step" && skill.skillPackRoot) {
|
||||
const {
|
||||
resolveDesignStepBinding,
|
||||
repeatableInstanceProductTag,
|
||||
CREATION_CURRENT_STEP_TAG,
|
||||
CREATION_MODULE_OPENING_TAG,
|
||||
formatPriorArtifactContext,
|
||||
@@ -550,18 +551,20 @@ export async function loadWorkerSkillWithContext(
|
||||
const { formatPlayAgentRosterForPrompt } = await import("./play-slots.js");
|
||||
rosterBlock = `${formatPlayAgentRosterForPrompt()}\n\n`;
|
||||
}
|
||||
if (binding.module.id === "reply-format") {
|
||||
const {
|
||||
loadPresentShellCatalog,
|
||||
formatPresentShellsForPrompt,
|
||||
} = await import("./present-shell-catalog.js");
|
||||
const shells = await loadPresentShellCatalog(
|
||||
if (
|
||||
binding.module.id === "reply-format" ||
|
||||
binding.module.id === "zero-layer-reply-format"
|
||||
) {
|
||||
const { loadShellRefCatalog, formatShellRefsForPrompt } = await import(
|
||||
"./shell-ref-catalog.js"
|
||||
);
|
||||
const shells = await loadShellRefCatalog(
|
||||
skill.skillPackRoot,
|
||||
skillsRoot,
|
||||
);
|
||||
const parts: string[] = [];
|
||||
if (shells.length) {
|
||||
parts.push(formatPresentShellsForPrompt(shells));
|
||||
parts.push(formatShellRefsForPrompt(shells));
|
||||
}
|
||||
const {
|
||||
loadMarkdownSafeSubsetCatalog,
|
||||
@@ -605,7 +608,12 @@ export async function loadWorkerSkillWithContext(
|
||||
...(inheritTag ? [inheritTag] : []),
|
||||
];
|
||||
const inputTags = [...new Set(baseInputs)];
|
||||
const outputTags = [binding.module.artifact];
|
||||
const outputTags = [
|
||||
repeatableInstanceProductTag({
|
||||
module: binding.module,
|
||||
step: binding.step,
|
||||
}) ?? binding.module.artifact,
|
||||
];
|
||||
const depSegments = depTags.map((tag, i) => ({
|
||||
id: `dep-${i}`,
|
||||
tier: "static" as const,
|
||||
|
||||
@@ -55,12 +55,20 @@ function findRuleBody(
|
||||
): Record<string, unknown> | undefined {
|
||||
const body = parsed.正文;
|
||||
if (!body || typeof body !== "object" || Array.isArray(body)) return undefined;
|
||||
const rules = (body as Record<string, unknown>).rules;
|
||||
const bodyRec = body as Record<string, unknown>;
|
||||
const directId =
|
||||
asString(bodyRec.规则名) ??
|
||||
asString(bodyRec.rule_id) ??
|
||||
asString(bodyRec.id) ??
|
||||
asString(bodyRec.规则id);
|
||||
if (directId === ruleId) return bodyRec;
|
||||
const rules = bodyRec.rules;
|
||||
if (!Array.isArray(rules)) return undefined;
|
||||
for (const row of rules) {
|
||||
if (!row || typeof row !== "object" || Array.isArray(row)) continue;
|
||||
const r = row as Record<string, unknown>;
|
||||
const id =
|
||||
asString(r.规则名) ??
|
||||
asString(r.rule_id) ??
|
||||
asString(r.id) ??
|
||||
asString(r.规则id);
|
||||
@@ -94,16 +102,22 @@ export function buildChanceBatchFromGenerationRule(
|
||||
if (!isElementPool(pool)) continue;
|
||||
const poolId =
|
||||
asString(pool.pool_id) ?? asString(pool.池id) ?? asString(pool.名称);
|
||||
const entries = (pool.条目 ?? pool.entries) as PoolEntry[] | undefined;
|
||||
const entries = (pool.条目 ?? pool.entries) as unknown[] | undefined;
|
||||
if (!poolId || !Array.isArray(entries) || !entries.length) continue;
|
||||
const items = entries
|
||||
.map((e, idx) => {
|
||||
const id = asString(e.id) ?? asString(e.内容) ?? `item-${idx}`;
|
||||
if (typeof e === "string") {
|
||||
const id = e.trim();
|
||||
return id ? { id } : undefined;
|
||||
}
|
||||
if (!e || typeof e !== "object" || Array.isArray(e)) return undefined;
|
||||
const row = e as PoolEntry;
|
||||
const id = asString(row.id) ?? asString(row.内容) ?? `item-${idx}`;
|
||||
const weight =
|
||||
typeof e.权重 === "number"
|
||||
? e.权重
|
||||
: typeof e.weight === "number"
|
||||
? e.weight
|
||||
typeof row.权重 === "number"
|
||||
? row.权重
|
||||
: typeof row.weight === "number"
|
||||
? row.weight
|
||||
: undefined;
|
||||
return id ? { id, weight } : undefined;
|
||||
})
|
||||
|
||||
@@ -21,6 +21,8 @@ import {
|
||||
export const OPENING_SETUP_ARTIFACT_TAG = "设计.开场白与开场变量";
|
||||
/** 对话落盘开场正文族(可 设计.开场白#槽位 增殖) */
|
||||
export const OPENING_PRODUCT_FAMILY = "设计.开场白";
|
||||
/** 0 层开场族(可 设计.0层开场白与初态#槽位 增殖) */
|
||||
export const ZERO_LAYER_OPENING_FAMILY = "设计.0层开场白与初态";
|
||||
export const OPENING_OUTPUT_TAG = "输出.开场白";
|
||||
export const OPENING_INITIAL_VARS_TAG = "运行.初始变量";
|
||||
export const OPENING_CURRENT_VARS_TAG = "变量.当前";
|
||||
@@ -89,6 +91,7 @@ function asTrimmed(v: unknown): string | undefined {
|
||||
}
|
||||
|
||||
type OpeningCandidateEntry = {
|
||||
id: string;
|
||||
text: string;
|
||||
persona: OpeningUserPersona | null;
|
||||
variables: OpeningVariable[];
|
||||
@@ -148,6 +151,7 @@ function collectCandidateEntries(raw: unknown): OpeningCandidateEntry[] {
|
||||
if (typeof item === "string" && item.trim()) {
|
||||
const split = splitYamlFrontmatter(item);
|
||||
out.push({
|
||||
id: "",
|
||||
text: asOpeningBodyText(split.text) || split.text,
|
||||
persona: split.persona,
|
||||
variables: [],
|
||||
@@ -174,6 +178,7 @@ function collectCandidateEntries(raw: unknown): OpeningCandidateEntry[] {
|
||||
) ||
|
||||
split.persona;
|
||||
out.push({
|
||||
id: asTrimmed(row.id) || asTrimmed(row.候选id) || "",
|
||||
text: asOpeningBodyText(split.text) || split.text,
|
||||
persona,
|
||||
variables: variablesFromCandidate(row),
|
||||
@@ -379,6 +384,22 @@ export function readSealedOpeningPersona(
|
||||
return parseUserPersonaField(raw) || parseOpeningUserPersona(raw);
|
||||
}
|
||||
|
||||
/** 默认候选排到第 0 条,游玩的左右下标才能对上「第一次进入 0 层的那条」。 */
|
||||
function orderOpeningCandidates(
|
||||
entries: OpeningCandidateEntry[],
|
||||
defaultId: string | undefined,
|
||||
): OpeningCandidateEntry[] {
|
||||
const id = defaultId?.trim();
|
||||
if (!id) return entries;
|
||||
const index = entries.findIndex((entry) => entry.id === id);
|
||||
if (index <= 0) return entries;
|
||||
return [
|
||||
entries[index]!,
|
||||
...entries.slice(0, index),
|
||||
...entries.slice(index + 1),
|
||||
];
|
||||
}
|
||||
|
||||
function bodyRecord(fragmentBody: unknown): Record<string, unknown> | null {
|
||||
if (!fragmentBody || typeof fragmentBody !== "object" || Array.isArray(fragmentBody)) {
|
||||
return null;
|
||||
@@ -402,16 +423,21 @@ export function parseOpeningSealPayload(
|
||||
}
|
||||
})();
|
||||
|
||||
const primary =
|
||||
asOpeningBodyText(body?.开场白全文) ||
|
||||
asOpeningBodyText(body?.["输出.开场白"]) ||
|
||||
(typeof fragment?.正文 === "string"
|
||||
? asOpeningBodyText(fragment.正文)
|
||||
: undefined);
|
||||
const fromList = orderOpeningCandidates(
|
||||
collectCandidateEntries(body?.开场白候选),
|
||||
asTrimmed(body?.默认候选id) || asTrimmed(body?.默认候选ID),
|
||||
);
|
||||
// 候选里的正文已是那一屏。开场白全文是旧稿副本,有候选时不再另算一条。
|
||||
const primary = fromList.length
|
||||
? undefined
|
||||
: asOpeningBodyText(body?.开场白全文) ||
|
||||
asOpeningBodyText(body?.["输出.开场白"]) ||
|
||||
(typeof fragment?.正文 === "string"
|
||||
? asOpeningBodyText(fragment.正文)
|
||||
: undefined);
|
||||
|
||||
const defaultPersona = parseUserPersonaField(body?.用户角色);
|
||||
const sharedVariables = collectVariables(body?.开场变量);
|
||||
const fromList = collectCandidateEntries(body?.开场白候选);
|
||||
const candidates: string[] = [];
|
||||
const personas: Array<OpeningUserPersona | null> = [];
|
||||
const variableSets: OpeningVariable[][] = [];
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
OPENING_PERSONA_CHOICE_TAG,
|
||||
OPENING_PRODUCT_FAMILY,
|
||||
OPENING_SETUP_ARTIFACT_TAG,
|
||||
ZERO_LAYER_OPENING_FAMILY,
|
||||
SLOT_OPENING_SELECTED_INDEX,
|
||||
parseOpeningSealPayload,
|
||||
splitOpeningDocument,
|
||||
@@ -95,7 +96,13 @@ export function collectOpenings(board: Blackboard): PlayAuxOpening[] {
|
||||
order: number;
|
||||
}> = [];
|
||||
for (const e of board.listTagIndex()) {
|
||||
if (dictateProductFamily(e.tag) !== OPENING_PRODUCT_FAMILY) continue;
|
||||
const family = dictateProductFamily(e.tag);
|
||||
if (
|
||||
family !== OPENING_PRODUCT_FAMILY &&
|
||||
family !== ZERO_LAYER_OPENING_FAMILY
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
const item = board.getLatestByTag(e.tag);
|
||||
const content = item?.content?.trim() ?? "";
|
||||
if (!content) continue;
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* 壳参考库:shell-refs/{id}/prompt.md。
|
||||
* 正文组成与 0 层正文组成读取后,按本局启用的区改一版并固定。
|
||||
*/
|
||||
import { readFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import {
|
||||
getModuleSection,
|
||||
parseModulePromptSections,
|
||||
} from "./creation-flow.js";
|
||||
import { parsePresentShellCatalog } from "./present-shell-catalog.js";
|
||||
|
||||
const DEFAULT_SKILLS_ROOT = path.resolve(
|
||||
path.dirname(fileURLToPath(import.meta.url)),
|
||||
"../../skills",
|
||||
);
|
||||
|
||||
export const SHELL_REF_CATALOG_FILENAME = "shell-refs/catalog.yaml";
|
||||
|
||||
export type ShellRefEntry = {
|
||||
id: string;
|
||||
name: string;
|
||||
intro: string;
|
||||
layout: string;
|
||||
/** 固定摆放与这一次示例填法。样本里的名称、栏目、内部小节按本局改写。 */
|
||||
note: string;
|
||||
sample: string;
|
||||
};
|
||||
|
||||
export async function loadShellRefCatalog(
|
||||
skillPackRoot: string,
|
||||
skillsRoot = DEFAULT_SKILLS_ROOT,
|
||||
): Promise<ShellRefEntry[]> {
|
||||
const catalogPath = path.join(
|
||||
skillsRoot,
|
||||
skillPackRoot,
|
||||
SHELL_REF_CATALOG_FILENAME,
|
||||
);
|
||||
let raw: string;
|
||||
try {
|
||||
raw = await readFile(catalogPath, "utf8");
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
const index = parsePresentShellCatalog(raw);
|
||||
const out: ShellRefEntry[] = [];
|
||||
for (const entry of index) {
|
||||
const promptPath = path.join(
|
||||
skillsRoot,
|
||||
skillPackRoot,
|
||||
"shell-refs",
|
||||
entry.id,
|
||||
"prompt.md",
|
||||
);
|
||||
let promptMd = "";
|
||||
try {
|
||||
promptMd = await readFile(promptPath, "utf8");
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
const sections = parseModulePromptSections(promptMd);
|
||||
const intro = getModuleSection(sections, "intro") ?? "";
|
||||
const layout = getModuleSection(sections, "layout") ?? "";
|
||||
const note = getModuleSection(sections, "note") ?? "";
|
||||
const sample = getModuleSection(sections, "sample") ?? "";
|
||||
if (!intro || !layout || !note || !sample) continue;
|
||||
out.push({
|
||||
id: entry.id,
|
||||
name: entry.name,
|
||||
intro,
|
||||
layout,
|
||||
note,
|
||||
sample,
|
||||
});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export function formatShellRefsForPrompt(
|
||||
shells: readonly ShellRefEntry[],
|
||||
): string {
|
||||
const lines = [
|
||||
"## 【壳参考库】程序从仓库动态装载",
|
||||
"",
|
||||
"选一只与本局已启用区域最接近的参考。备注写两类:不替换的前端结构,以及可按本局用途替换的部位。本局没有的区从样本骨架删除并重排,不留空位。美化用配色、字体层级、边框、间距和简单内联 SVG,按本次能写稳的程度完成。创作时生成一次前端并固定,游玩只灌入各区内容。",
|
||||
"",
|
||||
];
|
||||
for (const shell of shells) {
|
||||
lines.push(`### \`${shell.id}\` · ${shell.name}`, "");
|
||||
lines.push(shell.intro, "");
|
||||
lines.push("布局:", "", "```", shell.layout, "```", "");
|
||||
lines.push("备注:", "", shell.note, "");
|
||||
lines.push("前端样本:", "", "```", shell.sample, "```", "");
|
||||
}
|
||||
return lines.join("\n").trim();
|
||||
}
|
||||
+524
-26
@@ -15,6 +15,7 @@ import {
|
||||
catalogModulesForIntake,
|
||||
formatRecipeCatalogForAgent,
|
||||
formatDictateRecipeBrief,
|
||||
formatRecipeExamplesForCreationPlan,
|
||||
formatSelectedRecipeForAgent,
|
||||
isCreationFlowComplete,
|
||||
loadAllRecipeDetails,
|
||||
@@ -28,6 +29,7 @@ import {
|
||||
listReadySteps,
|
||||
computeStepLayers,
|
||||
spawnRepeatableCreationStep,
|
||||
repeatableInstanceProductTag,
|
||||
spawnInstanceFromPrototype,
|
||||
hasSelectableCreationWork,
|
||||
hasAcceptedCloserStep,
|
||||
@@ -466,7 +468,7 @@ describe("creation-flow", () => {
|
||||
catalog?.modules.find((m) => m.name === "具体实例")?.params?.[0]?.key,
|
||||
).toBe("rule_id");
|
||||
expect(
|
||||
catalog?.modules.find((m) => m.name === "上下文投影排序")?.auto,
|
||||
catalog?.modules.find((m) => m.name === "上下文排序")?.auto,
|
||||
).toBe(true);
|
||||
const gen = catalog?.modules.find((m) => m.name === "生成规则");
|
||||
expect(gen?.when).toBeTruthy();
|
||||
@@ -476,8 +478,7 @@ describe("creation-flow", () => {
|
||||
expect(block).toContain("【能力");
|
||||
expect(block).toContain("美学纲领与交互范式:");
|
||||
expect(block).toContain("生成规则〔可反复〕〔先验产物〕");
|
||||
expect(block).toContain("开场白与开场变量〔收口〕");
|
||||
expect(block).toContain("上下文投影排序〔程序步〕");
|
||||
expect(block).toContain("上下文排序〔程序步〕");
|
||||
expect(block).toContain("何时用");
|
||||
expect(block).toContain("何时不用");
|
||||
expect(block).toContain("role=prototype");
|
||||
@@ -489,6 +490,8 @@ describe("creation-flow", () => {
|
||||
const dictateBlock = formatModuleCatalogForDictate(catalog!);
|
||||
expect(dictateBlock).toMatch(/^- 叙事指南:/m);
|
||||
expect(dictateBlock).toMatch(/^- 故事推进:/m);
|
||||
expect(dictateBlock).toContain("开场白与开场变量〔可反复〕〔收口〕");
|
||||
expect(dictateBlock).toContain("信息可见范围〔落档程序〕");
|
||||
expect(dictateBlock).not.toMatch(/^- 叙事指南与故事推进/m);
|
||||
expect(
|
||||
catalog?.modules.find((m) => m.name === "叙事指南与故事推进")?.intake,
|
||||
@@ -499,16 +502,17 @@ describe("creation-flow", () => {
|
||||
expect(catalog?.modules.find((m) => m.name === "故事推进")?.intake).toBe(
|
||||
"dictate",
|
||||
);
|
||||
expect(catalog?.modules.find((m) => m.name === "用户需求")?.libraries).toEqual([
|
||||
expect(catalog?.modules.find((m) => m.name === "故事推进")?.libraries).toEqual([
|
||||
"preferences",
|
||||
]);
|
||||
expect(catalog?.modules.some((m) => m.id === "user-requirements")).toBe(false);
|
||||
expect(
|
||||
catalog?.modules.find((m) => m.name === "叙事指南")?.libraries,
|
||||
).toEqual(["style-packs"]);
|
||||
expect(
|
||||
catalog?.modules.find((m) => m.name === "叙事指南与故事推进")?.libraries,
|
||||
).toEqual(["style-packs"]);
|
||||
expect(dictateBlock).toContain("【偏好库 · 可选用】");
|
||||
expect(dictateBlock).not.toContain("【偏好库 · 可选用】");
|
||||
expect(
|
||||
catalogModulesForIntake(catalog!, "recipe").some(
|
||||
(m) => m.name === "叙事指南",
|
||||
@@ -570,36 +574,44 @@ describe("creation-flow", () => {
|
||||
expect(dictate?.recipes.some((r) => r.name === "数据化跑团体验")).toBe(true);
|
||||
expect(dictate?.recipes.some((r) => r.id === "快穿短局")).toBe(true);
|
||||
expect(dictate?.recipes.some((r) => r.name === "快穿短局")).toBe(true);
|
||||
expect(dictate?.recipes.some((r) => r.id === "正文组成")).toBe(false);
|
||||
expect(dictate?.recipes).toHaveLength(4);
|
||||
expect(dictate?.recipes[0]?.id).toBe("快穿短局");
|
||||
const entry = dictate!.recipes.find((r) => r.id === "文本生成器")!;
|
||||
expect(entry.family).toBe("dictate");
|
||||
const detail = await loadRecipeDetail("dialogue/world-simulator", entry);
|
||||
expect(detail.core).toMatch(/范例|模仿|转述|游玩/);
|
||||
expect(detail.method?.join("")).toMatch(/范例|写法|输入/);
|
||||
expect(detail.tierTendencies?.some((item) => item.module_id === "narrative-guide")).toBe(true);
|
||||
expect(detail.seed).toBeNull();
|
||||
expect(formatDictateRecipeBrief(detail)).toMatch(/模仿|范例|转述|开玩/);
|
||||
expect(formatDictateRecipeBrief(detail)).toMatch(/模仿要点|学哪些面|要点|文风/);
|
||||
|
||||
const longform = dictate!.recipes.find((r) => r.id === "交互式长文生成器")!;
|
||||
const longDetail = await loadRecipeDetail("dialogue/world-simulator", longform);
|
||||
expect(longDetail.brief).toMatch(/长文/);
|
||||
expect(longDetail.core).toMatch(/长文|分段/);
|
||||
expect(longDetail.brief).toMatch(/长连续/);
|
||||
expect(longDetail.method?.join("")).toMatch(/变量|动态表/);
|
||||
expect(longDetail.method?.join("")).toMatch(/硬机制|数据化跑团/);
|
||||
|
||||
const rpg = dictate!.recipes.find((r) => r.id === "数据化跑团体验")!;
|
||||
const rpgDetail = await loadRecipeDetail("dialogue/world-simulator", rpg);
|
||||
expect(rpgDetail.brief).toMatch(/美学|真值|映射/);
|
||||
expect(rpgDetail.core).toMatch(/美学|真值|declare_variable|何时落盘/);
|
||||
expect(formatDictateRecipeBrief(rpgDetail)).toContain("declare_variable");
|
||||
expect(formatDictateRecipeBrief(rpgDetail)).toContain("何时落盘");
|
||||
expect(formatDictateRecipeBrief(rpgDetail)).toMatch(/美学纲领|体验核心/);
|
||||
expect(rpgDetail.brief).toMatch(/硬判断|结构化状态/);
|
||||
expect(rpgDetail.method?.join("")).toMatch(/长短|连续性/);
|
||||
expect(rpgDetail.tierTendencies?.some((item) => item.module_id === "mechanism")).toBe(true);
|
||||
|
||||
const skip = dictate!.recipes.find((r) => r.id === "快穿短局")!;
|
||||
const skipDetail = await loadRecipeDetail("dialogue/world-simulator", skip);
|
||||
expect(skipDetail.seed).toBeNull();
|
||||
expect(skipDetail.brief).toMatch(/一核|短局|主角/);
|
||||
expect(skipDetail.core).toMatch(/一核|开场白|黑板/);
|
||||
expect(skipDetail.when).toMatch(/快穿|一次性|短任务/);
|
||||
expect(formatDictateRecipeBrief(skipDetail)).toMatch(/舞台骨架|生成规则|拓扑/);
|
||||
expect(formatDictateRecipeBrief(skipDetail)).toMatch(/回合推演|数据化跑团体验/);
|
||||
expect(skipDetail.brief).toMatch(/体验核|短反馈/);
|
||||
expect(skipDetail.signals?.join("")).toMatch(/快穿|一次性|短任务/);
|
||||
|
||||
const examples = formatRecipeExamplesForCreationPlan([
|
||||
detail,
|
||||
longDetail,
|
||||
rpgDetail,
|
||||
skipDetail,
|
||||
]);
|
||||
expect(examples).toContain("适用信号");
|
||||
expect(examples).toContain("档位倾向");
|
||||
expect(examples).toContain("variable-design=有必要");
|
||||
expect(examples).not.toContain("固定路径");
|
||||
});
|
||||
|
||||
it("parses selected recipe ref", () => {
|
||||
@@ -844,10 +856,8 @@ modules:
|
||||
);
|
||||
expect(prompt).toBeTruthy();
|
||||
const opening = extractModuleOpening(prompt!);
|
||||
expect(opening).toContain("原型世界");
|
||||
expect(opening).toContain("变在哪里");
|
||||
expect(opening).toContain("代入");
|
||||
expect(opening).toContain("最想反复感受到");
|
||||
expect(opening).toContain("反复获得什么体验");
|
||||
expect(opening).toContain("站在什么位置");
|
||||
expect(opening).not.toContain("nail清站位");
|
||||
|
||||
const {
|
||||
@@ -884,11 +894,400 @@ modules:
|
||||
}),
|
||||
currentStepName: "美学纲领与交互范式",
|
||||
});
|
||||
expect(binding?.opening).toContain("原型世界");
|
||||
expect(binding?.opening).toContain("反复获得什么体验");
|
||||
expect(binding?.modulePrompt).toContain("```task");
|
||||
expect(binding?.modulePrompt).not.toContain("最想反复感受到的是什么");
|
||||
});
|
||||
|
||||
it("loads the migrated world-blueprint contract", async () => {
|
||||
const catalog = await loadModuleCatalog("dialogue/world-simulator");
|
||||
const module = catalog?.modules.find((item) => item.id === "world-blueprint");
|
||||
expect(module?.layer).toBe("final");
|
||||
expect(module?.mount).toEqual(["world-simulator"]);
|
||||
|
||||
const prompt = await loadModulePrompt(
|
||||
"dialogue/world-simulator",
|
||||
"world-blueprint",
|
||||
);
|
||||
const { parseModulePromptSections } = await import(
|
||||
"../src/skills/creation-flow.js"
|
||||
);
|
||||
const sections = parseModulePromptSections(prompt!);
|
||||
expect(sections.blocks.task).toContain("稳定反馈");
|
||||
expect(sections.blocks.output).toContain('"核心运转"');
|
||||
expect(sections.blocks.output).not.toContain('"自评"');
|
||||
expect(sections.blocks.output).not.toContain('"追问"');
|
||||
expect(sections.blocks.score).toContain("支撑度");
|
||||
expect(sections.blocks.score).toContain("克制度");
|
||||
expect(sections.blocks.score).not.toContain("0–10");
|
||||
});
|
||||
|
||||
it("loads the migrated generation-rules contract", async () => {
|
||||
const catalog = await loadModuleCatalog("dialogue/world-simulator");
|
||||
const module = catalog?.modules.find((item) => item.id === "generation-rules");
|
||||
expect(module?.layer).toBe("final");
|
||||
expect(module?.mount).toEqual(["world-simulator", "auditor"]);
|
||||
expect(module?.repeatable).toBe(true);
|
||||
|
||||
const prompt = await loadModulePrompt(
|
||||
"dialogue/world-simulator",
|
||||
"generation-rules",
|
||||
);
|
||||
const { parseModulePromptSections } = await import(
|
||||
"../src/skills/creation-flow.js"
|
||||
);
|
||||
const sections = parseModulePromptSections(prompt!);
|
||||
expect(sections.blocks.meta).toContain("两份或以上");
|
||||
expect(sections.blocks.task).toContain("填写工作流");
|
||||
expect(sections.blocks.output).toContain('"字段"');
|
||||
expect(sections.blocks.output).toContain('"填写工作流"');
|
||||
expect(sections.blocks.output).toContain('"依赖关系"');
|
||||
expect(sections.blocks.output).not.toContain('"rules"');
|
||||
expect(sections.blocks.output).not.toContain('"自评"');
|
||||
expect(sections.blocks.score).toContain("字段妥当");
|
||||
expect(sections.blocks.score).toContain("池与提示");
|
||||
expect(sections.blocks.score).toContain("填写工作流");
|
||||
});
|
||||
|
||||
it("loads the migrated concrete-instances contract", async () => {
|
||||
const catalog = await loadModuleCatalog("dialogue/world-simulator");
|
||||
const module = catalog?.modules.find(
|
||||
(item) => item.id === "concrete-instances",
|
||||
);
|
||||
expect(module?.layer).toBe("final");
|
||||
expect(module?.mount).toEqual(["world-simulator"]);
|
||||
expect(module?.repeatable).toBe(true);
|
||||
|
||||
const prompt = await loadModulePrompt(
|
||||
"dialogue/world-simulator",
|
||||
"concrete-instances",
|
||||
);
|
||||
const { parseModulePromptSections } = await import(
|
||||
"../src/skills/creation-flow.js"
|
||||
);
|
||||
const sections = parseModulePromptSections(prompt!);
|
||||
expect(sections.blocks.meta).toContain("字段很少");
|
||||
expect(sections.blocks.task).toContain("一份产物只含一条记录");
|
||||
expect(sections.blocks.output).toContain('"名称"');
|
||||
expect(sections.blocks.output).toContain('"记录"');
|
||||
expect(sections.blocks.output).not.toContain('"自评"');
|
||||
expect(sections.blocks.score).toContain("工作流符合度");
|
||||
expect(sections.blocks.score).not.toContain("充实度");
|
||||
});
|
||||
|
||||
it("loads the migrated narrative-guide contract", async () => {
|
||||
const catalog = await loadModuleCatalog("dialogue/world-simulator");
|
||||
const module = catalog?.modules.find((item) => item.id === "narrative-guide");
|
||||
expect(module?.layer).toBe("final");
|
||||
expect(module?.mount).toEqual(["narrator", "world-simulator"]);
|
||||
expect(module?.libraries).toEqual(["style-packs"]);
|
||||
|
||||
const prompt = await loadModulePrompt(
|
||||
"dialogue/world-simulator",
|
||||
"narrative-guide",
|
||||
);
|
||||
const { parseModulePromptSections } = await import(
|
||||
"../src/skills/creation-flow.js"
|
||||
);
|
||||
const sections = parseModulePromptSections(prompt!);
|
||||
expect(sections.blocks.task).toContain("read_library_entry");
|
||||
expect(sections.blocks.task).toContain("风格推荐");
|
||||
expect(sections.blocks.output).toContain('"选用文风"');
|
||||
expect(sections.blocks.output).toContain('"写法要求"');
|
||||
expect(sections.blocks.output).not.toContain('"示范"');
|
||||
expect(sections.blocks.output).not.toContain('"自评"');
|
||||
expect(sections.blocks.score).toContain("遣词可执行");
|
||||
expect(sections.blocks.score).toContain("文风选用");
|
||||
expect(sections.blocks.score).toContain("本局化");
|
||||
});
|
||||
|
||||
it("loads the migrated story-progression contract", async () => {
|
||||
const catalog = await loadModuleCatalog("dialogue/world-simulator");
|
||||
const module = catalog?.modules.find(
|
||||
(item) => item.id === "story-progression",
|
||||
);
|
||||
expect(module?.layer).toBe("final");
|
||||
expect(module?.mount).toEqual(["world-simulator", "narrator"]);
|
||||
expect(module?.libraries).toEqual(["preferences"]);
|
||||
|
||||
const prompt = await loadModulePrompt(
|
||||
"dialogue/world-simulator",
|
||||
"story-progression",
|
||||
);
|
||||
const { parseModulePromptSections } = await import(
|
||||
"../src/skills/creation-flow.js"
|
||||
);
|
||||
const sections = parseModulePromptSections(prompt!);
|
||||
expect(sections.blocks.task).toContain("只选定一种");
|
||||
expect(sections.blocks.task).toContain("read_library_entry");
|
||||
expect(sections.blocks.output).toContain('"用户输入用法"');
|
||||
expect(sections.blocks.output).toContain('"扩写要求"');
|
||||
expect(sections.blocks.output).toContain('"续写要求"');
|
||||
expect(sections.blocks.output).toContain('"单轮篇幅"');
|
||||
expect(sections.blocks.output).toContain('"目标字数"');
|
||||
expect(sections.blocks.output).toContain('"扩写占比"');
|
||||
expect(sections.blocks.output).toContain('"续写占比"');
|
||||
expect(sections.blocks.output).toContain('"OOC处理"');
|
||||
expect(sections.blocks.output).not.toContain('"自评"');
|
||||
expect(sections.blocks.score).toContain("唯一性与切分");
|
||||
expect(sections.blocks.score).toContain("可执行性");
|
||||
expect(sections.blocks.score).toContain("偏好本局化");
|
||||
});
|
||||
|
||||
it("loads the migrated mechanism contract", async () => {
|
||||
const catalog = await loadModuleCatalog("dialogue/world-simulator");
|
||||
const module = catalog?.modules.find((item) => item.id === "mechanism");
|
||||
expect(module?.layer).toBe("final");
|
||||
expect(module?.mount).toEqual(["world-simulator"]);
|
||||
|
||||
const prompt = await loadModulePrompt(
|
||||
"dialogue/world-simulator",
|
||||
"mechanism",
|
||||
);
|
||||
const { parseModulePromptSections } = await import(
|
||||
"../src/skills/creation-flow.js"
|
||||
);
|
||||
const sections = parseModulePromptSections(prompt!);
|
||||
expect(sections.blocks.task).toContain("多条共同作用");
|
||||
expect(sections.blocks.task).toContain("缺失后果");
|
||||
expect(sections.blocks.output).toContain('"既成机制"');
|
||||
expect(sections.blocks.output).toContain('"因果影响"');
|
||||
expect(sections.blocks.output).toContain('"推演口径"');
|
||||
expect(sections.blocks.output).toContain('"共同作用"');
|
||||
expect(sections.blocks.output).not.toContain('"缺失后果"');
|
||||
expect(sections.blocks.output).not.toContain('"自评"');
|
||||
expect(sections.blocks.score).toContain("骨架必要度");
|
||||
expect(sections.blocks.score).toContain("因果清晰度");
|
||||
expect(sections.blocks.score).toContain("推演开放度");
|
||||
expect(sections.blocks.examples).toContain("灵气复苏");
|
||||
expect(sections.blocks.examples).toContain("全民直播求生");
|
||||
});
|
||||
|
||||
it("loads the migrated topology contract", async () => {
|
||||
const catalog = await loadModuleCatalog("dialogue/world-simulator");
|
||||
const module = catalog?.modules.find((item) => item.id === "topology");
|
||||
expect(module?.layer).toBe("final");
|
||||
expect(module?.mount).toEqual(["world-simulator", "auditor"]);
|
||||
expect(module?.repeatable).toBe(true);
|
||||
|
||||
const prompt = await loadModulePrompt(
|
||||
"dialogue/world-simulator",
|
||||
"topology",
|
||||
);
|
||||
const { parseModulePromptSections } = await import(
|
||||
"../src/skills/creation-flow.js"
|
||||
);
|
||||
const sections = parseModulePromptSections(prompt!);
|
||||
expect(sections.blocks.task).toContain("一张图");
|
||||
expect(sections.blocks.task).toContain("static");
|
||||
expect(sections.blocks.task).toContain("record");
|
||||
expect(sections.blocks.task).toContain("expand");
|
||||
expect(sections.blocks.output).toContain('"节点字段"');
|
||||
expect(sections.blocks.output).toContain('"连接字段"');
|
||||
expect(sections.blocks.output).toContain('"初始图"');
|
||||
expect(sections.blocks.output).toContain('"维护规则"');
|
||||
expect(sections.blocks.output).not.toContain('"自评"');
|
||||
expect(sections.blocks.output).not.toContain("SVG");
|
||||
expect(sections.blocks.score).toContain("对象适配");
|
||||
expect(sections.blocks.score).toContain("字段合同");
|
||||
expect(sections.blocks.score).toContain("初始图有效性");
|
||||
expect(sections.blocks.score).toContain("维护可执行性");
|
||||
expect(sections.blocks.score).toContain("长期一致性");
|
||||
});
|
||||
|
||||
it("loads the migrated variable-design contract", async () => {
|
||||
const catalog = await loadModuleCatalog("dialogue/world-simulator");
|
||||
const module = catalog?.modules.find((item) => item.id === "variable-design");
|
||||
expect(module?.layer).toBe("final");
|
||||
expect(module?.mount).toEqual(["world-simulator", "auditor"]);
|
||||
|
||||
const prompt = await loadModulePrompt(
|
||||
"dialogue/world-simulator",
|
||||
"variable-design",
|
||||
);
|
||||
const { parseModulePromptSections } = await import(
|
||||
"../src/skills/creation-flow.js"
|
||||
);
|
||||
const sections = parseModulePromptSections(prompt!);
|
||||
expect(sections.blocks.task).toContain("deterministic");
|
||||
expect(sections.blocks.task).toContain("narrative_inference");
|
||||
expect(sections.blocks.task).toContain("inline_tool");
|
||||
expect(sections.blocks.task).toContain("auditor_bundle");
|
||||
expect(sections.blocks.output).toContain('"允许写入者"');
|
||||
expect(sections.blocks.output).toContain('"更新规则"');
|
||||
expect(sections.blocks.output).toContain('"派生映射"');
|
||||
expect(sections.blocks.output).toContain('"触发规则"');
|
||||
expect(sections.blocks.output).toContain('"维护复杂度"');
|
||||
expect(sections.blocks.output).not.toContain('"自评"');
|
||||
expect(sections.blocks.output).not.toContain("maintain.v1");
|
||||
expect(sections.blocks.output).not.toContain("隐藏段");
|
||||
expect(sections.blocks.score).toContain("真值必要性");
|
||||
expect(sections.blocks.score).toContain("字段妥当性");
|
||||
expect(sections.blocks.score).toContain("更新可执行性");
|
||||
expect(sections.blocks.score).toContain("权限与公开性");
|
||||
expect(sections.blocks.score).toContain("映射与触发一致性");
|
||||
});
|
||||
|
||||
it("loads the migrated status-bar contract", async () => {
|
||||
const catalog = await loadModuleCatalog("dialogue/world-simulator");
|
||||
const module = catalog?.modules.find((item) => item.id === "status-bar");
|
||||
expect(module?.layer).toBe("final");
|
||||
expect(module?.mount).toEqual(["world-simulator", "narrator"]);
|
||||
|
||||
const prompt = await loadModulePrompt(
|
||||
"dialogue/world-simulator",
|
||||
"status-bar",
|
||||
);
|
||||
const { parseModulePromptSections } = await import(
|
||||
"../src/skills/creation-flow.js"
|
||||
);
|
||||
const sections = parseModulePromptSections(prompt!);
|
||||
expect(sections.blocks.task).toContain("语义区域");
|
||||
expect(sections.blocks.task).toContain("inline");
|
||||
expect(sections.blocks.output).toContain('"语义区域"');
|
||||
expect(sections.blocks.output).toContain("program_projection");
|
||||
expect(sections.blocks.output).toContain("maintenance_channel");
|
||||
expect(sections.blocks.output).toContain("state_ops");
|
||||
expect(sections.blocks.output).not.toContain('"启用"');
|
||||
expect(sections.blocks.output).not.toContain("1000~2000");
|
||||
expect(sections.blocks.output).not.toContain('"自评"');
|
||||
expect(sections.blocks.score).toContain("区域必要性");
|
||||
expect(sections.blocks.score).toContain("来源一致性");
|
||||
expect(sections.blocks.score).toContain("内容可执行性");
|
||||
expect(sections.blocks.score).toContain("信息负担");
|
||||
expect(sections.blocks.score).toContain("职责边界");
|
||||
});
|
||||
|
||||
it("loads the migrated random-range contract", async () => {
|
||||
const catalog = await loadModuleCatalog("dialogue/world-simulator");
|
||||
const module = catalog?.modules.find((item) => item.id === "random-range");
|
||||
expect(module?.layer).toBe("final");
|
||||
expect(module?.mount).toEqual(["world-simulator"]);
|
||||
|
||||
const prompt = await loadModulePrompt(
|
||||
"dialogue/world-simulator",
|
||||
"random-range",
|
||||
);
|
||||
const { parseModulePromptSections } = await import(
|
||||
"../src/skills/creation-flow.js"
|
||||
);
|
||||
const sections = parseModulePromptSections(prompt!);
|
||||
expect(sections.blocks.task).toContain("名称.随机数");
|
||||
expect(sections.blocks.task).toContain("只剩 1 个");
|
||||
expect(sections.blocks.output).toContain("敏捷.随机数");
|
||||
expect(sections.blocks.output).not.toContain("票 id");
|
||||
expect(sections.blocks.output).not.toContain('"自评"');
|
||||
expect(sections.blocks.score).toContain("覆盖度");
|
||||
expect(sections.blocks.score).toContain("克制");
|
||||
});
|
||||
|
||||
it("loads the migrated reply-format contract", async () => {
|
||||
const catalog = await loadModuleCatalog("dialogue/world-simulator");
|
||||
const module = catalog?.modules.find((item) => item.id === "reply-format");
|
||||
expect(module?.layer).toBe("final");
|
||||
expect(module?.mount).toEqual(["world-simulator", "narrator"]);
|
||||
|
||||
const prompt = await loadModulePrompt(
|
||||
"dialogue/world-simulator",
|
||||
"reply-format",
|
||||
);
|
||||
const { parseModulePromptSections } = await import(
|
||||
"../src/skills/creation-flow.js"
|
||||
);
|
||||
const sections = parseModulePromptSections(prompt!);
|
||||
expect(sections.blocks.task).toContain("普通楼层");
|
||||
expect(sections.blocks.task).toContain("present.onData");
|
||||
expect(sections.blocks.output).toContain('"数据契约"');
|
||||
expect(sections.blocks.output).toContain('"区域落点"');
|
||||
expect(sections.blocks.output).toContain('"frontend"');
|
||||
expect(sections.blocks.output).toContain('"html"');
|
||||
expect(sections.blocks.output).toContain('"css"');
|
||||
expect(sections.blocks.output).toContain('"js"');
|
||||
expect(sections.blocks.output).toContain('"示例灌数"');
|
||||
expect(sections.blocks.output).not.toContain('"自评"');
|
||||
expect(sections.blocks.score).toContain("区域落实");
|
||||
expect(sections.blocks.score).toContain("权威绑定");
|
||||
expect(sections.blocks.score).toContain("实现完整");
|
||||
expect(sections.blocks.score).toContain("本局化");
|
||||
});
|
||||
|
||||
it("loads the migrated opening-setup contract", async () => {
|
||||
const catalog = await loadModuleCatalog("dialogue/world-simulator");
|
||||
const module = catalog?.modules.find((item) => item.id === "opening-setup");
|
||||
expect(module?.layer).toBe("final");
|
||||
expect(module?.mount).toEqual(["world-simulator", "narrator"]);
|
||||
expect(module?.closer).toBe(true);
|
||||
expect(module?.repeatable).toBe(true);
|
||||
|
||||
const prompt = await loadModulePrompt(
|
||||
"dialogue/world-simulator",
|
||||
"opening-setup",
|
||||
);
|
||||
const { parseModulePromptSections } = await import(
|
||||
"../src/skills/creation-flow.js"
|
||||
);
|
||||
const sections = parseModulePromptSections(prompt!);
|
||||
expect(sections.blocks.task).toContain("默认只生成 1 条");
|
||||
expect(sections.blocks.task).toContain("@玩家");
|
||||
expect(sections.blocks.task).toContain("随机");
|
||||
expect(sections.blocks.output).toContain('"默认候选id"');
|
||||
expect(sections.blocks.output).toContain('"开场白候选"');
|
||||
expect(sections.blocks.output).not.toContain('"开场白全文"');
|
||||
expect(sections.blocks.output).toContain("present.v1");
|
||||
expect(sections.blocks.output).not.toContain('"自评"');
|
||||
expect(sections.blocks.score).toContain("开场必要");
|
||||
expect(sections.blocks.score).toContain("前端符合");
|
||||
expect(sections.blocks.score).toContain("状态自洽");
|
||||
expect(sections.blocks.score).toContain("角色占位");
|
||||
expect(sections.blocks.score).toContain("收口可切换");
|
||||
});
|
||||
|
||||
it("loads the three zero-layer presentation contracts", async () => {
|
||||
const catalog = await loadModuleCatalog("dialogue/world-simulator");
|
||||
const ids = [
|
||||
"zero-layer-status",
|
||||
"zero-layer-reply-format",
|
||||
"zero-layer-opening-setup",
|
||||
];
|
||||
for (const id of ids) {
|
||||
const module = catalog?.modules.find((item) => item.id === id);
|
||||
expect(module?.layer).toBe("final");
|
||||
expect(module?.mount).toEqual(["world-simulator", "narrator"]);
|
||||
const prompt = await loadModulePrompt("dialogue/world-simulator", id);
|
||||
const { parseModulePromptSections } = await import(
|
||||
"../src/skills/creation-flow.js"
|
||||
);
|
||||
const sections = parseModulePromptSections(prompt!);
|
||||
expect(sections.blocks.task).toBeTruthy();
|
||||
expect(sections.blocks.output).not.toContain('"自评"');
|
||||
expect(sections.blocks.score).toBeTruthy();
|
||||
}
|
||||
|
||||
expect(
|
||||
catalog?.modules.find((item) => item.id === "zero-layer-opening-setup")
|
||||
?.closer,
|
||||
).toBe(true);
|
||||
expect(
|
||||
catalog?.modules.find((item) => item.id === "zero-layer-opening-setup")
|
||||
?.repeatable,
|
||||
).toBe(true);
|
||||
const formatPrompt = await loadModulePrompt(
|
||||
"dialogue/world-simulator",
|
||||
"zero-layer-reply-format",
|
||||
);
|
||||
expect(formatPrompt).toContain("zero-layer.update.v1");
|
||||
expect(formatPrompt).toContain("slot_updates");
|
||||
expect(formatPrompt).toContain("base_revision");
|
||||
|
||||
const openingPrompt = await loadModulePrompt(
|
||||
"dialogue/world-simulator",
|
||||
"zero-layer-opening-setup",
|
||||
);
|
||||
expect(openingPrompt).toContain("zero-layer.card.v1");
|
||||
expect(openingPrompt).toContain("@玩家");
|
||||
expect(openingPrompt).toContain("不输出任何 `名称.随机数`");
|
||||
});
|
||||
|
||||
it("parses and validates step params from catalog", async () => {
|
||||
const flow = parseCreationFlow(`{
|
||||
"status": "open",
|
||||
@@ -954,7 +1353,67 @@ modules:
|
||||
expect(loaded.promptBody).toContain("【本步对象】");
|
||||
expect(loaded.promptBody).toContain("target: 怪物");
|
||||
expect(loaded.promptBody).toContain("rule_id: monsters");
|
||||
expect(loaded.worker.outputTags).toContain("设计.生成规则");
|
||||
expect(loaded.worker.outputTags).toEqual(["设计.生成规则#monsters"]);
|
||||
});
|
||||
|
||||
it("lands each rule, instance, and opening on its own tag", () => {
|
||||
const catalog = parseModuleCatalog(`
|
||||
modules:
|
||||
- id: generation-rules
|
||||
name: 生成规则
|
||||
declaration: 一条规则一份产物
|
||||
artifact: 设计.生成规则
|
||||
repeatable: true
|
||||
- id: concrete-instances
|
||||
name: 具体实例
|
||||
declaration: 一条记录一份产物
|
||||
artifact: 设计.具体实例
|
||||
repeatable: true
|
||||
- id: opening-setup
|
||||
name: 开场白与开场变量
|
||||
declaration: 一条开场一份产物
|
||||
artifact: 设计.开场白与开场变量
|
||||
repeatable: true
|
||||
closer: true
|
||||
- id: aesthetics-interaction
|
||||
name: 美学纲领与交互范式
|
||||
declaration: 一次写完
|
||||
artifact: 设计.美学纲领与交互范式
|
||||
`)!;
|
||||
const rule = catalog.modules.find((m) => m.id === "generation-rules")!;
|
||||
const row = catalog.modules.find((m) => m.id === "concrete-instances")!;
|
||||
const opening = catalog.modules.find((m) => m.id === "opening-setup")!;
|
||||
const once = catalog.modules.find((m) => m.id === "aesthetics-interaction")!;
|
||||
expect(
|
||||
repeatableInstanceProductTag({
|
||||
module: rule,
|
||||
step: { id: "生成规则·怪物", role: "instance", params: { rule_id: "女租客" } },
|
||||
}),
|
||||
).toBe("设计.生成规则#女租客");
|
||||
expect(
|
||||
repeatableInstanceProductTag({
|
||||
module: row,
|
||||
step: { id: "具体实例#1", role: "instance", params: { batch_goal: "雷樱" } },
|
||||
}),
|
||||
).toBe("设计.具体实例#1");
|
||||
expect(
|
||||
repeatableInstanceProductTag({
|
||||
module: row,
|
||||
step: { id: "具体实例·雷樱", role: "instance", params: { batch_goal: "雷樱" } },
|
||||
}),
|
||||
).toBe("设计.具体实例#雷樱");
|
||||
expect(
|
||||
repeatableInstanceProductTag({
|
||||
module: opening,
|
||||
step: { id: "开场白与开场变量#dorm", role: "instance" },
|
||||
}),
|
||||
).toBe("设计.开场白#dorm");
|
||||
expect(
|
||||
repeatableInstanceProductTag({
|
||||
module: once,
|
||||
step: { id: "美学纲领与交互范式", role: "instance" },
|
||||
}),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("injects empty prior-artifact plan and still lets the step run", async () => {
|
||||
@@ -1300,6 +1759,45 @@ modules:
|
||||
expect(second.step.id).toBe("生成规则#2");
|
||||
});
|
||||
|
||||
it("spawns an opening instance from its own prototype without depending on itself", () => {
|
||||
const catalog = parseModuleCatalog(`
|
||||
modules:
|
||||
- name: 开场白与开场变量
|
||||
declaration: 可增殖收口
|
||||
artifact: 设计.开场白与开场变量
|
||||
closer: true
|
||||
repeatable: true
|
||||
`)!;
|
||||
const flow = parseCreationFlow(`{
|
||||
"status": "open",
|
||||
"steps": [
|
||||
{
|
||||
"id": "开场白与开场变量",
|
||||
"name": "开场白与开场变量",
|
||||
"role": "prototype",
|
||||
"depends_on": []
|
||||
}
|
||||
]
|
||||
}`)!;
|
||||
const spawned = spawnInstanceFromPrototype({
|
||||
flow,
|
||||
catalog,
|
||||
prototypeId: "开场白与开场变量",
|
||||
acceptedStepIds: [],
|
||||
});
|
||||
expect("error" in spawned).toBe(false);
|
||||
if ("error" in spawned) return;
|
||||
expect(spawned.step.role).toBe("instance");
|
||||
expect(spawned.step.from).toBe("开场白与开场变量");
|
||||
expect(spawned.step.depends_on).toEqual([]);
|
||||
const proto = spawned.flow.steps.find((s) => s.role === "prototype");
|
||||
expect(proto?.depends_on).toEqual([]);
|
||||
expect(spawned.flow.steps.map((s) => s.id)).toEqual([
|
||||
"开场白与开场变量",
|
||||
"开场白与开场变量#1",
|
||||
]);
|
||||
});
|
||||
|
||||
it("removeUnstartedInstance drops empty spawn and closer dep", () => {
|
||||
const flow = parseCreationFlow(`{
|
||||
"status": "open",
|
||||
|
||||
@@ -0,0 +1,274 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
CREATION_PLAN_TAG,
|
||||
buildDictateSystemPrompt,
|
||||
creationPlanModules,
|
||||
formatCreationPlanCapabilityCatalog,
|
||||
formatCreationPlanProgress,
|
||||
runDictateTurn,
|
||||
validateCreationPlan,
|
||||
type CreationPlan,
|
||||
} from "../src/dictate/index.js";
|
||||
import {
|
||||
formatModuleCatalogForDictate,
|
||||
loadModuleCatalog,
|
||||
} from "../src/skills/creation-flow.js";
|
||||
import type { AgentDriver } from "../src/runtime/driver.js";
|
||||
|
||||
async function fixture() {
|
||||
const catalog = await loadModuleCatalog("dialogue/world-simulator");
|
||||
expect(catalog).toBeTruthy();
|
||||
const modules = creationPlanModules(catalog!);
|
||||
const plan: CreationPlan = {
|
||||
schema: "creation-plan.v1",
|
||||
技能: "本局创作方案",
|
||||
brief: "以封闭雨夜中的身份猜疑兑现持续压迫感",
|
||||
recipe: {
|
||||
primary: "封闭空间悬疑",
|
||||
secondary_traits: ["关系拉扯"],
|
||||
method: ["让线索公开程度与人物关系共同改变每轮选择"],
|
||||
references: [],
|
||||
},
|
||||
items: modules.map((module, index) => ({
|
||||
module_id: module.id,
|
||||
tier: index === 0 ? "必须" : "有一定效果",
|
||||
why: `${module.name}对本局的作用`,
|
||||
missing: "",
|
||||
instances: [],
|
||||
})),
|
||||
};
|
||||
return { catalog: catalog!, modules, plan };
|
||||
}
|
||||
|
||||
describe("creation plan", () => {
|
||||
it("covers all current content modules and excludes approved mechanical or legacy nodes", async () => {
|
||||
const { modules } = await fixture();
|
||||
expect(modules).toHaveLength(14);
|
||||
expect(modules.map((module) => module.id)).not.toEqual(
|
||||
expect.arrayContaining([
|
||||
"creation-plan",
|
||||
"narrative",
|
||||
"variable-context",
|
||||
"context-order",
|
||||
"worker-spec",
|
||||
"refine",
|
||||
"user-requirements",
|
||||
]),
|
||||
);
|
||||
expect(modules.map((module) => module.id)).toEqual(
|
||||
expect.arrayContaining([
|
||||
"aesthetics-interaction",
|
||||
"generation-rules",
|
||||
"reply-format",
|
||||
"opening-setup",
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("selects exactly one presentation chain", async () => {
|
||||
const catalog = await loadModuleCatalog("dialogue/world-simulator");
|
||||
expect(catalog).toBeTruthy();
|
||||
const zeroModules = creationPlanModules(catalog!, "zero_layer");
|
||||
const ids = zeroModules.map((module) => module.id);
|
||||
expect(zeroModules).toHaveLength(14);
|
||||
expect(ids).toEqual(
|
||||
expect.arrayContaining([
|
||||
"zero-layer-status",
|
||||
"zero-layer-reply-format",
|
||||
"zero-layer-opening-setup",
|
||||
]),
|
||||
);
|
||||
expect(ids).not.toEqual(
|
||||
expect.arrayContaining(["status-bar", "reply-format", "opening-setup"]),
|
||||
);
|
||||
|
||||
const plan: CreationPlan = {
|
||||
schema: "creation-plan.v1",
|
||||
技能: "本局创作方案",
|
||||
brief: "持久终端体验",
|
||||
presentation_mode: "zero_layer",
|
||||
recipe: {
|
||||
primary: "终端操作",
|
||||
secondary_traits: [],
|
||||
method: ["原地更新持久卡"],
|
||||
references: [],
|
||||
},
|
||||
items: zeroModules.map((module) => ({
|
||||
module_id: module.id,
|
||||
tier: "必须",
|
||||
why: module.name,
|
||||
missing: "",
|
||||
instances: [],
|
||||
})),
|
||||
};
|
||||
expect(validateCreationPlan(JSON.stringify(plan), catalog).ok).toBe(true);
|
||||
|
||||
plan.items[plan.items.findIndex((item) => item.module_id === "zero-layer-status")] = {
|
||||
module_id: "status-bar",
|
||||
tier: "必须",
|
||||
why: "错误混入普通楼层",
|
||||
missing: "",
|
||||
instances: [],
|
||||
};
|
||||
const mixed = validateCreationPlan(JSON.stringify(plan), catalog);
|
||||
expect(mixed.ok).toBe(false);
|
||||
expect(mixed.missingIds).toContain("zero-layer-status");
|
||||
expect(mixed.unknownIds).toContain("status-bar");
|
||||
});
|
||||
|
||||
it("rejects missing module ids and reports them", async () => {
|
||||
const { catalog, plan } = await fixture();
|
||||
const omitted = plan.items.at(-1)!.module_id;
|
||||
plan.items.pop();
|
||||
const result = validateCreationPlan(JSON.stringify(plan), catalog);
|
||||
expect(result.ok).toBe(false);
|
||||
expect(result.missingIds).toEqual([omitted]);
|
||||
expect(result.error).toContain(omitted);
|
||||
});
|
||||
|
||||
it("uses a short catalog before the plan and grouped state after it", async () => {
|
||||
const { catalog, plan } = await fixture();
|
||||
const shortCatalog = formatCreationPlanCapabilityCatalog(catalog);
|
||||
expect(shortCatalog).toContain("items 必须覆盖");
|
||||
expect(shortCatalog).toContain("presentation_mode=`message|zero_layer`");
|
||||
expect(shortCatalog).toContain("0层回复呈现");
|
||||
expect(shortCatalog).not.toContain("creation-plan(本局创作方案)");
|
||||
|
||||
const rules = plan.items.find((item) => item.module_id === "generation-rules");
|
||||
expect(rules).toBeTruthy();
|
||||
rules!.tier = "必须";
|
||||
const unused = plan.items.find((item) => item.module_id === "topology");
|
||||
expect(unused).toBeTruthy();
|
||||
unused!.tier = "没有意义";
|
||||
const progress = formatCreationPlanProgress({
|
||||
plan,
|
||||
catalog,
|
||||
products: [
|
||||
{ tag: catalog.modules.find((m) => m.id === "aesthetics-interaction")!.artifact, content: "{}" },
|
||||
{ tag: "设计.生成规则#tenants", content: "{}" },
|
||||
],
|
||||
});
|
||||
expect(progress).toContain("【本局创作方案 · 当前决策】");
|
||||
expect(progress).toContain("不是本轮待办");
|
||||
expect(progress).toContain("何时用 / 何时不用");
|
||||
expect(progress).toContain("### 必须");
|
||||
expect(progress).toContain("〔已落〕");
|
||||
expect(progress).toContain("要落两份或以上同类对象");
|
||||
expect(progress).toContain("字段很少、也不承载玩法");
|
||||
const unusedSection = progress.split("### 没有意义")[1] ?? "";
|
||||
expect(unusedSection).toContain("topology");
|
||||
expect(unusedSection).not.toContain("何时用:");
|
||||
|
||||
const oldFullGuide = formatModuleCatalogForDictate(catalog);
|
||||
const before = buildDictateSystemPrompt({
|
||||
capabilityCatalog: shortCatalog,
|
||||
recipeExamples: "- text-generator:文本生成案例",
|
||||
});
|
||||
const after = buildDictateSystemPrompt({ creationPlanState: progress });
|
||||
expect(before).toContain("尚无本局创作方案");
|
||||
expect(after).toContain("当前决策");
|
||||
expect(after).toContain("收口前的核对清单");
|
||||
expect(after).not.toContain("旧配方案例目录");
|
||||
expect(after.length).toBeLessThan(
|
||||
buildDictateSystemPrompt({ moduleGuide: oldFullGuide }).length,
|
||||
);
|
||||
});
|
||||
|
||||
it("blocks an incomplete plan insert before writing", async () => {
|
||||
const { catalog, plan } = await fixture();
|
||||
const missingId = plan.items.at(-1)!.module_id;
|
||||
plan.items.pop();
|
||||
const products: Array<{ tag: string; content: string }> = [];
|
||||
const driver: AgentDriver = {
|
||||
async run(input) {
|
||||
const outcome = await input.handleStep([
|
||||
{
|
||||
id: "p1",
|
||||
name: "insert",
|
||||
arguments: JSON.stringify({
|
||||
position: CREATION_PLAN_TAG,
|
||||
content: JSON.stringify(plan),
|
||||
}),
|
||||
},
|
||||
]);
|
||||
expect(outcome.kind).toBe("continue");
|
||||
if (outcome.kind === "continue") {
|
||||
const payload = JSON.parse(outcome.results[0]!.content) as {
|
||||
error?: string;
|
||||
missing_ids?: string[];
|
||||
};
|
||||
expect(payload.error).toContain("能力覆盖校验失败");
|
||||
expect(payload.missing_ids).toContain(missingId);
|
||||
}
|
||||
return { iterations: 1, stop: { kind: "text", content: "方案未写入。" } };
|
||||
},
|
||||
};
|
||||
|
||||
const result = await runDictateTurn({
|
||||
llm: {} as never,
|
||||
dialogue: [{ role: "user", text: "做一个封闭空间悬疑" }],
|
||||
moduleCatalog: catalog,
|
||||
driver,
|
||||
handlers: {
|
||||
listProducts: () => products,
|
||||
writeProduct: (tag, content) => products.push({ tag, content }),
|
||||
deleteProduct: () => false,
|
||||
readTag: () => undefined,
|
||||
writeTag: () => undefined,
|
||||
deleteTag: () => false,
|
||||
clearDialogue: () => undefined,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.wroteTags).toEqual([]);
|
||||
expect(products).toEqual([]);
|
||||
});
|
||||
|
||||
it("warns but does not reject an opening with required gaps", async () => {
|
||||
const { catalog, plan } = await fixture();
|
||||
const requiredId = plan.items[0]!.module_id;
|
||||
const products = [{ tag: CREATION_PLAN_TAG, content: JSON.stringify(plan) }];
|
||||
const driver: AgentDriver = {
|
||||
async run(input) {
|
||||
const outcome = await input.handleStep([
|
||||
{
|
||||
id: "o1",
|
||||
name: "insert",
|
||||
arguments: JSON.stringify({
|
||||
position: "设计.开场白",
|
||||
content: "@玩家在雨夜醒来。",
|
||||
}),
|
||||
},
|
||||
]);
|
||||
expect(outcome.kind).toBe("continue");
|
||||
if (outcome.kind === "continue") {
|
||||
const payload = JSON.parse(outcome.results[0]!.content) as {
|
||||
ok?: boolean;
|
||||
missing_required_module_ids?: string[];
|
||||
};
|
||||
expect(payload.ok).toBe(true);
|
||||
expect(payload.missing_required_module_ids).toContain(requiredId);
|
||||
}
|
||||
return { iterations: 1, stop: { kind: "text", content: "开场已写入,并提示缺项。" } };
|
||||
},
|
||||
};
|
||||
|
||||
await runDictateTurn({
|
||||
llm: {} as never,
|
||||
dialogue: [{ role: "user", text: "先写开场" }],
|
||||
moduleCatalog: catalog,
|
||||
driver,
|
||||
handlers: {
|
||||
listProducts: () => products,
|
||||
writeProduct: (tag, content) => products.push({ tag, content }),
|
||||
deleteProduct: () => false,
|
||||
readTag: () => undefined,
|
||||
writeTag: () => undefined,
|
||||
deleteTag: () => false,
|
||||
clearDialogue: () => undefined,
|
||||
},
|
||||
});
|
||||
|
||||
expect(products.some((product) => product.tag === "设计.开场白")).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -46,6 +46,33 @@ describe("dictate play-bind", () => {
|
||||
expect(refs.indexOf("设计.正文组成")).toBeLessThan(histIdx);
|
||||
});
|
||||
|
||||
it("groups context by change frequency before applying importance order", () => {
|
||||
const fragment = (skill: string, stability: "stable" | "semi") =>
|
||||
JSON.stringify({
|
||||
schema: "context-fragment.v1",
|
||||
技能: skill,
|
||||
brief: skill,
|
||||
mount: ["world-simulator"],
|
||||
稳变: stability,
|
||||
正文: { value: skill },
|
||||
开放问题: [],
|
||||
});
|
||||
const order = buildDictateContextOrder({
|
||||
products: [
|
||||
{ tag: "设计.稳定约束", content: fragment("稳定约束", "stable"), order: 10 },
|
||||
{ tag: "设计.阶段备忘", content: fragment("阶段备忘", "semi"), order: -50 },
|
||||
],
|
||||
playSlots: defaultPlaySlots(),
|
||||
});
|
||||
const refs = order.slots[0]!.inserts.map((i) => i.ref);
|
||||
const history = refs.indexOf(DIALOGUE_HISTORY_TAG);
|
||||
expect(refs.indexOf("设计.稳定约束")).toBeLessThan(history);
|
||||
expect(refs.indexOf("设计.阶段备忘")).toBeGreaterThan(history);
|
||||
expect(refs.indexOf("设计.阶段备忘")).toBeLessThan(
|
||||
refs.indexOf("用户.最新输入"),
|
||||
);
|
||||
});
|
||||
|
||||
it("skips opening and worker-set tags; narrator only gets style", () => {
|
||||
const board = new Blackboard();
|
||||
board.write({
|
||||
@@ -118,7 +145,7 @@ describe("dictate play-bind", () => {
|
||||
});
|
||||
board.write({
|
||||
tag: "设计.故事推进",
|
||||
content: "用户输入用法:大纲扩写。禁止扮演停笔。",
|
||||
content: "用户输入用法:大纲扩写。禁止续写与自动写入。",
|
||||
source: "dictate",
|
||||
metadata: { [DICTATE_ORDER_META_KEY]: -22 },
|
||||
});
|
||||
@@ -147,7 +174,7 @@ describe("dictate play-bind", () => {
|
||||
expect(narrRefs).not.toContain("用户.需求");
|
||||
expect(JSON.parse(result.workerSetJson).play_slots.narrator).toBe(true);
|
||||
expect(board.getContentByTag("设计.故事推进")).toBe(
|
||||
"用户输入用法:大纲扩写。禁止扮演停笔。",
|
||||
"用户输入用法:大纲扩写。禁止续写与自动写入。",
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
buildDictateModuleReadIndex,
|
||||
runDictateTurn,
|
||||
} from "../src/dictate/index.js";
|
||||
import {
|
||||
formatModuleCatalogForDictate,
|
||||
loadModuleCatalog,
|
||||
} from "../src/skills/creation-flow.js";
|
||||
import type { AgentDriver } from "../src/runtime/driver.js";
|
||||
|
||||
describe("dictate read_library_entry", () => {
|
||||
it("lists short entries on read_module and returns selected full entries only", async () => {
|
||||
const catalog = await loadModuleCatalog("dialogue/world-simulator");
|
||||
const moduleReadIndex = await buildDictateModuleReadIndex({
|
||||
skillPackRoot: "dialogue/world-simulator",
|
||||
catalog: catalog!,
|
||||
});
|
||||
const preferenceModule = moduleReadIndex.get("story-progression")!;
|
||||
const entryId = preferenceModule.libraries[0]!.entries[0]!.id;
|
||||
const tags = new Map<string, string>();
|
||||
const driver: AgentDriver = {
|
||||
async run(input) {
|
||||
const outcome = await input.handleStep([
|
||||
{
|
||||
id: "m1",
|
||||
name: "read_module",
|
||||
arguments: JSON.stringify({ module_id: "story-progression" }),
|
||||
},
|
||||
{
|
||||
id: "l1",
|
||||
name: "read_library_entry",
|
||||
arguments: JSON.stringify({
|
||||
library_id: "preferences",
|
||||
entry_ids: [entryId],
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: "l2",
|
||||
name: "read_library_entry",
|
||||
arguments: JSON.stringify({
|
||||
library_id: "style-packs",
|
||||
entry_ids: ["missing"],
|
||||
}),
|
||||
},
|
||||
]);
|
||||
if (outcome.kind === "continue") {
|
||||
const module = JSON.parse(outcome.results[0]!.content);
|
||||
const selected = JSON.parse(outcome.results[1]!.content);
|
||||
const rejected = JSON.parse(outcome.results[2]!.content);
|
||||
expect(module.libraries[0].entries[0].summary).toBeTruthy();
|
||||
expect(selected.entries).toHaveLength(1);
|
||||
expect(selected.entries[0].content).toBeTruthy();
|
||||
expect(rejected.error).toContain("未绑定库");
|
||||
}
|
||||
return { iterations: 1, stop: { kind: "text", content: "已读取" } };
|
||||
},
|
||||
};
|
||||
|
||||
await runDictateTurn({
|
||||
llm: {} as never,
|
||||
dialogue: [],
|
||||
moduleCatalog: catalog,
|
||||
moduleReadIndex,
|
||||
driver,
|
||||
handlers: {
|
||||
listProducts: () => [],
|
||||
writeProduct: () => undefined,
|
||||
deleteProduct: () => false,
|
||||
readTag: (tag) => tags.get(tag),
|
||||
writeTag: (tag, content) => {
|
||||
tags.set(tag, content);
|
||||
},
|
||||
deleteTag: (tag) => tags.delete(tag),
|
||||
clearDialogue: () => undefined,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("does not inject full bound libraries into the default dictate catalog", async () => {
|
||||
const catalog = await loadModuleCatalog("dialogue/world-simulator");
|
||||
const block = formatModuleCatalogForDictate(catalog!);
|
||||
expect(block).not.toContain("starter-no-user-ntr-victim");
|
||||
expect(block).not.toContain("用户代入角色不做被 NTR");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,279 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
DICTATE_MODULE_READ_STATE_TAG,
|
||||
buildDictateModuleReadIndex,
|
||||
runDictateTurn,
|
||||
} from "../src/dictate/index.js";
|
||||
import { loadModuleCatalog } from "../src/skills/creation-flow.js";
|
||||
import type { AgentDriver } from "../src/runtime/driver.js";
|
||||
|
||||
async function setup() {
|
||||
const catalog = await loadModuleCatalog("dialogue/world-simulator");
|
||||
expect(catalog).toBeTruthy();
|
||||
const moduleReadIndex = await buildDictateModuleReadIndex({
|
||||
skillPackRoot: "dialogue/world-simulator",
|
||||
catalog: catalog!,
|
||||
});
|
||||
return { catalog: catalog!, moduleReadIndex };
|
||||
}
|
||||
|
||||
describe("dictate read_module", () => {
|
||||
it("rejects insert before reading the current prompt version", async () => {
|
||||
const { catalog, moduleReadIndex } = await setup();
|
||||
const tags = new Map<string, string>();
|
||||
const driver: AgentDriver = {
|
||||
async run(input) {
|
||||
const outcome = await input.handleStep([
|
||||
{
|
||||
id: "i1",
|
||||
name: "insert",
|
||||
arguments: JSON.stringify({
|
||||
position: "设计.美学纲领与交互范式",
|
||||
content: "{}",
|
||||
self_score: {
|
||||
dims: [
|
||||
{ name: "核心体验", score: 5 },
|
||||
{ name: "背景与规则", score: 5 },
|
||||
],
|
||||
},
|
||||
}),
|
||||
},
|
||||
]);
|
||||
if (outcome.kind === "continue") {
|
||||
const payload = JSON.parse(outcome.results[0]!.content);
|
||||
expect(payload.error).toContain('read_module("aesthetics-interaction")');
|
||||
}
|
||||
return { iterations: 1, stop: { kind: "text", content: "未写入" } };
|
||||
},
|
||||
};
|
||||
|
||||
const result = await runDictateTurn({
|
||||
llm: {} as never,
|
||||
dialogue: [],
|
||||
moduleCatalog: catalog,
|
||||
moduleReadIndex,
|
||||
driver,
|
||||
handlers: handlers(tags),
|
||||
});
|
||||
expect(result.wroteTags).toEqual([]);
|
||||
});
|
||||
|
||||
it("allows reading multiple modules then inserting both in the same step", async () => {
|
||||
const { catalog, moduleReadIndex } = await setup();
|
||||
const tags = new Map<string, string>();
|
||||
const driver: AgentDriver = {
|
||||
async run(input) {
|
||||
const outcome = await input.handleStep([
|
||||
{
|
||||
id: "r1",
|
||||
name: "read_module",
|
||||
arguments: JSON.stringify({ module_id: "美学纲领与交互范式" }),
|
||||
},
|
||||
{
|
||||
id: "r2",
|
||||
name: "read_module",
|
||||
arguments: JSON.stringify({ module_id: "protagonist" }),
|
||||
},
|
||||
{
|
||||
id: "i1",
|
||||
name: "insert",
|
||||
arguments: JSON.stringify({
|
||||
position: "设计.美学纲领与交互范式",
|
||||
content: "{}",
|
||||
self_score: {
|
||||
dims: [
|
||||
{ name: "核心体验", score: 5 },
|
||||
{ name: "背景与规则", score: 3, gap: "规则因果仍需确认" },
|
||||
],
|
||||
},
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: "i2",
|
||||
name: "insert",
|
||||
arguments: JSON.stringify({
|
||||
position: "设计.主角设定",
|
||||
content: "{}",
|
||||
self_score: {
|
||||
dims: [
|
||||
{ name: "宽松度", score: 5 },
|
||||
{ name: "必要度", score: 5 },
|
||||
],
|
||||
},
|
||||
}),
|
||||
},
|
||||
]);
|
||||
if (outcome.kind === "continue") {
|
||||
const read = JSON.parse(outcome.results[0]!.content);
|
||||
expect(read.task).toBeTruthy();
|
||||
expect(read.version).toMatch(/^[a-f0-9]{16}$/);
|
||||
expect(JSON.parse(outcome.results[2]!.content).ok).toBe(true);
|
||||
expect(JSON.parse(outcome.results[3]!.content).ok).toBe(true);
|
||||
}
|
||||
return { iterations: 1, stop: { kind: "text", content: "已写入" } };
|
||||
},
|
||||
};
|
||||
|
||||
const result = await runDictateTurn({
|
||||
llm: {} as never,
|
||||
dialogue: [],
|
||||
moduleCatalog: catalog,
|
||||
moduleReadIndex,
|
||||
driver,
|
||||
handlers: handlers(tags),
|
||||
});
|
||||
expect(result.wroteTags).toEqual([
|
||||
"设计.美学纲领与交互范式",
|
||||
"设计.主角设定",
|
||||
]);
|
||||
expect(tags.get(DICTATE_MODULE_READ_STATE_TAG)).toContain("protagonist");
|
||||
});
|
||||
|
||||
it("returns presentation references for reply-format", async () => {
|
||||
const { moduleReadIndex } = await setup();
|
||||
const module = moduleReadIndex.get("reply-format");
|
||||
expect(module?.presentation_references?.shells.length).toBeGreaterThan(0);
|
||||
expect(module?.presentation_references?.markdown_safe_subset).toContain(
|
||||
"正文安全子集",
|
||||
);
|
||||
expect(module?.presentation_references?.iframe_contract).toContain(
|
||||
"present.onData",
|
||||
);
|
||||
});
|
||||
|
||||
it("requires another read when the prompt version changes", async () => {
|
||||
const { catalog, moduleReadIndex } = await setup();
|
||||
const tags = new Map<string, string>();
|
||||
const driver: AgentDriver = {
|
||||
async run(input) {
|
||||
await input.handleStep([
|
||||
{
|
||||
id: "r1",
|
||||
name: "read_module",
|
||||
arguments: JSON.stringify({ module_id: "aesthetics-interaction" }),
|
||||
},
|
||||
]);
|
||||
moduleReadIndex.get("aesthetics-interaction")!.version = "new-version";
|
||||
const outcome = await input.handleStep([
|
||||
{
|
||||
id: "i1",
|
||||
name: "insert",
|
||||
arguments: JSON.stringify({
|
||||
position: "设计.美学纲领与交互范式",
|
||||
content: "{}",
|
||||
}),
|
||||
},
|
||||
]);
|
||||
if (outcome.kind === "continue") {
|
||||
expect(JSON.parse(outcome.results[0]!.content).error).toContain(
|
||||
"当前提示版本",
|
||||
);
|
||||
}
|
||||
return { iterations: 2, stop: { kind: "text", content: "需重读" } };
|
||||
},
|
||||
};
|
||||
const result = await runDictateTurn({
|
||||
llm: {} as never,
|
||||
dialogue: [],
|
||||
moduleCatalog: catalog,
|
||||
moduleReadIndex,
|
||||
driver,
|
||||
handlers: handlers(tags),
|
||||
});
|
||||
expect(result.wroteTags).toEqual([]);
|
||||
});
|
||||
|
||||
it("validates score dimensions and stores low-score gaps as metadata", async () => {
|
||||
const { catalog, moduleReadIndex } = await setup();
|
||||
const tags = new Map<string, string>();
|
||||
let metadata: unknown;
|
||||
const driver: AgentDriver = {
|
||||
async run(input) {
|
||||
await input.handleStep([
|
||||
{
|
||||
id: "r1",
|
||||
name: "read_module",
|
||||
arguments: JSON.stringify({ module_id: "aesthetics-interaction" }),
|
||||
},
|
||||
]);
|
||||
const rejected = await input.handleStep([
|
||||
{
|
||||
id: "i1",
|
||||
name: "insert",
|
||||
arguments: JSON.stringify({
|
||||
position: "设计.美学纲领与交互范式",
|
||||
content: "{}",
|
||||
self_score: {
|
||||
dims: [
|
||||
{ name: "核心体验", score: 3 },
|
||||
{ name: "背景与规则", score: 5 },
|
||||
],
|
||||
},
|
||||
}),
|
||||
},
|
||||
]);
|
||||
if (rejected.kind === "continue") {
|
||||
expect(JSON.parse(rejected.results[0]!.content).error).toContain(
|
||||
"必须写 gap",
|
||||
);
|
||||
}
|
||||
await input.handleStep([
|
||||
{
|
||||
id: "i2",
|
||||
name: "insert",
|
||||
arguments: JSON.stringify({
|
||||
position: "设计.美学纲领与交互范式",
|
||||
content: "{}",
|
||||
self_score: {
|
||||
dims: [
|
||||
{ name: "核心体验", score: 3, gap: "发生方式仍偏宽" },
|
||||
{ name: "背景与规则", score: 5 },
|
||||
],
|
||||
},
|
||||
}),
|
||||
},
|
||||
]);
|
||||
return { iterations: 3, stop: { kind: "text", content: "已写入" } };
|
||||
},
|
||||
};
|
||||
await runDictateTurn({
|
||||
llm: {} as never,
|
||||
dialogue: [],
|
||||
moduleCatalog: catalog,
|
||||
moduleReadIndex,
|
||||
driver,
|
||||
handlers: {
|
||||
...handlers(tags),
|
||||
writeProduct: (tag, content, order, meta) => {
|
||||
tags.set(tag, content);
|
||||
metadata = { order, ...meta };
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(metadata).toMatchObject({
|
||||
layer: "final",
|
||||
selfScore: {
|
||||
dims: expect.arrayContaining([
|
||||
expect.objectContaining({ name: "核心体验", score: 3 }),
|
||||
]),
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function handlers(tags: Map<string, string>) {
|
||||
return {
|
||||
listProducts: () =>
|
||||
[...tags.entries()]
|
||||
.filter(([tag]) => tag.startsWith("设计.") || tag.startsWith("用户."))
|
||||
.map(([tag, content]) => ({ tag, content })),
|
||||
writeProduct: (tag: string, content: string) => tags.set(tag, content),
|
||||
deleteProduct: (tag: string) => tags.delete(tag),
|
||||
readTag: (tag: string) => tags.get(tag),
|
||||
writeTag: (tag: string, content: string) => {
|
||||
tags.set(tag, content);
|
||||
},
|
||||
deleteTag: (tag: string) => tags.delete(tag),
|
||||
clearDialogue: () => undefined,
|
||||
};
|
||||
}
|
||||
+155
-32
@@ -73,7 +73,7 @@ describe("dictate context", () => {
|
||||
expect(isDictateModeValue("recipe")).toBe(false);
|
||||
});
|
||||
|
||||
it("system prompt requires insert toolcall and relative order", () => {
|
||||
it("system prompt starts from a plan and keeps generic write tools", () => {
|
||||
const prompt = buildDictateSystemPrompt({
|
||||
recipeName: "数据化跑团体验",
|
||||
recipeBrief:
|
||||
@@ -81,9 +81,8 @@ describe("dictate context", () => {
|
||||
moduleGuide:
|
||||
"【能力 · 何时落盘】\n- 美学纲领与交互范式:…\n 落盘:insert 「设计.美学纲领与交互范式」\n 何时用:还不能回答站位",
|
||||
});
|
||||
expect(prompt).toContain("设计.正文组成");
|
||||
expect(prompt).toContain("设计.开场白");
|
||||
expect(prompt).toContain("输出.开场白");
|
||||
expect(prompt).toContain("creation-plan.v1");
|
||||
expect(prompt).toContain("设计.本局创作方案");
|
||||
expect(prompt).toContain("insert");
|
||||
expect(prompt).toContain("delete");
|
||||
expect(prompt).toContain("declare_variable");
|
||||
@@ -93,29 +92,15 @@ describe("dictate context", () => {
|
||||
expect(prompt).toContain("同 position 再 insert");
|
||||
expect(prompt).toContain("数据化跑团体验");
|
||||
expect(prompt).toMatch(/order/);
|
||||
expect(prompt).toContain("仅允许 toolcall");
|
||||
expect(prompt).toContain("设计.模仿范例");
|
||||
expect(prompt).toContain("设计.模仿要点");
|
||||
expect(prompt).toContain("设计.故事推进");
|
||||
expect(prompt).toContain("设计.叙事指南");
|
||||
expect(prompt).toContain("大纲扩写");
|
||||
expect(prompt).toContain("扮演停笔关掉");
|
||||
expect(prompt).toContain("只挂载这些产物");
|
||||
expect(prompt).toContain("【能力 · 何时落盘】");
|
||||
expect(prompt).toContain("设计.美学纲领与交互范式");
|
||||
expect(prompt).toContain("同轮可多次 insert");
|
||||
expect(prompt).toContain("不抢跑未齐条件的下游");
|
||||
expect(prompt).toContain("也不自动开跑 D");
|
||||
expect(prompt).toContain("快穿短局");
|
||||
expect(prompt).toContain("一核短局");
|
||||
expect(prompt).toMatch(/舞台骨架/);
|
||||
expect(prompt).toMatch(/覆盖 C/);
|
||||
expect(prompt).toContain("设计.主角设定");
|
||||
expect(prompt).toContain("reply_module");
|
||||
expect(prompt).toContain("对用户可见回复");
|
||||
expect(prompt).toContain("有 N 个 reply_module");
|
||||
expect(prompt).toContain("〔可反复〕须拆分");
|
||||
expect(prompt).toContain("设计.具体实例#");
|
||||
expect(prompt).toContain("通常 2~3 个");
|
||||
expect(prompt).toContain("给 2~3 个选项来问");
|
||||
expect(prompt).toContain("不是 DAG");
|
||||
expect(prompt).toContain("不是用户已选路径");
|
||||
expect(prompt).toContain("自然回应");
|
||||
expect(prompt).not.toContain("体验锚定(程序打分)");
|
||||
expect(prompt).not.toContain("## 本轮落什么");
|
||||
});
|
||||
|
||||
it("formats module catalog for dictate with land tags and when", async () => {
|
||||
@@ -209,13 +194,12 @@ describe("dictate context", () => {
|
||||
});
|
||||
const brief = index.get("设计.美学纲领与交互范式");
|
||||
expect(brief).toBeTruthy();
|
||||
expect(brief!).toContain("# 模块 · 美学纲领与交互范式");
|
||||
expect(brief!).toContain("回答时请附带本模块");
|
||||
expect(brief!).toContain("# 本轮关注 · 美学纲领与交互范式");
|
||||
expect(brief!).toContain("不要求固定标题");
|
||||
expect(brief!).toContain("本模块内容包括");
|
||||
expect(brief!).toContain("## 写入用户回复时做什么");
|
||||
expect(brief!).toContain("## 探测与追问(本模块口径)");
|
||||
expect(brief!).toContain("展示优于提问");
|
||||
expect(brief!).toMatch(/自评关注|交互范式|美学纲领/);
|
||||
expect(brief!).toContain("同轮多个产物时合并回应");
|
||||
expect(brief!).toMatch(/自评关注|核心体验|背景与规则/);
|
||||
|
||||
const alias = index.get("设计.开场白");
|
||||
expect(alias).toContain("开场白与开场变量");
|
||||
@@ -357,6 +341,145 @@ describe("dictate context", () => {
|
||||
expect(products.map((p) => p.tag)).toEqual(["设计.具体实例#lei-ying"]);
|
||||
});
|
||||
|
||||
it("splits a concrete-instance records array into one product per record", async () => {
|
||||
const products: { tag: string; content: string }[] = [];
|
||||
const body = JSON.stringify({
|
||||
schema: "context-fragment.v1",
|
||||
技能: "具体实例",
|
||||
brief: "十位目标",
|
||||
正文: {
|
||||
rule_id: "目标女配",
|
||||
batch_id: "target-npcs-roster",
|
||||
本批数量: 3,
|
||||
批次条件: ["待清算的恶女名录"],
|
||||
records: [
|
||||
{ 姓名: "苏晚", 身份: "同学" },
|
||||
{ 姓名: "林乔", 身份: "上司" },
|
||||
{ 姓名: "苏晚", 身份: "同名另一人" },
|
||||
],
|
||||
},
|
||||
});
|
||||
const driver: AgentDriver = {
|
||||
async run(input) {
|
||||
const outcome = await input.handleStep([
|
||||
{
|
||||
id: "c1",
|
||||
name: "insert",
|
||||
arguments: JSON.stringify({
|
||||
position: "设计.具体实例#target-npcs-roster",
|
||||
content: body,
|
||||
}),
|
||||
},
|
||||
]);
|
||||
expect(outcome.kind).toBe("continue");
|
||||
if (outcome.kind === "continue") {
|
||||
const payload = JSON.parse(outcome.results[0]!.content) as {
|
||||
ok?: boolean;
|
||||
split?: number;
|
||||
positions?: string[];
|
||||
};
|
||||
expect(payload.ok).toBe(true);
|
||||
expect(payload.split).toBe(3);
|
||||
expect(payload.positions).toEqual([
|
||||
"设计.具体实例#苏晚",
|
||||
"设计.具体实例#林乔",
|
||||
"设计.具体实例#苏晚-2",
|
||||
]);
|
||||
}
|
||||
return { iterations: 1, stop: { kind: "text", content: "已拆开" } };
|
||||
},
|
||||
};
|
||||
const result = await runDictateTurn({
|
||||
llm: {} as never,
|
||||
dialogue: [{ role: "user", text: "把这三位分开落盘" }],
|
||||
driver,
|
||||
repeatableFamilies: ["设计.具体实例"],
|
||||
handlers: {
|
||||
listProducts: () => products,
|
||||
writeProduct: (tag, content) => {
|
||||
products.push({ tag, content });
|
||||
},
|
||||
deleteProduct: () => false,
|
||||
readTag: () => undefined,
|
||||
writeTag: () => undefined,
|
||||
deleteTag: () => false,
|
||||
clearDialogue: () => undefined,
|
||||
},
|
||||
});
|
||||
expect(result.wroteTags).toEqual([
|
||||
"设计.具体实例#苏晚",
|
||||
"设计.具体实例#林乔",
|
||||
"设计.具体实例#苏晚-2",
|
||||
]);
|
||||
const first = JSON.parse(products[0]!.content) as {
|
||||
brief: string;
|
||||
正文: { 规则?: string; 名称?: string; 记录?: { 姓名?: string }; batch_id?: string };
|
||||
};
|
||||
expect(first.brief).toBe("苏晚");
|
||||
expect(first.正文.规则).toBe("目标女配");
|
||||
expect(first.正文.名称).toBe("苏晚");
|
||||
expect(first.正文.记录?.姓名).toBe("苏晚");
|
||||
expect(first.正文.batch_id).toBeUndefined();
|
||||
});
|
||||
|
||||
it("prepares runtime tools before persisting an opening", async () => {
|
||||
const events: string[] = [];
|
||||
const driver: AgentDriver = {
|
||||
async run(input) {
|
||||
const prepared = await input.handleStep([
|
||||
{
|
||||
id: "prepare",
|
||||
name: "prepare_opening",
|
||||
arguments: "{}",
|
||||
},
|
||||
]);
|
||||
expect(prepared.kind).toBe("continue");
|
||||
const outcome = await input.handleStep([
|
||||
{
|
||||
id: "opening",
|
||||
name: "insert",
|
||||
arguments: JSON.stringify({
|
||||
position: "设计.开场白",
|
||||
content: "@玩家推开门。",
|
||||
}),
|
||||
},
|
||||
]);
|
||||
expect(outcome.kind).toBe("continue");
|
||||
if (outcome.kind === "continue") {
|
||||
const payload = JSON.parse(outcome.results[0]!.content) as {
|
||||
runtime_prepared_before_opening?: boolean;
|
||||
preparation_mode?: string;
|
||||
};
|
||||
expect(payload.runtime_prepared_before_opening).toBe(true);
|
||||
expect(payload.preparation_mode).toBe("prepare_opening");
|
||||
}
|
||||
return {
|
||||
text: "开场已写入",
|
||||
iterations: 1,
|
||||
stop: { kind: "text", content: "开场已写入" },
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
await runDictateTurn({
|
||||
llm: {} as never,
|
||||
dialogue: [{ role: "user", text: "生成开场" }],
|
||||
driver,
|
||||
handlers: {
|
||||
listProducts: () => [],
|
||||
prepareOpening: () => events.push("prepare"),
|
||||
writeProduct: () => events.push("write"),
|
||||
deleteProduct: () => false,
|
||||
readTag: () => undefined,
|
||||
writeTag: () => undefined,
|
||||
deleteTag: () => false,
|
||||
clearDialogue: () => undefined,
|
||||
},
|
||||
});
|
||||
|
||||
expect(events).toEqual(["prepare", "write"]);
|
||||
});
|
||||
|
||||
it("insert tool result includes reply_module from lookup", async () => {
|
||||
let capturedSpec: string | undefined;
|
||||
const driver: AgentDriver = {
|
||||
@@ -378,7 +501,7 @@ describe("dictate context", () => {
|
||||
reply_module?: string;
|
||||
};
|
||||
expect(payload.ok).toBe(true);
|
||||
expect(payload.reply_module).toContain("回答时请附带本模块");
|
||||
expect(payload.reply_module).toContain("自然回复");
|
||||
capturedSpec = payload.reply_module;
|
||||
}
|
||||
return {
|
||||
@@ -601,7 +724,7 @@ describe("dictate context", () => {
|
||||
};
|
||||
expect(a.reply_module).toContain("美学纲领");
|
||||
expect(b.reply_module).toContain("主角设定");
|
||||
expect(a.reply_hint).toMatch(/多个 reply_module|本步已带回模块/);
|
||||
expect(a.reply_hint).toMatch(/多个产物|本步已带回模块/);
|
||||
expect(b.reply_hint).toContain("本步已带回模块");
|
||||
}
|
||||
return {
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { AgentDriver } from "../src/runtime/driver.js";
|
||||
import { buildDictateMessages, runDictateTurn } from "../src/dictate/index.js";
|
||||
import { scoreExperienceAnchor } from "../src/dictate/experience-anchor.js";
|
||||
|
||||
const anchored = {
|
||||
体验锚定: {
|
||||
站位: "完全代入",
|
||||
核心感觉: "垄断生存资源的掌控感",
|
||||
轮转停点: "用户要做决定时停笔等用户",
|
||||
},
|
||||
};
|
||||
|
||||
describe("experience anchor score", () => {
|
||||
it("scores the three fields and ignores self-score", () => {
|
||||
const onlySelf = scoreExperienceAnchor(
|
||||
JSON.stringify({
|
||||
自评: { 维度: [{ 名: "美学纲领", 分数: 10 }] },
|
||||
正文: { 设定逻辑: { 完备度判断: { 核心状态: "核心明确", 判断依据: "我觉得够了" } } },
|
||||
}),
|
||||
);
|
||||
expect(onlySelf.anchored).toBe(false);
|
||||
expect(onlySelf.passed).toBe(0);
|
||||
|
||||
const full = scoreExperienceAnchor(JSON.stringify(anchored));
|
||||
expect(full.anchored).toBe(true);
|
||||
expect(full.passed).toBe(3);
|
||||
expect(full.items.map((item) => item.id)).toEqual(["站位", "核心感觉", "轮转停点"]);
|
||||
});
|
||||
|
||||
it("rejects placeholders and unanswered stance", () => {
|
||||
const score = scoreExperienceAnchor(
|
||||
JSON.stringify({
|
||||
体验锚定: {
|
||||
站位: "代入还是旁观",
|
||||
核心感觉: "未定",
|
||||
轮转停点: "等用户",
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(score.anchored).toBe(false);
|
||||
expect(score.items.every((item) => !item.ok)).toBe(true);
|
||||
});
|
||||
|
||||
it("falls back to aesthetics body fields and prose labels", () => {
|
||||
const fromBody = scoreExperienceAnchor(
|
||||
JSON.stringify({
|
||||
正文: {
|
||||
设定逻辑: {
|
||||
参与方式: { 用户与user关系: { 结论: "完全代入" } },
|
||||
内容维度: { 核心感觉: { 已知: "求生时的紧张感" } },
|
||||
},
|
||||
交互范式: {
|
||||
描写权限: { 思考与抉择: "要替用户做决定时必须停笔等用户" },
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(fromBody.anchored).toBe(true);
|
||||
|
||||
const prose = scoreExperienceAnchor(
|
||||
["站位:旁观", "核心感觉:看棋子互相算计", "轮转停点:场面收束前停笔等用户下令"].join("\n"),
|
||||
);
|
||||
expect(prose.anchored).toBe(true);
|
||||
expect(prose.items.find((item) => item.id === "站位")?.value).toBe("旁观");
|
||||
});
|
||||
|
||||
it("does not inject a fixed anchor gate into dictate context", () => {
|
||||
const body =
|
||||
buildDictateMessages({
|
||||
systemPrompt: "SYS",
|
||||
products: [
|
||||
{
|
||||
tag: "设计.美学纲领与交互范式",
|
||||
content: JSON.stringify({
|
||||
体验锚定: { 站位: "完全代入", 核心感觉: "未定", 轮转停点: "未定" },
|
||||
}),
|
||||
},
|
||||
],
|
||||
dialogue: [],
|
||||
})[1]?.content ?? "";
|
||||
expect(body).toContain("当前产物");
|
||||
expect(body).toContain("设计.美学纲领与交互范式");
|
||||
expect(body).not.toContain("体验锚定(程序打分");
|
||||
expect(body).not.toContain("不得 insert 开场白");
|
||||
});
|
||||
|
||||
it("does not block opening on a fixed 3/3 anchor count", async () => {
|
||||
const products = new Map<string, string>();
|
||||
const driver: AgentDriver = {
|
||||
async run(input) {
|
||||
const first = await input.handleStep([
|
||||
{
|
||||
id: "o1",
|
||||
name: "insert",
|
||||
arguments: JSON.stringify({ position: "设计.开场白", content: "@玩家睁眼。" }),
|
||||
},
|
||||
]);
|
||||
expect(first.kind).toBe("continue");
|
||||
if (first.kind === "continue") {
|
||||
const payload = JSON.parse(first.results[0]!.content) as {
|
||||
ok?: boolean;
|
||||
error?: string;
|
||||
};
|
||||
expect(payload.ok).toBe(true);
|
||||
expect(payload.error).toBeUndefined();
|
||||
}
|
||||
|
||||
const sameStep = await input.handleStep([
|
||||
{
|
||||
id: "a1",
|
||||
name: "insert",
|
||||
arguments: JSON.stringify({
|
||||
position: "设计.美学纲领与交互范式",
|
||||
content: JSON.stringify(anchored),
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: "o2",
|
||||
name: "insert",
|
||||
arguments: JSON.stringify({ position: "设计.开场白", content: "@玩家睁眼。" }),
|
||||
},
|
||||
]);
|
||||
expect(sameStep.kind).toBe("continue");
|
||||
if (sameStep.kind === "continue") {
|
||||
const aesthetics = JSON.parse(sameStep.results[0]!.content) as {
|
||||
ok?: boolean;
|
||||
experience_anchor?: unknown;
|
||||
};
|
||||
const opening = JSON.parse(sameStep.results[1]!.content) as { ok?: boolean };
|
||||
expect(aesthetics.ok).toBe(true);
|
||||
expect(aesthetics.experience_anchor).toBeUndefined();
|
||||
expect(opening.ok).toBe(true);
|
||||
}
|
||||
return { iterations: 1, stop: { kind: "text", content: "已补三件并写开场。" } };
|
||||
},
|
||||
};
|
||||
|
||||
const result = await runDictateTurn({
|
||||
llm: {} as never,
|
||||
recipeName: "快穿短局",
|
||||
dialogue: [{ role: "user", text: "先开场" }],
|
||||
driver,
|
||||
handlers: {
|
||||
listProducts: () => [...products.entries()].map(([tag, content]) => ({ tag, content })),
|
||||
writeProduct: (tag, content) => {
|
||||
products.set(tag, content);
|
||||
},
|
||||
deleteProduct: (tag) => products.delete(tag),
|
||||
readTag: () => undefined,
|
||||
writeTag: () => undefined,
|
||||
deleteTag: () => false,
|
||||
clearDialogue: () => undefined,
|
||||
},
|
||||
});
|
||||
|
||||
expect(products.has("设计.开场白")).toBe(true);
|
||||
expect(result.wroteTags).toContain("设计.美学纲领与交互范式");
|
||||
expect(result.wroteTags).toContain("设计.开场白");
|
||||
});
|
||||
|
||||
it("does not block opening for recipes that skip the aesthetics gate", async () => {
|
||||
const products = new Map<string, string>();
|
||||
const driver: AgentDriver = {
|
||||
async run(input) {
|
||||
const outcome = await input.handleStep([
|
||||
{
|
||||
id: "o1",
|
||||
name: "insert",
|
||||
arguments: JSON.stringify({ position: "设计.开场白", content: "@玩家睁眼。" }),
|
||||
},
|
||||
]);
|
||||
expect(outcome.kind).toBe("continue");
|
||||
if (outcome.kind === "continue") {
|
||||
const payload = JSON.parse(outcome.results[0]!.content) as { ok?: boolean; error?: string };
|
||||
expect(payload.ok).toBe(true);
|
||||
expect(payload.error).toBeUndefined();
|
||||
}
|
||||
return { iterations: 1, stop: { kind: "text", content: "开场已写。" } };
|
||||
},
|
||||
};
|
||||
|
||||
await runDictateTurn({
|
||||
llm: {} as never,
|
||||
recipeName: "文本生成器",
|
||||
dialogue: [{ role: "user", text: "来一段开篇" }],
|
||||
driver,
|
||||
handlers: {
|
||||
listProducts: () => [...products.entries()].map(([tag, content]) => ({ tag, content })),
|
||||
writeProduct: (tag, content) => {
|
||||
products.set(tag, content);
|
||||
},
|
||||
deleteProduct: () => false,
|
||||
readTag: () => undefined,
|
||||
writeTag: () => undefined,
|
||||
deleteTag: () => false,
|
||||
clearDialogue: () => undefined,
|
||||
},
|
||||
});
|
||||
|
||||
expect(products.get("设计.开场白")).toContain("@玩家");
|
||||
});
|
||||
});
|
||||
@@ -5,6 +5,23 @@ import {
|
||||
} from "../web/json-doc.js";
|
||||
|
||||
describe("tryParseJsonDoc", () => {
|
||||
it("joins a fragment whose root object closed before later keys", () => {
|
||||
const raw = `{
|
||||
"schema":"context-fragment-v1",
|
||||
"技能":"主角设定",
|
||||
"brief":"点题",
|
||||
"brief_ok":true,
|
||||
"mount":"world-simulator"
|
||||
},
|
||||
"稳变":"stable",
|
||||
"正文": { "称呼": "李宗" }
|
||||
}`;
|
||||
const parsed = tryParseJsonDoc(raw);
|
||||
expect(parsed?.schema).toBe("context-fragment-v1");
|
||||
expect(parsed?.稳变).toBe("stable");
|
||||
expect(parsed?.正文?.称呼).toBe("李宗");
|
||||
});
|
||||
|
||||
it("keeps interior Chinese quotes in an otherwise valid fragment", () => {
|
||||
const raw = JSON.stringify({
|
||||
schema: "context-fragment.v1",
|
||||
|
||||
@@ -98,4 +98,26 @@ describe("maintain need_generate sampling", () => {
|
||||
});
|
||||
expect(buildChanceBatchFromGenerationRule(rules, "nickname")).toBeNull();
|
||||
});
|
||||
|
||||
it("reads flat single-rule body and string pool entries", () => {
|
||||
const rules = JSON.stringify({
|
||||
正文: {
|
||||
rule_id: "wasteland-npc",
|
||||
池: [
|
||||
{
|
||||
名称: "公开职业池",
|
||||
条目: ["厨师", "医生", "掮客"],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
const batch = buildChanceBatchFromGenerationRule(rules, "wasteland-npc");
|
||||
expect(batch).toHaveLength(1);
|
||||
expect(batch![0]!.id).toBe("pool:公开职业池");
|
||||
expect(batch![0]!.items?.map((item) => item.id)).toEqual([
|
||||
"厨师",
|
||||
"医生",
|
||||
"掮客",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -53,6 +53,39 @@ describe("opening-seal", () => {
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("reads the default candidate body without a copied 开场白全文", () => {
|
||||
const raw = JSON.stringify({
|
||||
schema: "context-fragment.v1",
|
||||
技能: "开场白与开场变量",
|
||||
正文: {
|
||||
默认候选id: "gate",
|
||||
开场白候选: [
|
||||
{
|
||||
id: "dorm",
|
||||
正文: "@玩家推开宿舍门。",
|
||||
meta: { 开场变量: [{ 名: "口粮", 值: 1 }] },
|
||||
},
|
||||
{
|
||||
id: "gate",
|
||||
正文: "@玩家停在闸机前。",
|
||||
meta: { 开场变量: [{ 名: "债额", 值: 3, 依据: "闸机票" }] },
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
const first = parseOpeningSealPayload(raw, 0);
|
||||
expect(first?.candidates).toEqual([
|
||||
"@玩家停在闸机前。",
|
||||
"@玩家推开宿舍门。",
|
||||
]);
|
||||
expect(first?.selectedText).toBe("@玩家停在闸机前。");
|
||||
expect(first?.variables).toEqual([
|
||||
{ name: "债额", value: 3, note: "闸机票" },
|
||||
]);
|
||||
const second = parseOpeningSealPayload(raw, 1);
|
||||
expect(second?.selectedText).toBe("@玩家推开宿舍门。");
|
||||
});
|
||||
|
||||
it("parses 1+ opening candidates and selects by index", () => {
|
||||
const first = parseOpeningSealPayload(fragment, 0);
|
||||
expect(first?.candidates).toHaveLength(2);
|
||||
|
||||
@@ -1,10 +1,31 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
canonicalizeInsertedContent,
|
||||
extractLastProductJson,
|
||||
selectJsonPayload,
|
||||
tryParseJsonDoc,
|
||||
} from "../src/parse/json-doc.js";
|
||||
|
||||
describe("canonicalizeInsertedContent", () => {
|
||||
it("extracts a broken fragment string into canonical JSON", () => {
|
||||
const raw = `{
|
||||
"schema":"context-fragment-v1",
|
||||
"技能":"主角设定",
|
||||
"brief":"点题",
|
||||
"mount":"world-simulator"
|
||||
},
|
||||
"正文": { "称呼": "@玩家" }
|
||||
}`;
|
||||
const stored = canonicalizeInsertedContent(raw);
|
||||
expect(JSON.parse(stored)).toMatchObject({
|
||||
schema: "context-fragment.v1",
|
||||
技能: "主角设定",
|
||||
正文: { 称呼: "@玩家" },
|
||||
});
|
||||
expect(canonicalizeInsertedContent("开场是一段散文。")).toBe("开场是一段散文。");
|
||||
});
|
||||
});
|
||||
|
||||
describe("selectJsonPayload", () => {
|
||||
it("keeps content when it is already the product", () => {
|
||||
const json = '{"schema":"context-fragment.v1","brief":"a","正文":{}}';
|
||||
|
||||
@@ -236,7 +236,7 @@ describe("phase runtime", () => {
|
||||
if (session.waitingReason?.kind === "worker_questions") {
|
||||
expect(session.waitingReason.questions[0]?.id).toBe("module-opening");
|
||||
expect(session.waitingReason.questions[0]?.prompt).toContain(
|
||||
"玩家实际会待着玩的地方",
|
||||
"会反复影响你的外部舞台",
|
||||
);
|
||||
}
|
||||
expect(llmCalls.calls()).toBe(0);
|
||||
|
||||
@@ -108,4 +108,21 @@ describe("play-aux view", () => {
|
||||
expect(dorm?.persona?.name).toBe("李宗");
|
||||
expect(gate?.persona?.name).toBe("阿泽");
|
||||
});
|
||||
|
||||
it("lists proliferated zero-layer openings for play swipe", () => {
|
||||
const board = new Blackboard();
|
||||
board.write({
|
||||
tag: "设计.0层开场白与初态#dorm",
|
||||
content: "@玩家推开宿舍门。",
|
||||
source: "agent",
|
||||
});
|
||||
board.write({
|
||||
tag: "设计.0层开场白与初态#gate",
|
||||
content: "@玩家停在闸机前。",
|
||||
source: "agent",
|
||||
});
|
||||
const view = buildPlayAuxView(board, { [SLOT_OPENING_SELECTED_INDEX]: 1 });
|
||||
expect(view.openings.map((o) => o.slot).sort()).toEqual(["dorm", "gate"]);
|
||||
expect(view.selectedOpeningIndex).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
formatShellRefsForPrompt,
|
||||
loadShellRefCatalog,
|
||||
} from "../src/skills/shell-ref-catalog.js";
|
||||
|
||||
const PACK = "dialogue/world-simulator";
|
||||
|
||||
describe("shell ref catalog", () => {
|
||||
it("loads the five reference shells with layout and sample", async () => {
|
||||
const shells = await loadShellRefCatalog(PACK);
|
||||
expect(shells.map((shell) => shell.id)).toEqual([
|
||||
"reading",
|
||||
"dual-text",
|
||||
"data-cards",
|
||||
"alongside",
|
||||
"paged",
|
||||
"piece",
|
||||
]);
|
||||
for (const shell of shells) {
|
||||
expect(shell.intro).toBeTruthy();
|
||||
expect(shell.note).toContain("不替换");
|
||||
expect(shell.note).toContain("可替换");
|
||||
expect(shell.layout).toContain(`id: ${shell.id}`);
|
||||
expect(shell.sample).toContain("present.onData");
|
||||
}
|
||||
});
|
||||
|
||||
it("tells creation to drop unused regions", async () => {
|
||||
const shells = await loadShellRefCatalog(PACK);
|
||||
const block = formatShellRefsForPrompt(shells);
|
||||
expect(block).toContain("【壳参考库】");
|
||||
expect(block).toContain("不留空位");
|
||||
expect(block).toContain("可按本局用途替换");
|
||||
expect(block).not.toContain("斗鱼弹幕");
|
||||
expect(block).not.toContain("规则怪谈");
|
||||
expect(block).toContain("`dual-text`");
|
||||
expect(block).not.toContain("chat_monitor");
|
||||
});
|
||||
});
|
||||
+130
-39
@@ -188,6 +188,30 @@ function canRefreshMessage(msg) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 游玩 0 层开场:多条时用 ‹ › 切换,选中的一条替换开场消息。
|
||||
* 与消息版本 swipe 分开,不重 roll。
|
||||
*/
|
||||
function renderOpeningSwipeHtml(view, msg) {
|
||||
if (!isPlayView(view)) return "";
|
||||
if (String(msg?.title ?? "").trim() !== "开场白") return "";
|
||||
const openings = view.playAux?.openings ?? [];
|
||||
const total = openings.length;
|
||||
if (total < 2) return "";
|
||||
const current = Math.min(
|
||||
total - 1,
|
||||
Math.max(0, Number(view.playAux?.selectedOpeningIndex) || 0),
|
||||
);
|
||||
const id = esc(msg.id);
|
||||
const prev = current - 1;
|
||||
const next = current + 1;
|
||||
return `<span class="msg-variant-nav" title="‹ › 切换开场,选中的一条作为 0 层">
|
||||
<button type="button" class="msg-action" data-msg-action="opening-prev" data-msg-id="${id}" data-opening-index="${prev}"${prev < 0 ? " disabled" : ""} aria-label="上一条开场">‹</button>
|
||||
<span class="msg-variant-count">${current + 1}/${total}</span>
|
||||
<button type="button" class="msg-action" data-msg-action="opening-next" data-msg-id="${id}" data-opening-index="${next}"${next >= total ? " disabled" : ""} aria-label="下一条开场">›</button>
|
||||
</span>`;
|
||||
}
|
||||
|
||||
/** ‹ n/total › 多版切换;末页 › 与 ↻ 均可重 roll;单版只显示 ↻ */
|
||||
function renderVariantNavHtml(msg) {
|
||||
if (!msg?.id) return "";
|
||||
@@ -741,10 +765,7 @@ function openContentPeek(payload) {
|
||||
}
|
||||
|
||||
function formatDictatePeekHtml(content) {
|
||||
const trimmed = String(content ?? "").trim();
|
||||
if (!trimmed) return `<p class="empty-sm">(无正文)</p>`;
|
||||
// 与侧栏同一路径:勿走 formatArtifactBodyHtml(## 分段会丢掉前置 JSON/正文)
|
||||
return `<div class="dictate-product-body">${safeBodyHtml(trimmed)}</div>`;
|
||||
return renderStoredProductHtml(content);
|
||||
}
|
||||
|
||||
const dictateProductContentByEl = new WeakMap();
|
||||
@@ -1144,6 +1165,13 @@ function wireMessageFeedActions(feed, handlers) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (action === "opening-prev" || action === "opening-next") {
|
||||
const index = Number(btn.getAttribute("data-opening-index"));
|
||||
if (!Number.isFinite(index) || index < 0) return;
|
||||
h.onSelectOpening?.(index);
|
||||
return;
|
||||
}
|
||||
|
||||
if (action === "variant-prev") {
|
||||
h.onSwitchVariant?.(messageId, "prev");
|
||||
return;
|
||||
@@ -1231,9 +1259,64 @@ function isPlayProcessMessage(msg, view) {
|
||||
return !isPlayFinalReply(msg, view);
|
||||
}
|
||||
|
||||
/** 开场回退把多份产物拼成 `【设计.tag】` + JSON。拆开后按产物卡渲染,不把结构化原文当散文。 */
|
||||
function splitBracketProductSections(text) {
|
||||
const trimmed = String(text || "").trim();
|
||||
const re = /^【((?:设计|用户)\.[^】]+)】[ \t]*$/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 null;
|
||||
const sections = [];
|
||||
if (hits[0].index > 0) {
|
||||
const preamble = trimmed.slice(0, hits[0].index).trim();
|
||||
if (preamble) sections.push({ title: "", content: preamble });
|
||||
}
|
||||
for (let i = 0; i < hits.length; i++) {
|
||||
const h = hits[i];
|
||||
const end = i + 1 < hits.length ? hits[i + 1].index : trimmed.length;
|
||||
sections.push({
|
||||
title: h.title,
|
||||
content: trimmed.slice(h.headerEnd, end).trim(),
|
||||
});
|
||||
}
|
||||
return sections;
|
||||
}
|
||||
|
||||
function renderStoredProductHtml(content) {
|
||||
const trimmed = String(content ?? "").trim();
|
||||
if (!trimmed) return `<p class="empty-sm">(无正文)</p>`;
|
||||
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
||||
const parsed = tryParseJsonDoc(trimmed);
|
||||
if (parsed != null && typeof parsed === "object") {
|
||||
return formatArtifactBodyHtml(trimmed, {
|
||||
flat: true,
|
||||
hideAskSidecar: true,
|
||||
hideScores: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
return `<div class="dictate-product-body">${safeBodyHtml(trimmed)}</div>`;
|
||||
}
|
||||
|
||||
function formatPlayPresentHtml(body, view) {
|
||||
const trimmed = stripPresentSourceFences((body || "").trim());
|
||||
if (!trimmed) return `<p class="empty-sm">(无正文)</p>`;
|
||||
const bundled = splitBracketProductSections(trimmed);
|
||||
if (bundled) {
|
||||
return bundled
|
||||
.map((sec) => {
|
||||
if (!sec.title) return `<p class="msg-bundle-lead">${esc(sec.content)}</p>`;
|
||||
return renderStoredProductHtml(sec.content);
|
||||
})
|
||||
.join("");
|
||||
}
|
||||
const tweaks = view?.presentationTweaks;
|
||||
const fallbackShell =
|
||||
tweaks?.shell_id && PRESENT_SHELL_IDS?.includes?.(tweaks.shell_id)
|
||||
@@ -2041,6 +2124,7 @@ function normalizeContextFragmentDoc(doc) {
|
||||
if (!doc || typeof doc !== "object" || Array.isArray(doc)) return doc;
|
||||
const row = { ...doc };
|
||||
const hasBody = row.正文 != null || row.body != null;
|
||||
if (row.schema === "context-fragment-v1") row.schema = "context-fragment.v1";
|
||||
const looksFrag =
|
||||
row.schema === "context-fragment.v1" ||
|
||||
(typeof row.技能 === "string" && row.技能.trim()) ||
|
||||
@@ -2625,27 +2709,36 @@ function collectOpeningCandidates(body) {
|
||||
const sharedVars = openingVariablesFrom(body);
|
||||
const rows = [];
|
||||
const seen = new Set();
|
||||
const push = (text, persona, variables) => {
|
||||
const push = (text, persona, variables, id) => {
|
||||
const value = openingTextFrom(text);
|
||||
if (!value || seen.has(value)) return;
|
||||
seen.add(value);
|
||||
rows.push({
|
||||
id: String(id || "").trim(),
|
||||
text: value,
|
||||
persona: persona || fallback,
|
||||
variables: variables?.length ? variables : sharedVars,
|
||||
});
|
||||
};
|
||||
push(body.开场白全文, fallback, sharedVars);
|
||||
if (Array.isArray(body.开场白候选)) {
|
||||
for (const item of body.开场白候选) {
|
||||
if (typeof item === "string") push(item, fallback, sharedVars);
|
||||
else if (item && typeof item === "object") {
|
||||
push(
|
||||
item.正文 || item.全文 || item.text || item.开场白 || item.内容,
|
||||
openingPersonaFrom(item, fallback),
|
||||
openingVariablesFrom(item),
|
||||
);
|
||||
}
|
||||
const listed = Array.isArray(body.开场白候选) ? body.开场白候选 : [];
|
||||
if (!listed.length) push(body.开场白全文, fallback, sharedVars, "");
|
||||
for (const item of listed) {
|
||||
if (typeof item === "string") push(item, fallback, sharedVars, "");
|
||||
else if (item && typeof item === "object") {
|
||||
push(
|
||||
item.正文 || item.全文 || item.text || item.开场白 || item.内容,
|
||||
openingPersonaFrom(item, fallback),
|
||||
openingVariablesFrom(item),
|
||||
item.id || item.候选id,
|
||||
);
|
||||
}
|
||||
}
|
||||
const defaultId = String(body.默认候选id || body.默认候选ID || "").trim();
|
||||
if (defaultId) {
|
||||
const index = rows.findIndex((row) => row.id === defaultId);
|
||||
if (index > 0) {
|
||||
const [picked] = rows.splice(index, 1);
|
||||
rows.unshift(picked);
|
||||
}
|
||||
}
|
||||
return rows;
|
||||
@@ -5690,24 +5783,22 @@ function renderDictateProductsList(view) {
|
||||
.replace(/#/g, " · ");
|
||||
return `<div class="dictate-product dictate-product--rail" role="listitem" data-dictate-product>
|
||||
<button type="button" class="dictate-product-sum" aria-expanded="false">
|
||||
<span class="dictate-product-sum-inner">
|
||||
<span class="dictate-product-sum-top">
|
||||
<span class="dictate-product-tag" title="${esc(tag)}">${esc(shortTag || tag || "未命名")}</span>
|
||||
${
|
||||
order != null
|
||||
? `<span class="dictate-product-order" title="相对序">${order}</span>`
|
||||
: ""
|
||||
}
|
||||
<span class="dictate-product-chevron" aria-hidden="true"></span>
|
||||
</span>
|
||||
<span class="dictate-product-sum-top">
|
||||
<span class="dictate-product-tag" title="${esc(tag)}">${esc(shortTag || tag || "未命名")}</span>
|
||||
${
|
||||
preview
|
||||
? `<span class="dictate-product-preview">${esc(preview)}${
|
||||
content.length > 72 ? "…" : ""
|
||||
}</span>`
|
||||
: `<span class="dictate-product-preview is-empty">(空)</span>`
|
||||
order != null
|
||||
? `<span class="dictate-product-order" title="相对序">${order}</span>`
|
||||
: ""
|
||||
}
|
||||
<span class="dictate-product-chevron" aria-hidden="true"></span>
|
||||
</span>
|
||||
${
|
||||
preview
|
||||
? `<span class="dictate-product-preview">${esc(preview)}${
|
||||
content.length > 72 ? "…" : ""
|
||||
}</span>`
|
||||
: `<span class="dictate-product-preview is-empty">(空)</span>`
|
||||
}
|
||||
</button>
|
||||
<div class="dictate-product-body" hidden data-pending="1"></div>
|
||||
</div>`;
|
||||
@@ -5720,9 +5811,7 @@ function hydrateDictateProductBody(card) {
|
||||
const body = card.querySelector(".dictate-product-body");
|
||||
if (!body || body.dataset.pending !== "1") return;
|
||||
const content = readDictateProductContent(card);
|
||||
body.innerHTML = content.trim()
|
||||
? safeBodyHtml(content)
|
||||
: `<p class="empty-sm">(无正文)</p>`;
|
||||
body.innerHTML = renderStoredProductHtml(content);
|
||||
delete body.dataset.pending;
|
||||
}
|
||||
|
||||
@@ -6050,9 +6139,7 @@ export function renderMessageFeed(view, loading, handlers = {}) {
|
||||
? `${view.uiPrompt}${
|
||||
view.selectedRecipe?.name
|
||||
? `\n\n已选配方:${view.selectedRecipe.name}`
|
||||
: view.recipes?.length
|
||||
? "\n\n(请先在新建作品时选定配方)"
|
||||
: ""
|
||||
: ""
|
||||
}`
|
||||
: view.waitingReason?.kind === "worker_questions"
|
||||
? "在下方回答提问。"
|
||||
@@ -6122,11 +6209,12 @@ function appendMessagesToFeed(feed, visible, view, handlers, activeQuestions, op
|
||||
!label.includes(subtitle) &&
|
||||
subtitle !== label;
|
||||
const headInner = isUser
|
||||
? `<span class="msg-time">${fmtTime(msg.createdAt)}</span>${renderMsgVariantBadge(msg)}`
|
||||
? renderMsgVariantBadge(msg)
|
||||
: `<div class="msg-head-main">
|
||||
<span class="msg-tag">${esc(label)}</span>
|
||||
${showSubtitle ? `<span class="msg-subtitle">${esc(subtitle)}</span>` : ""}
|
||||
</div>
|
||||
${renderOpeningSwipeHtml(view, msg)}
|
||||
${renderMsgVariantBadge(msg)}
|
||||
<span class="msg-time">${fmtTime(msg.createdAt)}</span>`;
|
||||
const menuTrigger = `<button type="button" class="msg-menu-trigger" data-msg-menu-trigger aria-label="打开这条消息的操作菜单" title="更多操作">⋯</button>`;
|
||||
@@ -6136,7 +6224,10 @@ function appendMessagesToFeed(feed, visible, view, handlers, activeQuestions, op
|
||||
card.dataset.originalText = body;
|
||||
|
||||
const questionsActive = kind === "worker_questions" && Boolean(activeQuestions);
|
||||
const showThinking = !isUser && msg.thinking && isPlayView(view);
|
||||
const showThinking =
|
||||
!isUser &&
|
||||
Boolean(msg.thinking) &&
|
||||
(isPlayView(view) || kind === "dictate_reply");
|
||||
const innerBody = questionsActive
|
||||
? `<p class="msg-q-index">${esc(body)}</p>`
|
||||
: kind === "worker_questions"
|
||||
|
||||
+272
-195
@@ -10,7 +10,7 @@ import {
|
||||
} from "./agent-ui.js";
|
||||
import { downloadMarkdown, sessionToMarkdown } from "./export.js";
|
||||
import { renderIntakePanel } from "./intake-ui.js";
|
||||
import { displayCreationModeLabel, displaySkillPackLabel, isFlowPlanReview, reviewComposerCopy } from "./display-labels.js";
|
||||
import { displaySkillPackLabel, isFlowPlanReview, reviewComposerCopy } from "./display-labels.js";
|
||||
import {
|
||||
clearQuestionCardState,
|
||||
collectQuestionAnswers,
|
||||
@@ -34,6 +34,16 @@ import {
|
||||
initMarkdownRender,
|
||||
isMarkdownRenderEnabled,
|
||||
} from "./markdown.js";
|
||||
import {
|
||||
filterUsedApiProfiles,
|
||||
forgetApiProfileUsage,
|
||||
loadApiProfileUsage,
|
||||
membersUsageRank,
|
||||
pruneApiProfileUsage,
|
||||
recordApiProfileUsage,
|
||||
seedApiProfileUsage,
|
||||
sortByApiProfileUsage,
|
||||
} from "./api-profile-usage.js";
|
||||
import { wireDirectiveAutocomplete } from "./directive-autocomplete.js";
|
||||
import {
|
||||
refreshDirectiveHighlight,
|
||||
@@ -79,18 +89,61 @@ let livePollFailCount = 0;
|
||||
let inflightSeq = 0;
|
||||
/** 底栏菜单:只切当前 API,不编辑 */
|
||||
let apiProfileList = [];
|
||||
let apiProfileGroups = [];
|
||||
let activeApiProfileId = null;
|
||||
/** idle | loading | ready | error */
|
||||
let apiProfileMenuStatus = "idle";
|
||||
let apiProfileMenuError = "";
|
||||
let apiProfileSwitchingId = null;
|
||||
let apiProfileSwitchingLabel = "";
|
||||
|
||||
const LIVE_POLL_MS = 280;
|
||||
const WORKSPACE_SESSION_KEY = "wa-workspace-session-v1";
|
||||
|
||||
const $ = (id) => document.getElementById(id);
|
||||
|
||||
const PHASE = { idle: "待命", running: "执行中", waiting_user: "等待你", done: "已完成", error: "出错" };
|
||||
|
||||
function readWorkspaceSession() {
|
||||
try {
|
||||
const value = JSON.parse(sessionStorage.getItem(WORKSPACE_SESSION_KEY) || "null");
|
||||
if (!value || typeof value !== "object" || typeof value.bookId !== "string") {
|
||||
return null;
|
||||
}
|
||||
return value;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function rememberWorkspaceSession() {
|
||||
if (!activeBookId || !sessionId || lastView?.bookId !== activeBookId) return;
|
||||
try {
|
||||
const mode = isPlayView(lastView) ? "play" : "design";
|
||||
sessionStorage.setItem(
|
||||
WORKSPACE_SESSION_KEY,
|
||||
JSON.stringify({
|
||||
bookId: activeBookId,
|
||||
sessionId,
|
||||
mode,
|
||||
playSaveId:
|
||||
mode === "play" ? activePlaySaveId || "play-working" : null,
|
||||
navLevel: sidebarNav.level === "book" ? "book" : "root",
|
||||
}),
|
||||
);
|
||||
} catch {
|
||||
/* sessionStorage 不可用时维持无记忆模式 */
|
||||
}
|
||||
}
|
||||
|
||||
function forgetWorkspaceSession() {
|
||||
try {
|
||||
sessionStorage.removeItem(WORKSPACE_SESSION_KEY);
|
||||
} catch {
|
||||
/* sessionStorage 不可用 */
|
||||
}
|
||||
}
|
||||
|
||||
/** 用户任务三态(表层);底层 waitingReason 映射进来 */
|
||||
const USER_TASK = {
|
||||
speak: { id: "speak", label: "说话", title: "继续说" },
|
||||
@@ -158,7 +211,8 @@ function resolveUserTask(view, loading) {
|
||||
if (canOfferSaveProduct(view)) {
|
||||
return {
|
||||
id: "speak",
|
||||
label: "可保存",
|
||||
label: USER_TASK.speak.label,
|
||||
hideChip: true,
|
||||
title: "落档产物",
|
||||
hint: view.hasProduct
|
||||
? "可再保存一份新定稿,或用已有产物开玩。"
|
||||
@@ -300,6 +354,10 @@ function esc(s) {
|
||||
return String(s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||
}
|
||||
|
||||
function attr(s) {
|
||||
return esc(s).replace(/"/g, """);
|
||||
}
|
||||
|
||||
function proposedOutputCopy(proposed) {
|
||||
const params = proposed?.params;
|
||||
const target =
|
||||
@@ -585,8 +643,10 @@ function resolveComposer(view, loading) {
|
||||
|
||||
function composerModeChip(spec) {
|
||||
const id = spec.task?.id;
|
||||
if (spec.task?.hideChip) return "";
|
||||
if (id !== "speak" && id !== "answer" && id !== "review" && id !== "busy") return "";
|
||||
const label = spec.task.label || "";
|
||||
if (!label) return "";
|
||||
const tip = [spec.taskTitle, spec.taskHint].filter(Boolean).join(" — ");
|
||||
const tone = spec.acceptAction?.tone ? ` data-tone="${esc(spec.acceptAction.tone)}"` : "";
|
||||
return `<span class="composer-mode-chip" data-task="${esc(id)}"${tone} title="${esc(tip)}">${esc(label)}</span>`;
|
||||
@@ -888,6 +948,7 @@ function summarizeApiProfile(profile) {
|
||||
id: profile.id,
|
||||
name: profile.name || "未命名",
|
||||
model: profile.model || "",
|
||||
groupId: profile.groupId || "",
|
||||
hasKey: Boolean(String(profile.apiKey ?? "").trim()),
|
||||
};
|
||||
}
|
||||
@@ -903,8 +964,7 @@ function composerMoreMenuTitle() {
|
||||
|
||||
function composerApiSubmenuMeta() {
|
||||
if (apiProfileSwitchingId) {
|
||||
const p = apiProfileList.find((x) => x.id === apiProfileSwitchingId);
|
||||
return { desc: p ? `切换到 ${p.name}…` : "切换中…", title: "API" };
|
||||
return { desc: apiProfileSwitchingLabel || "切换中…", title: "API" };
|
||||
}
|
||||
if (apiProfileMenuStatus === "loading" || apiProfileMenuStatus === "idle") {
|
||||
return { desc: "加载中…", title: "API" };
|
||||
@@ -916,12 +976,47 @@ function composerApiSubmenuMeta() {
|
||||
return { desc: "未配置", title: "API" };
|
||||
}
|
||||
const active = activeApiProfile();
|
||||
const group = apiProfileGroups.find((item) => item.id === active?.groupId);
|
||||
const groupName = group?.name ? `${group.name} · ` : "";
|
||||
const model = active?.model ? ` · ${active.model}` : "";
|
||||
return {
|
||||
desc: active?.name || "选择配置",
|
||||
title: active ? `API · ${active.name}` : "API",
|
||||
desc: active ? `${groupName}${active.name}${model}` : "选择配置",
|
||||
title: active ? `API · ${active.name}${model}` : "API",
|
||||
};
|
||||
}
|
||||
|
||||
function composerProfileButtonHtml(profile, switching) {
|
||||
const active = profile.id === activeApiProfileId;
|
||||
const disabled = !profile.hasKey || switching;
|
||||
const model = profile.model || profile.name || "未指定模型";
|
||||
const nameNote =
|
||||
profile.name && profile.name !== profile.model ? profile.name : "";
|
||||
const desc = !profile.hasKey ? "未设置 Key" : nameNote;
|
||||
const forget =
|
||||
!active && profile.id
|
||||
? `<button type="button" class="more-profile-forget" data-forget-profile="${attr(profile.id)}" title="移出曾用记录,配置仍保留">移出</button>`
|
||||
: "";
|
||||
return `<div class="more-profile-row">
|
||||
<button
|
||||
type="button"
|
||||
class="more-menu-item more-theme-item more-profile-item${active ? " is-active" : ""}"
|
||||
data-activate-profile="${attr(profile.id)}"
|
||||
role="menuitemradio"
|
||||
aria-checked="${active ? "true" : "false"}"
|
||||
${disabled ? "disabled" : ""}
|
||||
${profile.id === apiProfileSwitchingId ? "aria-busy=\"true\"" : ""}
|
||||
title="${attr(model)}"
|
||||
>
|
||||
<span class="more-theme-copy">
|
||||
<span class="more-theme-name">${esc(model)}</span>
|
||||
${desc ? `<span class="more-theme-desc">${esc(desc)}</span>` : ""}
|
||||
</span>
|
||||
<span class="more-toggle-mark" aria-hidden="true"></span>
|
||||
</button>
|
||||
${forget}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function composerApiProfileItemsHtml() {
|
||||
if (apiProfileMenuStatus === "loading" || apiProfileMenuStatus === "idle") {
|
||||
return `<p class="more-menu-empty">加载 API 配置…</p>`;
|
||||
@@ -933,50 +1028,60 @@ function composerApiProfileItemsHtml() {
|
||||
return `<p class="more-menu-empty">还没有 API 配置。<a href="/settings.html">去设置添加</a></p>`;
|
||||
}
|
||||
const switching = Boolean(apiProfileSwitchingId);
|
||||
return apiProfileList
|
||||
.map((p) => {
|
||||
const active = p.id === activeApiProfileId;
|
||||
const disabled = !p.hasKey || switching;
|
||||
const desc = !p.hasKey ? "未设置 Key" : p.model || "未指定模型";
|
||||
return `<button
|
||||
type="button"
|
||||
class="more-menu-item more-theme-item more-profile-item${active ? " is-active" : ""}"
|
||||
data-activate-profile="${esc(p.id)}"
|
||||
role="menuitemradio"
|
||||
aria-checked="${active ? "true" : "false"}"
|
||||
${disabled ? "disabled" : ""}
|
||||
${p.id === apiProfileSwitchingId ? "aria-busy=\"true\"" : ""}
|
||||
title="${esc(p.name)}${p.model ? ` · ${esc(p.model)}` : ""}"
|
||||
>
|
||||
<span class="more-theme-copy">
|
||||
<span class="more-theme-name">${esc(p.name)}</span>
|
||||
<span class="more-theme-desc">${esc(desc)}</span>
|
||||
</span>
|
||||
<span class="more-toggle-mark" aria-hidden="true"></span>
|
||||
</button>`;
|
||||
const usage = loadApiProfileUsage();
|
||||
const buckets = new Map();
|
||||
for (const profile of apiProfileList) {
|
||||
const key = profile.groupId || "";
|
||||
if (!buckets.has(key)) buckets.set(key, []);
|
||||
buckets.get(key).push(profile);
|
||||
}
|
||||
for (const [key, members] of buckets) {
|
||||
buckets.set(key, filterUsedApiProfiles(members, usage, activeApiProfileId));
|
||||
}
|
||||
const ordered = [
|
||||
...apiProfileGroups.filter((group) => buckets.has(group.id)),
|
||||
...[...buckets.keys()]
|
||||
.filter((id) => id && !apiProfileGroups.some((group) => group.id === id))
|
||||
.map((id) => ({ id, name: "未命名组" })),
|
||||
];
|
||||
if (buckets.has("")) ordered.push({ id: "", name: "未分组" });
|
||||
const sortedGroups = sortByApiProfileUsage(ordered, (group) =>
|
||||
membersUsageRank(buckets.get(group.id) || [], usage),
|
||||
);
|
||||
return sortedGroups
|
||||
.map((group) => {
|
||||
const members = buckets.get(group.id) || [];
|
||||
if (!members.length) return "";
|
||||
if (members.length === 1) {
|
||||
return composerProfileButtonHtml(members[0], switching);
|
||||
}
|
||||
const containsActive = members.some((profile) => profile.id === activeApiProfileId);
|
||||
const existing = document.querySelector(
|
||||
`#composer-api-menu [data-api-group="${CSS.escape(group.id)}"]`,
|
||||
);
|
||||
const open = existing ? existing.hasAttribute("open") : containsActive;
|
||||
const activeModel = members.find((profile) => profile.id === activeApiProfileId)?.model;
|
||||
const desc = activeModel || `${members.length} 个曾用`;
|
||||
return `<details class="api-fold" data-api-group="${attr(group.id)}"${open ? " open" : ""}>
|
||||
<summary class="more-menu-item more-theme-item api-fold-sum${containsActive ? " is-active" : ""}">
|
||||
<span class="more-theme-copy">
|
||||
<span class="more-theme-name">${esc(group.name)}</span>
|
||||
<span class="more-theme-desc">${esc(desc)}</span>
|
||||
</span>
|
||||
<span class="api-fold-chevron" aria-hidden="true"></span>
|
||||
</summary>
|
||||
<div class="api-fold-list" role="group" aria-label="${attr(group.name)}">
|
||||
${members.map((profile) => composerProfileButtonHtml(profile, switching)).join("")}
|
||||
</div>
|
||||
</details>`;
|
||||
})
|
||||
.join("");
|
||||
}
|
||||
|
||||
function composerApiSubmenuHtml() {
|
||||
function composerApiSectionHtml() {
|
||||
const meta = composerApiSubmenuMeta();
|
||||
const wasSubOpen = $("composer-api-submenu")?.hasAttribute("open");
|
||||
return `<details class="more-submenu" id="composer-api-submenu"${wasSubOpen ? " open" : ""}>
|
||||
<summary
|
||||
class="more-menu-item more-theme-item more-submenu-sum"
|
||||
title="${esc(meta.title)}"
|
||||
aria-label="${esc(meta.title)}"
|
||||
>
|
||||
<span class="more-theme-copy">
|
||||
<span class="more-theme-name">API</span>
|
||||
<span class="more-theme-desc" id="composer-api-submenu-desc">${esc(meta.desc)}</span>
|
||||
</span>
|
||||
<span class="more-submenu-chevron" aria-hidden="true"></span>
|
||||
</summary>
|
||||
<div class="more-submenu-panel" id="composer-api-menu" role="menu">
|
||||
${composerApiProfileItemsHtml()}
|
||||
</div>
|
||||
</details>`;
|
||||
return `<div class="more-menu-label" id="composer-api-label" title="${attr(meta.title)}">API</div>
|
||||
<div id="composer-api-menu" role="group" aria-label="API 配置">${composerApiProfileItemsHtml()}</div>`;
|
||||
}
|
||||
|
||||
function composerMoreMenuHtml() {
|
||||
@@ -1006,7 +1111,7 @@ function composerMoreMenuHtml() {
|
||||
<span class="hamburger-icon" aria-hidden="true"></span>
|
||||
</summary>
|
||||
<div class="more-menu-panel composer-more-panel" role="menu">
|
||||
${composerApiSubmenuHtml()}
|
||||
${composerApiSectionHtml()}
|
||||
<div class="more-menu-sep" role="separator"></div>
|
||||
<div class="more-menu-label">显示</div>
|
||||
<button
|
||||
@@ -1059,13 +1164,8 @@ function syncApiProfileMenuUi() {
|
||||
const host = $("composer-api-menu");
|
||||
if (host) host.innerHTML = composerApiProfileItemsHtml();
|
||||
const meta = composerApiSubmenuMeta();
|
||||
const desc = $("composer-api-submenu-desc");
|
||||
if (desc) desc.textContent = meta.desc;
|
||||
const subSum = document.querySelector("#composer-api-submenu > summary");
|
||||
if (subSum) {
|
||||
subSum.title = meta.title;
|
||||
subSum.setAttribute("aria-label", meta.title);
|
||||
}
|
||||
const label = $("composer-api-label");
|
||||
if (label) label.title = meta.title;
|
||||
const sum = document.querySelector("#composer-more-menu .more-menu-sum");
|
||||
if (sum) {
|
||||
const title = composerMoreMenuTitle();
|
||||
@@ -1081,7 +1181,10 @@ async function loadApiProfiles() {
|
||||
try {
|
||||
const data = await api("/api/profiles");
|
||||
apiProfileList = (data.profiles || []).map(summarizeApiProfile);
|
||||
apiProfileGroups = data.groups || [];
|
||||
activeApiProfileId = data.activeProfileId ?? apiProfileList[0]?.id ?? null;
|
||||
pruneApiProfileUsage(new Set(apiProfileList.map((p) => p.id)));
|
||||
seedApiProfileUsage(activeApiProfileId);
|
||||
apiProfileMenuStatus = "ready";
|
||||
syncApiProfileMenuUi();
|
||||
} catch (err) {
|
||||
@@ -1093,7 +1196,6 @@ async function loadApiProfiles() {
|
||||
|
||||
function closeComposerMoreMenu(menu) {
|
||||
menu?.removeAttribute("open");
|
||||
menu?.querySelector("#composer-api-submenu")?.removeAttribute("open");
|
||||
}
|
||||
|
||||
async function activateApiProfileFromMenu(id, menu) {
|
||||
@@ -1104,17 +1206,21 @@ async function activateApiProfileFromMenu(id, menu) {
|
||||
const profile = apiProfileList.find((p) => p.id === id);
|
||||
if (!profile?.hasKey) return;
|
||||
apiProfileSwitchingId = id;
|
||||
apiProfileSwitchingLabel = profile ? `切换到 ${profile.name}…` : "切换中…";
|
||||
syncApiProfileMenuUi();
|
||||
try {
|
||||
const result = await api(`/api/profiles/${encodeURIComponent(id)}/activate`, {
|
||||
method: "POST",
|
||||
});
|
||||
activeApiProfileId = result.activeProfileId || id;
|
||||
recordApiProfileUsage(activeApiProfileId);
|
||||
apiProfileSwitchingId = null;
|
||||
apiProfileSwitchingLabel = "";
|
||||
syncApiProfileMenuUi();
|
||||
closeComposerMoreMenu(menu);
|
||||
} catch (err) {
|
||||
apiProfileSwitchingId = null;
|
||||
apiProfileSwitchingLabel = "";
|
||||
syncApiProfileMenuUi();
|
||||
alert(err.message || "切换失败");
|
||||
}
|
||||
@@ -2104,6 +2210,7 @@ function showCatalog({ closeSession = true } = {}) {
|
||||
selectedBookIds = new Set();
|
||||
renderPathBar();
|
||||
renderBookList();
|
||||
if (!closeSession) rememberWorkspaceSession();
|
||||
}
|
||||
|
||||
function renderExplorerRow({
|
||||
@@ -2230,15 +2337,15 @@ function renderSpecChildren(book, product, list) {
|
||||
}
|
||||
}
|
||||
|
||||
function appendNewPlayRow(book, list, instanceId, { child = false } = {}) {
|
||||
list.appendChild(
|
||||
renderExplorerRow({
|
||||
name: "+ 新开一局",
|
||||
actionClass: "explorer-action",
|
||||
child,
|
||||
onClick: () => void startNewPlayForBook(book.id, instanceId),
|
||||
}),
|
||||
);
|
||||
function appendNewPlayFromSnapshot(book, list, instanceId) {
|
||||
const btn = document.createElement("button");
|
||||
btn.type = "button";
|
||||
btn.className = "explorer-add child";
|
||||
btn.textContent = "+";
|
||||
btn.title = "从这份创作快照新开一局";
|
||||
btn.setAttribute("aria-label", "新开一局");
|
||||
btn.addEventListener("click", () => void startNewPlayForBook(book.id, instanceId));
|
||||
list.appendChild(btn);
|
||||
}
|
||||
|
||||
function appendCurrentDesignRow(book, list) {
|
||||
@@ -2250,7 +2357,7 @@ function appendCurrentDesignRow(book, list) {
|
||||
meta: live ? "进行中" : "继续",
|
||||
active: live,
|
||||
actionClass: "explorer-design",
|
||||
title: "打开创作会话;定稿会出现在下方",
|
||||
title: "继续创作;定稿会出现在下方",
|
||||
onClick: () => void resumeDesignSession(book.id),
|
||||
}),
|
||||
);
|
||||
@@ -2275,13 +2382,11 @@ function renderBookContents(book, list) {
|
||||
if (!products.length) {
|
||||
const hint = document.createElement("p");
|
||||
hint.className = "explorer-hint";
|
||||
hint.textContent = bookPlayReady(book.id) ? "还没有规格" : "定稿后会出现在这里";
|
||||
hint.textContent = bookPlayReady(book.id) ? "还没有创作快照" : "定稿后会出现在这里";
|
||||
list.appendChild(hint);
|
||||
if (bookPlayReady(book.id)) appendNewPlayRow(book, list);
|
||||
return;
|
||||
}
|
||||
|
||||
let anyExpanded = false;
|
||||
for (const product of products) {
|
||||
const expanded = specIsExpanded(book.id, product.id);
|
||||
const bits = [];
|
||||
@@ -2290,7 +2395,7 @@ function renderBookContents(book, list) {
|
||||
renderExplorerRow({
|
||||
name: product.label,
|
||||
meta: bits.join(" · "),
|
||||
title: "展开或收起;双击改名;右键编辑产物",
|
||||
title: "已保存的创作快照;展开可看游玩",
|
||||
actionClass: "explorer-spec",
|
||||
twistie: true,
|
||||
expanded,
|
||||
@@ -2303,14 +2408,8 @@ function renderBookContents(book, list) {
|
||||
showSaveMenu(book.id, product.id, product.label, e.clientX, e.clientY),
|
||||
}),
|
||||
);
|
||||
if (expanded) {
|
||||
anyExpanded = true;
|
||||
renderSpecChildren(book, product, list);
|
||||
appendNewPlayRow(book, list, product.id, { child: true });
|
||||
}
|
||||
}
|
||||
if (!anyExpanded) {
|
||||
appendNewPlayRow(book, list, playingInstanceId || products[0]?.id);
|
||||
if (expanded) renderSpecChildren(book, product, list);
|
||||
appendNewPlayFromSnapshot(book, list, product.id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2524,7 +2623,7 @@ function setupBookBoxSelect() {
|
||||
});
|
||||
}
|
||||
|
||||
async function loadPlaySave(bookId, saveId) {
|
||||
async function loadPlaySave(bookId, saveId, { silent = false } = {}) {
|
||||
try {
|
||||
const products = productInstancesForBook(bookId);
|
||||
const run = playRunsForBook(bookId).find((r) => r.id === saveId);
|
||||
@@ -2538,9 +2637,11 @@ async function loadPlaySave(bookId, saveId) {
|
||||
);
|
||||
activePlaySaveId = saveId === "play-working" ? null : saveId;
|
||||
renderSession(data.session, false);
|
||||
return true;
|
||||
} catch (err) {
|
||||
if (lastView) renderSession(lastView, false);
|
||||
alert(err.message);
|
||||
if (!silent) alert(err.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2697,6 +2798,7 @@ function renderHeader(view, loading) {
|
||||
function closeWorkspace({ emptyCopy = "" } = {}) {
|
||||
hideEnterPicker();
|
||||
stopLivePoll();
|
||||
forgetWorkspaceSession();
|
||||
sessionId = null;
|
||||
lastView = null;
|
||||
activeBookId = null;
|
||||
@@ -3003,6 +3105,7 @@ function renderSession(view, loading = false) {
|
||||
},
|
||||
});
|
||||
renderComposer(view, loading);
|
||||
rememberWorkspaceSession();
|
||||
if (loading) startLivePoll();
|
||||
else stopLivePoll();
|
||||
}
|
||||
@@ -3386,130 +3489,20 @@ async function loadBooks() {
|
||||
|
||||
function openNewBookDialog() {
|
||||
$("input-book-title").value = "";
|
||||
void populateDirectorSelect();
|
||||
$("dialog-new-book").showModal();
|
||||
$("input-book-title").focus();
|
||||
}
|
||||
|
||||
async function populateDirectorSelect() {
|
||||
const sel = $("select-director");
|
||||
const desc = $("director-desc");
|
||||
const modeSel = $("select-creation-mode");
|
||||
const modeDesc = $("creation-mode-desc");
|
||||
const fieldDirector = $("field-director");
|
||||
let recipeDirectors = [];
|
||||
let dictateDirectors = [];
|
||||
|
||||
const fillOptions = (list) => {
|
||||
if (!sel) return;
|
||||
sel.innerHTML = "";
|
||||
if (!list.length) {
|
||||
sel.innerHTML = `<option value="">暂无配方</option>`;
|
||||
sel.required = false;
|
||||
return;
|
||||
}
|
||||
for (const d of list) {
|
||||
const opt = document.createElement("option");
|
||||
opt.value = d.id;
|
||||
opt.textContent = d.name || d.id;
|
||||
if (d.declaration) opt.dataset.declaration = d.declaration;
|
||||
sel.appendChild(opt);
|
||||
}
|
||||
sel.value = list[0].id;
|
||||
sel.required = true;
|
||||
};
|
||||
|
||||
const syncDesc = () => {
|
||||
if (!sel || !desc) return;
|
||||
if (!modeSel?.value) {
|
||||
desc.hidden = true;
|
||||
desc.textContent = "";
|
||||
return;
|
||||
}
|
||||
const list =
|
||||
modeSel.value === "dictate" ? dictateDirectors : recipeDirectors;
|
||||
const cur = list.find((d) => d.id === sel.value);
|
||||
const text = (cur?.declaration ?? "").trim();
|
||||
desc.hidden = !text;
|
||||
desc.textContent = text;
|
||||
};
|
||||
|
||||
const syncMode = () => {
|
||||
const mode = modeSel?.value?.trim() || "";
|
||||
if (!mode) {
|
||||
if (fieldDirector) fieldDirector.hidden = true;
|
||||
if (modeDesc) modeDesc.hidden = true;
|
||||
if (desc) desc.hidden = true;
|
||||
if (sel) {
|
||||
sel.innerHTML = `<option value="">请先选择进料方式</option>`;
|
||||
sel.required = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
const dictate = mode === "dictate";
|
||||
if (fieldDirector) fieldDirector.hidden = false;
|
||||
fillOptions(dictate ? dictateDirectors : recipeDirectors);
|
||||
if (modeDesc) {
|
||||
modeDesc.hidden = false;
|
||||
modeDesc.textContent = dictate
|
||||
? `${displayCreationModeLabel("dictate")}:对话写入产物;变量/映射用工具钉死;下方为对应配方。`
|
||||
: `${displayCreationModeLabel("recipe")}:按工作流计划逐步验收技能;下方为对应配方。`;
|
||||
}
|
||||
syncDesc();
|
||||
};
|
||||
|
||||
if (modeSel && !modeSel.dataset.bound) {
|
||||
modeSel.dataset.bound = "1";
|
||||
modeSel.addEventListener("change", syncMode);
|
||||
}
|
||||
if (sel && !sel.dataset.boundDesc) {
|
||||
sel.dataset.boundDesc = "1";
|
||||
sel.addEventListener("change", syncDesc);
|
||||
}
|
||||
if (!sel) return;
|
||||
try {
|
||||
const data = await api("/api/directors");
|
||||
recipeDirectors = data.recipeDirectors ?? data.directors ?? [];
|
||||
dictateDirectors = data.dictateDirectors ?? [];
|
||||
// 打开对话框时重置为「先选进料」
|
||||
if (modeSel) {
|
||||
modeSel.value = "";
|
||||
const placeholder = [...modeSel.options].find((o) => !o.value);
|
||||
if (placeholder) placeholder.selected = true;
|
||||
}
|
||||
syncMode();
|
||||
} catch (err) {
|
||||
sel.innerHTML = `<option value="">加载失败</option>`;
|
||||
if (desc) {
|
||||
desc.hidden = false;
|
||||
desc.textContent = err.message;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function createBook() {
|
||||
const title = $("input-book-title").value.trim() || "未命名作品";
|
||||
const creationMode = $("select-creation-mode")?.value?.trim() || "";
|
||||
const recipeId = $("select-director")?.value?.trim();
|
||||
if (creationMode !== "recipe" && creationMode !== "dictate") {
|
||||
alert("请先选择进料方式");
|
||||
return;
|
||||
}
|
||||
if (!recipeId) {
|
||||
alert("请选择配方");
|
||||
return;
|
||||
}
|
||||
$("btn-create-book").disabled = true;
|
||||
try {
|
||||
const directorsMeta = await api("/api/directors").catch(() => null);
|
||||
const orchestratorId = directorsMeta?.skillPackId || "world-simulator";
|
||||
const data = await api("/api/books", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
title,
|
||||
orchestratorId,
|
||||
creationMode,
|
||||
recipeId,
|
||||
orchestratorId: "world-simulator",
|
||||
creationMode: "dictate",
|
||||
}),
|
||||
});
|
||||
$("dialog-new-book").close();
|
||||
@@ -3747,8 +3740,12 @@ async function openModelComparePicker(messageId) {
|
||||
<label class="compare-profile-item">
|
||||
<input type="checkbox" name="compare-profile" value="${esc(p.id)}" ${i < 2 ? "checked" : ""} />
|
||||
<span>
|
||||
<strong>${esc(p.name)}</strong>
|
||||
<div class="muted">${esc(p.model)}</div>
|
||||
<strong>${esc(p.model || p.name)}</strong>
|
||||
<div class="muted">${esc(
|
||||
[p.name && p.name !== p.model ? p.name : "", p.baseUrl || ""]
|
||||
.filter(Boolean)
|
||||
.join(" · "),
|
||||
)}</div>
|
||||
</span>
|
||||
</label>`,
|
||||
)
|
||||
@@ -4133,6 +4130,16 @@ $("composer")?.addEventListener("click", (e) => {
|
||||
void loadApiProfiles();
|
||||
return;
|
||||
}
|
||||
const forgetBtn = e.target.closest("[data-forget-profile]");
|
||||
if (forgetBtn && menu.contains(forgetBtn)) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const id = forgetBtn.getAttribute("data-forget-profile");
|
||||
if (id && id !== activeApiProfileId && forgetApiProfileUsage(id)) {
|
||||
syncApiProfileMenuUi();
|
||||
}
|
||||
return;
|
||||
}
|
||||
const profileBtn = e.target.closest("[data-activate-profile]");
|
||||
if (profileBtn && menu.contains(profileBtn)) {
|
||||
e.preventDefault();
|
||||
@@ -4155,23 +4162,93 @@ document.addEventListener("click", (e) => {
|
||||
document.querySelectorAll("details.more-menu[open]").forEach((d) => {
|
||||
if (!d.contains(e.target)) {
|
||||
d.removeAttribute("open");
|
||||
d.querySelectorAll("details.more-submenu[open]").forEach((sub) => {
|
||||
d.querySelectorAll("details.more-submenu[open], details.api-fold[open]").forEach((sub) => {
|
||||
sub.removeAttribute("open");
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/** API 组二级菜单:同时只展开一个 */
|
||||
document.addEventListener("toggle", (e) => {
|
||||
const fold = e.target;
|
||||
if (!(fold instanceof HTMLDetailsElement) || !fold.classList.contains("api-fold") || !fold.open) {
|
||||
return;
|
||||
}
|
||||
const root = fold.closest("#composer-api-menu");
|
||||
if (!root) return;
|
||||
root.querySelectorAll("details.api-fold[open]").forEach((other) => {
|
||||
if (other !== fold) other.removeAttribute("open");
|
||||
});
|
||||
}, true);
|
||||
|
||||
async function restoreWorkspaceSession() {
|
||||
const saved = readWorkspaceSession();
|
||||
if (!saved || !books.some((book) => book.id === saved.bookId)) {
|
||||
forgetWorkspaceSession();
|
||||
return false;
|
||||
}
|
||||
|
||||
const bookId = saved.bookId;
|
||||
activeBookId = bookId;
|
||||
activePlaySaveId =
|
||||
saved.playSaveId && saved.playSaveId !== "play-working"
|
||||
? saved.playSaveId
|
||||
: null;
|
||||
if (saved.navLevel === "book") {
|
||||
navigateToBook(bookId);
|
||||
} else {
|
||||
sidebarNav = { level: "root" };
|
||||
document.body.dataset.navLevel = "root";
|
||||
renderPathBar();
|
||||
renderBookList();
|
||||
}
|
||||
|
||||
if (saved.sessionId) {
|
||||
try {
|
||||
const view = await api(
|
||||
`/api/sessions/${encodeURIComponent(saved.sessionId)}`,
|
||||
);
|
||||
if (view?.bookId === bookId) {
|
||||
renderSession(view, false);
|
||||
void fetchPlaySavesForBook(bookId);
|
||||
return true;
|
||||
}
|
||||
} catch {
|
||||
/* 服务端会话已失效时,从作品与存档恢复 */
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (saved.mode === "play") {
|
||||
await fetchPlaySavesForBook(bookId, true);
|
||||
const restored = saved.playSaveId
|
||||
? await loadPlaySave(bookId, saved.playSaveId, { silent: true })
|
||||
: false;
|
||||
if (!restored) await openCurrentPlay(bookId);
|
||||
} else {
|
||||
await openBookDesign(bookId);
|
||||
}
|
||||
if (saved.navLevel !== "book") {
|
||||
showCatalog({ closeSession: false });
|
||||
}
|
||||
return Boolean(sessionId && activeBookId === bookId);
|
||||
} catch {
|
||||
forgetWorkspaceSession();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function init() {
|
||||
initColorTheme();
|
||||
initUiFontScale();
|
||||
initMarkdownRender();
|
||||
syncThemeMenuUi();
|
||||
try {
|
||||
void populateDirectorSelect();
|
||||
await Promise.all([loadBooks(), loadPersonas(), loadApiProfiles()]);
|
||||
if (books.length) {
|
||||
showCatalog();
|
||||
const restored = await restoreWorkspaceSession();
|
||||
if (!restored) showCatalog();
|
||||
} else {
|
||||
renderEmpty();
|
||||
openNewBookDialog();
|
||||
|
||||
+67
-80
@@ -10,7 +10,7 @@
|
||||
href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;600;700&family=Noto+Serif+SC:wght@500;600;700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link rel="stylesheet" href="/styles.css?v=20260922-menu-unify" />
|
||||
<link rel="stylesheet" href="/styles.css?v=20260928-shell-order" />
|
||||
<script>
|
||||
(function () {
|
||||
try {
|
||||
@@ -32,7 +32,64 @@
|
||||
</head>
|
||||
<body data-lifecycle="design" data-nav-level="root">
|
||||
<div class="layout">
|
||||
<aside class="panel panel-rail" id="panel-books">
|
||||
<div class="main-chrome" id="main-chrome">
|
||||
<header class="main-head" id="main-head">
|
||||
<div class="main-head-left" aria-hidden="true"></div>
|
||||
<nav class="main-app-nav" aria-label="应用">
|
||||
<div id="persona-picker-host" class="persona-picker-host" hidden></div>
|
||||
<a href="/modules.html">技能</a>
|
||||
<a href="/shells.html">呈现壳</a>
|
||||
<a href="/libraries.html">库</a>
|
||||
<a href="/stats.html">Token</a>
|
||||
<a href="/settings.html">设置</a>
|
||||
</nav>
|
||||
<div class="main-head-right">
|
||||
<button type="button" class="btn-sm main-head-action" id="btn-save-instance" hidden>
|
||||
保存定稿
|
||||
</button>
|
||||
<button type="button" class="btn-sm main-head-action" id="btn-save-play" hidden>
|
||||
保存游玩
|
||||
</button>
|
||||
<button type="button" class="btn-sm main-head-action" id="btn-export" disabled>
|
||||
导出
|
||||
</button>
|
||||
<details class="more-menu theme-menu" id="theme-menu">
|
||||
<summary class="btn-sm more-menu-sum" id="theme-menu-sum" title="主题">
|
||||
<span class="hamburger-icon" aria-hidden="true"></span>
|
||||
</summary>
|
||||
<div class="more-menu-panel" role="menu">
|
||||
<div class="more-menu-label">主题</div>
|
||||
<button type="button" class="more-menu-item more-theme-item" data-color-theme="paper" role="menuitem">
|
||||
<span class="more-theme-icon" aria-hidden="true">📖</span>
|
||||
<span class="more-theme-copy"><span class="more-theme-name">仿纸暖黄</span><span class="more-theme-desc">经典护眼</span></span>
|
||||
</button>
|
||||
<button type="button" class="more-menu-item more-theme-item" data-color-theme="modern" role="menuitem">
|
||||
<span class="more-theme-icon" aria-hidden="true">✨</span>
|
||||
<span class="more-theme-copy"><span class="more-theme-name">现代清爽</span><span class="more-theme-desc">干净明快</span></span>
|
||||
</button>
|
||||
<button type="button" class="more-menu-item more-theme-item" data-color-theme="dark" role="menuitem">
|
||||
<span class="more-theme-icon" aria-hidden="true">🌙</span>
|
||||
<span class="more-theme-copy"><span class="more-theme-name">夜间暗色</span><span class="more-theme-desc">黑底灰字</span></span>
|
||||
</button>
|
||||
<button type="button" class="more-menu-item more-theme-item" data-color-theme="green" role="menuitem">
|
||||
<span class="more-theme-icon" aria-hidden="true">🍃</span>
|
||||
<span class="more-theme-copy"><span class="more-theme-name">豆沙绿</span><span class="more-theme-desc">自然疗愈</span></span>
|
||||
</button>
|
||||
<div class="more-menu-sep" role="separator"></div>
|
||||
<div class="more-menu-label">字号</div>
|
||||
<div class="more-font-scale" role="group" aria-label="字号">
|
||||
<button type="button" class="more-font-btn" data-font-scale-bump="-1" title="缩小" aria-label="缩小">−</button>
|
||||
<span class="more-font-value" id="theme-font-scale-value">100</span>
|
||||
<button type="button" class="more-font-btn" data-font-scale-bump="1" title="放大" aria-label="放大">+</button>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
|
||||
<div class="workspace-shell">
|
||||
<aside class="panel panel-rail" id="panel-books">
|
||||
<button
|
||||
type="button"
|
||||
class="rail-expand"
|
||||
@@ -81,65 +138,10 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</aside>
|
||||
|
||||
<main class="panel panel-main">
|
||||
<div class="main-chrome" id="main-chrome">
|
||||
<header class="main-head" id="main-head">
|
||||
<div class="main-head-left" aria-hidden="true"></div>
|
||||
<nav class="main-app-nav" aria-label="应用">
|
||||
<div id="persona-picker-host" class="persona-picker-host" hidden></div>
|
||||
<a href="/modules.html">技能</a>
|
||||
<a href="/shells.html">呈现壳</a>
|
||||
<a href="/libraries.html">库</a>
|
||||
<a href="/stats.html">Token</a>
|
||||
<a href="/settings.html">设置</a>
|
||||
</nav>
|
||||
<div class="main-head-right">
|
||||
<button type="button" class="btn-sm main-head-action" id="btn-save-instance" hidden>
|
||||
保存定稿
|
||||
</button>
|
||||
<button type="button" class="btn-sm main-head-action" id="btn-save-play" hidden>
|
||||
保存游玩
|
||||
</button>
|
||||
<button type="button" class="btn-sm main-head-action" id="btn-export" disabled>
|
||||
导出
|
||||
</button>
|
||||
<details class="more-menu theme-menu" id="theme-menu">
|
||||
<summary class="btn-sm more-menu-sum" id="theme-menu-sum" title="主题">
|
||||
<span class="hamburger-icon" aria-hidden="true"></span>
|
||||
</summary>
|
||||
<div class="more-menu-panel" role="menu">
|
||||
<div class="more-menu-label">主题</div>
|
||||
<button type="button" class="more-menu-item more-theme-item" data-color-theme="paper" role="menuitem">
|
||||
<span class="more-theme-icon" aria-hidden="true">📖</span>
|
||||
<span class="more-theme-copy"><span class="more-theme-name">仿纸暖黄</span><span class="more-theme-desc">经典护眼</span></span>
|
||||
</button>
|
||||
<button type="button" class="more-menu-item more-theme-item" data-color-theme="modern" role="menuitem">
|
||||
<span class="more-theme-icon" aria-hidden="true">✨</span>
|
||||
<span class="more-theme-copy"><span class="more-theme-name">现代清爽</span><span class="more-theme-desc">干净明快</span></span>
|
||||
</button>
|
||||
<button type="button" class="more-menu-item more-theme-item" data-color-theme="dark" role="menuitem">
|
||||
<span class="more-theme-icon" aria-hidden="true">🌙</span>
|
||||
<span class="more-theme-copy"><span class="more-theme-name">夜间暗色</span><span class="more-theme-desc">黑底灰字</span></span>
|
||||
</button>
|
||||
<button type="button" class="more-menu-item more-theme-item" data-color-theme="green" role="menuitem">
|
||||
<span class="more-theme-icon" aria-hidden="true">🍃</span>
|
||||
<span class="more-theme-copy"><span class="more-theme-name">豆沙绿</span><span class="more-theme-desc">自然疗愈</span></span>
|
||||
</button>
|
||||
<div class="more-menu-sep" role="separator"></div>
|
||||
<div class="more-menu-label">字号</div>
|
||||
<div class="more-font-scale" role="group" aria-label="字号">
|
||||
<button type="button" class="more-font-btn" data-font-scale-bump="-1" title="缩小" aria-label="缩小">−</button>
|
||||
<span class="more-font-value" id="theme-font-scale-value">100</span>
|
||||
<button type="button" class="more-font-btn" data-font-scale-bump="1" title="放大" aria-label="放大">+</button>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
<div class="main-stack">
|
||||
<main class="panel panel-main">
|
||||
<div class="main-stack">
|
||||
<div class="main-body">
|
||||
<section class="panel-feed" id="panel-feed">
|
||||
<div id="skill-picker" class="skill-picker" hidden></div>
|
||||
@@ -186,30 +188,15 @@
|
||||
<span class="status" id="status-pill"><span class="status-dot" id="status-dot"></span><span id="status-text">—</span></span>
|
||||
<div id="composer-main"></div>
|
||||
</footer>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<dialog id="dialog-new-book" class="dialog">
|
||||
<form method="dialog" id="form-new-book">
|
||||
<h2>新建作品</h2>
|
||||
<p class="dialog-desc">先选进料方式,再选对应配方(两套目录互不共享)。</p>
|
||||
<label class="field">
|
||||
进料方式
|
||||
<select id="select-creation-mode" required>
|
||||
<option value="" selected disabled>请选择…</option>
|
||||
<option value="recipe">工序编排</option>
|
||||
<option value="dictate">对话落盘</option>
|
||||
</select>
|
||||
</label>
|
||||
<p class="dialog-desc" id="creation-mode-desc" hidden></p>
|
||||
<label class="field" id="field-director" hidden>
|
||||
配方
|
||||
<select id="select-director">
|
||||
<option value="">请先选择进料方式</option>
|
||||
</select>
|
||||
</label>
|
||||
<p class="dialog-desc" id="director-desc" hidden></p>
|
||||
<p class="dialog-desc">创建后直接说你想写什么;助手会在对话中判断合适的创作路径。</p>
|
||||
<label class="field">
|
||||
名称
|
||||
<input id="input-book-title" type="text" placeholder="未命名作品" maxlength="80" autofocus />
|
||||
@@ -337,6 +324,6 @@
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<script type="module" src="/app.js?v=20260922-menu-unify"></script>
|
||||
<script type="module" src="/app.js?v=20260928-session-restore"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -99,6 +99,55 @@ export function softenJsonText(s) {
|
||||
return t;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根对象被提前写成 `} , "下一键"` 时丢掉这个 `}`。
|
||||
* 只在 JSON.parse 失败后使用;合法对象不会出现这种根级衔接。
|
||||
*/
|
||||
export function repairPrematureRootClose(s) {
|
||||
const text = String(s || "");
|
||||
let out = "";
|
||||
let depth = 0;
|
||||
let inString = false;
|
||||
let escaped = false;
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const c = text[i];
|
||||
if (inString) {
|
||||
out += c;
|
||||
if (escaped) escaped = false;
|
||||
else if (c === "\\") escaped = true;
|
||||
else if (c === '"') inString = false;
|
||||
continue;
|
||||
}
|
||||
if (c === '"') {
|
||||
inString = true;
|
||||
out += c;
|
||||
continue;
|
||||
}
|
||||
if (c === "{") {
|
||||
depth += 1;
|
||||
out += c;
|
||||
continue;
|
||||
}
|
||||
if (c === "}") {
|
||||
if (depth === 1 && continuesAsSiblingKey(text, i + 1)) continue;
|
||||
if (depth > 0) depth -= 1;
|
||||
out += c;
|
||||
continue;
|
||||
}
|
||||
out += c;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function continuesAsSiblingKey(text, from) {
|
||||
let j = from;
|
||||
while (j < text.length && /\s/.test(text[j])) j += 1;
|
||||
if (text[j] !== ",") return false;
|
||||
j += 1;
|
||||
while (j < text.length && /\s/.test(text[j])) j += 1;
|
||||
return text[j] === '"';
|
||||
}
|
||||
|
||||
/** 截断的 {… 补齐引号/括号,便于验收卡仍能出 mosaic */
|
||||
export function repairTruncatedJsonObject(slice) {
|
||||
let s = String(slice || "").trim();
|
||||
@@ -138,6 +187,10 @@ export function tryParseJsonDoc(text) {
|
||||
}
|
||||
|
||||
const cleaned = softenJsonText(convertStructuralSmartQuotes(stripped));
|
||||
const repaired = repairPrematureRootClose(cleaned);
|
||||
const repairedParsed =
|
||||
tryParse(repaired) ?? tryParse(softenJsonText(repaired));
|
||||
if (repairedParsed != null) return repairedParsed;
|
||||
if (
|
||||
(cleaned.startsWith('"') && cleaned.endsWith('"')) ||
|
||||
(cleaned.startsWith("'") && cleaned.endsWith("'"))
|
||||
@@ -153,9 +206,12 @@ export function tryParseJsonDoc(text) {
|
||||
const objEnd = cleaned.lastIndexOf("}");
|
||||
if (objStart >= 0 && objEnd > objStart) {
|
||||
const slice = cleaned.slice(objStart, objEnd + 1);
|
||||
const repairedSlice = repairPrematureRootClose(slice);
|
||||
const parsed =
|
||||
tryParse(slice) ??
|
||||
tryParse(softenJsonText(slice)) ??
|
||||
tryParse(repairedSlice) ??
|
||||
tryParse(softenJsonText(repairedSlice)) ??
|
||||
tryParse(repairTruncatedJsonObject(slice)) ??
|
||||
tryParse(softenJsonText(repairTruncatedJsonObject(cleaned.slice(objStart))));
|
||||
if (parsed != null) return parsed;
|
||||
|
||||
+1
-1
@@ -87,7 +87,7 @@ function setTab(tab, pushUrl = true) {
|
||||
panelTitleEl.textContent = isStyle ? "文风库" : "偏好库";
|
||||
panelSubtitleEl.textContent = isStyle
|
||||
? "跨卡「怎么写」;对话 + 样本提取 → 叙事指南选用"
|
||||
: "跨卡硬约束;对话提取 →「用户需求」节点选用";
|
||||
: "跨卡硬约束;由绑定偏好库的能力按需选用";
|
||||
extractTitleEl.textContent = isStyle ? "用 LLM 提取文风" : "用 LLM 提取偏好";
|
||||
extractHelpEl.textContent = isStyle
|
||||
? "描述想要的写法,或贴样本;助手整理成可入库草稿。点「入库」写入列表。"
|
||||
|
||||
+223
-34
@@ -12,7 +12,7 @@
|
||||
--ok: #16a34a;
|
||||
--warn: #ca8a04;
|
||||
--danger: #dc2626;
|
||||
--books-w: 216px;
|
||||
--books-w: clamp(160px, 17vw, 196px);
|
||||
--books-w-collapsed: 40px;
|
||||
--coord-w: clamp(280px, 24vw, 340px);
|
||||
--coord-w-collapsed: 40px;
|
||||
@@ -113,13 +113,21 @@ a.link:hover { text-decoration: underline; }
|
||||
|
||||
.layout {
|
||||
display: grid;
|
||||
grid-template-columns: var(--books-w) 1fr;
|
||||
grid-template-rows: auto minmax(0, 1fr);
|
||||
height: 100%;
|
||||
max-height: 100dvh;
|
||||
overflow: hidden;
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
.workspace-shell {
|
||||
display: grid;
|
||||
grid-template-columns: var(--books-w) minmax(0, 1fr);
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.panel { background: var(--surface); border: 1px solid var(--border); min-height: 0; }
|
||||
.panel-rail,
|
||||
.panel-books {
|
||||
@@ -324,10 +332,10 @@ body[data-nav-level="root"] .burst-badge { display: none; }
|
||||
|
||||
body.rail-collapsed .panel-rail { align-items: stretch; }
|
||||
/* composer æÂÂÃ¥ÂÂ
容èªéÂÂåºÂï¼Âä¸Âé´åºåÂÂæÂÂå©ä½Âé«Â度ï¼ÂæÂ´é¡µéÂÂå¨è§Âå£åÂÂ
*/
|
||||
/* panel-main:上=常驻顶栏;下=工作区 */
|
||||
/* panel-main 只承载工作区;全局顶栏位于 workspace-shell 上方 */
|
||||
.panel-main {
|
||||
display: grid;
|
||||
grid-template-rows: auto minmax(0, 1fr);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
@@ -336,6 +344,7 @@ body.rail-collapsed .panel-rail { align-items: stretch; }
|
||||
}
|
||||
|
||||
.main-stack {
|
||||
flex: 1 1 auto;
|
||||
display: grid;
|
||||
grid-template-rows: minmax(0, 1fr) auto;
|
||||
min-width: 0;
|
||||
@@ -2357,8 +2366,38 @@ body.design-workspace .flow-step[data-run="done"] .flow-name {
|
||||
@media (hover: none) {
|
||||
.explorer-del { opacity: 0.7; }
|
||||
}
|
||||
.explorer-action .explorer-name { color: var(--accent); font-weight: 500; }
|
||||
.book-list > .explorer-action { margin-top: 4px; }
|
||||
.explorer-add {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
margin: 0 0 2px 8px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--muted);
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.explorer-add.child { margin-left: 28px; }
|
||||
.explorer-add:hover {
|
||||
background: var(--bg);
|
||||
color: var(--accent);
|
||||
}
|
||||
.explorer-add:focus-visible {
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
.explorer-add:disabled {
|
||||
color: var(--muted);
|
||||
opacity: 0.45;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.explorer-add:disabled:hover { background: transparent; color: var(--muted); }
|
||||
.explorer-design .explorer-name { font-weight: 500; }
|
||||
.book-list > .explorer-design + .explorer-hint { margin-top: 2px; }
|
||||
.book-enter-pop {
|
||||
@@ -2527,6 +2566,14 @@ body.rail-collapsed .message-feed {
|
||||
font-size: 11px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
/* 用户气泡不再单独占一行显示时间;操作菜单叠在右下角,不挤正文 */
|
||||
.msg-user-row .msg-foot {
|
||||
position: absolute;
|
||||
right: 4px;
|
||||
bottom: 4px;
|
||||
padding: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
.msg-head-main { display: flex; gap: 6px; align-items: baseline; min-width: 0; flex: 1; }
|
||||
.msg-subtitle { color: var(--muted); font-weight: normal; }
|
||||
.msg-variant-badge {
|
||||
@@ -2615,7 +2662,7 @@ body.rail-collapsed .message-feed {
|
||||
font-size: 14px;
|
||||
padding: 10px 14px;
|
||||
}
|
||||
.msg-user-row .msg-body { padding-bottom: 4px; }
|
||||
.msg-user-row .msg-body { padding-bottom: 8px; }
|
||||
.msg-assistant-row .msg-head + .msg-body { padding-top: 4px; }
|
||||
.msg-pending { opacity: 0.85; }
|
||||
|
||||
@@ -3261,8 +3308,8 @@ body[data-lifecycle="play"] .msg-assistant-row .msg-head {
|
||||
padding-top: 6px;
|
||||
}
|
||||
body[data-lifecycle="play"] .msg-user-row .msg-bubble {
|
||||
width: min(100%, 28rem);
|
||||
max-width: 90%;
|
||||
width: min(100%, 42rem);
|
||||
max-width: 92%;
|
||||
}
|
||||
body[data-lifecycle="play"] .msg-play-final .msg-bubble {
|
||||
width: 100%;
|
||||
@@ -5812,11 +5859,33 @@ menu.context-menu[hidden],
|
||||
box-shadow: 0 0 0 2px var(--accent-soft);
|
||||
}
|
||||
.composer-shell-actions {
|
||||
position: absolute;
|
||||
right: 6px;
|
||||
bottom: 4px;
|
||||
z-index: 3;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding-bottom: 2px;
|
||||
gap: 6px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.14s ease;
|
||||
}
|
||||
.composer-input-shell:hover > .composer-shell-actions,
|
||||
.composer-shell-actions:focus-within {
|
||||
opacity: 0.8;
|
||||
pointer-events: auto;
|
||||
}
|
||||
.composer-shell-actions:hover {
|
||||
opacity: 0.85;
|
||||
}
|
||||
@media (hover: none) {
|
||||
.composer-shell-actions {
|
||||
opacity: 0.8;
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
.persona-picker-host {
|
||||
display: flex;
|
||||
@@ -6264,11 +6333,87 @@ menu.context-menu[hidden],
|
||||
right: auto;
|
||||
top: auto;
|
||||
bottom: calc(100% + 6px);
|
||||
min-width: 220px;
|
||||
max-width: min(80vw, 280px);
|
||||
min-width: 240px;
|
||||
max-width: min(86vw, 340px);
|
||||
/* visible:二级 API 菜单向右飞出,避免被 overflow:auto 裁切 */
|
||||
overflow: visible;
|
||||
z-index: 140;
|
||||
}
|
||||
.api-fold {
|
||||
position: relative;
|
||||
display: block;
|
||||
}
|
||||
.api-fold-sum {
|
||||
list-style: none;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.api-fold-sum .more-theme-copy {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
.api-fold-sum::-webkit-details-marker { display: none; }
|
||||
.api-fold-sum::marker { content: ""; }
|
||||
.api-fold-chevron {
|
||||
flex: 0 0 auto;
|
||||
width: 1em;
|
||||
text-align: center;
|
||||
color: var(--muted);
|
||||
}
|
||||
.api-fold-chevron::before { content: "›"; }
|
||||
.api-fold[open] > .api-fold-sum .api-fold-chevron,
|
||||
.api-fold-sum:hover .api-fold-chevron {
|
||||
color: var(--accent);
|
||||
}
|
||||
.api-fold-sum:hover,
|
||||
.api-fold[open] > .api-fold-sum {
|
||||
background: var(--bg);
|
||||
color: var(--accent);
|
||||
}
|
||||
.api-fold-list {
|
||||
position: absolute;
|
||||
left: calc(100% + 4px);
|
||||
top: 0;
|
||||
z-index: 150;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1px;
|
||||
min-width: 220px;
|
||||
max-width: min(80vw, 300px);
|
||||
max-height: min(60vh, 420px);
|
||||
overflow: auto;
|
||||
margin: 0;
|
||||
padding: 4px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
background: var(--surface);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
@media (max-width: 560px) {
|
||||
.api-fold-list {
|
||||
position: static;
|
||||
left: auto;
|
||||
top: auto;
|
||||
margin: 2px 0 4px 8px;
|
||||
padding: 2px 0 4px 6px;
|
||||
min-width: 0;
|
||||
max-width: none;
|
||||
max-height: 240px;
|
||||
border: none;
|
||||
border-left: 1px solid var(--border);
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
background: transparent;
|
||||
}
|
||||
.api-fold-chevron::before { content: "▾"; }
|
||||
.api-fold[open] > .api-fold-sum .api-fold-chevron::before { content: "▴"; }
|
||||
}
|
||||
.api-fold-sum:focus-visible,
|
||||
.api-fold-list .more-profile-item:focus-visible {
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
.more-submenu {
|
||||
position: relative;
|
||||
display: block;
|
||||
@@ -6362,6 +6507,59 @@ menu.context-menu[hidden],
|
||||
background: var(--accent-soft);
|
||||
color: var(--accent);
|
||||
}
|
||||
.more-profile-row {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
min-width: 0;
|
||||
}
|
||||
.more-profile-row .more-profile-item {
|
||||
flex: 1 1 auto;
|
||||
width: auto;
|
||||
min-width: 0;
|
||||
}
|
||||
.more-profile-forget {
|
||||
flex: 0 0 auto;
|
||||
margin: 0;
|
||||
padding: 0 8px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: var(--muted);
|
||||
font: inherit;
|
||||
font-size: 11px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.more-profile-forget:hover {
|
||||
color: var(--accent);
|
||||
background: var(--bg);
|
||||
}
|
||||
.more-profile-forget:focus-visible {
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
/* more-profile-models:与 more-submenu 默认向右飞出一致,不再强制向下展开 */
|
||||
.more-model-filter {
|
||||
margin: 2px 4px 6px;
|
||||
width: calc(100% - 8px);
|
||||
box-sizing: border-box;
|
||||
padding: 6px 8px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
background: var(--bg);
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
font-size: 12px;
|
||||
}
|
||||
.more-model-filter:focus-visible {
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
.more-model-empty {
|
||||
margin: 0;
|
||||
padding: 6px 10px 8px;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
}
|
||||
.more-menu-empty {
|
||||
margin: 0;
|
||||
padding: 8px 10px 10px;
|
||||
@@ -7123,10 +7321,9 @@ menu.context-menu[hidden],
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
background: var(--surface);
|
||||
overflow: hidden;
|
||||
border: none;
|
||||
background: transparent;
|
||||
overflow: visible;
|
||||
}
|
||||
.dictate-products-empty-block {
|
||||
margin: 28px 16px;
|
||||
@@ -7160,9 +7357,11 @@ menu.context-menu[hidden],
|
||||
border-bottom: none;
|
||||
}
|
||||
.dictate-product--rail > .dictate-product-sum {
|
||||
display: block;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
padding: 8px 12px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
border: none;
|
||||
@@ -7173,29 +7372,20 @@ menu.context-menu[hidden],
|
||||
cursor: context-menu;
|
||||
user-select: none;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
.dictate-product-sum-inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
padding: 8px 10px 8px 12px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
min-width: 0;
|
||||
pointer-events: none; /* 整行命中落在 button 上,避免子节点吞事件 */
|
||||
transition: background 0.1s ease;
|
||||
}
|
||||
.dictate-product--rail > .dictate-product-sum:hover .dictate-product-sum-inner {
|
||||
.dictate-product--rail > .dictate-product-sum:hover {
|
||||
background: color-mix(in srgb, var(--accent-soft) 50%, transparent);
|
||||
}
|
||||
.dictate-product--rail > .dictate-product-sum:focus-visible {
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
.dictate-product--rail.is-open > .dictate-product-sum .dictate-product-sum-inner {
|
||||
.dictate-product--rail.is-open > .dictate-product-sum {
|
||||
background: color-mix(in srgb, var(--bg) 70%, var(--surface));
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
@@ -7256,7 +7446,6 @@ menu.context-menu[hidden],
|
||||
white-space: nowrap;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
padding-right: 20px;
|
||||
}
|
||||
.dictate-product--rail .dictate-product-preview.is-empty {
|
||||
font-style: italic;
|
||||
@@ -7304,7 +7493,7 @@ menu.context-menu[hidden],
|
||||
max-width: 100%;
|
||||
}
|
||||
body[data-coord-rail-mode="products"] .coord-drawer-body {
|
||||
padding: 8px;
|
||||
padding: 0;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
min-width: 0;
|
||||
|
||||
Reference in New Issue
Block a user