引入游玩回合与开场收口,抽出可替换 agent 司机并支持运行中止;完善预设洞位、世界书拼装与试跑,并打磨会话运行时与 Web 交互。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -12,12 +12,12 @@
|
|||||||
| 叙事指南与故事推进 | **专用 mosaic**(风格与写法 \| 推进与决策) | `renderNarrativeBodyHtml` |
|
| 叙事指南与故事推进 | **专用 mosaic**(风格与写法 \| 推进与决策) | `renderNarrativeBodyHtml` |
|
||||||
| 变量设计 | **结构化卡**(真值 / side_effects) | `renderVariableDesignHtml` |
|
| 变量设计 | **结构化卡**(真值 / side_effects) | `renderVariableDesignHtml` |
|
||||||
| 变量控制上下文 | 通用 fragment | context-fragment |
|
| 变量控制上下文 | 通用 fragment | context-fragment |
|
||||||
| 正文组成 | 通用 fragment + 游玩期 **present 壳** | fragment;游玩 `present-shells.js` |
|
| 正文组成 | **专用卡**:present 壳渲染 `正文.示例.灌数`(看美化)+ 契约折叠 | `renderReplyFormatBodyHtml` |
|
||||||
| 设计监控栏 | 通用 fragment | context-fragment |
|
| 设计监控栏 | 通用 fragment | context-fragment |
|
||||||
| 游玩拓扑 | **槽位芯片卡** | `renderPlaySlotsHtml` |
|
| 游玩拓扑 | **槽位芯片卡** | `renderPlaySlotsHtml` |
|
||||||
| 上下文投影排序 | **排序卡** | `renderContextOrderHtml` |
|
| 上下文投影排序 | **排序卡** | `renderContextOrderHtml` |
|
||||||
| 裁决包 settlement.v1 | **分节卡** | `renderSettlementSectionsHtml` |
|
| 裁决包 settlement.v1 | **分节卡** | `renderSettlementSectionsHtml` |
|
||||||
| 开场白与开场变量 | 通用 fragment | context-fragment |
|
| 开场白与开场变量 | 专用:候选 swipe + 呈现壳 | `renderOpeningSetupBodyHtml` |
|
||||||
| 拓扑图谱 | (骨架未写) | — |
|
| 拓扑图谱 | (骨架未写) | — |
|
||||||
|
|
||||||
**说明:** 「通用 fragment」= 公共头(brief/mount/自评)+ 正文结构化树,已比生 JSON 友好;「专用」= 按技能排版的 mosaic/卡片。
|
**说明:** 「通用 fragment」= 公共头(brief/mount/自评)+ 正文结构化树,已比生 JSON 友好;「专用」= 按技能排版的 mosaic/卡片。
|
||||||
|
|||||||
@@ -121,7 +121,8 @@
|
|||||||
"steps": [
|
"steps": [
|
||||||
{ "id": "美学纲领与交互范式", "name": "美学纲领与交互范式", "depends_on": [] },
|
{ "id": "美学纲领与交互范式", "name": "美学纲领与交互范式", "depends_on": [] },
|
||||||
{ "id": "生成规则", "name": "生成规则", "depends_on": ["美学纲领与交互范式"] },
|
{ "id": "生成规则", "name": "生成规则", "depends_on": ["美学纲领与交互范式"] },
|
||||||
{ "id": "生成规则#2", "name": "生成规则", "depends_on": ["生成规则"] }
|
{ "id": "生成规则#2", "name": "生成规则", "depends_on": ["生成规则"] },
|
||||||
|
{ "id": "美学纲领与交互范式·改", "name": "美学纲领与交互范式", "mode": "revise", "revises": "美学纲领与交互范式", "depends_on": ["生成规则#2"] }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -133,9 +134,13 @@
|
|||||||
| `id` | 本局步骤唯一键;同技能多次必须不同 |
|
| `id` | 本局步骤唯一键;同技能多次必须不同 |
|
||||||
| `name` | 必须与技能目录中的**固定中文名**完全一致(可重复) |
|
| `name` | 必须与技能目录中的**固定中文名**完全一致(可重复) |
|
||||||
| `depends_on` | 依赖的其它步骤 **id**(若某 name 在本流程唯一,也可写 name) |
|
| `depends_on` | 依赖的其它步骤 **id**(若某 name 在本流程唯一,也可写 name) |
|
||||||
|
| `mode` | 可选。缺省 / `fresh` = 从零新建;`revise` = 回头修改,继承 `revises` 所指那步的既有产物 |
|
||||||
|
| `revises` | `mode=revise` 时必填:被改的既有步骤 id(须更前、同 name) |
|
||||||
|
| `params` | 可选。普通节点=编排参数。〔先验产物〕=编排器规划的产物内容(可空;执行注入【规划产物】) |
|
||||||
|
|
||||||
程序用中文 `name` 映射到 `artifact` tag;流程 JSON **不写**英文模块 id / tag。
|
程序用中文 `name` 映射到 `artifact` tag;流程 JSON **不写**英文模块 id / tag。
|
||||||
**禁止**一次排死全程固定长链;「生成规则」「具体实例」等标了 `repeatable` 的技能应允许多次编入。
|
**禁止**一次排死全程固定长链;「生成规则」「具体实例」等标了 `repeatable` 的技能应允许多次**新建**编入。已完成节点要改用 `mode=revise`,不要再排一条 fresh。
|
||||||
|
〔先验产物〕节点可提前规划多条「写什么」写入 params;规划可空,「生成什么」在步内钉。
|
||||||
|
|
||||||
## 2.5 已合并技能(勿拆回)
|
## 2.5 已合并技能(勿拆回)
|
||||||
|
|
||||||
@@ -326,6 +331,8 @@ boundary: |
|
|||||||
|------|------|
|
|------|------|
|
||||||
| `declaration` | 插入编排提示,选型 |
|
| `declaration` | 插入编排提示,选型 |
|
||||||
| `name` / `artifact` | 流程与执行映射 |
|
| `name` / `artifact` | 流程与执行映射 |
|
||||||
|
| `kind` | 可选;节点特性。`prior-artifact` = 先验产物(须先定写什么;params 是规划产物,执行注入【规划产物】,不拦确认开干) |
|
||||||
|
| `params` | 可选;普通节点=编排参数。〔先验产物〕=规划产物字段(可提前填,空则步内钉) |
|
||||||
| `opening` | 可选覆盖;一般只写在 prompt 的 `opening` 块 |
|
| `opening` | 可选覆盖;一般只写在 prompt 的 `opening` 块 |
|
||||||
|
|
||||||
改 `name` 会破坏已有工作流计划 JSON,尽量不改。
|
改 `name` 会破坏已有工作流计划 JSON,尽量不改。
|
||||||
@@ -462,12 +469,12 @@ artifact:
|
|||||||
| 美学纲领与交互范式 | `aesthetics-interaction` | 范例 |
|
| 美学纲领与交互范式 | `aesthetics-interaction` | 范例 |
|
||||||
| 实现机制 | `mechanism` | 已写 |
|
| 实现机制 | `mechanism` | 已写 |
|
||||||
| 舞台骨架 | `world-blueprint` | 已写(`context-fragment.v1`;社会结构 + 世界状况) |
|
| 舞台骨架 | `world-blueprint` | 已写(`context-fragment.v1`;社会结构 + 世界状况) |
|
||||||
| 生成规则 | `generation-rules` | 已写(`context-fragment.v1`;可反复;合同严、正文可长) |
|
| 生成规则 | `generation-rules` | 已写(`context-fragment.v1`;可反复;〔先验产物〕;合同严、正文可长) |
|
||||||
| 具体实例 | `concrete-instances` | 已写(`context-fragment.v1`;可反复;只按规则执行) |
|
| 具体实例 | `concrete-instances` | 已写(`context-fragment.v1`;可反复;〔先验产物〕;只按规则执行) |
|
||||||
| 叙事指南与故事推进 | `narrative` | 已写(`context-fragment.v1`;遣词/笔墨/禁忌+推进;旧称叙事指南) |
|
| 叙事指南与故事推进 | `narrative` | 已写(`context-fragment.v1`;遣词/笔墨/禁忌+推进;旧称叙事指南) |
|
||||||
| 正文组成 | `reply-format` | 已写(用户可见版式+隐藏段+前端拆分;旧称设计回复格式) |
|
| 正文组成 | `reply-format` | 已写(壳适配 + 示例 present 预览看美化;旧称设计回复格式) |
|
||||||
| 设计监控栏 | `status-bar` | 已写(只盯会变信息;旧称设计状态栏) |
|
| 设计监控栏 | `status-bar` | 已写(只盯会变信息;旧称设计状态栏) |
|
||||||
| 开场白与开场变量 | `opening-setup` | 已写(开场正文+初值;守正文组成) |
|
| 开场白与开场变量 | `opening-setup` | 已写(〔收口〕:1~多条开场;选定后落库并保存定稿) |
|
||||||
| 拓扑图谱 | `topology` | 待细写 |
|
| 拓扑图谱 | `topology` | 待细写 |
|
||||||
| 变量设计与更新规则 | `variable-design` | 已写;见 progressive-data-design.md |
|
| 变量设计与更新规则 | `variable-design` | 已写;见 progressive-data-design.md |
|
||||||
| 变量控制上下文 | `variable-context` | 已写 |
|
| 变量控制上下文 | `variable-context` | 已写 |
|
||||||
|
|||||||
@@ -57,7 +57,7 @@
|
|||||||
生成规则正文:必要性判断 + `rules[]`(生成与描写 + **单层**产物格式 + 可选多池绑顶层字段);自评=必要性/属性妥当/格式准确。
|
生成规则正文:必要性判断 + `rules[]`(生成与描写 + **单层**产物格式 + 可选多池绑顶层字段);自评=必要性/属性妥当/格式准确。
|
||||||
美学纲领自评仍为:交互范式 / 美学纲领 / 整体协调(勿与生成规则三维混用)。
|
美学纲领自评仍为:交互范式 / 美学纲领 / 整体协调(勿与生成规则三维混用)。
|
||||||
叙事指南与故事推进正文:一份全文(纲领/遣词/笔墨/禁忌/推进等);**整份**双挂转述与主世界层,不为省 token 拆两套投影。artifact=`设计.叙事指南与故事推进`。
|
叙事指南与故事推进正文:一份全文(纲领/遣词/笔墨/禁忌/推进等);**整份**双挂转述与主世界层,不为省 token 拆两套投影。artifact=`设计.叙事指南与故事推进`。
|
||||||
正文组成:**基于固定呈现壳适配微调**(选 `shell_id` + 微调轴;块只挂壳已有区域);artifact=`设计.正文组成`。见 `docs/play-presentation-shells.md`。
|
正文组成:**基于固定呈现壳适配微调**(选 `shell_id` + 微调轴;块只挂壳已有区域);正文含 **`示例`**(`present.v1` 灌数,验收壳与美化;非正史,文笔不重要);artifact=`设计.正文组成`。见 `docs/play-presentation-shells.md`。
|
||||||
设计监控栏:只监控会变字段(可含攻略目标/场景交互物);artifact=`设计.监控栏`(旧称状态栏)。
|
设计监控栏:只监控会变字段(可含攻略目标/场景交互物);artifact=`设计.监控栏`(旧称状态栏)。
|
||||||
变量设计:真值 + **维护语句** + **Data映射索引**(指向具体实例)+ side_effects。
|
变量设计:真值 + **维护语句** + **Data映射索引**(指向具体实例)+ side_effects。
|
||||||
变量控制上下文:各槽视野 + **旁观汇总**;mount 含主世界层与旁观维护。
|
变量控制上下文:各槽视野 + **旁观汇总**;mount 含主世界层与旁观维护。
|
||||||
@@ -82,8 +82,8 @@ order 4: 用户.最新输入(full)
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"schema": "context-order.v1",
|
"schema": "context-order.v1",
|
||||||
"brief": "扁平投影序",
|
"brief": "先名册再分配:旁观 + 主世界层 + 转述",
|
||||||
"play_slots": { "gm": true, "narrator": true, "perspective": false },
|
"play_slots": { "auditor": true, "gm": true, "narrator": true, "perspective": false },
|
||||||
"slots": [
|
"slots": [
|
||||||
{
|
{
|
||||||
"ref": "world-simulator",
|
"ref": "world-simulator",
|
||||||
@@ -99,6 +99,8 @@ order 4: 用户.最新输入(full)
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
创作期 LLM 也可先写 `agents[]`(`id` 为 `auditor` / `gm` / `narrator` / `perspective` / `chance`,含 `enabled` / `when` / `inserts`);程序会收成上表的 `slots` + `play_slots`。运行时仍读 `slots[].ref`(worker ref)。
|
||||||
|
|
||||||
| 字段 | 说明 |
|
| 字段 | 说明 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| `order` | 每槽独立扁平数字序;**0 建议为人设** |
|
| `order` | 每槽独立扁平数字序;**0 建议为人设** |
|
||||||
|
|||||||
@@ -36,9 +36,11 @@ Book 跨 Session:过程、资产、游玩
|
|||||||
新建作品 → UI 选配方 → 用户首句
|
新建作品 → UI 选配方 → 用户首句
|
||||||
→ design-flow:排出近期 设计.创作流程(工作流计划 / 增量 DAG,status=open|closed)
|
→ design-flow:排出近期 设计.创作流程(工作流计划 / 增量 DAG,status=open|closed)
|
||||||
→ 反复 design-step:注入当前技能 modules/{id}/prompt.md + 依赖产物
|
→ 反复 design-step:注入当前技能 modules/{id}/prompt.md + 依赖产物
|
||||||
→ 不够则再 design-flow(可追加 / 反复调用 repeatable 技能)→ closed
|
→ 不够则再 design-flow(可追加 / 反复调用 repeatable 技能作**新建**;已完成节点用回头修改继承旧稿)→ closed
|
||||||
→ 收成 设计.worker集(运行规格)→ 验收后才可进 play
|
→ 收成 设计.worker集(运行规格)→ 验收后才可进 play
|
||||||
→ (可选)opening-generator → 用户手动进游玩
|
→ 开场白与开场变量(若编排):1~多条开场 → 选定 → 程序落库、关 DAG、保存 instance
|
||||||
|
→ (无开场白节点时可选)opening-generator 兜底落库
|
||||||
|
→ 用户手动进游玩
|
||||||
→ run:执行单元按契约从黑板重装;acceptance=review 处停、压缩过程 tag
|
→ run:执行单元按契约从黑板重装;acceptance=review 处停、压缩过程 tag
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
→ 按「要同时看见什么」选 shell_id(不是按好看)
|
→ 按「要同时看见什么」选 shell_id(不是按好看)
|
||||||
→ 只改允许的微调轴
|
→ 只改允许的微调轴
|
||||||
→ 禁止改分区骨架、禁止发明第八种布局
|
→ 禁止改分区骨架、禁止发明第八种布局
|
||||||
游玩期:往壳区域灌数;正文随 600~3000 字撑高滚动
|
游玩期:往壳区域灌数;正文随 500~2000 字撑高滚动
|
||||||
```
|
```
|
||||||
|
|
||||||
选壳依据(同时可见关系):
|
选壳依据(同时可见关系):
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
|
|
||||||
| 优先级 | 区域 | 视觉待遇 |
|
| 优先级 | 区域 | 视觉待遇 |
|
||||||
|--------|------|----------|
|
|--------|------|----------|
|
||||||
| **P0** | `body` | 阅读主轴;**随字数撑高**(单轮按 600~3000 字设计),由页面/消息区滚动 |
|
| **P0** | `body` | 阅读主轴;**随字数撑高**(单轮按 500~2000 字设计),由页面/消息区滚动 |
|
||||||
| **P1** | 建议行动 | 要决策时醒目;`choice_dock` 坞 **sticky 贴底** |
|
| **P1** | 建议行动 | 要决策时醒目;`choice_dock` 坞 **sticky 贴底** |
|
||||||
| **P2** | `monitor` | 芯片密排,高度固定矮 |
|
| **P2** | `monitor` | 芯片密排,高度固定矮 |
|
||||||
| **P3** | `header` / `footer` / `aside` | 辅助;双栏 **aside sticky**,长文仍可对照 |
|
| **P3** | `header` / `footer` / `aside` | 辅助;双栏 **aside sticky**,长文仍可对照 |
|
||||||
@@ -88,6 +88,7 @@
|
|||||||
正文.呈现壳.shell_id → 七选一
|
正文.呈现壳.shell_id → 七选一
|
||||||
正文.呈现壳.微调 → 只填允许轴
|
正文.呈现壳.微调 → 只填允许轴
|
||||||
正文.可见块[] → 挂到壳已有区域
|
正文.可见块[] → 挂到壳已有区域
|
||||||
|
正文.示例.灌数 → present.v1 形状预览(非正史;专供验收壳/美化)
|
||||||
|
|
||||||
设计.监控栏 → monitor 字段表
|
设计.监控栏 → monitor 字段表
|
||||||
运行.本轮.裁决 → body 事实;suggested_actions → 行动区
|
运行.本轮.裁决 → body 事实;suggested_actions → 行动区
|
||||||
@@ -130,4 +131,4 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
修订:2026-08-13 · 七壳;明确信息层级与主面积。
|
修订:2026-08-22 · 七壳;终稿阅读柱;单轮正文 500~2000 字。
|
||||||
|
|||||||
+60
-25
@@ -216,39 +216,72 @@ type UnsupportedPresetSection = {
|
|||||||
|
|
||||||
## 4. 上下文装配规则
|
## 4. 上下文装配规则
|
||||||
|
|
||||||
一次请求的上下文装配按以下顺序执行:
|
运行时只走一份 `promptOrder`:遇到普通条目原样插入,遇到 marker 就填洞。空的酒馆角色卡 / 世界书 marker **不填我们的正文**(跳过)。
|
||||||
|
|
||||||
|
### 4.1 Marker 词汇表
|
||||||
|
|
||||||
|
酒馆原有(导入后保留位置,空洞跳过):
|
||||||
|
|
||||||
```text
|
```text
|
||||||
1. 读取 PresetPackage.promptOrder。
|
worldInfoBefore personaDescription charDescription charPersonality
|
||||||
|
scenario worldInfoAfter dialogueExamples chatHistory
|
||||||
|
```
|
||||||
|
|
||||||
|
我们新增(由 context-order 填值;生成内容按 **世界书条目** 看待,不是角色卡字段):
|
||||||
|
|
||||||
|
| identifier | 填什么 |
|
||||||
|
|---|---|
|
||||||
|
| `worldBookBefore` | `对话.历史` 之前的 inserts,每条为标题+正文 |
|
||||||
|
| `chatHistory` | 共用酒馆 id;填 `对话.历史` |
|
||||||
|
| `worldBookAfter` | 历史之后、本轮之前(变量、工单等)。**不是** `worldInfoAfter` |
|
||||||
|
| `currentTurn` | `用户.最新输入`,role=user |
|
||||||
|
| `postTurn` | 本轮输入之后的 inserts(可空) |
|
||||||
|
|
||||||
|
`worldInfoBefore` / `worldInfoAfter` 都在酒馆历史 **前**,不能承接「我们写的、历史后」,因此不要复用那两个 id。
|
||||||
|
|
||||||
|
### 4.2 导入时插入四个洞
|
||||||
|
|
||||||
|
导入 SillyTavern `prompt_order` 时,以 `chatHistory` 为锚点切开并插入:
|
||||||
|
|
||||||
|
```text
|
||||||
|
[...ST 历史前条目...]
|
||||||
|
worldBookBefore
|
||||||
|
chatHistory
|
||||||
|
worldBookAfter
|
||||||
|
currentTurn
|
||||||
|
[...ST 历史后条目...]
|
||||||
|
postTurn
|
||||||
|
```
|
||||||
|
|
||||||
|
若原文没有 `chatHistory`,把它补在历史前条目之后,再接后四个洞。原生预设也可以直接按这份扩展 order 来写。无选用预设时,play worker 仍按现有 `assembleWorkerContext` 拼装(不走 prompt_order)。
|
||||||
|
|
||||||
|
社区预设里包着 WI 洞的装饰分隔会留在历史前段,中间可能是空的。第一版接受。
|
||||||
|
|
||||||
|
### 4.3 六段夹心(扫 order 后的效果)
|
||||||
|
|
||||||
|
```text
|
||||||
|
1. 酒馆内、历史前(ST 普通条目)
|
||||||
|
2. 我们写的、历史前(worldBookBefore)
|
||||||
|
3. 对话历史(chatHistory)
|
||||||
|
4. 我们写的、历史后本轮前(worldBookAfter)
|
||||||
|
5. 本轮对话(currentTurn)
|
||||||
|
6. 本轮对话后(ST 历史后条目 + postTurn)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.4 装配步骤
|
||||||
|
|
||||||
|
```text
|
||||||
|
1. 读取(已扩展过的)PresetPackage.promptOrder。
|
||||||
2. 过滤 enabled=false 的顺序项。
|
2. 过滤 enabled=false 的顺序项。
|
||||||
3. 找到对应 PresetPromptEntry。
|
3. 找到对应 PresetPromptEntry。
|
||||||
4. 再过滤 entry.enabled=false 的条目。
|
4. 再过滤 entry.enabled=false 的条目。
|
||||||
5. 对 marker 条目进行运行时替换。
|
5. 对 marker:用 WorldInfoPack 解析我们的四个洞 + chatHistory;酒馆角色卡/WI 恒为空。
|
||||||
6. 对普通 content 条目进行变量替换。
|
6. 普通 content 条目原样插入。
|
||||||
7. 按 role 合并成 LLM messages。
|
7. 按条目 role 合成 LLM messages。
|
||||||
8. 应用 generation 参数。
|
8. 应用 generation 参数。
|
||||||
```
|
```
|
||||||
|
|
||||||
其中 marker 条目用于挂载运行时上下文:
|
不实现:世界书关键词、概率、sticky、Depth 插进历史中间、outlet、正则、插件脚本。
|
||||||
|
|
||||||
```text
|
|
||||||
charDescription
|
|
||||||
可以映射为当前创作项目的设定说明。
|
|
||||||
|
|
||||||
charPersonality
|
|
||||||
可以映射为文风、叙事人称、角色行为约束。
|
|
||||||
|
|
||||||
worldInfoBefore / worldInfoAfter
|
|
||||||
可以映射为长期设定、世界观、知识库片段。
|
|
||||||
|
|
||||||
scenario
|
|
||||||
可以映射为当前创作目标或当前阶段说明。
|
|
||||||
|
|
||||||
chatHistory
|
|
||||||
可以映射为历史会话、已有正文、用户反馈摘要。
|
|
||||||
```
|
|
||||||
|
|
||||||
这些映射不是 SillyTavern 原义的完整复刻,而是为了兼容预设资产,让它们能服务我们的写作 agent。
|
|
||||||
|
|
||||||
## 5. 与创作流程的关系
|
## 5. 与创作流程的关系
|
||||||
|
|
||||||
@@ -270,6 +303,8 @@ Runtime Session
|
|||||||
|
|
||||||
因此,同一个 preset 可以用于多个创作流程;同一个创作流程也可以切换不同 preset。二者是正交关系。
|
因此,同一个 preset 可以用于多个创作流程;同一个创作流程也可以切换不同 preset。二者是正交关系。
|
||||||
|
|
||||||
|
生成参数可全局合并进 LLM 请求。**prompt 条目 + marker 填洞只在 play worker 上用**:创作步 / 总管 tool loop 不得把角色卡骨架套到 design-step 上,也不得在填洞后再叠一份相同的 user 上下文。
|
||||||
|
|
||||||
## 6. 第一版导入策略
|
## 6. 第一版导入策略
|
||||||
|
|
||||||
第一版导入器只做保守转换:
|
第一版导入器只做保守转换:
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
|
|
||||||
| kind | 名称 | 何时存 | 存什么 |
|
| kind | 名称 | 何时存 | 存什么 |
|
||||||
|------|------|--------|--------|
|
|------|------|--------|--------|
|
||||||
| **`instance`** | 创作定稿截面 | Worker 集 accept;可选含 **锁定开局** | `设计.worker集`、静态设定 tag、`运行.初始变量`、`输出.开场白`;**不含**后续轮次、事件流 |
|
| **`instance`** | 创作定稿截面 | Worker 集 accept;**选定开场后自动存**(也可手动) | `设计.worker集`、静态设定 tag、`运行.初始变量`、`输出.开场白`;**不含**后续轮次、事件流 |
|
||||||
| **`opening`** | 开局锁定(可选单独存) | 创作末尾 swipe 选定开场后 | 与 instance 中开局部分相同;便于「同 Worker 集、换开局再开 play 线」 |
|
| **`opening`** | 开局锁定(可选单独存) | 创作末尾 swipe 选定开场后 | 与 instance 中开局部分相同;便于「同 Worker 集、换开局再开 play 线」 |
|
||||||
| **`run`** | 游玩存档 | play 任意时刻 | instance/opening 层 + `运行.*`、轮次、变量当前值、对话 |
|
| **`run`** | 游玩存档 | play 任意时刻 | instance/opening 层 + `运行.*`、轮次、变量当前值、对话 |
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -35,7 +35,7 @@
|
|||||||
|
|
||||||
**一层选型**:新建作品只选**配方**,不要再叠「能力包 + 配方」两层。
|
**一层选型**:新建作品只选**配方**,不要再叠「能力包 + 配方」两层。
|
||||||
|
|
||||||
**调味**:配方给出的建议步骤是**近期起点**(horizon);编排时可增量追加、可反复调用标了可反复的技能。验收的是本局【工作流计划】与【运行规格】,不是磁盘上的静态菜谱。
|
**调味**:配方给出的建议步骤是**近期起点**(horizon);编排时可增量追加、可反复调用标了可反复的技能(每次=彻底新建)。已完成节点要改用**回头修改**(继承旧产物),不必整局重开。验收的是本局【工作流计划】与【运行规格】,不是磁盘上的静态菜谱。
|
||||||
|
|
||||||
**剧本拆分**:旧称「剧本」同时罩住流程与规格,易混。现固定拆成:
|
**剧本拆分**:旧称「剧本」同时罩住流程与规格,易混。现固定拆成:
|
||||||
- **工作流计划** = 怎么排技能步骤(DAG)
|
- **工作流计划** = 怎么排技能步骤(DAG)
|
||||||
@@ -100,7 +100,7 @@
|
|||||||
|---------|----------|------|
|
|---------|----------|------|
|
||||||
| `design-flow` | **创作 · 流程编排** | 以已选【配方】为起点,编排/增量修订工作流计划(可变 DAG) |
|
| `design-flow` | **创作 · 流程编排** | 以已选【配方】为起点,编排/增量修订工作流计划(可变 DAG) |
|
||||||
| `design-step` | **创作 · 执行步骤** | 按工作流计划执行当前【技能】 |
|
| `design-step` | **创作 · 执行步骤** | 按工作流计划执行当前【技能】 |
|
||||||
| `opening-generator` | **开局 · 开场白** | 创作末尾可选 |
|
| `opening-generator` | **开局 · 开场白** | 无 `opening-setup` 时的兜底落库;有则选定开场白节点即收口 |
|
||||||
| `worker-spec`(技能) | **游玩拓扑** | 勾选固定槽;旧称「Worker 规格」,勿再当自由发明演员 |
|
| `worker-spec`(技能) | **游玩拓扑** | 勾选固定槽;旧称「Worker 规格」,勿再当自由发明演员 |
|
||||||
| `design-core` 等 | (已废弃) | 旧分步 skill;勿再调度 |
|
| `design-core` 等 | (已废弃) | 旧分步 skill;勿再调度 |
|
||||||
|
|
||||||
@@ -182,7 +182,7 @@
|
|||||||
| `intake` | 描述需求 | 发送 |
|
| `intake` | 描述需求 | 发送 |
|
||||||
| `input` | 补充说明 | 发送 / 补充并发送 |
|
| `input` | 补充说明 | 发送 / 补充并发送 |
|
||||||
| `worker_questions` | 答题 | 补充并发送 |
|
| `worker_questions` | 答题 | 补充并发送 |
|
||||||
| `approve_step` | 确认 | 同意并开始 |
|
| `approve_step` | 确认 | 同意并开始;回头修改时文案为「回头修改 · {技能名}」 |
|
||||||
| `review_artifact` | 视产物(勿一律「验收」) | **流程编排** → 确认编排;技能步 → 接受;开场 → 选定此开场。旁挂「按意见改编排 / 按意见修改」 |
|
| `review_artifact` | 视产物(勿一律「验收」) | **流程编排** → 确认编排;技能步 → 接受;开场 → 选定此开场。旁挂「按意见改编排 / 按意见修改」 |
|
||||||
| `next_intent` | 说话 | 继续(可留空) |
|
| `next_intent` | 说话 | 继续(可留空) |
|
||||||
| `revision` | 说话 | 按意见修改 |
|
| `revision` | 说话 | 按意见修改 |
|
||||||
|
|||||||
@@ -86,8 +86,11 @@ declaration: …
|
|||||||
| 字段 | 作用 |
|
| 字段 | 作用 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| `id` / `name` / `declaration` / `artifact` | 索引与产物映射;`declaration` 可被 prompt `meta` 覆盖 |
|
| `id` / `name` / `declaration` / `artifact` | 索引与产物映射;`declaration` 可被 prompt `meta` 覆盖 |
|
||||||
| `repeatable` | 可选;`true` = 允许同能力多次编入增量 DAG |
|
| `repeatable` | 可选;`true` = 允许同能力多次**新建**编入增量 DAG(每次彻底新的,不改旧条) |
|
||||||
| `params` | 可选;编排期 `steps[].params` 声明(`key`/`label`/`required`/`hint`);必填项须在进执行前钉齐 |
|
| `closer` | 可选;`true` = 创作终节点(选定后落库、关 DAG、保存定稿) |
|
||||||
|
| `auto` | 可选;`true` = 程序步(提案后直接执行) |
|
||||||
|
| `kind` | 可选;节点特性。缺省=普通执行步。`prior-artifact` = **先验产物**(须先定写什么;params 是规划产物,执行注入【规划产物】,不拦确认开干) |
|
||||||
|
| `params` | 可选;普通节点=编排期 `steps[].params` 声明(必填项须在进执行前钉齐)。〔先验产物〕=规划产物字段(可提前填,空则步内钉) |
|
||||||
| `opening` | 可选覆盖;一般只写在 prompt 的 `opening` 块 |
|
| `opening` | 可选覆盖;一般只写在 prompt 的 `opening` 块 |
|
||||||
|
|
||||||
编排器注入【能力 · 可选工序】时,会读取各能力 `prompt.md` 的 `meta`(`declaration` / `when` / `when_not` / `boundary`),**不是**只看 catalog 短声明。执行全文仍只在 design-step 注入。
|
编排器注入【能力 · 可选工序】时,会读取各能力 `prompt.md` 的 `meta`(`declaration` / `when` / `when_not` / `boundary`),**不是**只看 catalog 短声明。执行全文仍只在 design-step 注入。
|
||||||
@@ -104,6 +107,15 @@ declaration: …
|
|||||||
|
|
||||||
配方**不要**重复罗列各能力调用条件;那是能力 meta 的职责。旧字段 `hint` 仍可读作兜底。
|
配方**不要**重复罗列各能力调用条件;那是能力 meta 的职责。旧字段 `hint` 仍可读作兜底。
|
||||||
|
|
||||||
|
### 节点特性(`kind`)
|
||||||
|
|
||||||
|
节点特性是 catalog 上的固定系统,不是某一步的特例。缺省为普通执行步;后续会加更多 kind。
|
||||||
|
|
||||||
|
| kind | 中文 | 行为 |
|
||||||
|
|------|------|------|
|
||||||
|
| (缺省) | 普通执行步 | 有必填 params 时,确认开干前钉齐 |
|
||||||
|
| `prior-artifact` | 先验产物 | 须先定「写什么」(生成规则、具体实例)。编排器可提前把规划写入 `params`——规划多条是正确的。执行时注入【规划产物】;规划可空,步内钉/修订。不拦确认开干 |
|
||||||
|
|
||||||
### 节点循环(通用)
|
### 节点循环(通用)
|
||||||
|
|
||||||
```text
|
```text
|
||||||
@@ -113,15 +125,21 @@ declaration: …
|
|||||||
→ 用户选选项 / 写意见 →「按意见修改」
|
→ 用户选选项 / 写意见 →「按意见修改」
|
||||||
(产物 + 追问 + 选项 + 意见 + 节点要求一并重跑)→ 新产物
|
(产物 + 追问 + 选项 + 意见 + 节点要求一并重跑)→ 新产物
|
||||||
→ 循环直到「接受」
|
→ 循环直到「接受」
|
||||||
→「下一步想写什么」(可留空,交给工作流计划 DAG)
|
→ 直接提案下一节点并确认开干(普通节点可在此钉编排参数;〔先验产物〕不在此钉「写什么」;〔程序步〕如投影排序跳过确认直接执行)
|
||||||
→ 展示下一节点并确认开干(可在此钉编排参数,如生成规则的 target)
|
· 可反复技能再来一次 = 彻底新建(新 id / 新 params)
|
||||||
|
· 已完成节点要改 = 回头修改(mode=revise,继承旧产物)
|
||||||
→ 下一节点 opening / design-step …
|
→ 下一节点 opening / design-step …
|
||||||
直到创作收口
|
直到「开场白与开场变量」(〔收口〕)
|
||||||
|
→ 产出 1~多条开场白 → 选定 → 程序落库、关闭 DAG、保存创作定稿
|
||||||
```
|
```
|
||||||
|
|
||||||
「按意见修改」只重跑当前节点,不得跳到「下一步想写什么」。底栏空且未选追问时 Enter=接受;已选追问时 Enter=按意见修改。点按钮仍只做按钮自己的事。
|
「按意见修改」只重跑当前节点,不得跳到「下一步想写什么」。底栏空且未选追问时 Enter=接受;已选追问时 Enter=按意见修改。点按钮仍只做按钮自己的事。
|
||||||
|
|
||||||
有编排参数的能力:可在确认开干时钉齐 params;勿把「生成什么」拖到执行中途才问。
|
「回头修改」是编排层能力:给已完成步骤追加一条 `mode=revise` 的新步,执行时注入既有产物继续改。它**不是**〔可反复〕再来一次——后者从零新建。漏了前面的节点、或谈完才发现要改上游,用回头修改,不必整局重开。
|
||||||
|
|
||||||
|
〔先验产物〕节点(生成规则、具体实例):编排器可提前规划「写什么」写入 params——提前规划多条是正确的。确认开干不补「生成什么」;执行时注入【规划产物】,步内钉或修订。
|
||||||
|
|
||||||
|
有编排参数的**普通**能力:可在确认开干时钉齐 params。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -140,15 +158,15 @@ recipes/world-simulator|expand-assistant/recipe.yaml
|
|||||||
| 美学纲领与交互范式 | `aesthetics-interaction` | **范例已写**;前端 mosaic 视图 |
|
| 美学纲领与交互范式 | `aesthetics-interaction` | **范例已写**;前端 mosaic 视图 |
|
||||||
| 实现机制 | `mechanism` | **已写**:`context-fragment.v1`(支撑点正文 + 自评 + 追问);`feeds: gm`;**专用卡已接** |
|
| 实现机制 | `mechanism` | **已写**:`context-fragment.v1`(支撑点正文 + 自评 + 追问);`feeds: gm`;**专用卡已接** |
|
||||||
| 舞台骨架 | `world-blueprint` | **已写**:`context-fragment.v1`;社会结构 + 世界状况;**专用卡已接** |
|
| 舞台骨架 | `world-blueprint` | **已写**:`context-fragment.v1`;社会结构 + 世界状况;**专用卡已接** |
|
||||||
| 生成规则 | `generation-rules` | **已写**:`context-fragment.v1`;宽原则 + 严合同键;可反复;`params.target`;挂 gm+auditor;**专用卡已接** |
|
| 生成规则 | `generation-rules` | **已写**:`context-fragment.v1`;宽原则 + 严合同键;可反复;**〔先验产物〕**;规划字段 `target`;挂 gm+auditor;**专用卡已接** |
|
||||||
| 具体实例 | `concrete-instances` | **已写**:`context-fragment.v1`;只按规则执行;可反复;`params.rule_id` |
|
| 具体实例 | `concrete-instances` | **已写**:`context-fragment.v1`;只按规则执行;可反复;**〔先验产物〕**;规划字段 `rule_id` |
|
||||||
| 叙事指南与故事推进 | `narrative` | **已写**:一份全文双挂;**不**为省 token 拆投影;结构化卡 |
|
| 叙事指南与故事推进 | `narrative` | **已写**:一份全文双挂;**不**为省 token 拆投影;结构化卡 |
|
||||||
| 拓扑图谱 | `topology` | 骨架,待细写(拟改为选项小循环或降级) |
|
| 拓扑图谱 | `topology` | 骨架,待细写(拟改为选项小循环或降级) |
|
||||||
| 设计监控栏 | `status-bar` | **已写**:只盯会变信息;通用 fragment 卡 |
|
| 设计监控栏 | `status-bar` | **已写**:只盯会变信息;通用 fragment 卡 |
|
||||||
| 正文组成 | `reply-format` | **已写**:壳适配;P2 present 渲染已接 |
|
| 正文组成 | `reply-format` | **已写**:壳适配 + 正文.`示例`(present.v1 预览,看美化);P2 present 渲染已接 |
|
||||||
| 变量设计与更新规则 | `variable-design` | **已写**;专用/结构化卡 |
|
| 变量设计与更新规则 | `variable-design` | **已写**;专用/结构化卡 |
|
||||||
| 变量控制上下文 | `variable-context` | **已写**:旁观汇总;mount 含 auditor;通用 fragment 卡 |
|
| 变量控制上下文 | `variable-context` | **已写**:旁观汇总;mount 含 auditor;通用 fragment 卡 |
|
||||||
| 开场白与开场变量 | `opening-setup` | **已写**:开场守版式+同真相初值 |
|
| 开场白与开场变量 | `opening-setup` | **已写**:开场守版式+同真相初值;**〔收口〕**选定后落库并保存定稿 |
|
||||||
|
|
||||||
共用收成(池内保留,按需):
|
共用收成(池内保留,按需):
|
||||||
|
|
||||||
@@ -170,6 +188,6 @@ recipes/world-simulator|expand-assistant/recipe.yaml
|
|||||||
1. 只选配方 → 出**近期**创作流程(`status=open`)
|
1. 只选配方 → 出**近期**创作流程(`status=open`)
|
||||||
2. design-step 能切割出 `opening`/`task`/…
|
2. design-step 能切割出 `opening`/`task`/…
|
||||||
3. 有 `opening` 时先程序开场再 LLM
|
3. 有 `opening` 时先程序开场再 LLM
|
||||||
4. 可追加同能力多次(不同 step.id + params);收成前 `status=closed`
|
4. 可追加同能力多次(不同 step.id + params,**彻底新建**);也可 `mode=revise` 回头修改已完成节点(继承旧产物);收成前 `status=closed`
|
||||||
5. UI 用编排术语;有编排参数的步骤须展示 params
|
5. UI 用编排术语;有编排参数的步骤须展示 params;回头修改在流程上标「回头修改」,确认开干文案与「接下来生成」区分
|
||||||
6. 缺必填 params 的步骤不得视为可执行(校验失败 / 编排先 askUser)
|
6. 缺必填 params 的**普通**步骤不得视为可执行(校验失败 / 编排先 askUser);〔先验产物〕规划可空,不拦确认开干;回头修改可继承原步 params
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ worker-templates/
|
|||||||
```text
|
```text
|
||||||
选导演(recipes)→ design-flow → 验收近期 设计.创作流程(status=open)
|
选导演(recipes)→ design-flow → 验收近期 设计.创作流程(status=open)
|
||||||
→ 反复 design-step(注入 modules/{id}/prompt.md)
|
→ 反复 design-step(注入 modules/{id}/prompt.md)
|
||||||
→ 不够则再 design-flow(可追加 repeatable 能力)→ closed
|
→ 不够则再 design-flow(可追加 repeatable 新建,或 mode=revise 回头修改)→ closed
|
||||||
→ (可选)opening-generator → 手动进 play
|
→ (可选)opening-generator → 手动进 play
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -76,28 +76,15 @@ feeds: design_only
|
|||||||
## probe
|
## probe
|
||||||
|
|
||||||
```probe
|
```probe
|
||||||
用户首句或黑板已有的直接采纳。一次只在产物「追问」写 1~2 点(建议选项 + 示例);顶层 askUser 留 null,不要同一问写两遍。
|
本步只钉三件:站位(代入 / 镜头看哪 / 爽从哪来)、核心感觉(反复感受到什么)、轮转边界(何时等用户)。
|
||||||
已有一轮追问答复或核心体验+权限边界已可执行:`追问.题目` 用 `[]`,让用户验收。禁止这轮问交互、下轮问美学地来回打转。
|
|
||||||
|
|
||||||
【默认问题已覆盖】变造(原型+变点)、代入与否、最想感受到的核心。首答后按缺口补,勿重问已答清的。
|
用户首句或黑板已有的直接采纳。默认问题已覆盖变造、代入、核心感觉;开场若已跳过,把「用户.需求」首句当答复。已答清的不重问。
|
||||||
若开场已跳过(用户.需求已有首句):把首句当默认问题的答复,只补真正缺口。
|
|
||||||
|
|
||||||
【参与 · 结构性】
|
缺其中一件:在产物「追问」写 1~2 点(建议选项 + 短场景);顶层 askUser 为 null。三件够验收:`追问.题目` 为 `[]`,内容维度等其余键写「未定」或省略。
|
||||||
- 用户与 <user>:代入程度 / 情感距离 / 控制期待 / 多角色
|
|
||||||
- 输入解释:主视角行动 vs 世界行动;正文人称(第二人称 / 上帝 / 第一人称…)
|
|
||||||
- 焦点位置:自己 / 人物 / 群像 / 关系 / 世界规则 / 感觉 / 叙事
|
|
||||||
- 满足来源:感同身受 / 互动 / 观察 / 掌控 / 创作质量
|
|
||||||
完备:能判断用户站哪、镜头看哪、爽从哪来。
|
|
||||||
|
|
||||||
【内容 · 深度随焦点】
|
用户讲人物、舞台、规则:只抽取能改这三件的一句写入正文,其余写入「开放问题」,本步不追问细节。
|
||||||
核心感觉;人物与关系;身体与感官;背景与规则(宜短);意义与主题。
|
|
||||||
变造已定位的,只挖服务核心体验的部分。
|
|
||||||
|
|
||||||
【轮转】
|
用两种站位或两种感觉的短场景让用户选或改,不追问开放式「你还想要什么」。
|
||||||
思考与抉择、言语:何时等、怎么等、例外、输入后怎么补(只确认/补细节/补心理/连带后果)。
|
|
||||||
|
|
||||||
【收成三块】
|
|
||||||
体验内核(含形状:过程/张力/层次/构成/循环);呈现要点;边界禁忌。
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## output
|
## output
|
||||||
|
|||||||
@@ -4,8 +4,13 @@
|
|||||||
# name = 固定中文名(流程 JSON 的 name;同能力可多次时靠 step.id 区分)
|
# name = 固定中文名(流程 JSON 的 name;同能力可多次时靠 step.id 区分)
|
||||||
# declaration = catalog 短声明(可被 prompt.md ```meta 覆盖)
|
# declaration = catalog 短声明(可被 prompt.md ```meta 覆盖)
|
||||||
# artifact = 执行期产物 tag(程序映射;不写进流程 JSON)
|
# artifact = 执行期产物 tag(程序映射;不写进流程 JSON)
|
||||||
# repeatable = true 时允许同能力多次编入增量 DAG(如生成规则、具体实例)
|
# repeatable = true 时允许同能力多次**新建**编入增量 DAG(如生成规则、具体实例)。
|
||||||
# params = 可选;编排进 DAG 时 steps[].params 的声明(required 须在执行前钉齐)
|
# 回头修改已完成节点不是 repeatable:DAG 用 mode=revise + revises,继承旧产物。
|
||||||
|
# closer = true 时为创作终节点:选定后程序落库、关 DAG、保存定稿
|
||||||
|
# auto = true 时为程序步:不抛默认问题、不经「同意并开始」,提案后直接执行(产物仍验收)
|
||||||
|
# kind = 节点特性。缺省=普通执行步。prior-artifact=先验产物(须先定写什么;params 是规划产物,不拦执行)
|
||||||
|
# params = 可选;普通节点=编排进 DAG 时 steps[].params 的声明(required 须在执行前钉齐)
|
||||||
|
# 〔先验产物〕=规划产物字段(可提前填,空则步内钉)
|
||||||
# opening = 可选;覆盖 prompt.md 里 ```opening 默认问题(一般只写在 prompt.md)
|
# opening = 可选;覆盖 prompt.md 里 ```opening 默认问题(一般只写在 prompt.md)
|
||||||
#
|
#
|
||||||
# 编排选型:程序会读取各 modules/{id}/prompt.md 的 meta
|
# 编排选型:程序会读取各 modules/{id}/prompt.md 的 meta
|
||||||
@@ -15,8 +20,10 @@
|
|||||||
# 后来写能力:同结构 prompt.md(含默认问题)+ 本表一行索引
|
# 后来写能力:同结构 prompt.md(含默认问题)+ 本表一行索引
|
||||||
#
|
#
|
||||||
# 世界模拟器常用能力见下「体验→世界→机制→呈现→收成」;编排按需选用,勿默认全选。
|
# 世界模拟器常用能力见下「体验→世界→机制→呈现→收成」;编排按需选用,勿默认全选。
|
||||||
# 流程是可变增量 DAG:勿一次排完全程;可反复调用标了 repeatable 的能力。
|
# 流程是可变增量 DAG:勿一次排完全程;可反复调用标了 repeatable 的能力(每次=彻底新建)。
|
||||||
# 有编排参数的能力:DAG 步骤必须是带 params 的具体调用,禁止空壳进 design-step。
|
# 已完成节点要改:编排 mode=revise(继承旧稿),不要再排一条 fresh。
|
||||||
|
# 有编排参数的普通能力:DAG 步骤必须是带 params 的具体调用,禁止空壳进 design-step。
|
||||||
|
# 〔先验产物〕例外:params 是规划产物,可空;「写什么」在步内钉。
|
||||||
|
|
||||||
modules:
|
modules:
|
||||||
# —— 体验契约 ——
|
# —— 体验契约 ——
|
||||||
@@ -37,6 +44,7 @@ modules:
|
|||||||
|
|
||||||
- id: generation-rules
|
- id: generation-rules
|
||||||
name: 生成规则
|
name: 生成规则
|
||||||
|
kind: prior-artifact
|
||||||
repeatable: true
|
repeatable: true
|
||||||
declaration: >-
|
declaration: >-
|
||||||
仅当某类内容既重要又无法凭基底稳定生成时:写出规则
|
仅当某类内容既重要又无法凭基底稳定生成时:写出规则
|
||||||
@@ -45,19 +53,17 @@ modules:
|
|||||||
params:
|
params:
|
||||||
- key: target
|
- key: target
|
||||||
label: 生成对象
|
label: 生成对象
|
||||||
required: true
|
hint: 规划字段;编排器已知写哪一类就填。空则步内钉
|
||||||
hint: 本轮为哪一类内容建规则;编排时 askUser 给选项+其它;用户改对象时必须同步改 rule_id 与步骤 id
|
|
||||||
- key: rule_id
|
- key: rule_id
|
||||||
label: 规则 id
|
label: 规则 id
|
||||||
required: false
|
hint: 英文 kebab-case,须与 target 对应(女租客→female-tenants);改对象时禁止沿用旧 id
|
||||||
hint: 英文 kebab-case,须与 target 对应(女租客→female-tenants,丧尸→zombies);改对象时禁止沿用旧 id
|
|
||||||
- key: lifecycle_intent
|
- key: lifecycle_intent
|
||||||
label: 生命周期意图
|
label: 生命周期意图
|
||||||
required: false
|
hint: runtime_only | seed_only | seed_and_runtime;不明则步内钉
|
||||||
hint: runtime_only | seed_only | seed_and_runtime;不明则 askUser
|
|
||||||
|
|
||||||
- id: concrete-instances
|
- id: concrete-instances
|
||||||
name: 具体实例
|
name: 具体实例
|
||||||
|
kind: prior-artifact
|
||||||
repeatable: true
|
repeatable: true
|
||||||
declaration: >-
|
declaration: >-
|
||||||
按预生成规则原样产出实际产物;不设计、不改合同;可按 rule_id/批次反复调用
|
按预生成规则原样产出实际产物;不设计、不改合同;可按 rule_id/批次反复调用
|
||||||
@@ -65,15 +71,12 @@ modules:
|
|||||||
params:
|
params:
|
||||||
- key: rule_id
|
- key: rule_id
|
||||||
label: 调用规则
|
label: 调用规则
|
||||||
required: true
|
hint: 规划字段;须来自已验收生成规则中 seed_only/seed_and_runtime 的 rule_id。空则步内钉
|
||||||
hint: 必须来自已验收生成规则中 seed_only/seed_and_runtime 的真实 rule_id;编排时给选项
|
|
||||||
- key: batch_goal
|
- key: batch_goal
|
||||||
label: 批次目标
|
label: 批次目标
|
||||||
required: false
|
|
||||||
hint: 本批要锚定什么(如开局重要NPC)
|
hint: 本批要锚定什么(如开局重要NPC)
|
||||||
- key: count
|
- key: count
|
||||||
label: 数量
|
label: 数量
|
||||||
required: false
|
|
||||||
hint: 数字或「按规则」;contextual 时写清规模依据
|
hint: 数字或「按规则」;contextual 时写清规模依据
|
||||||
|
|
||||||
- id: narrative
|
- id: narrative
|
||||||
@@ -127,9 +130,10 @@ modules:
|
|||||||
# —— 开局(创作偏晚) ——
|
# —— 开局(创作偏晚) ——
|
||||||
- id: opening-setup
|
- id: opening-setup
|
||||||
name: 开场白与开场变量
|
name: 开场白与开场变量
|
||||||
|
closer: true
|
||||||
declaration: >-
|
declaration: >-
|
||||||
写出遵循正文组成/叙事等契约的开场一小段,并钉同真相的开场变量初值;
|
创作终节点:写出 1~多条开场白并钉同真相初值;
|
||||||
开场为主、填表为辅;可与 opening-generator 落库
|
选定后程序落库、结束创作并保存定稿
|
||||||
artifact: 设计.开场白与开场变量
|
artifact: 设计.开场白与开场变量
|
||||||
|
|
||||||
# —— 收成 / 钉声明(共用;世界模拟器按需) ——
|
# —— 收成 / 钉声明(共用;世界模拟器按需) ——
|
||||||
@@ -144,9 +148,10 @@ modules:
|
|||||||
- id: context-order
|
- id: context-order
|
||||||
name: 上下文投影排序
|
name: 上下文投影排序
|
||||||
repeatable: false
|
repeatable: false
|
||||||
|
auto: true
|
||||||
declaration: >-
|
declaration: >-
|
||||||
根据各上游上下文概括,为启用槽写出扁平投影序(含对话.历史与 projection);
|
先抄固定执行单元名册,再为每个已启用者写出扁平投影序
|
||||||
挂谁已定、排第几在本步;不发明执行单元、不重写长文
|
(含对话.历史与 projection);不发明槽、不重写长文、不演情节选择题
|
||||||
artifact: 设计.上下文投影排序
|
artifact: 设计.上下文投影排序
|
||||||
|
|
||||||
- id: refine
|
- id: refine
|
||||||
|
|||||||
@@ -27,7 +27,11 @@ feeds: gm
|
|||||||
## opening
|
## opening
|
||||||
|
|
||||||
```opening
|
```opening
|
||||||
规则已由【本步参数】rule_id 钉死。有批次目标或数量偏好可一句带过;否则直接「按规则生成」。
|
先认定本批按哪条规则生成:
|
||||||
|
- 若【规划产物】已有 rule_id,以此为起点;要换规则就在本步改(须仍是已验收的预生成规则)。
|
||||||
|
- 若规划为空,从已验收生成规则里选出一条预生成规则。
|
||||||
|
|
||||||
|
有批次目标或数量偏好可一句带过;否则直接「按规则生成」。
|
||||||
```
|
```
|
||||||
|
|
||||||
## task
|
## task
|
||||||
@@ -37,9 +41,11 @@ feeds: gm
|
|||||||
|
|
||||||
本步 = 规则执行器。禁止重议对象族、生命周期、schema 或描写方法。
|
本步 = 规则执行器。禁止重议对象族、生命周期、schema 或描写方法。
|
||||||
|
|
||||||
|
本步是〔先验产物〕:先定调用哪条规则,再生成 records。读【规划产物】当起点;规划可空。
|
||||||
|
|
||||||
执行顺序:
|
执行顺序:
|
||||||
1. 取出 `params.rule_id` 对应规则;非预生成生命周期或缺信息 → 停止并指出缺口。
|
1. 认定 `rule_id`:规划有则采用(用户要换则以最新为准,须仍是已验收的预生成规则);空则先从依赖产物里选出。不要停下来让编排器补参。非预生成生命周期或缺信息 → 停止并指出缺口。
|
||||||
2. 数量:`params.count` 有数字用数字,否则用规则「数量」;`batch_goal` 只作不违反规则的筛选。
|
2. 数量:规划 `count` 有数字用数字,否则用规则「数量」;`batch_goal` 只作不违反规则的筛选。
|
||||||
3. 完全按规则的「生成与描写」+「产物格式」+「池」生成 `records`(单层 JSON;键=schema 顶层字段)。
|
3. 完全按规则的「生成与描写」+「产物格式」+「池」生成 `records`(单层 JSON;键=schema 顶层字段)。
|
||||||
4. 按规则自检;不合格内部重做。已有产物则追加批次(除非用户要求替换)。
|
4. 按规则自检;不合格内部重做。已有产物则追加批次(除非用户要求替换)。
|
||||||
5. 输出 JSON;追问通常 `[]`。
|
5. 输出 JSON;追问通常 `[]`。
|
||||||
@@ -72,9 +78,9 @@ summary:`具体实例 · {对象} · {n}条`
|
|||||||
"mount": ["world-simulator"],
|
"mount": ["world-simulator"],
|
||||||
"稳变": "stable",
|
"稳变": "stable",
|
||||||
"正文": {
|
"正文": {
|
||||||
"rule_id": "与 params 一致",
|
"rule_id": "与本步认定一致(规划有则对齐)",
|
||||||
"本批数量": 3,
|
"本批数量": 3,
|
||||||
"批次条件": ["params.batch_goal 等;无则 []"],
|
"批次条件": ["规划 batch_goal 等;无则 []"],
|
||||||
"batch_id": "英文 kebab-case",
|
"batch_id": "英文 kebab-case",
|
||||||
"records": [
|
"records": [
|
||||||
{
|
{
|
||||||
@@ -111,7 +117,7 @@ summary:`具体实例 · {对象} · {n}条`
|
|||||||
## checklist
|
## checklist
|
||||||
|
|
||||||
```checklist
|
```checklist
|
||||||
- [ ] 按 params.rule_id 的预生成规则执行?
|
- [ ] 按本步认定的 rule_id(规划或步内钉)的预生成规则执行?
|
||||||
- [ ] records 单层、键值符合产物格式与全部约束?
|
- [ ] records 单层、键值符合产物格式与全部约束?
|
||||||
- [ ] 自评为合规模?未改合同?
|
- [ ] 自评为合规模?未改合同?
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
# 上下文投影排序
|
# 上下文投影排序
|
||||||
|
|
||||||
> 技能文档。程序只切割下方 **fence 块**;`##` 标题仅供人读。
|
> 技能文档。程序只切割下方 **fence 块**;`##` 标题仅供人读。
|
||||||
> **本步只排出扁平投影序,不发明执行单元,不重写设定长文。**
|
> **先抄程序钉死的执行单元名册,再给每个已启用者分配上下文与顺序。**
|
||||||
> 「对话.历史」也是可投影标签(改 projection 裁剪长度);见 `docs/context-fragment-design.md`。
|
> 不发明执行单元,不重写设定长文,不演游玩选项。
|
||||||
|
> 「对话.历史」也是可投影标签;见 `docs/context-fragment-design.md`。
|
||||||
|
|
||||||
## meta
|
## meta
|
||||||
|
|
||||||
@@ -11,36 +12,32 @@ name: 上下文投影排序
|
|||||||
id: context-order
|
id: context-order
|
||||||
artifact: 设计.上下文投影排序
|
artifact: 设计.上下文投影排序
|
||||||
declaration: >
|
declaration: >
|
||||||
根据各上游上下文的概括,为每个启用槽写出扁平投影序
|
先列出本局固定执行单元(程序名册),再为每个已启用者写出扁平投影序
|
||||||
(含对话.历史标签与 projection);不发明槽、不重写正文
|
(含对话.历史与 projection);不发明槽、不重写正文、不演情节选择题
|
||||||
when: |
|
when: |
|
||||||
游玩槽已确认(或可按默认),且主要上下文块已有可摘要产物,
|
游玩槽已确认(或可按默认),且主要上下文块已有可摘要产物,
|
||||||
需要决定插入顺序与投影级别、再收成运行规格时。
|
需要决定各执行单元看见什么、以何顺序插入时。
|
||||||
when_not: |
|
when_not: |
|
||||||
体验/站位仍混沌 → 先美学纲领与交互范式。
|
体验/站位仍混沌 → 先美学纲领与交互范式。
|
||||||
槽位未定且用户拒绝默认 → 先游玩拓扑。
|
槽位未定且用户拒绝默认 → 先游玩拓扑。
|
||||||
上游上下文仍大量空洞、无法判断稳/变 → 先补缺口,勿空排。
|
上游上下文仍大量空洞、无法判断稳/变 → 先补缺口,勿空排。
|
||||||
只需勾选槽 → 游玩拓扑;只需合并终稿 → 细化终稿。
|
只需勾选槽 → 游玩拓扑;只需合并终稿 → 细化终稿。
|
||||||
boundary: |
|
boundary: |
|
||||||
本技能:读各块概括 → 产出每槽 inserts[](order / ref / projection / 可选 note);须含对话.历史。
|
本技能:抄名册 → 凭各块 brief/挂载/稳变 给每槽 inserts[]。
|
||||||
无「历史前/后」硬分区;对话.历史本身是可排序的一项。
|
名册权威在程序(及已勾选的游玩拓扑);本步不增白名单外 id。
|
||||||
游玩拓扑:槽位权威;本步不增白名单外 ref(含按需 chance 不进每轮序)。
|
中段技能可带 mount 与稳/变;最终 order 以本步为准。
|
||||||
中段技能:可带 mount 与稳/变信号,最终 order 以本步为准。
|
|
||||||
细化终稿:把本步排序表收进运行规格 context_order;本步不输出完整 设计.worker集。
|
细化终稿:把本步排序表收进运行规格 context_order;本步不输出完整 设计.worker集。
|
||||||
|
禁止把依赖产物里的游玩示例(迎上去/推开/反客为主 等)当成当前要问的题。
|
||||||
feeds: design_only
|
feeds: design_only
|
||||||
```
|
```
|
||||||
|
|
||||||
## opening
|
## opening
|
||||||
|
|
||||||
```opening
|
```opening
|
||||||
准备排出各游玩槽的扁平投影序(只排序,不改设定正文)。
|
按已验收产物的概括,给每个固定执行单元分配上下文顺序(只排序,不改设定、不演游玩)。
|
||||||
|
|
||||||
请确认或补充:
|
名册由程序钉死:旁观维护(表格)/ 主世界层 / 叙事转述 / 可选角色视角 / 按需机遇。
|
||||||
1. 启用槽是否仍是默认?(世界模拟:主世界层 + 叙事转述;角色视角默认关)
|
若前面产物够用,直接回复「按已有概括排序」。
|
||||||
2. 「对话.历史」希望夹在哪、投影多狠?(默认 summary)
|
|
||||||
3. 有没有「几乎永不改」或「每轮必变」的块要特别强调?
|
|
||||||
|
|
||||||
若前面产物够用,可直接回复「按已有概括排序」。
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## task
|
## task
|
||||||
@@ -48,40 +45,45 @@ feeds: design_only
|
|||||||
```task
|
```task
|
||||||
你正在执行「上下文投影排序」。产物写入「设计.上下文投影排序」。
|
你正在执行「上下文投影排序」。产物写入「设计.上下文投影排序」。
|
||||||
|
|
||||||
输入:已启用槽;各上游上下文的短摘要(及 mount / 稳变信号,若有)。
|
执行顺序(必须按此,禁止颠倒):
|
||||||
输出:按槽分组的格式化排序表——Runtime/终稿据此把投影插到固定位置。
|
1. **抄名册**:以程序注入的【本局固定执行单元】为准列出 agents[](id / label / enabled / when / duty)。
|
||||||
|
启用与否跟已勾选游玩拓扑走;未勾选则按默认:旁观+主世界层+转述开,角色视角关,机遇关。
|
||||||
|
2. **再分配**:只对 enabled 的每轮槽写 inserts(order / ref / projection / 可选 note)。
|
||||||
|
凭上游 **brief、挂载、稳变** 排序,禁止把百科全文再读一遍当创作。
|
||||||
|
|
||||||
规则:
|
规则:
|
||||||
1. 禁止发明执行单元或白名单外 ref。
|
1. 禁止发明白名单外 id(含画图等未入册槽)。
|
||||||
2. 禁止重写上游长文;只写 order、ref、projection 与一句 note(勿再写硬锚点分区)。
|
2. 禁止重写上游长文;只写顺序与投影。
|
||||||
3. 每槽扁平数字序:order 0=该槽固定人设;其后按「稳→对话.历史→易变」软排。
|
3. 每轮槽:order 0=该槽固定人设;其后按「稳→对话.历史→易变」软排。
|
||||||
4. 必须包含一项 ref=`对话.历史`(默认 projection=summary);历史由程序维护,本步只决定夹在哪、投影多狠。
|
4. 主世界层与转述必须含 ref=`对话.历史`(默认 projection=summary);旁观维护默认不含历史。
|
||||||
5. 常见块名用新 artifact:设计.舞台骨架 / 设计.叙事指南与故事推进 / 设计.正文组成 / 设计.监控栏 等。
|
5. 按需槽(机遇)enabled 时 inserts 可空。
|
||||||
6. 越稳越靠前;本轮输入/真值/裁决通常在历史之后。
|
6. 越稳越靠前;本轮输入/真值/裁决通常在历史之后。
|
||||||
7. 同一内容可挂多槽;chance 为按需槽,不必排进每轮 inserts。
|
7. 同一内容可挂多槽;chance 不必排进每轮 inserts。
|
||||||
8. summary:如 `投影排序 · 主世界层 N 条 · 含历史`。
|
8. summary:如 `投影排序 · 旁观+主世界层+转述`。
|
||||||
|
|
||||||
若程序已发默认问题:禁止重复同一开场;在首答上产出表。
|
若程序已发默认问题:禁止重复同一开场;用户说「按已有概括排序」或等价同意 → **立刻产出表**,禁止再问情节内选项。
|
||||||
|
禁止 askUser 出游玩选择题(迎上去/推开她/反客为主 等)。材料已齐则顶层 askUser 为 null。
|
||||||
```
|
```
|
||||||
|
|
||||||
## principles
|
## principles
|
||||||
|
|
||||||
```principles
|
```principles
|
||||||
1. 晚排序:此时才能区分常变与重要;勿回退去发明新槽。
|
1. 名册先行:先有哪些执行单元,再谈每单元看什么。
|
||||||
2. 概括驱动:凭摘要与稳变信号排序,不把百科全文再读一遍当创作。
|
2. 晚排序:此时才能区分常变与重要;勿回退去发明新槽。
|
||||||
3. 历史是标签:用投影裁剪,不要假装「整段历史永远在中间锚点」。
|
3. 概括驱动:凭摘要与稳变信号排序,不把设定长文再当创作。
|
||||||
4. 缓存优先:少变块靠前;高频变块靠后。
|
4. 历史是标签:用投影裁剪;旁观维护默认不进对话.历史。
|
||||||
5. 投影分级:full / summary / fields / fixed——历史默认 summary。
|
5. 缓存优先:少变块靠前;高频变块靠后。
|
||||||
6. 可修订已有排序表;用户改稳变判断时只改序,不改上游产物 id。
|
6. 投影分级:full / summary / fields / fixed——历史默认 summary。
|
||||||
|
7. 可修订已有排序表;用户改稳变判断时只改序,不改上游产物 id。
|
||||||
```
|
```
|
||||||
|
|
||||||
## probe
|
## probe
|
||||||
|
|
||||||
```probe
|
```probe
|
||||||
一次 askUser 1~2 点,优先 options:
|
一次 askUser 1~2 点,且必须关于「挂哪槽 / 稳还是变 / 投影级别」:
|
||||||
- 某块稳/变判断冲突时,问用户更偏「少改利缓存」还是「每轮必须看见」。
|
- 某块稳/变判断冲突时,问更偏「少改利缓存」还是「每轮必须看见」。
|
||||||
- 角色视角若开启,问哪些块禁止进主世界层。
|
- 角色视角若开启,问哪些块禁止进主世界层。
|
||||||
已齐则直接产出,勿为凑问题而问。
|
已齐则直接产出,勿为凑问题而问。禁止问剧情里发生什么、谁敲门、玩家选哪一招。
|
||||||
```
|
```
|
||||||
|
|
||||||
## output
|
## output
|
||||||
@@ -89,29 +91,64 @@ feeds: design_only
|
|||||||
```output
|
```output
|
||||||
{
|
{
|
||||||
"schema": "context-order.v1",
|
"schema": "context-order.v1",
|
||||||
"brief": "一句话:本局扁平投影序(含对话.历史)",
|
"brief": "一句话:本局有哪些执行单元、各自上下文怎么排",
|
||||||
"play_slots": { "gm": true, "narrator": true, "perspective": false },
|
"agents": [
|
||||||
"slots": [
|
|
||||||
{
|
{
|
||||||
"ref": "world-simulator",
|
"id": "auditor",
|
||||||
"label": "主世界层",
|
"label": "旁观维护",
|
||||||
|
"enabled": true,
|
||||||
|
"when": "every_turn",
|
||||||
|
"duty": "查表/补规则,不写正文、不写真相",
|
||||||
"inserts": [
|
"inserts": [
|
||||||
{ "order": 0, "ref": "worker.persona", "projection": "fixed", "note": "槽位人设" },
|
{ "order": 0, "ref": "worker.persona", "projection": "fixed", "note": "人设" },
|
||||||
{ "order": 1, "ref": "设计.实现机制", "projection": "summary", "note": "承重规则摘要" },
|
{ "order": 1, "ref": "设计.变量设计与更新规则", "projection": "summary" },
|
||||||
{ "order": 2, "ref": "对话.历史", "projection": "summary", "note": "按投影裁剪" },
|
{ "order": 2, "ref": "设计.生成规则", "projection": "summary" },
|
||||||
{ "order": 3, "ref": "变量.当前", "projection": "fields", "note": "真值" },
|
{ "order": 3, "ref": "变量.当前", "projection": "fields" },
|
||||||
{ "order": 4, "ref": "用户.最新输入", "projection": "full", "note": "本轮" }
|
{ "order": 4, "ref": "用户.最新输入", "projection": "full" }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ref": "narrator",
|
"id": "gm",
|
||||||
"label": "叙事转述",
|
"label": "主世界层",
|
||||||
|
"enabled": true,
|
||||||
|
"when": "every_turn",
|
||||||
|
"duty": "读真值与规则,出裁决包",
|
||||||
"inserts": [
|
"inserts": [
|
||||||
{ "order": 0, "ref": "worker.persona", "projection": "fixed", "note": "转述人设" },
|
{ "order": 0, "ref": "worker.persona", "projection": "fixed" },
|
||||||
|
{ "order": 1, "ref": "设计.舞台骨架", "projection": "summary" },
|
||||||
|
{ "order": 2, "ref": "对话.历史", "projection": "summary" },
|
||||||
|
{ "order": 3, "ref": "变量.当前", "projection": "fields" },
|
||||||
|
{ "order": 4, "ref": "用户.最新输入", "projection": "full" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "narrator",
|
||||||
|
"label": "叙事转述",
|
||||||
|
"enabled": true,
|
||||||
|
"when": "every_turn",
|
||||||
|
"duty": "只把裁决写成用户可见正文",
|
||||||
|
"inserts": [
|
||||||
|
{ "order": 0, "ref": "worker.persona", "projection": "fixed" },
|
||||||
{ "order": 1, "ref": "设计.叙事指南与故事推进", "projection": "summary" },
|
{ "order": 1, "ref": "设计.叙事指南与故事推进", "projection": "summary" },
|
||||||
{ "order": 2, "ref": "对话.历史", "projection": "summary" },
|
{ "order": 2, "ref": "对话.历史", "projection": "summary" },
|
||||||
{ "order": 3, "ref": "运行.本轮.裁决", "projection": "full", "note": "settlement" }
|
{ "order": 3, "ref": "运行.本轮.裁决", "projection": "full" }
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "perspective",
|
||||||
|
"label": "角色视角",
|
||||||
|
"enabled": false,
|
||||||
|
"when": "every_turn",
|
||||||
|
"duty": "强信息隔离时出反应建议",
|
||||||
|
"inserts": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "chance",
|
||||||
|
"label": "机遇裁定",
|
||||||
|
"enabled": false,
|
||||||
|
"when": "on_demand",
|
||||||
|
"duty": "骰子/抽签;按需调用,不进每轮",
|
||||||
|
"inserts": []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -120,10 +157,12 @@ feeds: design_only
|
|||||||
## checklist
|
## checklist
|
||||||
|
|
||||||
```checklist
|
```checklist
|
||||||
|
- [ ] agents 只使用程序名册 id(auditor / gm / narrator / perspective / chance)
|
||||||
- [ ] 未发明白名单外执行单元
|
- [ ] 未发明白名单外执行单元
|
||||||
- [ ] 未重写上游长文,只有排序/投影
|
- [ ] 未重写上游长文,只有排序/投影
|
||||||
- [ ] 每启用槽有 order 0 人设位
|
- [ ] 未把游玩选项示例当成当前提问
|
||||||
- [ ] 每槽含「对话.历史」且有合理 projection
|
- [ ] 已启用的每轮槽有 order 0 人设位
|
||||||
|
- [ ] 主世界层与转述含「对话.历史」;旁观维护默认无历史
|
||||||
- [ ] 少变块整体靠前、本轮/真值靠后(软规则)
|
- [ ] 少变块整体靠前、本轮/真值靠后(软规则)
|
||||||
- [ ] 与游玩拓扑勾选一致
|
- [ ] 与游玩拓扑勾选一致
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -36,8 +36,11 @@ feeds: gm,auditor
|
|||||||
## opening
|
## opening
|
||||||
|
|
||||||
```opening
|
```opening
|
||||||
本轮生成对象已由编排参数钉死(见【本步参数】target)。请直接谈该类内容:
|
先认定本轮规则写哪一类内容:
|
||||||
|
- 若【规划产物】已有对象,以此为起点;要改对象就在本步改。
|
||||||
|
- 若规划为空,先钉对象族,再谈规则。
|
||||||
|
|
||||||
|
然后谈:
|
||||||
1. 若不做专门规则、只靠世界设定临场写,最怕出现什么不可接受的结果?
|
1. 若不做专门规则、只靠世界设定临场写,最怕出现什么不可接受的结果?
|
||||||
2. 更需要:开局先钉一批、游玩中再随机补,还是只在游玩期动态生成?
|
2. 更需要:开局先钉一批、游玩中再随机补,还是只在游玩期动态生成?
|
||||||
3. 哪些产物字段需要「池」?(池挂在 JSON 字段上,如气质标签池→`气质`;可多个;没有就说没有)
|
3. 哪些产物字段需要「池」?(池挂在 JSON 字段上,如气质标签池→`气质`;可多个;没有就说没有)
|
||||||
@@ -56,19 +59,19 @@ feeds: gm,auditor
|
|||||||
3. **池**(可选):每个池挂在产物格式里的某个字段(变量)上,供该字段抽取/组装/枚举;
|
3. **池**(可选):每个池挂在产物格式里的某个字段(变量)上,供该字段抽取/组装/枚举;
|
||||||
一条规则可多个池;无则 `池: []`。
|
一条规则可多个池;无则 `池: []`。
|
||||||
|
|
||||||
对象族已由【本步参数】`target` 钉死;禁止再问「这一步生成什么」,禁止顺手扩到其它族。
|
本步是〔先验产物〕:先定本轮对象族,再写规则。读【规划产物】当起点;规划可空。禁止顺手扩到其它族。
|
||||||
|
|
||||||
执行顺序:
|
执行顺序:
|
||||||
1. 读 params 与依赖。缺 `target` → 停止并提示补参。
|
1. 认定对象族:规划有 `target` 则采用(用户要改则以最新为准);规划为空则先钉对象。不要停下来让编排器补参。
|
||||||
2. 双门槛:重要性 + 基础上下文不足。任一项不成立 → 结论「无需专门规则」,`rules: []`。
|
2. 双门槛:重要性 + 基础上下文不足。任一项不成立 → 结论「无需专门规则」,`rules: []`。
|
||||||
3. 两项成立时,生命周期三选一(params.lifecycle_intent 合理则采用):
|
3. 两项成立时,生命周期三选一(规划里 `lifecycle_intent` 合理则采用):
|
||||||
- `runtime_only`:仅游玩期动态生成;不要求「具体实例」。
|
- `runtime_only`:仅游玩期动态生成;不要求「具体实例」。
|
||||||
- `seed_only`:由「具体实例」预生成;规则用完可丢。
|
- `seed_only`:由「具体实例」预生成;规则用完可丢。
|
||||||
- `seed_and_runtime`:先种子实例;规则与实例都保留,游玩期仍可按同规则生成。
|
- `seed_and_runtime`:先种子实例;规则与实例都保留,游玩期仍可按同规则生成。
|
||||||
4. 写满规则三块:生成与描写(可较长)+ 产物格式(**单层 JSON**:每条产物是一层键值,无嵌套对象)+ 池(需要才填;绑顶层字段)。
|
4. 写满规则三块:生成与描写(可较长)+ 产物格式(**单层 JSON**:每条产物是一层键值,无嵌套对象)+ 池(需要才填;绑顶层字段)。
|
||||||
5. 逐字段过筛:每个属性都要能回答「对本对象族是否必要」。冗余字段删掉(如女主规则里的「性别」);对象族必需字段不得漏(如 D&D 怪物的等级、攻击骰)。
|
5. 逐字段过筛:每个属性都要能回答「对本对象族是否必要」。冗余字段删掉(如女主规则里的「性别」);对象族必需字段不得漏(如 D&D 怪物的等级、攻击骰)。
|
||||||
6. `rule_id`:params 已给则用;否则稳定 kebab-case。已有产物按 rule_id 增量。
|
6. `rule_id`:规划已给则用;否则稳定 kebab-case。已有产物按 rule_id 增量。
|
||||||
若本步对象与 params 不一致(用户中途改了对象),以最新对象为准并重钉对应英文 rule_id,禁止沿用旧 id。
|
若本步对象与规划不一致,以最新对象为准并重钉对应英文 rule_id,禁止沿用旧 id。
|
||||||
7. 按自评三维打分并填追问;输出 JSON。禁止夹带最终实例 records。
|
7. 按自评三维打分并填追问;输出 JSON。禁止夹带最终实例 records。
|
||||||
|
|
||||||
若程序已发默认问题:禁止重复开场;在首答上补洞。
|
若程序已发默认问题:禁止重复开场;在首答上补洞。
|
||||||
@@ -93,7 +96,7 @@ summary:建立 → `生成规则 · {对象} · {仅动态|仅预生成|预生
|
|||||||
## probe
|
## probe
|
||||||
|
|
||||||
```probe
|
```probe
|
||||||
每轮 1~2 点。禁止重问 params 已钉的 target / rule_id。
|
每轮 1~2 点。规划已钉且用户未改对象时,禁止重问 target / rule_id。规划为空或用户要换对象时,先问本轮生成什么。
|
||||||
|
|
||||||
优先:整条规则是否必要;某字段该不该进表(冗余 vs 漏接口);单层格式类型/枚举/边界;哪些字段要池。
|
优先:整条规则是否必要;某字段该不该进表(冗余 vs 漏接口);单层格式类型/枚举/边界;哪些字段要池。
|
||||||
若双门槛不成立 → 建议跳过,停追问字段与格式。
|
若双门槛不成立 → 建议跳过,停追问字段与格式。
|
||||||
@@ -111,9 +114,9 @@ summary:建立 → `生成规则 · {对象} · {仅动态|仅预生成|预生
|
|||||||
"稳变": "stable",
|
"稳变": "stable",
|
||||||
"正文": {
|
"正文": {
|
||||||
"本步参数": {
|
"本步参数": {
|
||||||
"target": "与 params 一致",
|
"target": "与本步认定的对象一致(规划有则对齐,步内改则以最新为准)",
|
||||||
"rule_id": "已钉或本步拟定;跳过时可空字符串",
|
"rule_id": "已钉或本步拟定;跳过时可空字符串",
|
||||||
"lifecycle_intent": "params 原值或空"
|
"lifecycle_intent": "规划原值、本步钉死值或空"
|
||||||
},
|
},
|
||||||
"必要性判断": {
|
"必要性判断": {
|
||||||
"生成对象": "本轮对象族",
|
"生成对象": "本轮对象族",
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ feeds: narrator,gm
|
|||||||
美学里的感觉已有方向时,请谈「正文怎么写、故事怎么往前」(想到什么写什么):
|
美学里的感觉已有方向时,请谈「正文怎么写、故事怎么往前」(想到什么写什么):
|
||||||
|
|
||||||
1. 像什么文风?遣词什么味道?笔墨花在谁身上?有什么绝对不能写?
|
1. 像什么文风?遣词什么味道?笔墨花在谁身上?有什么绝对不能写?
|
||||||
2. 故事怎么推进?要不要跟好感/进度挂钩?有没有情境要换一套写法?
|
2. 故事往前靠什么:场面自己推、等用户决策,还是无专门推进?正文若有几种写法档要切换(官能 / 爽文 / 恐怖 / 情色这类),点名即可;没有就说没有。
|
||||||
```
|
```
|
||||||
|
|
||||||
## task
|
## task
|
||||||
@@ -45,9 +45,10 @@ mount:`narrator` 与 `world-simulator` 都挂同一份全文——不要为省
|
|||||||
|
|
||||||
执行顺序:
|
执行顺序:
|
||||||
1. 继承美学体验与禁忌,改写成做法向句子。
|
1. 继承美学体验与禁忌,改写成做法向句子。
|
||||||
2. 写纲领、遣词(含示范)、笔墨焦点、禁忌/不偏好;需要时写情境备用。
|
2. 写纲领、遣词(含示范)、笔墨焦点、禁忌/不偏好。
|
||||||
3. 写推进与决策(可写「无专门推进」);变量只写钩子。
|
3. 情境备用 = 写法档(体裁/调性容器),不是具体桥段。有切换则每条写:名字=档名(如「官能小说」「网文爽文」「展示恐怖」「展示情色」),用法=该档怎么遣词、笔墨落哪、何时切进来。无切换则 `[]`。用户若报出「当众折辱」「独处一室」这类场合,升一档写进对应写法档,场合本身不单列。
|
||||||
4. 自评 + 追问。
|
4. 写推进与决策(可写「无专门推进」);变量只写钩子。
|
||||||
|
5. 自评 + 追问。
|
||||||
|
|
||||||
summary:`叙事指南与故事推进 · …`
|
summary:`叙事指南与故事推进 · …`
|
||||||
```
|
```
|
||||||
@@ -58,14 +59,21 @@ summary:`叙事指南与故事推进 · …`
|
|||||||
1. 感觉 vs 做法:美学写要什么爽;本步写怎么遣词、笔墨落哪、故事怎么推。
|
1. 感觉 vs 做法:美学写要什么爽;本步写怎么遣词、笔墨落哪、故事怎么推。
|
||||||
2. 遣词与禁忌要可执行;绝对禁忌 ≠ 不偏好。
|
2. 遣词与禁忌要可执行;绝对禁忌 ≠ 不偏好。
|
||||||
3. 推进可轻可无;表规格交给变量/生成规则。
|
3. 推进可轻可无;表规格交给变量/生成规则。
|
||||||
4. 整份双挂即可;禁止拆成两个技能。
|
4. 情境备用是写法档:名字用「官能小说」「网文爽文」「展示恐怖」「展示情色」同级;用法写该档遣词与何时切入。无切换则空数组。
|
||||||
|
5. 整份双挂即可;禁止拆成两个技能。
|
||||||
```
|
```
|
||||||
|
|
||||||
## probe
|
## probe
|
||||||
|
|
||||||
```probe
|
```probe
|
||||||
每轮 1~2 点。已在美学写清的感觉勿重问。
|
本步只钉四件:遣词可执行、笔墨落点、绝对禁忌、推进方式(场面推进 / 用户决策 / 无专门推进)。
|
||||||
优先:体裁遣词、笔墨详谁、禁忌、推进门控。
|
|
||||||
|
已在美学写清的感觉改写成做法向句子,不重问「想感受到什么」。
|
||||||
|
|
||||||
|
缺其中一件:在产物「追问」写 1~2 点(建议选项 + 短句示例)。四件够验收:`追问.题目` 为 `[]`。推进方式不明则正文写「无专门推进」,不问好感公式、进度表或事件池。
|
||||||
|
|
||||||
|
用户谈到变量增减、生成规则、舞台百科:只抽取能改遣词 / 禁忌 / 推进方式的一句,其余写入「开放问题」。
|
||||||
|
用户提到要换写法:收成写法档写入「情境备用」(官能 / 爽文 / 恐怖 / 情色这类),不追问具体场合怎么摆;没提则 `[]`。
|
||||||
```
|
```
|
||||||
|
|
||||||
## output
|
## output
|
||||||
@@ -99,7 +107,16 @@ summary:`叙事指南与故事推进 · …`
|
|||||||
"绝对禁忌": ["…"],
|
"绝对禁忌": ["…"],
|
||||||
"不偏好": ["…"]
|
"不偏好": ["…"]
|
||||||
},
|
},
|
||||||
"情境备用": [],
|
"情境备用": [
|
||||||
|
{
|
||||||
|
"名字": "展示情色",
|
||||||
|
"用法": "切到身体与权力交换时:感官词加重、节奏放慢、对话变短;切回日常则收。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"名字": "网文爽文",
|
||||||
|
"用法": "打脸/反转时:可复述对方原话、结果写满、过程可略。"
|
||||||
|
}
|
||||||
|
],
|
||||||
"推进与决策": {
|
"推进与决策": {
|
||||||
"有无专门推进": "有|无",
|
"有无专门推进": "有|无",
|
||||||
"驱动力来源": "…",
|
"驱动力来源": "…",
|
||||||
@@ -126,18 +143,20 @@ summary:`叙事指南与故事推进 · …`
|
|||||||
```
|
```
|
||||||
|
|
||||||
硬规则:合法 JSON;示范至少 1 段;禁忌与不偏好都要有;双挂;禁止 order;summary 见 task。
|
硬规则:合法 JSON;示范至少 1 段;禁忌与不偏好都要有;双挂;禁止 order;summary 见 task。
|
||||||
|
情境备用若有:名字必须是写法档(官能小说 / 网文爽文 / 展示恐怖 / 展示情色 同级),用法写该档的遣词与切入口;不是「当众折辱」「物资告急」这类具体桥段。无切换则 `[]`。
|
||||||
|
|
||||||
## checklist
|
## checklist
|
||||||
|
|
||||||
```checklist
|
```checklist
|
||||||
- [ ] 一份产物同时服务转述与主世界层?
|
- [ ] 一份产物同时服务转述与主世界层?
|
||||||
- [ ] 遣词+示范+禁忌够用?推进清楚或明确无?
|
- [ ] 遣词+示范+禁忌够用?推进清楚或明确无?
|
||||||
|
- [ ] 情境备用若有,是否是写法档(官能/爽文/恐怖/情色同级)而非具体桥段?无切换是否为 `[]`?
|
||||||
- [ ] 未为省 token 拆两套投影?
|
- [ ] 未为省 token 拆两套投影?
|
||||||
```
|
```
|
||||||
|
|
||||||
## examples
|
## examples
|
||||||
|
|
||||||
```examples
|
```examples
|
||||||
好:一次问清爽文遣词 + 好感门控倒追,整份挂两边。
|
好:一次钉清遣词 + 笔墨落点 + 禁忌;推进写「等用户决策」或「无专门推进」;情境备用是「展示情色」「网文爽文」两档及切换时笔墨。
|
||||||
坏:拆成「文风技能」和「推进技能」两步;或为省 token 给两边裁成两套残缺稿。
|
坏:拆成「文风技能」和「推进技能」两步;追问好感公式 / 事件池;或把「当众折辱」「独处一室」当成情境备用。
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
> 技能文档。程序只切割下方 **fence 块**;`##` 标题仅供人读。
|
> 技能文档。程序只切割下方 **fence 块**;`##` 标题仅供人读。
|
||||||
> 产物外壳:`docs/context-fragment-design.md`。
|
> 产物外壳:`docs/context-fragment-design.md`。
|
||||||
> 创作偏晚:规格与呈现契约已齐,写「迈进世界的第一拍」并钉开场真值。
|
> **创作终节点**:规格与呈现契约已齐。写出 1~多条开场白供选定;选定后程序落库、关闭工作流、保存定稿。
|
||||||
> 磁盘 worker `opening-generator` 可执行/落库本产物到 `输出.开场白` 与 `运行.初始变量`。
|
> 不必再调磁盘 worker `opening-generator`(无本步时它仍可作兜底落库)。
|
||||||
|
|
||||||
## meta
|
## meta
|
||||||
|
|
||||||
@@ -12,20 +12,21 @@ name: 开场白与开场变量
|
|||||||
id: opening-setup
|
id: opening-setup
|
||||||
artifact: 设计.开场白与开场变量
|
artifact: 设计.开场白与开场变量
|
||||||
declaration: >
|
declaration: >
|
||||||
写出遵循正文组成/叙事/监控栏等契约的开场一小段,并钉与之同真相的开场变量初值;
|
创作终节点:写出 1~多条遵循正文组成/叙事/监控栏契约的开场白,并钉与之同真相的开场变量初值;
|
||||||
开场为主、填表为辅
|
选定后程序落库、结束创作并保存定稿
|
||||||
when: |
|
when: |
|
||||||
体验、舞台、叙事、变量(若有)、正文组成等已大致可引用,
|
体验、舞台、叙事、变量(若有)、正文组成、细化终稿等已大致可引用,
|
||||||
需要可开玩的第一段剧情与初始真值快照时;通常在细化终稿前后、进游玩前。
|
需要可开玩的第一段剧情与初始真值快照时;排在收成链之后,作为最后一步。
|
||||||
when_not: |
|
when_not: |
|
||||||
体验/呈现仍混沌 → 先美学与正文组成等。
|
体验/呈现仍混沌 → 先美学与正文组成等。
|
||||||
只要改文风不要开场 → 叙事指南。
|
只要改文风不要开场 → 叙事指南。
|
||||||
只要改真值规则不要开场文 → 变量设计。
|
只要改真值规则不要开场文 → 变量设计。
|
||||||
|
尚未细化终稿、Worker 集未齐 → 先收成。
|
||||||
boundary: |
|
boundary: |
|
||||||
本技能:开场正文(按正文组成块序拼出用户可见第一屏)+ 开场变量初值 + 可选监控栏初值示意。
|
本技能是创作终节点:1~多条开场正文(按正文组成块序)+ 开场变量初值 + 可选监控栏初值示意。
|
||||||
必须遵守已验收的叙事指南、正文组成、监控栏、变量设计;不重做这些契约。
|
必须遵守已验收的叙事指南、正文组成、监控栏、变量设计;不重做这些契约。
|
||||||
具体实例:开场可引用已有实例名片,不在本步新造百科。
|
具体实例:开场可引用已有实例名片,不在本步新造百科。
|
||||||
opening-generator:可将本产物写入 输出.开场白 / 运行.初始变量 / 变量.当前。
|
选定后由程序写入 输出.开场白 / 运行.初始变量 / 变量.当前,关闭 DAG 并保存定稿;不要在本步之后再排其它能力。
|
||||||
feeds: narrator
|
feeds: narrator
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -42,19 +43,20 @@ feeds: narrator
|
|||||||
## task
|
## task
|
||||||
|
|
||||||
```task
|
```task
|
||||||
你正在执行「开场白与开场变量」。产物必须是 **context-fragment.v1** JSON,写入「设计.开场白与开场变量」。
|
你正在执行「开场白与开场变量」。这是**创作最后一步**。产物必须是 **context-fragment.v1** JSON,写入「设计.开场白与开场变量」。
|
||||||
|
|
||||||
主产物是**开场一小段剧情**(用户第一屏),不是填表工具。
|
主产物是 **1~多条开场剧情**(用户第一屏),不是填表工具。用户将选定其中一条;选定后程序结束创作并保存。
|
||||||
必须遵循:叙事指南与故事推进、正文组成(可见块序、隐藏段若开场就要写)、监控栏字段(若启用)、变量真值名单。
|
必须遵循:叙事指南与故事推进、正文组成(可见块序、隐藏段若开场就要写)、监控栏字段(若启用)、变量真值名单。
|
||||||
|
|
||||||
执行顺序:
|
执行顺序:
|
||||||
1. 读依赖:worker 集/美学/叙事/正文组成/监控栏/变量设计/舞台与实例。
|
1. 读依赖:worker 集/美学/叙事/正文组成/监控栏/变量设计/舞台与实例。
|
||||||
2. 按正文组成的可见块写出开场各块内容(至少正文块);隐藏段仅当开场即需声明初值变更时使用(通常初值走「开场变量」字段,隐藏段可空)。
|
2. 写出 **1~3 条**可开玩的开场白候选(角度/第一拍可不同,设定必须同一真相)。推荐那条写入 `开场白全文`,全部写入 `开场白候选`。
|
||||||
3. 钉开场变量:与开场事实同一真相;能从设定推断的填上;冲突则以开场叙述为准并改表。
|
3. 按正文组成的可见块写出开场各块内容(至少正文块);隐藏段仅当开场即需声明初值变更时使用(通常初值走「开场变量」字段,隐藏段可空)。
|
||||||
4. 若有监控栏:给出开场时监控栏展示快照(短)。
|
4. 钉开场变量:与开场事实同一真相;能从设定推断的填上;冲突则以开场叙述为准并改表。
|
||||||
5. 自评 + 追问;输出 JSON。
|
5. 若有监控栏:给出开场时监控栏展示快照(短)。
|
||||||
|
6. 自评 + 追问;输出 JSON。
|
||||||
|
|
||||||
禁止:先问卷填表再糊开场;禁止开场与初值打架;禁止重写 Worker 集。
|
禁止:先问卷填表再糊开场;禁止开场与初值打架;禁止重写 Worker 集;禁止只交空壳等下一节点。
|
||||||
|
|
||||||
summary:`开场白与开场变量 · …`(点题场景)。
|
summary:`开场白与开场变量 · …`(点题场景)。
|
||||||
```
|
```
|
||||||
@@ -95,7 +97,12 @@ summary:`开场白与开场变量 · …`(点题场景)。
|
|||||||
"内容": "该块开场文本;可空块写空字符串并说明为何首轮空"
|
"内容": "该块开场文本;可空块写空字符串并说明为何首轮空"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"开场白全文": "按块序拼好的用户可见开场(主阅读件)",
|
"开场白全文": "推荐那条:按块序拼好的用户可见开场(主阅读件)",
|
||||||
|
"开场白候选": [
|
||||||
|
"推荐那条(与开场白全文相同)",
|
||||||
|
"可选第二条:同一真相、不同第一拍/角度",
|
||||||
|
"可选第三条"
|
||||||
|
],
|
||||||
"开场变量": [
|
"开场变量": [
|
||||||
{
|
{
|
||||||
"名": "与变量设计真值名一致",
|
"名": "与变量设计真值名一致",
|
||||||
@@ -153,13 +160,14 @@ summary:`开场白与开场变量 · …`(点题场景)。
|
|||||||
|
|
||||||
硬规则:
|
硬规则:
|
||||||
1. context-fragment.v1;自评契约符合/同真相/可开玩。
|
1. context-fragment.v1;自评契约符合/同真相/可开玩。
|
||||||
2. `开场白全文` 与 `开场变量` 必填;监控栏快照无监控栏则为 `[]`。
|
2. `开场白全文` 与 `开场变量` 必填;`开场白候选` 1~3 条(含推荐);监控栏快照无监控栏则为 `[]`。
|
||||||
3. 不输出完整 worker 集;不发明新真值名(须来自变量设计,若无变量设计则可极少数字段并标明临时)。
|
3. 不输出完整 worker 集;不发明新真值名(须来自变量设计,若无变量设计则可极少数字段并标明临时)。
|
||||||
4. summary:`开场白与开场变量 · {brief 缩略}`。
|
4. summary:`开场白与开场变量 · {brief 缩略}`。
|
||||||
|
|
||||||
## checklist
|
## checklist
|
||||||
|
|
||||||
```checklist
|
```checklist
|
||||||
|
- [ ] 写出 1~3 条开场候选,推荐条与 `开场白全文` 一致?
|
||||||
- [ ] 开场遵循正文组成块序与叙事口径?
|
- [ ] 开场遵循正文组成块序与叙事口径?
|
||||||
- [ ] 变量与开场同真相?可行动空间清楚?
|
- [ ] 变量与开场同真相?可行动空间清楚?
|
||||||
- [ ] 未剧透隐藏档?未写成第一章全文?
|
- [ ] 未剧透隐藏档?未写成第一章全文?
|
||||||
|
|||||||
@@ -45,7 +45,8 @@ feeds: narrator
|
|||||||
|
|
||||||
1. 要微调什么?(监控字段、块称呼、要不要建议行动、tone_chrome…)
|
1. 要微调什么?(监控字段、块称呼、要不要建议行动、tone_chrome…)
|
||||||
2. 有没有隐藏段给模型/程序维护变量?
|
2. 有没有隐藏段给模型/程序维护变量?
|
||||||
3. 不要谈「再发明一种全新布局」。
|
3. 定稿时会用前面情节架空一幕灌进壳里,专供你看版式/美化(内容随便,不当正史)。
|
||||||
|
4. 不要谈「再发明一种全新布局」。
|
||||||
```
|
```
|
||||||
|
|
||||||
## task
|
## task
|
||||||
@@ -64,7 +65,8 @@ feeds: narrator
|
|||||||
4. 监控栏:字段意图(细则可交「设计监控栏」);无 monitor 的壳写「不启用」。
|
4. 监控栏:字段意图(细则可交「设计监控栏」);无 monitor 的壳写「不启用」。
|
||||||
5. 隐藏段(若有):定界与用途。
|
5. 隐藏段(若有):定界与用途。
|
||||||
6. 填「呈现壳.微调」;前端拆分区域必须与壳一致。
|
6. 填「呈现壳.微调」;前端拆分区域必须与壳一致。
|
||||||
7. 自评 + 追问;禁止写 context `order`。
|
7. **示例**:用前面已定情节架空一幕,按本步壳与可见块灌进 `present.v1` 形状,专供验收版式/美化;示例文可短,游玩正文字数不在本步打磨。
|
||||||
|
8. 自评 + 追问;禁止写 context `order`。
|
||||||
|
|
||||||
summary:`正文组成 · 适配 {shell_id} · …`。
|
summary:`正文组成 · 适配 {shell_id} · …`。
|
||||||
```
|
```
|
||||||
@@ -73,7 +75,8 @@ summary:`正文组成 · 适配 {shell_id} · …`。
|
|||||||
|
|
||||||
```principles
|
```principles
|
||||||
1. 创作 = 选壳 + 适配微调
|
1. 创作 = 选壳 + 适配微调
|
||||||
不是 UI 创意;布局骨架只读。
|
不是 UI 创意;布局骨架只读。本步验收重点是「看起来对不对」,不是故事写得好不好。
|
||||||
|
「按意见修改」:以最新产物为底;用户要求按先后都有效,冲突时以较新的为准。用户说整体可以、或只提美化/行距/显示名 → 不换 shell_id。换壳必须用户点名。
|
||||||
|
|
||||||
2. 只改微调轴
|
2. 只改微调轴
|
||||||
字段、显示名、开关、tone_chrome、空态;不改分区、不增区域、不自造壳。
|
字段、显示名、开关、tone_chrome、空态;不改分区、不增区域、不自造壳。
|
||||||
@@ -85,6 +88,11 @@ summary:`正文组成 · 适配 {shell_id} · …`。
|
|||||||
|
|
||||||
5. 与转述、变量分工
|
5. 与转述、变量分工
|
||||||
文风归叙事指南;真值归变量设计;本步只交壳适配契约。
|
文风归叙事指南;真值归变量设计;本步只交壳适配契约。
|
||||||
|
游玩单轮用户主读正文默认 500~2000 字,写进「拼装与终稿.单轮正文篇幅」,供转述遵守。
|
||||||
|
|
||||||
|
6. 示例 = 壳与美化预览,非正史
|
||||||
|
用已定材料随便架空一幕,把各区域/监控/行动坞撑满,让用户看见版式。
|
||||||
|
禁止把示例当成开场白或游玩 canon;禁止为写好看而新造设定;文笔及格即可,勿打磨成章。
|
||||||
```
|
```
|
||||||
|
|
||||||
## probe
|
## probe
|
||||||
@@ -92,8 +100,8 @@ summary:`正文组成 · 适配 {shell_id} · …`。
|
|||||||
```probe
|
```probe
|
||||||
每轮 1~2 点。
|
每轮 1~2 点。
|
||||||
|
|
||||||
优先:选哪个壳;微调哪些字段/显示名;隐藏段要不要。
|
优先:选哪个壳;微调哪些字段/显示名;隐藏段要不要;示例灌数是否让版式看得清。
|
||||||
不追问:新布局、完整 CSS、事件池 schema、文风遣词。
|
不追问:新布局、完整 CSS、事件池 schema、文风遣词、示例文笔好坏。
|
||||||
```
|
```
|
||||||
|
|
||||||
## output
|
## output
|
||||||
@@ -130,7 +138,7 @@ summary:`正文组成 · 适配 {shell_id} · …`。
|
|||||||
"可空": true,
|
"可空": true,
|
||||||
"填充方": "叙事转述|程序拼装|监控栏|其它",
|
"填充方": "叙事转述|程序拼装|监控栏|其它",
|
||||||
"内容形态": "散文|短列表|键值行|其它",
|
"内容形态": "散文|短列表|键值行|其它",
|
||||||
"示例": "可选一句/三行示意"
|
"示例": "可选:该块一句占位(总预览见正文.示例)"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"监控栏锚点": {
|
"监控栏锚点": {
|
||||||
@@ -172,7 +180,27 @@ summary:`正文组成 · 适配 {shell_id} · …`。
|
|||||||
"终稿tag": "通常 输出.用户展示",
|
"终稿tag": "通常 输出.用户展示",
|
||||||
"拼装方": "叙事转述一次写出各块|程序按块拼接|混合",
|
"拼装方": "叙事转述一次写出各块|程序按块拼接|混合",
|
||||||
"推荐灌数": "分块壳优先 present.v1;prose 可为纯 Markdown",
|
"推荐灌数": "分块壳优先 present.v1;prose 可为纯 Markdown",
|
||||||
|
"单轮正文篇幅": "默认 500~2000 字(汉字,写入 body);短于 500 仅当用户明确要求极短",
|
||||||
"与裁决包关系": "转述读 settlement 再填各块;隐藏段可含 variable_changes 镜像"
|
"与裁决包关系": "转述读 settlement 再填各块;隐藏段可含 variable_changes 镜像"
|
||||||
|
},
|
||||||
|
"示例": {
|
||||||
|
"性质": "验收预览|非正史|仅看壳与美化",
|
||||||
|
"依据": ["前面已定情节/设定要点,一两句即可"],
|
||||||
|
"用来说明": "这幕只为撑开版式(分区、监控、行动坞、显示名…),内容可糙",
|
||||||
|
"灌数": {
|
||||||
|
"schema": "present.v1",
|
||||||
|
"shell": "与正文.呈现壳.shell_id 一致",
|
||||||
|
"blocks": {
|
||||||
|
"monitor": "若该壳有 monitor:短键值;否则省略",
|
||||||
|
"header": "若有则短字;无则省略",
|
||||||
|
"body": "架空主读文(短段即可,勿写成章)",
|
||||||
|
"aside": "若有则短线索/备注;无则省略",
|
||||||
|
"footer": "若有则可空或一句"
|
||||||
|
},
|
||||||
|
"meta": {
|
||||||
|
"suggested_actions": ["若该壳开建议行动则给 2~3 个占位选项;否则 []"]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"自评": {
|
"自评": {
|
||||||
@@ -191,6 +219,11 @@ summary:`正文组成 · 适配 {shell_id} · …`。
|
|||||||
"名": "可实现",
|
"名": "可实现",
|
||||||
"分数": 8,
|
"分数": 8,
|
||||||
"说明": "shell_id 是否在七种壳内;分区与隐藏段是否可交给转述/程序"
|
"说明": "shell_id 是否在七种壳内;分区与隐藏段是否可交给转述/程序"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"名": "版式可感",
|
||||||
|
"分数": 8,
|
||||||
|
"说明": "示例灌数是否按壳撑开各区,使人一眼能验收美化(不论文笔)"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"薄弱点": "一句话"
|
"薄弱点": "一句话"
|
||||||
@@ -213,10 +246,12 @@ summary:`正文组成 · 适配 {shell_id} · …`。
|
|||||||
1. 合法 JSON;含公共三段外壳。
|
1. 合法 JSON;含公共三段外壳。
|
||||||
2. 正文须含 `呈现壳.shell_id`(七种之一)与 `微调`;可见块只挂壳已有区域。
|
2. 正文须含 `呈现壳.shell_id`(七种之一)与 `微调`;可见块只挂壳已有区域。
|
||||||
3. `可见块` 至少 1 条(通常含 body);禁止自造区域 id / 第五种壳。
|
3. `可见块` 至少 1 条(通常含 body);禁止自造区域 id / 第五种壳。
|
||||||
4. 禁止程序 API schema、context `order`、完整 CSS。
|
4. 正文须含 `示例`:`性质` 标明非正史;`灌数` 为 `present.v1`,`shell` 与本步 shell_id 一致;只填该壳已有区域。
|
||||||
5. 自评:可见完备 / 监控克制 / 可实现(=壳适配是否可执行)。
|
5. 示例内容可糙:只为验收壳与美化;禁止当开场白/正史;禁止为示例新造设定。
|
||||||
6. summary:`正文组成 · 适配 {shell_id} · {brief 缩略}`。
|
6. 禁止程序 API schema、context `order`、完整 CSS。
|
||||||
7. 兼容旧称产物 tag「设计.回复格式」。
|
7. 自评:可见完备 / 监控克制 / 可实现 / 版式可感。
|
||||||
|
8. summary:`正文组成 · 适配 {shell_id} · {brief 缩略}`。
|
||||||
|
9. 兼容旧称产物 tag「设计.回复格式」。
|
||||||
|
|
||||||
## checklist
|
## checklist
|
||||||
|
|
||||||
@@ -225,18 +260,22 @@ summary:`正文组成 · 适配 {shell_id} · …`。
|
|||||||
- [ ] shell_id 在七种壳内?可见块未超出该壳分区?
|
- [ ] shell_id 在七种壳内?可见块未超出该壳分区?
|
||||||
- [ ] 微调轴已填或明确默认?监控栏未塞死名片字段?
|
- [ ] 微调轴已填或明确默认?监控栏未塞死名片字段?
|
||||||
- [ ] 隐藏段(若有)定界清楚?未写 CSS 工程?
|
- [ ] 隐藏段(若有)定界清楚?未写 CSS 工程?
|
||||||
|
- [ ] 有示例灌数(present.v1),壳一致、各区撑开,且标明非正史?
|
||||||
|
- [ ] 未把示例写成开场白或打磨成长文?
|
||||||
```
|
```
|
||||||
|
|
||||||
## examples
|
## examples
|
||||||
|
|
||||||
```examples
|
```examples
|
||||||
好:
|
好:
|
||||||
- 适配 chat_monitor:微调监控字段=好感/今日话题;body 显示名=「短信」。
|
- 适配 chat_monitor:微调监控字段=好感/今日话题;body 显示名=「短信」;示例灌数把气泡区+监控撑开,文笔随便。
|
||||||
- 适配 chapter_reader:只开 header+body;aside 关。
|
- 适配 chapter_reader:只开 header+body;aside 关;示例给短章头+一段 body。
|
||||||
- 适配 turn_panel:开 suggested_actions;监控只盯体力/好感。
|
- 适配 turn_panel:开 suggested_actions;监控只盯体力/好感;示例 meta 带 2~3 个占位行动。
|
||||||
|
|
||||||
坏:
|
坏:
|
||||||
- 「我们自定义一种左右分栏新界面」。
|
- 「我们自定义一种左右分栏新界面」。
|
||||||
- 不写 shell_id,只写「要好看的 UI」。
|
- 不写 shell_id,只写「要好看的 UI」。
|
||||||
- 监控栏列出姓名、性别等死设定。
|
- 监控栏列出姓名、性别等死设定。
|
||||||
|
- 示例写成第一章正史,或为写好看新造未定设定。
|
||||||
|
- 示例 shell 与呈现壳不一致,或灌了壳没有的区域。
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ process:
|
|||||||
- 需要门控/分档/程序事件:先「变量设计与更新规则」,再「变量控制上下文」。变量与 Progressive 不是执行单元。
|
- 需要门控/分档/程序事件:先「变量设计与更新规则」,再「变量控制上下文」。变量与 Progressive 不是执行单元。
|
||||||
- 挂载归属(非顺序):世界/机制/生成 → 主世界层;叙事指南与故事推进 → 叙事转述(推进兼挂主世界层);美学进常驻。勿为世界观或性格表另开槽。拓扑图谱在固定槽已够用时通常跳过。
|
- 挂载归属(非顺序):世界/机制/生成 → 主世界层;叙事指南与故事推进 → 叙事转述(推进兼挂主世界层);美学进常驻。勿为世界观或性格表另开槽。拓扑图谱在固定槽已够用时通常跳过。
|
||||||
- 需要把美学「感觉」落成遣词/笔墨焦点/禁忌与推进时用「叙事指南与故事推进」;事件表 schema 仍用生成规则,真值门控用变量设计。
|
- 需要把美学「感觉」落成遣词/笔墨焦点/禁忌与推进时用「叙事指南与故事推进」;事件表 schema 仍用生成规则,真值门控用变量设计。
|
||||||
- 收成链(顺序固定):上游上下文大致齐 →「上下文投影排序」→「细化终稿」→ status: closed。禁止在排序步发明 ref 或重写长文。
|
- 收成链(顺序固定):上游上下文大致齐 →「上下文投影排序」→「细化终稿」→ 需要开场则排「开场白与开场变量」(〔收口〕:选定后结束创作并保存)。
|
||||||
- 进游玩前可排「开场白与开场变量」(遵循正文组成/叙事/变量);再由 opening-generator 落库到输出.开场白与运行.初始变量。
|
- 「开场白与开场变量」写出 1~多条开场白;选定后程序落库到输出.开场白与运行.初始变量,不必再调 opening-generator。
|
||||||
- 变量:真值 + Data 映射索引(实例表在具体实例)+ 维护语句(裁决包/隐藏段,非 chance)+ 旁观汇总挂载(变量控制上下文)。
|
- 变量:真值 + Data 映射索引(实例表在具体实例)+ 维护语句(裁决包/隐藏段,非 chance)+ 旁观汇总挂载(变量控制上下文)。
|
||||||
|
|
||||||
principles:
|
principles:
|
||||||
|
|||||||
@@ -39,5 +39,7 @@ prompt_excerpt: |
|
|||||||
"meta": { "suggested_actions": ["…"] }
|
"meta": { "suggested_actions": ["…"] }
|
||||||
}
|
}
|
||||||
monitor 用短键值(芯片),勿写长文。只填该壳已有区域。
|
monitor 用短键值(芯片),勿写长文。只填该壳已有区域。
|
||||||
|
用户主读正文(body)默认 500~2000 字(汉字):写成可连读的场面/对话推进,不要几句短信就结束。
|
||||||
|
禁止在正文里写出 tag 名(「输出.用户展示」)、压缩摘要或过程日志。
|
||||||
禁止写出 do_not_say;禁止编造包中没有的事实。
|
禁止写出 do_not_say;禁止编造包中没有的事实。
|
||||||
若裁决含 tone_hint / npc_moves / suggested_actions,须体现在 body 或 meta。
|
若裁决含 tone_hint / npc_moves / suggested_actions,须体现在 body 或 meta。
|
||||||
|
|||||||
@@ -34,19 +34,19 @@ contextSegments:
|
|||||||
label: "## 【用户已选配方】只读引用"
|
label: "## 【用户已选配方】只读引用"
|
||||||
- id: user-demand
|
- id: user-demand
|
||||||
tier: dynamic
|
tier: dynamic
|
||||||
tags: ["用户.需求", "book.brief", "用户.最新输入", "用户.worker答复", "用户.修订说明"]
|
tags: ["用户.需求", "book.brief", "用户.worker答复"]
|
||||||
label: "## 用户表述"
|
label: "## 用户表述"
|
||||||
---
|
---
|
||||||
|
|
||||||
# 创作 · 流程编排
|
# 创作 · 流程编排
|
||||||
|
|
||||||
你只做一件事:根据用户表述,编排或**增量修订**一份剧本流程(可变 DAG)。
|
你只做一件事:根据【用户要求】编排或**增量修订**一份剧本流程(可变 DAG)。要求按先后都有效,冲突时以较新的为准。
|
||||||
|
|
||||||
上下文里会有:
|
上下文里会有:
|
||||||
|
|
||||||
1. **【流程进度】**(程序钉死):已完成哪些步、草案里已有哪些步、哪些能力才允许再编入。必须先读完再排。
|
1. **【流程进度】**(程序钉死):已完成哪些步、草案里已有哪些步、哪些能力才允许再编入。必须先读完再排。
|
||||||
2. **【用户已选配方】**:该方法的适用、核心思路、设计流程与原则 + 近期起点 steps——**不是**锁死流水线;**禁止**替用户改选其它配方
|
2. **【用户已选配方】**:该方法的适用、核心思路、设计流程与原则 + 近期起点 steps——**不是**锁死流水线;**禁止**替用户改选其它配方
|
||||||
3. **【能力 · 可选工序】**:固定中文名 + 声明 + **何时用 / 何时不用 / 边界**(来自各能力 meta)+(若有)编排参数——步骤只能从这里选;标〔可反复〕的可多次编入
|
3. **【能力 · 可选工序】**:固定中文名 + 声明 + **何时用 / 何时不用 / 边界**(来自各能力 meta)+(若有)编排参数——步骤只能从这里选;标〔可反复〕的可多次**新建**编入;已完成节点要改用回头修改(mode=revise)
|
||||||
4. **【已有剧本草案】/【已验收步骤】**:若有,在其上追加或改未验收步,**不要**推倒重来
|
4. **【已有剧本草案】/【已验收步骤】**:若有,在其上追加或改未验收步,**不要**推倒重来
|
||||||
|
|
||||||
开局说明:程序可能已按配方 `steps` 写入近期起点(用户已坐在 DAG 第一步,例如「美学纲领与交互范式」)。此时**不要**把开局步再规划一遍;仅追加缺口。listed 步已齐仍需扩写时,保留已完成 id,只加新步。
|
开局说明:程序可能已按配方 `steps` 写入近期起点(用户已坐在 DAG 第一步,例如「美学纲领与交互范式」)。此时**不要**把开局步再规划一遍;仅追加缺口。listed 步已齐仍需扩写时,保留已完成 id,只加新步。
|
||||||
@@ -59,11 +59,14 @@ contextSegments:
|
|||||||
|
|
||||||
- **跟配方方法论**:用配方的 core / process / principles 理解整局怎么设计、何时收成。
|
- **跟配方方法论**:用配方的 core / process / principles 理解整局怎么设计、何时收成。
|
||||||
- **跟能力 meta**:某步该不该排,看能力的「何时用 / 何时不用 / 边界」;**不要**在配方里找各能力调用条件,也不要凭题材默认全选。
|
- **跟能力 meta**:某步该不该排,看能力的「何时用 / 何时不用 / 边界」;**不要**在配方里找各能力调用条件,也不要凭题材默认全选。
|
||||||
- 有「编排参数」声明的能力:写入 DAG 时可先留空壳;**确认开干前**必须钉齐必填 params(用户在「下一步」意向后、点同意开始时补齐)。
|
- 〔先验产物〕(生成规则、具体实例):可提前规划「写什么」写入 params;执行时程序注入【规划产物】。规划可空——对象在步内钉。提前规划多条(不同对象/批次)是正确的。**禁止**为钉「生成什么 / 调用哪个规则」而 askUser 拦在确认开干前。
|
||||||
- 参数不明时:可先产出带空 params 的步骤,或 askUser;禁止把「生成什么 / 调用哪个规则」拖到 design-step 执行中途。
|
- 其它有「编排参数」的能力:写入 DAG 时可先留空壳;**确认开干前**必须钉齐必填 params。
|
||||||
- 用户先认可**下一节点**(含参数),再进入执行;用户提出新要求时,再调本 worker 修订未验收步或追加新步。
|
- 用户先认可**下一节点**,再进入执行;用户提出新要求时,再调本 worker 修订未验收步或追加新步。
|
||||||
|
|
||||||
同能力**可以**多次出现(尤其〔可反复〕):每次一次调用、一次验收、产物写入同一 artifact(增量补全)。id 应尽量带上调用目标,如 `生成规则·怪物`,避免无信息的 `#2`。
|
同能力**可以**多次出现。必须分清两种:
|
||||||
|
|
||||||
|
- **彻底新建**(〔可反复〕如生成规则、具体实例):`mode` 缺省或 `"fresh"`,新 id + 新 params。每一次都是新对象/新批次,**不要**改旧条。id 应尽量带上调用目标,如 `生成规则·怪物`,避免无信息的 `#2`。
|
||||||
|
- **回头修改**:用户要改**已经做过**的节点(漏了、谈浅了、后来发现要改)→ 追加新 id,`"mode": "revise"`,`"revises": "<原步 id>"`。程序会注入既有产物,在旧稿上改。非反复技能**禁止**再排一条 fresh;只能 revise。插在最近已验收步之后、尚未执行的步之前。
|
||||||
|
|
||||||
## 产出(唯一)
|
## 产出(唯一)
|
||||||
|
|
||||||
@@ -94,6 +97,13 @@ contextSegments:
|
|||||||
"count": "按规则"
|
"count": "按规则"
|
||||||
},
|
},
|
||||||
"depends_on": ["生成规则·重要NPC"]
|
"depends_on": ["生成规则·重要NPC"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "美学纲领与交互范式·改",
|
||||||
|
"name": "美学纲领与交互范式",
|
||||||
|
"mode": "revise",
|
||||||
|
"revises": "美学纲领与交互范式",
|
||||||
|
"depends_on": ["实现机制"]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -103,25 +113,28 @@ contextSegments:
|
|||||||
|
|
||||||
1. `steps` 数组顺序 = **建议执行顺序**(依赖须指向更前的步骤)
|
1. `steps` 数组顺序 = **建议执行顺序**(依赖须指向更前的步骤)
|
||||||
2. `name` = 能力池里的固定中文名(可重复)
|
2. `name` = 能力池里的固定中文名(可重复)
|
||||||
3. `id` = 本局步骤唯一键;同 name 多次时必须不同;优先语义化(`生成规则·{对象}`)
|
3. `id` = 本局步骤唯一键;同 name 多次时必须不同;新建优先语义化(`生成规则·{对象}`);回头修改用 `{原id}·改`
|
||||||
4. `depends_on` = 其它步骤的 **id**(若某 name 在本流程唯一,也可写该 name)
|
4. `depends_on` = 其它步骤的 **id**(若某 name 在本流程唯一,也可写该 name)
|
||||||
5. `params` = 本步调用参数(对象)。能力目录声明了编排参数时,**必填项必须写出**;可选则能推断就写
|
5. `params` = 本步调用参数(对象)。〔先验产物〕:此处是规划产物(可空;已知对象就填,便于区分多条)。其它能力目录声明了编排参数时,**必填项必须写出**;可选则能推断就写。回头修改可省略,程序会从 `revises` 那步继承
|
||||||
6. `status`:`"open"` = 还可能追加;`"closed"` = 不再扩步(可走收成)
|
6. `status`:`"open"` = 还可能追加;`"closed"` = 不再扩步(可走收成)
|
||||||
7. 已验收步骤的 id **必须保留**;只能追加新步,或改未验收步的依赖/顺序/params
|
7. 已验收步骤的 id **必须保留**;只能追加新步,或改未验收步的依赖/顺序/params
|
||||||
8. 已完成的**非反复**技能(美学纲领、实现机制、舞台骨架、叙事指南等)禁止再排一次执行;只有目录标了〔可反复〕的(生成规则、具体实例等)才可再编入新 id
|
8. 已完成的**非反复**技能禁止再排一条 `mode=fresh`;要改旧稿只能追加 `mode=revise` + `revises`。〔可反复〕再编入新 id 且 fresh = 彻底新建(新对象/新批次),不是改旧条;改旧条同样用 revise
|
||||||
9. 配方建议 steps 只作近期起点;按能力 meta 按需选用,勿默认全选、勿一次排满
|
9. 配方建议 steps 只作近期起点;按能力 meta 按需选用,勿默认全选、勿一次排满
|
||||||
10. **禁止**把「交互」与「美学」拆成两步
|
10. **禁止**把「交互」与「美学」拆成两步
|
||||||
11. **禁止**在本步写能力正文、自由 Worker 列表、表结构
|
11. **禁止**在本步写能力正文、自由 Worker 列表、表结构
|
||||||
12. **禁止**编排「反复发明执行单元」;收成链:确认「游玩拓扑」→(上下文已多时)「上下文投影排序」→「细化终稿」。挂谁可早、排第几须晚,勿让中段技能写死 order
|
12. **禁止**编排「反复发明执行单元」;收成链:确认「游玩拓扑」→(上下文已多时)「上下文投影排序」→「细化终稿」→(可开玩时)「开场白与开场变量」。挂谁可早、排第几须晚,勿让中段技能写死 order
|
||||||
13. 参数或选型不够时,用 askUser 问 1~2 点(**优先带 options,并允许其它**)。
|
13. 「开场白与开场变量」标〔收口〕:排在细化终稿之后作为最后一步;排入后不要再追加其它步,可将 `status` 设为 `closed`。选定开场由程序落库并保存定稿,不必再调 opening-generator
|
||||||
只许问**排布层**的事:下一步排哪个能力、某步的必填 params 取什么值。
|
14. 参数或选型不够时,用 askUser 问 1~2 点(**优先带 options,并允许其它**)。
|
||||||
|
只许问**排布层**的事:下一步排哪个能力、普通节点的必填 params 取什么值、是新建还是回头修改哪一步。
|
||||||
|
〔先验产物〕的「生成什么 / 调用哪个规则」在步内钉,**不要**为此 askUser。
|
||||||
**禁止**问体验偏好、设定细节、口味倾向等能力正文问题——那是 design-step 的活;
|
**禁止**问体验偏好、设定细节、口味倾向等能力正文问题——那是 design-step 的活;
|
||||||
已验收步骤里问过的题,**禁止**再抛一遍(用户会读成「那一步要重做」)
|
已验收步骤里问过的题,**禁止**再抛一遍(用户会读成「那一步要重做」)
|
||||||
14. 依赖真实产物的步骤(如具体实例依赖某条生成规则)须等上游验收后再编排,并从产物中列出可选项供用户选
|
15. 依赖真实产物的步骤(如具体实例依赖某条生成规则)须等上游验收后再编排,并从产物中列出可选项供用户选
|
||||||
15. `summary`:`流程 · N 步 · open|closed · …`
|
16. `summary`:`流程 · N 步 · open|closed · …`
|
||||||
16. **改对象必须改后台 id**:用户把「生成规则」从女租客改成丧尸怪物时,须同步改
|
17. **改对象必须改后台 id**:用户把「生成规则」从女租客改成丧尸怪物时(编排修订或步内改规划),须同步改
|
||||||
`params.target`、`params.rule_id`(对应英文 kebab-case,如 `zombies`)、以及步骤 `id`(如 `生成规则·丧尸怪物`)。
|
`params.target`、`params.rule_id`(对应英文 kebab-case,如 `zombies`)、以及步骤 `id`(如 `生成规则·丧尸怪物`)。
|
||||||
**禁止**只改中文对象却沿用旧 `rule_id` / 旧步骤 id。
|
**禁止**只改中文对象却沿用旧 `rule_id` / 旧步骤 id。
|
||||||
|
这是**改这条规则的对象**(回头修改或改未验收步),与「再来一条新的生成规则」不是一回事。
|
||||||
|
|
||||||
## 自检
|
## 自检
|
||||||
|
|
||||||
@@ -130,10 +143,12 @@ contextSegments:
|
|||||||
- 是否误排了多次「发明 Worker」?应改为至多一次「游玩拓扑」
|
- 是否误排了多次「发明 Worker」?应改为至多一次「游玩拓扑」
|
||||||
- 每步是否按能力 meta 的何时用/不用判断过,而非题材默认?
|
- 每步是否按能力 meta 的何时用/不用判断过,而非题材默认?
|
||||||
- 每步 name 都在【能力】里?同名多次是否都有不同 id?
|
- 每步 name 都在【能力】里?同名多次是否都有不同 id?
|
||||||
- 有编排参数的步骤,必填 params 是否已钉齐?缺参是否应先 askUser 而不是产出空壳?
|
- 有编排参数的普通步骤,必填 params 是否已钉齐?缺参是否应先 askUser 而不是产出空壳?
|
||||||
|
- 〔先验产物〕是否误把「生成什么」当成确认开干前必填?规划可空;已知对象才写入 params
|
||||||
- depends_on 是否都指向更靠前的步骤 id?
|
- depends_on 是否都指向更靠前的步骤 id?
|
||||||
- 已验收 id 是否都还在?
|
- 已验收 id 是否都还在?
|
||||||
- 【流程进度】里已完成的非反复技能,是否误当成新规划又排了一次?
|
- 【流程进度】里已完成的非反复技能,是否误当成 fresh 又排了一次?要改旧稿应 mode=revise
|
||||||
- askUser 是否只问了排哪步 / 填哪个参数,而不是体验偏好等正文问题?
|
- askUser 是否只问了排哪步 / 填哪个参数 / 新建还是回头修改,而不是体验偏好等正文问题?
|
||||||
- 需要反复补同类内容时,是否用了新 id + 新 params 追加而非改写旧步?
|
- 需要反复补同类内容时,是否用了新 id + 新 params + fresh 追加(彻底新建),而不是改写旧步?
|
||||||
|
- 用户要改已完成节点时,是否用了 mode=revise + revises,而不是再排一条 fresh?
|
||||||
- 收成前是否把 `status` 设为 `closed`?
|
- 收成前是否把 `status` 设为 `closed`?
|
||||||
|
|||||||
@@ -29,24 +29,37 @@ contextSegments:
|
|||||||
label: "## 【本步】当前工序 id(对应流程 steps[].id)"
|
label: "## 【本步】当前工序 id(对应流程 steps[].id)"
|
||||||
- id: user-demand
|
- id: user-demand
|
||||||
tier: dynamic
|
tier: dynamic
|
||||||
tags: ["用户.需求", "book.brief", "用户.最新输入", "用户.worker答复", "用户.修订说明"]
|
tags: ["用户.需求", "book.brief", "用户.worker答复"]
|
||||||
label: "## 用户表述"
|
label: "## 用户表述"
|
||||||
---
|
---
|
||||||
|
|
||||||
# 创作 · 执行步骤
|
# 创作 · 执行步骤
|
||||||
|
|
||||||
你只做 **【本步】** 标明的那一个工序(流程里的一步 id → 能力 name)。
|
你执行工作流计划里 **【本步】** 那一个工序(步骤 id → 能力 name):按注入的【本步方法】写出该步产物(程序指定的 output tag),交用户验收。其它步骤产物不改;流程只读。
|
||||||
|
|
||||||
程序会在提示词中追加该工序的方法正文,并注入依赖步骤的已验收产物。
|
程序会追加该工序的方法正文,并注入依赖步骤的已验收产物。
|
||||||
|
|
||||||
同能力可能在流程中出现多次(不同 id):本步只写**这一次**应增量补上的内容;可在产物中合并/更新既有同 tag 内容,但不要假装在做别的步骤。
|
同能力可多次出现(不同 id):
|
||||||
|
|
||||||
## 纪律
|
- **新建**(mode 缺省 / fresh):只写这一次应增量补上的新内容;可合并既有同 tag 内容,但不改旧条、不假装在做别的步骤。〔可反复〕的「再来一次」属于这种。
|
||||||
|
- **回头修改**(mode=revise):以【既有产物 · 继承修改】为底稿,改用户点名的部分,保留其余;不从零另起,不再走开场询问。
|
||||||
|
- **按意见修改**(验收底栏):以【待改底稿】(最新产物)为底。用户要求按先后都有效,冲突时以较新的为准;未点名推翻的旧条仍须留在产物里。禁止原样交回。
|
||||||
|
|
||||||
1. 只写本步产物(程序指定的 output tag);不要改其它步骤产物
|
## 执行
|
||||||
2. 产物用简洁 JSON 或结构化中文,方便界面渲染;少写机器变量名
|
|
||||||
3. 若本步有**默认问题**:程序已先发给用户;首答在「用户.worker答复」/「创作.能力开场白」。**禁止**再用 LLM 重复同一开场白
|
1. 认定本步要钉的事项:读【本步方法】里 `task` 开头(钉什么、写到哪个 tag)和 `meta.boundary` 的「本技能:」行。
|
||||||
4. 信息不足:context-fragment 产物把题写在「追问」(1~2 点,建议选项 + 示例),顶层 askUser 为 null。仅完全无法产出时才用顶层 askUser
|
2. 读【用户要求】与依赖产物。若有默认问题:程序已发出,首答在「用户.worker答复」——在其答复上继续,不重复开场白。
|
||||||
5. `summary`:`{本步能力名} · …`
|
3. 按方法正文填产物。〔先验产物〕读【规划产物】当起点(可空、可在步内修订);其它节点编排参数见【本步参数】,直接采用。
|
||||||
6. **禁止**重排或扩写流程;流程只读。需要追加「再来一次生成规则」等 → 由总管再调 design-flow
|
4. 产物用简洁 JSON 或结构化中文,少写机器变量名。`summary`:`{本步能力名} · …`
|
||||||
7. **本步参数**由编排期写入 steps[].params,程序会注入【本步参数】。按参数执行;禁止再问「这一步生成什么 / 调用哪个规则」。参数缺失或与目录必填项不符 → 停止产出,提示返回 design-flow 补参
|
|
||||||
|
## 缺料时
|
||||||
|
|
||||||
|
「不足」只指上面第 1 步认定的事项还不能验收。
|
||||||
|
|
||||||
|
- 缺其中一件:在产物「追问」写 1~2 点(建议选项 + 示例),顶层 askUser 为 null。半残 JSON 仍写入 outputs 交验收。仅完全没有可展示正文时才用顶层 askUser。
|
||||||
|
- 这些事项够验收:`追问.题目` 为 `[]`。方法未点名为必须钉死的键,写「未定」或省略,不发问。
|
||||||
|
- 用户谈到 `boundary` 里交给其它技能的话题:只抽取能改本步必钉事项的一句写入正文,其余写入产物「开放问题」(无该键则在说明里记一句),本步不就此追问。
|
||||||
|
- 〔先验产物〕规划为空:先在本步钉「写什么」,再填产物。不要退回编排器补参。
|
||||||
|
- 普通节点【本步参数】缺必填项或与目录不符:停止产出,提示返回流程编排补参。
|
||||||
|
|
||||||
|
需要追加「再来一次」或回头修改其它步时,由编排器再调流程编排。
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ id: opening-generator
|
|||||||
skill: world-simulator
|
skill: world-simulator
|
||||||
name: 开局 · 开场白
|
name: 开局 · 开场白
|
||||||
description: >-
|
description: >-
|
||||||
【创作末尾】优先落库「设计.开场白与开场变量」;若无则结合已定规格写出开场白,
|
【兜底】DAG 未排「开场白与开场变量」时,在创作末尾落库开场白与初值。
|
||||||
并钉与开场同真相的初值。主产物是开场白,不是填表。须用户验收。
|
若该节点已选定,程序会收口,不必再调本 worker。
|
||||||
version: 3
|
version: 3
|
||||||
stage: design-end
|
stage: design-end
|
||||||
inputTags:
|
inputTags:
|
||||||
@@ -29,13 +29,18 @@ contextSegments:
|
|||||||
label: "## 已定世界与故事规格"
|
label: "## 已定世界与故事规格"
|
||||||
- id: user
|
- id: user
|
||||||
tier: dynamic
|
tier: dynamic
|
||||||
tags: ["用户.需求", "用户.最新输入", "用户.worker答复", "用户.修订说明", "运行.初始变量", "输出.开场白"]
|
tags: ["用户.需求", "用户.最新输入", "用户.worker答复", "用户.修订说明"]
|
||||||
label: "## 用户与开局草稿"
|
label: "## 用户表述"
|
||||||
|
- id: opening-draft
|
||||||
|
tier: static
|
||||||
|
tags: ["运行.初始变量", "输出.开场白"]
|
||||||
|
label: "## 【已有开场】只保留最新稿;若有则在其上改,不要另起"
|
||||||
---
|
---
|
||||||
|
|
||||||
# 开局 · 开场白
|
# 开局 · 开场白
|
||||||
|
|
||||||
创作末尾:规格已定。优先读取 **`设计.开场白与开场变量`**:
|
创作末尾兜底(DAG **没有**「开场白与开场变量」时才需要本 worker)。
|
||||||
|
规格已定。优先读取 **`设计.开场白与开场变量`**:
|
||||||
|
|
||||||
- 若已有且可用:把其中「开场白全文」写入 `输出.开场白`,把「开场变量」写成 `运行.初始变量` / `变量.当前`(表格式);可微调不可另起炉灶。
|
- 若已有且可用:把其中「开场白全文」写入 `输出.开场白`,把「开场变量」写成 `运行.初始变量` / `变量.当前`(表格式);可微调不可另起炉灶。
|
||||||
- 若无:再站在 Worker 集 + 正文组成 + 叙事指南 + 变量设计上现写开场,并钉同真相初值。
|
- 若无:再站在 Worker 集 + 正文组成 + 叙事指南 + 变量设计上现写开场,并钉同真相初值。
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { BlackboardItem } from "../types/blackboard.js";
|
import type { BlackboardItem } from "../types/blackboard.js";
|
||||||
import type { RuntimeSession } from "../types/runtime.js";
|
import type { RuntimeSession } from "../types/runtime.js";
|
||||||
|
import { SLOT_PLAY_TURN_QUEUE, SLOT_PLAY_LAYER_ACTIVE } from "../skills/play-turn.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 判断黑板 tag 是否属于 run 阶段(非实例化确认稿)。
|
* 判断黑板 tag 是否属于 run 阶段(非实例化确认稿)。
|
||||||
@@ -12,9 +13,14 @@ export function isRunPhaseBlackboardTag(tag: string): boolean {
|
|||||||
if (/^book\./.test(tag)) return false;
|
if (/^book\./.test(tag)) return false;
|
||||||
if (/^情境\./.test(tag)) return false;
|
if (/^情境\./.test(tag)) return false;
|
||||||
if (/^博弈\./.test(tag)) return false;
|
if (/^博弈\./.test(tag)) return false;
|
||||||
|
// 锁定开局:跟规格一起进 instance
|
||||||
|
if (tag === "输出.开场白" || tag === "运行.初始变量" || tag === "变量.当前") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// run 层:剥离
|
// run 层:剥离(含聊天历史,不写进角色卡)
|
||||||
return (
|
return (
|
||||||
|
tag === "对话.历史" ||
|
||||||
/^运行\./.test(tag) ||
|
/^运行\./.test(tag) ||
|
||||||
/^世界\.(当前|裁决)/.test(tag) ||
|
/^世界\.(当前|裁决)/.test(tag) ||
|
||||||
/^场景\.公开/.test(tag) ||
|
/^场景\.公开/.test(tag) ||
|
||||||
@@ -35,6 +41,11 @@ export function prepareRuntimeSessionForInstance(session: RuntimeSession): Runti
|
|||||||
copy.pendingDecision = undefined;
|
copy.pendingDecision = undefined;
|
||||||
copy.currentWorkerId = undefined;
|
copy.currentWorkerId = undefined;
|
||||||
copy.waitingReason = undefined;
|
copy.waitingReason = undefined;
|
||||||
|
copy.slots = {
|
||||||
|
...copy.slots,
|
||||||
|
[SLOT_PLAY_TURN_QUEUE]: undefined,
|
||||||
|
[SLOT_PLAY_LAYER_ACTIVE]: undefined,
|
||||||
|
};
|
||||||
copy.artifacts = copy.artifacts.filter((a) =>
|
copy.artifacts = copy.artifacts.filter((a) =>
|
||||||
a.outputTags.every((tag) => !isRunPhaseBlackboardTag(tag)),
|
a.outputTags.every((tag) => !isRunPhaseBlackboardTag(tag)),
|
||||||
);
|
);
|
||||||
|
|||||||
+52
-35
@@ -1,6 +1,10 @@
|
|||||||
import type { LlmConfig } from "../config/env.js";
|
import type { LlmConfig } from "../config/env.js";
|
||||||
import type { GenerationParameters } from "../types/preset.js";
|
import type { GenerationParameters } from "../types/preset.js";
|
||||||
import { consumeOpenAiToolStream } from "./stream-complete.js";
|
import { consumeOpenAiToolStream } from "./stream-complete.js";
|
||||||
|
import {
|
||||||
|
currentAbortSignal,
|
||||||
|
throwIfAborted,
|
||||||
|
} from "./run-abort.js";
|
||||||
|
|
||||||
export type ToolCallPayload = {
|
export type ToolCallPayload = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -58,6 +62,8 @@ export type CompleteOptions = {
|
|||||||
generation?: GenerationParameters;
|
generation?: GenerationParameters;
|
||||||
/** 统计用途,如 main_agent / worker:write-rules */
|
/** 统计用途,如 main_agent / worker:write-rules */
|
||||||
caller?: string;
|
caller?: string;
|
||||||
|
/** 取消进行中的 LLM 请求(停止并重试) */
|
||||||
|
signal?: AbortSignal;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CompleteWithToolsOptions = CompleteOptions & {
|
export type CompleteWithToolsOptions = CompleteOptions & {
|
||||||
@@ -237,6 +243,24 @@ function extractMessageParts(message: Record<string, unknown> | undefined): {
|
|||||||
return { content: "", toolCalls };
|
return { content: "", toolCalls };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function llmFetch(
|
||||||
|
url: string,
|
||||||
|
apiKey: string,
|
||||||
|
body: unknown,
|
||||||
|
options?: CompleteOptions,
|
||||||
|
): Promise<Response> {
|
||||||
|
throwIfAborted(options?.signal);
|
||||||
|
return fetch(url, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${apiKey}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
signal: options?.signal ?? currentAbortSignal(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export class OpenAiCompatibleProvider implements LlmProvider {
|
export class OpenAiCompatibleProvider implements LlmProvider {
|
||||||
constructor(private readonly config: LlmConfig) {}
|
constructor(private readonly config: LlmConfig) {}
|
||||||
|
|
||||||
@@ -245,14 +269,12 @@ export class OpenAiCompatibleProvider implements LlmProvider {
|
|||||||
options?: CompleteOptions,
|
options?: CompleteOptions,
|
||||||
): Promise<CompleteResult> {
|
): Promise<CompleteResult> {
|
||||||
const url = `${this.config.baseUrl.replace(/\/$/, "")}/chat/completions`;
|
const url = `${this.config.baseUrl.replace(/\/$/, "")}/chat/completions`;
|
||||||
const response = await fetch(url, {
|
const response = await llmFetch(
|
||||||
method: "POST",
|
url,
|
||||||
headers: {
|
this.config.apiKey,
|
||||||
"Content-Type": "application/json",
|
buildRequestBody(this.config, messages, options),
|
||||||
Authorization: `Bearer ${this.config.apiKey}`,
|
options,
|
||||||
},
|
);
|
||||||
body: JSON.stringify(buildRequestBody(this.config, messages, options)),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const body = await response.text();
|
const body = await response.text();
|
||||||
@@ -282,16 +304,12 @@ export class OpenAiCompatibleProvider implements LlmProvider {
|
|||||||
callbacks: StreamCallbacks = {},
|
callbacks: StreamCallbacks = {},
|
||||||
): Promise<CompleteResult> {
|
): Promise<CompleteResult> {
|
||||||
const url = `${this.config.baseUrl.replace(/\/$/, "")}/chat/completions`;
|
const url = `${this.config.baseUrl.replace(/\/$/, "")}/chat/completions`;
|
||||||
const response = await fetch(url, {
|
const response = await llmFetch(
|
||||||
method: "POST",
|
url,
|
||||||
headers: {
|
this.config.apiKey,
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: `Bearer ${this.config.apiKey}`,
|
|
||||||
},
|
|
||||||
body: JSON.stringify(
|
|
||||||
buildRequestBody(this.config, messages, { ...options, stream: true }),
|
buildRequestBody(this.config, messages, { ...options, stream: true }),
|
||||||
),
|
options,
|
||||||
});
|
);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const body = await response.text();
|
const body = await response.text();
|
||||||
@@ -319,19 +337,15 @@ export class OpenAiCompatibleProvider implements LlmProvider {
|
|||||||
options: CompleteWithToolsOptions,
|
options: CompleteWithToolsOptions,
|
||||||
): Promise<CompleteWithToolsResult> {
|
): Promise<CompleteWithToolsResult> {
|
||||||
const url = `${this.config.baseUrl.replace(/\/$/, "")}/chat/completions`;
|
const url = `${this.config.baseUrl.replace(/\/$/, "")}/chat/completions`;
|
||||||
const response = await fetch(url, {
|
const response = await llmFetch(
|
||||||
method: "POST",
|
url,
|
||||||
headers: {
|
this.config.apiKey,
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: `Bearer ${this.config.apiKey}`,
|
|
||||||
},
|
|
||||||
body: JSON.stringify(
|
|
||||||
buildRequestBody(this.config, messages, {
|
buildRequestBody(this.config, messages, {
|
||||||
...options,
|
...options,
|
||||||
tools: options.tools,
|
tools: options.tools,
|
||||||
}),
|
}),
|
||||||
),
|
options,
|
||||||
});
|
);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const body = await response.text();
|
const body = await response.text();
|
||||||
@@ -362,20 +376,16 @@ export class OpenAiCompatibleProvider implements LlmProvider {
|
|||||||
callbacks: StreamCallbacks,
|
callbacks: StreamCallbacks,
|
||||||
): Promise<CompleteWithToolsResult> {
|
): Promise<CompleteWithToolsResult> {
|
||||||
const url = `${this.config.baseUrl.replace(/\/$/, "")}/chat/completions`;
|
const url = `${this.config.baseUrl.replace(/\/$/, "")}/chat/completions`;
|
||||||
const response = await fetch(url, {
|
const response = await llmFetch(
|
||||||
method: "POST",
|
url,
|
||||||
headers: {
|
this.config.apiKey,
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: `Bearer ${this.config.apiKey}`,
|
|
||||||
},
|
|
||||||
body: JSON.stringify(
|
|
||||||
buildRequestBody(this.config, messages, {
|
buildRequestBody(this.config, messages, {
|
||||||
...options,
|
...options,
|
||||||
tools: options.tools,
|
tools: options.tools,
|
||||||
stream: true,
|
stream: true,
|
||||||
}),
|
}),
|
||||||
),
|
options,
|
||||||
});
|
);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const body = await response.text();
|
const body = await response.text();
|
||||||
@@ -425,6 +435,7 @@ export class MockLlmProvider implements LlmProvider {
|
|||||||
_messages: ChatMessage[],
|
_messages: ChatMessage[],
|
||||||
options?: CompleteOptions,
|
options?: CompleteOptions,
|
||||||
): Promise<CompleteResult> {
|
): Promise<CompleteResult> {
|
||||||
|
throwIfAborted(options?.signal);
|
||||||
const step = this.nextStep();
|
const step = this.nextStep();
|
||||||
const response =
|
const response =
|
||||||
typeof step === "string"
|
typeof step === "string"
|
||||||
@@ -448,6 +459,7 @@ export class MockLlmProvider implements LlmProvider {
|
|||||||
options?: CompleteOptions,
|
options?: CompleteOptions,
|
||||||
callbacks: StreamCallbacks = {},
|
callbacks: StreamCallbacks = {},
|
||||||
): Promise<CompleteResult> {
|
): Promise<CompleteResult> {
|
||||||
|
throwIfAborted(options?.signal);
|
||||||
const step = this.nextStep();
|
const step = this.nextStep();
|
||||||
const response =
|
const response =
|
||||||
typeof step === "string"
|
typeof step === "string"
|
||||||
@@ -455,10 +467,12 @@ export class MockLlmProvider implements LlmProvider {
|
|||||||
: (step.content ?? JSON.stringify({ action: "ask_user", reason: "mock" }));
|
: (step.content ?? JSON.stringify({ action: "ask_user", reason: "mock" }));
|
||||||
const reasoning = "用户需要明确分工 → 调用 design-intake 产出 worker 集。";
|
const reasoning = "用户需要明确分工 → 调用 design-intake 产出 worker 集。";
|
||||||
for (const ch of reasoning) {
|
for (const ch of reasoning) {
|
||||||
|
throwIfAborted(options?.signal);
|
||||||
callbacks.onReasoningDelta?.(ch);
|
callbacks.onReasoningDelta?.(ch);
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
}
|
}
|
||||||
for (const ch of response) {
|
for (const ch of response) {
|
||||||
|
throwIfAborted(options?.signal);
|
||||||
callbacks.onContentDelta?.(ch);
|
callbacks.onContentDelta?.(ch);
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
}
|
}
|
||||||
@@ -480,6 +494,7 @@ export class MockLlmProvider implements LlmProvider {
|
|||||||
_messages: ChatMessage[],
|
_messages: ChatMessage[],
|
||||||
_options: CompleteWithToolsOptions,
|
_options: CompleteWithToolsOptions,
|
||||||
): Promise<CompleteWithToolsResult> {
|
): Promise<CompleteWithToolsResult> {
|
||||||
|
throwIfAborted(_options?.signal);
|
||||||
const step = this.nextStep();
|
const step = this.nextStep();
|
||||||
if (typeof step === "string") {
|
if (typeof step === "string") {
|
||||||
return {
|
return {
|
||||||
@@ -507,9 +522,11 @@ export class MockLlmProvider implements LlmProvider {
|
|||||||
_options: CompleteWithToolsOptions,
|
_options: CompleteWithToolsOptions,
|
||||||
callbacks: StreamCallbacks,
|
callbacks: StreamCallbacks,
|
||||||
): Promise<CompleteWithToolsResult> {
|
): Promise<CompleteWithToolsResult> {
|
||||||
|
throwIfAborted(_options?.signal);
|
||||||
const reasoning =
|
const reasoning =
|
||||||
"用户需要明确 Worker 分工 → 先读取黑板与 worker 列表 → 调用 design-intake 产出 worker 集。";
|
"用户需要明确 Worker 分工 → 先读取黑板与 worker 列表 → 调用 design-intake 产出 worker 集。";
|
||||||
for (const ch of reasoning) {
|
for (const ch of reasoning) {
|
||||||
|
throwIfAborted(_options?.signal);
|
||||||
callbacks.onReasoningDelta?.(ch);
|
callbacks.onReasoningDelta?.(ch);
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
}
|
}
|
||||||
|
|||||||
+73
-33
@@ -1,4 +1,5 @@
|
|||||||
import { assemblePresetMessages, mergeMessages } from "../preset/assembler.js";
|
import { loadAppSettings } from "../config/settings.js";
|
||||||
|
import { resolveActivePreset } from "../preset/store.js";
|
||||||
import type { PresetPackage } from "../types/preset.js";
|
import type { PresetPackage } from "../types/preset.js";
|
||||||
import type {
|
import type {
|
||||||
ChatMessage,
|
ChatMessage,
|
||||||
@@ -9,9 +10,35 @@ import type {
|
|||||||
LlmProvider,
|
LlmProvider,
|
||||||
StreamCallbacks,
|
StreamCallbacks,
|
||||||
} from "./client.js";
|
} from "./client.js";
|
||||||
|
import {
|
||||||
|
TokenTrackingProvider,
|
||||||
|
type LlmTrackingContext,
|
||||||
|
} from "./token-tracker.js";
|
||||||
|
|
||||||
|
export type LlmTrackingRef = { current: LlmTrackingContext };
|
||||||
|
|
||||||
|
function readActivePreset(): PresetPackage | null {
|
||||||
|
return resolveActivePreset(loadAppSettings().activePresetId);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 在所有 LLM 请求前注入当前 preset 的 prompt 片段与生成参数。
|
* 统计在内、预设在外:痕迹里的 messages / generation 就是发给模型的那一份。
|
||||||
|
* 当前没有选用预设时仍包一层,请求原样转发。
|
||||||
|
*/
|
||||||
|
export function wrapLlmForSession(
|
||||||
|
inner: LlmProvider,
|
||||||
|
trackingRef?: LlmTrackingRef,
|
||||||
|
getPreset: () => PresetPackage | null = readActivePreset,
|
||||||
|
): LlmProvider {
|
||||||
|
const tracked = trackingRef
|
||||||
|
? new TokenTrackingProvider(inner, () => trackingRef.current)
|
||||||
|
: inner;
|
||||||
|
return new PresetLlmProvider(tracked, getPreset);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在所有 LLM 请求上合并当前 preset 的生成参数。
|
||||||
|
* prompt / marker 不在此前置,由 play worker 按扩展 prompt_order 填洞。
|
||||||
*/
|
*/
|
||||||
export class PresetLlmProvider implements LlmProvider {
|
export class PresetLlmProvider implements LlmProvider {
|
||||||
constructor(
|
constructor(
|
||||||
@@ -19,41 +46,57 @@ export class PresetLlmProvider implements LlmProvider {
|
|||||||
private readonly getPreset: () => PresetPackage | null,
|
private readonly getPreset: () => PresetPackage | null,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
private prepare(
|
||||||
|
messages: ChatMessage[],
|
||||||
|
options?: CompleteOptions,
|
||||||
|
): { messages: ChatMessage[]; options: CompleteOptions | undefined } {
|
||||||
|
const preset = this.getPreset();
|
||||||
|
if (!preset) return { messages, options };
|
||||||
|
// 生成参数全局合并;prompt/marker 只在 play worker 按 prompt_order 填洞,这里不再前置。
|
||||||
|
return {
|
||||||
|
messages,
|
||||||
|
options: {
|
||||||
|
...options,
|
||||||
|
generation: options?.generation ?? preset.generation,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async complete(
|
async complete(
|
||||||
messages: ChatMessage[],
|
messages: ChatMessage[],
|
||||||
options?: CompleteOptions,
|
options?: CompleteOptions,
|
||||||
): Promise<CompleteResult> {
|
): Promise<CompleteResult> {
|
||||||
const preset = this.getPreset();
|
const prepared = this.prepare(messages, options);
|
||||||
if (!preset) {
|
return this.inner.complete(prepared.messages, prepared.options);
|
||||||
return this.inner.complete(messages, options);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const presetMessages = assemblePresetMessages(preset);
|
async completeStream(
|
||||||
const merged = mergeMessages(presetMessages, messages);
|
messages: ChatMessage[],
|
||||||
const generation = options?.generation ?? preset.generation;
|
options?: CompleteOptions,
|
||||||
|
callbacks: StreamCallbacks = {},
|
||||||
return this.inner.complete(merged, {
|
): Promise<CompleteResult> {
|
||||||
...options,
|
if (!this.inner.completeStream) {
|
||||||
generation,
|
const result = await this.complete(messages, options);
|
||||||
});
|
if (result.reasoning) callbacks.onReasoningDelta?.(result.reasoning);
|
||||||
|
if (result.content) callbacks.onContentDelta?.(result.content);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
const prepared = this.prepare(messages, options);
|
||||||
|
return this.inner.completeStream(
|
||||||
|
prepared.messages,
|
||||||
|
prepared.options,
|
||||||
|
callbacks,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async completeWithTools(
|
async completeWithTools(
|
||||||
messages: ChatMessage[],
|
messages: ChatMessage[],
|
||||||
options: CompleteWithToolsOptions,
|
options: CompleteWithToolsOptions,
|
||||||
): Promise<CompleteWithToolsResult> {
|
): Promise<CompleteWithToolsResult> {
|
||||||
const preset = this.getPreset();
|
const prepared = this.prepare(messages, options);
|
||||||
if (!preset) {
|
return this.inner.completeWithTools(prepared.messages, {
|
||||||
return this.inner.completeWithTools(messages, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
const presetMessages = assemblePresetMessages(preset);
|
|
||||||
const merged = mergeMessages(presetMessages, messages);
|
|
||||||
const generation = options.generation ?? preset.generation;
|
|
||||||
|
|
||||||
return this.inner.completeWithTools(merged, {
|
|
||||||
...options,
|
...options,
|
||||||
generation,
|
...prepared.options,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,17 +105,14 @@ export class PresetLlmProvider implements LlmProvider {
|
|||||||
options: CompleteWithToolsOptions,
|
options: CompleteWithToolsOptions,
|
||||||
callbacks: StreamCallbacks,
|
callbacks: StreamCallbacks,
|
||||||
): Promise<CompleteWithToolsResult> {
|
): Promise<CompleteWithToolsResult> {
|
||||||
const preset = this.getPreset();
|
|
||||||
if (!this.inner.completeWithToolsStream) {
|
if (!this.inner.completeWithToolsStream) {
|
||||||
return this.completeWithTools(messages, options);
|
return this.completeWithTools(messages, options);
|
||||||
}
|
}
|
||||||
const presetMessages = preset ? assemblePresetMessages(preset) : [];
|
const prepared = this.prepare(messages, options);
|
||||||
const merged = preset ? mergeMessages(presetMessages, messages) : messages;
|
return this.inner.completeWithToolsStream(
|
||||||
const generation = options.generation ?? preset?.generation;
|
prepared.messages,
|
||||||
|
{ ...options, ...prepared.options },
|
||||||
return this.inner.completeWithToolsStream(merged, {
|
callbacks,
|
||||||
...options,
|
);
|
||||||
generation,
|
|
||||||
}, callbacks);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { AsyncLocalStorage } from "node:async_hooks";
|
||||||
|
|
||||||
|
const storage = new AsyncLocalStorage<AbortSignal>();
|
||||||
|
|
||||||
|
export function runWithAbortSignal<T>(
|
||||||
|
signal: AbortSignal,
|
||||||
|
fn: () => Promise<T>,
|
||||||
|
): Promise<T> {
|
||||||
|
return storage.run(signal, fn);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function currentAbortSignal(): AbortSignal | undefined {
|
||||||
|
return storage.getStore();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isAbortError(err: unknown): boolean {
|
||||||
|
if (!err || typeof err !== "object") return false;
|
||||||
|
const name = "name" in err ? String(err.name) : "";
|
||||||
|
if (name === "AbortError") return true;
|
||||||
|
const message = "message" in err ? String(err.message) : "";
|
||||||
|
return /aborted|AbortError/i.test(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function throwIfAborted(signal?: AbortSignal): void {
|
||||||
|
const active = signal ?? currentAbortSignal();
|
||||||
|
if (!active?.aborted) return;
|
||||||
|
const err = new Error("This operation was aborted");
|
||||||
|
err.name = "AbortError";
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
@@ -32,14 +32,21 @@ export type LlmTrackingContext = {
|
|||||||
|
|
||||||
function capturePendingTrace(
|
function capturePendingTrace(
|
||||||
ctx: LlmTrackingContext,
|
ctx: LlmTrackingContext,
|
||||||
messages: Array<{ role: string; content: string }>,
|
messages: Array<{
|
||||||
|
role: string;
|
||||||
|
content?: string | null;
|
||||||
|
tool_calls?: unknown;
|
||||||
|
tool_call_id?: string;
|
||||||
|
}>,
|
||||||
caller: string | undefined,
|
caller: string | undefined,
|
||||||
model?: string,
|
model?: string,
|
||||||
|
generation?: CompleteOptions["generation"],
|
||||||
): void {
|
): void {
|
||||||
ctx.pendingContextTrace = buildContextTrace({
|
ctx.pendingContextTrace = buildContextTrace({
|
||||||
caller: caller ?? "unknown",
|
caller: caller ?? "unknown",
|
||||||
messages,
|
messages,
|
||||||
model,
|
model,
|
||||||
|
generation,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,7 +137,13 @@ export class TokenTrackingProvider implements LlmProvider {
|
|||||||
async () => {
|
async () => {
|
||||||
const result = await this.inner.complete(messages, options);
|
const result = await this.inner.complete(messages, options);
|
||||||
const ctx = this.getContext();
|
const ctx = this.getContext();
|
||||||
capturePendingTrace(ctx, messages, options?.caller, result.model);
|
capturePendingTrace(
|
||||||
|
ctx,
|
||||||
|
messages,
|
||||||
|
options?.caller,
|
||||||
|
result.model,
|
||||||
|
options?.generation,
|
||||||
|
);
|
||||||
recordUsage(ctx, options?.caller, result.model, result.usage);
|
recordUsage(ctx, options?.caller, result.model, result.usage);
|
||||||
if (result.reasoning?.trim()) {
|
if (result.reasoning?.trim()) {
|
||||||
ctx.pendingReasoning = result.reasoning.trim();
|
ctx.pendingReasoning = result.reasoning.trim();
|
||||||
@@ -168,7 +181,13 @@ export class TokenTrackingProvider implements LlmProvider {
|
|||||||
onContentDelta: callbacks.onContentDelta,
|
onContentDelta: callbacks.onContentDelta,
|
||||||
});
|
});
|
||||||
const ctx = this.getContext();
|
const ctx = this.getContext();
|
||||||
capturePendingTrace(ctx, messages, options?.caller, result.model);
|
capturePendingTrace(
|
||||||
|
ctx,
|
||||||
|
messages,
|
||||||
|
options?.caller,
|
||||||
|
result.model,
|
||||||
|
options?.generation,
|
||||||
|
);
|
||||||
recordUsage(ctx, options?.caller, result.model, result.usage);
|
recordUsage(ctx, options?.caller, result.model, result.usage);
|
||||||
if (result.reasoning?.trim()) {
|
if (result.reasoning?.trim()) {
|
||||||
ctx.pendingReasoning = result.reasoning.trim();
|
ctx.pendingReasoning = result.reasoning.trim();
|
||||||
@@ -189,7 +208,13 @@ export class TokenTrackingProvider implements LlmProvider {
|
|||||||
async () => {
|
async () => {
|
||||||
const result = await this.inner.completeWithTools(messages, options);
|
const result = await this.inner.completeWithTools(messages, options);
|
||||||
const ctx = this.getContext();
|
const ctx = this.getContext();
|
||||||
capturePendingTrace(ctx, messages, options.caller, result.model);
|
capturePendingTrace(
|
||||||
|
ctx,
|
||||||
|
messages,
|
||||||
|
options.caller,
|
||||||
|
result.model,
|
||||||
|
options.generation,
|
||||||
|
);
|
||||||
recordUsage(ctx, options.caller, result.model, result.usage);
|
recordUsage(ctx, options.caller, result.model, result.usage);
|
||||||
if (result.reasoning?.trim()) {
|
if (result.reasoning?.trim()) {
|
||||||
ctx.pendingReasoning = result.reasoning.trim();
|
ctx.pendingReasoning = result.reasoning.trim();
|
||||||
@@ -224,7 +249,13 @@ export class TokenTrackingProvider implements LlmProvider {
|
|||||||
onContentDelta: callbacks.onContentDelta,
|
onContentDelta: callbacks.onContentDelta,
|
||||||
});
|
});
|
||||||
const ctx = this.getContext();
|
const ctx = this.getContext();
|
||||||
capturePendingTrace(ctx, messages, options.caller, result.model);
|
capturePendingTrace(
|
||||||
|
ctx,
|
||||||
|
messages,
|
||||||
|
options.caller,
|
||||||
|
result.model,
|
||||||
|
options.generation,
|
||||||
|
);
|
||||||
recordUsage(ctx, options.caller, result.model, result.usage);
|
recordUsage(ctx, options.caller, result.model, result.usage);
|
||||||
if (result.reasoning?.trim()) {
|
if (result.reasoning?.trim()) {
|
||||||
ctx.pendingReasoning = result.reasoning.trim();
|
ctx.pendingReasoning = result.reasoning.trim();
|
||||||
|
|||||||
@@ -77,6 +77,9 @@ const EFFECT_LABEL: Record<string, string> = {
|
|||||||
run_programmatic_review: "程序验收",
|
run_programmatic_review: "程序验收",
|
||||||
emit_message: "发消息",
|
emit_message: "发消息",
|
||||||
propose_next_creation_step: "提案下一步",
|
propose_next_creation_step: "提案下一步",
|
||||||
|
seal_creation_opening: "收口开场白",
|
||||||
|
run_play_turn: "开游玩回合",
|
||||||
|
continue_play_turn: "续游玩管线",
|
||||||
};
|
};
|
||||||
|
|
||||||
const TOOL_LABEL: Record<string, string> = {
|
const TOOL_LABEL: Record<string, string> = {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { randomUUID } from "node:crypto";
|
import { randomUUID } from "node:crypto";
|
||||||
import type { LlmProvider } from "../llm/client.js";
|
import type { LlmProvider } from "../llm/client.js";
|
||||||
|
import { createLocalLlmDriver, type AgentDriver } from "../runtime/driver.js";
|
||||||
import { normalizeQuestions } from "../skills/question-protocol.js";
|
import { normalizeQuestions } from "../skills/question-protocol.js";
|
||||||
import type { BlackboardTagIndex } from "../types/blackboard.js";
|
import type { BlackboardTagIndex } from "../types/blackboard.js";
|
||||||
import type { MainAgentDecision, RuntimeSession } from "../types/runtime.js";
|
import type { MainAgentDecision, RuntimeSession } from "../types/runtime.js";
|
||||||
@@ -45,7 +46,14 @@ ${workerLines}
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class MainAgent {
|
export class MainAgent {
|
||||||
constructor(private readonly llm: LlmProvider) {}
|
private readonly driver: AgentDriver;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly llm: LlmProvider,
|
||||||
|
driver?: AgentDriver,
|
||||||
|
) {
|
||||||
|
this.driver = driver ?? createLocalLlmDriver(llm);
|
||||||
|
}
|
||||||
|
|
||||||
/** @deprecated 单次 JSON 决策;请用 runToolLoop */
|
/** @deprecated 单次 JSON 决策;请用 runToolLoop */
|
||||||
async decide(context: MainAgentContext): Promise<MainAgentDecision> {
|
async decide(context: MainAgentContext): Promise<MainAgentDecision> {
|
||||||
@@ -70,6 +78,7 @@ export class MainAgent {
|
|||||||
this.llm,
|
this.llm,
|
||||||
context,
|
context,
|
||||||
options.handlers,
|
options.handlers,
|
||||||
|
this.driver,
|
||||||
);
|
);
|
||||||
return decision;
|
return decision;
|
||||||
}
|
}
|
||||||
|
|||||||
+47
-102
@@ -1,12 +1,15 @@
|
|||||||
import type { ChatMessage, LlmProvider, ParsedToolCall, StreamCallbacks } from "../llm/client.js";
|
import type { LlmProvider, ParsedToolCall } from "../llm/client.js";
|
||||||
import { supportsToolStream } from "../llm/stream-complete.js";
|
|
||||||
import { toolCallToDecision, validateLoopToolCall } from "../runtime/tool-registry.js";
|
import { toolCallToDecision, validateLoopToolCall } from "../runtime/tool-registry.js";
|
||||||
|
import {
|
||||||
|
createLocalLlmDriver,
|
||||||
|
type AgentDriver,
|
||||||
|
type DriverToolCall,
|
||||||
|
} from "../runtime/driver.js";
|
||||||
import type { MainAgentDecision } from "../types/runtime.js";
|
import type { MainAgentDecision } from "../types/runtime.js";
|
||||||
import { isMainAgentTerminalTool } from "../types/tools.js";
|
import { isMainAgentTerminalTool } from "../types/tools.js";
|
||||||
import { MAIN_AGENT_TOOL_DEFINITIONS } from "./tools.js";
|
import { MAIN_AGENT_TOOL_DEFINITIONS } from "./tools.js";
|
||||||
import { buildMainAgentUserPrompt, parseMainAgentDecision } from "./main-agent.js";
|
import { buildMainAgentUserPrompt, parseMainAgentDecision } from "./main-agent.js";
|
||||||
import type { MainAgentContext } from "./main-agent.js";
|
import type { MainAgentContext } from "./main-agent.js";
|
||||||
import { debugLog } from "../log.js";
|
|
||||||
|
|
||||||
export type ToolLoopHandlers = {
|
export type ToolLoopHandlers = {
|
||||||
readBlackboard: (tags: string[]) => Record<string, string>;
|
readBlackboard: (tags: string[]) => Record<string, string>;
|
||||||
@@ -31,8 +34,6 @@ export type ToolLoopResult = {
|
|||||||
toolTrace: string[];
|
toolTrace: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const MAX_TOOL_LOOP_ITERATIONS = 12;
|
|
||||||
|
|
||||||
function buildToolLoopSystemPrompt(
|
function buildToolLoopSystemPrompt(
|
||||||
workers: Array<{ id: string; description: string }>,
|
workers: Array<{ id: string; description: string }>,
|
||||||
): string {
|
): string {
|
||||||
@@ -54,35 +55,15 @@ ${workerLines}
|
|||||||
选定节点后立即调用 run_worker。`;
|
选定节点后立即调用 run_worker。`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function completeToolsPreferStream(
|
|
||||||
llm: LlmProvider,
|
|
||||||
messages: ChatMessage[],
|
|
||||||
callbacks: StreamCallbacks,
|
|
||||||
): Promise<Awaited<ReturnType<LlmProvider["completeWithTools"]>>> {
|
|
||||||
if (supportsToolStream(llm)) {
|
|
||||||
return llm.completeWithToolsStream!(messages, {
|
|
||||||
tools: MAIN_AGENT_TOOL_DEFINITIONS,
|
|
||||||
caller: "main_agent",
|
|
||||||
}, callbacks);
|
|
||||||
}
|
|
||||||
const result = await llm.completeWithTools(messages, {
|
|
||||||
tools: MAIN_AGENT_TOOL_DEFINITIONS,
|
|
||||||
caller: "main_agent",
|
|
||||||
});
|
|
||||||
if (result.reasoning) {
|
|
||||||
callbacks.onReasoningDelta?.(result.reasoning);
|
|
||||||
}
|
|
||||||
if (result.content) {
|
|
||||||
callbacks.onContentDelta?.(result.content);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
function executeLoopTool(
|
function executeLoopTool(
|
||||||
call: ParsedToolCall,
|
call: DriverToolCall | ParsedToolCall,
|
||||||
handlers: ToolLoopHandlers,
|
handlers: ToolLoopHandlers,
|
||||||
): string {
|
): string {
|
||||||
const name = validateLoopToolCall(call);
|
const name = validateLoopToolCall({
|
||||||
|
id: call.id,
|
||||||
|
name: call.name,
|
||||||
|
arguments: call.arguments,
|
||||||
|
});
|
||||||
const args = JSON.parse(call.arguments || "{}") as Record<string, unknown>;
|
const args = JSON.parse(call.arguments || "{}") as Record<string, unknown>;
|
||||||
|
|
||||||
switch (name) {
|
switch (name) {
|
||||||
@@ -104,67 +85,31 @@ function executeLoopTool(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function assistantMessageFromToolCalls(
|
|
||||||
content: string | null,
|
|
||||||
toolCalls: ParsedToolCall[],
|
|
||||||
): ChatMessage {
|
|
||||||
return {
|
|
||||||
role: "assistant",
|
|
||||||
content,
|
|
||||||
tool_calls: toolCalls.map((tc) => ({
|
|
||||||
id: tc.id,
|
|
||||||
type: "function" as const,
|
|
||||||
function: { name: tc.name, arguments: tc.arguments },
|
|
||||||
})),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function emitThinkingDone(handlers: ToolLoopHandlers, reasoning?: string): void {
|
|
||||||
const text = reasoning?.trim() ?? "";
|
|
||||||
if (text) handlers.onThinkingDone?.(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 总管 tool loop:在 running 相位内可多轮调用 read_blackboard 等,
|
* 总管 tool loop:在 running 相位内可多轮调用 read_blackboard 等,
|
||||||
* 直到调用终止 tool 并返回 MainAgentDecision。
|
* 直到调用终止 tool 并返回 MainAgentDecision。
|
||||||
|
*
|
||||||
|
* 循环本身交给 {@link AgentDriver};本函数只解释总管 tool 语义。
|
||||||
*/
|
*/
|
||||||
export async function runMainAgentToolLoop(
|
export async function runMainAgentToolLoop(
|
||||||
llm: LlmProvider,
|
llm: LlmProvider,
|
||||||
context: MainAgentContext,
|
context: MainAgentContext,
|
||||||
handlers: ToolLoopHandlers,
|
handlers: ToolLoopHandlers,
|
||||||
|
driver: AgentDriver = createLocalLlmDriver(llm),
|
||||||
): Promise<ToolLoopResult> {
|
): Promise<ToolLoopResult> {
|
||||||
const messages: ChatMessage[] = [
|
|
||||||
{
|
|
||||||
role: "system",
|
|
||||||
content: buildToolLoopSystemPrompt(context.availableWorkers),
|
|
||||||
},
|
|
||||||
{ role: "user", content: buildMainAgentUserPrompt(context) },
|
|
||||||
];
|
|
||||||
|
|
||||||
const toolTrace: string[] = [];
|
const toolTrace: string[] = [];
|
||||||
const streamCallbacks: StreamCallbacks = {
|
|
||||||
onReasoningDelta: (delta) => handlers.onThinkingDelta?.(delta),
|
|
||||||
};
|
|
||||||
|
|
||||||
for (let iteration = 1; iteration <= MAX_TOOL_LOOP_ITERATIONS; iteration++) {
|
const run = await driver.run({
|
||||||
debugLog("llm", `总管循环 第${iteration}轮`);
|
system: buildToolLoopSystemPrompt(context.availableWorkers),
|
||||||
const result = await completeToolsPreferStream(llm, messages, streamCallbacks);
|
messages: [{ role: "user", content: buildMainAgentUserPrompt(context) }],
|
||||||
emitThinkingDone(handlers, result.reasoning);
|
tools: MAIN_AGENT_TOOL_DEFINITIONS,
|
||||||
|
caller: "main_agent",
|
||||||
if (result.toolCalls.length === 0) {
|
label: "总管循环",
|
||||||
if (result.content?.trim()) {
|
onThinkingDelta: (delta) => handlers.onThinkingDelta?.(delta),
|
||||||
const decision = parseMainAgentDecision(result.content);
|
onThinkingDone: (text) => handlers.onThinkingDone?.(text),
|
||||||
return { decision, iterations: iteration, toolTrace };
|
handleStep: (calls) => {
|
||||||
}
|
const terminalCalls = calls.filter((tc) => isMainAgentTerminalTool(tc.name));
|
||||||
throw new Error("Main Agent returned no tool calls and no content");
|
const loopCalls = calls.filter((tc) => !isMainAgentTerminalTool(tc.name));
|
||||||
}
|
|
||||||
|
|
||||||
const terminalCalls = result.toolCalls.filter((tc) =>
|
|
||||||
isMainAgentTerminalTool(tc.name),
|
|
||||||
);
|
|
||||||
const loopCalls = result.toolCalls.filter(
|
|
||||||
(tc) => !isMainAgentTerminalTool(tc.name),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (terminalCalls.length > 1) {
|
if (terminalCalls.length > 1) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@@ -172,34 +117,34 @@ export async function runMainAgentToolLoop(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
messages.push(
|
|
||||||
assistantMessageFromToolCalls(result.content, result.toolCalls),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (terminalCalls.length === 1) {
|
if (terminalCalls.length === 1) {
|
||||||
const terminal = terminalCalls[0];
|
const terminal = terminalCalls[0];
|
||||||
handlers.onToolCall?.(terminal.name, terminal.arguments);
|
handlers.onToolCall?.(terminal.name, terminal.arguments);
|
||||||
toolTrace.push(`${terminal.name} (terminal)`);
|
toolTrace.push(`${terminal.name} (terminal)`);
|
||||||
|
return { kind: "conclude" as const, call: terminal };
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = loopCalls.map((call) => {
|
||||||
|
const output = executeLoopTool(call, handlers);
|
||||||
|
handlers.onToolCall?.(call.name, output.slice(0, 200));
|
||||||
|
toolTrace.push(call.name);
|
||||||
|
return { callId: call.id, content: output };
|
||||||
|
});
|
||||||
|
return { kind: "continue" as const, results };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (run.stop.kind === "text") {
|
||||||
return {
|
return {
|
||||||
decision: toolCallToDecision(terminal),
|
decision: parseMainAgentDecision(run.stop.content),
|
||||||
iterations: iteration,
|
iterations: run.iterations,
|
||||||
toolTrace,
|
toolTrace,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const call of loopCalls) {
|
return {
|
||||||
const output = executeLoopTool(call, handlers);
|
decision: toolCallToDecision(run.stop.call),
|
||||||
handlers.onToolCall?.(call.name, output.slice(0, 200));
|
iterations: run.iterations,
|
||||||
toolTrace.push(call.name);
|
toolTrace,
|
||||||
messages.push({
|
};
|
||||||
role: "tool",
|
|
||||||
content: output,
|
|
||||||
tool_call_id: call.id,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(
|
|
||||||
`Main Agent tool loop exceeded ${MAX_TOOL_LOOP_ITERATIONS} iterations`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import type { ChatMessage } from "../llm/client.js";
|
import type { ChatMessage } from "../llm/client.js";
|
||||||
import type { PresetPackage } from "../types/preset.js";
|
import type { PresetPackage } from "../types/preset.js";
|
||||||
|
import { isStUnfilledMarker } from "./markers.js";
|
||||||
|
|
||||||
export type MarkerResolver = (identifier: string) => string | null;
|
export type MarkerResolver = (identifier: string) => string | null;
|
||||||
|
|
||||||
const defaultMarkerResolver: MarkerResolver = () => null;
|
const defaultMarkerResolver: MarkerResolver = () => null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 按 prompt_order 装配 preset 消息,插入在所有业务 prompt 之前。
|
* 按 prompt_order 装配 preset 消息。
|
||||||
|
* 酒馆角色卡 / WI marker 不走 resolver(空洞跳过);自有洞由 resolver 填。
|
||||||
*/
|
*/
|
||||||
export function assemblePresetMessages(
|
export function assemblePresetMessages(
|
||||||
preset: PresetPackage,
|
preset: PresetPackage,
|
||||||
@@ -25,8 +27,10 @@ export function assemblePresetMessages(
|
|||||||
if (!entry || !entry.enabled) continue;
|
if (!entry || !entry.enabled) continue;
|
||||||
|
|
||||||
let content = entry.content.trim();
|
let content = entry.content.trim();
|
||||||
if (!content && entry.marker) {
|
const markerId = entry.sourceIdentifier || entry.id;
|
||||||
const resolved = resolveMarker(entry.sourceIdentifier);
|
const skipFill = isStUnfilledMarker(markerId);
|
||||||
|
if (!content && entry.marker && !skipFill) {
|
||||||
|
const resolved = resolveMarker(markerId);
|
||||||
if (resolved?.trim()) content = resolved.trim();
|
if (resolved?.trim()) content = resolved.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+16
-16
@@ -7,6 +7,7 @@ import type {
|
|||||||
PresetPromptOrderItem,
|
PresetPromptOrderItem,
|
||||||
UnsupportedPresetSection,
|
UnsupportedPresetSection,
|
||||||
} from "../types/preset.js";
|
} from "../types/preset.js";
|
||||||
|
import { applyExtendedMarkers } from "./markers.js";
|
||||||
|
|
||||||
type StPrompt = {
|
type StPrompt = {
|
||||||
identifier?: string;
|
identifier?: string;
|
||||||
@@ -186,17 +187,6 @@ export function importSillyTavernPreset(
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const referenced = new Set(promptOrder.map((o) => o.promptId));
|
|
||||||
const unreferencedCount = prompts.filter(
|
|
||||||
(p) => !referenced.has(p.id),
|
|
||||||
).length;
|
|
||||||
|
|
||||||
const enabledCount = promptOrder.filter((o) => {
|
|
||||||
if (!o.enabled) return false;
|
|
||||||
const entry = prompts.find((p) => p.id === o.promptId);
|
|
||||||
return entry?.enabled !== false;
|
|
||||||
}).length;
|
|
||||||
|
|
||||||
const generation = readGeneration(raw);
|
const generation = readGeneration(raw);
|
||||||
const generationFields = Object.entries(generation)
|
const generationFields = Object.entries(generation)
|
||||||
.filter(([, v]) => v !== undefined)
|
.filter(([, v]) => v !== undefined)
|
||||||
@@ -211,7 +201,7 @@ export function importSillyTavernPreset(
|
|||||||
? (rawInput as { name: string }).name
|
? (rawInput as { name: string }).name
|
||||||
: "导入的预设");
|
: "导入的预设");
|
||||||
|
|
||||||
const preset: PresetPackage = {
|
const preset: PresetPackage = applyExtendedMarkers({
|
||||||
id: options.id ?? `${slugify(presetName) || "preset"}-${randomUUID().slice(0, 8)}`,
|
id: options.id ?? `${slugify(presetName) || "preset"}-${randomUUID().slice(0, 8)}`,
|
||||||
name: presetName,
|
name: presetName,
|
||||||
source: "sillytavern",
|
source: "sillytavern",
|
||||||
@@ -221,13 +211,23 @@ export function importSillyTavernPreset(
|
|||||||
unsupported: collectUnsupported(raw),
|
unsupported: collectUnsupported(raw),
|
||||||
importedAt: new Date().toISOString(),
|
importedAt: new Date().toISOString(),
|
||||||
raw: rawInput,
|
raw: rawInput,
|
||||||
};
|
});
|
||||||
|
|
||||||
|
const framedReferenced = new Set(preset.promptOrder.map((o) => o.promptId));
|
||||||
|
const framedUnreferenced = preset.prompts.filter(
|
||||||
|
(p) => !framedReferenced.has(p.id),
|
||||||
|
).length;
|
||||||
|
const framedEnabled = preset.promptOrder.filter((o) => {
|
||||||
|
if (!o.enabled) return false;
|
||||||
|
const entry = preset.prompts.find((p) => p.id === o.promptId);
|
||||||
|
return entry?.enabled !== false;
|
||||||
|
}).length;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
preset,
|
preset,
|
||||||
promptCount: prompts.length,
|
promptCount: preset.prompts.length,
|
||||||
enabledCount,
|
enabledCount: framedEnabled,
|
||||||
unreferencedCount,
|
unreferencedCount: framedUnreferenced,
|
||||||
missingIdentifiers,
|
missingIdentifiers,
|
||||||
generationFields,
|
generationFields,
|
||||||
warnings,
|
warnings,
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
import type {
|
||||||
|
PresetPackage,
|
||||||
|
PresetPromptEntry,
|
||||||
|
PresetPromptOrderItem,
|
||||||
|
PresetPromptRole,
|
||||||
|
} from "../types/preset.js";
|
||||||
|
|
||||||
|
/** 酒馆角色卡 / WI marker:导入后保留位置,运行时不填我们的正文。 */
|
||||||
|
export const ST_UNFILLED_MARKERS = new Set([
|
||||||
|
"worldInfoBefore",
|
||||||
|
"personaDescription",
|
||||||
|
"charDescription",
|
||||||
|
"charPersonality",
|
||||||
|
"scenario",
|
||||||
|
"worldInfoAfter",
|
||||||
|
"dialogueExamples",
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const CHAT_HISTORY_MARKER = "chatHistory";
|
||||||
|
export const WORLD_BOOK_BEFORE = "worldBookBefore";
|
||||||
|
export const WORLD_BOOK_AFTER = "worldBookAfter";
|
||||||
|
export const CURRENT_TURN = "currentTurn";
|
||||||
|
export const POST_TURN = "postTurn";
|
||||||
|
|
||||||
|
/** 导入时插入的自有洞(不含共用的 chatHistory)。 */
|
||||||
|
export const APP_INSERTED_MARKERS = [
|
||||||
|
WORLD_BOOK_BEFORE,
|
||||||
|
WORLD_BOOK_AFTER,
|
||||||
|
CURRENT_TURN,
|
||||||
|
POST_TURN,
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const APP_INSERTED_SET = new Set<string>(APP_INSERTED_MARKERS);
|
||||||
|
|
||||||
|
type MarkerDef = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
role: PresetPromptRole;
|
||||||
|
};
|
||||||
|
|
||||||
|
const MARKER_DEFS: MarkerDef[] = [
|
||||||
|
{ id: WORLD_BOOK_BEFORE, name: "世界书(历史前)", role: "system" },
|
||||||
|
{ id: CHAT_HISTORY_MARKER, name: "对话历史", role: "system" },
|
||||||
|
{ id: WORLD_BOOK_AFTER, name: "世界书(历史后)", role: "system" },
|
||||||
|
{ id: CURRENT_TURN, name: "本轮对话", role: "user" },
|
||||||
|
{ id: POST_TURN, name: "本轮之后", role: "system" },
|
||||||
|
];
|
||||||
|
|
||||||
|
function upsertMarkerEntry(
|
||||||
|
prompts: PresetPromptEntry[],
|
||||||
|
def: MarkerDef,
|
||||||
|
): PresetPromptEntry[] {
|
||||||
|
if (prompts.some((p) => p.id === def.id)) return prompts;
|
||||||
|
return [
|
||||||
|
...prompts,
|
||||||
|
{
|
||||||
|
id: def.id,
|
||||||
|
name: def.name,
|
||||||
|
enabled: true,
|
||||||
|
role: def.role,
|
||||||
|
content: "",
|
||||||
|
marker: true,
|
||||||
|
sourceIdentifier: def.id,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function reindex(
|
||||||
|
order: Omit<PresetPromptOrderItem, "orderIndex">[],
|
||||||
|
): PresetPromptOrderItem[] {
|
||||||
|
return order.map((item, orderIndex) => ({ ...item, orderIndex }));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 以 chatHistory 为锚点插入四个自有 marker。
|
||||||
|
* 可重复调用:先摘掉已插入的自有洞,再按规则放回。
|
||||||
|
*/
|
||||||
|
export function applyExtendedMarkers(preset: PresetPackage): PresetPackage {
|
||||||
|
let prompts = [...preset.prompts];
|
||||||
|
for (const def of MARKER_DEFS) {
|
||||||
|
prompts = upsertMarkerEntry(prompts, def);
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawOrder = [...preset.promptOrder]
|
||||||
|
.sort((a, b) => a.orderIndex - b.orderIndex)
|
||||||
|
.filter((item) => !APP_INSERTED_SET.has(item.promptId));
|
||||||
|
|
||||||
|
const histIndex = rawOrder.findIndex(
|
||||||
|
(item) => item.promptId === CHAT_HISTORY_MARKER,
|
||||||
|
);
|
||||||
|
|
||||||
|
const inserted = (id: string): PresetPromptOrderItem => ({
|
||||||
|
promptId: id,
|
||||||
|
enabled: true,
|
||||||
|
orderIndex: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
let next: PresetPromptOrderItem[];
|
||||||
|
if (histIndex < 0) {
|
||||||
|
next = reindex([
|
||||||
|
...rawOrder,
|
||||||
|
inserted(WORLD_BOOK_BEFORE),
|
||||||
|
inserted(CHAT_HISTORY_MARKER),
|
||||||
|
inserted(WORLD_BOOK_AFTER),
|
||||||
|
inserted(CURRENT_TURN),
|
||||||
|
inserted(POST_TURN),
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
const before = rawOrder.slice(0, histIndex);
|
||||||
|
const history = rawOrder[histIndex]!;
|
||||||
|
const after = rawOrder.slice(histIndex + 1);
|
||||||
|
next = reindex([
|
||||||
|
...before,
|
||||||
|
inserted(WORLD_BOOK_BEFORE),
|
||||||
|
history,
|
||||||
|
inserted(WORLD_BOOK_AFTER),
|
||||||
|
inserted(CURRENT_TURN),
|
||||||
|
...after,
|
||||||
|
inserted(POST_TURN),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...preset,
|
||||||
|
prompts,
|
||||||
|
promptOrder: next,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isStUnfilledMarker(identifier: string): boolean {
|
||||||
|
return ST_UNFILLED_MARKERS.has(identifier);
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import type { ChatMessage } from "../llm/client.js";
|
||||||
|
import type { PresetPackage } from "../types/preset.js";
|
||||||
|
import { assemblePresetMessages } from "./assembler.js";
|
||||||
|
import { applyExtendedMarkers } from "./markers.js";
|
||||||
|
import {
|
||||||
|
resolveWorldInfoMarker,
|
||||||
|
type WorldInfoPack,
|
||||||
|
} from "./world-info-pack.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Play worker:system 契约 + 按扩展 prompt_order 填洞。
|
||||||
|
* 不要再把同一份 context-order 正文叠成第二包 user。
|
||||||
|
*/
|
||||||
|
export function assemblePlayWorkerMessages(params: {
|
||||||
|
systemPrompt: string;
|
||||||
|
preset: PresetPackage;
|
||||||
|
pack: WorldInfoPack;
|
||||||
|
}): ChatMessage[] {
|
||||||
|
const framed = applyExtendedMarkers(params.preset);
|
||||||
|
const filled = assemblePresetMessages(framed, (id) =>
|
||||||
|
resolveWorldInfoMarker(params.pack, id),
|
||||||
|
);
|
||||||
|
return [{ role: "system", content: params.systemPrompt }, ...filled];
|
||||||
|
}
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
import type { ChatMessage } from "../llm/client.js";
|
||||||
|
import type { PresetPackage } from "../types/preset.js";
|
||||||
|
import { assemblePlayWorkerMessages } from "./play-frame.js";
|
||||||
|
import type { WorldInfoPack } from "./world-info-pack.js";
|
||||||
|
|
||||||
|
/** 试跑窗口用的短契约,不是 play worker SKILL。 */
|
||||||
|
export const PROBE_SYSTEM_PROMPT =
|
||||||
|
"试跑本条预设:根据已拼好的上下文写一轮短回复。产物是对白或叙述,对应本轮用户输入。";
|
||||||
|
|
||||||
|
export const DEFAULT_PROBE_CONTEXT = {
|
||||||
|
loreBefore: "一座雨夜的港口旅馆。柜台点着油灯。",
|
||||||
|
history: "店员:今晚只剩阁楼那间。\n你:好。",
|
||||||
|
loreAfter: "时间:深夜。地点:旅馆大厅。",
|
||||||
|
postTurn: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PresetProbeInput = {
|
||||||
|
message: string;
|
||||||
|
loreBefore?: string;
|
||||||
|
history?: string;
|
||||||
|
loreAfter?: string;
|
||||||
|
postTurn?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PresetProbeMessage = {
|
||||||
|
role: string;
|
||||||
|
content: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PresetProbeResult = {
|
||||||
|
messages: PresetProbeMessage[];
|
||||||
|
reply: string | null;
|
||||||
|
reasoning: string | null;
|
||||||
|
completed: boolean;
|
||||||
|
skipReason?: string;
|
||||||
|
error?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function optionalEntry(
|
||||||
|
id: string,
|
||||||
|
name: string,
|
||||||
|
content: string,
|
||||||
|
): WorldInfoPack["worldBookBefore"] {
|
||||||
|
const text = content.trim();
|
||||||
|
return text ? [{ id, name, content: text }] : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function worldInfoPackFromProbeInput(
|
||||||
|
input: PresetProbeInput,
|
||||||
|
): WorldInfoPack {
|
||||||
|
const loreBefore = input.loreBefore ?? DEFAULT_PROBE_CONTEXT.loreBefore;
|
||||||
|
const history = input.history ?? DEFAULT_PROBE_CONTEXT.history;
|
||||||
|
const loreAfter = input.loreAfter ?? DEFAULT_PROBE_CONTEXT.loreAfter;
|
||||||
|
const postTurn = input.postTurn ?? DEFAULT_PROBE_CONTEXT.postTurn;
|
||||||
|
|
||||||
|
return {
|
||||||
|
worldBookBefore: optionalEntry("probe-before", "试跑设定", loreBefore),
|
||||||
|
history: history.trim(),
|
||||||
|
worldBookAfter: optionalEntry("probe-after", "本轮状态", loreAfter),
|
||||||
|
turn: input.message.trim(),
|
||||||
|
postTurn: optionalEntry("probe-post", "尾注", postTurn),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function assemblePresetProbe(
|
||||||
|
preset: PresetPackage,
|
||||||
|
input: PresetProbeInput,
|
||||||
|
): ChatMessage[] {
|
||||||
|
return assemblePlayWorkerMessages({
|
||||||
|
systemPrompt: PROBE_SYSTEM_PROMPT,
|
||||||
|
preset,
|
||||||
|
pack: worldInfoPackFromProbeInput(input),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function serializeProbeMessages(
|
||||||
|
messages: ChatMessage[],
|
||||||
|
): PresetProbeMessage[] {
|
||||||
|
return messages.map((m) => ({
|
||||||
|
role: m.role,
|
||||||
|
content: typeof m.content === "string" ? m.content : String(m.content ?? ""),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function runPresetProbe(params: {
|
||||||
|
preset: PresetPackage;
|
||||||
|
input: PresetProbeInput;
|
||||||
|
complete?: (
|
||||||
|
messages: ChatMessage[],
|
||||||
|
) => Promise<{ content: string; reasoning?: string }>;
|
||||||
|
}): Promise<PresetProbeResult> {
|
||||||
|
const message = params.input.message.trim();
|
||||||
|
if (!message) {
|
||||||
|
throw new Error("缺少本轮输入");
|
||||||
|
}
|
||||||
|
|
||||||
|
const messages = assemblePresetProbe(params.preset, {
|
||||||
|
...params.input,
|
||||||
|
message,
|
||||||
|
});
|
||||||
|
const serialized = serializeProbeMessages(messages);
|
||||||
|
|
||||||
|
if (!params.complete) {
|
||||||
|
return {
|
||||||
|
messages: serialized,
|
||||||
|
reply: null,
|
||||||
|
reasoning: null,
|
||||||
|
completed: false,
|
||||||
|
skipReason: "未请求生成",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await params.complete(messages);
|
||||||
|
return {
|
||||||
|
messages: serialized,
|
||||||
|
reply: result.content,
|
||||||
|
reasoning: result.reasoning ?? null,
|
||||||
|
completed: true,
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
return {
|
||||||
|
messages: serialized,
|
||||||
|
reply: null,
|
||||||
|
reasoning: null,
|
||||||
|
completed: false,
|
||||||
|
error: err instanceof Error ? err.message : String(err),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
import type { Blackboard } from "../blackboard/blackboard.js";
|
||||||
|
import type { BlackboardInputMerge } from "../types/blackboard.js";
|
||||||
|
import {
|
||||||
|
isDialogueHistoryRef,
|
||||||
|
DIALOGUE_HISTORY_TAG,
|
||||||
|
} from "../skills/dialogue-history.js";
|
||||||
|
import {
|
||||||
|
renderContextSegmentBody,
|
||||||
|
type ContextSegmentDef,
|
||||||
|
} from "../skills/context-segments.js";
|
||||||
|
import {
|
||||||
|
CHAT_HISTORY_MARKER,
|
||||||
|
CURRENT_TURN,
|
||||||
|
POST_TURN,
|
||||||
|
WORLD_BOOK_AFTER,
|
||||||
|
WORLD_BOOK_BEFORE,
|
||||||
|
isStUnfilledMarker,
|
||||||
|
} from "./markers.js";
|
||||||
|
|
||||||
|
export const CURRENT_TURN_TAG = "用户.最新输入";
|
||||||
|
|
||||||
|
const CURRENT_TURN_ALIASES = new Set([
|
||||||
|
CURRENT_TURN_TAG,
|
||||||
|
"currentTurn",
|
||||||
|
"user.latest",
|
||||||
|
"用户最新输入",
|
||||||
|
]);
|
||||||
|
|
||||||
|
export function isCurrentTurnRef(ref: string): boolean {
|
||||||
|
return CURRENT_TURN_ALIASES.has(ref.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
export type WorldInfoEntry = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
content: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type WorldInfoPack = {
|
||||||
|
worldBookBefore: WorldInfoEntry[];
|
||||||
|
history: string;
|
||||||
|
worldBookAfter: WorldInfoEntry[];
|
||||||
|
turn: string;
|
||||||
|
postTurn: WorldInfoEntry[];
|
||||||
|
};
|
||||||
|
|
||||||
|
function emptyPack(): WorldInfoPack {
|
||||||
|
return {
|
||||||
|
worldBookBefore: [],
|
||||||
|
history: "",
|
||||||
|
worldBookAfter: [],
|
||||||
|
turn: "",
|
||||||
|
postTurn: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function entryName(segment: ContextSegmentDef): string {
|
||||||
|
const raw = (segment.label ?? "").replace(/^#+\s*/, "").trim();
|
||||||
|
return raw || segment.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toEntry(segment: ContextSegmentDef, content: string): WorldInfoEntry {
|
||||||
|
return {
|
||||||
|
id: segment.id,
|
||||||
|
name: entryName(segment),
|
||||||
|
content: content.trim(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatWorldInfoEntries(entries: WorldInfoEntry[]): string {
|
||||||
|
return entries
|
||||||
|
.filter((e) => e.content.trim())
|
||||||
|
.map((e) => {
|
||||||
|
const title = e.name.trim();
|
||||||
|
const body = e.content.trim();
|
||||||
|
return title ? `### ${title}\n\n${body}` : body;
|
||||||
|
})
|
||||||
|
.join("\n\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按 contextSegments 相对「对话.历史」「用户.最新输入」切成四袋。
|
||||||
|
*/
|
||||||
|
export function worldInfoPackFromSegments(params: {
|
||||||
|
segments: ContextSegmentDef[];
|
||||||
|
inputs: Record<string, string>;
|
||||||
|
blackboard: Blackboard;
|
||||||
|
inputMerge?: BlackboardInputMerge;
|
||||||
|
}): WorldInfoPack {
|
||||||
|
const inputMerge = params.inputMerge ?? "latest";
|
||||||
|
const pack = emptyPack();
|
||||||
|
let phase: "before" | "after" | "post" = "before";
|
||||||
|
|
||||||
|
for (const segment of params.segments) {
|
||||||
|
const tags = segment.tags ?? [];
|
||||||
|
const isHist = tags.some((t) => isDialogueHistoryRef(t));
|
||||||
|
const isTurn = tags.some((t) => isCurrentTurnRef(t));
|
||||||
|
const body = renderContextSegmentBody(
|
||||||
|
segment,
|
||||||
|
params.inputs,
|
||||||
|
params.blackboard,
|
||||||
|
inputMerge,
|
||||||
|
).trim();
|
||||||
|
|
||||||
|
if (isHist) {
|
||||||
|
pack.history = body;
|
||||||
|
phase = "after";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (isTurn) {
|
||||||
|
pack.turn = body;
|
||||||
|
phase = "post";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!body) continue;
|
||||||
|
const entry = toEntry(segment, body);
|
||||||
|
if (phase === "before") pack.worldBookBefore.push(entry);
|
||||||
|
else if (phase === "after") pack.worldBookAfter.push(entry);
|
||||||
|
else pack.postTurn.push(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!pack.history.trim() && params.inputs[DIALOGUE_HISTORY_TAG]?.trim()) {
|
||||||
|
pack.history = params.inputs[DIALOGUE_HISTORY_TAG]!.trim();
|
||||||
|
}
|
||||||
|
if (!pack.turn.trim() && params.inputs[CURRENT_TURN_TAG]?.trim()) {
|
||||||
|
pack.turn = params.inputs[CURRENT_TURN_TAG]!.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
return pack;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveWorldInfoMarker(
|
||||||
|
pack: WorldInfoPack,
|
||||||
|
identifier: string,
|
||||||
|
): string | null {
|
||||||
|
if (isStUnfilledMarker(identifier)) return null;
|
||||||
|
switch (identifier) {
|
||||||
|
case WORLD_BOOK_BEFORE: {
|
||||||
|
const text = formatWorldInfoEntries(pack.worldBookBefore);
|
||||||
|
return text || null;
|
||||||
|
}
|
||||||
|
case CHAT_HISTORY_MARKER:
|
||||||
|
return pack.history.trim() || null;
|
||||||
|
case WORLD_BOOK_AFTER: {
|
||||||
|
const text = formatWorldInfoEntries(pack.worldBookAfter);
|
||||||
|
return text || null;
|
||||||
|
}
|
||||||
|
case CURRENT_TURN:
|
||||||
|
return pack.turn.trim() || null;
|
||||||
|
case POST_TURN: {
|
||||||
|
const text = formatWorldInfoEntries(pack.postTurn);
|
||||||
|
return text || null;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
/**
|
||||||
|
* 可替换的 agent 司机:问模型 → 有 tool-call 则交给调用方处理 → 再问,直到不再欠请求。
|
||||||
|
*
|
||||||
|
* 相位机、黑板、规格不走这里。换 dsh / 其它 harness 时只换 {@link AgentDriver} 实现。
|
||||||
|
*
|
||||||
|
* @module runtime/driver
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type {
|
||||||
|
ChatMessage,
|
||||||
|
CompleteWithToolsResult,
|
||||||
|
LlmProvider,
|
||||||
|
StreamCallbacks,
|
||||||
|
ToolDefinition,
|
||||||
|
} from "../llm/client.js";
|
||||||
|
import { supportsToolStream } from "../llm/stream-complete.js";
|
||||||
|
import { debugLog } from "../log.js";
|
||||||
|
|
||||||
|
export type DriverToolCall = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
arguments: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 一步里所有 tool-call 的处理结果:继续把结果写回,或本步结账停循环。 */
|
||||||
|
export type DriverStepOutcome =
|
||||||
|
| { kind: "continue"; results: Array<{ callId: string; content: string }> }
|
||||||
|
| { kind: "conclude"; call: DriverToolCall };
|
||||||
|
|
||||||
|
export type DriverRunInput = {
|
||||||
|
system: string;
|
||||||
|
messages: ChatMessage[];
|
||||||
|
tools: ToolDefinition[];
|
||||||
|
caller?: string;
|
||||||
|
/** 日志用短名,如「总管循环」 */
|
||||||
|
label?: string;
|
||||||
|
maxIterations?: number;
|
||||||
|
onThinkingDelta?: (delta: string) => void;
|
||||||
|
onThinkingDone?: (text: string) => void;
|
||||||
|
handleStep: (
|
||||||
|
calls: DriverToolCall[],
|
||||||
|
) => DriverStepOutcome | Promise<DriverStepOutcome>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DriverStop =
|
||||||
|
| { kind: "text"; content: string }
|
||||||
|
| { kind: "conclude"; call: DriverToolCall };
|
||||||
|
|
||||||
|
export type DriverRunResult = {
|
||||||
|
iterations: number;
|
||||||
|
stop: DriverStop;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 司机:直到模型不再欠一次请求。 */
|
||||||
|
export type AgentDriver = {
|
||||||
|
run(input: DriverRunInput): Promise<DriverRunResult>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DEFAULT_DRIVER_MAX_ITERATIONS = 12;
|
||||||
|
|
||||||
|
function assistantMessageFromToolCalls(
|
||||||
|
content: string | null,
|
||||||
|
toolCalls: DriverToolCall[],
|
||||||
|
): ChatMessage {
|
||||||
|
return {
|
||||||
|
role: "assistant",
|
||||||
|
content,
|
||||||
|
tool_calls: toolCalls.map((tc) => ({
|
||||||
|
id: tc.id,
|
||||||
|
type: "function" as const,
|
||||||
|
function: { name: tc.name, arguments: tc.arguments },
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function emitThinkingDone(
|
||||||
|
input: DriverRunInput,
|
||||||
|
reasoning?: string,
|
||||||
|
): void {
|
||||||
|
const text = reasoning?.trim() ?? "";
|
||||||
|
if (text) input.onThinkingDone?.(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function completeToolsPreferStream(
|
||||||
|
llm: LlmProvider,
|
||||||
|
messages: ChatMessage[],
|
||||||
|
tools: ToolDefinition[],
|
||||||
|
caller: string | undefined,
|
||||||
|
callbacks: StreamCallbacks,
|
||||||
|
): Promise<CompleteWithToolsResult> {
|
||||||
|
const options = { tools, caller };
|
||||||
|
if (supportsToolStream(llm)) {
|
||||||
|
return llm.completeWithToolsStream!(messages, options, callbacks);
|
||||||
|
}
|
||||||
|
const result = await llm.completeWithTools(messages, options);
|
||||||
|
if (result.reasoning) callbacks.onReasoningDelta?.(result.reasoning);
|
||||||
|
if (result.content) callbacks.onContentDelta?.(result.content);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 现有 OpenAI 兼容 provider 上的司机,行为对齐旧 tool-loop。 */
|
||||||
|
export function createLocalLlmDriver(llm: LlmProvider): AgentDriver {
|
||||||
|
return {
|
||||||
|
async run(input: DriverRunInput): Promise<DriverRunResult> {
|
||||||
|
const maxIterations = input.maxIterations ?? DEFAULT_DRIVER_MAX_ITERATIONS;
|
||||||
|
const messages: ChatMessage[] = [
|
||||||
|
{ role: "system", content: input.system },
|
||||||
|
...input.messages,
|
||||||
|
];
|
||||||
|
const streamCallbacks: StreamCallbacks = {
|
||||||
|
onReasoningDelta: (delta) => input.onThinkingDelta?.(delta),
|
||||||
|
};
|
||||||
|
const label = input.label ?? "driver";
|
||||||
|
|
||||||
|
for (let iteration = 1; iteration <= maxIterations; iteration++) {
|
||||||
|
debugLog("llm", `${label} 第${iteration}轮`);
|
||||||
|
const result = await completeToolsPreferStream(
|
||||||
|
llm,
|
||||||
|
messages,
|
||||||
|
input.tools,
|
||||||
|
input.caller,
|
||||||
|
streamCallbacks,
|
||||||
|
);
|
||||||
|
emitThinkingDone(input, result.reasoning);
|
||||||
|
|
||||||
|
if (result.toolCalls.length === 0) {
|
||||||
|
const content = result.content?.trim() ?? "";
|
||||||
|
if (content) {
|
||||||
|
return { iterations: iteration, stop: { kind: "text", content } };
|
||||||
|
}
|
||||||
|
throw new Error("Driver returned no tool calls and no content");
|
||||||
|
}
|
||||||
|
|
||||||
|
const calls: DriverToolCall[] = result.toolCalls.map((tc) => ({
|
||||||
|
id: tc.id,
|
||||||
|
name: tc.name,
|
||||||
|
arguments: tc.arguments,
|
||||||
|
}));
|
||||||
|
messages.push(assistantMessageFromToolCalls(result.content, calls));
|
||||||
|
|
||||||
|
const outcome = await input.handleStep(calls);
|
||||||
|
if (outcome.kind === "conclude") {
|
||||||
|
return {
|
||||||
|
iterations: iteration,
|
||||||
|
stop: { kind: "conclude", call: outcome.call },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const item of outcome.results) {
|
||||||
|
messages.push({
|
||||||
|
role: "tool",
|
||||||
|
content: item.content,
|
||||||
|
tool_call_id: item.callId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Driver exceeded ${maxIterations} iterations`);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
+11
-19
@@ -1,23 +1,21 @@
|
|||||||
import { profileToLlmConfig } from "../config/api-profiles.js";
|
import { profileToLlmConfig } from "../config/api-profiles.js";
|
||||||
import {
|
import {
|
||||||
ensureActiveProfileDefault,
|
ensureActiveProfileDefault,
|
||||||
loadAppSettings,
|
|
||||||
resolveActiveProfile,
|
resolveActiveProfile,
|
||||||
} from "../config/settings.js";
|
} from "../config/settings.js";
|
||||||
import { resolveActivePreset } from "../preset/store.js";
|
|
||||||
import {
|
import {
|
||||||
createMockMainAgentResponse,
|
createMockMainAgentResponse,
|
||||||
MockLlmProvider,
|
MockLlmProvider,
|
||||||
OpenAiCompatibleProvider,
|
OpenAiCompatibleProvider,
|
||||||
type LlmProvider,
|
type LlmProvider,
|
||||||
} from "../llm/client.js";
|
} from "../llm/client.js";
|
||||||
import { PresetLlmProvider } from "../llm/preset-wrapper.js";
|
import type { PresetPackage } from "../types/preset.js";
|
||||||
import {
|
import {
|
||||||
TokenTrackingProvider,
|
wrapLlmForSession,
|
||||||
type LlmTrackingContext,
|
type LlmTrackingRef,
|
||||||
} from "../llm/token-tracker.js";
|
} from "../llm/preset-wrapper.js";
|
||||||
|
|
||||||
export type LlmTrackingRef = { current: LlmTrackingContext };
|
export type { LlmTrackingRef };
|
||||||
|
|
||||||
function buildInnerLlm(): LlmProvider {
|
function buildInnerLlm(): LlmProvider {
|
||||||
ensureActiveProfileDefault();
|
ensureActiveProfileDefault();
|
||||||
@@ -42,22 +40,11 @@ function buildInnerLlm(): LlmProvider {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function wrapWithPreset(inner: LlmProvider): LlmProvider {
|
|
||||||
const settings = loadAppSettings();
|
|
||||||
const preset = resolveActivePreset(settings.activePresetId);
|
|
||||||
if (!preset) return inner;
|
|
||||||
return new PresetLlmProvider(inner, () =>
|
|
||||||
resolveActivePreset(loadAppSettings().activePresetId),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Web/CLI 默认 LLM:本地 profile + 全局 preset + token 统计 */
|
/** Web/CLI 默认 LLM:本地 profile + 全局 preset + token 统计 */
|
||||||
export function createDefaultMainAgentLlm(
|
export function createDefaultMainAgentLlm(
|
||||||
trackingRef?: LlmTrackingRef,
|
trackingRef?: LlmTrackingRef,
|
||||||
): LlmProvider {
|
): LlmProvider {
|
||||||
const llm = wrapWithPreset(buildInnerLlm());
|
return wrapLlmForSession(buildInnerLlm(), trackingRef);
|
||||||
if (!trackingRef) return llm;
|
|
||||||
return new TokenTrackingProvider(llm, () => trackingRef.current);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function hasRealLlmConfig(): boolean {
|
export function hasRealLlmConfig(): boolean {
|
||||||
@@ -76,3 +63,8 @@ export function reloadDefaultMainAgentLlm(
|
|||||||
): LlmProvider {
|
): LlmProvider {
|
||||||
return createDefaultMainAgentLlm(trackingRef);
|
return createDefaultMainAgentLlm(trackingRef);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 试跑指定预设:生成参数跟这条预设走,不依赖当前选用。 */
|
||||||
|
export function createLlmForPreset(preset: PresetPackage): LlmProvider {
|
||||||
|
return wrapLlmForSession(buildInnerLlm(), undefined, () => preset);
|
||||||
|
}
|
||||||
|
|||||||
@@ -158,6 +158,8 @@ export class RuntimeOrchestrator {
|
|||||||
await this.runProgrammaticReview(effect.artifactId);
|
await this.runProgrammaticReview(effect.artifactId);
|
||||||
break;
|
break;
|
||||||
case "invoke_main_agent":
|
case "invoke_main_agent":
|
||||||
|
case "propose_next_creation_step":
|
||||||
|
case "seal_creation_opening":
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+134
-30
@@ -30,6 +30,12 @@ import {
|
|||||||
isModuleOpeningQuestions,
|
isModuleOpeningQuestions,
|
||||||
normalizeQuestions,
|
normalizeQuestions,
|
||||||
} from "../skills/question-protocol.js";
|
} from "../skills/question-protocol.js";
|
||||||
|
import {
|
||||||
|
CREATION_SEALED_WAITING_MESSAGE,
|
||||||
|
SLOT_CREATION_SEALED_BY_OPENING,
|
||||||
|
isOpeningSealArtifact,
|
||||||
|
} from "../skills/opening-seal.js";
|
||||||
|
import { inferLifecycleStage } from "../skills/worker-declaration.js";
|
||||||
import type { QuestionItem } from "../types/questions.js";
|
import type { QuestionItem } from "../types/questions.js";
|
||||||
|
|
||||||
function nowIso(): string {
|
function nowIso(): string {
|
||||||
@@ -81,7 +87,45 @@ function mergeSlotText(
|
|||||||
const trimmed = text.trim();
|
const trimmed = text.trim();
|
||||||
if (!trimmed) return;
|
if (!trimmed) return;
|
||||||
const prev = String(slots[key] ?? "").trim();
|
const prev = String(slots[key] ?? "").trim();
|
||||||
slots[key] = prev ? `${prev}\n\n${trimmed}` : trimmed;
|
if (!prev) {
|
||||||
|
slots[key] = trimmed;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
prev === trimmed ||
|
||||||
|
prev.includes(`\n\n${trimmed}`) ||
|
||||||
|
prev.startsWith(`${trimmed}\n\n`)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
slots[key] = `${prev}\n\n${trimmed}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 同一步内多次「按意见修改」叠意见;较新的写在后面 */
|
||||||
|
function accumulateRevisionInstruction(prev: unknown, next: string): string {
|
||||||
|
const trimmed = next.trim();
|
||||||
|
const old = typeof prev === "string" ? prev.trim() : "";
|
||||||
|
if (!old || old === trimmed) return trimmed;
|
||||||
|
if (old.includes(trimmed)) return old;
|
||||||
|
return `${old}\n\n——\n\n${trimmed}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 验收后把本步意见并入「用户.需求」按时间保留,再清掉本步槽位以免下一步误进修订 */
|
||||||
|
function archiveStepNotesIntoDemand(slots: Record<string, unknown>): void {
|
||||||
|
const reply =
|
||||||
|
typeof slots["用户.worker答复"] === "string"
|
||||||
|
? slots["用户.worker答复"].trim()
|
||||||
|
: "";
|
||||||
|
const note =
|
||||||
|
typeof slots["用户.修订说明"] === "string"
|
||||||
|
? slots["用户.修订说明"].trim()
|
||||||
|
: "";
|
||||||
|
if (reply) mergeSlotText(slots, "用户.需求", reply);
|
||||||
|
if (note) mergeSlotText(slots, "用户.需求", note);
|
||||||
|
delete slots["用户.修订说明"];
|
||||||
|
delete slots.revisionInstruction;
|
||||||
|
delete slots.revisionTargetArtifactId;
|
||||||
|
delete slots["用户.worker答复"];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 追加事件到 history 并 touch */
|
/** 追加事件到 history 并 touch */
|
||||||
@@ -102,6 +146,30 @@ function running(session: RuntimeSession): RuntimeSession {
|
|||||||
return touch({ ...session, phase: "running", waitingReason: undefined });
|
return touch({ ...session, phase: "running", waitingReason: undefined });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 游玩连跑下一执行单元;创作则交总管 */
|
||||||
|
function afterPlayOrMainAgent(
|
||||||
|
session: RuntimeSession,
|
||||||
|
next: RuntimeSession,
|
||||||
|
): ApplyEventResult {
|
||||||
|
const cleared = {
|
||||||
|
...next,
|
||||||
|
pendingArtifactId: undefined,
|
||||||
|
currentWorkerId: undefined,
|
||||||
|
pendingDecision: undefined,
|
||||||
|
resumeContext: undefined,
|
||||||
|
};
|
||||||
|
if (inferLifecycleStage(session) === "play") {
|
||||||
|
return {
|
||||||
|
session: touch(running(cleared)),
|
||||||
|
effects: [{ type: "continue_play_turn" }],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
session: touch(running(cleared)),
|
||||||
|
effects: [{ type: "invoke_main_agent" }],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function findArtifact(
|
function findArtifact(
|
||||||
session: RuntimeSession,
|
session: RuntimeSession,
|
||||||
artifactId: string,
|
artifactId: string,
|
||||||
@@ -150,11 +218,14 @@ function beginRevisionRerun(
|
|||||||
artifact: ArtifactRecord,
|
artifact: ArtifactRecord,
|
||||||
instruction: string,
|
instruction: string,
|
||||||
): ApplyEventResult {
|
): ApplyEventResult {
|
||||||
const trimmed = instruction.trim();
|
const trimmed = accumulateRevisionInstruction(
|
||||||
|
session.slots["用户.修订说明"] ?? session.slots.revisionInstruction,
|
||||||
|
instruction,
|
||||||
|
);
|
||||||
const slots: Record<string, unknown> = {
|
const slots: Record<string, unknown> = {
|
||||||
...session.slots,
|
...session.slots,
|
||||||
"用户.修订说明": trimmed,
|
"用户.修订说明": trimmed,
|
||||||
"用户.最新输入": trimmed,
|
"用户.最新输入": instruction.trim(),
|
||||||
revisionInstruction: trimmed,
|
revisionInstruction: trimmed,
|
||||||
// 本次重跑的底稿;重跑写不出新版时只许退回这一份
|
// 本次重跑的底稿;重跑写不出新版时只许退回这一份
|
||||||
revisionTargetArtifactId: artifact.id,
|
revisionTargetArtifactId: artifact.id,
|
||||||
@@ -319,7 +390,10 @@ function handleWorkerCompleted(
|
|||||||
return fail(session, `Artifact not found: ${event.payload.artifactId}`);
|
return fail(session, `Artifact not found: ${event.payload.artifactId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const mode = session.acceptanceMode ?? "user_confirmed";
|
const mode =
|
||||||
|
inferLifecycleStage(session) === "play"
|
||||||
|
? "no_confirmation"
|
||||||
|
: (session.acceptanceMode ?? "user_confirmed");
|
||||||
let next = appendHistory(session, event);
|
let next = appendHistory(session, event);
|
||||||
|
|
||||||
if (next.slots.revisionTargetArtifactId) {
|
if (next.slots.revisionTargetArtifactId) {
|
||||||
@@ -346,16 +420,13 @@ function handleWorkerCompleted(
|
|||||||
|
|
||||||
if (mode === "no_confirmation") {
|
if (mode === "no_confirmation") {
|
||||||
next = updateArtifact(next, artifact.id, { status: "accepted" });
|
next = updateArtifact(next, artifact.id, { status: "accepted" });
|
||||||
return {
|
return afterPlayOrMainAgent(session, {
|
||||||
session: touch({
|
...next,
|
||||||
...running(next),
|
|
||||||
pendingArtifactId: undefined,
|
pendingArtifactId: undefined,
|
||||||
currentWorkerId: undefined,
|
currentWorkerId: undefined,
|
||||||
pendingDecision: undefined,
|
pendingDecision: undefined,
|
||||||
resumeContext: undefined,
|
resumeContext: undefined,
|
||||||
}),
|
});
|
||||||
effects: [{ type: "invoke_main_agent" }],
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// programmatic_review
|
// programmatic_review
|
||||||
@@ -587,9 +658,8 @@ export function applyEvent(
|
|||||||
if (session.waitingReason?.kind === "worker_questions") {
|
if (session.waitingReason?.kind === "worker_questions") {
|
||||||
slots["用户.worker答复"] = text;
|
slots["用户.worker答复"] = text;
|
||||||
if (text) slots["用户.最新输入"] = text;
|
if (text) slots["用户.最新输入"] = text;
|
||||||
if (demandKey && text) {
|
// 开场/追问答复只进「用户.worker答复」,不追加进「用户.需求」
|
||||||
mergeSlotText(slots, demandKey, text);
|
// (否则后续步骤的用户表述会把各步答复叠成一份长转录,修订意见被淹没)
|
||||||
}
|
|
||||||
} else if (session.waitingReason?.kind === "input" && text) {
|
} else if (session.waitingReason?.kind === "input" && text) {
|
||||||
const key = demandKey || "用户.需求";
|
const key = demandKey || "用户.需求";
|
||||||
applyUserTextToSlots(slots, text, key, {
|
applyUserTextToSlots(slots, text, key, {
|
||||||
@@ -615,6 +685,28 @@ export function applyEvent(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (inferLifecycleStage(session) === "play") {
|
||||||
|
return {
|
||||||
|
session: touch(running({ ...next, resumeContext: undefined })),
|
||||||
|
effects: [{ type: "run_play_turn" }],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (session.slots[SLOT_CREATION_SEALED_BY_OPENING]) {
|
||||||
|
return {
|
||||||
|
session: waiting(touch({ ...next, resumeContext: undefined }), {
|
||||||
|
kind: "input",
|
||||||
|
message: CREATION_SEALED_WAITING_MESSAGE,
|
||||||
|
}),
|
||||||
|
effects: [
|
||||||
|
{
|
||||||
|
type: "emit_message",
|
||||||
|
message: "创作已收口并保存。请切换到「游玩」开始。",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// 常规定稿用户输入 → 等总管下一步
|
// 常规定稿用户输入 → 等总管下一步
|
||||||
return {
|
return {
|
||||||
session: touch(running({ ...next, resumeContext: undefined })),
|
session: touch(running({ ...next, resumeContext: undefined })),
|
||||||
@@ -817,7 +909,10 @@ export function applyEvent(
|
|||||||
|
|
||||||
case "worker_completed": {
|
case "worker_completed": {
|
||||||
const result = handleWorkerCompleted(session, event);
|
const result = handleWorkerCompleted(session, event);
|
||||||
const mode = session.acceptanceMode ?? "user_confirmed";
|
const mode =
|
||||||
|
inferLifecycleStage(session) === "play"
|
||||||
|
? "no_confirmation"
|
||||||
|
: (session.acceptanceMode ?? "user_confirmed");
|
||||||
if (mode === "user_confirmed" && result.session.pendingArtifactId) {
|
if (mode === "user_confirmed" && result.session.pendingArtifactId) {
|
||||||
const sidecar = asOptionalSidecarQuestions(event.payload.questions);
|
const sidecar = asOptionalSidecarQuestions(event.payload.questions);
|
||||||
const assessment =
|
const assessment =
|
||||||
@@ -908,6 +1003,7 @@ export function applyEvent(
|
|||||||
return fail(session, `Artifact not found: ${event.payload.artifactId}`);
|
return fail(session, `Artifact not found: ${event.payload.artifactId}`);
|
||||||
}
|
}
|
||||||
const slots: Record<string, unknown> = { ...session.slots };
|
const slots: Record<string, unknown> = { ...session.slots };
|
||||||
|
archiveStepNotesIntoDemand(slots);
|
||||||
// 仅终稿 tag「设计.worker集」表示完整 Worker 集验收;草稿单位验收不得开 play
|
// 仅终稿 tag「设计.worker集」表示完整 Worker 集验收;草稿单位验收不得开 play
|
||||||
const isFinalSet = artifact.outputTags.some((tag) => tag === "设计.worker集");
|
const isFinalSet = artifact.outputTags.some((tag) => tag === "设计.worker集");
|
||||||
if (isFinalSet) {
|
if (isFinalSet) {
|
||||||
@@ -924,24 +1020,29 @@ export function applyEvent(
|
|||||||
pendingDecision: undefined,
|
pendingDecision: undefined,
|
||||||
currentWorkerId: undefined,
|
currentWorkerId: undefined,
|
||||||
};
|
};
|
||||||
// 创作步验收后:先问下一步意向,再展示下一节点确认开干(勿直接连跑)
|
// 开场白终节点:选定后落库、关 DAG,不再问「下一步想写什么」。
|
||||||
const gateNextIntent =
|
if (isOpeningSealArtifact(artifact)) {
|
||||||
!isFinalSet &&
|
|
||||||
(artifact.workerId === "design-step" ||
|
|
||||||
artifact.workerId === "design-flow");
|
|
||||||
if (gateNextIntent) {
|
|
||||||
return {
|
return {
|
||||||
session: waiting(cleared, {
|
session: waiting(cleared, {
|
||||||
kind: "next_intent",
|
kind: "input",
|
||||||
afterWorkerId: artifact.workerId,
|
message: CREATION_SEALED_WAITING_MESSAGE,
|
||||||
}),
|
}),
|
||||||
effects: [
|
effects: [{ type: "seal_creation_opening" }],
|
||||||
{
|
};
|
||||||
type: "emit_message",
|
}
|
||||||
message:
|
if (inferLifecycleStage(session) === "play") {
|
||||||
"这一步已收下。下一步想写什么?可留空,直接发送后看下一节点并确认开干。",
|
return afterPlayOrMainAgent(session, cleared);
|
||||||
},
|
}
|
||||||
],
|
// 创作步验收后直接提案下一步(有待执行步则确认开干;否则交总管扩步)。
|
||||||
|
// 不再先停在「下一步想写什么」——与「同意并开始」叠成双重确认。
|
||||||
|
// 细化终稿也走提案,而不是 invoke_main_agent(否则容易把刚收下的终稿再跑一遍)。
|
||||||
|
const proposeNext =
|
||||||
|
artifact.workerId === "design-step" ||
|
||||||
|
artifact.workerId === "design-flow";
|
||||||
|
if (proposeNext) {
|
||||||
|
return {
|
||||||
|
session: touch(running(cleared)),
|
||||||
|
effects: [{ type: "propose_next_creation_step" }],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
@@ -1029,7 +1130,10 @@ export function applyEvent(
|
|||||||
session: touch(
|
session: touch(
|
||||||
running({ ...next, pendingArtifactId: undefined }),
|
running({ ...next, pendingArtifactId: undefined }),
|
||||||
),
|
),
|
||||||
effects: [{ type: "invoke_main_agent" }],
|
effects:
|
||||||
|
inferLifecycleStage(session) === "play"
|
||||||
|
? [{ type: "continue_play_turn" }]
|
||||||
|
: [{ type: "invoke_main_agent" }],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+233
-20
@@ -62,6 +62,7 @@ import {
|
|||||||
CREATION_MODULE_OPENING_STATE_TAG,
|
CREATION_MODULE_OPENING_STATE_TAG,
|
||||||
SLOT_CREATION_MODULE_OPENING_STATE,
|
SLOT_CREATION_MODULE_OPENING_STATE,
|
||||||
loadModuleCatalog,
|
loadModuleCatalog,
|
||||||
|
isReviseStep,
|
||||||
mergeCreationFlowPreservingAccepted,
|
mergeCreationFlowPreservingAccepted,
|
||||||
nextPendingStep,
|
nextPendingStep,
|
||||||
parseCreationFlow,
|
parseCreationFlow,
|
||||||
@@ -71,10 +72,12 @@ import {
|
|||||||
resolveDesignStepBinding,
|
resolveDesignStepBinding,
|
||||||
shouldSkipModuleOpening,
|
shouldSkipModuleOpening,
|
||||||
isProgressPointerTag,
|
isProgressPointerTag,
|
||||||
|
missingRequiredStepParams,
|
||||||
stepUnitId,
|
stepUnitId,
|
||||||
stringifyCreationFlow,
|
stringifyCreationFlow,
|
||||||
stringifyModuleOpeningState,
|
stringifyModuleOpeningState,
|
||||||
type CreationFlowStepParams,
|
type CreationFlowStepParams,
|
||||||
|
type ModuleCatalogEntry,
|
||||||
type ModuleParamSpec,
|
type ModuleParamSpec,
|
||||||
} from "../skills/creation-flow.js";
|
} from "../skills/creation-flow.js";
|
||||||
import {
|
import {
|
||||||
@@ -86,6 +89,22 @@ import {
|
|||||||
SLOT_ASKED_QUESTIONS,
|
SLOT_ASKED_QUESTIONS,
|
||||||
} from "../skills/question-protocol.js";
|
} from "../skills/question-protocol.js";
|
||||||
import { parseWorkerSetYaml } from "../skills/worker-set-parse.js";
|
import { parseWorkerSetYaml } from "../skills/worker-set-parse.js";
|
||||||
|
import {
|
||||||
|
closeCreationFlowRaw,
|
||||||
|
CREATION_SEALED_WAITING_MESSAGE,
|
||||||
|
mergeOpeningTablePatch,
|
||||||
|
OPENING_CURRENT_VARS_TAG,
|
||||||
|
OPENING_INITIAL_VARS_TAG,
|
||||||
|
OPENING_OUTPUT_TAG,
|
||||||
|
OPENING_SETUP_ARTIFACT_TAG,
|
||||||
|
parseOpeningSealPayload,
|
||||||
|
SLOT_CREATION_SEALED_BY_OPENING,
|
||||||
|
SLOT_OPENING_SELECTED_INDEX,
|
||||||
|
} from "../skills/opening-seal.js";
|
||||||
|
import {
|
||||||
|
readPlayTurnQueue,
|
||||||
|
withPlayTurnQueue,
|
||||||
|
} from "../skills/play-turn.js";
|
||||||
import { debugLog, labelAction, logStateChange, sessionSnap } from "../log.js";
|
import { debugLog, labelAction, logStateChange, sessionSnap } from "../log.js";
|
||||||
import {
|
import {
|
||||||
executeChance,
|
executeChance,
|
||||||
@@ -272,6 +291,26 @@ export class PhaseRuntime {
|
|||||||
this.lastWorkerRunSnapshot = null;
|
this.lastWorkerRunSnapshot = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行中卡住:有进行中的 worker 则按开跑快照重跑;否则重跑总管。
|
||||||
|
*/
|
||||||
|
async retryStuckRun(): Promise<RuntimeSession> {
|
||||||
|
const workerId = this.session.currentWorkerId;
|
||||||
|
const snap = this.lastWorkerRunSnapshot;
|
||||||
|
if (workerId && snap && snap.workerId === workerId) {
|
||||||
|
this.restoreFromCheckpoint(
|
||||||
|
snap.runtimeSession,
|
||||||
|
snap.blackboardItems,
|
||||||
|
);
|
||||||
|
return this.rerunWorker(workerId);
|
||||||
|
}
|
||||||
|
if (this.session.phase === "running" && !this.session.waitingReason) {
|
||||||
|
await this.runMainAgent();
|
||||||
|
return this.session;
|
||||||
|
}
|
||||||
|
throw new Error("当前没有可重试的执行");
|
||||||
|
}
|
||||||
|
|
||||||
/** 重 roll 指定 worker(刷新 Skill 回复) */
|
/** 重 roll 指定 worker(刷新 Skill 回复) */
|
||||||
async rerunWorker(workerId: string): Promise<RuntimeSession> {
|
async rerunWorker(workerId: string): Promise<RuntimeSession> {
|
||||||
const effect: Extract<PhaseEffect, { type: "run_worker" }> = {
|
const effect: Extract<PhaseEffect, { type: "run_worker" }> = {
|
||||||
@@ -575,15 +614,38 @@ export class PhaseRuntime {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 用户接受 pendingArtifact;创作单位验收 vs 终稿验收分流 */
|
/** 用户接受 pendingArtifact;创作单位验收 vs 终稿验收分流 */
|
||||||
async acceptArtifact(artifactId?: string): Promise<RuntimeSession> {
|
async acceptArtifact(
|
||||||
|
artifactId?: string,
|
||||||
|
opts?: { openingIndex?: number },
|
||||||
|
): Promise<RuntimeSession> {
|
||||||
const id = artifactId ?? this.session.pendingArtifactId;
|
const id = artifactId ?? this.session.pendingArtifactId;
|
||||||
if (!id) throw new Error("当前没有待验收的产物");
|
if (!id) throw new Error("当前没有待验收的产物");
|
||||||
const artifact = this.session.artifacts.find((a) => a.id === id);
|
const artifact = this.session.artifacts.find((a) => a.id === id);
|
||||||
|
if (artifact && opts?.openingIndex != null && Number.isFinite(opts.openingIndex)) {
|
||||||
|
this.session = {
|
||||||
|
...this.session,
|
||||||
|
slots: {
|
||||||
|
...this.session.slots,
|
||||||
|
[SLOT_OPENING_SELECTED_INDEX]: Math.trunc(opts.openingIndex),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const unitAccept = artifact ? isDesignUnitArtifact(artifact) : false;
|
||||||
|
// 细化终稿写「设计.worker集」,不是单位草稿,但仍须记入已验收步骤,
|
||||||
|
// 否则编排器会当成没做完再跑一遍。须在 dispatch 之前写入。
|
||||||
|
const shouldRecordStep = Boolean(
|
||||||
|
artifact && (unitAccept || artifact.workerId === "design-step"),
|
||||||
|
);
|
||||||
|
if (artifact && shouldRecordStep) {
|
||||||
|
const slots = { ...this.session.slots };
|
||||||
|
this.recordCreationUnitAccepted(slots, artifact);
|
||||||
|
this.session = { ...this.session, slots };
|
||||||
|
}
|
||||||
|
|
||||||
await this.dispatch({ type: "user_accepted_artifact", payload: { artifactId: id } });
|
await this.dispatch({ type: "user_accepted_artifact", payload: { artifactId: id } });
|
||||||
this.persistInstanceWorkerDeclaration();
|
this.persistInstanceWorkerDeclaration();
|
||||||
|
|
||||||
if (artifact) {
|
if (artifact) {
|
||||||
const unitAccept = isDesignUnitArtifact(artifact);
|
|
||||||
const result = compressAfterWorkerAccept({
|
const result = compressAfterWorkerAccept({
|
||||||
blackboard: this.blackboard,
|
blackboard: this.blackboard,
|
||||||
workerId: artifact.workerId,
|
workerId: artifact.workerId,
|
||||||
@@ -598,8 +660,7 @@ export class PhaseRuntime {
|
|||||||
lastCompressedWorkerId: artifact.workerId,
|
lastCompressedWorkerId: artifact.workerId,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (unitAccept) {
|
if (shouldRecordStep) {
|
||||||
this.recordCreationUnitAccepted(slots, artifact);
|
|
||||||
const unitId = String(slots[SLOT_CREATION_CURRENT_UNIT] ?? "").trim();
|
const unitId = String(slots[SLOT_CREATION_CURRENT_UNIT] ?? "").trim();
|
||||||
this.onMessage(
|
this.onMessage(
|
||||||
`[创作单位已验收] ${unitId || artifact.workerId}:${
|
`[创作单位已验收] ${unitId || artifact.workerId}:${
|
||||||
@@ -608,7 +669,8 @@ export class PhaseRuntime {
|
|||||||
);
|
);
|
||||||
slots[SLOT_CREATION_CURRENT_UNIT] = undefined;
|
slots[SLOT_CREATION_CURRENT_UNIT] = undefined;
|
||||||
slots[SLOT_CREATION_UNIT_ANCHOR_AT] = undefined;
|
slots[SLOT_CREATION_UNIT_ANCHOR_AT] = undefined;
|
||||||
} else {
|
}
|
||||||
|
if (!unitAccept) {
|
||||||
this.onMessage(
|
this.onMessage(
|
||||||
`[上下文已压缩] ${artifact.workerId}:保留 ${result.finals.map((f) => f.tag).join("、") || "(无终产物)"};归档 ${result.archivedTags.length} 个过程 tag。下一阶段以定稿为准。`,
|
`[上下文已压缩] ${artifact.workerId}:保留 ${result.finals.map((f) => f.tag).join("、") || "(无终产物)"};归档 ${result.archivedTags.length} 个过程 tag。下一阶段以定稿为准。`,
|
||||||
);
|
);
|
||||||
@@ -814,6 +876,15 @@ export class PhaseRuntime {
|
|||||||
case "propose_next_creation_step":
|
case "propose_next_creation_step":
|
||||||
await this.proposeNextCreationStep();
|
await this.proposeNextCreationStep();
|
||||||
break;
|
break;
|
||||||
|
case "seal_creation_opening":
|
||||||
|
this.sealCreationOpening();
|
||||||
|
break;
|
||||||
|
case "run_play_turn":
|
||||||
|
await this.startPlayTurn();
|
||||||
|
break;
|
||||||
|
case "continue_play_turn":
|
||||||
|
await this.continuePlayTurn();
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -822,6 +893,10 @@ export class PhaseRuntime {
|
|||||||
* 用户填完「下一步想写什么」后:有待执行步则提案并进入确认;否则交总管扩步/收口。
|
* 用户填完「下一步想写什么」后:有待执行步则提案并进入确认;否则交总管扩步/收口。
|
||||||
*/
|
*/
|
||||||
private async proposeNextCreationStep(): Promise<void> {
|
private async proposeNextCreationStep(): Promise<void> {
|
||||||
|
if (this.session.slots[SLOT_CREATION_SEALED_BY_OPENING]) {
|
||||||
|
this.onMessage(CREATION_SEALED_WAITING_MESSAGE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.syncSlotsToBlackboard(this.session);
|
this.syncSlotsToBlackboard(this.session);
|
||||||
const flow = parseCreationFlow(
|
const flow = parseCreationFlow(
|
||||||
this.blackboard.getContentByTag(CREATION_FLOW_TAG),
|
this.blackboard.getContentByTag(CREATION_FLOW_TAG),
|
||||||
@@ -853,6 +928,9 @@ export class PhaseRuntime {
|
|||||||
const active = this.getActiveSkill();
|
const active = this.getActiveSkill();
|
||||||
let paramSpecs: ModuleParamSpec[] = [];
|
let paramSpecs: ModuleParamSpec[] = [];
|
||||||
let declaration = "";
|
let declaration = "";
|
||||||
|
let auto = false;
|
||||||
|
let kind: ModuleCatalogEntry["kind"];
|
||||||
|
let pendingModule: ModuleCatalogEntry | null = null;
|
||||||
if (active?.name) {
|
if (active?.name) {
|
||||||
try {
|
try {
|
||||||
const skill = await loadSkill(active.name);
|
const skill = await loadSkill(active.name);
|
||||||
@@ -861,6 +939,9 @@ export class PhaseRuntime {
|
|||||||
const mod = catalog?.modules.find((m) => m.name === pending.name);
|
const mod = catalog?.modules.find((m) => m.name === pending.name);
|
||||||
paramSpecs = mod?.params ?? [];
|
paramSpecs = mod?.params ?? [];
|
||||||
declaration = mod?.declaration?.trim() || "";
|
declaration = mod?.declaration?.trim() || "";
|
||||||
|
auto = mod?.auto === true;
|
||||||
|
kind = mod?.kind;
|
||||||
|
pendingModule = mod ?? null;
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
/* catalog optional */
|
/* catalog optional */
|
||||||
@@ -870,13 +951,7 @@ export class PhaseRuntime {
|
|||||||
debugLog("step", `提案下一步 ${pending.id}(${pending.name})`);
|
debugLog("step", `提案下一步 ${pending.id}(${pending.name})`);
|
||||||
|
|
||||||
const params = pending.params ? { ...pending.params } : {};
|
const params = pending.params ? { ...pending.params } : {};
|
||||||
const paramsMissing = paramSpecs
|
const paramsMissing = missingRequiredStepParams(pending, pendingModule);
|
||||||
.filter((p) => p.required)
|
|
||||||
.map((p) => p.key)
|
|
||||||
.filter((key) => {
|
|
||||||
const v = params[key];
|
|
||||||
return v == null || (typeof v === "string" && !v.trim());
|
|
||||||
});
|
|
||||||
|
|
||||||
const intent =
|
const intent =
|
||||||
typeof this.session.slots["用户.下一步意向"] === "string"
|
typeof this.session.slots["用户.下一步意向"] === "string"
|
||||||
@@ -890,8 +965,9 @@ export class PhaseRuntime {
|
|||||||
: paramsMissing.length
|
: paramsMissing.length
|
||||||
? `;待钉参数:${paramsMissing.join("、")}`
|
? `;待钉参数:${paramsMissing.join("、")}`
|
||||||
: "";
|
: "";
|
||||||
|
const revise = isReviseStep(pending);
|
||||||
const reason = [
|
const reason = [
|
||||||
`下一步:${pending.name}`,
|
revise ? `下一步:回头修改 · ${pending.name}` : `下一步:${pending.name}`,
|
||||||
declaration ? `—— ${declaration}` : "",
|
declaration ? `—— ${declaration}` : "",
|
||||||
paramHint,
|
paramHint,
|
||||||
intent ? `(你的意向:${intent})` : "",
|
intent ? `(你的意向:${intent})` : "",
|
||||||
@@ -912,6 +988,9 @@ export class PhaseRuntime {
|
|||||||
hint: p.hint,
|
hint: p.hint,
|
||||||
})),
|
})),
|
||||||
intent: intent || undefined,
|
intent: intent || undefined,
|
||||||
|
...(kind ? { kind } : {}),
|
||||||
|
...(revise ? { mode: "revise" as const } : {}),
|
||||||
|
...(pending.revises ? { revises: pending.revises } : {}),
|
||||||
};
|
};
|
||||||
this.blackboard.write({
|
this.blackboard.write({
|
||||||
tag: CREATION_PROPOSED_STEP_TAG,
|
tag: CREATION_PROPOSED_STEP_TAG,
|
||||||
@@ -930,7 +1009,8 @@ export class PhaseRuntime {
|
|||||||
action: "run_worker",
|
action: "run_worker",
|
||||||
workerId: "design-step",
|
workerId: "design-step",
|
||||||
reason,
|
reason,
|
||||||
requiresApproval: true,
|
// 程序步(如投影排序)且参数已齐:跳过「同意并开始」,直接执行。
|
||||||
|
requiresApproval: !(auto && paramsMissing.length === 0),
|
||||||
});
|
});
|
||||||
await this.dispatch({
|
await this.dispatch({
|
||||||
type: "main_agent_decision_created",
|
type: "main_agent_decision_created",
|
||||||
@@ -938,6 +1018,81 @@ export class PhaseRuntime {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 选定开场白后:把候选落成 输出.开场白 / 初值表,关闭 DAG。
|
||||||
|
*/
|
||||||
|
private sealCreationOpening(): void {
|
||||||
|
const selectedRaw = this.session.slots[SLOT_OPENING_SELECTED_INDEX];
|
||||||
|
const selectedIndex =
|
||||||
|
typeof selectedRaw === "number"
|
||||||
|
? selectedRaw
|
||||||
|
: typeof selectedRaw === "string" && selectedRaw.trim()
|
||||||
|
? Number(selectedRaw)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
const fragmentRaw =
|
||||||
|
this.blackboard.getContentByTag(OPENING_SETUP_ARTIFACT_TAG) ?? "";
|
||||||
|
const payload = parseOpeningSealPayload(fragmentRaw, selectedIndex);
|
||||||
|
const existingOpening = this.blackboard.getContentByTag(OPENING_OUTPUT_TAG)?.trim();
|
||||||
|
const openingText = payload?.selectedText?.trim() || existingOpening || "";
|
||||||
|
|
||||||
|
if (openingText) {
|
||||||
|
this.writeWorkerTagContent(OPENING_OUTPUT_TAG, openingText, "opening-setup");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payload?.variables.length) {
|
||||||
|
const source = "worker:opening-setup";
|
||||||
|
const initialPatch = mergeOpeningTablePatch(
|
||||||
|
this.blackboard.getContentByTag(OPENING_INITIAL_VARS_TAG),
|
||||||
|
payload.variables,
|
||||||
|
source,
|
||||||
|
);
|
||||||
|
if (initialPatch) {
|
||||||
|
this.writeWorkerTagContent(OPENING_INITIAL_VARS_TAG, initialPatch, "opening-setup");
|
||||||
|
}
|
||||||
|
const currentPatch = mergeOpeningTablePatch(
|
||||||
|
this.blackboard.getContentByTag(OPENING_CURRENT_VARS_TAG),
|
||||||
|
payload.variables,
|
||||||
|
source,
|
||||||
|
);
|
||||||
|
if (currentPatch) {
|
||||||
|
this.writeWorkerTagContent(OPENING_CURRENT_VARS_TAG, currentPatch, "opening-setup");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const closed = closeCreationFlowRaw(
|
||||||
|
this.blackboard.getContentByTag(CREATION_FLOW_TAG),
|
||||||
|
);
|
||||||
|
if (closed) {
|
||||||
|
this.blackboard.write({
|
||||||
|
tag: CREATION_FLOW_TAG,
|
||||||
|
content: closed,
|
||||||
|
source: "runtime",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const n = payload?.candidates.length ?? (openingText ? 1 : 0);
|
||||||
|
const which =
|
||||||
|
n > 1 && payload
|
||||||
|
? `第 ${payload.selectedIndex + 1}/${n} 条`
|
||||||
|
: n
|
||||||
|
? "开场白"
|
||||||
|
: "开场";
|
||||||
|
this.onMessage(
|
||||||
|
`[创作收口] 已选定${which},工作流计划已关闭。可切换到「游玩」。`,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.session = {
|
||||||
|
...this.session,
|
||||||
|
slots: {
|
||||||
|
...this.session.slots,
|
||||||
|
[SLOT_CREATION_SEALED_BY_OPENING]: true,
|
||||||
|
[CREATION_FLOW_TAG]: closed ?? this.session.slots[CREATION_FLOW_TAG],
|
||||||
|
[OPENING_OUTPUT_TAG]: openingText || this.session.slots[OPENING_OUTPUT_TAG],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private applyProposedStepParams(params: CreationFlowStepParams): void {
|
private applyProposedStepParams(params: CreationFlowStepParams): void {
|
||||||
const raw =
|
const raw =
|
||||||
(typeof this.session.slots[CREATION_PROPOSED_STEP_TAG] === "string"
|
(typeof this.session.slots[CREATION_PROPOSED_STEP_TAG] === "string"
|
||||||
@@ -947,6 +1102,7 @@ export class PhaseRuntime {
|
|||||||
if (!raw?.trim()) return;
|
if (!raw?.trim()) return;
|
||||||
let snap: {
|
let snap: {
|
||||||
stepId?: string;
|
stepId?: string;
|
||||||
|
kind?: string;
|
||||||
paramSpecs?: Array<{ key: string; required?: boolean }>;
|
paramSpecs?: Array<{ key: string; required?: boolean }>;
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
@@ -967,7 +1123,10 @@ export class PhaseRuntime {
|
|||||||
content,
|
content,
|
||||||
source: "user",
|
source: "user",
|
||||||
});
|
});
|
||||||
const paramsMissing = (snap.paramSpecs ?? [])
|
const paramsMissing =
|
||||||
|
snap.kind === "prior-artifact"
|
||||||
|
? []
|
||||||
|
: (snap.paramSpecs ?? [])
|
||||||
.filter((p) => p.required)
|
.filter((p) => p.required)
|
||||||
.map((p) => p.key)
|
.map((p) => p.key)
|
||||||
.filter((key) => {
|
.filter((key) => {
|
||||||
@@ -1122,6 +1281,7 @@ export class PhaseRuntime {
|
|||||||
this.onMessage(
|
this.onMessage(
|
||||||
`[Worker] ${workerId} 执行中…${resolved.source === "declaration" ? "(声明驱动)" : ""}`,
|
`[Worker] ${workerId} 执行中…${resolved.source === "declaration" ? "(声明驱动)" : ""}`,
|
||||||
);
|
);
|
||||||
|
debugLog("step", `调用模型 ${workerId}`);
|
||||||
this.onWorkerStreamStart?.(workerId);
|
this.onWorkerStreamStart?.(workerId);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -1131,10 +1291,7 @@ export class PhaseRuntime {
|
|||||||
slots,
|
slots,
|
||||||
blackboard: this.blackboard,
|
blackboard: this.blackboard,
|
||||||
llm: workerLlm,
|
llm: workerLlm,
|
||||||
declared:
|
declared: { worker: resolved.worker, promptBody: resolved.promptBody },
|
||||||
resolved.source === "declaration"
|
|
||||||
? { worker: resolved.worker, promptBody: resolved.promptBody }
|
|
||||||
: undefined,
|
|
||||||
stream: {
|
stream: {
|
||||||
onThinkingDelta: (delta) =>
|
onThinkingDelta: (delta) =>
|
||||||
this.onWorkerThinkingDelta?.(workerId, delta),
|
this.onWorkerThinkingDelta?.(workerId, delta),
|
||||||
@@ -1193,8 +1350,13 @@ export class PhaseRuntime {
|
|||||||
});
|
});
|
||||||
this.session = { ...this.session, artifacts: [...this.session.artifacts, artifact] };
|
this.session = { ...this.session, artifacts: [...this.session.artifacts, artifact] };
|
||||||
|
|
||||||
|
const hasPlayVisible = Object.keys(result.outputs).some(
|
||||||
|
(t) => t === "输出.用户展示" || t === "输出.开场白",
|
||||||
|
);
|
||||||
this.onMessage(
|
this.onMessage(
|
||||||
`[Worker] ${workerId} 已完成\n\n${result.preview}${result.preview.length >= 4000 ? "\n\n…" : ""}`,
|
`[Worker] ${workerId} 已完成\n\n${result.preview}${
|
||||||
|
!hasPlayVisible && result.preview.length >= 4000 ? "\n\n…" : ""
|
||||||
|
}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
await this.dispatch({
|
await this.dispatch({
|
||||||
@@ -1907,8 +2069,59 @@ export class PhaseRuntime {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 游玩:按 playWorkerIds 开一轮 */
|
||||||
|
private async startPlayTurn(): Promise<void> {
|
||||||
|
this.syncSlotsToBlackboard(this.session);
|
||||||
|
const ids = this.getInstanceWorkerDeclaration().playWorkerIds;
|
||||||
|
if (!ids.length) {
|
||||||
|
this.onMessage(
|
||||||
|
"[游玩] 运行规格里没有可上场的执行单元,无法推进回合。",
|
||||||
|
);
|
||||||
|
this.waitForPlayInput();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const [first, ...rest] = ids;
|
||||||
|
this.session = {
|
||||||
|
...this.session,
|
||||||
|
slots: withPlayTurnQueue(this.session.slots, rest),
|
||||||
|
};
|
||||||
|
await this.processEffects([{ type: "run_worker", workerId: first }]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 游玩:跑管线下一个;队空则等用户 */
|
||||||
|
private async continuePlayTurn(): Promise<void> {
|
||||||
|
if (this.session.phase === "waiting_user") return;
|
||||||
|
const queue = readPlayTurnQueue(this.session.slots);
|
||||||
|
if (!queue.length) {
|
||||||
|
this.waitForPlayInput();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const [next, ...rest] = queue;
|
||||||
|
this.session = {
|
||||||
|
...this.session,
|
||||||
|
slots: withPlayTurnQueue(this.session.slots, rest),
|
||||||
|
};
|
||||||
|
await this.processEffects([{ type: "run_worker", workerId: next }]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private waitForPlayInput(): void {
|
||||||
|
this.session = {
|
||||||
|
...this.session,
|
||||||
|
phase: "waiting_user",
|
||||||
|
waitingReason: { kind: "input" },
|
||||||
|
currentWorkerId: undefined,
|
||||||
|
pendingArtifactId: undefined,
|
||||||
|
pendingDecision: undefined,
|
||||||
|
resumeContext: undefined,
|
||||||
|
slots: withPlayTurnQueue(this.session.slots, undefined),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/** invoke_main_agent 副作用:running 时调用总管 LLM,链式推进直到需用户介入 */
|
/** invoke_main_agent 副作用:running 时调用总管 LLM,链式推进直到需用户介入 */
|
||||||
private async maybeRunMainAgent(effects: PhaseEffect[]): Promise<void> {
|
private async maybeRunMainAgent(effects: PhaseEffect[]): Promise<void> {
|
||||||
|
if (inferLifecycleStage(this.session) === "play") return;
|
||||||
|
if (this.session.slots[SLOT_CREATION_SEALED_BY_OPENING]) return;
|
||||||
if (this.mainAgent && effects.some((e) => e.type === "invoke_main_agent")) {
|
if (this.mainAgent && effects.some((e) => e.type === "invoke_main_agent")) {
|
||||||
await this.runMainAgent();
|
await this.runMainAgent();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -399,7 +399,10 @@ export function buildFocus(
|
|||||||
return {
|
return {
|
||||||
actorType: "user",
|
actorType: "user",
|
||||||
actorLabel: "你",
|
actorLabel: "你",
|
||||||
action: "补充说明",
|
action:
|
||||||
|
reason.message && /创作已收口/.test(reason.message)
|
||||||
|
? "创作已收口"
|
||||||
|
: "补充说明",
|
||||||
detail: reason.message?.slice(0, 120),
|
detail: reason.message?.slice(0, 120),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -412,6 +415,7 @@ export function buildFocus(
|
|||||||
try {
|
try {
|
||||||
return JSON.parse(String(session.slots["创作.待确认步骤"])) as {
|
return JSON.parse(String(session.slots["创作.待确认步骤"])) as {
|
||||||
name?: string;
|
name?: string;
|
||||||
|
mode?: string;
|
||||||
};
|
};
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
@@ -423,7 +427,9 @@ export function buildFocus(
|
|||||||
actorId: "orchestrator",
|
actorId: "orchestrator",
|
||||||
actorLabel: "编排",
|
actorLabel: "编排",
|
||||||
action: proposed?.name
|
action: proposed?.name
|
||||||
? `确认开始 · ${proposed.name}`
|
? proposed.mode === "revise"
|
||||||
|
? `回头修改 · ${proposed.name}`
|
||||||
|
: `确认开始 · ${proposed.name}`
|
||||||
: `建议调用 ${displayWorkerLabel(worker)}`,
|
: `建议调用 ${displayWorkerLabel(worker)}`,
|
||||||
detail: session.pendingDecision?.reason,
|
detail: session.pendingDecision?.reason,
|
||||||
};
|
};
|
||||||
@@ -467,6 +473,7 @@ export function buildFocus(
|
|||||||
const art = session.artifacts.find((a) => a.id === session.pendingArtifactId);
|
const art = session.artifacts.find((a) => a.id === session.pendingArtifactId);
|
||||||
const copy = reviewComposerCopy(art?.workerId, {
|
const copy = reviewComposerCopy(art?.workerId, {
|
||||||
hasQuestions: Boolean(reason.questions?.length),
|
hasQuestions: Boolean(reason.questions?.length),
|
||||||
|
outputTags: art?.outputTags,
|
||||||
});
|
});
|
||||||
const optionalQs = reason.questions?.length
|
const optionalQs = reason.questions?.length
|
||||||
? `;另有 ${reason.questions.length} 道可选追问`
|
? `;另有 ${reason.questions.length} 道可选追问`
|
||||||
|
|||||||
@@ -146,10 +146,13 @@ export type ReviewComposerCopy = {
|
|||||||
*/
|
*/
|
||||||
export function reviewComposerCopy(
|
export function reviewComposerCopy(
|
||||||
workerId: string | undefined | null,
|
workerId: string | undefined | null,
|
||||||
opts?: { hasQuestions?: boolean },
|
opts?: { hasQuestions?: boolean; outputTags?: readonly string[] },
|
||||||
): ReviewComposerCopy {
|
): ReviewComposerCopy {
|
||||||
const hasQuestions = Boolean(opts?.hasQuestions);
|
const hasQuestions = Boolean(opts?.hasQuestions);
|
||||||
const id = (workerId ?? "").trim();
|
const id = (workerId ?? "").trim();
|
||||||
|
const openingReview =
|
||||||
|
id === "opening-generator" ||
|
||||||
|
(opts?.outputTags ?? []).includes("设计.开场白与开场变量");
|
||||||
|
|
||||||
if (id === "design-flow") {
|
if (id === "design-flow") {
|
||||||
return {
|
return {
|
||||||
@@ -169,7 +172,7 @@ export function reviewComposerCopy(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (id === "opening-generator") {
|
if (id === "opening-generator" || openingReview) {
|
||||||
return {
|
return {
|
||||||
chip: "开场",
|
chip: "开场",
|
||||||
kicker: "待选定",
|
kicker: "待选定",
|
||||||
|
|||||||
+490
-28
@@ -10,6 +10,22 @@ import {
|
|||||||
import type { RunSnapshot, RunSnapshotMeta, SnapshotKind } from "../types/run-snapshot.js";
|
import type { RunSnapshot, RunSnapshotMeta, SnapshotKind } from "../types/run-snapshot.js";
|
||||||
import { toRunSnapshotMeta } from "../types/run-snapshot.js";
|
import { toRunSnapshotMeta } from "../types/run-snapshot.js";
|
||||||
import { materializeInstanceSnapshotPayload } from "../book/snapshot-filters.js";
|
import { materializeInstanceSnapshotPayload } from "../book/snapshot-filters.js";
|
||||||
|
import {
|
||||||
|
CREATION_SEALED_WAITING_MESSAGE,
|
||||||
|
INSTANCE_OPENING_SNAPSHOT_LABEL,
|
||||||
|
OPENING_OUTPUT_TAG,
|
||||||
|
SLOT_CREATION_SEALED_BY_OPENING,
|
||||||
|
} from "../skills/opening-seal.js";
|
||||||
|
import {
|
||||||
|
PLAY_WORKING_SNAPSHOT_ID,
|
||||||
|
PLAY_WORKING_SNAPSHOT_LABEL,
|
||||||
|
SLOT_PLAY_LAYER_ACTIVE,
|
||||||
|
isPlayLayerActive,
|
||||||
|
} from "../skills/play-turn.js";
|
||||||
|
import {
|
||||||
|
DIALOGUE_HISTORY_TAG,
|
||||||
|
appendDialogueHistoryTurn,
|
||||||
|
} from "../skills/dialogue-history.js";
|
||||||
import { PhaseRuntime, createDecision } from "../runtime/phase-runtime.js";
|
import { PhaseRuntime, createDecision } from "../runtime/phase-runtime.js";
|
||||||
import { createSession } from "../runtime/phase-machine.js";
|
import { createSession } from "../runtime/phase-machine.js";
|
||||||
import {
|
import {
|
||||||
@@ -18,6 +34,7 @@ import {
|
|||||||
reloadDefaultMainAgentLlm,
|
reloadDefaultMainAgentLlm,
|
||||||
type LlmTrackingRef,
|
type LlmTrackingRef,
|
||||||
} from "../runtime/llm-factory.js";
|
} from "../runtime/llm-factory.js";
|
||||||
|
import { isAbortError, runWithAbortSignal } from "../llm/run-abort.js";
|
||||||
import { getSessionTokenTotals, type MessageTokenUsage } from "../stats/token-store.js";
|
import { getSessionTokenTotals, type MessageTokenUsage } from "../stats/token-store.js";
|
||||||
import type { PersistedBookSession } from "../types/book-session.js";
|
import type { PersistedBookSession } from "../types/book-session.js";
|
||||||
import type { RuntimeSession, WaitingReason } from "../types/runtime.js";
|
import type { RuntimeSession, WaitingReason } from "../types/runtime.js";
|
||||||
@@ -179,6 +196,8 @@ export type SessionView = {
|
|||||||
/** @deprecated 用 skillCatalog */
|
/** @deprecated 用 skillCatalog */
|
||||||
pipeline: StageStep[];
|
pipeline: StageStep[];
|
||||||
lifecycleStage: LifecycleStage;
|
lifecycleStage: LifecycleStage;
|
||||||
|
/** 已从定稿开出聊天层(不是只把顶栏标成游玩) */
|
||||||
|
playLayerActive: boolean;
|
||||||
playReady: boolean;
|
playReady: boolean;
|
||||||
skillCatalog: SkillCatalogEntry[];
|
skillCatalog: SkillCatalogEntry[];
|
||||||
toolTrace: ToolTraceEntry[];
|
toolTrace: ToolTraceEntry[];
|
||||||
@@ -271,7 +290,13 @@ export type ProposedNextStepView = {
|
|||||||
required: boolean;
|
required: boolean;
|
||||||
hint?: string;
|
hint?: string;
|
||||||
}>;
|
}>;
|
||||||
|
/** 节点特性:prior-artifact = 先验产物(不在确认开干时钉「写什么」) */
|
||||||
|
kind?: string;
|
||||||
|
/** 用户在「下一步想写什么」里写的意向(与步骤 mode 不同) */
|
||||||
intent?: string;
|
intent?: string;
|
||||||
|
/** 步骤怎么跑:revise = 回头修改既有产物 */
|
||||||
|
mode?: "fresh" | "revise";
|
||||||
|
revises?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ReviewArtifactView = {
|
export type ReviewArtifactView = {
|
||||||
@@ -279,6 +304,7 @@ export type ReviewArtifactView = {
|
|||||||
workerId: string;
|
workerId: string;
|
||||||
summary?: string;
|
summary?: string;
|
||||||
body: string;
|
body: string;
|
||||||
|
outputTags?: string[];
|
||||||
/** 产出该产物的 worker_output 消息(验收态下 feed 会隐藏该消息) */
|
/** 产出该产物的 worker_output 消息(验收态下 feed 会隐藏该消息) */
|
||||||
sourceMessageId?: string;
|
sourceMessageId?: string;
|
||||||
/** 与 sourceMessage 同源的全量 LLM 上下文,供待验收卡片右键查看 */
|
/** 与 sourceMessage 同源的全量 LLM 上下文,供待验收卡片右键查看 */
|
||||||
@@ -301,6 +327,12 @@ type ManagedSession = {
|
|||||||
recipeOptions?: RecipeCatalogEntry[];
|
recipeOptions?: RecipeCatalogEntry[];
|
||||||
/** 开局模块 opening,缓存给意图页 */
|
/** 开局模块 opening,缓存给意图页 */
|
||||||
openingGuide?: { stepId: string; stepName: string; text: string } | null;
|
openingGuide?: { stepId: string; stepName: string; text: string } | null;
|
||||||
|
/** 当前 LLM 请求的取消器(停止并重试) */
|
||||||
|
runAbort?: AbortController;
|
||||||
|
/** 递增世代:后一次请求作废前一次 */
|
||||||
|
runGen?: number;
|
||||||
|
/** 本轮开始时的消息条数,取消时裁掉执行中产生的调度句 */
|
||||||
|
runMessageCutoff?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class SessionManager {
|
export class SessionManager {
|
||||||
@@ -327,7 +359,11 @@ export class SessionManager {
|
|||||||
if (stage === "play" && !canEnterPlay(session)) {
|
if (stage === "play" && !canEnterPlay(session)) {
|
||||||
throw new Error("实例尚未就绪,无法进入游玩");
|
throw new Error("实例尚未就绪,无法进入游玩");
|
||||||
}
|
}
|
||||||
s.runtime.setLifecycleStage(stage);
|
if (stage === "play") {
|
||||||
|
this.enterPlayLayer(s, { resume: true });
|
||||||
|
} else {
|
||||||
|
this.enterDesignLayer(s);
|
||||||
|
}
|
||||||
if (s.bookId) this.persist(s);
|
if (s.bookId) this.persist(s);
|
||||||
return this.toView(sessionId);
|
return this.toView(sessionId);
|
||||||
}
|
}
|
||||||
@@ -582,10 +618,10 @@ export class SessionManager {
|
|||||||
return toRunSnapshotMeta(snapshot);
|
return toRunSnapshotMeta(snapshot);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 列出某 Book 的全部存档 */
|
/** 列出某 Book 的全部存档(不含内部「当前游玩」工作副本) */
|
||||||
listGameSnapshots(bookId: string): RunSnapshotMeta[] {
|
listGameSnapshots(bookId: string): RunSnapshotMeta[] {
|
||||||
if (!getBook(bookId)) throw new Error("Book 不存在");
|
if (!getBook(bookId)) throw new Error("Book 不存在");
|
||||||
return listRunSnapshots(bookId);
|
return listRunSnapshots(bookId).filter((s) => s.id !== PLAY_WORKING_SNAPSHOT_ID);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 仅游玩 run 存档(UI 抽屉用) */
|
/** 仅游玩 run 存档(UI 抽屉用) */
|
||||||
@@ -604,7 +640,7 @@ export class SessionManager {
|
|||||||
if (!canEnterPlay(session)) {
|
if (!canEnterPlay(session)) {
|
||||||
throw new Error("须先验收 Worker 集,才能保存游玩存档");
|
throw new Error("须先验收 Worker 集,才能保存游玩存档");
|
||||||
}
|
}
|
||||||
if (inferLifecycleStage(session) !== "play") {
|
if (inferLifecycleStage(session) !== "play" || !isPlayLayerActive(session.slots)) {
|
||||||
throw new Error("请先切换到「游玩」再保存游玩存档");
|
throw new Error("请先切换到「游玩」再保存游玩存档");
|
||||||
}
|
}
|
||||||
return this.saveGameSnapshot(sessionId, label, "run", note);
|
return this.saveGameSnapshot(sessionId, label, "run", note);
|
||||||
@@ -618,16 +654,97 @@ export class SessionManager {
|
|||||||
if (!canEnterPlay(session)) {
|
if (!canEnterPlay(session)) {
|
||||||
throw new Error("须先验收 Worker 集,才能开始游玩");
|
throw new Error("须先验收 Worker 集,才能开始游玩");
|
||||||
}
|
}
|
||||||
|
this.archivePlayWorking(s.bookId);
|
||||||
|
this.enterPlayLayer(s, { resume: false });
|
||||||
|
this.persist(s);
|
||||||
|
return this.toView(sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
let runtimeSession = structuredClone(session);
|
/** 进入游玩层:resume 时恢复「当前游玩」,否则从定稿新开一条聊天 */
|
||||||
|
private enterPlayLayer(
|
||||||
|
s: ManagedSession,
|
||||||
|
opts: { resume: boolean },
|
||||||
|
): void {
|
||||||
|
const session = s.runtime.getSession();
|
||||||
|
if (!canEnterPlay(session)) {
|
||||||
|
throw new Error("实例尚未就绪,无法进入游玩");
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
opts.resume &&
|
||||||
|
isPlayLayerActive(session.slots) &&
|
||||||
|
inferLifecycleStage(session) === "play"
|
||||||
|
) {
|
||||||
|
this.sanitizePlayHitl(s);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (s.bookId) this.ensureInstanceSnapshot(s);
|
||||||
|
if (opts.resume && s.bookId) {
|
||||||
|
const working = loadRunSnapshotFile(s.bookId, PLAY_WORKING_SNAPSHOT_ID);
|
||||||
|
if (working?.kind === "run") {
|
||||||
|
this.restoreSnapshotInPlace(s, working, "play");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.beginNewPlayFromInstance(s);
|
||||||
|
if (s.bookId) this.savePlayWorking(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 回到创作层:先把当前聊天写入「当前游玩」,再加载定稿 */
|
||||||
|
private enterDesignLayer(s: ManagedSession): void {
|
||||||
|
const session = s.runtime.getSession();
|
||||||
|
if (
|
||||||
|
inferLifecycleStage(session) === "design" &&
|
||||||
|
!isPlayLayerActive(session.slots)
|
||||||
|
) {
|
||||||
|
s.runtime.setLifecycleStage("design");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (s.bookId && isPlayLayerActive(session.slots)) {
|
||||||
|
this.savePlayWorking(s);
|
||||||
|
}
|
||||||
|
const instance = s.bookId ? this.latestInstanceSnapshot(s.bookId) : null;
|
||||||
|
if (instance) {
|
||||||
|
this.restoreSnapshotInPlace(s, instance, "design");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let runtimeSession = structuredClone(s.runtime.getSession());
|
||||||
let blackboardItems = s.runtime.getBlackboard().exportItems();
|
let blackboardItems = s.runtime.getBlackboard().exportItems();
|
||||||
({ runtimeSession, blackboardItems } = materializeInstanceSnapshotPayload({
|
({ runtimeSession, blackboardItems } = materializeInstanceSnapshotPayload({
|
||||||
runtimeSession,
|
runtimeSession,
|
||||||
blackboardItems,
|
blackboardItems,
|
||||||
}));
|
}));
|
||||||
|
runtimeSession.id = session.id;
|
||||||
|
runtimeSession.slots = {
|
||||||
|
...runtimeSession.slots,
|
||||||
|
uiLifecycleStage: "design",
|
||||||
|
[SLOT_PLAY_LAYER_ACTIVE]: undefined,
|
||||||
|
};
|
||||||
|
runtimeSession.phase = "waiting_user";
|
||||||
|
runtimeSession.waitingReason = { kind: "input" };
|
||||||
|
s.runtime.restoreFromCheckpoint(runtimeSession, blackboardItems);
|
||||||
|
}
|
||||||
|
|
||||||
|
private beginNewPlayFromInstance(s: ManagedSession): void {
|
||||||
|
const keepId = s.runtime.getSession().id;
|
||||||
|
let runtimeSession: RuntimeSession;
|
||||||
|
let blackboardItems: import("../types/blackboard.js").BlackboardItem[];
|
||||||
|
const instance = s.bookId ? this.latestInstanceSnapshot(s.bookId) : null;
|
||||||
|
if (instance) {
|
||||||
|
runtimeSession = structuredClone(instance.runtimeSession);
|
||||||
|
blackboardItems = structuredClone(instance.blackboardItems);
|
||||||
|
} else {
|
||||||
|
runtimeSession = structuredClone(s.runtime.getSession());
|
||||||
|
blackboardItems = s.runtime.getBlackboard().exportItems();
|
||||||
|
}
|
||||||
|
({ runtimeSession, blackboardItems } = materializeInstanceSnapshotPayload({
|
||||||
|
runtimeSession,
|
||||||
|
blackboardItems,
|
||||||
|
}));
|
||||||
|
runtimeSession.id = keepId;
|
||||||
runtimeSession.slots = {
|
runtimeSession.slots = {
|
||||||
...runtimeSession.slots,
|
...runtimeSession.slots,
|
||||||
uiLifecycleStage: "play",
|
uiLifecycleStage: "play",
|
||||||
|
[SLOT_PLAY_LAYER_ACTIVE]: true,
|
||||||
startupCompleted: true,
|
startupCompleted: true,
|
||||||
};
|
};
|
||||||
runtimeSession.phase = "waiting_user";
|
runtimeSession.phase = "waiting_user";
|
||||||
@@ -636,10 +753,166 @@ export class SessionManager {
|
|||||||
runtimeSession.pendingDecision = undefined;
|
runtimeSession.pendingDecision = undefined;
|
||||||
runtimeSession.currentWorkerId = undefined;
|
runtimeSession.currentWorkerId = undefined;
|
||||||
runtimeSession.resumeContext = undefined;
|
runtimeSession.resumeContext = undefined;
|
||||||
|
|
||||||
s.runtime.restoreFromCheckpoint(runtimeSession, blackboardItems);
|
s.runtime.restoreFromCheckpoint(runtimeSession, blackboardItems);
|
||||||
this.persist(s);
|
const opening =
|
||||||
return this.toView(sessionId);
|
s.runtime.getBlackboard().getContentByTag(OPENING_OUTPUT_TAG)?.trim() ?? "";
|
||||||
|
if (opening) {
|
||||||
|
s.runtime.getBlackboard().write({
|
||||||
|
tag: DIALOGUE_HISTORY_TAG,
|
||||||
|
content: appendDialogueHistoryTurn("", { role: "助手", text: opening }),
|
||||||
|
source: "runtime",
|
||||||
|
});
|
||||||
|
s.messages = [this.playOpeningMessage(opening)];
|
||||||
|
} else {
|
||||||
|
s.messages = [];
|
||||||
|
}
|
||||||
|
s.branchState = createMessageBranchState();
|
||||||
|
}
|
||||||
|
|
||||||
|
private restoreSnapshotInPlace(
|
||||||
|
s: ManagedSession,
|
||||||
|
snapshot: RunSnapshot,
|
||||||
|
layer: LifecycleStage,
|
||||||
|
): void {
|
||||||
|
const keepId = s.runtime.getSession().id;
|
||||||
|
let runtimeSession = structuredClone(snapshot.runtimeSession);
|
||||||
|
let blackboardItems = structuredClone(snapshot.blackboardItems);
|
||||||
|
if (snapshot.kind === "instance" || layer === "design") {
|
||||||
|
({ runtimeSession, blackboardItems } = materializeInstanceSnapshotPayload({
|
||||||
|
runtimeSession,
|
||||||
|
blackboardItems,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
runtimeSession.id = keepId;
|
||||||
|
runtimeSession.slots = {
|
||||||
|
...runtimeSession.slots,
|
||||||
|
uiLifecycleStage: layer,
|
||||||
|
[SLOT_PLAY_LAYER_ACTIVE]: layer === "play" ? true : undefined,
|
||||||
|
startupCompleted: true,
|
||||||
|
};
|
||||||
|
if (layer === "play") {
|
||||||
|
runtimeSession.phase = runtimeSession.phase === "done"
|
||||||
|
? "waiting_user"
|
||||||
|
: runtimeSession.phase;
|
||||||
|
if (!runtimeSession.waitingReason) {
|
||||||
|
runtimeSession.waitingReason = { kind: "input" };
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const sealed = Boolean(runtimeSession.slots[SLOT_CREATION_SEALED_BY_OPENING]);
|
||||||
|
runtimeSession.phase = "waiting_user";
|
||||||
|
runtimeSession.waitingReason = {
|
||||||
|
kind: "input",
|
||||||
|
...(sealed ? { message: CREATION_SEALED_WAITING_MESSAGE } : {}),
|
||||||
|
};
|
||||||
|
runtimeSession.pendingArtifactId = undefined;
|
||||||
|
runtimeSession.pendingDecision = undefined;
|
||||||
|
runtimeSession.currentWorkerId = undefined;
|
||||||
|
}
|
||||||
|
s.runtime.restoreFromCheckpoint(runtimeSession, blackboardItems);
|
||||||
|
s.messages = snapshot.messages.map((m) => ({ ...m })) as ChatMessage[];
|
||||||
|
s.branchState = createMessageBranchState();
|
||||||
|
if (layer === "play") this.sanitizePlayHitl(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 游玩层不保留创作 HITL(验收 / 确认下一步 / 修订) */
|
||||||
|
private sanitizePlayHitl(s: ManagedSession): void {
|
||||||
|
const session = s.runtime.getSession();
|
||||||
|
const play =
|
||||||
|
isPlayLayerActive(session.slots) || inferLifecycleStage(session) === "play";
|
||||||
|
if (!play) return;
|
||||||
|
const kind = session.waitingReason?.kind;
|
||||||
|
if (
|
||||||
|
kind !== "review_artifact" &&
|
||||||
|
kind !== "approve_step" &&
|
||||||
|
kind !== "revision" &&
|
||||||
|
kind !== "next_intent"
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
session.phase = "waiting_user";
|
||||||
|
session.waitingReason = { kind: "input" };
|
||||||
|
session.pendingArtifactId = undefined;
|
||||||
|
session.pendingDecision = undefined;
|
||||||
|
session.currentWorkerId = undefined;
|
||||||
|
session.resumeContext = undefined;
|
||||||
|
session.updatedAt = new Date().toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private savePlayWorking(s: ManagedSession): void {
|
||||||
|
if (!s.bookId) return;
|
||||||
|
const book = getBook(s.bookId);
|
||||||
|
const runtimeSession = structuredClone(s.runtime.getSession());
|
||||||
|
const snapshot: RunSnapshot = {
|
||||||
|
version: 1,
|
||||||
|
id: PLAY_WORKING_SNAPSHOT_ID,
|
||||||
|
bookId: s.bookId,
|
||||||
|
label: PLAY_WORKING_SNAPSHOT_LABEL,
|
||||||
|
kind: "run",
|
||||||
|
orchestratorId:
|
||||||
|
sessionSkillPackId(runtimeSession) ??
|
||||||
|
(book ? bookSkillPackId(book) : "") ??
|
||||||
|
"",
|
||||||
|
runtimeSession,
|
||||||
|
blackboardItems: s.runtime.getBlackboard().exportItems(),
|
||||||
|
messages: s.messages.map((m) => ({ ...m })),
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
note: "切换创作/游玩时自动保存的当前聊天",
|
||||||
|
};
|
||||||
|
saveRunSnapshotFile(snapshot);
|
||||||
|
}
|
||||||
|
|
||||||
|
private archivePlayWorking(bookId: string): void {
|
||||||
|
const working = loadRunSnapshotFile(bookId, PLAY_WORKING_SNAPSHOT_ID);
|
||||||
|
if (!working) return;
|
||||||
|
if (!working.messages.some((m) => m.role === "user")) return;
|
||||||
|
const stamped: RunSnapshot = {
|
||||||
|
...working,
|
||||||
|
id: randomUUID(),
|
||||||
|
label: `游玩 ${new Date().toLocaleString("zh-CN", {
|
||||||
|
month: "numeric",
|
||||||
|
day: "numeric",
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
})}`,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
note: "新建游玩前自动保存",
|
||||||
|
};
|
||||||
|
saveRunSnapshotFile(stamped);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ensureInstanceSnapshot(s: ManagedSession): void {
|
||||||
|
if (!s.bookId) return;
|
||||||
|
if (this.latestInstanceSnapshot(s.bookId)) return;
|
||||||
|
if (!canEnterPlay(s.runtime.getSession())) return;
|
||||||
|
try {
|
||||||
|
this.saveGameSnapshot(
|
||||||
|
s.runtime.getSession().id,
|
||||||
|
INSTANCE_OPENING_SNAPSHOT_LABEL,
|
||||||
|
"instance",
|
||||||
|
"进入游玩前自动保存定稿",
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
console.warn("[会话] 进入游玩前保存定稿失败", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private latestInstanceSnapshot(bookId: string): RunSnapshot | null {
|
||||||
|
const meta = this.listGameSnapshots(bookId).find((s) => s.kind === "instance");
|
||||||
|
if (!meta) return null;
|
||||||
|
return loadRunSnapshotFile(bookId, meta.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private playOpeningMessage(text: string): ChatMessage {
|
||||||
|
return {
|
||||||
|
id: randomUUID(),
|
||||||
|
role: "system",
|
||||||
|
text,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
kind: "system_info",
|
||||||
|
actor: "narrator",
|
||||||
|
title: "开场白",
|
||||||
|
body: text,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 从存档读档:替换当前作品进度,可继续创作 */
|
/** 从存档读档:替换当前作品进度,可继续创作 */
|
||||||
@@ -665,10 +938,16 @@ export class SessionManager {
|
|||||||
runtimeSession,
|
runtimeSession,
|
||||||
blackboardItems,
|
blackboardItems,
|
||||||
}));
|
}));
|
||||||
|
runtimeSession.slots = {
|
||||||
|
...runtimeSession.slots,
|
||||||
|
uiLifecycleStage: "design",
|
||||||
|
[SLOT_PLAY_LAYER_ACTIVE]: undefined,
|
||||||
|
};
|
||||||
} else {
|
} else {
|
||||||
runtimeSession.slots = {
|
runtimeSession.slots = {
|
||||||
...runtimeSession.slots,
|
...runtimeSession.slots,
|
||||||
uiLifecycleStage: "play",
|
uiLifecycleStage: "play",
|
||||||
|
[SLOT_PLAY_LAYER_ACTIVE]: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -725,6 +1004,16 @@ export class SessionManager {
|
|||||||
opts?: { answers?: QuestionAnswer[] },
|
opts?: { answers?: QuestionAnswer[] },
|
||||||
): Promise<SessionView> {
|
): Promise<SessionView> {
|
||||||
const s = this.require(id);
|
const s = this.require(id);
|
||||||
|
this.sanitizePlayHitl(s);
|
||||||
|
const waitingBefore = s.runtime.getSession().waitingReason;
|
||||||
|
if (
|
||||||
|
inferLifecycleStage(s.runtime.getSession()) === "play" &&
|
||||||
|
!isPlayLayerActive(s.runtime.getSession().slots) &&
|
||||||
|
waitingBefore?.kind === "input"
|
||||||
|
) {
|
||||||
|
this.enterPlayLayer(s, { resume: true });
|
||||||
|
if (s.bookId) this.persist(s);
|
||||||
|
}
|
||||||
this.clearAgentThinking(id);
|
this.clearAgentThinking(id);
|
||||||
recordPreMessageCheckpoint(
|
recordPreMessageCheckpoint(
|
||||||
s.branchState,
|
s.branchState,
|
||||||
@@ -764,6 +1053,7 @@ export class SessionManager {
|
|||||||
if (s.bookId) this.syncBookPreview(s.bookId, s.messages);
|
if (s.bookId) this.syncBookPreview(s.bookId, s.messages);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
const owned = await this.runExclusive(id, s, async () => {
|
||||||
if (reason?.kind === "approve_step") {
|
if (reason?.kind === "approve_step") {
|
||||||
await s.runtime.rejectStep(trimmed || text);
|
await s.runtime.rejectStep(trimmed || text);
|
||||||
} else if (reason?.kind === "review_artifact") {
|
} else if (reason?.kind === "review_artifact") {
|
||||||
@@ -784,15 +1074,15 @@ export class SessionManager {
|
|||||||
} else {
|
} else {
|
||||||
await s.runtime.submitInput(trimmed || text);
|
await s.runtime.submitInput(trimmed || text);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
if (!owned) return this.toView(id);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const detail = err instanceof Error ? err.message : String(err);
|
const detail = err instanceof Error ? err.message : String(err);
|
||||||
console.error("[会话] 发送消息失败", detail);
|
console.error("[会话] 发送消息失败", detail);
|
||||||
s.messages.push(this.msg("system", formatRuntimeError(detail)));
|
s.messages.push(this.msg("system", formatRuntimeError(detail)));
|
||||||
if (s.bookId) this.syncBookPreview(s.bookId, s.messages);
|
if (s.bookId) this.syncBookPreview(s.bookId, s.messages);
|
||||||
} finally {
|
|
||||||
this.clearAgentThinking(id);
|
|
||||||
this.syncCreationDialogue(s);
|
|
||||||
}
|
}
|
||||||
|
this.syncCreationDialogue(s);
|
||||||
this.persist(s);
|
this.persist(s);
|
||||||
return this.toView(id);
|
return this.toView(id);
|
||||||
}
|
}
|
||||||
@@ -838,6 +1128,7 @@ export class SessionManager {
|
|||||||
if (s.bookId) this.syncBookPreview(s.bookId, s.messages);
|
if (s.bookId) this.syncBookPreview(s.bookId, s.messages);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const owned = await this.runExclusive(id, s, async () => {
|
||||||
if (isSidecar) {
|
if (isSidecar) {
|
||||||
await s.runtime.resolveSidecarQuestions(aiText);
|
await s.runtime.resolveSidecarQuestions(aiText);
|
||||||
// 作答必须写回产物,否则验收后进度空转、追问在美学/交互间打转
|
// 作答必须写回产物,否则验收后进度空转、追问在美学/交互间打转
|
||||||
@@ -845,15 +1136,15 @@ export class SessionManager {
|
|||||||
} else {
|
} else {
|
||||||
await s.runtime.submitInput(aiText);
|
await s.runtime.submitInput(aiText);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
if (!owned) return this.toView(id);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const detail = err instanceof Error ? err.message : String(err);
|
const detail = err instanceof Error ? err.message : String(err);
|
||||||
console.error("[会话] 回答追问失败", detail);
|
console.error("[会话] 回答追问失败", detail);
|
||||||
s.messages.push(this.msg("system", formatRuntimeError(detail)));
|
s.messages.push(this.msg("system", formatRuntimeError(detail)));
|
||||||
if (s.bookId) this.syncBookPreview(s.bookId, s.messages);
|
if (s.bookId) this.syncBookPreview(s.bookId, s.messages);
|
||||||
} finally {
|
|
||||||
this.clearAgentThinking(id);
|
|
||||||
this.syncCreationDialogue(s);
|
|
||||||
}
|
}
|
||||||
|
this.syncCreationDialogue(s);
|
||||||
this.persist(s);
|
this.persist(s);
|
||||||
return this.toView(id);
|
return this.toView(id);
|
||||||
}
|
}
|
||||||
@@ -872,7 +1163,10 @@ export class SessionManager {
|
|||||||
this.clearAgentThinking(id);
|
this.clearAgentThinking(id);
|
||||||
s.messages.push(this.msg("user", "跳过可选追问"));
|
s.messages.push(this.msg("user", "跳过可选追问"));
|
||||||
try {
|
try {
|
||||||
await s.runtime.resolveSidecarQuestions();
|
const owned = await this.runExclusive(id, s, () =>
|
||||||
|
s.runtime.resolveSidecarQuestions(),
|
||||||
|
);
|
||||||
|
if (!owned) return this.toView(id);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const detail = err instanceof Error ? err.message : String(err);
|
const detail = err instanceof Error ? err.message : String(err);
|
||||||
s.messages.push(this.msg("system", formatRuntimeError(detail)));
|
s.messages.push(this.msg("system", formatRuntimeError(detail)));
|
||||||
@@ -918,7 +1212,10 @@ export class SessionManager {
|
|||||||
if (s.bookId) this.syncBookPreview(s.bookId, s.messages);
|
if (s.bookId) this.syncBookPreview(s.bookId, s.messages);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await s.runtime.submitInput(trimmed);
|
const owned = await this.runExclusive(id, s, () =>
|
||||||
|
s.runtime.submitInput(trimmed),
|
||||||
|
);
|
||||||
|
if (!owned) return this.toView(id);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const detail = err instanceof Error ? err.message : String(err);
|
const detail = err instanceof Error ? err.message : String(err);
|
||||||
s.messages.push(this.msg("system", formatRuntimeError(detail)));
|
s.messages.push(this.msg("system", formatRuntimeError(detail)));
|
||||||
@@ -974,7 +1271,10 @@ export class SessionManager {
|
|||||||
if (s.bookId) this.syncBookPreview(s.bookId, s.messages);
|
if (s.bookId) this.syncBookPreview(s.bookId, s.messages);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await s.runtime.rerunWorker(workerId);
|
const owned = await this.runExclusive(id, s, () =>
|
||||||
|
s.runtime.rerunWorker(workerId),
|
||||||
|
);
|
||||||
|
if (!owned) return this.toView(id);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const detail = err instanceof Error ? err.message : String(err);
|
const detail = err instanceof Error ? err.message : String(err);
|
||||||
s.messages.push(this.msg("system", formatRuntimeError(detail)));
|
s.messages.push(this.msg("system", formatRuntimeError(detail)));
|
||||||
@@ -997,6 +1297,32 @@ export class SessionManager {
|
|||||||
return this.toView(id);
|
return this.toView(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 中止当前生成并重跑卡住的 worker / 总管 */
|
||||||
|
async abortAndRetry(id: string): Promise<SessionView> {
|
||||||
|
const s = this.require(id);
|
||||||
|
const session = s.runtime.getSession();
|
||||||
|
if (session.phase !== "running" || session.waitingReason) {
|
||||||
|
throw new Error("当前没有正在执行的任务");
|
||||||
|
}
|
||||||
|
const cutoff = s.runMessageCutoff ?? s.messages.length;
|
||||||
|
s.messages = s.messages.slice(0, cutoff);
|
||||||
|
if (s.bookId) this.syncBookPreview(s.bookId, s.messages);
|
||||||
|
try {
|
||||||
|
const owned = await this.runExclusive(id, s, async () => {
|
||||||
|
await s.runtime.retryStuckRun();
|
||||||
|
});
|
||||||
|
if (!owned) return this.toView(id);
|
||||||
|
} catch (err) {
|
||||||
|
const detail = err instanceof Error ? err.message : String(err);
|
||||||
|
console.error("[会话] 停止并重试失败", detail);
|
||||||
|
s.messages.push(this.msg("system", formatRuntimeError(detail)));
|
||||||
|
if (s.bookId) this.syncBookPreview(s.bookId, s.messages);
|
||||||
|
}
|
||||||
|
this.syncCreationDialogue(s);
|
||||||
|
this.persist(s);
|
||||||
|
return this.toView(id);
|
||||||
|
}
|
||||||
|
|
||||||
/** 删除消息及其后的对话 */
|
/** 删除消息及其后的对话 */
|
||||||
async deleteMessage(id: string, messageId: string): Promise<SessionView> {
|
async deleteMessage(id: string, messageId: string): Promise<SessionView> {
|
||||||
const s = this.require(id);
|
const s = this.require(id);
|
||||||
@@ -1053,7 +1379,10 @@ export class SessionManager {
|
|||||||
): Promise<SessionView> {
|
): Promise<SessionView> {
|
||||||
const s = this.require(id);
|
const s = this.require(id);
|
||||||
try {
|
try {
|
||||||
await s.runtime.approve(stepParams);
|
const owned = await this.runExclusive(id, s, () =>
|
||||||
|
s.runtime.approve(stepParams),
|
||||||
|
);
|
||||||
|
if (!owned) return this.toView(id);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const detail = err instanceof Error ? err.message : String(err);
|
const detail = err instanceof Error ? err.message : String(err);
|
||||||
console.error("[会话] 确认开干失败", detail);
|
console.error("[会话] 确认开干失败", detail);
|
||||||
@@ -1065,7 +1394,10 @@ export class SessionManager {
|
|||||||
async confirmIntake(id: string): Promise<SessionView> {
|
async confirmIntake(id: string): Promise<SessionView> {
|
||||||
const s = this.require(id);
|
const s = this.require(id);
|
||||||
try {
|
try {
|
||||||
await s.runtime.confirmIntake();
|
const owned = await this.runExclusive(id, s, () =>
|
||||||
|
s.runtime.confirmIntake(),
|
||||||
|
);
|
||||||
|
if (!owned) return this.toView(id);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const detail = err instanceof Error ? err.message : String(err);
|
const detail = err instanceof Error ? err.message : String(err);
|
||||||
console.error("[会话] 确认需求失败", detail);
|
console.error("[会话] 确认需求失败", detail);
|
||||||
@@ -1075,7 +1407,10 @@ export class SessionManager {
|
|||||||
return this.toView(id);
|
return this.toView(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
async accept(id: string): Promise<SessionView> {
|
async accept(
|
||||||
|
id: string,
|
||||||
|
opts?: { openingIndex?: number },
|
||||||
|
): Promise<SessionView> {
|
||||||
const s = this.require(id);
|
const s = this.require(id);
|
||||||
const pendingId = s.runtime.getSession().pendingArtifactId;
|
const pendingId = s.runtime.getSession().pendingArtifactId;
|
||||||
const waiting = s.runtime.getSession().waitingReason;
|
const waiting = s.runtime.getSession().waitingReason;
|
||||||
@@ -1085,7 +1420,21 @@ export class SessionManager {
|
|||||||
? s.runtime.getSession().artifacts.find((a) => a.id === pendingId)
|
? s.runtime.getSession().artifacts.find((a) => a.id === pendingId)
|
||||||
: undefined;
|
: undefined;
|
||||||
const stageBefore = inferLifecycleStage(s.runtime.getSession());
|
const stageBefore = inferLifecycleStage(s.runtime.getSession());
|
||||||
await s.runtime.acceptArtifact();
|
const sealedBefore = Boolean(
|
||||||
|
s.runtime.getSession().slots[SLOT_CREATION_SEALED_BY_OPENING],
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
const owned = await this.runExclusive(id, s, () =>
|
||||||
|
s.runtime.acceptArtifact(undefined, opts),
|
||||||
|
);
|
||||||
|
if (!owned) return this.toView(id);
|
||||||
|
} catch (err) {
|
||||||
|
const detail = err instanceof Error ? err.message : String(err);
|
||||||
|
console.error("[会话] 接受产物失败", detail);
|
||||||
|
s.messages.push(this.msg("system", formatRuntimeError(detail)));
|
||||||
|
this.persist(s);
|
||||||
|
return this.toView(id);
|
||||||
|
}
|
||||||
// 接受产物 = 不再用追问完善;询问模块随 waitingReason 清除而收起
|
// 接受产物 = 不再用追问完善;询问模块随 waitingReason 清除而收起
|
||||||
if (hadSidecarQuestions) {
|
if (hadSidecarQuestions) {
|
||||||
s.messages.push(
|
s.messages.push(
|
||||||
@@ -1101,6 +1450,8 @@ export class SessionManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.persist(s);
|
this.persist(s);
|
||||||
|
this.maybeSaveOpeningInstance(s, sealedBefore);
|
||||||
|
this.persist(s);
|
||||||
return this.toView(id);
|
return this.toView(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1109,6 +1460,7 @@ export class SessionManager {
|
|||||||
const s = this.require(id);
|
const s = this.require(id);
|
||||||
const waiting = s.runtime.getSession().waitingReason;
|
const waiting = s.runtime.getSession().waitingReason;
|
||||||
try {
|
try {
|
||||||
|
const owned = await this.runExclusive(id, s, async () => {
|
||||||
if (waiting?.kind === "approve_step") {
|
if (waiting?.kind === "approve_step") {
|
||||||
await s.runtime.rejectStep(reason ?? "用户暂不执行");
|
await s.runtime.rejectStep(reason ?? "用户暂不执行");
|
||||||
} else if (waiting?.kind === "review_artifact") {
|
} else if (waiting?.kind === "review_artifact") {
|
||||||
@@ -1116,6 +1468,8 @@ export class SessionManager {
|
|||||||
} else {
|
} else {
|
||||||
throw new Error("当前没有可拒绝的确认或验收");
|
throw new Error("当前没有可拒绝的确认或验收");
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
if (!owned) return this.toView(id);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const detail = err instanceof Error ? err.message : String(err);
|
const detail = err instanceof Error ? err.message : String(err);
|
||||||
console.error("[会话] 打回失败", detail);
|
console.error("[会话] 打回失败", detail);
|
||||||
@@ -1127,26 +1481,42 @@ export class SessionManager {
|
|||||||
|
|
||||||
async runOutline(id: string): Promise<SessionView> {
|
async runOutline(id: string): Promise<SessionView> {
|
||||||
const s = this.require(id);
|
const s = this.require(id);
|
||||||
await s.runtime.submitDecision(
|
try {
|
||||||
|
const owned = await this.runExclusive(id, s, () =>
|
||||||
|
s.runtime.submitDecision(
|
||||||
createDecision({
|
createDecision({
|
||||||
action: "run_worker",
|
action: "run_worker",
|
||||||
reason: "根据创作简报生成大纲",
|
reason: "根据创作简报生成大纲",
|
||||||
workerId: "outline",
|
workerId: "outline",
|
||||||
requiresApproval: true,
|
requiresApproval: true,
|
||||||
}),
|
}),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
if (!owned) return this.toView(id);
|
||||||
|
} catch (err) {
|
||||||
|
const detail = err instanceof Error ? err.message : String(err);
|
||||||
|
s.messages.push(this.msg("system", formatRuntimeError(detail)));
|
||||||
|
}
|
||||||
return this.toView(id);
|
return this.toView(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
async finish(id: string): Promise<SessionView> {
|
async finish(id: string): Promise<SessionView> {
|
||||||
const s = this.require(id);
|
const s = this.require(id);
|
||||||
await s.runtime.submitDecision(
|
try {
|
||||||
|
const owned = await this.runExclusive(id, s, () =>
|
||||||
|
s.runtime.submitDecision(
|
||||||
createDecision({
|
createDecision({
|
||||||
action: "finish",
|
action: "finish",
|
||||||
reason: "创作流程结束",
|
reason: "创作流程结束",
|
||||||
requiresApproval: false,
|
requiresApproval: false,
|
||||||
}),
|
}),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
if (!owned) return this.toView(id);
|
||||||
|
} catch (err) {
|
||||||
|
const detail = err instanceof Error ? err.message : String(err);
|
||||||
|
s.messages.push(this.msg("system", formatRuntimeError(detail)));
|
||||||
|
}
|
||||||
return this.toView(id);
|
return this.toView(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1308,6 +1678,42 @@ export class SessionManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 选定开场后正式保存一份 instance 定稿(可进游玩的规格+开局) */
|
||||||
|
private maybeSaveOpeningInstance(
|
||||||
|
s: ManagedSession,
|
||||||
|
sealedBefore: boolean,
|
||||||
|
): void {
|
||||||
|
if (sealedBefore) return;
|
||||||
|
if (!s.bookId) return;
|
||||||
|
const session = s.runtime.getSession();
|
||||||
|
if (!session.slots[SLOT_CREATION_SEALED_BY_OPENING]) return;
|
||||||
|
if (!canEnterPlay(session)) {
|
||||||
|
s.messages.push(
|
||||||
|
this.msg(
|
||||||
|
"system",
|
||||||
|
"[创作收口] 开场已选定,但 Worker 集尚未验收,未写入创作定稿存档。",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const meta = this.saveGameSnapshot(
|
||||||
|
session.id,
|
||||||
|
INSTANCE_OPENING_SNAPSHOT_LABEL,
|
||||||
|
"instance",
|
||||||
|
"选定开场后自动保存",
|
||||||
|
);
|
||||||
|
s.messages.push(
|
||||||
|
this.msg("system", `[创作收口] 已保存创作定稿「${meta.label}」。`),
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
const detail = err instanceof Error ? err.message : String(err);
|
||||||
|
s.messages.push(
|
||||||
|
this.msg("system", `[创作收口] 开场已选定,定稿存档未写入:${detail}`),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private persist(s: ManagedSession): void {
|
private persist(s: ManagedSession): void {
|
||||||
if (!s.bookId) return;
|
if (!s.bookId) return;
|
||||||
const book = getBook(s.bookId);
|
const book = getBook(s.bookId);
|
||||||
@@ -1423,7 +1829,7 @@ export class SessionManager {
|
|||||||
|
|
||||||
private buildLiveStreamView(sessionId: string): LiveStreamView | undefined {
|
private buildLiveStreamView(sessionId: string): LiveStreamView | undefined {
|
||||||
const worker = this.workerLive.get(sessionId);
|
const worker = this.workerLive.get(sessionId);
|
||||||
if (worker && (worker.thinking || worker.output)) {
|
if (worker) {
|
||||||
return {
|
return {
|
||||||
actor: "worker",
|
actor: "worker",
|
||||||
actorId: worker.workerId,
|
actorId: worker.workerId,
|
||||||
@@ -1449,6 +1855,42 @@ export class SessionManager {
|
|||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private beginRun(s: ManagedSession, sessionId: string): number {
|
||||||
|
s.runAbort?.abort();
|
||||||
|
s.runAbort = new AbortController();
|
||||||
|
s.runGen = (s.runGen ?? 0) + 1;
|
||||||
|
s.runMessageCutoff = s.messages.length;
|
||||||
|
this.clearAgentThinking(sessionId);
|
||||||
|
return s.runGen;
|
||||||
|
}
|
||||||
|
|
||||||
|
private isCurrentRun(s: ManagedSession, gen: number): boolean {
|
||||||
|
return s.runGen === gen;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** true = 本轮仍有效;false = 已被停止并重试取代 */
|
||||||
|
private async runExclusive(
|
||||||
|
id: string,
|
||||||
|
s: ManagedSession,
|
||||||
|
fn: () => Promise<unknown>,
|
||||||
|
): Promise<boolean> {
|
||||||
|
const gen = this.beginRun(s, id);
|
||||||
|
try {
|
||||||
|
await runWithAbortSignal(s.runAbort!.signal, fn);
|
||||||
|
return this.isCurrentRun(s, gen);
|
||||||
|
} catch (err) {
|
||||||
|
if (isAbortError(err) && !this.isCurrentRun(s, gen)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
if (this.isCurrentRun(s, gen)) {
|
||||||
|
this.clearAgentThinking(id);
|
||||||
|
s.runAbort = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private enrichDisplayMessages(
|
private enrichDisplayMessages(
|
||||||
session: RuntimeSession,
|
session: RuntimeSession,
|
||||||
messages: ChatMessage[],
|
messages: ChatMessage[],
|
||||||
@@ -1595,6 +2037,7 @@ export class SessionManager {
|
|||||||
|
|
||||||
private toView(id: string, resumed = false, resumeHint?: string): SessionView {
|
private toView(id: string, resumed = false, resumeHint?: string): SessionView {
|
||||||
const s = this.require(id);
|
const s = this.require(id);
|
||||||
|
this.sanitizePlayHitl(s);
|
||||||
const session = s.runtime.getSession();
|
const session = s.runtime.getSession();
|
||||||
const reason = session.waitingReason;
|
const reason = session.waitingReason;
|
||||||
const book = s.bookId ? getBook(s.bookId) : null;
|
const book = s.bookId ? getBook(s.bookId) : null;
|
||||||
@@ -1667,7 +2110,15 @@ export class SessionManager {
|
|||||||
: "描述你想写什么…",
|
: "描述你想写什么…",
|
||||||
});
|
});
|
||||||
} else if (reason?.kind === "input") {
|
} else if (reason?.kind === "input") {
|
||||||
if (reason.questions?.length) {
|
const playNow =
|
||||||
|
inferLifecycleStage(session) === "play" || isPlayLayerActive(session.slots);
|
||||||
|
if (playNow) {
|
||||||
|
actions.push({
|
||||||
|
type: "send_message",
|
||||||
|
label: "发送",
|
||||||
|
placeholder: "说你要做什么…",
|
||||||
|
});
|
||||||
|
} else if (reason.questions?.length) {
|
||||||
const qs = normalizeQuestions(reason.questions);
|
const qs = normalizeQuestions(reason.questions);
|
||||||
const hasAssessment = Boolean(reason.message?.trim());
|
const hasAssessment = Boolean(reason.message?.trim());
|
||||||
hints.push(
|
hints.push(
|
||||||
@@ -1732,7 +2183,13 @@ export class SessionManager {
|
|||||||
);
|
);
|
||||||
if (proposed) {
|
if (proposed) {
|
||||||
hints.push(
|
hints.push(
|
||||||
`确认开始「${proposed.name}」${
|
proposed.mode === "revise"
|
||||||
|
? `回头修改「${proposed.name}」${
|
||||||
|
proposed.paramsMissing.length
|
||||||
|
? `(请先补齐:${proposed.paramsMissing.join("、")})`
|
||||||
|
: "(继承既有产物继续改,不是从零再生成)"
|
||||||
|
}`
|
||||||
|
: `确认开始「${proposed.name}」${
|
||||||
proposed.paramsMissing.length
|
proposed.paramsMissing.length
|
||||||
? `(请先补齐:${proposed.paramsMissing.join("、")})`
|
? `(请先补齐:${proposed.paramsMissing.join("、")})`
|
||||||
: ""
|
: ""
|
||||||
@@ -1749,10 +2206,10 @@ export class SessionManager {
|
|||||||
actions.push({
|
actions.push({
|
||||||
type: "send_message",
|
type: "send_message",
|
||||||
label: "说明意见",
|
label: "说明意见",
|
||||||
placeholder: "不同意可说明要改成什么…",
|
placeholder: "改排、回头改某步,或说明不要这一步…",
|
||||||
});
|
});
|
||||||
} else if (reason?.kind === "next_intent") {
|
} else if (reason?.kind === "next_intent") {
|
||||||
hints.push("下一步想写什么?可留空,发送后会展示下一节点供你确认。");
|
hints.push("下一步想写什么?可留空;也可说回头改某步。发送后会展示下一节点供你确认。");
|
||||||
actions.push({
|
actions.push({
|
||||||
type: "send_message",
|
type: "send_message",
|
||||||
label: "继续",
|
label: "继续",
|
||||||
@@ -1859,6 +2316,7 @@ export class SessionManager {
|
|||||||
actions,
|
actions,
|
||||||
pipeline: buildPipeline(session),
|
pipeline: buildPipeline(session),
|
||||||
lifecycleStage,
|
lifecycleStage,
|
||||||
|
playLayerActive: isPlayLayerActive(session.slots),
|
||||||
playReady: canEnterPlay(session),
|
playReady: canEnterPlay(session),
|
||||||
skillCatalog,
|
skillCatalog,
|
||||||
toolTrace: buildToolTrace(messages),
|
toolTrace: buildToolTrace(messages),
|
||||||
@@ -2246,7 +2704,10 @@ function readProposedNextStep(
|
|||||||
}))
|
}))
|
||||||
.filter((p) => p.key)
|
.filter((p) => p.key)
|
||||||
: [],
|
: [],
|
||||||
|
kind: typeof doc.kind === "string" ? doc.kind : undefined,
|
||||||
intent: typeof doc.intent === "string" ? doc.intent : undefined,
|
intent: typeof doc.intent === "string" ? doc.intent : undefined,
|
||||||
|
mode: doc.mode === "revise" ? "revise" : undefined,
|
||||||
|
revises: typeof doc.revises === "string" ? doc.revises : undefined,
|
||||||
};
|
};
|
||||||
} catch {
|
} catch {
|
||||||
return undefined;
|
return undefined;
|
||||||
@@ -2416,6 +2877,7 @@ function buildReviewArtifactView(
|
|||||||
workerId: art.workerId,
|
workerId: art.workerId,
|
||||||
summary,
|
summary,
|
||||||
body,
|
body,
|
||||||
|
outputTags: art.outputTags,
|
||||||
sourceMessageId: outputMsg?.id,
|
sourceMessageId: outputMsg?.id,
|
||||||
contextTrace: outputMsg?.contextTrace,
|
contextTrace: outputMsg?.contextTrace,
|
||||||
workerSet,
|
workerSet,
|
||||||
|
|||||||
@@ -22,12 +22,17 @@ import {
|
|||||||
patchPresetEntries,
|
patchPresetEntries,
|
||||||
type PresetEntryPatch,
|
type PresetEntryPatch,
|
||||||
} from "../preset/entries.js";
|
} from "../preset/entries.js";
|
||||||
|
import { runPresetProbe } from "../preset/probe.js";
|
||||||
import {
|
import {
|
||||||
deletePreset,
|
deletePreset,
|
||||||
getPreset,
|
getPreset,
|
||||||
importAndSavePreset,
|
importAndSavePreset,
|
||||||
listPresets,
|
listPresets,
|
||||||
} from "../preset/store.js";
|
} from "../preset/store.js";
|
||||||
|
import {
|
||||||
|
createLlmForPreset,
|
||||||
|
hasRealLlmConfig,
|
||||||
|
} from "../runtime/llm-factory.js";
|
||||||
import { sessionManager } from "./session-manager.js";
|
import { sessionManager } from "./session-manager.js";
|
||||||
|
|
||||||
async function readBody(req: IncomingMessage): Promise<string> {
|
async function readBody(req: IncomingMessage): Promise<string> {
|
||||||
@@ -226,6 +231,52 @@ export async function handleSettingsApi(
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const presetProbeMatch = pathname.match(/^\/api\/presets\/([^/]+)\/probe$/);
|
||||||
|
if (presetProbeMatch && req.method === "POST") {
|
||||||
|
const id = decodeURIComponent(presetProbeMatch[1]);
|
||||||
|
const preset = getPreset(id);
|
||||||
|
if (!preset) {
|
||||||
|
json(res, 404, { error: "预设不存在" });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const body = JSON.parse(await readBody(req)) as {
|
||||||
|
message?: string;
|
||||||
|
loreBefore?: string;
|
||||||
|
history?: string;
|
||||||
|
loreAfter?: string;
|
||||||
|
postTurn?: string;
|
||||||
|
complete?: boolean;
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
const wantComplete = body.complete !== false;
|
||||||
|
const canComplete = wantComplete && hasRealLlmConfig();
|
||||||
|
const llm = canComplete ? createLlmForPreset(preset) : null;
|
||||||
|
const result = await runPresetProbe({
|
||||||
|
preset,
|
||||||
|
input: {
|
||||||
|
message: body.message ?? "",
|
||||||
|
loreBefore: body.loreBefore,
|
||||||
|
history: body.history,
|
||||||
|
loreAfter: body.loreAfter,
|
||||||
|
postTurn: body.postTurn,
|
||||||
|
},
|
||||||
|
complete: llm
|
||||||
|
? (messages) =>
|
||||||
|
llm.complete(messages, { caller: "preset-probe" })
|
||||||
|
: undefined,
|
||||||
|
});
|
||||||
|
if (!llm && wantComplete) {
|
||||||
|
result.skipReason = "未配置 API Key,只返回拼装后的上下文";
|
||||||
|
}
|
||||||
|
json(res, 200, result);
|
||||||
|
} catch (err) {
|
||||||
|
json(res, 400, {
|
||||||
|
error: err instanceof Error ? err.message : String(err),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
const presetEntriesMatch = pathname.match(
|
const presetEntriesMatch = pathname.match(
|
||||||
/^\/api\/presets\/([^/]+)\/entries$/,
|
/^\/api\/presets\/([^/]+)\/entries$/,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -177,7 +177,7 @@ function buildWorldSimulatorCatalog(
|
|||||||
id: "declare-ready",
|
id: "declare-ready",
|
||||||
stage: "design",
|
stage: "design",
|
||||||
label: "实例就绪",
|
label: "实例就绪",
|
||||||
purpose: "Worker 集已 accept 即可进游玩;若声明了开局,建议先验收开场白(非强制锁定)。",
|
purpose: "Worker 集已 accept 即可进游玩;若走了开场白终节点,选定后会自动保存定稿。",
|
||||||
status: hasAcceptedWorkerSet(session) ? "done" : "pending",
|
status: hasAcceptedWorkerSet(session) ? "done" : "pending",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -444,8 +444,10 @@ const server = createServer(async (req, res) => {
|
|||||||
const body = JSON.parse(await readBody(req)) as {
|
const body = JSON.parse(await readBody(req)) as {
|
||||||
action?: string;
|
action?: string;
|
||||||
stepParams?: Record<string, unknown>;
|
stepParams?: Record<string, unknown>;
|
||||||
|
openingIndex?: number;
|
||||||
};
|
};
|
||||||
let view;
|
let view;
|
||||||
|
try {
|
||||||
switch (body.action) {
|
switch (body.action) {
|
||||||
case "approve":
|
case "approve":
|
||||||
view = await sessionManager.approve(
|
view = await sessionManager.approve(
|
||||||
@@ -459,7 +461,12 @@ const server = createServer(async (req, res) => {
|
|||||||
view = await sessionManager.confirmIntake(sessionId);
|
view = await sessionManager.confirmIntake(sessionId);
|
||||||
break;
|
break;
|
||||||
case "accept":
|
case "accept":
|
||||||
view = await sessionManager.accept(sessionId);
|
view = await sessionManager.accept(
|
||||||
|
sessionId,
|
||||||
|
typeof body.openingIndex === "number"
|
||||||
|
? { openingIndex: body.openingIndex }
|
||||||
|
: undefined,
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
case "skip_questions":
|
case "skip_questions":
|
||||||
view = await sessionManager.skipQuestions(sessionId);
|
view = await sessionManager.skipQuestions(sessionId);
|
||||||
@@ -473,10 +480,19 @@ const server = createServer(async (req, res) => {
|
|||||||
case "finish":
|
case "finish":
|
||||||
view = await sessionManager.finish(sessionId);
|
view = await sessionManager.finish(sessionId);
|
||||||
break;
|
break;
|
||||||
|
case "retry_run":
|
||||||
|
view = await sessionManager.abortAndRetry(sessionId);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
json(res, 400, { error: "未知 action" });
|
json(res, 400, { error: "未知 action" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
} catch (err) {
|
||||||
|
json(res, 400, {
|
||||||
|
error: err instanceof Error ? err.message : "操作失败",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
json(res, 200, view);
|
json(res, 200, view);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -163,12 +163,22 @@ export function projectFragmentContent(
|
|||||||
const frag = parseContextFragment(raw);
|
const frag = parseContextFragment(raw);
|
||||||
const mode = projection.trim().toLowerCase();
|
const mode = projection.trim().toLowerCase();
|
||||||
if (!frag) {
|
if (!frag) {
|
||||||
if (mode === "summary" && raw.length > 800) return `${raw.slice(0, 800)}…`;
|
if ((mode === "summary" || mode === "brief" || mode === "index") && raw.length > 800) {
|
||||||
|
return `${raw.slice(0, 800)}…`;
|
||||||
|
}
|
||||||
return raw;
|
return raw;
|
||||||
}
|
}
|
||||||
if (mode === "summary" || mode === "brief") {
|
if (mode === "summary" || mode === "brief") {
|
||||||
return frag.brief || raw;
|
return frag.brief || raw;
|
||||||
}
|
}
|
||||||
|
if (mode === "index") {
|
||||||
|
const lines: string[] = [];
|
||||||
|
if (frag.技能) lines.push(`技能:${frag.技能}`);
|
||||||
|
if (frag.brief) lines.push(frag.brief);
|
||||||
|
if (frag.mount.length) lines.push(`挂载:${frag.mount.join("、")}`);
|
||||||
|
if (frag.稳变) lines.push(`稳变:${frag.稳变}`);
|
||||||
|
return lines.join("\n") || frag.brief || raw;
|
||||||
|
}
|
||||||
if (mode === "fields") {
|
if (mode === "fields") {
|
||||||
if (frag.正文 && typeof frag.正文 === "object" && !Array.isArray(frag.正文)) {
|
if (frag.正文 && typeof frag.正文 === "object" && !Array.isArray(frag.正文)) {
|
||||||
const keys = Object.keys(frag.正文 as object);
|
const keys = Object.keys(frag.正文 as object);
|
||||||
@@ -237,9 +247,11 @@ export function contextFragmentToView(raw: unknown): ContextFragmentView {
|
|||||||
const probe = rawDoc?.追问;
|
const probe = rawDoc?.追问;
|
||||||
if (probe && typeof probe === "object" && !Array.isArray(probe)) {
|
if (probe && typeof probe === "object" && !Array.isArray(probe)) {
|
||||||
const row = probe as Record<string, unknown>;
|
const row = probe as Record<string, unknown>;
|
||||||
const lines: string[] = [];
|
|
||||||
if (typeof row.导语 === "string" && row.导语.trim()) lines.push(row.导语.trim());
|
|
||||||
const qs = Array.isArray(row.题目) ? row.题目 : [];
|
const qs = Array.isArray(row.题目) ? row.题目 : [];
|
||||||
|
const lines: string[] = [];
|
||||||
|
if (qs.length && typeof row.导语 === "string" && row.导语.trim()) {
|
||||||
|
lines.push(row.导语.trim());
|
||||||
|
}
|
||||||
qs.forEach((q, i) => {
|
qs.forEach((q, i) => {
|
||||||
if (!q || typeof q !== "object") return;
|
if (!q || typeof q !== "object") return;
|
||||||
const r = q as Record<string, unknown>;
|
const r = q as Record<string, unknown>;
|
||||||
@@ -282,7 +294,9 @@ export function extractFragmentAskSidecar(raw: unknown): {
|
|||||||
|
|
||||||
const looksFragment =
|
const looksFragment =
|
||||||
row.schema === CONTEXT_FRAGMENT_SCHEMA ||
|
row.schema === CONTEXT_FRAGMENT_SCHEMA ||
|
||||||
(typeof row.brief === "string" && ("正文" in row || "body" in row));
|
typeof row.技能 === "string" ||
|
||||||
|
(typeof row.brief === "string" &&
|
||||||
|
("正文" in row || "body" in row || "追问" in row));
|
||||||
if (!looksFragment) return { questions: [], assessment: "" };
|
if (!looksFragment) return { questions: [], assessment: "" };
|
||||||
|
|
||||||
const assessmentParts: string[] = [];
|
const assessmentParts: string[] = [];
|
||||||
@@ -358,7 +372,12 @@ export function extractFragmentAskSidecar(raw: unknown): {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const questions = normalizeQuestions(rawQs);
|
const questions = normalizeQuestions(rawQs);
|
||||||
const assessment = [lead, ...assessmentParts].filter(Boolean).join("\n");
|
const assessment = [
|
||||||
|
questions.length ? lead : "",
|
||||||
|
...assessmentParts,
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join("\n");
|
||||||
return { questions, assessment };
|
return { questions, assessment };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,9 +6,14 @@
|
|||||||
import type { ContextSegmentDef } from "./context-segments.js";
|
import type { ContextSegmentDef } from "./context-segments.js";
|
||||||
import { extractJsonObjectText } from "./worker-set-parse.js";
|
import { extractJsonObjectText } from "./worker-set-parse.js";
|
||||||
import {
|
import {
|
||||||
|
DEFAULT_ON_DEMAND_REFS,
|
||||||
|
DEFAULT_PLAY_SLOT_REFS,
|
||||||
|
defaultPlaySlots,
|
||||||
parsePlaySlots,
|
parsePlaySlots,
|
||||||
playSlotIdForRef,
|
playSlotIdForRef,
|
||||||
|
PLAY_SLOT_META,
|
||||||
PLAY_SLOTS_WITHOUT_DIALOGUE_HISTORY,
|
PLAY_SLOTS_WITHOUT_DIALOGUE_HISTORY,
|
||||||
|
resolvePlayAgentId,
|
||||||
type PlaySlotsConfig,
|
type PlaySlotsConfig,
|
||||||
} from "./play-slots.js";
|
} from "./play-slots.js";
|
||||||
import {
|
import {
|
||||||
@@ -87,20 +92,53 @@ function parseInsert(raw: unknown): ContextOrderInsert | undefined {
|
|||||||
function parseSlot(raw: unknown): ContextOrderSlot | undefined {
|
function parseSlot(raw: unknown): ContextOrderSlot | undefined {
|
||||||
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return undefined;
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return undefined;
|
||||||
const row = raw as Record<string, unknown>;
|
const row = raw as Record<string, unknown>;
|
||||||
const ref = asString(row.ref) ?? asString(row.id);
|
const rawRef = asString(row.ref) ?? asString(row.id);
|
||||||
if (!ref) return undefined;
|
if (!rawRef) return undefined;
|
||||||
|
const slotId = resolvePlayAgentId(rawRef);
|
||||||
|
const ref = slotId
|
||||||
|
? slotId === "chance"
|
||||||
|
? DEFAULT_ON_DEMAND_REFS.chance
|
||||||
|
: DEFAULT_PLAY_SLOT_REFS[slotId]
|
||||||
|
: rawRef;
|
||||||
const insertsRaw = Array.isArray(row.inserts) ? row.inserts : [];
|
const insertsRaw = Array.isArray(row.inserts) ? row.inserts : [];
|
||||||
const inserts = insertsRaw
|
const inserts = insertsRaw
|
||||||
.map(parseInsert)
|
.map(parseInsert)
|
||||||
.filter((x): x is ContextOrderInsert => Boolean(x))
|
.filter((x): x is ContextOrderInsert => Boolean(x))
|
||||||
.sort((a, b) => a.order - b.order);
|
.sort((a, b) => a.order - b.order);
|
||||||
|
const label =
|
||||||
|
asString(row.label) ??
|
||||||
|
asString(row.name) ??
|
||||||
|
(slotId && slotId !== "chance" ? PLAY_SLOT_META[slotId].label : undefined);
|
||||||
return {
|
return {
|
||||||
ref,
|
ref,
|
||||||
label: asString(row.label) ?? asString(row.name),
|
label,
|
||||||
inserts,
|
inserts,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function agentEnabledFlag(row: Record<string, unknown>): boolean {
|
||||||
|
if (typeof row.enabled === "boolean") return row.enabled;
|
||||||
|
if (row.enabled === "false" || row.enabled === 0) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function playSlotsFromAgents(
|
||||||
|
rows: Record<string, unknown>[],
|
||||||
|
): PlaySlotsConfig {
|
||||||
|
const base = defaultPlaySlots();
|
||||||
|
for (const row of rows) {
|
||||||
|
const rawId = asString(row.id) ?? asString(row.ref) ?? asString(row.label);
|
||||||
|
if (!rawId) continue;
|
||||||
|
const slotId = resolvePlayAgentId(rawId);
|
||||||
|
if (!slotId) continue;
|
||||||
|
const on = agentEnabledFlag(row);
|
||||||
|
if (slotId === "chance") base.chance = on;
|
||||||
|
else if (slotId === "auditor") base.auditor = on;
|
||||||
|
else base[slotId] = on;
|
||||||
|
}
|
||||||
|
return base;
|
||||||
|
}
|
||||||
|
|
||||||
export function parseContextOrder(raw: unknown): ContextOrderDoc | undefined {
|
export function parseContextOrder(raw: unknown): ContextOrderDoc | undefined {
|
||||||
let row: Record<string, unknown> | null = null;
|
let row: Record<string, unknown> | null = null;
|
||||||
if (typeof raw === "string") {
|
if (typeof raw === "string") {
|
||||||
@@ -118,16 +156,32 @@ export function parseContextOrder(raw: unknown): ContextOrderDoc | undefined {
|
|||||||
}
|
}
|
||||||
if (!row) return undefined;
|
if (!row) return undefined;
|
||||||
|
|
||||||
const slotsRaw = Array.isArray(row.slots) ? row.slots : [];
|
const agentsRaw = Array.isArray(row.agents) ? row.agents : [];
|
||||||
const slots = slotsRaw
|
const agentRows = agentsRaw.filter(
|
||||||
|
(a): a is Record<string, unknown> =>
|
||||||
|
Boolean(a) && typeof a === "object" && !Array.isArray(a),
|
||||||
|
);
|
||||||
|
const fromAgents = agentRows
|
||||||
|
.filter(agentEnabledFlag)
|
||||||
.map(parseSlot)
|
.map(parseSlot)
|
||||||
.filter((x): x is ContextOrderSlot => Boolean(x));
|
.filter((x): x is ContextOrderSlot => Boolean(x));
|
||||||
|
|
||||||
|
const slotsRaw = Array.isArray(row.slots) ? row.slots : [];
|
||||||
|
const fromSlots = slotsRaw
|
||||||
|
.map(parseSlot)
|
||||||
|
.filter((x): x is ContextOrderSlot => Boolean(x));
|
||||||
|
|
||||||
|
const slots = fromAgents.length ? fromAgents : fromSlots;
|
||||||
if (!slots.length && row.schema !== CONTEXT_ORDER_SCHEMA) return undefined;
|
if (!slots.length && row.schema !== CONTEXT_ORDER_SCHEMA) return undefined;
|
||||||
|
|
||||||
|
const play_slots = fromAgents.length
|
||||||
|
? playSlotsFromAgents(agentRows)
|
||||||
|
: parsePlaySlots(row.play_slots ?? row.playSlots);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
schema: CONTEXT_ORDER_SCHEMA,
|
schema: CONTEXT_ORDER_SCHEMA,
|
||||||
brief: asString(row.brief),
|
brief: asString(row.brief),
|
||||||
play_slots: parsePlaySlots(row.play_slots ?? row.playSlots),
|
play_slots,
|
||||||
slots,
|
slots,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+153
-33
@@ -89,17 +89,40 @@ function readTagContent(
|
|||||||
return items[items.length - 1]?.content?.trim() ?? "";
|
return items[items.length - 1]?.content?.trim() ?? "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 单段投影正文(不含 label)。供世界书 marker 填洞复用。 */
|
||||||
|
export function renderContextSegmentBody(
|
||||||
|
segment: ContextSegmentDef,
|
||||||
|
inputs: Record<string, string>,
|
||||||
|
blackboard: Blackboard,
|
||||||
|
inputMerge: BlackboardInputMerge,
|
||||||
|
skipExact?: Set<string>,
|
||||||
|
skipTags?: Set<string>,
|
||||||
|
): string {
|
||||||
|
return formatSegmentBody(
|
||||||
|
segment,
|
||||||
|
inputs,
|
||||||
|
blackboard,
|
||||||
|
inputMerge,
|
||||||
|
skipExact,
|
||||||
|
skipTags,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function formatSegmentBody(
|
function formatSegmentBody(
|
||||||
segment: ContextSegmentDef,
|
segment: ContextSegmentDef,
|
||||||
inputs: Record<string, string>,
|
inputs: Record<string, string>,
|
||||||
blackboard: Blackboard,
|
blackboard: Blackboard,
|
||||||
inputMerge: BlackboardInputMerge,
|
inputMerge: BlackboardInputMerge,
|
||||||
|
skipExact?: Set<string>,
|
||||||
|
skipTags?: Set<string>,
|
||||||
): string {
|
): string {
|
||||||
if (segment.inline?.trim()) {
|
if (segment.inline?.trim()) {
|
||||||
return applyProjection(segment.inline.trim(), segment.projection);
|
return applyProjection(segment.inline.trim(), segment.projection);
|
||||||
}
|
}
|
||||||
const parts: string[] = [];
|
const parts: string[] = [];
|
||||||
|
const seen = new Set<string>();
|
||||||
for (const tag of segment.tags) {
|
for (const tag of segment.tags) {
|
||||||
|
if (skipTags?.has(tag)) continue;
|
||||||
let content: string;
|
let content: string;
|
||||||
if (isDialogueHistoryRef(tag)) {
|
if (isDialogueHistoryRef(tag)) {
|
||||||
content = resolveDialogueHistory(inputs, blackboard, inputMerge);
|
content = resolveDialogueHistory(inputs, blackboard, inputMerge);
|
||||||
@@ -115,8 +138,22 @@ function formatSegmentBody(
|
|||||||
content = applyPolicy(content, segment.policy);
|
content = applyPolicy(content, segment.policy);
|
||||||
content = applyProjection(content, segment.projection);
|
content = applyProjection(content, segment.projection);
|
||||||
}
|
}
|
||||||
if (!content.trim()) continue;
|
const trimmed = content.trim();
|
||||||
parts.push(content.trim());
|
if (!trimmed) continue;
|
||||||
|
if (skipExact?.has(trimmed)) continue;
|
||||||
|
if (seen.has(trimmed)) continue;
|
||||||
|
if (
|
||||||
|
parts.some(
|
||||||
|
(p) =>
|
||||||
|
p === trimmed ||
|
||||||
|
p.includes(`\n\n${trimmed}`) ||
|
||||||
|
p.startsWith(`${trimmed}\n\n`),
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
seen.add(trimmed);
|
||||||
|
parts.push(trimmed);
|
||||||
}
|
}
|
||||||
return parts.join("\n\n");
|
return parts.join("\n\n");
|
||||||
}
|
}
|
||||||
@@ -141,6 +178,61 @@ function applyProjection(content: string, projection?: string): string {
|
|||||||
return projectFragmentContent(content, projection);
|
return projectFragmentContent(content, projection);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const USER_REQUIREMENT_TAGS = new Set([
|
||||||
|
"用户.需求",
|
||||||
|
"book.brief",
|
||||||
|
"用户.最新输入",
|
||||||
|
"用户.worker答复",
|
||||||
|
"用户.修订说明",
|
||||||
|
]);
|
||||||
|
|
||||||
|
const CREATION_REVIEW_WORKERS = new Set([
|
||||||
|
"design-step",
|
||||||
|
"design-flow",
|
||||||
|
"opening-generator",
|
||||||
|
]);
|
||||||
|
|
||||||
|
function alreadyCovered(parts: string[], next: string): boolean {
|
||||||
|
const trimmed = next.trim();
|
||||||
|
if (!trimmed) return true;
|
||||||
|
return parts.some(
|
||||||
|
(p) =>
|
||||||
|
p === trimmed ||
|
||||||
|
p.includes(`\n\n${trimmed}`) ||
|
||||||
|
p.startsWith(`${trimmed}\n\n`),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function pushUnique(parts: string[], next: string): void {
|
||||||
|
const trimmed = next.trim();
|
||||||
|
if (!trimmed || alreadyCovered(parts, trimmed)) return;
|
||||||
|
parts.push(trimmed);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 创作步:用户要求按时间排;冲突只看较新,旧条未点名推翻则仍有效。 */
|
||||||
|
function collectUserRequirementBlock(params: {
|
||||||
|
inputs: Record<string, string>;
|
||||||
|
blackboard: Blackboard;
|
||||||
|
inputMerge: BlackboardInputMerge;
|
||||||
|
}): string {
|
||||||
|
const { inputs, blackboard, inputMerge } = params;
|
||||||
|
const ordered = [
|
||||||
|
readTagContent("用户.需求", inputs, blackboard, inputMerge),
|
||||||
|
readTagContent("book.brief", inputs, blackboard, inputMerge),
|
||||||
|
readTagContent("用户.worker答复", inputs, blackboard, inputMerge),
|
||||||
|
readTagContent("用户.最新输入", inputs, blackboard, inputMerge),
|
||||||
|
readTagContent("用户.修订说明", inputs, blackboard, inputMerge),
|
||||||
|
];
|
||||||
|
const parts: string[] = [];
|
||||||
|
for (const item of ordered) pushUnique(parts, item);
|
||||||
|
if (!parts.length) return "";
|
||||||
|
return [
|
||||||
|
"## 【用户要求】按先后排列。冲突时以较新的为准;未被较新要求改写的旧条仍有效。",
|
||||||
|
"",
|
||||||
|
...parts,
|
||||||
|
].join("\n\n");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 按 contextSegments 拼装 user 侧上下文。
|
* 按 contextSegments 拼装 user 侧上下文。
|
||||||
* 无 segments 时回退为 JSON inputs(兼容旧 skill)。
|
* 无 segments 时回退为 JSON inputs(兼容旧 skill)。
|
||||||
@@ -178,37 +270,16 @@ export function assembleWorkerContext(params: {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const blocks: string[] = [];
|
|
||||||
|
|
||||||
const render = (seg: ContextSegmentDef) => {
|
|
||||||
const body = formatSegmentBody(seg, params.inputs, params.blackboard, inputMerge);
|
|
||||||
if (!body) return;
|
|
||||||
if (seg.label) {
|
|
||||||
blocks.push(`${seg.label}\n\n${body}`);
|
|
||||||
} else {
|
|
||||||
blocks.push(body);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 严格按 segments 数组顺序(投影排序表顺序);不再按 static/dynamic 重排
|
|
||||||
for (const seg of segments) render(seg);
|
|
||||||
|
|
||||||
// 定稿摘要:若未在 segments 中声明,仍附在末尾
|
|
||||||
const brief = params.inputs[CONTEXT_BRIEF_TAG]?.trim();
|
|
||||||
const briefInSegments = segments.some((s) => s.tags.includes(CONTEXT_BRIEF_TAG));
|
|
||||||
if (brief && !briefInSegments) {
|
|
||||||
blocks.push(`## 上下文.定稿摘要\n\n${brief}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 修订态:把本步 outputTags 上已有正文钉成「待改底稿」,禁止无故推倒重写
|
|
||||||
const revisionNote = readTagContent(
|
const revisionNote = readTagContent(
|
||||||
"用户.修订说明",
|
"用户.修订说明",
|
||||||
params.inputs,
|
params.inputs,
|
||||||
params.blackboard,
|
params.blackboard,
|
||||||
inputMerge,
|
inputMerge,
|
||||||
).trim();
|
).trim();
|
||||||
|
const inheritExisting = segments.some((s) => s.id === "inherit-existing");
|
||||||
|
const splitUserReq = CREATION_REVIEW_WORKERS.has(params.workerId);
|
||||||
|
|
||||||
const draftBlocks: string[] = [];
|
const draftBlocks: string[] = [];
|
||||||
if (revisionNote) {
|
|
||||||
for (const tag of params.outputTags) {
|
for (const tag of params.outputTags) {
|
||||||
const alreadyInSegments = segments.some((s) => s.tags.includes(tag));
|
const alreadyInSegments = segments.some((s) => s.tags.includes(tag));
|
||||||
if (alreadyInSegments) continue;
|
if (alreadyInSegments) continue;
|
||||||
@@ -221,19 +292,66 @@ export function assembleWorkerContext(params: {
|
|||||||
if (!draft) continue;
|
if (!draft) continue;
|
||||||
draftBlocks.push(`### \`${tag}\`\n\n${draft}`);
|
draftBlocks.push(`### \`${tag}\`\n\n${draft}`);
|
||||||
}
|
}
|
||||||
if (draftBlocks.length) {
|
|
||||||
|
// 只有本步真有底稿(或回头修改)才算修订;上一步残留的修订说明仍作为用户要求保留
|
||||||
|
const inheritMode = inheritExisting || (Boolean(revisionNote) && draftBlocks.length > 0);
|
||||||
|
|
||||||
|
const skipTags = new Set<string>();
|
||||||
|
if (splitUserReq) {
|
||||||
|
for (const tag of USER_REQUIREMENT_TAGS) skipTags.add(tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
const blocks: string[] = [];
|
||||||
|
|
||||||
|
const render = (seg: ContextSegmentDef) => {
|
||||||
|
if (inheritMode && seg.id === "module-opening") return;
|
||||||
|
const body = formatSegmentBody(
|
||||||
|
seg,
|
||||||
|
params.inputs,
|
||||||
|
params.blackboard,
|
||||||
|
inputMerge,
|
||||||
|
undefined,
|
||||||
|
skipTags,
|
||||||
|
);
|
||||||
|
if (!body) return;
|
||||||
|
if (seg.label) {
|
||||||
|
blocks.push(`${seg.label}\n\n${body}`);
|
||||||
|
} else {
|
||||||
|
blocks.push(body);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 严格按 segments 数组顺序(投影排序表顺序);不再按 static/dynamic 重排
|
||||||
|
for (const seg of segments) render(seg);
|
||||||
|
|
||||||
|
if (splitUserReq) {
|
||||||
|
const reqBlock = collectUserRequirementBlock({
|
||||||
|
inputs: params.inputs,
|
||||||
|
blackboard: params.blackboard,
|
||||||
|
inputMerge,
|
||||||
|
});
|
||||||
|
if (reqBlock) blocks.push(reqBlock);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 定稿摘要:若未在 segments 中声明,仍附在末尾
|
||||||
|
const brief = params.inputs[CONTEXT_BRIEF_TAG]?.trim();
|
||||||
|
const briefInSegments = segments.some((s) => s.tags.includes(CONTEXT_BRIEF_TAG));
|
||||||
|
if (brief && !briefInSegments) {
|
||||||
|
blocks.push(`## 上下文.定稿摘要\n\n${brief}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inheritMode && draftBlocks.length) {
|
||||||
blocks.push(
|
blocks.push(
|
||||||
[
|
[
|
||||||
"## 【待改底稿】",
|
"## 【待改底稿】",
|
||||||
"",
|
"",
|
||||||
"下列为**当前已有产物**。按「用户.修订说明」以及「用户.worker答复」中的追问作答(选项与补充)在其上修改,并写回同名 outputTags。",
|
"下列为**当前已有产物**(只保留这一份最新稿)。在其上落实【用户要求】里较新的条目;与旧要求冲突时以较新的为准,未点名改写的旧条仍须保留在产物里。",
|
||||||
"节点方法(task / principles / probe / output)仍须遵守。保留未要求改动的结构与结论;禁止无故整份重写。",
|
"节点方法(task / principles / probe / output)仍须遵守。禁止无视较新要求原样交回。",
|
||||||
"",
|
"",
|
||||||
...draftBlocks,
|
...draftBlocks,
|
||||||
].join("\n"),
|
].join("\n"),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const taskLines = [
|
const taskLines = [
|
||||||
"## 本步任务",
|
"## 本步任务",
|
||||||
@@ -243,10 +361,12 @@ export function assembleWorkerContext(params: {
|
|||||||
`- outputTags: ${params.outputTags.map((t) => `\`${t}\``).join("、") || "(无)"}`,
|
`- outputTags: ${params.outputTags.map((t) => `\`${t}\``).join("、") || "(无)"}`,
|
||||||
"",
|
"",
|
||||||
];
|
];
|
||||||
if (revisionNote) {
|
if (inheritMode) {
|
||||||
taskLines.push(
|
taskLines.push(
|
||||||
"**修订模式**:接着上方【待改底稿】(若有)与依赖产物,落实「用户.修订说明」以及「用户.worker答复」里的追问作答(选项与补充意见)。",
|
inheritExisting
|
||||||
"不要从零另起一份;不要扩大修改面。标为「只读 / 已定稿」的分区仍不可改。节点方法仍有效。",
|
? "**回头修改**:接着上方【既有产物 · 继承修改】(以及【待改底稿】若有)继续改,写回同一产物。"
|
||||||
|
: "**修订模式**:以【待改底稿】为底,落实【用户要求】;冲突时以较新的为准。",
|
||||||
|
"不要从零另起一份。标为「只读 / 已定稿」的分区仍不可改。",
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
taskLines.push(
|
taskLines.push(
|
||||||
|
|||||||
+306
-53
@@ -53,9 +53,41 @@ const DEFAULT_SKILLS_ROOT = path.resolve(
|
|||||||
"../../skills",
|
"../../skills",
|
||||||
);
|
);
|
||||||
|
|
||||||
/** 步骤调用参数(编排期钉死;执行期只读) */
|
/** 步骤调用参数。普通节点:编排期钉死、执行只读。〔先验产物〕:编排器规划的产物内容(可空),执行时注入为【规划产物】。 */
|
||||||
export type CreationFlowStepParams = Record<string, unknown>;
|
export type CreationFlowStepParams = Record<string, unknown>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 节点特性。缺省 = 普通执行步。后续会加更多 kind。
|
||||||
|
* prior-artifact = 先验产物:须先定「写什么」(生成规则、具体实例);编排器可提前规划,步内再钉/修订。
|
||||||
|
*/
|
||||||
|
export const MODULE_NODE_KINDS = ["prior-artifact"] as const;
|
||||||
|
export type ModuleNodeKind = (typeof MODULE_NODE_KINDS)[number];
|
||||||
|
|
||||||
|
export const MODULE_NODE_KIND_FLAGS: Record<ModuleNodeKind, string> = {
|
||||||
|
"prior-artifact": "〔先验产物〕",
|
||||||
|
};
|
||||||
|
|
||||||
|
export function parseModuleNodeKind(raw: unknown): ModuleNodeKind | undefined {
|
||||||
|
if (typeof raw !== "string") return undefined;
|
||||||
|
const key = raw.trim();
|
||||||
|
return (MODULE_NODE_KINDS as readonly string[]).includes(key)
|
||||||
|
? (key as ModuleNodeKind)
|
||||||
|
: undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isPriorArtifactModule(
|
||||||
|
module: { kind?: ModuleNodeKind } | null | undefined,
|
||||||
|
): boolean {
|
||||||
|
return module?.kind === "prior-artifact";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 本步怎么跑:
|
||||||
|
* - fresh(缺省)= 从零产出。〔可反复〕再编入新 id 必须用这个——彻底新建,不改旧条。
|
||||||
|
* - revise = 回头修改:继承 `revises` 所指那步的既有产物继续改。
|
||||||
|
*/
|
||||||
|
export type CreationFlowStepMode = "fresh" | "revise";
|
||||||
|
|
||||||
export type CreationFlowStep = {
|
export type CreationFlowStep = {
|
||||||
/**
|
/**
|
||||||
* 本局步骤唯一 id(验收与 depends_on 用这个)。
|
* 本局步骤唯一 id(验收与 depends_on 用这个)。
|
||||||
@@ -67,10 +99,15 @@ export type CreationFlowStep = {
|
|||||||
/** 依赖的其它步骤 id(旧稿若 name 唯一也可写 name) */
|
/** 依赖的其它步骤 id(旧稿若 name 唯一也可写 name) */
|
||||||
depends_on: string[];
|
depends_on: string[];
|
||||||
/**
|
/**
|
||||||
* 本步调用参数。有「编排参数」声明的能力必须在进 design-step 前钉齐必填项。
|
* 本步调用参数。
|
||||||
* 例:生成规则 → target;具体实例 → rule_id。
|
* 普通节点:有必填声明时须在进执行前钉齐。
|
||||||
|
* 〔先验产物〕:编排器规划的产物内容(可空);执行注入【规划产物】,步内可修订。
|
||||||
*/
|
*/
|
||||||
params?: CreationFlowStepParams;
|
params?: CreationFlowStepParams;
|
||||||
|
/** 缺省 fresh。revise = 继承旧产物修改,不是再生成一条。 */
|
||||||
|
mode?: CreationFlowStepMode;
|
||||||
|
/** mode=revise 时:被改的既有步骤 id(须更前、同 name) */
|
||||||
|
revises?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CreationFlowStatus = "open" | "closed";
|
export type CreationFlowStatus = "open" | "closed";
|
||||||
@@ -111,14 +148,29 @@ export type ModuleCatalogEntry = {
|
|||||||
* 程序不硬拦;给编排与校验提示。
|
* 程序不硬拦;给编排与校验提示。
|
||||||
*/
|
*/
|
||||||
repeatable?: boolean;
|
repeatable?: boolean;
|
||||||
|
/**
|
||||||
|
* 可选:创作终节点。选定后程序收口并保存,其后不要再追加步骤。
|
||||||
|
*/
|
||||||
|
closer?: boolean;
|
||||||
|
/**
|
||||||
|
* 可选:程序步。不抛默认问题、不经「同意并开始」,提案后直接执行;
|
||||||
|
* 产物仍走验收。适合投影排序这类只排序、几乎不问用户的收成步。
|
||||||
|
*/
|
||||||
|
auto?: boolean;
|
||||||
|
/**
|
||||||
|
* 可选:节点特性。缺省为普通执行步。
|
||||||
|
* prior-artifact = 先验产物,须先定写什么;params 是规划产物而非拦执行的必填项。
|
||||||
|
*/
|
||||||
|
kind?: ModuleNodeKind;
|
||||||
/**
|
/**
|
||||||
* 可选:默认问题(开场白)。优先用 prompt.md 的 ```opening 块;
|
* 可选:默认问题(开场白)。优先用 prompt.md 的 ```opening 块;
|
||||||
* catalog 写了则作覆盖。程序发出,不经 LLM。
|
* catalog 写了则作覆盖。程序发出,不经 LLM。
|
||||||
*/
|
*/
|
||||||
opening?: string;
|
opening?: string;
|
||||||
/**
|
/**
|
||||||
* 可选:编排期步骤参数声明。
|
* 可选:步骤参数声明。
|
||||||
* 有 required 项时,steps[].params 必须在进执行前钉齐;勿把选型推迟到 design-step。
|
* 普通节点:required 须在进执行前钉齐。
|
||||||
|
* 〔先验产物〕:规划产物字段;可提前写入 params,空则步内钉,不拦确认开干。
|
||||||
*/
|
*/
|
||||||
params?: ModuleParamSpec[];
|
params?: ModuleParamSpec[];
|
||||||
/** 来自 prompt.md ```meta:何时该选用(编排选型) */
|
/** 来自 prompt.md ```meta:何时该选用(编排选型) */
|
||||||
@@ -191,15 +243,21 @@ export type CreationFlowUserView = {
|
|||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
depends_on: string[];
|
depends_on: string[];
|
||||||
/** 同能力第几次(>1 时 UI 可标「再来」) */
|
/** 同能力第几次(>1 时 UI 可标「再来」;回头修改不计入「第 N 次新建」) */
|
||||||
occurrence?: number;
|
occurrence?: number;
|
||||||
|
/** fresh=从零新建;revise=回头修改 */
|
||||||
|
mode?: CreationFlowStepMode;
|
||||||
|
/** 回头修改时:被改的既有步骤 id */
|
||||||
|
revises?: string;
|
||||||
/** 目录里的短声明(有则展示) */
|
/** 目录里的短声明(有则展示) */
|
||||||
declaration?: string;
|
declaration?: string;
|
||||||
repeatable?: boolean;
|
repeatable?: boolean;
|
||||||
/** 本步调用参数(编排期钉死) */
|
/** 本步调用参数(普通节点编排钉死;先验产物=规划内容) */
|
||||||
params?: CreationFlowStepParams;
|
params?: CreationFlowStepParams;
|
||||||
/** 参数缺必填项时的提示(给人看) */
|
/** 参数缺必填项时的提示(给人看);先验产物不拦执行,不出现此项 */
|
||||||
paramsMissing?: string[];
|
paramsMissing?: string[];
|
||||||
|
/** 节点特性(如先验产物) */
|
||||||
|
kind?: ModuleNodeKind;
|
||||||
/** 已执行 / 将要执行 / 未执行 */
|
/** 已执行 / 将要执行 / 未执行 */
|
||||||
runState: FlowStepRunState;
|
runState: FlowStepRunState;
|
||||||
}>;
|
}>;
|
||||||
@@ -241,11 +299,12 @@ export function normalizeStepParams(
|
|||||||
return Object.keys(out).length > 0 ? out : undefined;
|
return Object.keys(out).length > 0 ? out : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 目录声明的必填参数中,本步仍缺失的 key(按声明顺序) */
|
/** 目录声明的必填参数中,本步仍缺失的 key(按声明顺序)。〔先验产物〕不拦执行,恒为 []。 */
|
||||||
export function missingRequiredStepParams(
|
export function missingRequiredStepParams(
|
||||||
step: Pick<CreationFlowStep, "params">,
|
step: Pick<CreationFlowStep, "params">,
|
||||||
module: ModuleCatalogEntry | null | undefined,
|
module: ModuleCatalogEntry | null | undefined,
|
||||||
): string[] {
|
): string[] {
|
||||||
|
if (isPriorArtifactModule(module)) return [];
|
||||||
const specs = module?.params ?? [];
|
const specs = module?.params ?? [];
|
||||||
if (specs.length === 0) return [];
|
if (specs.length === 0) return [];
|
||||||
const params = step.params ?? {};
|
const params = step.params ?? {};
|
||||||
@@ -278,14 +337,120 @@ export function formatStepParamsForPrompt(
|
|||||||
.join("\n");
|
.join("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 为缺 id 的步骤补齐唯一 id;同 name 多次 → name#2、name#3… */
|
const PRIOR_ARTIFACT_CONTEXT_TITLE =
|
||||||
export function ensureCreationFlowStepIds(
|
"## 【规划产物】编排器预先为本步规划的产物内容(先验;缺则步内钉;步内可修订)";
|
||||||
steps: Array<{
|
|
||||||
|
/** 〔先验产物〕注入块:规划可空,身份写在片段自身。 */
|
||||||
|
export function formatPriorArtifactContext(
|
||||||
|
params: CreationFlowStepParams | null | undefined,
|
||||||
|
): string {
|
||||||
|
const body = formatStepParamsForPrompt(params);
|
||||||
|
const note =
|
||||||
|
!params || Object.keys(params).length === 0
|
||||||
|
? "编排器尚未规划本步具体写什么。先在本步与用户钉「写什么」,再填产物。"
|
||||||
|
: "以上为先验规划,不是锁死合同。用户改对象或范围时,以本步最新认定为准,并重钉对应英文 id。";
|
||||||
|
return `${PRIOR_ARTIFACT_CONTEXT_TITLE}\n\n${body}\n\n${note}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const REVISE_MODE_ALIASES = new Set([
|
||||||
|
"revise",
|
||||||
|
"amend",
|
||||||
|
"edit",
|
||||||
|
"修改",
|
||||||
|
"回头修改",
|
||||||
|
"继承修改",
|
||||||
|
]);
|
||||||
|
const FRESH_MODE_ALIASES = new Set([
|
||||||
|
"fresh",
|
||||||
|
"new",
|
||||||
|
"create",
|
||||||
|
"新建",
|
||||||
|
"再来",
|
||||||
|
"再来一次",
|
||||||
|
]);
|
||||||
|
|
||||||
|
/** 解析 DAG 步骤的 mode;无法识别则视为缺省(fresh) */
|
||||||
|
export function parseCreationFlowStepMode(raw: unknown): CreationFlowStepMode | undefined {
|
||||||
|
if (typeof raw !== "string") return undefined;
|
||||||
|
const key = raw.trim().toLowerCase();
|
||||||
|
if (!key) return undefined;
|
||||||
|
if (REVISE_MODE_ALIASES.has(key) || REVISE_MODE_ALIASES.has(raw.trim())) {
|
||||||
|
return "revise";
|
||||||
|
}
|
||||||
|
if (FRESH_MODE_ALIASES.has(key) || FRESH_MODE_ALIASES.has(raw.trim())) {
|
||||||
|
return "fresh";
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isReviseStep(
|
||||||
|
step: Pick<CreationFlowStep, "mode"> | null | undefined,
|
||||||
|
): boolean {
|
||||||
|
return step?.mode === "revise";
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseStepRevisesRef(raw: Record<string, unknown>): string | undefined {
|
||||||
|
for (const key of ["revises", "revise_of", "revises_id"] as const) {
|
||||||
|
const v = raw[key];
|
||||||
|
if (typeof v === "string" && v.trim()) return v.trim();
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
type DraftCreationFlowStep = {
|
||||||
id?: string;
|
id?: string;
|
||||||
name: string;
|
name: string;
|
||||||
depends_on: string[];
|
depends_on: string[];
|
||||||
params?: CreationFlowStepParams;
|
params?: CreationFlowStepParams;
|
||||||
}>,
|
mode?: CreationFlowStepMode;
|
||||||
|
revises?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function withStepModeFields(
|
||||||
|
step: DraftCreationFlowStep,
|
||||||
|
): Pick<CreationFlowStep, "mode" | "revises"> {
|
||||||
|
return {
|
||||||
|
...(step.mode === "revise" ? { mode: "revise" as const } : {}),
|
||||||
|
...(step.revises ? { revises: step.revises } : {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 回头修改步:补 revises(最近一条同名更前步),并继承其 params(本步已写的优先)。
|
||||||
|
*/
|
||||||
|
export function normalizeReviseSteps(
|
||||||
|
steps: CreationFlowStep[],
|
||||||
|
): CreationFlowStep[] {
|
||||||
|
return steps.map((step, i) => {
|
||||||
|
if (step.mode !== "revise") return step;
|
||||||
|
let revises = step.revises?.trim() || "";
|
||||||
|
if (!revises) {
|
||||||
|
for (let j = i - 1; j >= 0; j--) {
|
||||||
|
if (steps[j]?.name === step.name) {
|
||||||
|
revises = steps[j]!.id;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const origin = revises
|
||||||
|
? steps.find((s) => s.id === revises || s.name === revises)
|
||||||
|
: undefined;
|
||||||
|
const inherited = origin?.params;
|
||||||
|
const params =
|
||||||
|
inherited && Object.keys(inherited).length
|
||||||
|
? { ...inherited, ...(step.params ?? {}) }
|
||||||
|
: step.params;
|
||||||
|
return {
|
||||||
|
...step,
|
||||||
|
...(revises ? { revises } : {}),
|
||||||
|
...(params && Object.keys(params).length ? { params } : {}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 为缺 id 的步骤补齐唯一 id;同 name 多次 → name#n;回头修改 → name·改 */
|
||||||
|
export function ensureCreationFlowStepIds(
|
||||||
|
steps: Array<DraftCreationFlowStep>,
|
||||||
): CreationFlowStep[] {
|
): CreationFlowStep[] {
|
||||||
const used = new Set<string>();
|
const used = new Set<string>();
|
||||||
const nameCount = new Map<string, number>();
|
const nameCount = new Map<string, number>();
|
||||||
@@ -295,11 +460,21 @@ export function ensureCreationFlowStepIds(
|
|||||||
const name = raw.name.trim();
|
const name = raw.name.trim();
|
||||||
const n = (nameCount.get(name) ?? 0) + 1;
|
const n = (nameCount.get(name) ?? 0) + 1;
|
||||||
nameCount.set(name, n);
|
nameCount.set(name, n);
|
||||||
|
const revise = raw.mode === "revise";
|
||||||
|
|
||||||
let id = typeof raw.id === "string" ? raw.id.trim() : "";
|
let id = typeof raw.id === "string" ? raw.id.trim() : "";
|
||||||
if (!id) {
|
if (!id) {
|
||||||
|
if (revise) {
|
||||||
|
id = n === 1 ? `${name}·改` : `${name}·改`;
|
||||||
|
if (used.has(id)) {
|
||||||
|
let i = 2;
|
||||||
|
while (used.has(`${name}·改#${i}`)) i++;
|
||||||
|
id = `${name}·改#${i}`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
id = n === 1 ? name : `${name}#${n}`;
|
id = n === 1 ? name : `${name}#${n}`;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (used.has(id)) {
|
if (used.has(id)) {
|
||||||
let i = 2;
|
let i = 2;
|
||||||
while (used.has(`${id}#${i}`)) i++;
|
while (used.has(`${id}#${i}`)) i++;
|
||||||
@@ -311,9 +486,10 @@ export function ensureCreationFlowStepIds(
|
|||||||
name,
|
name,
|
||||||
depends_on: raw.depends_on.map((d) => d.trim()).filter(Boolean),
|
depends_on: raw.depends_on.map((d) => d.trim()).filter(Boolean),
|
||||||
...(raw.params ? { params: raw.params } : {}),
|
...(raw.params ? { params: raw.params } : {}),
|
||||||
|
...withStepModeFields(raw),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return out;
|
return normalizeReviseSteps(out);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 按 id 或(唯一)name 解析步骤引用 */
|
/** 按 id 或(唯一)name 解析步骤引用 */
|
||||||
@@ -378,12 +554,7 @@ export function parseCreationFlow(raw: string | undefined | null): CreationFlow
|
|||||||
const stepsRaw = row.steps;
|
const stepsRaw = row.steps;
|
||||||
if (!Array.isArray(stepsRaw) || stepsRaw.length === 0) return null;
|
if (!Array.isArray(stepsRaw) || stepsRaw.length === 0) return null;
|
||||||
|
|
||||||
const drafted: Array<{
|
const drafted: DraftCreationFlowStep[] = [];
|
||||||
id?: string;
|
|
||||||
name: string;
|
|
||||||
depends_on: string[];
|
|
||||||
params?: CreationFlowStepParams;
|
|
||||||
}> = [];
|
|
||||||
for (const item of stepsRaw) {
|
for (const item of stepsRaw) {
|
||||||
if (!item || typeof item !== "object" || Array.isArray(item)) return null;
|
if (!item || typeof item !== "object" || Array.isArray(item)) return null;
|
||||||
const s = item as Record<string, unknown>;
|
const s = item as Record<string, unknown>;
|
||||||
@@ -395,7 +566,17 @@ export function parseCreationFlow(raw: string | undefined | null): CreationFlow
|
|||||||
? depsRaw.map((d) => String(d).trim()).filter(Boolean)
|
? depsRaw.map((d) => String(d).trim()).filter(Boolean)
|
||||||
: [];
|
: [];
|
||||||
const params = normalizeStepParams(s.params);
|
const params = normalizeStepParams(s.params);
|
||||||
drafted.push({ id, name, depends_on, ...(params ? { params } : {}) });
|
const mode =
|
||||||
|
parseCreationFlowStepMode(s.mode) ?? parseCreationFlowStepMode(s.intent);
|
||||||
|
const revises = parseStepRevisesRef(s);
|
||||||
|
drafted.push({
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
depends_on,
|
||||||
|
...(params ? { params } : {}),
|
||||||
|
...(mode ? { mode } : {}),
|
||||||
|
...(revises ? { revises } : {}),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const steps = ensureCreationFlowStepIds(drafted);
|
const steps = ensureCreationFlowStepIds(drafted);
|
||||||
@@ -440,6 +621,9 @@ export function parseModuleCatalog(raw: string): ModuleCatalog | null {
|
|||||||
? m.opening.trim()
|
? m.opening.trim()
|
||||||
: undefined;
|
: undefined;
|
||||||
const repeatable = m.repeatable === true;
|
const repeatable = m.repeatable === true;
|
||||||
|
const closer = m.closer === true;
|
||||||
|
const auto = m.auto === true;
|
||||||
|
const kind = parseModuleNodeKind(m.kind);
|
||||||
const params = parseModuleParamSpecs(m.params);
|
const params = parseModuleParamSpecs(m.params);
|
||||||
modules.push({
|
modules.push({
|
||||||
id,
|
id,
|
||||||
@@ -447,6 +631,9 @@ export function parseModuleCatalog(raw: string): ModuleCatalog | null {
|
|||||||
declaration,
|
declaration,
|
||||||
artifact,
|
artifact,
|
||||||
...(repeatable ? { repeatable: true } : {}),
|
...(repeatable ? { repeatable: true } : {}),
|
||||||
|
...(closer ? { closer: true } : {}),
|
||||||
|
...(auto ? { auto: true } : {}),
|
||||||
|
...(kind ? { kind } : {}),
|
||||||
...(opening ? { opening } : {}),
|
...(opening ? { opening } : {}),
|
||||||
...(params ? { params } : {}),
|
...(params ? { params } : {}),
|
||||||
});
|
});
|
||||||
@@ -702,7 +889,9 @@ export function mergeCreationFlowPreservingAccepted(params: {
|
|||||||
const changed =
|
const changed =
|
||||||
current.name !== frozen.name ||
|
current.name !== frozen.name ||
|
||||||
JSON.stringify(current.depends_on) !== JSON.stringify(frozen.depends_on) ||
|
JSON.stringify(current.depends_on) !== JSON.stringify(frozen.depends_on) ||
|
||||||
JSON.stringify(current.params ?? null) !== JSON.stringify(frozen.params ?? null);
|
JSON.stringify(current.params ?? null) !== JSON.stringify(frozen.params ?? null) ||
|
||||||
|
(current.mode ?? "fresh") !== (frozen.mode ?? "fresh") ||
|
||||||
|
(current.revises ?? "") !== (frozen.revises ?? "");
|
||||||
if (changed) {
|
if (changed) {
|
||||||
steps[at] = frozen;
|
steps[at] = frozen;
|
||||||
restored.push(frozen.id);
|
restored.push(frozen.id);
|
||||||
@@ -795,12 +984,29 @@ export async function loadModuleCatalog(
|
|||||||
/** 注入 design-flow:能力名 + 选型字段(meta)+ 编排参数;非执行全文 */
|
/** 注入 design-flow:能力名 + 选型字段(meta)+ 编排参数;非执行全文 */
|
||||||
export function formatModuleCatalogForAgent(catalog: ModuleCatalog): string {
|
export function formatModuleCatalogForAgent(catalog: ModuleCatalog): string {
|
||||||
const lines = catalog.modules.map((m) => {
|
const lines = catalog.modules.map((m) => {
|
||||||
const flags = m.repeatable ? "〔可反复〕" : "";
|
const flags = [
|
||||||
|
m.repeatable ? "〔可反复〕" : "",
|
||||||
|
m.closer ? "〔收口〕" : "",
|
||||||
|
m.auto ? "〔程序步〕" : "",
|
||||||
|
m.kind ? MODULE_NODE_KIND_FLAGS[m.kind] : "",
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join("");
|
||||||
const parts: string[] = [`- ${m.name}${flags}:${m.declaration}`];
|
const parts: string[] = [`- ${m.name}${flags}:${m.declaration}`];
|
||||||
if (m.when) parts.push(` 何时用:${indentMultiline(m.when, " ")}`);
|
if (m.when) parts.push(` 何时用:${indentMultiline(m.when, " ")}`);
|
||||||
if (m.when_not) parts.push(` 何时不用:${indentMultiline(m.when_not, " ")}`);
|
if (m.when_not) parts.push(` 何时不用:${indentMultiline(m.when_not, " ")}`);
|
||||||
if (m.boundary) parts.push(` 边界:${indentMultiline(m.boundary, " ")}`);
|
if (m.boundary) parts.push(` 边界:${indentMultiline(m.boundary, " ")}`);
|
||||||
if (m.params && m.params.length > 0) {
|
if (m.params && m.params.length > 0) {
|
||||||
|
if (isPriorArtifactModule(m)) {
|
||||||
|
parts.push(
|
||||||
|
` 规划产物字段(可提前写入 params;空则步内钉):${m.params
|
||||||
|
.map((p) => {
|
||||||
|
const hint = p.hint ? `,${p.hint}` : "";
|
||||||
|
return `${p.key}(${p.label}${hint})`;
|
||||||
|
})
|
||||||
|
.join(";")}`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
parts.push(
|
parts.push(
|
||||||
` 编排参数:${m.params
|
` 编排参数:${m.params
|
||||||
.map((p) => {
|
.map((p) => {
|
||||||
@@ -811,13 +1017,16 @@ export function formatModuleCatalogForAgent(catalog: ModuleCatalog): string {
|
|||||||
.join(";")}`,
|
.join(";")}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return parts.join("\n");
|
return parts.join("\n");
|
||||||
});
|
});
|
||||||
return [
|
return [
|
||||||
"【能力 · 可选工序】",
|
"【能力 · 可选工序】",
|
||||||
"按需选用,勿默认全选;步骤名只能从这里选;标〔可反复〕的可多次编入。",
|
"按需选用,勿默认全选;步骤名只能从这里选;标〔可反复〕的可多次编入;标〔收口〕的是终节点:选定后结束创作并保存,排在细化终稿之后,其后不要再追加步骤;标〔程序步〕的确认编排后直接执行(不抛默认问题、不经同意并开始),产物仍验收。",
|
||||||
"选型依据是下方「何时用 / 何时不用 / 边界」(来自各能力 meta);有「编排参数」的步骤必须在 DAG 里写齐 params,缺参时用 askUser 选项+其它,禁止空壳进执行。",
|
"标〔先验产物〕的步骤(如生成规则、具体实例):须先定「写什么」。编排器可以提前规划多条并写入 params——这是正确的;执行时程序注入【规划产物】。规划可空,「生成什么」在步内钉。禁止为钉对象而 askUser 拦在确认开干前。规划或修订对象时同步改 rule_id(英文 kebab-case)与步骤 id(如 生成规则·{对象}),禁止只改中文沿用旧后台 id。",
|
||||||
"生成规则:改 target 时必须同步改 rule_id(英文 kebab-case)与步骤 id(生成规则·{对象});禁止只改中文对象沿用旧后台 id。",
|
"其它有「编排参数」的步骤:确认开干前写齐必填 params;缺参时用 askUser 选项+其它。",
|
||||||
|
"选型依据是下方「何时用 / 何时不用 / 边界」(来自各能力 meta)。",
|
||||||
|
"两种「再来一次」必须分开:〔可反复〕再编入新 id(mode 缺省/fresh)= 彻底新建一条;回头修改已完成节点 = mode=revise + revises=原步 id,继承旧产物继续改。禁止把回头修改写成又一条 fresh。",
|
||||||
"不要把能力执行全文塞进本步;执行由 design-step 注入。",
|
"不要把能力执行全文塞进本步;执行由 design-step 注入。",
|
||||||
lines.join("\n"),
|
lines.join("\n"),
|
||||||
].join("\n");
|
].join("\n");
|
||||||
@@ -1153,8 +1362,8 @@ export function formatFlowProgressForAgent(params: {
|
|||||||
repeatable: boolean,
|
repeatable: boolean,
|
||||||
) => {
|
) => {
|
||||||
const flag = repeatable
|
const flag = repeatable
|
||||||
? "〔可反复:可再追加不同 id〕"
|
? "〔可反复:mode=fresh 再追加 = 彻底新建;要改旧条用 mode=revise〕"
|
||||||
: "〔不可反复:禁止再排一次执行〕";
|
: "〔不可反复:禁止再排新建;要改已完成节点用 mode=revise 回头修改〕";
|
||||||
const idBit = id ? `(id: ${id})` : "";
|
const idBit = id ? `(id: ${id})` : "";
|
||||||
doneLines.push(`- ${name}${idBit} · ${why} ${flag}`);
|
doneLines.push(`- ${name}${idBit} · ${why} ${flag}`);
|
||||||
doneNames.add(name);
|
doneNames.add(name);
|
||||||
@@ -1196,9 +1405,12 @@ export function formatFlowProgressForAgent(params: {
|
|||||||
const draftLines: string[] = [];
|
const draftLines: string[] = [];
|
||||||
if (flow) {
|
if (flow) {
|
||||||
for (const step of flow.steps) {
|
for (const step of flow.steps) {
|
||||||
if (doneIds.has(step.id) || doneNames.has(step.name)) continue;
|
if (doneIds.has(step.id) || isStepAccepted(step, accepted)) continue;
|
||||||
|
const reviseBit = isReviseStep(step)
|
||||||
|
? ` · 回头修改${step.revises ? `(revises: ${step.revises})` : ""}`
|
||||||
|
: "";
|
||||||
draftLines.push(
|
draftLines.push(
|
||||||
`- ${step.name}(id: ${step.id})· 已在草案,保留原 id;不要当作新规划再写一遍`,
|
`- ${step.name}(id: ${step.id})${reviseBit} · 已在草案,保留原 id;不要当作新规划再写一遍`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1210,9 +1422,9 @@ export function formatFlowProgressForAgent(params: {
|
|||||||
const lines = [
|
const lines = [
|
||||||
"【流程进度】(程序钉死,必须遵守)",
|
"【流程进度】(程序钉死,必须遵守)",
|
||||||
"你的任务是追加缺口,不是从头规划。",
|
"你的任务是追加缺口,不是从头规划。",
|
||||||
"已完成的非反复技能:只保留原 id 作 depends_on 锚点,禁止再作为将要执行的一步。",
|
"已完成的非反复技能:禁止再排一次「新建」(mode=fresh)。用户要改已完成节点 → 追加新 id,mode=revise,revises=原步 id;程序会继承既有产物。",
|
||||||
"草案里已有的步骤:原样保留,不要重排一遍开局。",
|
"〔可反复〕再编入新 id 且 mode 缺省/fresh = 彻底新建一条(新对象/新批次),不是改旧的。要改已有那一条 → 同样用 mode=revise。",
|
||||||
"只有能力目录标了〔可反复〕的(如生成规则、具体实例)才允许再编入新 id。",
|
"草案里已有的步骤:原样保留,不要重排一遍开局。回头修改步插在最近已验收步之后、尚未执行的步之前。",
|
||||||
];
|
];
|
||||||
if (doneLines.length) {
|
if (doneLines.length) {
|
||||||
lines.push("", "已完成:", ...doneLines);
|
lines.push("", "已完成:", ...doneLines);
|
||||||
@@ -1258,10 +1470,7 @@ export function validateCreationFlow(
|
|||||||
const allowed = catalog
|
const allowed = catalog
|
||||||
? new Set(catalog.modules.map((m) => m.name))
|
? new Set(catalog.modules.map((m) => m.name))
|
||||||
: null;
|
: null;
|
||||||
const nameCount = new Map<string, number>();
|
const reportedFreshDup = new Set<string>();
|
||||||
for (const step of flow.steps) {
|
|
||||||
nameCount.set(step.name, (nameCount.get(step.name) ?? 0) + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < flow.steps.length; i++) {
|
for (let i = 0; i < flow.steps.length; i++) {
|
||||||
const step = flow.steps[i]!;
|
const step = flow.steps[i]!;
|
||||||
@@ -1275,14 +1484,44 @@ export function validateCreationFlow(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const mod = catalog?.modules.find((m) => m.name === step.name);
|
const mod = catalog?.modules.find((m) => m.name === step.name);
|
||||||
if (
|
if (isReviseStep(step)) {
|
||||||
catalog &&
|
const ref = step.revises?.trim() ?? "";
|
||||||
(nameCount.get(step.name) ?? 0) > 1 &&
|
if (!ref) {
|
||||||
mod &&
|
|
||||||
mod.repeatable !== true
|
|
||||||
) {
|
|
||||||
errors.push(
|
errors.push(
|
||||||
`「${step.name}」出现多次,但目录未标 repeatable(仅可反复能力可同名多次)`,
|
`「${step.id}」是回头修改,但未标明 revises(应指向更前的同名步骤 id)`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const origin = findStepByRef(flow, ref);
|
||||||
|
if (!origin) {
|
||||||
|
errors.push(
|
||||||
|
`「${step.id}」回头修改「${ref}」,但流程中没有该步骤`,
|
||||||
|
);
|
||||||
|
} else if (origin.id === step.id) {
|
||||||
|
errors.push(`「${step.id}」不能回头修改自己`);
|
||||||
|
} else if (origin.name !== step.name) {
|
||||||
|
errors.push(
|
||||||
|
`「${step.id}」回头修改「${origin.id}」,但能力名不同(${step.name} ≠ ${origin.name})`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const originIndex = flow.steps.findIndex((s) => s.id === origin.id);
|
||||||
|
if (originIndex >= i) {
|
||||||
|
errors.push(
|
||||||
|
`「${step.id}」回头修改「${origin.id}」,但「${origin.id}」未排在其前面`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
catalog &&
|
||||||
|
mod &&
|
||||||
|
mod.repeatable !== true &&
|
||||||
|
!reportedFreshDup.has(step.name) &&
|
||||||
|
flow.steps.filter((s) => s.name === step.name && !isReviseStep(s)).length >
|
||||||
|
1
|
||||||
|
) {
|
||||||
|
reportedFreshDup.add(step.name);
|
||||||
|
errors.push(
|
||||||
|
`「${step.name}」出现多次新建,但目录未标 repeatable(非反复技能要改旧稿请用 mode=revise)`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1306,7 +1545,7 @@ export function validateCreationFlow(
|
|||||||
return { ok: errors.length === 0, errors };
|
return { ok: errors.length === 0, errors };
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 进 design-step 前:本步必填编排参数是否已齐 */
|
/** 进 design-step 前:本步必填编排参数是否已齐。〔先验产物〕不拦。 */
|
||||||
export function validateStepReadyToRun(
|
export function validateStepReadyToRun(
|
||||||
step: CreationFlowStep,
|
step: CreationFlowStep,
|
||||||
catalog: ModuleCatalog | null,
|
catalog: ModuleCatalog | null,
|
||||||
@@ -1362,14 +1601,19 @@ export function formatCreationFlowForUser(
|
|||||||
const decl = new Map(
|
const decl = new Map(
|
||||||
(catalog?.modules ?? []).map((m) => [m.name, m] as const),
|
(catalog?.modules ?? []).map((m) => [m.name, m] as const),
|
||||||
);
|
);
|
||||||
const seenName = new Map<string, number>();
|
const seenFreshName = new Map<string, number>();
|
||||||
const next = nextPendingStep(flow, acceptedStepIds);
|
const next = nextPendingStep(flow, acceptedStepIds);
|
||||||
return {
|
return {
|
||||||
brief: flow.brief,
|
brief: flow.brief,
|
||||||
status: flow.status,
|
status: flow.status,
|
||||||
steps: flow.steps.map((s, i) => {
|
steps: flow.steps.map((s, i) => {
|
||||||
const n = (seenName.get(s.name) ?? 0) + 1;
|
const revise = isReviseStep(s);
|
||||||
seenName.set(s.name, n);
|
let occurrence: number | undefined;
|
||||||
|
if (!revise) {
|
||||||
|
const n = (seenFreshName.get(s.name) ?? 0) + 1;
|
||||||
|
seenFreshName.set(s.name, n);
|
||||||
|
occurrence = n;
|
||||||
|
}
|
||||||
const mod = decl.get(s.name);
|
const mod = decl.get(s.name);
|
||||||
const paramsMissing = missingRequiredStepParams(s, mod);
|
const paramsMissing = missingRequiredStepParams(s, mod);
|
||||||
const runState: FlowStepRunState = isStepAccepted(s, acceptedStepIds)
|
const runState: FlowStepRunState = isStepAccepted(s, acceptedStepIds)
|
||||||
@@ -1382,9 +1626,12 @@ export function formatCreationFlowForUser(
|
|||||||
id: s.id,
|
id: s.id,
|
||||||
name: s.name,
|
name: s.name,
|
||||||
depends_on: s.depends_on,
|
depends_on: s.depends_on,
|
||||||
occurrence: n,
|
occurrence,
|
||||||
|
...(revise ? { mode: "revise" as const } : {}),
|
||||||
|
...(s.revises ? { revises: s.revises } : {}),
|
||||||
declaration: mod?.declaration,
|
declaration: mod?.declaration,
|
||||||
repeatable: mod?.repeatable,
|
repeatable: mod?.repeatable,
|
||||||
|
...(mod?.kind ? { kind: mod.kind } : {}),
|
||||||
runState,
|
runState,
|
||||||
...(s.params ? { params: s.params } : {}),
|
...(s.params ? { params: s.params } : {}),
|
||||||
...(paramsMissing.length > 0 ? { paramsMissing } : {}),
|
...(paramsMissing.length > 0 ? { paramsMissing } : {}),
|
||||||
@@ -1443,6 +1690,7 @@ export function nextPendingStep(
|
|||||||
acceptedStepIds: readonly string[],
|
acceptedStepIds: readonly string[],
|
||||||
): CreationFlowStep | null {
|
): CreationFlowStep | null {
|
||||||
if (!flow?.steps.length) return null;
|
if (!flow?.steps.length) return null;
|
||||||
|
const ready: CreationFlowStep[] = [];
|
||||||
for (const step of flow.steps) {
|
for (const step of flow.steps) {
|
||||||
if (isStepAccepted(step, acceptedStepIds)) continue;
|
if (isStepAccepted(step, acceptedStepIds)) continue;
|
||||||
const depsOk = step.depends_on.every((dep) => {
|
const depsOk = step.depends_on.every((dep) => {
|
||||||
@@ -1450,9 +1698,9 @@ export function nextPendingStep(
|
|||||||
if (!depStep) return false;
|
if (!depStep) return false;
|
||||||
return isStepAccepted(depStep, acceptedStepIds);
|
return isStepAccepted(depStep, acceptedStepIds);
|
||||||
});
|
});
|
||||||
if (depsOk) return step;
|
if (depsOk) ready.push(step);
|
||||||
}
|
}
|
||||||
return null;
|
return ready.find((s) => isReviseStep(s)) ?? ready[0] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 当前已列出的步骤是否都已验收(不管 status) */
|
/** 当前已列出的步骤是否都已验收(不管 status) */
|
||||||
@@ -1532,8 +1780,10 @@ export type DesignStepBinding = {
|
|||||||
module: ModuleCatalogEntry;
|
module: ModuleCatalogEntry;
|
||||||
depTags: string[];
|
depTags: string[];
|
||||||
modulePrompt: string;
|
modulePrompt: string;
|
||||||
/** 程序开场白;无则本步直接调 LLM */
|
/** 程序开场白;无则本步直接调 LLM。回头修改不抛 opening。 */
|
||||||
opening: string | null;
|
opening: string | null;
|
||||||
|
/** 回头修改时注入的既有产物 tag */
|
||||||
|
inheritTag: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1570,7 +1820,9 @@ export async function resolveDesignStepBinding(params: {
|
|||||||
`# ${module.name}\n\n(模块 prompt.md 缺失,请补充 skills/.../modules/${module.id}/prompt.md)`;
|
`# ${module.name}\n\n(模块 prompt.md 缺失,请补充 skills/.../modules/${module.id}/prompt.md)`;
|
||||||
|
|
||||||
const opening =
|
const opening =
|
||||||
module.opening?.trim() || extractModuleOpening(modulePromptRaw) || null;
|
isReviseStep(step) || module.auto
|
||||||
|
? null
|
||||||
|
: module.opening?.trim() || extractModuleOpening(modulePromptRaw) || null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
step,
|
step,
|
||||||
@@ -1578,5 +1830,6 @@ export async function resolveDesignStepBinding(params: {
|
|||||||
depTags: dependencyArtifactTags(step, catalog, flow),
|
depTags: dependencyArtifactTags(step, catalog, flow),
|
||||||
modulePrompt: formatModulePromptForLlm(modulePromptRaw),
|
modulePrompt: formatModulePromptForLlm(modulePromptRaw),
|
||||||
opening,
|
opening,
|
||||||
|
inheritTag: isReviseStep(step) ? module.artifact : null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import {
|
|||||||
slotOrderForRef,
|
slotOrderForRef,
|
||||||
} from "./context-order.js";
|
} from "./context-order.js";
|
||||||
import type { ContextSegmentDef } from "./context-segments.js";
|
import type { ContextSegmentDef } from "./context-segments.js";
|
||||||
|
import { PLAY_VISIBLE_BODY_INSTRUCTION } from "./present-packet.js";
|
||||||
|
|
||||||
type WorkerTemplateDoc = {
|
type WorkerTemplateDoc = {
|
||||||
id?: string;
|
id?: string;
|
||||||
@@ -44,8 +45,9 @@ type WorkerTemplateDoc = {
|
|||||||
/**
|
/**
|
||||||
* 解析本次 worker 的验收模式(创作 / run 共用入口)。
|
* 解析本次 worker 的验收模式(创作 / run 共用入口)。
|
||||||
* - design-* / opening-generator 磁盘创作 worker → 始终 user_confirmed
|
* - design-* / opening-generator 磁盘创作 worker → 始终 user_confirmed
|
||||||
|
* - 游玩回合:一律 no_confirmation(一句输入连跑,不穿插验收)
|
||||||
* - Worker 集 acceptance: review → user_confirmed;continue → no_confirmation
|
* - Worker 集 acceptance: review → user_confirmed;continue → no_confirmation
|
||||||
* - 未声明 acceptance:design 生命周期默认确认;play 缺省按 review(稳妥)
|
* - 未声明 acceptance:创作默认确认
|
||||||
*/
|
*/
|
||||||
export function resolveAcceptanceModeForWorker(params: {
|
export function resolveAcceptanceModeForWorker(params: {
|
||||||
session: RuntimeSession;
|
session: RuntimeSession;
|
||||||
@@ -56,6 +58,9 @@ export function resolveAcceptanceModeForWorker(params: {
|
|||||||
if (isDesignDiskWorker(workerId) || workerId === "opening-generator") {
|
if (isDesignDiskWorker(workerId) || workerId === "opening-generator") {
|
||||||
return "user_confirmed";
|
return "user_confirmed";
|
||||||
}
|
}
|
||||||
|
if (inferLifecycleStage(params.session) === "play") {
|
||||||
|
return "no_confirmation";
|
||||||
|
}
|
||||||
|
|
||||||
const decl = buildInstanceWorkerDeclaration(
|
const decl = buildInstanceWorkerDeclaration(
|
||||||
params.session,
|
params.session,
|
||||||
@@ -66,7 +71,6 @@ export function resolveAcceptanceModeForWorker(params: {
|
|||||||
if (entry?.acceptance === "continue") return "no_confirmation";
|
if (entry?.acceptance === "continue") return "no_confirmation";
|
||||||
if (entry?.acceptance === "review") return "user_confirmed";
|
if (entry?.acceptance === "review") return "user_confirmed";
|
||||||
|
|
||||||
// 未写明:创作阶段默认验收;游玩缺省也验收(避免静默连跑)
|
|
||||||
return "user_confirmed";
|
return "user_confirmed";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,6 +214,11 @@ export function buildDeclaredWorkerSkill(params: {
|
|||||||
params.template?.duty?.trim() ||
|
params.template?.duty?.trim() ||
|
||||||
`执行 ${id}`;
|
`执行 ${id}`;
|
||||||
const excerpt = params.template?.prompt_excerpt?.trim() || "";
|
const excerpt = params.template?.prompt_excerpt?.trim() || "";
|
||||||
|
const visibleBodyRule =
|
||||||
|
id === "narrator" || id === "round-present"
|
||||||
|
? PLAY_VISIBLE_BODY_INSTRUCTION
|
||||||
|
: "";
|
||||||
|
const excerptWithRule = [excerpt, visibleBodyRule].filter(Boolean).join("\n\n");
|
||||||
const presentation = params.entry.presentation
|
const presentation = params.entry.presentation
|
||||||
? JSON.stringify(params.entry.presentation, null, 2)
|
? JSON.stringify(params.entry.presentation, null, 2)
|
||||||
: "";
|
: "";
|
||||||
@@ -217,7 +226,7 @@ export function buildDeclaredWorkerSkill(params: {
|
|||||||
const premises = (params.workerSet?.core_premises ?? []).filter(Boolean);
|
const premises = (params.workerSet?.core_premises ?? []).filter(Boolean);
|
||||||
const residentSection = formatResidentPromptSection(resident, id);
|
const residentSection = formatResidentPromptSection(resident, id);
|
||||||
|
|
||||||
const personaText = [duty, excerpt].filter(Boolean).join("\n\n");
|
const personaText = [duty, excerptWithRule].filter(Boolean).join("\n\n");
|
||||||
const orderDoc = parseContextOrder(params.workerSet?.context_order);
|
const orderDoc = parseContextOrder(params.workerSet?.context_order);
|
||||||
const orderSlot = slotOrderForRef(orderDoc, id);
|
const orderSlot = slotOrderForRef(orderDoc, id);
|
||||||
let contextSegments: ContextSegmentDef[] | undefined;
|
let contextSegments: ContextSegmentDef[] | undefined;
|
||||||
@@ -277,7 +286,7 @@ export function buildDeclaredWorkerSkill(params: {
|
|||||||
"",
|
"",
|
||||||
duty,
|
duty,
|
||||||
"",
|
"",
|
||||||
excerpt ? `## 写法要点\n\n${excerpt}` : "",
|
excerptWithRule ? `## 写法要点\n\n${excerptWithRule}` : "",
|
||||||
presentation ? `## presentation(实例)\n\n\`\`\`json\n${presentation}\n\`\`\`` : "",
|
presentation ? `## presentation(实例)\n\n\`\`\`json\n${presentation}\n\`\`\`` : "",
|
||||||
narrative ? `## 叙事指南\n\n${narrative}` : "",
|
narrative ? `## 叙事指南\n\n${narrative}` : "",
|
||||||
premises.length
|
premises.length
|
||||||
|
|||||||
+40
-6
@@ -516,7 +516,9 @@ export async function loadWorkerSkillWithContext(
|
|||||||
resolveDesignStepBinding,
|
resolveDesignStepBinding,
|
||||||
CREATION_CURRENT_STEP_TAG,
|
CREATION_CURRENT_STEP_TAG,
|
||||||
CREATION_MODULE_OPENING_TAG,
|
CREATION_MODULE_OPENING_TAG,
|
||||||
|
formatPriorArtifactContext,
|
||||||
formatStepParamsForPrompt,
|
formatStepParamsForPrompt,
|
||||||
|
isPriorArtifactModule,
|
||||||
} = await import("./creation-flow.js");
|
} = await import("./creation-flow.js");
|
||||||
const binding = await resolveDesignStepBinding({
|
const binding = await resolveDesignStepBinding({
|
||||||
skillPackRoot: skill.skillPackRoot,
|
skillPackRoot: skill.skillPackRoot,
|
||||||
@@ -526,11 +528,27 @@ export async function loadWorkerSkillWithContext(
|
|||||||
skillsRoot,
|
skillsRoot,
|
||||||
});
|
});
|
||||||
if (binding) {
|
if (binding) {
|
||||||
|
const inheritTag = binding.inheritTag?.trim() || "";
|
||||||
|
const inheritNote = inheritTag
|
||||||
|
? `\n\n【回头修改】本步继承既有产物继续改,不是从零再生成。程序已注入【既有产物 · 继承修改】。保留未点名要改的部分,写回同一产物。禁止推倒重写,禁止再走一遍开场询问。`
|
||||||
|
: "";
|
||||||
const openingNote = binding.opening
|
const openingNote = binding.opening
|
||||||
? `\n\n【程序开场】若黑板有「${CREATION_MODULE_OPENING_TAG}」,该默认问题已由程序发给用户(不经 LLM);用户首答在「用户.worker答复」。勿重复同一开场白,在其答复与提示词基础上继续追问或产出。`
|
? `\n\n【程序开场】若黑板有「${CREATION_MODULE_OPENING_TAG}」,该默认问题已由程序发给用户(不经 LLM);用户首答在「用户.worker答复」。勿重复同一开场白,在其答复与提示词基础上继续追问或产出。`
|
||||||
: "";
|
: "";
|
||||||
const paramsBlock = `## 【本步参数】(编排期已钉;直接按此执行,勿再问「生成什么 / 调用哪个规则」)\n\n${formatStepParamsForPrompt(binding.step.params)}`;
|
const paramsBlock = isPriorArtifactModule(binding.module)
|
||||||
modulePromptBlock = `${paramsBlock}\n\n## 【本步方法 · ${binding.module.name}】\n\n${binding.modulePrompt.trim()}${openingNote}`;
|
? formatPriorArtifactContext(binding.step.params)
|
||||||
|
: `## 【本步参数】(编排期已钉;直接按此执行,勿再问「生成什么 / 调用哪个规则」)\n\n${formatStepParamsForPrompt(binding.step.params)}`;
|
||||||
|
let rosterBlock = "";
|
||||||
|
const depProjection =
|
||||||
|
binding.module.id === "context-order" ? "index" : undefined;
|
||||||
|
if (binding.module.id === "context-order") {
|
||||||
|
const { formatPlayAgentRosterForPrompt } = await import("./play-slots.js");
|
||||||
|
rosterBlock = `${formatPlayAgentRosterForPrompt()}\n\n`;
|
||||||
|
}
|
||||||
|
modulePromptBlock = `${rosterBlock}${paramsBlock}\n\n## 【本步方法 · ${binding.module.name}】\n\n${binding.modulePrompt.trim()}${openingNote}${inheritNote}`;
|
||||||
|
const depTags = inheritTag
|
||||||
|
? binding.depTags.filter((tag) => tag !== inheritTag)
|
||||||
|
: binding.depTags;
|
||||||
const baseInputs = [
|
const baseInputs = [
|
||||||
"用户.需求",
|
"用户.需求",
|
||||||
"book.brief",
|
"book.brief",
|
||||||
@@ -540,16 +558,29 @@ export async function loadWorkerSkillWithContext(
|
|||||||
"设计.创作流程",
|
"设计.创作流程",
|
||||||
CREATION_CURRENT_STEP_TAG,
|
CREATION_CURRENT_STEP_TAG,
|
||||||
CREATION_MODULE_OPENING_TAG,
|
CREATION_MODULE_OPENING_TAG,
|
||||||
...binding.depTags,
|
...depTags,
|
||||||
|
...(inheritTag ? [inheritTag] : []),
|
||||||
];
|
];
|
||||||
const inputTags = [...new Set(baseInputs)];
|
const inputTags = [...new Set(baseInputs)];
|
||||||
const outputTags = [binding.module.artifact];
|
const outputTags = [binding.module.artifact];
|
||||||
const depSegments = binding.depTags.map((tag, i) => ({
|
const depSegments = depTags.map((tag, i) => ({
|
||||||
id: `dep-${i}`,
|
id: `dep-${i}`,
|
||||||
tier: "static" as const,
|
tier: "static" as const,
|
||||||
tags: [tag],
|
tags: [tag],
|
||||||
label: `## 【依赖产物 · ${tag}】只读`,
|
label: `## 【依赖产物 · ${tag}】只读${depProjection === "index" ? "(仅 brief / 挂载 / 稳变)" : ""}`,
|
||||||
|
...(depProjection ? { projection: depProjection } : {}),
|
||||||
}));
|
}));
|
||||||
|
const inheritSegment = inheritTag
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
id: "inherit-existing",
|
||||||
|
tier: "static" as const,
|
||||||
|
tags: [inheritTag],
|
||||||
|
label:
|
||||||
|
"## 【既有产物 · 继承修改】在下列正文上继续改,写回同一产物;禁止从零另起一份",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [];
|
||||||
const openingSegment = binding.opening
|
const openingSegment = binding.opening
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
@@ -562,13 +593,16 @@ export async function loadWorkerSkillWithContext(
|
|||||||
: [];
|
: [];
|
||||||
patchedWorker = {
|
patchedWorker = {
|
||||||
...worker,
|
...worker,
|
||||||
name: `创作 · ${binding.module.name}`,
|
name: inheritTag
|
||||||
|
? `创作 · 回头修改 · ${binding.module.name}`
|
||||||
|
: `创作 · ${binding.module.name}`,
|
||||||
description: binding.module.declaration,
|
description: binding.module.declaration,
|
||||||
inputTags,
|
inputTags,
|
||||||
outputTags,
|
outputTags,
|
||||||
contextSegments: [
|
contextSegments: [
|
||||||
...(worker.contextSegments ?? []),
|
...(worker.contextSegments ?? []),
|
||||||
...openingSegment,
|
...openingSegment,
|
||||||
|
...inheritSegment,
|
||||||
...depSegments,
|
...depSegments,
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,200 @@
|
|||||||
|
/**
|
||||||
|
* 开场白终节点:选定后落库、关 DAG。
|
||||||
|
* 能力 `opening-setup` 与磁盘 worker `opening-generator` 共用这条收口。
|
||||||
|
*/
|
||||||
|
import {
|
||||||
|
parseTableDoc,
|
||||||
|
stringifyTableDoc,
|
||||||
|
type TableDoc,
|
||||||
|
} from "../blackboard/table-cells.js";
|
||||||
|
import {
|
||||||
|
parseCreationFlow,
|
||||||
|
stringifyCreationFlow,
|
||||||
|
} from "./creation-flow.js";
|
||||||
|
import { parseContextFragment } from "./context-fragment.js";
|
||||||
|
|
||||||
|
export const OPENING_SETUP_ARTIFACT_TAG = "设计.开场白与开场变量";
|
||||||
|
export const OPENING_OUTPUT_TAG = "输出.开场白";
|
||||||
|
export const OPENING_INITIAL_VARS_TAG = "运行.初始变量";
|
||||||
|
export const OPENING_CURRENT_VARS_TAG = "变量.当前";
|
||||||
|
|
||||||
|
export const SLOT_CREATION_SEALED_BY_OPENING = "creationSealedByOpening";
|
||||||
|
export const SLOT_OPENING_SELECTED_INDEX = "openingSelectedIndex";
|
||||||
|
|
||||||
|
export const CREATION_SEALED_WAITING_MESSAGE =
|
||||||
|
"开场已选定,创作已收口并保存。可切换到「游玩」。";
|
||||||
|
|
||||||
|
export const INSTANCE_OPENING_SNAPSHOT_LABEL = "创作定稿(开场)";
|
||||||
|
|
||||||
|
export function isOpeningLockTag(tag: string): boolean {
|
||||||
|
const t = tag.trim();
|
||||||
|
return t === OPENING_OUTPUT_TAG || t === OPENING_INITIAL_VARS_TAG;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isOpeningSealArtifact(artifact: {
|
||||||
|
workerId?: string;
|
||||||
|
outputTags?: readonly string[];
|
||||||
|
}): boolean {
|
||||||
|
if ((artifact.workerId ?? "").trim() === "opening-generator") return true;
|
||||||
|
return (artifact.outputTags ?? []).includes(OPENING_SETUP_ARTIFACT_TAG);
|
||||||
|
}
|
||||||
|
|
||||||
|
export type OpeningVariable = {
|
||||||
|
name: string;
|
||||||
|
value: unknown;
|
||||||
|
note?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type OpeningSealPayload = {
|
||||||
|
candidates: string[];
|
||||||
|
selectedIndex: number;
|
||||||
|
selectedText: string;
|
||||||
|
variables: OpeningVariable[];
|
||||||
|
};
|
||||||
|
|
||||||
|
function asTrimmed(v: unknown): string | undefined {
|
||||||
|
if (typeof v !== "string") return undefined;
|
||||||
|
const t = v.trim();
|
||||||
|
return t || undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectCandidateTexts(raw: unknown): string[] {
|
||||||
|
if (!Array.isArray(raw)) return [];
|
||||||
|
const out: string[] = [];
|
||||||
|
for (const item of raw) {
|
||||||
|
if (typeof item === "string" && item.trim()) {
|
||||||
|
out.push(item.trim());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (item && typeof item === "object" && !Array.isArray(item)) {
|
||||||
|
const row = item as Record<string, unknown>;
|
||||||
|
const text =
|
||||||
|
asTrimmed(row.全文) ||
|
||||||
|
asTrimmed(row.text) ||
|
||||||
|
asTrimmed(row.开场白) ||
|
||||||
|
asTrimmed(row.内容);
|
||||||
|
if (text) out.push(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectVariables(raw: unknown): OpeningVariable[] {
|
||||||
|
if (!Array.isArray(raw)) return [];
|
||||||
|
const out: OpeningVariable[] = [];
|
||||||
|
for (const item of raw) {
|
||||||
|
if (!item || typeof item !== "object" || Array.isArray(item)) continue;
|
||||||
|
const row = item as Record<string, unknown>;
|
||||||
|
const name =
|
||||||
|
asTrimmed(row.名) || asTrimmed(row.name) || asTrimmed(row.key) || "";
|
||||||
|
if (!name) continue;
|
||||||
|
const note = asTrimmed(row.依据) || asTrimmed(row.note);
|
||||||
|
out.push({
|
||||||
|
name,
|
||||||
|
value: row.值 !== undefined ? row.值 : row.value,
|
||||||
|
...(note ? { note } : {}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function bodyRecord(fragmentBody: unknown): Record<string, unknown> | null {
|
||||||
|
if (!fragmentBody || typeof fragmentBody !== "object" || Array.isArray(fragmentBody)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return fragmentBody as Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 从开场白 fragment(或裸 JSON)抽出候选与变量。 */
|
||||||
|
export function parseOpeningSealPayload(
|
||||||
|
raw: string | null | undefined,
|
||||||
|
selectedIndex = 0,
|
||||||
|
): OpeningSealPayload | null {
|
||||||
|
if (!raw?.trim()) return null;
|
||||||
|
const fragment = parseContextFragment(raw);
|
||||||
|
const body = bodyRecord(fragment?.正文) ?? (() => {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(raw) as unknown;
|
||||||
|
return bodyRecord(parsed) ?? bodyRecord((parsed as { 正文?: unknown })?.正文);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
const primary =
|
||||||
|
asTrimmed(body?.开场白全文) ||
|
||||||
|
asTrimmed(body?.["输出.开场白"]) ||
|
||||||
|
(typeof fragment?.正文 === "string" ? asTrimmed(fragment.正文) : undefined);
|
||||||
|
|
||||||
|
const fromList = collectCandidateTexts(body?.开场白候选);
|
||||||
|
const candidates: string[] = [];
|
||||||
|
const seen = new Set<string>();
|
||||||
|
const push = (text: string | undefined) => {
|
||||||
|
if (!text || seen.has(text)) return;
|
||||||
|
seen.add(text);
|
||||||
|
candidates.push(text);
|
||||||
|
};
|
||||||
|
push(primary);
|
||||||
|
for (const t of fromList) push(t);
|
||||||
|
if (candidates.length === 0) return null;
|
||||||
|
|
||||||
|
const max = candidates.length - 1;
|
||||||
|
const idx = Number.isFinite(selectedIndex)
|
||||||
|
? Math.min(max, Math.max(0, Math.trunc(selectedIndex)))
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
candidates,
|
||||||
|
selectedIndex: idx,
|
||||||
|
selectedText: candidates[idx] ?? candidates[0]!,
|
||||||
|
variables: collectVariables(body?.开场变量),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function openingVariablesToTableDoc(
|
||||||
|
variables: readonly OpeningVariable[],
|
||||||
|
source = "worker:opening-setup",
|
||||||
|
): TableDoc {
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
return {
|
||||||
|
rows: variables.map((v) => ({
|
||||||
|
key: v.name,
|
||||||
|
value: v.value,
|
||||||
|
rev: 1,
|
||||||
|
updatedAt: now,
|
||||||
|
source,
|
||||||
|
visibility: "visible" as const,
|
||||||
|
...(v.note ? { note: v.note } : {}),
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mergeOpeningTablePatch(
|
||||||
|
currentRaw: string | null | undefined,
|
||||||
|
variables: readonly OpeningVariable[],
|
||||||
|
source = "worker:opening-setup",
|
||||||
|
): string | null {
|
||||||
|
if (!variables.length) return null;
|
||||||
|
const patch = openingVariablesToTableDoc(variables, source);
|
||||||
|
const current = parseTableDoc(currentRaw);
|
||||||
|
if (!current) return stringifyTableDoc(patch);
|
||||||
|
const byKey = new Map(current.rows.map((r) => [r.key, r]));
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
for (const row of patch.rows) {
|
||||||
|
const prev = byKey.get(row.key);
|
||||||
|
if (prev && String(prev.source).startsWith("user")) continue;
|
||||||
|
byKey.set(row.key, {
|
||||||
|
...row,
|
||||||
|
rev: (prev?.rev ?? 0) + 1,
|
||||||
|
updatedAt: now,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return stringifyTableDoc({ rows: [...byKey.values()] });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function closeCreationFlowRaw(raw: string | null | undefined): string | null {
|
||||||
|
const flow = parseCreationFlow(raw);
|
||||||
|
if (!flow) return raw?.trim() ? raw : null;
|
||||||
|
if (flow.status === "closed") return stringifyCreationFlow(flow);
|
||||||
|
return stringifyCreationFlow({ ...flow, status: "closed" });
|
||||||
|
}
|
||||||
@@ -319,3 +319,54 @@ export function inferPlaySlotsFromWorkers(
|
|||||||
chance: hasChance,
|
chance: hasChance,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const AGENT_ID_ALIASES: Record<string, PlaySlotId | OnDemandSlotId> = {
|
||||||
|
auditor: "auditor",
|
||||||
|
旁观维护: "auditor",
|
||||||
|
表格管理: "auditor",
|
||||||
|
gm: "gm",
|
||||||
|
"world-simulator": "gm",
|
||||||
|
主世界层: "gm",
|
||||||
|
narrator: "narrator",
|
||||||
|
叙事转述: "narrator",
|
||||||
|
转述: "narrator",
|
||||||
|
perspective: "perspective",
|
||||||
|
"role-decide": "perspective",
|
||||||
|
角色视角: "perspective",
|
||||||
|
chance: "chance",
|
||||||
|
机遇裁定: "chance",
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 把模型写的 id / 中文名 / worker ref 收成槽键 */
|
||||||
|
export function resolvePlayAgentId(
|
||||||
|
raw: string,
|
||||||
|
): PlaySlotId | OnDemandSlotId | undefined {
|
||||||
|
const t = raw.trim();
|
||||||
|
return AGENT_ID_ALIASES[t] ?? AGENT_ID_ALIASES[t.toLowerCase()];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 注入投影排序等晚段步骤:名册由程序钉死,模型只抄不发明。 */
|
||||||
|
export function formatPlayAgentRosterForPrompt(): string {
|
||||||
|
const lines = [
|
||||||
|
"## 【本局固定执行单元 · 程序钉死】",
|
||||||
|
"",
|
||||||
|
"禁止增删改名,禁止发明未入白名单的槽(如画图)。先抄名册,再给每个已启用者分配上下文 inserts。",
|
||||||
|
"",
|
||||||
|
"每轮管线(顺序固定):",
|
||||||
|
];
|
||||||
|
for (const id of PLAY_SLOT_ORDER) {
|
||||||
|
const meta = PLAY_SLOT_META[id];
|
||||||
|
const def = id === "perspective" ? "默认关" : "默认开";
|
||||||
|
lines.push(
|
||||||
|
`- \`${id}\` → ref=\`${DEFAULT_PLAY_SLOT_REFS[id]}\` **${meta.label}**(${def}):${meta.purpose}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
lines.push("", "按需(不进每轮;enabled 时可空 inserts):");
|
||||||
|
for (const id of Object.keys(DEFAULT_ON_DEMAND_REFS) as OnDemandSlotId[]) {
|
||||||
|
const meta = ON_DEMAND_SLOT_META[id];
|
||||||
|
lines.push(
|
||||||
|
`- \`${id}\` → ref=\`${DEFAULT_ON_DEMAND_REFS[id]}\` **${meta.label}**(默认关):${meta.purpose}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return lines.join("\n");
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
/**
|
||||||
|
* 游玩回合:按运行规格 playWorkerIds 连跑(旁观 → 视角? → 主世界层 → 转述)。
|
||||||
|
* 与创作总管无关;收口后的输入走这里。
|
||||||
|
*/
|
||||||
|
export const SLOT_PLAY_TURN_QUEUE = "playTurnQueue";
|
||||||
|
export const SLOT_PLAY_LAYER_ACTIVE = "playLayerActive";
|
||||||
|
|
||||||
|
export const PLAY_WORKING_SNAPSHOT_ID = "play-working";
|
||||||
|
export const PLAY_WORKING_SNAPSHOT_LABEL = "当前游玩";
|
||||||
|
|
||||||
|
export function isPlayLayerActive(slots: Record<string, unknown> | undefined): boolean {
|
||||||
|
return Boolean(slots?.[SLOT_PLAY_LAYER_ACTIVE]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readPlayTurnQueue(
|
||||||
|
slots: Record<string, unknown> | undefined,
|
||||||
|
): string[] {
|
||||||
|
const raw = slots?.[SLOT_PLAY_TURN_QUEUE];
|
||||||
|
if (!Array.isArray(raw)) return [];
|
||||||
|
return raw
|
||||||
|
.filter((id): id is string => typeof id === "string" && Boolean(id.trim()))
|
||||||
|
.map((id) => id.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
export function withPlayTurnQueue(
|
||||||
|
slots: Record<string, unknown>,
|
||||||
|
queue: string[] | undefined,
|
||||||
|
): Record<string, unknown> {
|
||||||
|
return {
|
||||||
|
...slots,
|
||||||
|
[SLOT_PLAY_TURN_QUEUE]: queue?.length ? queue : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -132,6 +132,9 @@ function normalizeBlocks(raw: unknown): PresentBlocks {
|
|||||||
if (out.monitor === undefined && row.监控 !== undefined) {
|
if (out.monitor === undefined && row.监控 !== undefined) {
|
||||||
out.monitor = row.监控 as PresentBlocks["monitor"];
|
out.monitor = row.监控 as PresentBlocks["monitor"];
|
||||||
}
|
}
|
||||||
|
if (typeof out.body === "string") {
|
||||||
|
out.body = stripPresentSourceFences(out.body);
|
||||||
|
}
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,15 +173,35 @@ export function presentFromPlainText(
|
|||||||
return {
|
return {
|
||||||
schema: PRESENT_SCHEMA,
|
schema: PRESENT_SCHEMA,
|
||||||
shell,
|
shell,
|
||||||
blocks: { body: text },
|
blocks: { body: stripPresentSourceFences(text) },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 模型常把 tag 名写进正文开头;展示前剥掉 */
|
||||||
|
export function stripPresentSourceFences(raw: string): string {
|
||||||
|
let s = raw.trim();
|
||||||
|
for (let i = 0; i < 3; i += 1) {
|
||||||
|
const next = s
|
||||||
|
.replace(/^(#{1,6}\s*)?输出[.::]?\s*(用户展示|开场白)(?:\s*\n+|\s*$)/u, "")
|
||||||
|
.replace(/^【\s*输出[.::]?\s*(用户展示|开场白)\s*】(?:\s*\n+|\s*$)/u, "")
|
||||||
|
.trim();
|
||||||
|
if (next === s) break;
|
||||||
|
s = next;
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PLAY_VISIBLE_BODY_INSTRUCTION = [
|
||||||
|
"用户可见正文(present.v1 的 blocks.body,或纯 Markdown 主读)默认 **500~2000 字**(按汉字计)。",
|
||||||
|
"不要写成几句气泡短信就结束,除非用户明确要求极短。",
|
||||||
|
"禁止把 tag 名(如「输出.用户展示」)、压缩摘要、过程日志写进正文。",
|
||||||
|
].join("\n");
|
||||||
|
|
||||||
export function parsePresentPacket(
|
export function parsePresentPacket(
|
||||||
raw: string | undefined | null,
|
raw: string | undefined | null,
|
||||||
fallbackShell: PresentShellId = "prose",
|
fallbackShell: PresentShellId = "prose",
|
||||||
): PresentPacketView {
|
): PresentPacketView {
|
||||||
const text = raw?.trim() ?? "";
|
const text = stripPresentSourceFences(raw?.trim() ?? "");
|
||||||
if (!text) {
|
if (!text) {
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { Blackboard } from "../blackboard/blackboard.js";
|
import type { Blackboard } from "../blackboard/blackboard.js";
|
||||||
import type { RuntimeSession } from "../types/runtime.js";
|
import type { RuntimeSession } from "../types/runtime.js";
|
||||||
|
import { isPlayLayerActive } from "./play-turn.js";
|
||||||
import {
|
import {
|
||||||
deriveDesignStageScope,
|
deriveDesignStageScope,
|
||||||
deriveOnDemandWorkerScope,
|
deriveOnDemandWorkerScope,
|
||||||
@@ -45,8 +46,9 @@ export function canEnterPlay(session: RuntimeSession): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function inferLifecycleStage(session: RuntimeSession): LifecycleStage {
|
export function inferLifecycleStage(session: RuntimeSession): LifecycleStage {
|
||||||
const override = session.slots.uiLifecycleStage;
|
if (!canEnterPlay(session)) return "design";
|
||||||
if (override === "play" && canEnterPlay(session)) return "play";
|
if (session.slots.uiLifecycleStage === "play") return "play";
|
||||||
|
if (isPlayLayerActive(session.slots)) return "play";
|
||||||
return "design";
|
return "design";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,16 +3,13 @@ import {
|
|||||||
OpenAiCompatibleProvider,
|
OpenAiCompatibleProvider,
|
||||||
type LlmProvider,
|
type LlmProvider,
|
||||||
} from "../llm/client.js";
|
} from "../llm/client.js";
|
||||||
import { PresetLlmProvider } from "../llm/preset-wrapper.js";
|
|
||||||
import { resolveActivePreset } from "../preset/store.js";
|
|
||||||
import { loadAppSettings } from "../config/settings.js";
|
|
||||||
import {
|
import {
|
||||||
TokenTrackingProvider,
|
wrapLlmForSession,
|
||||||
type LlmTrackingContext,
|
type LlmTrackingRef,
|
||||||
} from "../llm/token-tracker.js";
|
} from "../llm/preset-wrapper.js";
|
||||||
import type { ParsedWorkerSkill, SkillWorkerLlmBindings } from "./types.js";
|
import type { ParsedWorkerSkill, SkillWorkerLlmBindings } from "./types.js";
|
||||||
|
|
||||||
type LlmTrackingRef = { current: LlmTrackingContext };
|
export type { LlmTrackingContext } from "../llm/token-tracker.js";
|
||||||
|
|
||||||
/** 按 ApiProfile.id 构建 LLM;找不到 profile 时回退 fallback */
|
/** 按 ApiProfile.id 构建 LLM;找不到 profile 时回退 fallback */
|
||||||
export function createLlmForProfileId(
|
export function createLlmForProfileId(
|
||||||
@@ -25,17 +22,10 @@ export function createLlmForProfileId(
|
|||||||
return fallback;
|
return fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
let inner: LlmProvider = new OpenAiCompatibleProvider(
|
return wrapLlmForSession(
|
||||||
profileToLlmConfig(profile),
|
new OpenAiCompatibleProvider(profileToLlmConfig(profile)),
|
||||||
|
trackingRef,
|
||||||
);
|
);
|
||||||
const preset = resolveActivePreset(loadAppSettings().activePresetId);
|
|
||||||
if (preset) {
|
|
||||||
inner = new PresetLlmProvider(inner, () =>
|
|
||||||
resolveActivePreset(loadAppSettings().activePresetId),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (!trackingRef) return inner;
|
|
||||||
return new TokenTrackingProvider(inner, () => trackingRef.current);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -142,5 +132,3 @@ export function filterInputsForRolePerspective(
|
|||||||
}
|
}
|
||||||
return filtered;
|
return filtered;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type { LlmTrackingContext };
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import type { GenerationParameters } from "./preset.js";
|
||||||
|
|
||||||
/** 一次 LLM 调用实际发送的上下文(全量,供右键观察) */
|
/** 一次 LLM 调用实际发送的上下文(全量,供右键观察) */
|
||||||
export type LlmContextMessage = {
|
export type LlmContextMessage = {
|
||||||
role: string;
|
role: string;
|
||||||
@@ -12,17 +14,51 @@ export type LlmContextTrace = {
|
|||||||
/** 便于列表展示 */
|
/** 便于列表展示 */
|
||||||
charCount: number;
|
charCount: number;
|
||||||
model?: string;
|
model?: string;
|
||||||
|
/** 与请求一并送出的生成参数(无则未带) */
|
||||||
|
generation?: GenerationParameters;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** 把即将写入请求 body.messages 的条目冻成痕迹(含 tool_calls 等附加字段) */
|
||||||
|
export function snapshotRequestMessages(
|
||||||
|
messages: Array<{
|
||||||
|
role: string;
|
||||||
|
content?: string | null;
|
||||||
|
tool_calls?: unknown;
|
||||||
|
tool_call_id?: string;
|
||||||
|
}>,
|
||||||
|
): LlmContextMessage[] {
|
||||||
|
return messages.map((m) => {
|
||||||
|
const hasExtras = Boolean(m.tool_calls) || Boolean(m.tool_call_id);
|
||||||
|
if (!hasExtras) {
|
||||||
|
return {
|
||||||
|
role: m.role,
|
||||||
|
content: typeof m.content === "string" ? m.content : String(m.content ?? ""),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
role: m.role,
|
||||||
|
content: JSON.stringify({
|
||||||
|
role: m.role,
|
||||||
|
content: m.content ?? "",
|
||||||
|
...(m.tool_calls ? { tool_calls: m.tool_calls } : {}),
|
||||||
|
...(m.tool_call_id ? { tool_call_id: m.tool_call_id } : {}),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function buildContextTrace(params: {
|
export function buildContextTrace(params: {
|
||||||
caller: string;
|
caller: string;
|
||||||
messages: Array<{ role: string; content: string }>;
|
messages: Array<{
|
||||||
|
role: string;
|
||||||
|
content?: string | null;
|
||||||
|
tool_calls?: unknown;
|
||||||
|
tool_call_id?: string;
|
||||||
|
}>;
|
||||||
model?: string;
|
model?: string;
|
||||||
|
generation?: GenerationParameters;
|
||||||
}): LlmContextTrace {
|
}): LlmContextTrace {
|
||||||
const messages = params.messages.map((m) => ({
|
const messages = snapshotRequestMessages(params.messages);
|
||||||
role: m.role,
|
|
||||||
content: typeof m.content === "string" ? m.content : String(m.content ?? ""),
|
|
||||||
}));
|
|
||||||
const charCount = messages.reduce((n, m) => n + m.content.length, 0);
|
const charCount = messages.reduce((n, m) => n + m.content.length, 0);
|
||||||
return {
|
return {
|
||||||
caller: params.caller,
|
caller: params.caller,
|
||||||
@@ -30,6 +66,9 @@ export function buildContextTrace(params: {
|
|||||||
messages,
|
messages,
|
||||||
charCount,
|
charCount,
|
||||||
model: params.model,
|
model: params.model,
|
||||||
|
...(params.generation && Object.keys(params.generation).length
|
||||||
|
? { generation: params.generation }
|
||||||
|
: {}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+11
-2
@@ -53,7 +53,10 @@ export type WaitingReason =
|
|||||||
} // worker 无产物时的阻塞提问
|
} // worker 无产物时的阻塞提问
|
||||||
| { kind: "revision"; instruction?: string } // 无意见打回时等补交;有意见则直接重跑
|
| { kind: "revision"; instruction?: string } // 无意见打回时等补交;有意见则直接重跑
|
||||||
| {
|
| {
|
||||||
/** 上一步已验收:先问「下一步想写什么」(可留空),再提案并确认开干 */
|
/**
|
||||||
|
* 旧路径:上一步验收后先问「下一步想写什么」。
|
||||||
|
* 现已改为验收后直接提案;保留此 kind 以兼容未完成的旧会话。
|
||||||
|
*/
|
||||||
kind: "next_intent";
|
kind: "next_intent";
|
||||||
afterWorkerId?: string;
|
afterWorkerId?: string;
|
||||||
afterUnitId?: string;
|
afterUnitId?: string;
|
||||||
@@ -329,7 +332,13 @@ export type PhaseEffect =
|
|||||||
| { type: "run_programmatic_review"; artifactId: string }
|
| { type: "run_programmatic_review"; artifactId: string }
|
||||||
| { type: "emit_message"; message: string }
|
| { type: "emit_message"; message: string }
|
||||||
/** 根据流程与用户下一步意向,提案下一节点并进入确认 */
|
/** 根据流程与用户下一步意向,提案下一节点并进入确认 */
|
||||||
| { type: "propose_next_creation_step" };
|
| { type: "propose_next_creation_step" }
|
||||||
|
/** 选定开场白:落库、关 DAG、标记创作收口 */
|
||||||
|
| { type: "seal_creation_opening" }
|
||||||
|
/** 游玩:按运行规格开一轮(旁观 → 主世界层 → 转述) */
|
||||||
|
| { type: "run_play_turn" }
|
||||||
|
/** 游玩:跑管线里的下一个执行单元;队空则等下一条用户输入 */
|
||||||
|
| { type: "continue_play_turn" };
|
||||||
|
|
||||||
/** applyEvent 的返回值:新会话快照 + 待处理副作用 + 可选错误 */
|
/** applyEvent 的返回值:新会话快照 + 待处理副作用 + 可选错误 */
|
||||||
export type ApplyEventResult = {
|
export type ApplyEventResult = {
|
||||||
|
|||||||
+118
-69
@@ -11,9 +11,16 @@ import type { ParsedWorkerSkill } from "../skills/types.js";
|
|||||||
import {
|
import {
|
||||||
extractJsonObjectText,
|
extractJsonObjectText,
|
||||||
isUsableWorkerSet,
|
isUsableWorkerSet,
|
||||||
|
looksLikeProseNotSpec,
|
||||||
parseWorkerSetYaml,
|
parseWorkerSetYaml,
|
||||||
} from "../skills/worker-set-parse.js";
|
} from "../skills/worker-set-parse.js";
|
||||||
import { assembleWorkerContext } from "../skills/context-segments.js";
|
import { assembleWorkerContext } from "../skills/context-segments.js";
|
||||||
|
import { isPlayLayerActive } from "../skills/play-turn.js";
|
||||||
|
import { loadAppSettings } from "../config/settings.js";
|
||||||
|
import { resolveActivePreset } from "../preset/store.js";
|
||||||
|
import { assemblePlayWorkerMessages } from "../preset/play-frame.js";
|
||||||
|
import { worldInfoPackFromSegments } from "../preset/world-info-pack.js";
|
||||||
|
import { PRESENT_TAG } from "../skills/present-packet.js";
|
||||||
import {
|
import {
|
||||||
mergeQuestionsPreferFragment,
|
mergeQuestionsPreferFragment,
|
||||||
normalizeQuestions,
|
normalizeQuestions,
|
||||||
@@ -21,11 +28,15 @@ import {
|
|||||||
} from "../skills/question-protocol.js";
|
} from "../skills/question-protocol.js";
|
||||||
import {
|
import {
|
||||||
extractFragmentAskSidecar,
|
extractFragmentAskSidecar,
|
||||||
expectsContextFragmentTag,
|
|
||||||
isUsableContextFragment,
|
isUsableContextFragment,
|
||||||
looksLikeFragmentDoc,
|
looksLikeFragmentDoc,
|
||||||
} from "../skills/context-fragment.js";
|
} from "../skills/context-fragment.js";
|
||||||
import { isProgressPointerTag } from "../skills/creation-flow.js";
|
import { isProgressPointerTag } from "../skills/creation-flow.js";
|
||||||
|
import {
|
||||||
|
CONTEXT_ORDER_TAG,
|
||||||
|
parseContextOrder,
|
||||||
|
serializeContextOrder,
|
||||||
|
} from "../skills/context-order.js";
|
||||||
|
|
||||||
export type WorkerRunParams = {
|
export type WorkerRunParams = {
|
||||||
skillName: string;
|
skillName: string;
|
||||||
@@ -53,6 +64,25 @@ export type WorkerRunResult = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const WORKER_SET_OUTPUT_TAGS = new Set(["设计.worker集", "设计.worker集.草稿"]);
|
const WORKER_SET_OUTPUT_TAGS = new Set(["设计.worker集", "设计.worker集.草稿"]);
|
||||||
|
const PLAY_FEED_OUTPUT_TAGS = new Set([PRESENT_TAG, "输出.开场白"]);
|
||||||
|
|
||||||
|
function formatWorkerPreview(
|
||||||
|
outputs: Record<string, string>,
|
||||||
|
summary: string,
|
||||||
|
): string {
|
||||||
|
const visible = Object.entries(outputs).filter(([tag]) =>
|
||||||
|
PLAY_FEED_OUTPUT_TAGS.has(tag),
|
||||||
|
);
|
||||||
|
if (visible.length) {
|
||||||
|
return visible.map(([, v]) => v).join("\n\n");
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
Object.entries(outputs)
|
||||||
|
.map(([tag, v]) => `### ${tag}\n\n${v}`)
|
||||||
|
.join("\n\n")
|
||||||
|
.slice(0, 4000) || summary
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const WORKER_OUTPUT_INSTRUCTION = `
|
const WORKER_OUTPUT_INSTRUCTION = `
|
||||||
|
|
||||||
@@ -74,7 +104,9 @@ askUser 每项可为:
|
|||||||
- outputs 的 key 必须是要求的 outputTags
|
- outputs 的 key 必须是要求的 outputTags
|
||||||
- **设计.worker集 / 设计.worker集.草稿**:value 必须是 JSON 对象文本(以 { 开头),禁止中文说明、元叙述、提问长文;禁止 YAML
|
- **设计.worker集 / 设计.worker集.草稿**:value 必须是 JSON 对象文本(以 { 开头),禁止中文说明、元叙述、提问长文;禁止 YAML
|
||||||
- **context-fragment.v1**:必须放在 outputs["<本步 artifact tag>"],禁止把片段当根对象。题目只写在产物「追问」(建议选项 + 示例);顶层 askUser 必须为 **null**。程序会把「追问」挂到询问卡。禁止同一问再抄一份 askUser。禁止写入「创作.当前步骤」等进度指针
|
- **context-fragment.v1**:必须放在 outputs["<本步 artifact tag>"],禁止把片段当根对象。题目只写在产物「追问」(建议选项 + 示例);顶层 askUser 必须为 **null**。程序会把「追问」挂到询问卡。禁止同一问再抄一份 askUser。禁止写入「创作.当前步骤」等进度指针
|
||||||
- **其它产物**:优先同时给 outputs + askUser;有产物时追问挂在产物下(用户可直接接受而不作答)。仅当完全无法产出时才留空 outputs、只填 askUser
|
- **其它产物**:优先同时给 outputs + askUser;有产物时追问挂在产物下(用户可直接接受而不作答)
|
||||||
|
- **残稿也要交**:JSON 不完美、字段不全、截断,只要有可展示正文就必须写入 outputs,交给用户验收(可打回重生成,也可接着改)。不要因为渲染不出就把稿扔掉改成提问
|
||||||
|
- 仅当完全没有可展示正文、只有提问时,才留空 outputs、只填 askUser
|
||||||
- 能推断选项时 **必须**给 options(完整句、可改写);不要只丢裸问题逼用户写长段
|
- 能推断选项时 **必须**给 options(完整句、可改写);不要只丢裸问题逼用户写长段
|
||||||
- 若 inputs 中 \`用户.需求\` / \`用户.博弈需求\`(或 book.brief)已有实质内容,禁止 askUser 要求用户重复提供其中已写明的情境、角色、规则等;仅对 genuinely 缺失且无法推断的要点提问
|
- 若 inputs 中 \`用户.需求\` / \`用户.博弈需求\`(或 book.brief)已有实质内容,禁止 askUser 要求用户重复提供其中已写明的情境、角色、规则等;仅对 genuinely 缺失且无法推断的要点提问
|
||||||
- summary 用于界面展示`;
|
- summary 用于界面展示`;
|
||||||
@@ -193,31 +225,46 @@ function recoverFragmentOutputs(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function dropUnusableFragmentOutputs(outputs: Record<string, string>): void {
|
/** 进度指针不得当产物;半残 JSON / 缺字段仍保留,交给验收卡。 */
|
||||||
|
function dropProgressPointerOutputs(outputs: Record<string, string>): void {
|
||||||
for (const tag of Object.keys(outputs)) {
|
for (const tag of Object.keys(outputs)) {
|
||||||
if (isProgressPointerTag(tag)) {
|
if (isProgressPointerTag(tag) || !outputs[tag]?.trim()) {
|
||||||
delete outputs[tag];
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const content = outputs[tag]!;
|
|
||||||
const requireFragment = expectsContextFragmentTag(tag);
|
|
||||||
const looksFragment =
|
|
||||||
isUsableContextFragment(content) ||
|
|
||||||
looksLikeFragmentDoc(tryParseObject(content)) ||
|
|
||||||
/"schema"\s*:\s*"context-fragment\.v1"/.test(content) ||
|
|
||||||
/"技能"\s*:/.test(content);
|
|
||||||
if ((requireFragment || looksFragment) && !isUsableContextFragment(content)) {
|
|
||||||
delete outputs[tag];
|
delete outputs[tag];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function tryParseObject(text: string): unknown {
|
/** 像结构化残稿(含截断 JSON),应进验收而不是改成提问。 */
|
||||||
const extracted = extractJsonObjectText(text) ?? text.trim();
|
function looksLikeStructuredDraft(text: string): boolean {
|
||||||
try {
|
const t = text.trim();
|
||||||
return JSON.parse(extracted);
|
if (!t) return false;
|
||||||
} catch {
|
if (t.startsWith("{") || t.startsWith("[")) return true;
|
||||||
return undefined;
|
if (/```(?:json)?/i.test(t) && t.includes("{")) return true;
|
||||||
|
if (/"schema"\s*:/.test(t)) return true;
|
||||||
|
if (/"技能"\s*:/.test(t) || /"正文"\s*:/.test(t)) return true;
|
||||||
|
if (/"inserts"\s*:/.test(t) || /"agents"\s*:/.test(t) || /"play_slots"\s*:/.test(t)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (/"workers"\s*:/.test(t) && t.includes("{")) return true;
|
||||||
|
return Boolean(extractJsonObjectText(t));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 几乎只有提问、没有稿。 */
|
||||||
|
function isQuestionOnlyText(text: string): boolean {
|
||||||
|
const t = text.trim();
|
||||||
|
if (!t) return true;
|
||||||
|
if (looksLikeStructuredDraft(t)) return false;
|
||||||
|
const qs = extractQuestionsFromText(t);
|
||||||
|
if (!qs.length) return false;
|
||||||
|
return looksLikeProseNotSpec(t) || /你的选择|请(?:描述|选择|确认|补充)/.test(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
function canonicalizeKnownOutputs(outputs: Record<string, string>): void {
|
||||||
|
const raw = outputs[CONTEXT_ORDER_TAG];
|
||||||
|
if (!raw?.trim()) return;
|
||||||
|
const doc = parseContextOrder(raw);
|
||||||
|
if (doc?.slots.length) {
|
||||||
|
outputs[CONTEXT_ORDER_TAG] = serializeContextOrder(doc);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,20 +286,14 @@ function parseWorkerResponse(
|
|||||||
try {
|
try {
|
||||||
parsed = JSON.parse(raw);
|
parsed = JSON.parse(raw);
|
||||||
} catch {
|
} catch {
|
||||||
if (outputTags.some((t) => WORKER_SET_OUTPUT_TAGS.has(t))) {
|
|
||||||
const questions = normalizeQuestions(extractQuestionsFromText(raw));
|
|
||||||
return {
|
|
||||||
outputs: {},
|
|
||||||
summary: "未产出合法协议 JSON",
|
|
||||||
preview: raw.slice(0, 600),
|
|
||||||
askUser: questions.length
|
|
||||||
? questions.slice(0, 2)
|
|
||||||
: normalizeQuestions([
|
|
||||||
"请补充设计所需的关键信息(上一次未产出合法 JSON 规格)。",
|
|
||||||
]),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const target = productOutputTags(outputTags)[0] ?? outputTags[0];
|
const target = productOutputTags(outputTags)[0] ?? outputTags[0];
|
||||||
|
if (target && looksLikeStructuredDraft(raw)) {
|
||||||
|
return finalizeParsedOutputs(
|
||||||
|
{ [target]: raw },
|
||||||
|
{ summary: "格式不完整,已交出残稿" },
|
||||||
|
outputTags,
|
||||||
|
);
|
||||||
|
}
|
||||||
if (target && isUsableContextFragment(raw)) {
|
if (target && isUsableContextFragment(raw)) {
|
||||||
return finalizeParsedOutputs(
|
return finalizeParsedOutputs(
|
||||||
{ [target]: raw },
|
{ [target]: raw },
|
||||||
@@ -260,6 +301,13 @@ function parseWorkerResponse(
|
|||||||
outputTags,
|
outputTags,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (target && raw.trim() && !isQuestionOnlyText(raw)) {
|
||||||
|
return finalizeParsedOutputs(
|
||||||
|
{ [target]: raw },
|
||||||
|
{ summary: "未按协议包一层,已交出原文" },
|
||||||
|
outputTags,
|
||||||
|
);
|
||||||
|
}
|
||||||
const questions = normalizeQuestions(extractQuestionsFromText(raw));
|
const questions = normalizeQuestions(extractQuestionsFromText(raw));
|
||||||
return {
|
return {
|
||||||
outputs: {},
|
outputs: {},
|
||||||
@@ -323,7 +371,7 @@ function finalizeParsedOutputs(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 先抽追问,再丢掉半残片段——无正文时仍要把题留给 LLM loop
|
// 先抽追问;半残稿仍留在 outputs,追问挂验收卡(不因缺字段丢掉正文)
|
||||||
let askAssessment: string | undefined;
|
let askAssessment: string | undefined;
|
||||||
const fragQuestions: QuestionItem[] = [];
|
const fragQuestions: QuestionItem[] = [];
|
||||||
for (const content of Object.values(outputs)) {
|
for (const content of Object.values(outputs)) {
|
||||||
@@ -339,7 +387,8 @@ function finalizeParsedOutputs(
|
|||||||
askUser = mergeQuestionsPreferFragment(askUser, fragQuestions);
|
askUser = mergeQuestionsPreferFragment(askUser, fragQuestions);
|
||||||
}
|
}
|
||||||
|
|
||||||
dropUnusableFragmentOutputs(outputs);
|
dropProgressPointerOutputs(outputs);
|
||||||
|
canonicalizeKnownOutputs(outputs);
|
||||||
|
|
||||||
if (Object.keys(outputs).length === 0 && (!askUser || askUser.length === 0)) {
|
if (Object.keys(outputs).length === 0 && (!askUser || askUser.length === 0)) {
|
||||||
askUser = normalizeQuestions(INCOMPLETE_FRAGMENT_ASK);
|
askUser = normalizeQuestions(INCOMPLETE_FRAGMENT_ASK);
|
||||||
@@ -351,11 +400,7 @@ function finalizeParsedOutputs(
|
|||||||
: Object.values(outputs)[0]?.slice(0, 80) ??
|
: Object.values(outputs)[0]?.slice(0, 80) ??
|
||||||
(askUser?.length ? `待补充:${askUser[0]!.prompt.slice(0, 40)}` : "Worker 已完成");
|
(askUser?.length ? `待补充:${askUser[0]!.prompt.slice(0, 40)}` : "Worker 已完成");
|
||||||
|
|
||||||
const preview =
|
const preview = formatWorkerPreview(outputs, summary);
|
||||||
Object.entries(outputs)
|
|
||||||
.map(([tag, v]) => `### ${tag}\n\n${v}`)
|
|
||||||
.join("\n\n")
|
|
||||||
.slice(0, 4000) || summary;
|
|
||||||
|
|
||||||
return sanitizeWorkerSetOutputs({
|
return sanitizeWorkerSetOutputs({
|
||||||
outputs,
|
outputs,
|
||||||
@@ -366,12 +411,11 @@ function finalizeParsedOutputs(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 规格 tag 必须是可用 JSON;非法散文改为 askUser,避免污染黑板 */
|
/** 规格 tag:能解析则收成 JSON;半残 JSON 保留进验收;纯提问才改成 askUser。 */
|
||||||
export function sanitizeWorkerSetOutputs(result: WorkerRunResult): WorkerRunResult {
|
export function sanitizeWorkerSetOutputs(result: WorkerRunResult): WorkerRunResult {
|
||||||
const outputs = { ...result.outputs };
|
const outputs = { ...result.outputs };
|
||||||
const askUser = [...(result.askUser ?? [])];
|
const askUser = [...(result.askUser ?? [])];
|
||||||
const askAssessment = result.askAssessment;
|
const askAssessment = result.askAssessment;
|
||||||
let droppedProse = false;
|
|
||||||
|
|
||||||
for (const tag of [...Object.keys(outputs)]) {
|
for (const tag of [...Object.keys(outputs)]) {
|
||||||
if (!WORKER_SET_OUTPUT_TAGS.has(tag)) continue;
|
if (!WORKER_SET_OUTPUT_TAGS.has(tag)) continue;
|
||||||
@@ -388,28 +432,17 @@ export function sanitizeWorkerSetOutputs(result: WorkerRunResult): WorkerRunResu
|
|||||||
: (extractJsonObjectText(extracted) ?? extracted);
|
: (extractJsonObjectText(extracted) ?? extracted);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
droppedProse = true;
|
if (looksLikeStructuredDraft(content)) {
|
||||||
|
outputs[tag] = content.trim();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (isQuestionOnlyText(content)) {
|
||||||
delete outputs[tag];
|
delete outputs[tag];
|
||||||
if (askUser.length === 0) {
|
if (askUser.length === 0) {
|
||||||
const qs = normalizeQuestions(extractQuestionsFromText(content));
|
const qs = normalizeQuestions(extractQuestionsFromText(content));
|
||||||
if (qs.length) askUser.push(...qs.slice(0, 2));
|
if (qs.length) askUser.push(...qs.slice(0, 2));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (droppedProse && askUser.length === 0) {
|
|
||||||
askUser.push(
|
|
||||||
...normalizeQuestions([
|
|
||||||
"请补充或确认开局关键前提(上一次把说明文字写进了规格字段,未产出合法 JSON)。",
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 提问时不要夹带半残规格
|
|
||||||
if (askUser.length > 0) {
|
|
||||||
for (const tag of WORKER_SET_OUTPUT_TAGS) {
|
|
||||||
// 保留仍合法的草稿;已在上面删掉非法的
|
|
||||||
void tag;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const summary =
|
const summary =
|
||||||
@@ -420,10 +453,7 @@ export function sanitizeWorkerSetOutputs(result: WorkerRunResult): WorkerRunResu
|
|||||||
const preview =
|
const preview =
|
||||||
askUser.length && Object.keys(outputs).length === 0
|
askUser.length && Object.keys(outputs).length === 0
|
||||||
? askUser.map((q) => `- ${q.prompt}`).join("\n")
|
? askUser.map((q) => `- ${q.prompt}`).join("\n")
|
||||||
: Object.entries(outputs)
|
: formatWorkerPreview(outputs, summary);
|
||||||
.map(([tag, v]) => `### ${tag}\n\n${v}`)
|
|
||||||
.join("\n\n")
|
|
||||||
.slice(0, 4000) || summary;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
outputs,
|
outputs,
|
||||||
@@ -527,7 +557,27 @@ export async function runWorkerSkill(params: WorkerRunParams): Promise<WorkerRun
|
|||||||
inputs[CONTEXT_BRIEF_TAG] = priorBrief;
|
inputs[CONTEXT_BRIEF_TAG] = priorBrief;
|
||||||
}
|
}
|
||||||
|
|
||||||
const userPayload = assembleWorkerContext({
|
const systemContent = promptBody + WORKER_OUTPUT_INSTRUCTION;
|
||||||
|
const playPreset = isPlayLayerActive(params.slots)
|
||||||
|
? resolveActivePreset(loadAppSettings().activePresetId)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const messages = playPreset
|
||||||
|
? assemblePlayWorkerMessages({
|
||||||
|
systemPrompt: systemContent,
|
||||||
|
preset: playPreset,
|
||||||
|
pack: worldInfoPackFromSegments({
|
||||||
|
segments: worker.contextSegments ?? [],
|
||||||
|
inputs,
|
||||||
|
blackboard: params.blackboard,
|
||||||
|
inputMerge,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
: [
|
||||||
|
{ role: "system" as const, content: systemContent },
|
||||||
|
{
|
||||||
|
role: "user" as const,
|
||||||
|
content: assembleWorkerContext({
|
||||||
inputs,
|
inputs,
|
||||||
segments: worker.contextSegments,
|
segments: worker.contextSegments,
|
||||||
blackboard: params.blackboard,
|
blackboard: params.blackboard,
|
||||||
@@ -535,14 +585,13 @@ export async function runWorkerSkill(params: WorkerRunParams): Promise<WorkerRun
|
|||||||
workerId: worker.id,
|
workerId: worker.id,
|
||||||
workerName: worker.name,
|
workerName: worker.name,
|
||||||
outputTags: worker.outputTags,
|
outputTags: worker.outputTags,
|
||||||
});
|
}),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const result = await completeWorkerPreferStream(
|
const result = await completeWorkerPreferStream(
|
||||||
params.llm,
|
params.llm,
|
||||||
[
|
messages,
|
||||||
{ role: "system", content: promptBody + WORKER_OUTPUT_INSTRUCTION },
|
|
||||||
{ role: "user", content: userPayload },
|
|
||||||
],
|
|
||||||
{
|
{
|
||||||
responseFormat: "json_object",
|
responseFormat: "json_object",
|
||||||
caller: `worker:${worker.id}`,
|
caller: `worker:${worker.id}`,
|
||||||
|
|||||||
@@ -106,9 +106,127 @@ describe("contextSegments assembly", () => {
|
|||||||
outputTags: ["设计.美学纲领与交互范式"],
|
outputTags: ["设计.美学纲领与交互范式"],
|
||||||
});
|
});
|
||||||
expect(text).toContain("待改底稿");
|
expect(text).toContain("待改底稿");
|
||||||
expect(text).toContain("用户.worker答复");
|
expect(text).toContain("用户要求");
|
||||||
|
expect(text).toContain("冲突时以较新的为准");
|
||||||
expect(text).toContain("偏冷");
|
expect(text).toContain("偏冷");
|
||||||
expect(text).toContain("再冷一点");
|
expect(text).toContain("再冷一点");
|
||||||
|
expect(text.split("再冷一点").length - 1).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("stale revision note without a draft stays as a user requirement, not revision mode", () => {
|
||||||
|
const bb = new Blackboard();
|
||||||
|
const text = assembleWorkerContext({
|
||||||
|
inputs: {
|
||||||
|
"用户.修订说明": "上一步留下的意见",
|
||||||
|
"用户.需求": "网恋对话",
|
||||||
|
},
|
||||||
|
segments: [
|
||||||
|
{
|
||||||
|
id: "user",
|
||||||
|
tier: "dynamic",
|
||||||
|
tags: ["用户.需求", "用户.修订说明"],
|
||||||
|
label: "## 用户表述",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
blackboard: bb,
|
||||||
|
workerId: "design-step",
|
||||||
|
workerName: "创作 · 正文组成",
|
||||||
|
outputTags: ["设计.正文组成"],
|
||||||
|
});
|
||||||
|
expect(text).not.toContain("修订模式");
|
||||||
|
expect(text).not.toContain("待改底稿");
|
||||||
|
expect(text).toContain("用户要求");
|
||||||
|
expect(text).toContain("上一步留下的意见");
|
||||||
|
expect(text).toContain("网恋对话");
|
||||||
|
expect(text).toContain("按 SKILL 与上方分区完成任务");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("collapses duplicate user-demand tags in the same segment", () => {
|
||||||
|
const bb = new Blackboard();
|
||||||
|
const text = assembleWorkerContext({
|
||||||
|
inputs: {
|
||||||
|
"用户.需求": "开场需求\n\n需要正文和选项",
|
||||||
|
"用户.worker答复": "需要正文和选项",
|
||||||
|
"用户.最新输入": "需要正文和选项",
|
||||||
|
},
|
||||||
|
segments: [
|
||||||
|
{
|
||||||
|
id: "user",
|
||||||
|
tier: "dynamic",
|
||||||
|
tags: ["用户.需求", "用户.worker答复", "用户.最新输入"],
|
||||||
|
label: "## 用户表述",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
blackboard: bb,
|
||||||
|
workerId: "design-step",
|
||||||
|
workerName: "创作 · 正文组成",
|
||||||
|
outputTags: ["设计.正文组成"],
|
||||||
|
});
|
||||||
|
expect(text.split("需要正文和选项").length - 1).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps earlier user requirements and puts the newer revision last", () => {
|
||||||
|
const bb = new Blackboard();
|
||||||
|
bb.write({
|
||||||
|
tag: "设计.正文组成",
|
||||||
|
content: '{"shell":"split_board"}',
|
||||||
|
source: "worker",
|
||||||
|
});
|
||||||
|
const text = assembleWorkerContext({
|
||||||
|
inputs: {
|
||||||
|
"用户.需求": "需要正文和监控",
|
||||||
|
"用户.修订说明": "状态放到上面",
|
||||||
|
"设计.正文组成": '{"shell":"split_board"}',
|
||||||
|
},
|
||||||
|
segments: [
|
||||||
|
{
|
||||||
|
id: "user",
|
||||||
|
tier: "dynamic",
|
||||||
|
tags: ["用户.需求", "用户.修订说明"],
|
||||||
|
label: "## 用户表述",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
blackboard: bb,
|
||||||
|
workerId: "design-step",
|
||||||
|
workerName: "创作 · 正文组成",
|
||||||
|
outputTags: ["设计.正文组成"],
|
||||||
|
});
|
||||||
|
expect(text).toContain("需要正文和监控");
|
||||||
|
expect(text).toContain("状态放到上面");
|
||||||
|
expect(text.indexOf("需要正文和监控")).toBeLessThan(text.indexOf("状态放到上面"));
|
||||||
|
expect(text).toContain("冲突时以较新的为准");
|
||||||
|
expect(text).toContain("待改底稿");
|
||||||
|
expect(text).toContain("split_board");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("inherit-existing asks the worker to keep the draft without a revision note", () => {
|
||||||
|
const bb = new Blackboard();
|
||||||
|
bb.write({
|
||||||
|
tag: "设计.美学纲领与交互范式",
|
||||||
|
content: '{"tone":"warm"}',
|
||||||
|
source: "worker",
|
||||||
|
});
|
||||||
|
const text = assembleWorkerContext({
|
||||||
|
inputs: {
|
||||||
|
"设计.美学纲领与交互范式": '{"tone":"warm"}',
|
||||||
|
},
|
||||||
|
segments: [
|
||||||
|
{
|
||||||
|
id: "inherit-existing",
|
||||||
|
tier: "static",
|
||||||
|
tags: ["设计.美学纲领与交互范式"],
|
||||||
|
label: "## 【既有产物 · 继承修改】在下列正文上继续改",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
blackboard: bb,
|
||||||
|
workerId: "design-step",
|
||||||
|
workerName: "创作 · 回头修改 · 美学纲领与交互范式",
|
||||||
|
outputTags: ["设计.美学纲领与交互范式"],
|
||||||
|
});
|
||||||
|
expect(text).toContain("既有产物 · 继承修改");
|
||||||
|
expect(text).toContain("回头修改");
|
||||||
|
expect(text).toContain("warm");
|
||||||
|
expect(text).not.toContain("按 SKILL 与上方分区完成任务");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("falls back to JSON inputs without segments", () => {
|
it("falls back to JSON inputs without segments", () => {
|
||||||
|
|||||||
@@ -77,6 +77,24 @@ describe("context-fragment.v1", () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not surface empty 追问 导语 as sidecar assessment", () => {
|
||||||
|
const side = extractFragmentAskSidecar({
|
||||||
|
schema: "context-fragment.v1",
|
||||||
|
brief: "返现都市",
|
||||||
|
正文: { 美学纲领: { 体验内核: "被宠" } },
|
||||||
|
自评: {
|
||||||
|
维度: [{ 名: "交互范式", 分数: 9, 说明: "站位已清" }],
|
||||||
|
},
|
||||||
|
追问: {
|
||||||
|
导语: "为贴近你要的质感,还需确认:",
|
||||||
|
题目: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(side.questions).toHaveLength(0);
|
||||||
|
expect(side.assessment).toContain("交互范式 9/10");
|
||||||
|
expect(side.assessment).not.toContain("还需确认");
|
||||||
|
});
|
||||||
|
|
||||||
it("isUsableContextFragment requires non-empty 正文", () => {
|
it("isUsableContextFragment requires non-empty 正文", () => {
|
||||||
expect(
|
expect(
|
||||||
isUsableContextFragment({
|
isUsableContextFragment({
|
||||||
@@ -125,6 +143,63 @@ describe("context-order.v1", () => {
|
|||||||
expect(segs[3].tier).toBe("dynamic");
|
expect(segs[3].tier).toBe("dynamic");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("accepts agents[] roster and maps gm → world-simulator", () => {
|
||||||
|
const doc = parseContextOrder({
|
||||||
|
schema: "context-order.v1",
|
||||||
|
brief: "旁观+主世界层+转述",
|
||||||
|
agents: [
|
||||||
|
{
|
||||||
|
id: "auditor",
|
||||||
|
label: "表格管理",
|
||||||
|
enabled: true,
|
||||||
|
when: "every_turn",
|
||||||
|
inserts: [
|
||||||
|
{ order: 0, ref: "worker.persona", projection: "fixed" },
|
||||||
|
{ order: 1, ref: "设计.变量设计与更新规则", projection: "summary" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "gm",
|
||||||
|
enabled: true,
|
||||||
|
inserts: [
|
||||||
|
{ order: 0, ref: "worker.persona", projection: "fixed" },
|
||||||
|
{ order: 1, ref: "对话.历史", projection: "summary" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ id: "narrator", enabled: true, inserts: [] },
|
||||||
|
{ id: "perspective", enabled: false, inserts: [] },
|
||||||
|
{ id: "chance", enabled: false, when: "on_demand", inserts: [] },
|
||||||
|
],
|
||||||
|
})!;
|
||||||
|
expect(doc.play_slots?.auditor).not.toBe(false);
|
||||||
|
expect(doc.play_slots?.gm).toBe(true);
|
||||||
|
expect(doc.play_slots?.perspective).toBe(false);
|
||||||
|
expect(doc.play_slots?.chance).toBe(false);
|
||||||
|
expect(slotOrderForRef(doc, "auditor")?.label).toBe("表格管理");
|
||||||
|
expect(slotOrderForRef(doc, "world-simulator")?.inserts.some((i) => i.ref === "对话.历史")).toBe(
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
expect(slotOrderForRef(doc, "role-decide")).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("index projection keeps brief, mount and 稳变 without 正文", () => {
|
||||||
|
const text = projectFragmentContent(
|
||||||
|
JSON.stringify({
|
||||||
|
schema: "context-fragment.v1",
|
||||||
|
技能: "美学纲领与交互范式",
|
||||||
|
brief: "免疫即权力",
|
||||||
|
mount: ["world-simulator", "narrator"],
|
||||||
|
稳变: "stable",
|
||||||
|
正文: { 美学纲领: { 体验内核: "很长不应出现在 index 里的正文" } },
|
||||||
|
}),
|
||||||
|
"index",
|
||||||
|
);
|
||||||
|
expect(text).toContain("免疫即权力");
|
||||||
|
expect(text).toContain("挂载:world-simulator、narrator");
|
||||||
|
expect(text).toContain("稳变:stable");
|
||||||
|
expect(text).not.toContain("很长不应出现在 index 里的正文");
|
||||||
|
});
|
||||||
|
|
||||||
it("reorders flat list and merges into worker set JSON", () => {
|
it("reorders flat list and merges into worker set JSON", () => {
|
||||||
const doc = parseContextOrder({
|
const doc = parseContextOrder({
|
||||||
schema: "context-order.v1",
|
schema: "context-order.v1",
|
||||||
|
|||||||
+192
-12
@@ -290,12 +290,21 @@ describe("creation-flow", () => {
|
|||||||
expect(
|
expect(
|
||||||
catalog?.modules.find((m) => m.name === "具体实例")?.repeatable,
|
catalog?.modules.find((m) => m.name === "具体实例")?.repeatable,
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
catalog?.modules.find((m) => m.name === "生成规则")?.kind,
|
||||||
|
).toBe("prior-artifact");
|
||||||
|
expect(
|
||||||
|
catalog?.modules.find((m) => m.name === "具体实例")?.kind,
|
||||||
|
).toBe("prior-artifact");
|
||||||
expect(
|
expect(
|
||||||
catalog?.modules.find((m) => m.name === "生成规则")?.params?.[0]?.key,
|
catalog?.modules.find((m) => m.name === "生成规则")?.params?.[0]?.key,
|
||||||
).toBe("target");
|
).toBe("target");
|
||||||
expect(
|
expect(
|
||||||
catalog?.modules.find((m) => m.name === "具体实例")?.params?.[0]?.key,
|
catalog?.modules.find((m) => m.name === "具体实例")?.params?.[0]?.key,
|
||||||
).toBe("rule_id");
|
).toBe("rule_id");
|
||||||
|
expect(
|
||||||
|
catalog?.modules.find((m) => m.name === "上下文投影排序")?.auto,
|
||||||
|
).toBe(true);
|
||||||
const gen = catalog?.modules.find((m) => m.name === "生成规则");
|
const gen = catalog?.modules.find((m) => m.name === "生成规则");
|
||||||
expect(gen?.when).toBeTruthy();
|
expect(gen?.when).toBeTruthy();
|
||||||
expect(gen?.when_not).toBeTruthy();
|
expect(gen?.when_not).toBeTruthy();
|
||||||
@@ -303,9 +312,12 @@ describe("creation-flow", () => {
|
|||||||
const block = formatModuleCatalogForAgent(catalog!);
|
const block = formatModuleCatalogForAgent(catalog!);
|
||||||
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("何时用");
|
expect(block).toContain("何时用");
|
||||||
expect(block).toContain("何时不用");
|
expect(block).toContain("何时不用");
|
||||||
|
expect(block).toContain("规划产物字段");
|
||||||
expect(block).toContain("编排参数");
|
expect(block).toContain("编排参数");
|
||||||
expect(block).not.toContain("设计.美学纲领与交互范式");
|
expect(block).not.toContain("设计.美学纲领与交互范式");
|
||||||
});
|
});
|
||||||
@@ -465,7 +477,8 @@ recipes:
|
|||||||
expect(loaded.promptBody).toContain("已完成:");
|
expect(loaded.promptBody).toContain("已完成:");
|
||||||
expect(loaded.promptBody).toContain("美学纲领与交互范式");
|
expect(loaded.promptBody).toContain("美学纲领与交互范式");
|
||||||
expect(loaded.promptBody).toContain("已验收");
|
expect(loaded.promptBody).toContain("已验收");
|
||||||
expect(loaded.promptBody).toContain("禁止再排一次执行");
|
expect(loaded.promptBody).toContain("禁止再排新建");
|
||||||
|
expect(loaded.promptBody).toContain("mode=revise");
|
||||||
expect(loaded.promptBody).toContain("生成规则");
|
expect(loaded.promptBody).toContain("生成规则");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -506,6 +519,7 @@ recipes:
|
|||||||
expect(loaded.promptBody).toContain("美学纲领与交互范式");
|
expect(loaded.promptBody).toContain("美学纲领与交互范式");
|
||||||
expect(loaded.promptBody).toContain("体验契约");
|
expect(loaded.promptBody).toContain("体验契约");
|
||||||
expect(loaded.promptBody).toContain("程序开场");
|
expect(loaded.promptBody).toContain("程序开场");
|
||||||
|
expect(loaded.promptBody).toContain("【本步参数】");
|
||||||
expect(loaded.worker.inputTags).toContain("创作.能力开场白");
|
expect(loaded.worker.inputTags).toContain("创作.能力开场白");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -606,24 +620,24 @@ recipes:
|
|||||||
expect(view.steps[0]?.params?.target).toBe("怪物");
|
expect(view.steps[0]?.params?.target).toBe("怪物");
|
||||||
|
|
||||||
const catalog = await loadModuleCatalog("dialogue/world-simulator");
|
const catalog = await loadModuleCatalog("dialogue/world-simulator");
|
||||||
expect(
|
const rules = catalog?.modules.find((m) => m.name === "生成规则");
|
||||||
catalog?.modules.find((m) => m.name === "生成规则")?.params?.some(
|
expect(rules?.kind).toBe("prior-artifact");
|
||||||
(p) => p.key === "target" && p.required,
|
expect(rules?.params?.some((p) => p.key === "target")).toBe(true);
|
||||||
),
|
|
||||||
).toBe(true);
|
|
||||||
expect(validateCreationFlow(flow, catalog).ok).toBe(true);
|
expect(validateCreationFlow(flow, catalog).ok).toBe(true);
|
||||||
|
|
||||||
const missing = parseCreationFlow(`{
|
const missing = parseCreationFlow(`{
|
||||||
"steps": [{ "name": "生成规则", "depends_on": [] }]
|
"steps": [{ "name": "生成规则", "depends_on": [] }]
|
||||||
}`)!;
|
}`)!;
|
||||||
// 编排期允许空壳;必填 params 在确认开干时钉齐
|
// 先验产物:编排期允许空壳,确认开干也不拦
|
||||||
expect(validateCreationFlow(missing, catalog).ok).toBe(true);
|
expect(validateCreationFlow(missing, catalog).ok).toBe(true);
|
||||||
const stepReady = validateStepReadyToRun(missing.steps[0]!, catalog);
|
const stepReady = validateStepReadyToRun(missing.steps[0]!, catalog);
|
||||||
expect(stepReady.ok).toBe(false);
|
expect(stepReady.ok).toBe(true);
|
||||||
expect(stepReady.errors.some((e) => e.includes("target"))).toBe(true);
|
const missingView = formatCreationFlowForUser(missing, catalog);
|
||||||
|
expect(missingView.steps[0]?.kind).toBe("prior-artifact");
|
||||||
|
expect(missingView.steps[0]?.paramsMissing).toBeUndefined();
|
||||||
|
|
||||||
const block = formatModuleCatalogForAgent(catalog!);
|
const block = formatModuleCatalogForAgent(catalog!);
|
||||||
expect(block).toContain("编排参数");
|
expect(block).toContain("规划产物字段");
|
||||||
expect(block).toContain("target");
|
expect(block).toContain("target");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -648,12 +662,47 @@ recipes:
|
|||||||
acceptedStepNames: [],
|
acceptedStepNames: [],
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
expect(loaded.promptBody).toContain("【本步参数】");
|
expect(loaded.promptBody).toContain("【规划产物】");
|
||||||
|
expect(loaded.promptBody).not.toContain("勿再问「生成什么");
|
||||||
expect(loaded.promptBody).toContain("target: 怪物");
|
expect(loaded.promptBody).toContain("target: 怪物");
|
||||||
expect(loaded.promptBody).toContain("rule_id: monsters");
|
expect(loaded.promptBody).toContain("rule_id: monsters");
|
||||||
expect(loaded.worker.outputTags).toContain("设计.生成规则");
|
expect(loaded.worker.outputTags).toContain("设计.生成规则");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("injects empty prior-artifact plan and still lets the step run", async () => {
|
||||||
|
const catalog = parseModuleCatalog(`
|
||||||
|
modules:
|
||||||
|
- name: 生成规则
|
||||||
|
declaration: 可执行规则
|
||||||
|
artifact: 设计.生成规则
|
||||||
|
kind: prior-artifact
|
||||||
|
params:
|
||||||
|
- key: target
|
||||||
|
label: 生成对象
|
||||||
|
required: true
|
||||||
|
`)!;
|
||||||
|
const empty = parseCreationFlow(`{
|
||||||
|
"steps": [{ "name": "生成规则", "depends_on": [] }]
|
||||||
|
}`)!;
|
||||||
|
expect(catalog.modules[0]?.kind).toBe("prior-artifact");
|
||||||
|
expect(validateStepReadyToRun(empty.steps[0]!, catalog).ok).toBe(true);
|
||||||
|
|
||||||
|
const loaded = await loadWorkerSkillWithContext(
|
||||||
|
"world-simulator",
|
||||||
|
"design-step",
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
flowRaw: JSON.stringify({
|
||||||
|
steps: [{ id: "生成规则", name: "生成规则", depends_on: [] }],
|
||||||
|
}),
|
||||||
|
currentStepName: "生成规则",
|
||||||
|
acceptedStepNames: [],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
expect(loaded.promptBody).toContain("【规划产物】");
|
||||||
|
expect(loaded.promptBody).toContain("尚未规划本步具体写什么");
|
||||||
|
});
|
||||||
|
|
||||||
it("nextPendingStep respects deps and accepted by id", () => {
|
it("nextPendingStep respects deps and accepted by id", () => {
|
||||||
const flow = parseCreationFlow(`{
|
const flow = parseCreationFlow(`{
|
||||||
"status": "open",
|
"status": "open",
|
||||||
@@ -730,4 +779,135 @@ recipes:
|
|||||||
}`)!;
|
}`)!;
|
||||||
expect(isCreationFlowComplete(legacy, ["美学纲领与交互范式"])).toBe(true);
|
expect(isCreationFlowComplete(legacy, ["美学纲领与交互范式"])).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("allows revising a non-repeatable skill and infers revises + id", () => {
|
||||||
|
const flow = parseCreationFlow(`{
|
||||||
|
"status": "open",
|
||||||
|
"steps": [
|
||||||
|
{ "name": "实现机制", "depends_on": [] },
|
||||||
|
{ "name": "实现机制", "mode": "回头修改", "depends_on": ["实现机制"] }
|
||||||
|
]
|
||||||
|
}`)!;
|
||||||
|
expect(flow.steps[1]).toMatchObject({
|
||||||
|
id: "实现机制·改",
|
||||||
|
name: "实现机制",
|
||||||
|
mode: "revise",
|
||||||
|
revises: "实现机制",
|
||||||
|
});
|
||||||
|
expect(validateCreationFlow(flow, sampleCatalog).ok).toBe(true);
|
||||||
|
const view = formatCreationFlowForUser(flow, sampleCatalog, ["实现机制"]);
|
||||||
|
expect(view.steps[1]?.mode).toBe("revise");
|
||||||
|
expect(view.steps[1]?.occurrence).toBeUndefined();
|
||||||
|
expect(view.steps[1]?.runState).toBe("current");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("still rejects two fresh copies of a non-repeatable skill", () => {
|
||||||
|
const flow = parseCreationFlow(`{
|
||||||
|
"steps": [
|
||||||
|
{ "name": "实现机制", "depends_on": [] },
|
||||||
|
{ "id": "实现机制#2", "name": "实现机制", "depends_on": ["实现机制"] }
|
||||||
|
]
|
||||||
|
}`)!;
|
||||||
|
const result = validateCreationFlow(flow, sampleCatalog);
|
||||||
|
expect(result.ok).toBe(false);
|
||||||
|
expect(result.errors.some((e) => e.includes("repeatable"))).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("treats a second generation-rules step as fresh, not revise", () => {
|
||||||
|
const flow = parseCreationFlow(`{
|
||||||
|
"steps": [
|
||||||
|
{ "id": "生成规则·怪物", "name": "生成规则", "params": { "target": "怪物" }, "depends_on": [] },
|
||||||
|
{ "id": "生成规则·NPC", "name": "生成规则", "params": { "target": "NPC" }, "depends_on": ["生成规则·怪物"] }
|
||||||
|
]
|
||||||
|
}`)!;
|
||||||
|
expect(flow.steps[1]?.mode).toBeUndefined();
|
||||||
|
expect(validateCreationFlow(flow, sampleCatalog).ok).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("revise of generation-rules inherits params from the origin step", () => {
|
||||||
|
const flow = parseCreationFlow(`{
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"id": "生成规则·怪物",
|
||||||
|
"name": "生成规则",
|
||||||
|
"params": { "target": "怪物", "rule_id": "monsters" },
|
||||||
|
"depends_on": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "生成规则·怪物·改",
|
||||||
|
"name": "生成规则",
|
||||||
|
"mode": "revise",
|
||||||
|
"revises": "生成规则·怪物",
|
||||||
|
"depends_on": ["生成规则·怪物"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`)!;
|
||||||
|
expect(flow.steps[1]?.params).toEqual({
|
||||||
|
target: "怪物",
|
||||||
|
rule_id: "monsters",
|
||||||
|
});
|
||||||
|
expect(validateCreationFlow(flow, sampleCatalog).ok).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("nextPendingStep prefers a ready revise over a later fresh step", () => {
|
||||||
|
const flow = parseCreationFlow(`{
|
||||||
|
"status": "open",
|
||||||
|
"steps": [
|
||||||
|
{ "name": "美学纲领与交互范式", "depends_on": [] },
|
||||||
|
{ "name": "生成规则", "depends_on": ["美学纲领与交互范式"] },
|
||||||
|
{
|
||||||
|
"name": "美学纲领与交互范式",
|
||||||
|
"mode": "revise",
|
||||||
|
"revises": "美学纲领与交互范式",
|
||||||
|
"depends_on": ["美学纲领与交互范式"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`)!;
|
||||||
|
expect(nextPendingStep(flow, ["美学纲领与交互范式"])?.mode).toBe("revise");
|
||||||
|
expect(nextPendingStep(flow, ["美学纲领与交互范式"])?.id).toBe(
|
||||||
|
"美学纲领与交互范式·改",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("injects inherit draft and skips opening on a revise design-step", async () => {
|
||||||
|
const flowRaw = JSON.stringify({
|
||||||
|
status: "open",
|
||||||
|
steps: [
|
||||||
|
{ id: "美学纲领与交互范式", name: "美学纲领与交互范式", depends_on: [] },
|
||||||
|
{
|
||||||
|
id: "美学纲领与交互范式·改",
|
||||||
|
name: "美学纲领与交互范式",
|
||||||
|
mode: "revise",
|
||||||
|
revises: "美学纲领与交互范式",
|
||||||
|
depends_on: ["美学纲领与交互范式"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const binding = await resolveDesignStepBinding({
|
||||||
|
skillPackRoot: "dialogue/world-simulator",
|
||||||
|
flowRaw,
|
||||||
|
currentStepName: "美学纲领与交互范式·改",
|
||||||
|
acceptedStepNames: ["美学纲领与交互范式"],
|
||||||
|
});
|
||||||
|
expect(binding?.opening).toBeNull();
|
||||||
|
expect(binding?.inheritTag).toBe("设计.美学纲领与交互范式");
|
||||||
|
|
||||||
|
const loaded = await loadWorkerSkillWithContext(
|
||||||
|
"world-simulator",
|
||||||
|
"design-step",
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
flowRaw,
|
||||||
|
currentStepName: "美学纲领与交互范式·改",
|
||||||
|
acceptedStepNames: ["美学纲领与交互范式"],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
expect(loaded.promptBody).toContain("【回头修改】");
|
||||||
|
expect(loaded.promptBody).not.toContain("程序开场");
|
||||||
|
expect(loaded.worker.name).toContain("回头修改");
|
||||||
|
expect(loaded.worker.inputTags).toContain("设计.美学纲领与交互范式");
|
||||||
|
expect(
|
||||||
|
loaded.worker.contextSegments?.some((s) => s.id === "inherit-existing"),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ describe("resolveAcceptanceModeForWorker", () => {
|
|||||||
expect(mode).toBe("user_confirmed");
|
expect(mode).toBe("user_confirmed");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("maps review → user_confirmed and continue → no_confirmation", () => {
|
it("maps review → user_confirmed and continue → no_confirmation in design", () => {
|
||||||
const bb = new Blackboard();
|
const bb = new Blackboard();
|
||||||
const yaml = JSON.stringify({
|
const yaml = JSON.stringify({
|
||||||
version: 1,
|
version: 1,
|
||||||
@@ -32,7 +32,7 @@ describe("resolveAcceptanceModeForWorker", () => {
|
|||||||
slots: {
|
slots: {
|
||||||
...createSession().slots,
|
...createSession().slots,
|
||||||
designInstanceReady: true,
|
designInstanceReady: true,
|
||||||
uiLifecycleStage: "play",
|
uiLifecycleStage: "design",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -51,6 +51,34 @@ describe("resolveAcceptanceModeForWorker", () => {
|
|||||||
}),
|
}),
|
||||||
).toBe("user_confirmed");
|
).toBe("user_confirmed");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("play ignores worker-set review and auto-continues", () => {
|
||||||
|
const bb = new Blackboard();
|
||||||
|
bb.write({
|
||||||
|
tag: "设计.worker集",
|
||||||
|
content: JSON.stringify({
|
||||||
|
version: 1,
|
||||||
|
workers: [{ ref: "narrator", acceptance: "review" }],
|
||||||
|
}),
|
||||||
|
source: "test",
|
||||||
|
});
|
||||||
|
const session = {
|
||||||
|
...createSession(),
|
||||||
|
slots: {
|
||||||
|
...createSession().slots,
|
||||||
|
designInstanceReady: true,
|
||||||
|
uiLifecycleStage: "play",
|
||||||
|
playLayerActive: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
expect(
|
||||||
|
resolveAcceptanceModeForWorker({
|
||||||
|
session,
|
||||||
|
blackboard: bb,
|
||||||
|
workerId: "narrator",
|
||||||
|
}),
|
||||||
|
).toBe("no_confirmation");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("buildDeclaredWorkerSkill", () => {
|
describe("buildDeclaredWorkerSkill", () => {
|
||||||
@@ -92,5 +120,6 @@ describe("buildDeclaredWorkerSkill", () => {
|
|||||||
expect(promptBody).toContain("残酷但不虐主");
|
expect(promptBody).toContain("残酷但不虐主");
|
||||||
expect(promptBody).toContain("主角免疫");
|
expect(promptBody).toContain("主角免疫");
|
||||||
expect(promptBody).toContain("声明驱动");
|
expect(promptBody).toContain("声明驱动");
|
||||||
|
expect(promptBody).toContain("500~2000");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -45,5 +45,11 @@ describe("display-labels", () => {
|
|||||||
const opening = reviewComposerCopy("opening-generator");
|
const opening = reviewComposerCopy("opening-generator");
|
||||||
expect(opening.acceptLabel).toBe("选定此开场");
|
expect(opening.acceptLabel).toBe("选定此开场");
|
||||||
expect(opening.kicker).toBe("待选定");
|
expect(opening.kicker).toBe("待选定");
|
||||||
|
|
||||||
|
const setup = reviewComposerCopy("design-step", {
|
||||||
|
outputTags: ["设计.开场白与开场变量"],
|
||||||
|
});
|
||||||
|
expect(setup.acceptLabel).toBe("选定此开场");
|
||||||
|
expect(setup.kind).toBe("opening");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import {
|
||||||
|
createMockToolCall,
|
||||||
|
MockLlmProvider,
|
||||||
|
} from "../src/llm/client.js";
|
||||||
|
import { createLocalLlmDriver } from "../src/runtime/driver.js";
|
||||||
|
|
||||||
|
describe("local LLM driver", () => {
|
||||||
|
it("continues while tools return results, then stops on text", async () => {
|
||||||
|
const llm = new MockLlmProvider([
|
||||||
|
createMockToolCall("lookup", { q: "a" }, "c1"),
|
||||||
|
"done",
|
||||||
|
]);
|
||||||
|
const driver = createLocalLlmDriver(llm);
|
||||||
|
const seen: string[] = [];
|
||||||
|
|
||||||
|
const run = await driver.run({
|
||||||
|
system: "sys",
|
||||||
|
messages: [{ role: "user", content: "go" }],
|
||||||
|
tools: [
|
||||||
|
{
|
||||||
|
type: "function",
|
||||||
|
function: {
|
||||||
|
name: "lookup",
|
||||||
|
description: "lookup",
|
||||||
|
parameters: { type: "object", properties: {} },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
handleStep: (calls) => {
|
||||||
|
seen.push(...calls.map((c) => c.name));
|
||||||
|
return {
|
||||||
|
kind: "continue",
|
||||||
|
results: calls.map((c) => ({ callId: c.id, content: "ok" })),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(seen).toEqual(["lookup"]);
|
||||||
|
expect(run.iterations).toBe(2);
|
||||||
|
expect(run.stop).toEqual({ kind: "text", content: "done" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("stops when handleStep concludes, without another model call", async () => {
|
||||||
|
const llm = new MockLlmProvider([
|
||||||
|
createMockToolCall("run_worker", { workerId: "x" }, "t1"),
|
||||||
|
"should-not-be-used",
|
||||||
|
]);
|
||||||
|
const driver = createLocalLlmDriver(llm);
|
||||||
|
|
||||||
|
const run = await driver.run({
|
||||||
|
system: "sys",
|
||||||
|
messages: [{ role: "user", content: "go" }],
|
||||||
|
tools: [
|
||||||
|
{
|
||||||
|
type: "function",
|
||||||
|
function: {
|
||||||
|
name: "run_worker",
|
||||||
|
description: "run",
|
||||||
|
parameters: { type: "object", properties: {} },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
handleStep: (calls) => ({ kind: "conclude", call: calls[0]! }),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(run.iterations).toBe(1);
|
||||||
|
expect(run.stop.kind).toBe("conclude");
|
||||||
|
if (run.stop.kind === "conclude") {
|
||||||
|
expect(run.stop.call.name).toBe("run_worker");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throws when the iteration cap is hit", async () => {
|
||||||
|
const llm = new MockLlmProvider([
|
||||||
|
createMockToolCall("ping", {}, "p1"),
|
||||||
|
]);
|
||||||
|
const driver = createLocalLlmDriver(llm);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
driver.run({
|
||||||
|
system: "sys",
|
||||||
|
messages: [{ role: "user", content: "go" }],
|
||||||
|
tools: [
|
||||||
|
{
|
||||||
|
type: "function",
|
||||||
|
function: {
|
||||||
|
name: "ping",
|
||||||
|
description: "ping",
|
||||||
|
parameters: { type: "object", properties: {} },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
maxIterations: 2,
|
||||||
|
handleStep: (calls) => ({
|
||||||
|
kind: "continue",
|
||||||
|
results: calls.map((c) => ({ callId: c.id, content: "pong" })),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
).rejects.toThrow(/exceeded 2 iterations/);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import {
|
||||||
|
closeCreationFlowRaw,
|
||||||
|
isOpeningSealArtifact,
|
||||||
|
parseOpeningSealPayload,
|
||||||
|
openingVariablesToTableDoc,
|
||||||
|
} from "../src/skills/opening-seal.js";
|
||||||
|
|
||||||
|
const fragment = JSON.stringify({
|
||||||
|
schema: "context-fragment.v1",
|
||||||
|
技能: "开场白与开场变量",
|
||||||
|
brief: "宿舍缺水",
|
||||||
|
mount: ["narrator"],
|
||||||
|
正文: {
|
||||||
|
开场白全文: "你在宿舍醒来,瓶里只剩一口水。",
|
||||||
|
开场白候选: [
|
||||||
|
"你在宿舍醒来,瓶里只剩一口水。",
|
||||||
|
"门外有人砸门。你还没想好要不要开。",
|
||||||
|
],
|
||||||
|
开场变量: [{ 名: "口粮", 值: 1, 依据: "开场只剩一口水" }],
|
||||||
|
},
|
||||||
|
开放问题: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("opening-seal", () => {
|
||||||
|
it("detects opening-setup and opening-generator artifacts", () => {
|
||||||
|
expect(
|
||||||
|
isOpeningSealArtifact({
|
||||||
|
workerId: "design-step",
|
||||||
|
outputTags: ["设计.开场白与开场变量"],
|
||||||
|
}),
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
isOpeningSealArtifact({
|
||||||
|
workerId: "opening-generator",
|
||||||
|
outputTags: ["输出.开场白"],
|
||||||
|
}),
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
isOpeningSealArtifact({
|
||||||
|
workerId: "design-step",
|
||||||
|
outputTags: ["设计.美学纲领与交互范式"],
|
||||||
|
}),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("parses 1+ opening candidates and selects by index", () => {
|
||||||
|
const first = parseOpeningSealPayload(fragment, 0);
|
||||||
|
expect(first?.candidates).toHaveLength(2);
|
||||||
|
expect(first?.selectedText).toContain("宿舍醒来");
|
||||||
|
const second = parseOpeningSealPayload(fragment, 1);
|
||||||
|
expect(second?.selectedText).toContain("砸门");
|
||||||
|
expect(second?.variables).toEqual([
|
||||||
|
{ name: "口粮", value: 1, note: "开场只剩一口水" },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("builds table rows from opening variables", () => {
|
||||||
|
const payload = parseOpeningSealPayload(fragment, 0);
|
||||||
|
const doc = openingVariablesToTableDoc(payload!.variables);
|
||||||
|
expect(doc.rows[0]?.key).toBe("口粮");
|
||||||
|
expect(doc.rows[0]?.value).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("closes an open creation flow", () => {
|
||||||
|
const raw = JSON.stringify({
|
||||||
|
version: 1,
|
||||||
|
status: "open",
|
||||||
|
steps: [{ id: "开场白与开场变量", name: "开场白与开场变量", depends_on: [] }],
|
||||||
|
});
|
||||||
|
const closed = JSON.parse(closeCreationFlowRaw(raw)!);
|
||||||
|
expect(closed.status).toBe("closed");
|
||||||
|
});
|
||||||
|
});
|
||||||
+227
-3
@@ -209,6 +209,72 @@ describe("phase machine", () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("later revision notes accumulate instead of replacing earlier ones", () => {
|
||||||
|
let session = createSession("default");
|
||||||
|
const artifact = createArtifact({
|
||||||
|
workerId: "design-step",
|
||||||
|
outputTags: ["设计.正文组成"],
|
||||||
|
});
|
||||||
|
session = {
|
||||||
|
...session,
|
||||||
|
artifacts: [artifact],
|
||||||
|
pendingArtifactId: artifact.id,
|
||||||
|
phase: "waiting_user",
|
||||||
|
waitingReason: { kind: "review_artifact", artifactId: artifact.id },
|
||||||
|
};
|
||||||
|
|
||||||
|
session = applyEvent(session, {
|
||||||
|
type: "user_rejected_artifact",
|
||||||
|
payload: { artifactId: artifact.id, reason: "不如原本的上中下样式" },
|
||||||
|
}).session;
|
||||||
|
|
||||||
|
session = {
|
||||||
|
...session,
|
||||||
|
phase: "waiting_user",
|
||||||
|
waitingReason: { kind: "review_artifact", artifactId: artifact.id },
|
||||||
|
pendingArtifactId: artifact.id,
|
||||||
|
};
|
||||||
|
|
||||||
|
const second = applyEvent(session, {
|
||||||
|
type: "user_rejected_artifact",
|
||||||
|
payload: { artifactId: artifact.id, reason: "状态很短,放到上面就行了" },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(second.session.slots["用户.修订说明"]).toContain("不如原本的上中下样式");
|
||||||
|
expect(second.session.slots["用户.修订说明"]).toContain("状态很短,放到上面就行了");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("accepting a design-step artifact clears stale revision notes", () => {
|
||||||
|
let session = createSession("default");
|
||||||
|
const artifact = createArtifact({
|
||||||
|
workerId: "design-step",
|
||||||
|
outputTags: ["设计.监控栏"],
|
||||||
|
});
|
||||||
|
session = {
|
||||||
|
...session,
|
||||||
|
artifacts: [artifact],
|
||||||
|
pendingArtifactId: artifact.id,
|
||||||
|
phase: "waiting_user",
|
||||||
|
waitingReason: { kind: "review_artifact", artifactId: artifact.id },
|
||||||
|
slots: {
|
||||||
|
"用户.修订说明": "上一步的意见",
|
||||||
|
revisionInstruction: "上一步的意见",
|
||||||
|
"用户.worker答复": "监控对象是剩余食物",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
session = applyEvent(session, {
|
||||||
|
type: "user_accepted_artifact",
|
||||||
|
payload: { artifactId: artifact.id },
|
||||||
|
}).session;
|
||||||
|
|
||||||
|
expect(session.slots["用户.修订说明"]).toBeUndefined();
|
||||||
|
expect(session.slots.revisionInstruction).toBeUndefined();
|
||||||
|
expect(session.slots["用户.worker答复"]).toBeUndefined();
|
||||||
|
expect(session.slots["用户.需求"]).toContain("监控对象是剩余食物");
|
||||||
|
expect(session.slots["用户.需求"]).toContain("上一步的意见");
|
||||||
|
});
|
||||||
|
|
||||||
it("failed revision rerun falls back to reviewing the previous artifact", () => {
|
it("failed revision rerun falls back to reviewing the previous artifact", () => {
|
||||||
let session = createSession("default");
|
let session = createSession("default");
|
||||||
const artifact = {
|
const artifact = {
|
||||||
@@ -281,7 +347,35 @@ describe("phase machine", () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("accepting design-step enters next_intent instead of invoking main agent", () => {
|
it("accepting design-step worker set proposes next step instead of invoking main agent", () => {
|
||||||
|
let session = createSession("default");
|
||||||
|
const artifact = createArtifact({
|
||||||
|
workerId: "design-step",
|
||||||
|
stepId: "细化终稿",
|
||||||
|
outputTags: ["设计.worker集"],
|
||||||
|
});
|
||||||
|
session = {
|
||||||
|
...session,
|
||||||
|
artifacts: [artifact],
|
||||||
|
pendingArtifactId: artifact.id,
|
||||||
|
phase: "waiting_user",
|
||||||
|
waitingReason: { kind: "review_artifact", artifactId: artifact.id },
|
||||||
|
};
|
||||||
|
|
||||||
|
const accepted = applyEvent(session, {
|
||||||
|
type: "user_accepted_artifact",
|
||||||
|
payload: { artifactId: artifact.id },
|
||||||
|
});
|
||||||
|
expect(accepted.session.slots.designInstanceReady).toBe(true);
|
||||||
|
expect(accepted.session.phase).toBe("running");
|
||||||
|
expect(accepted.session.waitingReason).toBeUndefined();
|
||||||
|
expect(accepted.effects).toEqual([{ type: "propose_next_creation_step" }]);
|
||||||
|
expect(accepted.effects.some((e) => e.type === "invoke_main_agent")).toBe(
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("accepting design-step proposes next step instead of invoking main agent", () => {
|
||||||
let session = createSession("default");
|
let session = createSession("default");
|
||||||
const artifact = createArtifact({
|
const artifact = createArtifact({
|
||||||
workerId: "design-step",
|
workerId: "design-step",
|
||||||
@@ -300,12 +394,22 @@ describe("phase machine", () => {
|
|||||||
type: "user_accepted_artifact",
|
type: "user_accepted_artifact",
|
||||||
payload: { artifactId: artifact.id },
|
payload: { artifactId: artifact.id },
|
||||||
});
|
});
|
||||||
expect(accepted.session.waitingReason?.kind).toBe("next_intent");
|
expect(accepted.session.phase).toBe("running");
|
||||||
|
expect(accepted.effects).toEqual([{ type: "propose_next_creation_step" }]);
|
||||||
expect(accepted.effects.some((e) => e.type === "invoke_main_agent")).toBe(
|
expect(accepted.effects.some((e) => e.type === "invoke_main_agent")).toBe(
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
});
|
||||||
|
|
||||||
const continued = applyEvent(accepted.session, {
|
it("legacy next_intent still proposes next step and records intent", () => {
|
||||||
|
let session = createSession("default");
|
||||||
|
session = {
|
||||||
|
...session,
|
||||||
|
phase: "waiting_user",
|
||||||
|
waitingReason: { kind: "next_intent", afterWorkerId: "design-step" },
|
||||||
|
};
|
||||||
|
|
||||||
|
const continued = applyEvent(session, {
|
||||||
type: "user_submitted_input",
|
type: "user_submitted_input",
|
||||||
payload: { text: "接下来写怪物生成规则" },
|
payload: { text: "接下来写怪物生成规则" },
|
||||||
});
|
});
|
||||||
@@ -317,4 +421,124 @@ describe("phase machine", () => {
|
|||||||
{ type: "propose_next_creation_step" },
|
{ type: "propose_next_creation_step" },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("accepting opening-setup seals creation instead of next_intent", () => {
|
||||||
|
let session = createSession("default");
|
||||||
|
const artifact = createArtifact({
|
||||||
|
workerId: "design-step",
|
||||||
|
stepId: "开场白与开场变量",
|
||||||
|
outputTags: ["设计.开场白与开场变量"],
|
||||||
|
});
|
||||||
|
session = {
|
||||||
|
...session,
|
||||||
|
artifacts: [artifact],
|
||||||
|
pendingArtifactId: artifact.id,
|
||||||
|
phase: "waiting_user",
|
||||||
|
waitingReason: { kind: "review_artifact", artifactId: artifact.id },
|
||||||
|
};
|
||||||
|
|
||||||
|
const accepted = applyEvent(session, {
|
||||||
|
type: "user_accepted_artifact",
|
||||||
|
payload: { artifactId: artifact.id },
|
||||||
|
});
|
||||||
|
expect(accepted.session.waitingReason?.kind).toBe("input");
|
||||||
|
expect(accepted.session.waitingReason).toMatchObject({
|
||||||
|
kind: "input",
|
||||||
|
});
|
||||||
|
expect(accepted.effects).toEqual([{ type: "seal_creation_opening" }]);
|
||||||
|
expect(
|
||||||
|
accepted.effects.some((e) => e.type === "invoke_main_agent"),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("accepting opening-generator also seals creation", () => {
|
||||||
|
let session = createSession("default");
|
||||||
|
const artifact = createArtifact({
|
||||||
|
workerId: "opening-generator",
|
||||||
|
outputTags: ["输出.开场白", "运行.初始变量"],
|
||||||
|
});
|
||||||
|
session = {
|
||||||
|
...session,
|
||||||
|
artifacts: [artifact],
|
||||||
|
pendingArtifactId: artifact.id,
|
||||||
|
phase: "waiting_user",
|
||||||
|
waitingReason: { kind: "review_artifact", artifactId: artifact.id },
|
||||||
|
};
|
||||||
|
|
||||||
|
const accepted = applyEvent(session, {
|
||||||
|
type: "user_accepted_artifact",
|
||||||
|
payload: { artifactId: artifact.id },
|
||||||
|
});
|
||||||
|
expect(accepted.session.waitingReason?.kind).toBe("input");
|
||||||
|
expect(accepted.effects).toEqual([{ type: "seal_creation_opening" }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sealed creation still blocks input until play", () => {
|
||||||
|
let session = createSession("default");
|
||||||
|
session = {
|
||||||
|
...session,
|
||||||
|
phase: "waiting_user",
|
||||||
|
waitingReason: { kind: "input" },
|
||||||
|
slots: { creationSealedByOpening: true, designInstanceReady: true },
|
||||||
|
};
|
||||||
|
const blocked = applyEvent(session, {
|
||||||
|
type: "user_submitted_input",
|
||||||
|
payload: { text: "我买金首饰" },
|
||||||
|
});
|
||||||
|
expect(blocked.effects).toEqual([
|
||||||
|
{ type: "emit_message", message: "创作已收口并保存。请切换到「游玩」开始。" },
|
||||||
|
]);
|
||||||
|
expect(blocked.effects.some((e) => e.type === "run_play_turn")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("play input starts a play turn even after opening seal", () => {
|
||||||
|
let session = createSession("default");
|
||||||
|
session = {
|
||||||
|
...session,
|
||||||
|
phase: "waiting_user",
|
||||||
|
waitingReason: { kind: "input" },
|
||||||
|
slots: {
|
||||||
|
creationSealedByOpening: true,
|
||||||
|
designInstanceReady: true,
|
||||||
|
uiLifecycleStage: "play",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const play = applyEvent(session, {
|
||||||
|
type: "user_submitted_input",
|
||||||
|
payload: { text: "我买金首饰" },
|
||||||
|
});
|
||||||
|
expect(play.session.phase).toBe("running");
|
||||||
|
expect(play.effects).toEqual([{ type: "run_play_turn" }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("accepting a play worker continues the play pipeline", () => {
|
||||||
|
let session = createSession("default");
|
||||||
|
const artifact = createArtifact({
|
||||||
|
workerId: "narrator",
|
||||||
|
outputTags: ["输出.用户展示"],
|
||||||
|
});
|
||||||
|
session = {
|
||||||
|
...session,
|
||||||
|
artifacts: [artifact],
|
||||||
|
pendingArtifactId: artifact.id,
|
||||||
|
phase: "waiting_user",
|
||||||
|
waitingReason: { kind: "review_artifact", artifactId: artifact.id },
|
||||||
|
slots: {
|
||||||
|
designInstanceReady: true,
|
||||||
|
uiLifecycleStage: "play",
|
||||||
|
playLayerActive: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const accepted = applyEvent(session, {
|
||||||
|
type: "user_accepted_artifact",
|
||||||
|
payload: { artifactId: artifact.id },
|
||||||
|
});
|
||||||
|
expect(accepted.effects).toEqual([{ type: "continue_play_turn" }]);
|
||||||
|
expect(accepted.effects.some((e) => e.type === "invoke_main_agent")).toBe(
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
expect(accepted.effects.some((e) => e.type === "propose_next_creation_step")).toBe(
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import {
|
|||||||
MockLlmProvider,
|
MockLlmProvider,
|
||||||
} from "../src/llm/client.js";
|
} from "../src/llm/client.js";
|
||||||
import { PhaseRuntime, createDecision } from "../src/runtime/phase-runtime.js";
|
import { PhaseRuntime, createDecision } from "../src/runtime/phase-runtime.js";
|
||||||
|
import { createSession } from "../src/runtime/phase-machine.js";
|
||||||
|
import { Blackboard } from "../src/blackboard/blackboard.js";
|
||||||
|
|
||||||
describe("phase runtime", () => {
|
describe("phase runtime", () => {
|
||||||
it("auto-loads default orchestrator and awaits first user input", async () => {
|
it("auto-loads default orchestrator and awaits first user input", async () => {
|
||||||
@@ -47,4 +49,82 @@ describe("phase runtime", () => {
|
|||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("play turn runs declared pipeline without review and waits for input", async () => {
|
||||||
|
const session = createSession("default");
|
||||||
|
session.phase = "waiting_user";
|
||||||
|
session.waitingReason = { kind: "input" };
|
||||||
|
session.slots = {
|
||||||
|
designInstanceReady: true,
|
||||||
|
uiLifecycleStage: "play",
|
||||||
|
playLayerActive: true,
|
||||||
|
startupCompleted: true,
|
||||||
|
};
|
||||||
|
const blackboard = new Blackboard();
|
||||||
|
blackboard.write({
|
||||||
|
tag: "设计.worker集",
|
||||||
|
content: JSON.stringify({
|
||||||
|
version: 1,
|
||||||
|
play_slots: {
|
||||||
|
auditor: false,
|
||||||
|
gm: true,
|
||||||
|
narrator: true,
|
||||||
|
perspective: false,
|
||||||
|
},
|
||||||
|
workers: [
|
||||||
|
{ ref: "world-simulator", acceptance: "continue" },
|
||||||
|
{ ref: "narrator", acceptance: "review" },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
source: "test",
|
||||||
|
});
|
||||||
|
const runtime = new PhaseRuntime({
|
||||||
|
autoStubWorker: true,
|
||||||
|
initialSession: session,
|
||||||
|
initialBlackboardItems: blackboard.exportItems(),
|
||||||
|
});
|
||||||
|
await runtime.submitInput("我网购一个金首饰试试");
|
||||||
|
expect(runtime.getSession().waitingReason?.kind).toBe("input");
|
||||||
|
expect(runtime.getSession().artifacts.map((a) => a.workerId)).toEqual([
|
||||||
|
"world-simulator",
|
||||||
|
"narrator",
|
||||||
|
]);
|
||||||
|
expect(runtime.getSession().artifacts.every((a) => a.status === "accepted")).toBe(
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("retryStuckRun restores the in-flight worker snapshot and reruns", async () => {
|
||||||
|
const runtime = new PhaseRuntime({ autoStubWorker: true });
|
||||||
|
await runtime.start();
|
||||||
|
await runtime.submitInput("网恋对象对话");
|
||||||
|
await runtime.submitDecision(
|
||||||
|
createDecision({
|
||||||
|
action: "run_worker",
|
||||||
|
reason: "编排创作流程",
|
||||||
|
workerId: "design-flow",
|
||||||
|
requiresApproval: false,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const snap = runtime.getLastWorkerRunSnapshot();
|
||||||
|
expect(snap?.workerId).toBe("design-flow");
|
||||||
|
const artifactsBefore = runtime.getSession().artifacts.length;
|
||||||
|
|
||||||
|
const session = runtime.getSession();
|
||||||
|
session.phase = "running";
|
||||||
|
session.waitingReason = undefined;
|
||||||
|
session.currentWorkerId = "design-flow";
|
||||||
|
|
||||||
|
await runtime.retryStuckRun();
|
||||||
|
expect(runtime.getSession().artifacts.length).toBeGreaterThan(artifactsBefore);
|
||||||
|
expect(
|
||||||
|
runtime.getSession().artifacts.filter((a) => a.workerId === "design-flow").length,
|
||||||
|
).toBeGreaterThanOrEqual(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("retryStuckRun throws when nothing is running", async () => {
|
||||||
|
const runtime = new PhaseRuntime({ autoStubWorker: true });
|
||||||
|
await runtime.start();
|
||||||
|
await expect(runtime.retryStuckRun()).rejects.toThrow("当前没有可重试的执行");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { importSillyTavernPreset } from "../src/preset/importer.js";
|
||||||
|
import { assemblePlayWorkerMessages } from "../src/preset/play-frame.js";
|
||||||
|
import { assemblePresetMessages } from "../src/preset/assembler.js";
|
||||||
|
import type { WorldInfoPack } from "../src/preset/world-info-pack.js";
|
||||||
|
|
||||||
|
const pack: WorldInfoPack = {
|
||||||
|
worldBookBefore: [{ id: "lore", name: "舞台", content: "古镇夜雨" }],
|
||||||
|
history: "用户:昨天来过",
|
||||||
|
worldBookAfter: [{ id: "vars", name: "变量", content: "HP 3" }],
|
||||||
|
turn: "我推门",
|
||||||
|
postTurn: [{ id: "tail", name: "尾注", content: "勿剧透" }],
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("assemblePlayWorkerMessages", () => {
|
||||||
|
it("walks extended prompt_order: ST pre, our before, history, our after, turn, ST post, postTurn", () => {
|
||||||
|
const report = importSillyTavernPreset({
|
||||||
|
prompts: [
|
||||||
|
{ identifier: "main", name: "Main", role: "system", content: "MAIN" },
|
||||||
|
{
|
||||||
|
identifier: "charDescription",
|
||||||
|
name: "Desc",
|
||||||
|
role: "system",
|
||||||
|
marker: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
identifier: "chatHistory",
|
||||||
|
name: "History",
|
||||||
|
role: "system",
|
||||||
|
marker: true,
|
||||||
|
},
|
||||||
|
{ identifier: "jailbreak", name: "PHI", role: "system", content: "PHI" },
|
||||||
|
],
|
||||||
|
prompt_order: [
|
||||||
|
{
|
||||||
|
character_id: 1,
|
||||||
|
order: [
|
||||||
|
{ identifier: "main", enabled: true },
|
||||||
|
{ identifier: "charDescription", enabled: true },
|
||||||
|
{ identifier: "chatHistory", enabled: true },
|
||||||
|
{ identifier: "jailbreak", enabled: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const messages = assemblePlayWorkerMessages({
|
||||||
|
systemPrompt: "SKILL",
|
||||||
|
preset: report.preset,
|
||||||
|
pack,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(messages.map((m) => [m.role, m.content])).toEqual([
|
||||||
|
["system", "SKILL"],
|
||||||
|
["system", "MAIN"],
|
||||||
|
["system", "### 舞台\n\n古镇夜雨"],
|
||||||
|
["system", "用户:昨天来过"],
|
||||||
|
["system", "### 变量\n\nHP 3"],
|
||||||
|
["user", "我推门"],
|
||||||
|
["system", "PHI"],
|
||||||
|
["system", "### 尾注\n\n勿剧透"],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not fill ST charDescription even if resolver returns content", () => {
|
||||||
|
const report = importSillyTavernPreset({
|
||||||
|
prompts: [
|
||||||
|
{
|
||||||
|
identifier: "charDescription",
|
||||||
|
name: "Desc",
|
||||||
|
role: "system",
|
||||||
|
marker: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
identifier: "chatHistory",
|
||||||
|
name: "History",
|
||||||
|
role: "system",
|
||||||
|
marker: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
prompt_order: [
|
||||||
|
{
|
||||||
|
character_id: 1,
|
||||||
|
order: [
|
||||||
|
{ identifier: "charDescription", enabled: true },
|
||||||
|
{ identifier: "chatHistory", enabled: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const messages = assemblePresetMessages(report.preset, (id) => {
|
||||||
|
if (id === "charDescription") return "CARD";
|
||||||
|
if (id === "chatHistory") return "HIST";
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
expect(messages.map((m) => m.content)).toEqual(["HIST"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import {
|
||||||
|
SLOT_PLAY_LAYER_ACTIVE,
|
||||||
|
SLOT_PLAY_TURN_QUEUE,
|
||||||
|
isPlayLayerActive,
|
||||||
|
readPlayTurnQueue,
|
||||||
|
withPlayTurnQueue,
|
||||||
|
} from "../src/skills/play-turn.js";
|
||||||
|
|
||||||
|
describe("play-turn slots", () => {
|
||||||
|
it("reads a trimmed queue and ignores junk", () => {
|
||||||
|
expect(readPlayTurnQueue(undefined)).toEqual([]);
|
||||||
|
expect(
|
||||||
|
readPlayTurnQueue({ [SLOT_PLAY_TURN_QUEUE]: [" auditor ", "", 3, "gm"] }),
|
||||||
|
).toEqual(["auditor", "gm"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("writes queue or clears when empty", () => {
|
||||||
|
const withIds = withPlayTurnQueue({ a: 1 }, ["gm", "narrator"]);
|
||||||
|
expect(withIds[SLOT_PLAY_TURN_QUEUE]).toEqual(["gm", "narrator"]);
|
||||||
|
expect(withPlayTurnQueue(withIds, []).playTurnQueue).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("detects play layer flag", () => {
|
||||||
|
expect(isPlayLayerActive(undefined)).toBe(false);
|
||||||
|
expect(isPlayLayerActive({ [SLOT_PLAY_LAYER_ACTIVE]: true })).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -51,6 +51,31 @@ describe("present.v1", () => {
|
|||||||
expect(p.shell).toBe("chapter_reader");
|
expect(p.shell).toBe("chapter_reader");
|
||||||
expect(p.blocks.body).toBe("hello");
|
expect(p.blocks.body).toBe("hello");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("strips 输出.用户展示 heading from plain body", () => {
|
||||||
|
const view = parsePresentPacket("### 输出.用户展示\n\n叮!账户来了。");
|
||||||
|
expect(view.packet.blocks.body).toBe("叮!账户来了。");
|
||||||
|
expect(String(view.packet.blocks.body)).not.toContain("用户展示");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("strips heading then parses present JSON", () => {
|
||||||
|
const view = parsePresentPacket(
|
||||||
|
'### 输出.用户展示\n\n{"schema":"present.v1","shell":"prose","blocks":{"body":"场面很长"}}',
|
||||||
|
);
|
||||||
|
expect(view.fallbackPlain).toBe(false);
|
||||||
|
expect(view.packet.blocks.body).toBe("场面很长");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("strips heading inside blocks.body", () => {
|
||||||
|
const view = parsePresentPacket(
|
||||||
|
JSON.stringify({
|
||||||
|
schema: "present.v1",
|
||||||
|
shell: "prose",
|
||||||
|
blocks: { body: "### 输出.用户展示\n正文" },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(view.packet.blocks.body).toBe("正文");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("shell adaptation from reply-format", () => {
|
describe("shell adaptation from reply-format", () => {
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { importSillyTavernPreset } from "../src/preset/importer.js";
|
||||||
|
import {
|
||||||
|
assemblePresetProbe,
|
||||||
|
DEFAULT_PROBE_CONTEXT,
|
||||||
|
PROBE_SYSTEM_PROMPT,
|
||||||
|
runPresetProbe,
|
||||||
|
} from "../src/preset/probe.js";
|
||||||
|
|
||||||
|
function samplePreset() {
|
||||||
|
return importSillyTavernPreset({
|
||||||
|
prompts: [
|
||||||
|
{ identifier: "main", name: "Main", role: "system", content: "MAIN" },
|
||||||
|
{
|
||||||
|
identifier: "charDescription",
|
||||||
|
name: "Desc",
|
||||||
|
role: "system",
|
||||||
|
marker: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
identifier: "chatHistory",
|
||||||
|
name: "History",
|
||||||
|
role: "system",
|
||||||
|
marker: true,
|
||||||
|
},
|
||||||
|
{ identifier: "jailbreak", name: "PHI", role: "system", content: "PHI" },
|
||||||
|
],
|
||||||
|
prompt_order: [
|
||||||
|
{
|
||||||
|
character_id: 1,
|
||||||
|
order: [
|
||||||
|
{ identifier: "main", enabled: true },
|
||||||
|
{ identifier: "charDescription", enabled: true },
|
||||||
|
{ identifier: "chatHistory", enabled: true },
|
||||||
|
{ identifier: "jailbreak", enabled: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}).preset;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("assemblePresetProbe", () => {
|
||||||
|
it("fills the sandwich with sample lore, history, and the current turn", () => {
|
||||||
|
const messages = assemblePresetProbe(samplePreset(), {
|
||||||
|
message: "我推开门。",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(messages.map((m) => [m.role, m.content])).toEqual([
|
||||||
|
["system", PROBE_SYSTEM_PROMPT],
|
||||||
|
["system", "MAIN"],
|
||||||
|
["system", `### 试跑设定\n\n${DEFAULT_PROBE_CONTEXT.loreBefore}`],
|
||||||
|
["system", DEFAULT_PROBE_CONTEXT.history],
|
||||||
|
["system", `### 本轮状态\n\n${DEFAULT_PROBE_CONTEXT.loreAfter}`],
|
||||||
|
["user", "我推开门。"],
|
||||||
|
["system", "PHI"],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("omits empty optional bags", () => {
|
||||||
|
const messages = assemblePresetProbe(samplePreset(), {
|
||||||
|
message: "你好",
|
||||||
|
loreBefore: "",
|
||||||
|
history: "",
|
||||||
|
loreAfter: "",
|
||||||
|
postTurn: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(messages.map((m) => m.content)).toEqual([
|
||||||
|
PROBE_SYSTEM_PROMPT,
|
||||||
|
"MAIN",
|
||||||
|
"你好",
|
||||||
|
"PHI",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("runPresetProbe", () => {
|
||||||
|
it("returns assembled messages without calling a model", async () => {
|
||||||
|
const result = await runPresetProbe({
|
||||||
|
preset: samplePreset(),
|
||||||
|
input: { message: "我推开门。" },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.completed).toBe(false);
|
||||||
|
expect(result.reply).toBeNull();
|
||||||
|
expect(result.messages.some((m) => m.content === "我推开门。")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns the model reply when complete is provided", async () => {
|
||||||
|
const result = await runPresetProbe({
|
||||||
|
preset: samplePreset(),
|
||||||
|
input: { message: "我推开门。" },
|
||||||
|
complete: async () => ({ content: "请进。", reasoning: "短回复" }),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.completed).toBe(true);
|
||||||
|
expect(result.reply).toBe("请进。");
|
||||||
|
expect(result.reasoning).toBe("短回复");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps messages when generate fails", async () => {
|
||||||
|
const result = await runPresetProbe({
|
||||||
|
preset: samplePreset(),
|
||||||
|
input: { message: "我推开门。" },
|
||||||
|
complete: async () => {
|
||||||
|
throw new Error("upstream down");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.completed).toBe(false);
|
||||||
|
expect(result.error).toBe("upstream down");
|
||||||
|
expect(result.messages.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects an empty turn", async () => {
|
||||||
|
await expect(
|
||||||
|
runPresetProbe({
|
||||||
|
preset: samplePreset(),
|
||||||
|
input: { message: " " },
|
||||||
|
}),
|
||||||
|
).rejects.toThrow("缺少本轮输入");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import type {
|
||||||
|
ChatMessage,
|
||||||
|
CompleteResult,
|
||||||
|
LlmProvider,
|
||||||
|
} from "../src/llm/client.js";
|
||||||
|
import { wrapLlmForSession } from "../src/llm/preset-wrapper.js";
|
||||||
|
import type { LlmTrackingContext } from "../src/llm/token-tracker.js";
|
||||||
|
import type { PresetPackage } from "../src/types/preset.js";
|
||||||
|
|
||||||
|
const samplePreset: PresetPackage = {
|
||||||
|
id: "p1",
|
||||||
|
name: "test-preset",
|
||||||
|
source: "native",
|
||||||
|
prompts: [
|
||||||
|
{
|
||||||
|
id: "sys",
|
||||||
|
name: "系统",
|
||||||
|
enabled: true,
|
||||||
|
role: "system",
|
||||||
|
content: "PRESET_RULES",
|
||||||
|
marker: false,
|
||||||
|
sourceIdentifier: "sys",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
promptOrder: [{ promptId: "sys", enabled: true, orderIndex: 0 }],
|
||||||
|
generation: { temperature: 0.91, maxOutputTokens: 1234 },
|
||||||
|
unsupported: [],
|
||||||
|
importedAt: "2026-01-01T00:00:00.000Z",
|
||||||
|
};
|
||||||
|
|
||||||
|
function recordingProvider(): {
|
||||||
|
provider: LlmProvider;
|
||||||
|
received: Array<{ messages: ChatMessage[]; generation?: unknown }>;
|
||||||
|
} {
|
||||||
|
const received: Array<{ messages: ChatMessage[]; generation?: unknown }> = [];
|
||||||
|
const provider: LlmProvider = {
|
||||||
|
async complete(messages, options): Promise<CompleteResult> {
|
||||||
|
received.push({ messages, generation: options?.generation });
|
||||||
|
return { content: "ok", model: "rec-model" };
|
||||||
|
},
|
||||||
|
async completeStream(messages, options, callbacks) {
|
||||||
|
received.push({ messages, generation: options?.generation });
|
||||||
|
callbacks?.onContentDelta?.("ok");
|
||||||
|
return { content: "ok", model: "rec-model" };
|
||||||
|
},
|
||||||
|
async completeWithTools(messages, options) {
|
||||||
|
received.push({ messages, generation: options.generation });
|
||||||
|
return { content: "ok", toolCalls: [], model: "rec-model" };
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return { provider, received };
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("wrapLlmForSession", () => {
|
||||||
|
it("context trace matches the exact messages and generation sent to the inner LLM", async () => {
|
||||||
|
const { provider, received } = recordingProvider();
|
||||||
|
const ctx: LlmTrackingContext = {};
|
||||||
|
const llm = wrapLlmForSession(provider, { current: ctx }, () => samplePreset);
|
||||||
|
|
||||||
|
await llm.complete(
|
||||||
|
[{ role: "user", content: "TASK" }],
|
||||||
|
{ caller: "worker:design-step" },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(received).toHaveLength(1);
|
||||||
|
expect(received[0].messages).toEqual([{ role: "user", content: "TASK" }]);
|
||||||
|
expect(received[0].generation).toMatchObject({
|
||||||
|
temperature: 0.91,
|
||||||
|
maxOutputTokens: 1234,
|
||||||
|
});
|
||||||
|
expect(ctx.pendingContextTrace?.messages).toEqual([
|
||||||
|
{ role: "user", content: "TASK" },
|
||||||
|
]);
|
||||||
|
expect(ctx.pendingContextTrace?.generation).toMatchObject({
|
||||||
|
temperature: 0.91,
|
||||||
|
maxOutputTokens: 1234,
|
||||||
|
});
|
||||||
|
expect(ctx.pendingContextTrace?.caller).toBe("worker:design-step");
|
||||||
|
expect(ctx.pendingContextTrace?.model).toBe("rec-model");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("completeStream also records the merged request", async () => {
|
||||||
|
const { provider, received } = recordingProvider();
|
||||||
|
const ctx: LlmTrackingContext = {};
|
||||||
|
const llm = wrapLlmForSession(provider, { current: ctx }, () => samplePreset);
|
||||||
|
const chunks: string[] = [];
|
||||||
|
|
||||||
|
await llm.completeStream!(
|
||||||
|
[{ role: "user", content: "TASK" }],
|
||||||
|
{ caller: "worker:stream" },
|
||||||
|
{ onContentDelta: (d) => chunks.push(d) },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(chunks.join("")).toBe("ok");
|
||||||
|
expect(received[0].messages).toEqual([{ role: "user", content: "TASK" }]);
|
||||||
|
expect(ctx.pendingContextTrace?.messages).toEqual(received[0].messages);
|
||||||
|
});
|
||||||
|
});
|
||||||
+59
-2
@@ -52,16 +52,73 @@ const samplePreset = {
|
|||||||
describe("importSillyTavernPreset", () => {
|
describe("importSillyTavernPreset", () => {
|
||||||
it("picks prompt_order with most enabled content", () => {
|
it("picks prompt_order with most enabled content", () => {
|
||||||
const report = importSillyTavernPreset(samplePreset, { name: "test" });
|
const report = importSillyTavernPreset(samplePreset, { name: "test" });
|
||||||
expect(report.enabledCount).toBe(3);
|
|
||||||
expect(report.preset.promptOrder.map((o) => o.promptId)).toEqual([
|
expect(report.preset.promptOrder.map((o) => o.promptId)).toEqual([
|
||||||
"custom-a",
|
"custom-a",
|
||||||
"custom-b",
|
"custom-b",
|
||||||
"main",
|
"main",
|
||||||
|
"worldBookBefore",
|
||||||
|
"chatHistory",
|
||||||
|
"worldBookAfter",
|
||||||
|
"currentTurn",
|
||||||
|
"postTurn",
|
||||||
]);
|
]);
|
||||||
|
expect(report.enabledCount).toBe(8);
|
||||||
expect(report.preset.generation.temperature).toBe(1);
|
expect(report.preset.generation.temperature).toBe(1);
|
||||||
expect(report.preset.generation.maxOutputTokens).toBe(60000);
|
expect(report.preset.generation.maxOutputTokens).toBe(60000);
|
||||||
expect(report.warnings.some((w) => w.includes("prompt_order"))).toBe(true);
|
expect(report.warnings.some((w) => w.includes("prompt_order"))).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("inserts app markers around chatHistory and leaves ST card/WI empty", () => {
|
||||||
|
const report = importSillyTavernPreset({
|
||||||
|
prompts: [
|
||||||
|
{ identifier: "main", name: "Main", role: "system", content: "MAIN" },
|
||||||
|
{
|
||||||
|
identifier: "charDescription",
|
||||||
|
name: "Char Description",
|
||||||
|
role: "system",
|
||||||
|
marker: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
identifier: "chatHistory",
|
||||||
|
name: "Chat History",
|
||||||
|
role: "system",
|
||||||
|
marker: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
identifier: "jailbreak",
|
||||||
|
name: "Post-History",
|
||||||
|
role: "system",
|
||||||
|
content: "PHI",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
prompt_order: [
|
||||||
|
{
|
||||||
|
character_id: 1,
|
||||||
|
order: [
|
||||||
|
{ identifier: "main", enabled: true },
|
||||||
|
{ identifier: "charDescription", enabled: true },
|
||||||
|
{ identifier: "chatHistory", enabled: true },
|
||||||
|
{ identifier: "jailbreak", enabled: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
expect(report.preset.promptOrder.map((o) => o.promptId)).toEqual([
|
||||||
|
"main",
|
||||||
|
"charDescription",
|
||||||
|
"worldBookBefore",
|
||||||
|
"chatHistory",
|
||||||
|
"worldBookAfter",
|
||||||
|
"currentTurn",
|
||||||
|
"jailbreak",
|
||||||
|
"postTurn",
|
||||||
|
]);
|
||||||
|
const messages = assemblePresetMessages(report.preset);
|
||||||
|
expect(messages).toEqual([
|
||||||
|
{ role: "system", content: "MAIN" },
|
||||||
|
{ role: "system", content: "PHI" },
|
||||||
|
]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("assemblePresetMessages", () => {
|
describe("assemblePresetMessages", () => {
|
||||||
@@ -79,7 +136,7 @@ describe("listEnabledPresetEntries", () => {
|
|||||||
it("lists enabled order with inject flags", () => {
|
it("lists enabled order with inject flags", () => {
|
||||||
const report = importSillyTavernPreset(samplePreset);
|
const report = importSillyTavernPreset(samplePreset);
|
||||||
const entries = listEnabledPresetEntries(report.preset);
|
const entries = listEnabledPresetEntries(report.preset);
|
||||||
expect(entries.length).toBe(3);
|
expect(entries.length).toBe(8);
|
||||||
expect(countInjectingEntries(entries)).toBe(2);
|
expect(countInjectingEntries(entries)).toBe(2);
|
||||||
expect(entries.filter((e) => e.willInject).map((e) => e.content)).toEqual([
|
expect(entries.filter((e) => e.willInject).map((e) => e.content)).toEqual([
|
||||||
"instruction A",
|
"instruction A",
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { MockLlmProvider } from "../src/llm/client.js";
|
||||||
|
import {
|
||||||
|
isAbortError,
|
||||||
|
runWithAbortSignal,
|
||||||
|
throwIfAborted,
|
||||||
|
} from "../src/llm/run-abort.js";
|
||||||
|
|
||||||
|
describe("run abort", () => {
|
||||||
|
it("isAbortError detects AbortError name", () => {
|
||||||
|
const err = new Error("This operation was aborted");
|
||||||
|
err.name = "AbortError";
|
||||||
|
expect(isAbortError(err)).toBe(true);
|
||||||
|
expect(isAbortError(new Error("LLM request failed"))).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throwIfAborted raises AbortError when the signal is aborted", () => {
|
||||||
|
const ac = new AbortController();
|
||||||
|
ac.abort();
|
||||||
|
expect(() => throwIfAborted(ac.signal)).toThrowError(/aborted/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("mock LLM respects the active abort signal", async () => {
|
||||||
|
const llm = new MockLlmProvider(["ok"]);
|
||||||
|
const ac = new AbortController();
|
||||||
|
ac.abort();
|
||||||
|
await expect(
|
||||||
|
runWithAbortSignal(ac.signal, () => llm.complete([{ role: "user", content: "x" }])),
|
||||||
|
).rejects.toMatchObject({ name: "AbortError" });
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import {
|
import {
|
||||||
filterBlackboardForInstance,
|
|
||||||
isRunPhaseBlackboardTag,
|
isRunPhaseBlackboardTag,
|
||||||
materializeInstanceSnapshotPayload,
|
materializeInstanceSnapshotPayload,
|
||||||
} from "../src/book/snapshot-filters.js";
|
} from "../src/book/snapshot-filters.js";
|
||||||
@@ -12,6 +11,10 @@ describe("snapshot-filters", () => {
|
|||||||
expect(isRunPhaseBlackboardTag("角色.A.设定")).toBe(false);
|
expect(isRunPhaseBlackboardTag("角色.A.设定")).toBe(false);
|
||||||
expect(isRunPhaseBlackboardTag("博弈.规则.草稿")).toBe(false);
|
expect(isRunPhaseBlackboardTag("博弈.规则.草稿")).toBe(false);
|
||||||
expect(isRunPhaseBlackboardTag("运行.事件流")).toBe(true);
|
expect(isRunPhaseBlackboardTag("运行.事件流")).toBe(true);
|
||||||
|
expect(isRunPhaseBlackboardTag("输出.开场白")).toBe(false);
|
||||||
|
expect(isRunPhaseBlackboardTag("运行.初始变量")).toBe(false);
|
||||||
|
expect(isRunPhaseBlackboardTag("变量.当前")).toBe(false);
|
||||||
|
expect(isRunPhaseBlackboardTag("对话.历史")).toBe(true);
|
||||||
expect(isRunPhaseBlackboardTag("角色.A.思考")).toBe(true);
|
expect(isRunPhaseBlackboardTag("角色.A.思考")).toBe(true);
|
||||||
expect(isRunPhaseBlackboardTag("世界.当前状态")).toBe(true);
|
expect(isRunPhaseBlackboardTag("世界.当前状态")).toBe(true);
|
||||||
});
|
});
|
||||||
@@ -19,6 +22,8 @@ describe("snapshot-filters", () => {
|
|||||||
it("materializeInstanceSnapshotPayload removes run blackboard items", () => {
|
it("materializeInstanceSnapshotPayload removes run blackboard items", () => {
|
||||||
const session = createSession("default");
|
const session = createSession("default");
|
||||||
session.slots.startupCompleted = true;
|
session.slots.startupCompleted = true;
|
||||||
|
session.slots.playLayerActive = true;
|
||||||
|
session.slots.playTurnQueue = ["narrator"];
|
||||||
session.artifacts = [
|
session.artifacts = [
|
||||||
{
|
{
|
||||||
id: "a1",
|
id: "a1",
|
||||||
@@ -44,6 +49,7 @@ describe("snapshot-filters", () => {
|
|||||||
{ id: "1", tag: "情境.实验.设定", content: "x", source: "worker", createdAt: "", updatedAt: "" },
|
{ id: "1", tag: "情境.实验.设定", content: "x", source: "worker", createdAt: "", updatedAt: "" },
|
||||||
{ id: "2", tag: "运行.事件流", content: "y", source: "worker", createdAt: "", updatedAt: "" },
|
{ id: "2", tag: "运行.事件流", content: "y", source: "worker", createdAt: "", updatedAt: "" },
|
||||||
{ id: "3", tag: "角色.A.设定", content: "z", source: "worker", createdAt: "", updatedAt: "" },
|
{ id: "3", tag: "角色.A.设定", content: "z", source: "worker", createdAt: "", updatedAt: "" },
|
||||||
|
{ id: "4", tag: "对话.历史", content: "## 用户\n\n买金", source: "runtime", createdAt: "", updatedAt: "" },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -53,6 +59,7 @@ describe("snapshot-filters", () => {
|
|||||||
]);
|
]);
|
||||||
expect(runtimeSession.artifacts).toHaveLength(1);
|
expect(runtimeSession.artifacts).toHaveLength(1);
|
||||||
expect(runtimeSession.artifacts[0].workerId).toBe("setup-scenario");
|
expect(runtimeSession.artifacts[0].workerId).toBe("setup-scenario");
|
||||||
expect(filterBlackboardForInstance).toBeDefined();
|
expect(runtimeSession.slots.playLayerActive).toBeUndefined();
|
||||||
|
expect(runtimeSession.slots.playTurnQueue).toBeUndefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -98,5 +98,14 @@ describe("worker-declaration", () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
expect(inferLifecycleStage(s2)).toBe("play");
|
expect(inferLifecycleStage(s2)).toBe("play");
|
||||||
|
|
||||||
|
const s3 = {
|
||||||
|
...sessionWithAcceptedWorkerSet(),
|
||||||
|
slots: {
|
||||||
|
...sessionWithAcceptedWorkerSet().slots,
|
||||||
|
playLayerActive: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
expect(inferLifecycleStage(s3)).toBe("play");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -191,7 +191,7 @@ describe("sanitizeWorkerSetOutputs", () => {
|
|||||||
expect(result.outputs["设计.美学纲领与交互范式"]).toContain("特权与惊惶");
|
expect(result.outputs["设计.美学纲领与交互范式"]).toContain("特权与惊惶");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not accept a fragment without 正文; keeps 追问 for the LLM loop", () => {
|
it("keeps incomplete fragment (no 正文) for review; 追问 hangs as sidecar", () => {
|
||||||
const result = parseWorkerResponseForTest(
|
const result = parseWorkerResponseForTest(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
outputs: {
|
outputs: {
|
||||||
@@ -210,12 +210,12 @@ describe("sanitizeWorkerSetOutputs", () => {
|
|||||||
}),
|
}),
|
||||||
["设计.美学纲领与交互范式"],
|
["设计.美学纲领与交互范式"],
|
||||||
);
|
);
|
||||||
expect(result.outputs["设计.美学纲领与交互范式"]).toBeUndefined();
|
expect(result.outputs["设计.美学纲领与交互范式"]).toContain("还差站位");
|
||||||
expect(result.askUser?.length).toBeGreaterThan(0);
|
expect(result.askUser?.length).toBeGreaterThan(0);
|
||||||
expect(result.askUser?.[0]?.prompt).toContain("你代入吗");
|
expect(result.askUser?.[0]?.prompt).toContain("你代入吗");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("asks to continue the loop when fragment JSON is unusable", () => {
|
it("keeps truncated fragment JSON as a reviewable draft", () => {
|
||||||
const result = parseWorkerResponseForTest(
|
const result = parseWorkerResponseForTest(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
outputs: {
|
outputs: {
|
||||||
@@ -226,7 +226,54 @@ describe("sanitizeWorkerSetOutputs", () => {
|
|||||||
}),
|
}),
|
||||||
["设计.美学纲领与交互范式"],
|
["设计.美学纲领与交互范式"],
|
||||||
);
|
);
|
||||||
expect(result.outputs["设计.美学纲领与交互范式"]).toBeUndefined();
|
expect(result.outputs["设计.美学纲领与交互范式"]).toContain("孤立免疫");
|
||||||
expect(result.askUser?.[0]?.prompt).toContain("还没写出可验收的产物");
|
expect(result.askUser).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("canonicalizes context-order agents[] into slots on the artifact tag", () => {
|
||||||
|
const result = parseWorkerResponseForTest(
|
||||||
|
JSON.stringify({
|
||||||
|
outputs: {
|
||||||
|
"设计.上下文投影排序": {
|
||||||
|
schema: "context-order.v1",
|
||||||
|
brief: "主世界层",
|
||||||
|
agents: [
|
||||||
|
{
|
||||||
|
id: "gm",
|
||||||
|
enabled: true,
|
||||||
|
inserts: [
|
||||||
|
{ order: 0, ref: "worker.persona", projection: "fixed" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
summary: "ok",
|
||||||
|
}),
|
||||||
|
["设计.上下文投影排序"],
|
||||||
|
);
|
||||||
|
const doc = JSON.parse(result.outputs["设计.上下文投影排序"]!);
|
||||||
|
expect(doc.slots[0].ref).toBe("world-simulator");
|
||||||
|
expect(doc.play_slots.gm).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps a whole-response truncated JSON as the artifact draft", () => {
|
||||||
|
const raw =
|
||||||
|
'{"outputs":{"设计.上下文投影排序":{"schema":"context-order.v1","brief":"旁观+转述","agents":[';
|
||||||
|
const result = parseWorkerResponseForTest(raw, ["设计.上下文投影排序"]);
|
||||||
|
expect(result.outputs["设计.上下文投影排序"]).toContain("context-order.v1");
|
||||||
|
expect(result.askUser).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("puts full 输出.用户展示 into preview without tag fence or 4000 cap", () => {
|
||||||
|
const body = "场面".repeat(2100);
|
||||||
|
const result = sanitizeWorkerSetOutputs({
|
||||||
|
outputs: { "输出.用户展示": body },
|
||||||
|
summary: "ok",
|
||||||
|
preview: "旧",
|
||||||
|
});
|
||||||
|
expect(result.preview).toBe(body);
|
||||||
|
expect(result.preview).not.toContain("### ");
|
||||||
|
expect(result.preview.length).toBeGreaterThan(4000);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { Blackboard } from "../src/blackboard/blackboard.js";
|
||||||
|
import { DIALOGUE_HISTORY_TAG } from "../src/skills/dialogue-history.js";
|
||||||
|
import {
|
||||||
|
CURRENT_TURN_TAG,
|
||||||
|
resolveWorldInfoMarker,
|
||||||
|
worldInfoPackFromSegments,
|
||||||
|
} from "../src/preset/world-info-pack.js";
|
||||||
|
import {
|
||||||
|
CURRENT_TURN,
|
||||||
|
WORLD_BOOK_AFTER,
|
||||||
|
WORLD_BOOK_BEFORE,
|
||||||
|
} from "../src/preset/markers.js";
|
||||||
|
import type { ContextSegmentDef } from "../src/skills/context-segments.js";
|
||||||
|
|
||||||
|
describe("worldInfoPackFromSegments", () => {
|
||||||
|
it("splits lore / history / after-history / turn by the two anchors", () => {
|
||||||
|
const bb = new Blackboard();
|
||||||
|
const segments: ContextSegmentDef[] = [
|
||||||
|
{
|
||||||
|
id: "stage",
|
||||||
|
tier: "static",
|
||||||
|
tags: [],
|
||||||
|
inline: "古镇夜雨",
|
||||||
|
label: "## 舞台骨架",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "hist",
|
||||||
|
tier: "dynamic",
|
||||||
|
tags: [DIALOGUE_HISTORY_TAG],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "vars",
|
||||||
|
tier: "dynamic",
|
||||||
|
tags: ["变量.当前"],
|
||||||
|
label: "## 变量",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "turn",
|
||||||
|
tier: "dynamic",
|
||||||
|
tags: [CURRENT_TURN_TAG],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "note",
|
||||||
|
tier: "dynamic",
|
||||||
|
tags: [],
|
||||||
|
inline: "尾注",
|
||||||
|
label: "## 本轮后",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const pack = worldInfoPackFromSegments({
|
||||||
|
segments,
|
||||||
|
inputs: {
|
||||||
|
[DIALOGUE_HISTORY_TAG]: "用户:你好",
|
||||||
|
"变量.当前": "HP 3",
|
||||||
|
[CURRENT_TURN_TAG]: "我推门进去",
|
||||||
|
},
|
||||||
|
blackboard: bb,
|
||||||
|
});
|
||||||
|
expect(pack.worldBookBefore.map((e) => e.content)).toEqual(["古镇夜雨"]);
|
||||||
|
expect(pack.history).toContain("你好");
|
||||||
|
expect(pack.worldBookAfter.map((e) => e.content)).toEqual(["HP 3"]);
|
||||||
|
expect(pack.turn).toBe("我推门进去");
|
||||||
|
expect(pack.postTurn.map((e) => e.content)).toEqual(["尾注"]);
|
||||||
|
expect(resolveWorldInfoMarker(pack, WORLD_BOOK_BEFORE)).toContain("古镇夜雨");
|
||||||
|
expect(resolveWorldInfoMarker(pack, WORLD_BOOK_AFTER)).toContain("HP 3");
|
||||||
|
expect(resolveWorldInfoMarker(pack, CURRENT_TURN)).toBe("我推门进去");
|
||||||
|
expect(resolveWorldInfoMarker(pack, "charDescription")).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
+503
-62
@@ -12,6 +12,7 @@ import {
|
|||||||
parsePresentDoc,
|
parsePresentDoc,
|
||||||
presentFromPlain,
|
presentFromPlain,
|
||||||
renderPresentShellHtml,
|
renderPresentShellHtml,
|
||||||
|
stripPresentSourceFences,
|
||||||
} from "./present-shells.js";
|
} from "./present-shells.js";
|
||||||
|
|
||||||
const HIDE_KINDS = new Set(["worker_stub"]);
|
const HIDE_KINDS = new Set(["worker_stub"]);
|
||||||
@@ -57,6 +58,10 @@ function esc(s) {
|
|||||||
.replace(/>/g, ">");
|
.replace(/>/g, ">");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function escAttr(s) {
|
||||||
|
return esc(s).replace(/"/g, """);
|
||||||
|
}
|
||||||
|
|
||||||
function renderThinkingBlock(thinking, { open = false } = {}) {
|
function renderThinkingBlock(thinking, { open = false } = {}) {
|
||||||
const text = (thinking ?? "").trim();
|
const text = (thinking ?? "").trim();
|
||||||
if (!text) return "";
|
if (!text) return "";
|
||||||
@@ -66,23 +71,39 @@ function renderThinkingBlock(thinking, { open = false } = {}) {
|
|||||||
</details>`;
|
</details>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isPlayView(view) {
|
||||||
|
return Boolean(view?.playLayerActive || view?.lifecycleStage === "play");
|
||||||
|
}
|
||||||
|
|
||||||
function renderLiveStreamBody(live, view) {
|
function renderLiveStreamBody(live, view) {
|
||||||
const thinking = (live?.thinking || view?.agentThinking || "").trim();
|
const thinking = (live?.thinking || view?.agentThinking || "").trim();
|
||||||
const output = (live?.output ?? "").trim();
|
const output = (live?.output ?? "").trim();
|
||||||
const showOutput =
|
|
||||||
Boolean(output) && (view?.lifecycleStage ?? document.body.dataset.lifecycle) === "play";
|
|
||||||
const parts = [];
|
const parts = [];
|
||||||
if (thinking) {
|
if (thinking) {
|
||||||
parts.push(
|
parts.push(
|
||||||
`<section class="msg-live-section"><header>思考</header><pre class="msg-live-pre">${esc(thinking)}</pre></section>`,
|
`<section class="msg-live-section"><header>思考</header><pre class="msg-live-pre">${esc(thinking)}</pre></section>`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (showOutput) {
|
if (output) {
|
||||||
parts.push(
|
parts.push(
|
||||||
`<section class="msg-live-section"><header>输出</header><pre class="msg-live-pre">${esc(output)}</pre></section>`,
|
`<section class="msg-live-section"><header>输出</header><pre class="msg-live-pre">${esc(output)}</pre></section>`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return parts.length ? parts.join("") : "…";
|
return parts.length
|
||||||
|
? parts.join("")
|
||||||
|
: `<p class="msg-live-empty">正在生成,尚未收到内容</p>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderRetryRunButton() {
|
||||||
|
return `<button type="button" class="msg-action msg-action-primary" data-act="retry_run" title="停止当前生成并重试">↻</button>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function wireRetryRun(root, handlers) {
|
||||||
|
root?.querySelector("[data-act=retry_run]")?.addEventListener("click", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
handlers.onRetryRun?.();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function fmtTime(iso) {
|
function fmtTime(iso) {
|
||||||
@@ -141,13 +162,19 @@ function hideMsgMenu() {
|
|||||||
const menu = document.getElementById("msg-action-menu");
|
const menu = document.getElementById("msg-action-menu");
|
||||||
if (menu) menu.hidden = true;
|
if (menu) menu.hidden = true;
|
||||||
msgMenuState.messageId = null;
|
msgMenuState.messageId = null;
|
||||||
|
msgMenuState.contextMessageId = null;
|
||||||
msgMenuState.rollbackMessageId = null;
|
msgMenuState.rollbackMessageId = null;
|
||||||
|
msgMenuState.rollbackMode = "keep";
|
||||||
|
msgMenuState.restoreDraft = "";
|
||||||
msgMenuState.card = null;
|
msgMenuState.card = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const msgMenuState = {
|
const msgMenuState = {
|
||||||
messageId: null,
|
messageId: null,
|
||||||
|
contextMessageId: null,
|
||||||
rollbackMessageId: null,
|
rollbackMessageId: null,
|
||||||
|
rollbackMode: "keep",
|
||||||
|
restoreDraft: "",
|
||||||
card: null,
|
card: null,
|
||||||
handlers: null,
|
handlers: null,
|
||||||
};
|
};
|
||||||
@@ -155,25 +182,38 @@ const msgMenuState = {
|
|||||||
function showMsgMenu(card, x, y) {
|
function showMsgMenu(card, x, y) {
|
||||||
const menu = document.getElementById("msg-action-menu");
|
const menu = document.getElementById("msg-action-menu");
|
||||||
if (!menu || !card) return;
|
if (!menu || !card) return;
|
||||||
const isReview = card.dataset.review === "1";
|
const pack = card.closest(".coord-pack");
|
||||||
const isReadOnly = card.dataset.readOnly === "1";
|
const rollbackHost = pack || card;
|
||||||
const canEdit = !isReview && card.dataset.canEdit === "1";
|
// 上下文按单次请求(这条消息)区分,不能回落到整包里的另一条
|
||||||
const canDelete = !isReview && !isReadOnly && Boolean(card.dataset.messageId);
|
const contextTarget = card.dataset.hasContext === "1" ? card : null;
|
||||||
const canRollback = !isReview && !isReadOnly && Boolean(card.dataset.rollbackMessageId);
|
const isReview = card.dataset.review === "1" || pack?.dataset.review === "1";
|
||||||
const hasContext = card.dataset.hasContext === "1";
|
const isReadOnly = card.dataset.readOnly === "1" || pack?.dataset.readOnly === "1";
|
||||||
|
const canEdit = !isReview && !pack && card.dataset.canEdit === "1";
|
||||||
|
const canDelete = !isReview && !isReadOnly && !pack && Boolean(card.dataset.messageId);
|
||||||
|
const rollbackMessageId = rollbackHost.dataset.rollbackMessageId ?? null;
|
||||||
|
const rollbackMode = rollbackHost.dataset.rollbackMode || "keep";
|
||||||
|
const canRollback = !isReview && !isReadOnly && Boolean(rollbackMessageId);
|
||||||
|
const hasContext = Boolean(contextTarget);
|
||||||
const editBtn = document.getElementById("msg-menu-edit");
|
const editBtn = document.getElementById("msg-menu-edit");
|
||||||
const delBtn = document.getElementById("msg-menu-delete");
|
const delBtn = document.getElementById("msg-menu-delete");
|
||||||
const rollbackBtn = document.getElementById("msg-menu-rollback");
|
const rollbackBtn = document.getElementById("msg-menu-rollback");
|
||||||
const ctxBtn = document.getElementById("msg-menu-context");
|
const ctxBtn = document.getElementById("msg-menu-context");
|
||||||
if (editBtn) editBtn.toggleAttribute("hidden", !canEdit);
|
if (editBtn) editBtn.toggleAttribute("hidden", !canEdit);
|
||||||
if (delBtn) delBtn.toggleAttribute("hidden", !canDelete);
|
if (delBtn) delBtn.toggleAttribute("hidden", !canDelete);
|
||||||
if (rollbackBtn) rollbackBtn.toggleAttribute("hidden", !canRollback);
|
if (rollbackBtn) {
|
||||||
|
rollbackBtn.toggleAttribute("hidden", !canRollback);
|
||||||
|
rollbackBtn.textContent =
|
||||||
|
rollbackMode === "pending-input" ? "回到这一轮输入" : "回退到这里";
|
||||||
|
}
|
||||||
if (ctxBtn) ctxBtn.toggleAttribute("hidden", !hasContext);
|
if (ctxBtn) ctxBtn.toggleAttribute("hidden", !hasContext);
|
||||||
menu.hidden = false;
|
menu.hidden = false;
|
||||||
menu.style.left = `${x}px`;
|
menu.style.left = `${x}px`;
|
||||||
menu.style.top = `${y}px`;
|
menu.style.top = `${y}px`;
|
||||||
msgMenuState.messageId = card.dataset.messageId ?? null;
|
msgMenuState.messageId = card.dataset.messageId ?? null;
|
||||||
msgMenuState.rollbackMessageId = card.dataset.rollbackMessageId ?? null;
|
msgMenuState.contextMessageId = contextTarget?.dataset.messageId ?? card.dataset.messageId ?? null;
|
||||||
|
msgMenuState.rollbackMessageId = rollbackMessageId;
|
||||||
|
msgMenuState.rollbackMode = rollbackMode;
|
||||||
|
msgMenuState.restoreDraft = rollbackHost.dataset.restoreDraft ?? "";
|
||||||
msgMenuState.card = card;
|
msgMenuState.card = card;
|
||||||
const rect = menu.getBoundingClientRect();
|
const rect = menu.getBoundingClientRect();
|
||||||
if (rect.right > window.innerWidth) {
|
if (rect.right > window.innerWidth) {
|
||||||
@@ -193,12 +233,15 @@ function wireMsgActionMenu(handlers) {
|
|||||||
menu.addEventListener("click", async (e) => {
|
menu.addEventListener("click", async (e) => {
|
||||||
const action = e.target.closest("[data-msg-menu-action]")?.getAttribute("data-msg-menu-action");
|
const action = e.target.closest("[data-msg-menu-action]")?.getAttribute("data-msg-menu-action");
|
||||||
const messageId = msgMenuState.messageId;
|
const messageId = msgMenuState.messageId;
|
||||||
|
const contextMessageId = msgMenuState.contextMessageId || messageId;
|
||||||
const rollbackMessageId = msgMenuState.rollbackMessageId;
|
const rollbackMessageId = msgMenuState.rollbackMessageId;
|
||||||
|
const rollbackMode = msgMenuState.rollbackMode;
|
||||||
|
const restoreDraft = msgMenuState.restoreDraft;
|
||||||
const card = msgMenuState.card;
|
const card = msgMenuState.card;
|
||||||
hideMsgMenu();
|
hideMsgMenu();
|
||||||
if (!messageId || !action) return;
|
if (!messageId || !action) return;
|
||||||
const bodyEl = card?.querySelector(".msg-body:not(.msg-body-editing), .coord-text");
|
const bodyEl = card?.querySelector(".msg-body:not(.msg-body-editing), .coord-text");
|
||||||
const body = bodyEl?.textContent ?? card?.dataset.originalText ?? "";
|
const body = card?.dataset.originalText || bodyEl?.textContent || "";
|
||||||
|
|
||||||
if (action === "copy") {
|
if (action === "copy") {
|
||||||
try {
|
try {
|
||||||
@@ -209,7 +252,7 @@ function wireMsgActionMenu(handlers) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (action === "view-context") {
|
if (action === "view-context") {
|
||||||
msgMenuState.handlers?.onViewContext?.(messageId);
|
msgMenuState.handlers?.onViewContext?.(contextMessageId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (action === "edit") {
|
if (action === "edit") {
|
||||||
@@ -218,8 +261,20 @@ function wireMsgActionMenu(handlers) {
|
|||||||
}
|
}
|
||||||
if (action === "rollback") {
|
if (action === "rollback") {
|
||||||
if (!rollbackMessageId) return;
|
if (!rollbackMessageId) return;
|
||||||
if (!confirm("回退到这条消息?之后的对话和创作状态将被移除。")) return;
|
const pendingInput = rollbackMode === "pending-input";
|
||||||
|
const confirmed = pendingInput
|
||||||
|
? confirm(
|
||||||
|
"回到这一轮的待输入?这一轮及之后的对话和创作状态将被移除,原文会填回底栏。",
|
||||||
|
)
|
||||||
|
: confirm("回退到这条消息?之后的对话和创作状态将被移除。");
|
||||||
|
if (!confirmed) return;
|
||||||
|
if (pendingInput) {
|
||||||
|
const restore = msgMenuState.handlers?.onRollbackToInput;
|
||||||
|
if (typeof restore === "function") restore(rollbackMessageId, restoreDraft);
|
||||||
|
else msgMenuState.handlers?.onDeleteMessage?.(rollbackMessageId);
|
||||||
|
} else {
|
||||||
msgMenuState.handlers?.onDeleteMessage?.(rollbackMessageId);
|
msgMenuState.handlers?.onDeleteMessage?.(rollbackMessageId);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (action === "delete") {
|
if (action === "delete") {
|
||||||
@@ -234,7 +289,7 @@ function wireMessageContextMenu(feed, handlers) {
|
|||||||
feed.dataset.contextWired = "1";
|
feed.dataset.contextWired = "1";
|
||||||
wireMsgActionMenu(handlers);
|
wireMsgActionMenu(handlers);
|
||||||
feed.addEventListener("contextmenu", (e) => {
|
feed.addEventListener("contextmenu", (e) => {
|
||||||
const bubble = e.target.closest(".msg-bubble, .workspace-review, .coord-line");
|
const bubble = e.target.closest(".msg-bubble, .workspace-review, .coord-line, .coord-pack");
|
||||||
const card = e.target.closest("[data-message-id]");
|
const card = e.target.closest("[data-message-id]");
|
||||||
if (!bubble || !card || card.classList.contains("msg-pending")) return;
|
if (!bubble || !card || card.classList.contains("msg-pending")) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -423,22 +478,40 @@ function formatQuestionsHtml(body) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** play 期助手终稿(含已验收的用户展示)用呈现壳渲染 */
|
/** play 期助手终稿(含已验收的用户展示)用呈现壳渲染 */
|
||||||
|
function isPlayHousekeepingMessage(msg) {
|
||||||
|
const title = String(msg.title ?? "");
|
||||||
|
const text = String(msg.text ?? msg.body ?? "").trim();
|
||||||
|
if (title === "上下文已压缩" || title === "阶段机") return true;
|
||||||
|
if (/^\[上下文已压缩\]/.test(text)) return true;
|
||||||
|
if (/^\[裁决合并\]/.test(text)) return true;
|
||||||
|
if (/^\[旁观维护\]/.test(text)) return true;
|
||||||
|
if (/归档\s*\d+\s*个过程 tag/.test(text)) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
function shouldRenderPlayPresent(msg, view) {
|
function shouldRenderPlayPresent(msg, view) {
|
||||||
if (view?.lifecycleStage !== "play") return false;
|
if (!isPlayView(view)) return false;
|
||||||
if (msg.role === "user") return false;
|
if (msg.role === "user") return false;
|
||||||
const kind = msg.kind ?? "";
|
const kind = msg.kind ?? "";
|
||||||
if (kind === "worker_questions" || kind === "orchestrator_thinking") return false;
|
if (kind === "worker_questions" || kind === "orchestrator_thinking") return false;
|
||||||
if (kind === "worker_output") return false; // 已走 formatArtifactBodyHtml
|
if (isPlayHousekeepingMessage(msg)) return false;
|
||||||
const actor = String(msg.actor ?? "");
|
const actor = String(msg.actor ?? "");
|
||||||
if (actor === "narrator" || /用户展示|开场白/.test(String(msg.title ?? ""))) {
|
const title = String(msg.title ?? "");
|
||||||
return true;
|
return actor === "narrator" || actor === "round-present" || /用户展示|开场白/.test(title);
|
||||||
}
|
}
|
||||||
// 助手可见终稿:无特殊 kind 的长文
|
|
||||||
return kind === "system_info" || kind === "worker_stub" || !kind;
|
function isPlayFinalReply(msg, view) {
|
||||||
|
if (!isPlayView(view) || msg.role === "user") return false;
|
||||||
|
return shouldRenderPlayPresent(msg, view);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPlayProcessMessage(msg, view) {
|
||||||
|
if (!isPlayView(view) || msg.role === "user") return false;
|
||||||
|
return !isPlayFinalReply(msg, view);
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatPlayPresentHtml(body, view) {
|
function formatPlayPresentHtml(body, view) {
|
||||||
const trimmed = (body || "").trim();
|
const trimmed = stripPresentSourceFences((body || "").trim());
|
||||||
if (!trimmed) return `<p class="empty-sm">(无正文)</p>`;
|
if (!trimmed) return `<p class="empty-sm">(无正文)</p>`;
|
||||||
const tweaks = view?.presentationTweaks;
|
const tweaks = view?.presentationTweaks;
|
||||||
const fallbackShell =
|
const fallbackShell =
|
||||||
@@ -562,7 +635,7 @@ function wireRailChrome() {
|
|||||||
|
|
||||||
function hasStartedCreation(view) {
|
function hasStartedCreation(view) {
|
||||||
if (!view?.id) return false;
|
if (!view?.id) return false;
|
||||||
if (view.lifecycleStage === "play") return true;
|
if (isPlayView(view)) return true;
|
||||||
if (hasUserMessages(view)) return true;
|
if (hasUserMessages(view)) return true;
|
||||||
const reason = view.waitingReason?.kind;
|
const reason = view.waitingReason?.kind;
|
||||||
if (reason && reason !== "intake") return true;
|
if (reason && reason !== "intake") return true;
|
||||||
@@ -626,6 +699,7 @@ function shouldShowInFeed(msg, view) {
|
|||||||
if (HIDE_KINDS.has(kind)) return false;
|
if (HIDE_KINDS.has(kind)) return false;
|
||||||
if (FEED_HIDDEN_KINDS.has(kind)) return false;
|
if (FEED_HIDDEN_KINDS.has(kind)) return false;
|
||||||
if (msg.compressed) return false;
|
if (msg.compressed) return false;
|
||||||
|
if (isPlayView(view) && isPlayHousekeepingMessage(msg)) return false;
|
||||||
if (isSkillSelectionMessage(msg)) return false;
|
if (isSkillSelectionMessage(msg)) return false;
|
||||||
if (isReviewSidecarQuestionStub(msg)) return false;
|
if (isReviewSidecarQuestionStub(msg)) return false;
|
||||||
if (
|
if (
|
||||||
@@ -635,11 +709,11 @@ function shouldShowInFeed(msg, view) {
|
|||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (kind === "worker_output" && view.waitingReason?.kind === "review_artifact") {
|
if (kind === "worker_output" && view.waitingReason?.kind === "review_artifact" && !isPlayView(view)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// 创作:编排器思考进「调度」,不占主区;游玩保持原样
|
// 创作:编排器思考进「调度」,不占主区;游玩保持原样(默认折叠)
|
||||||
if (kind === "orchestrator_thinking" && view.lifecycleStage !== "play") {
|
if (kind === "orchestrator_thinking" && !isPlayView(view)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (kind === "orchestrator_prompt") return false;
|
if (kind === "orchestrator_prompt") return false;
|
||||||
@@ -661,10 +735,10 @@ function maybeSyncRail(view) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function renderLifecycle(view) {
|
export function renderLifecycle(view) {
|
||||||
const toggle = document.getElementById("lifecycle-toggle");
|
|
||||||
if (!toggle) return;
|
|
||||||
const stage = view.lifecycleStage ?? "design";
|
const stage = view.lifecycleStage ?? "design";
|
||||||
document.body.dataset.lifecycle = stage;
|
document.body.dataset.lifecycle = stage;
|
||||||
|
const toggle = document.getElementById("lifecycle-toggle");
|
||||||
|
if (!toggle) return;
|
||||||
toggle.querySelectorAll("[data-stage]").forEach((btn) => {
|
toggle.querySelectorAll("[data-stage]").forEach((btn) => {
|
||||||
const s = btn.getAttribute("data-stage");
|
const s = btn.getAttribute("data-stage");
|
||||||
btn.classList.toggle("active", s === stage);
|
btn.classList.toggle("active", s === stage);
|
||||||
@@ -1541,7 +1615,7 @@ function renderKnownArtifactHtml(doc, opts = {}) {
|
|||||||
if (schema === "context-fragment.v1" || isContextFragmentLike(doc)) {
|
if (schema === "context-fragment.v1" || isContextFragmentLike(doc)) {
|
||||||
return renderContextFragmentHtml(doc, opts);
|
return renderContextFragmentHtml(doc, opts);
|
||||||
}
|
}
|
||||||
if (schema === "context-order.v1" || Array.isArray(doc.slots)) {
|
if (schema === "context-order.v1" || Array.isArray(doc.slots) || Array.isArray(doc.agents)) {
|
||||||
const orderHtml = renderContextOrderHtml(doc);
|
const orderHtml = renderContextOrderHtml(doc);
|
||||||
if (orderHtml) return orderHtml;
|
if (orderHtml) return orderHtml;
|
||||||
}
|
}
|
||||||
@@ -1666,6 +1740,105 @@ function renderContextFragmentHtml(doc, opts = {}) {
|
|||||||
return `<div class="artifact-friendly artifact-context-fragment">${parts.join("")}</div>`;
|
return `<div class="artifact-friendly artifact-context-fragment">${parts.join("")}</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 开场白终节点:1~多条候选用呈现壳展示,选定后由程序落库 */
|
||||||
|
function collectOpeningCandidateTexts(body) {
|
||||||
|
const texts = [];
|
||||||
|
const push = (value) => {
|
||||||
|
const text = String(value ?? "").trim();
|
||||||
|
if (text && !texts.includes(text)) texts.push(text);
|
||||||
|
};
|
||||||
|
push(body.开场白全文);
|
||||||
|
if (Array.isArray(body.开场白候选)) {
|
||||||
|
for (const item of body.开场白候选) {
|
||||||
|
if (typeof item === "string") push(item);
|
||||||
|
else if (item && typeof item === "object") {
|
||||||
|
push(item.全文 || item.text || item.开场白 || item.内容);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return texts;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderOpeningSetupBodyHtml(body) {
|
||||||
|
if (!body || typeof body !== "object" || Array.isArray(body)) return "";
|
||||||
|
const candidates = collectOpeningCandidateTexts(body);
|
||||||
|
if (!candidates.length) return "";
|
||||||
|
const total = candidates.length;
|
||||||
|
const slides = candidates
|
||||||
|
.map((text, i) => {
|
||||||
|
const hidden = i === 0 ? "" : " hidden";
|
||||||
|
const selected = i === 0 ? " is-selected" : "";
|
||||||
|
return `<article class="opening-candidate${selected}" data-opening-index="${i}"${hidden}>${renderPresentShellHtml(
|
||||||
|
presentFromPlain(text, "prose"),
|
||||||
|
esc,
|
||||||
|
)}</article>`;
|
||||||
|
})
|
||||||
|
.join("");
|
||||||
|
const nav =
|
||||||
|
total > 1
|
||||||
|
? `<div class="opening-picker-nav">
|
||||||
|
<button type="button" class="opening-picker-btn" data-opening-delta="-1">上一版</button>
|
||||||
|
<span class="opening-picker-pos" data-opening-pos>1 / ${total}</span>
|
||||||
|
<button type="button" class="opening-picker-btn" data-opening-delta="1">下一版</button>
|
||||||
|
</div>`
|
||||||
|
: "";
|
||||||
|
const vars = Array.isArray(body.开场变量) ? body.开场变量 : [];
|
||||||
|
const varsHtml = vars.length
|
||||||
|
? `<section class="artifact-block"><h4>开场变量</h4><ul class="artifact-list">${vars
|
||||||
|
.map((row) => {
|
||||||
|
const name = row?.名 || row?.name || row?.key || "";
|
||||||
|
const val = row?.值 !== undefined ? row.值 : row?.value;
|
||||||
|
const note = row?.依据 || row?.note || "";
|
||||||
|
const noteHtml = note
|
||||||
|
? ` <span class="muted">(${esc(String(note))})</span>`
|
||||||
|
: "";
|
||||||
|
return `<li><strong>${esc(String(name))}</strong>:${esc(String(val ?? ""))}${noteHtml}</li>`;
|
||||||
|
})
|
||||||
|
.join("")}</ul></section>`
|
||||||
|
: "";
|
||||||
|
const space = body.用户可行动空间
|
||||||
|
? `<section class="artifact-block"><h4>可行动空间</h4>${renderProseHtml(
|
||||||
|
body.用户可行动空间,
|
||||||
|
)}</section>`
|
||||||
|
: "";
|
||||||
|
return `<div class="opening-setup-body">
|
||||||
|
<div class="opening-picker" data-opening-picker="1" data-selected="0" data-total="${total}">
|
||||||
|
${slides}
|
||||||
|
${nav}
|
||||||
|
</div>
|
||||||
|
${varsHtml}
|
||||||
|
${space}
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function wireOpeningPicker() {
|
||||||
|
document.querySelectorAll("[data-opening-picker]").forEach((root) => {
|
||||||
|
if (root.dataset.wired === "1") return;
|
||||||
|
root.dataset.wired = "1";
|
||||||
|
const total = Math.max(1, Number(root.getAttribute("data-total")) || 1);
|
||||||
|
const show = (index) => {
|
||||||
|
const next = ((index % total) + total) % total;
|
||||||
|
root.setAttribute("data-selected", String(next));
|
||||||
|
root.querySelectorAll("[data-opening-index]").forEach((el) => {
|
||||||
|
const i = Number(el.getAttribute("data-opening-index"));
|
||||||
|
const on = i === next;
|
||||||
|
el.toggleAttribute("hidden", !on);
|
||||||
|
el.classList.toggle("is-selected", on);
|
||||||
|
});
|
||||||
|
const pos = root.querySelector("[data-opening-pos]");
|
||||||
|
if (pos) pos.textContent = `${next + 1} / ${total}`;
|
||||||
|
};
|
||||||
|
root.addEventListener("click", (e) => {
|
||||||
|
const btn = e.target.closest("[data-opening-delta]");
|
||||||
|
if (!btn || !root.contains(btn)) return;
|
||||||
|
const delta = Number(btn.getAttribute("data-opening-delta"));
|
||||||
|
if (delta !== 1 && delta !== -1) return;
|
||||||
|
const cur = Number(root.getAttribute("data-selected") || 0);
|
||||||
|
show(cur + delta);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/** 按技能分流正文视图;无专用模板则返回 ""(调用方走通用结构化) */
|
/** 按技能分流正文视图;无专用模板则返回 ""(调用方走通用结构化) */
|
||||||
function renderSpecialtyBodyHtml(body, skill) {
|
function renderSpecialtyBodyHtml(body, skill) {
|
||||||
if (!body || typeof body !== "object" || Array.isArray(body)) return "";
|
if (!body || typeof body !== "object" || Array.isArray(body)) return "";
|
||||||
@@ -1703,6 +1876,22 @@ function renderSpecialtyBodyHtml(body, skill) {
|
|||||||
const html = renderNarrativeBodyHtml(body);
|
const html = renderNarrativeBodyHtml(body);
|
||||||
if (html) return html;
|
if (html) return html;
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
body.呈现壳 != null ||
|
||||||
|
body.可见块 != null ||
|
||||||
|
(body.示例 != null && (skillName.includes("正文组成") || skillName.includes("回复格式")))
|
||||||
|
) {
|
||||||
|
const html = renderReplyFormatBodyHtml(body);
|
||||||
|
if (html) return html;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
body.开场白全文 != null ||
|
||||||
|
body.开场白候选 != null ||
|
||||||
|
skillName.includes("开场白")
|
||||||
|
) {
|
||||||
|
const html = renderOpeningSetupBodyHtml(body);
|
||||||
|
if (html) return html;
|
||||||
|
}
|
||||||
|
|
||||||
const looksAesthetics =
|
const looksAesthetics =
|
||||||
body.设定逻辑 != null ||
|
body.设定逻辑 != null ||
|
||||||
@@ -1782,6 +1971,101 @@ function renderSpecialtyBodyHtml(body, skill) {
|
|||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 正文组成 · 壳适配 + 示例灌数(present 壳预览,验收美化) */
|
||||||
|
function renderReplyFormatBodyHtml(body) {
|
||||||
|
if (!body || typeof body !== "object" || Array.isArray(body)) return "";
|
||||||
|
const shellDoc = body.呈现壳 && typeof body.呈现壳 === "object" ? body.呈现壳 : {};
|
||||||
|
const shellId =
|
||||||
|
typeof shellDoc.shell_id === "string" && PRESENT_SHELL_IDS.includes(shellDoc.shell_id)
|
||||||
|
? shellDoc.shell_id
|
||||||
|
: typeof shellDoc.shell_id === "string"
|
||||||
|
? shellDoc.shell_id
|
||||||
|
: "";
|
||||||
|
const tweaks =
|
||||||
|
shellDoc.微调 && typeof shellDoc.微调 === "object" && !Array.isArray(shellDoc.微调)
|
||||||
|
? shellDoc.微调
|
||||||
|
: {};
|
||||||
|
const example = body.示例 && typeof body.示例 === "object" ? body.示例 : null;
|
||||||
|
const packetRaw = example?.灌数;
|
||||||
|
const presentView =
|
||||||
|
packetRaw && typeof packetRaw === "object"
|
||||||
|
? parsePresentDoc(
|
||||||
|
{
|
||||||
|
...packetRaw,
|
||||||
|
shell: packetRaw.shell || packetRaw.shell_id || shellId || "prose",
|
||||||
|
},
|
||||||
|
shellId || "prose",
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const parts = [];
|
||||||
|
parts.push(`<div class="af-split-banner" role="note">
|
||||||
|
<span><b>本步看壳与美化</b></span>
|
||||||
|
<span>示例非正史 · 文笔不重要</span>
|
||||||
|
</div>`);
|
||||||
|
|
||||||
|
const metaBits = [];
|
||||||
|
if (shellId) {
|
||||||
|
metaBits.push(`<span class="ws-badge ws-badge-continue">壳 ${esc(shellId)}</span>`);
|
||||||
|
}
|
||||||
|
if (shellDoc.为何选它) {
|
||||||
|
metaBits.push(`<span class="ws-muted">${esc(String(shellDoc.为何选它))}</span>`);
|
||||||
|
}
|
||||||
|
if (body.版式隐喻) {
|
||||||
|
metaBits.push(`<span class="ws-muted">隐喻:${esc(String(body.版式隐喻))}</span>`);
|
||||||
|
}
|
||||||
|
if (metaBits.length) {
|
||||||
|
parts.push(`<div class="artifact-chip-row reply-format-shell-meta">${metaBits.join("")}</div>`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (presentView) {
|
||||||
|
const nature = example?.性质 ? esc(String(example.性质)) : "验收预览|非正史";
|
||||||
|
const explain = example?.用来说明 ? `<p class="ws-muted">${esc(String(example.用来说明))}</p>` : "";
|
||||||
|
parts.push(`<section class="af-panel af-panel-wide reply-format-preview">
|
||||||
|
<h4>版式预览 <span class="ws-badge">${nature}</span></h4>
|
||||||
|
${explain}
|
||||||
|
${renderPresentShellHtml(presentView.packet, esc, { tweaks })}
|
||||||
|
</section>`);
|
||||||
|
} else {
|
||||||
|
parts.push(
|
||||||
|
`<section class="af-panel"><p class="ws-muted">(暂无示例灌数;定稿时应给 present.v1 预览)</p></section>`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const contractKeys = [
|
||||||
|
"依据的体验与呈现",
|
||||||
|
"呈现壳",
|
||||||
|
"版式隐喻",
|
||||||
|
"可见块",
|
||||||
|
"监控栏锚点",
|
||||||
|
"隐藏段",
|
||||||
|
"前端拆分与美化",
|
||||||
|
"拼装与终稿",
|
||||||
|
];
|
||||||
|
const used = new Set(["示例"]);
|
||||||
|
for (const key of contractKeys) {
|
||||||
|
if (body[key] == null || body[key] === "") continue;
|
||||||
|
used.add(key);
|
||||||
|
parts.push(
|
||||||
|
`<details class="skill-example reply-format-contract"><summary>${esc(key)}</summary>${renderStructuredValueHtml(
|
||||||
|
body[key],
|
||||||
|
0,
|
||||||
|
)}</details>`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for (const [k, v] of Object.entries(body)) {
|
||||||
|
if (used.has(k) || v == null || v === "") continue;
|
||||||
|
parts.push(
|
||||||
|
`<details class="skill-example reply-format-contract"><summary>${esc(k)}</summary>${renderStructuredValueHtml(
|
||||||
|
v,
|
||||||
|
0,
|
||||||
|
)}</details>`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return `<div class="artifact-friendly artifact-reply-format">${parts.join("")}</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
/** 叙事指南 · 一份全文结构化卡(不强调双投影裁剪) */
|
/** 叙事指南 · 一份全文结构化卡(不强调双投影裁剪) */
|
||||||
function renderNarrativeBodyHtml(body) {
|
function renderNarrativeBodyHtml(body) {
|
||||||
const order = [
|
const order = [
|
||||||
@@ -2528,7 +2812,10 @@ function renderSelfScoreHtml(自评) {
|
|||||||
|
|
||||||
function renderProbeHtml(追问) {
|
function renderProbeHtml(追问) {
|
||||||
if (!追问 || typeof 追问 !== "object") return "";
|
if (!追问 || typeof 追问 !== "object") return "";
|
||||||
const qs = Array.isArray(追问.题目) ? 追问.题目 : [];
|
const qs = Array.isArray(追问.题目)
|
||||||
|
? 追问.题目.filter((q) => q && typeof q === "object" && (q.问 || q.prompt))
|
||||||
|
: [];
|
||||||
|
if (!qs.length) return "";
|
||||||
const cards = [];
|
const cards = [];
|
||||||
if (追问.导语) {
|
if (追问.导语) {
|
||||||
cards.push(`<p class="artifact-probe-lead">${esc(String(追问.导语))}</p>`);
|
cards.push(`<p class="artifact-probe-lead">${esc(String(追问.导语))}</p>`);
|
||||||
@@ -2555,7 +2842,13 @@ function renderProbeHtml(追问) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderContextOrderHtml(doc) {
|
function renderContextOrderHtml(doc) {
|
||||||
if (!Array.isArray(doc.slots) || !doc.slots.length) return null;
|
const slots =
|
||||||
|
Array.isArray(doc.slots) && doc.slots.length
|
||||||
|
? doc.slots
|
||||||
|
: Array.isArray(doc.agents)
|
||||||
|
? doc.agents.filter((a) => a && a.enabled !== false)
|
||||||
|
: [];
|
||||||
|
if (!slots.length) return null;
|
||||||
const parts = [];
|
const parts = [];
|
||||||
if (doc.brief) {
|
if (doc.brief) {
|
||||||
parts.push(
|
parts.push(
|
||||||
@@ -2564,7 +2857,7 @@ function renderContextOrderHtml(doc) {
|
|||||||
}
|
}
|
||||||
for (const slot of doc.slots) {
|
for (const slot of doc.slots) {
|
||||||
if (!slot || typeof slot !== "object") continue;
|
if (!slot || typeof slot !== "object") continue;
|
||||||
const title = slot.label || slot.ref || "槽";
|
const title = slot.label || slot.ref || slot.id || "槽";
|
||||||
const inserts = Array.isArray(slot.inserts) ? slot.inserts : [];
|
const inserts = Array.isArray(slot.inserts) ? slot.inserts : [];
|
||||||
if (!inserts.length && Array.isArray(slot.order)) {
|
if (!inserts.length && Array.isArray(slot.order)) {
|
||||||
// 扁平序:直接是条目列表
|
// 扁平序:直接是条目列表
|
||||||
@@ -2919,7 +3212,7 @@ function renderCreationFlowView(flowView) {
|
|||||||
return `<div class="review-parse-error" role="alert">
|
return `<div class="review-parse-error" role="alert">
|
||||||
<div class="review-parse-error-title">流程无法解析</div>
|
<div class="review-parse-error-title">流程无法解析</div>
|
||||||
<p>${esc(flowView.parseError)}</p>
|
<p>${esc(flowView.parseError)}</p>
|
||||||
<p class="review-parse-error-hint">需要 JSON:steps 数组,每步含 name(与可选 id)、depends_on、可选 params;可含 status=open|closed。</p>
|
<p class="review-parse-error-hint">需要 JSON:steps 数组,每步含 name(与可选 id)、depends_on、可选 params;可含 status=open|closed。回头修改用 mode=revise 与 revises。</p>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
if (!flowView.steps?.length) return "";
|
if (!flowView.steps?.length) return "";
|
||||||
@@ -2943,7 +3236,9 @@ function renderCreationFlowView(flowView) {
|
|||||||
? s.depends_on.map((d) => esc(d)).join("、")
|
? s.depends_on.map((d) => esc(d)).join("、")
|
||||||
: "无";
|
: "无";
|
||||||
const occ =
|
const occ =
|
||||||
s.occurrence && s.occurrence > 1
|
s.mode === "revise"
|
||||||
|
? `<span class="flow-occ flow-occ-revise">回头修改</span>`
|
||||||
|
: s.occurrence && s.occurrence > 1
|
||||||
? `<span class="flow-occ">第 ${esc(String(s.occurrence))} 次</span>`
|
? `<span class="flow-occ">第 ${esc(String(s.occurrence))} 次</span>`
|
||||||
: s.repeatable
|
: s.repeatable
|
||||||
? `<span class="flow-occ">可反复</span>`
|
? `<span class="flow-occ">可反复</span>`
|
||||||
@@ -3068,12 +3363,16 @@ function renderReviewWorkspace(review, opts = {}) {
|
|||||||
asPresentFallback:
|
asPresentFallback:
|
||||||
review.workerId === "narrator" ||
|
review.workerId === "narrator" ||
|
||||||
review.workerId === "opening-generator" ||
|
review.workerId === "opening-generator" ||
|
||||||
|
(Array.isArray(review.outputTags) &&
|
||||||
|
review.outputTags.includes("设计.开场白与开场变量")) ||
|
||||||
/用户展示|开场白/.test(review.body || ""),
|
/用户展示|开场白/.test(review.body || ""),
|
||||||
});
|
});
|
||||||
const contextId = review.sourceMessageId || review.id || "";
|
const contextId = review.sourceMessageId || review.id || "";
|
||||||
const hasContext = review.contextTrace ? "1" : "0";
|
const hasContext = review.contextTrace ? "1" : "0";
|
||||||
const label = displayWorkerLabel(review.workerId) || "产物";
|
const label = displayWorkerLabel(review.workerId) || "产物";
|
||||||
const copy = reviewComposerCopy(review.workerId);
|
const copy = reviewComposerCopy(review.workerId, {
|
||||||
|
outputTags: review.outputTags,
|
||||||
|
});
|
||||||
const sourceMsg =
|
const sourceMsg =
|
||||||
opts.sourceMessage ||
|
opts.sourceMessage ||
|
||||||
(review.sourceMessageId
|
(review.sourceMessageId
|
||||||
@@ -3211,25 +3510,65 @@ export function renderAgentPanel(view, loading) {
|
|||||||
timelineEl.scrollTop = timelineEl.scrollHeight;
|
timelineEl.scrollTop = timelineEl.scrollHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function coordOriginalText(msg) {
|
||||||
|
return String(msg?.body ?? msg?.text ?? "").trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 一轮 = 一次用户输入 + 直到下一次输入前的全部输出。 */
|
||||||
|
function groupCoordTurnPacks(messages) {
|
||||||
|
const packs = [];
|
||||||
|
for (const msg of messages) {
|
||||||
|
const isUser = msg.role === "user";
|
||||||
|
const last = packs[packs.length - 1];
|
||||||
|
if (isUser || !last) {
|
||||||
|
packs.push({ user: isUser ? msg : null, outputs: isUser ? [] : [msg] });
|
||||||
|
} else {
|
||||||
|
last.outputs.push(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return packs;
|
||||||
|
}
|
||||||
|
|
||||||
|
function packAnchorMessage(pack) {
|
||||||
|
return pack.user || pack.outputs[0] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function packLastMessage(pack) {
|
||||||
|
return pack.outputs[pack.outputs.length - 1] || pack.user || null;
|
||||||
|
}
|
||||||
|
|
||||||
function renderCoordLine(msg, view, opts = {}) {
|
function renderCoordLine(msg, view, opts = {}) {
|
||||||
const isUser = msg.role === "user";
|
const isUser = msg.role === "user";
|
||||||
const kind = msg.kind ?? (isUser ? "user_input" : "system_info");
|
const kind = msg.kind ?? (isUser ? "user_input" : "system_info");
|
||||||
const who = isUser ? "你" : msgLabel(msg);
|
const who = isUser ? "你" : msgLabel(msg);
|
||||||
|
const full = coordOriginalText(msg) || msgBody(msg, view);
|
||||||
const raw = msgBody(msg, view).replace(/\s+/g, " ").trim();
|
const raw = msgBody(msg, view).replace(/\s+/g, " ").trim();
|
||||||
const text = raw.length > 320 ? `${raw.slice(0, 320)}…` : raw;
|
const text = raw.length > 320 ? `${raw.slice(0, 320)}…` : raw;
|
||||||
const nav = renderVariantNavHtml(msg);
|
const nav = renderVariantNavHtml(msg);
|
||||||
|
const inPack = opts.inPack === true;
|
||||||
const allMessages = view.messages ?? [];
|
const allMessages = view.messages ?? [];
|
||||||
const messageIndex = allMessages.findIndex((item) => item.id === msg.id);
|
const rollbackMessageId = inPack ? null : getRollbackMessageId(allMessages, msg.id);
|
||||||
const rollbackMessageId = messageIndex >= 0 ? allMessages[messageIndex + 1]?.id : null;
|
|
||||||
const rollbackAttr = rollbackMessageId
|
const rollbackAttr = rollbackMessageId
|
||||||
? ` data-rollback-message-id="${esc(rollbackMessageId)}"`
|
? ` data-rollback-message-id="${esc(rollbackMessageId)}"`
|
||||||
: "";
|
: "";
|
||||||
const contextAttr = msg.contextTrace ? ` data-has-context="1"` : "";
|
const contextAttr = msg.contextTrace ? ` data-has-context="1"` : "";
|
||||||
const readOnlyAttr = opts.traceOnly ? ` data-read-only="1"` : "";
|
const readOnlyAttr = opts.traceOnly ? ` data-read-only="1"` : "";
|
||||||
|
const originalAttr = full ? ` data-original-text="${escAttr(full)}"` : "";
|
||||||
|
const menu = inPack
|
||||||
|
? ""
|
||||||
|
: `<button type="button" class="coord-line-menu" data-msg-menu-trigger aria-label="打开这条消息的操作菜单" title="更多操作">⋯</button>`;
|
||||||
const trace = msg.contextTrace;
|
const trace = msg.contextTrace;
|
||||||
|
const variantTotal = Math.max(1, Number(msg.branchTotal) || 1);
|
||||||
|
const variantIndex = Math.min(variantTotal, Math.max(1, (msg.branchIndex ?? 0) + 1));
|
||||||
|
const requestBits = [
|
||||||
|
opts.requestIndex ? `请求 ${opts.requestIndex}` : "",
|
||||||
|
variantTotal > 1 ? `${variantIndex}/${variantTotal} 版` : "",
|
||||||
|
].filter(Boolean);
|
||||||
|
const requestHint = requestBits.length ? requestBits.join(" · ") : "本次请求";
|
||||||
const traceMeta = trace
|
const traceMeta = trace
|
||||||
? `<button type="button" class="coord-context-trigger" data-context-trigger aria-label="查看 ${esc(who)} 的完整请求上下文">
|
? `<button type="button" class="coord-context-trigger" data-context-trigger aria-label="查看 ${esc(who)} 的${esc(requestHint)}上下文">
|
||||||
<span class="coord-context-label">保留上下文</span>
|
<span class="coord-context-label">保留上下文</span>
|
||||||
|
<span class="coord-context-request">${esc(requestHint)}</span>
|
||||||
<span>${trace.messages?.length ?? 0} 段</span>
|
<span>${trace.messages?.length ?? 0} 段</span>
|
||||||
<span>${((trace.charCount ?? 0) / 1024).toFixed(1)} KB</span>
|
<span>${((trace.charCount ?? 0) / 1024).toFixed(1)} KB</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -3240,14 +3579,73 @@ function renderCoordLine(msg, view, opts = {}) {
|
|||||||
</div>`
|
</div>`
|
||||||
: "";
|
: "";
|
||||||
const traceClass = trace ? " coord-line--trace" : "";
|
const traceClass = trace ? " coord-line--trace" : "";
|
||||||
return `<div class="coord-line ${isUser ? "coord-user" : "coord-agent"}${traceClass}" data-message-id="${esc(msg.id)}" data-kind="${esc(kind)}"${rollbackAttr}${contextAttr}${readOnlyAttr}>
|
return `<div class="coord-line ${isUser ? "coord-user" : "coord-agent"}${traceClass}" data-message-id="${esc(msg.id)}" data-kind="${esc(kind)}"${rollbackAttr}${contextAttr}${readOnlyAttr}${originalAttr}>
|
||||||
<button type="button" class="coord-line-menu" data-msg-menu-trigger aria-label="打开这条消息的操作菜单" title="更多操作">⋯</button>
|
${menu}
|
||||||
<div class="coord-who"><span>${esc(who)}</span><span class="coord-kind">${esc(kind)}</span><span class="coord-time">${fmtTime(msg.createdAt)}</span>${nav}</div>
|
<div class="coord-who"><span>${esc(who)}</span><span class="coord-kind">${esc(kind)}</span><span class="coord-time">${fmtTime(msg.createdAt)}</span>${nav}</div>
|
||||||
<div class="coord-text">${esc(text || "(空)")}</div>
|
<div class="coord-text">${esc(text || "(空)")}</div>
|
||||||
${traceMeta}
|
${traceMeta}
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderCoordPack(pack, view, opts = {}) {
|
||||||
|
const anchor = packAnchorMessage(pack);
|
||||||
|
if (!anchor) return "";
|
||||||
|
const last = packLastMessage(pack);
|
||||||
|
const pending = Boolean(pack.user) && pack.outputs.length === 0;
|
||||||
|
const userText = pack.user ? coordOriginalText(pack.user) : "";
|
||||||
|
const allMessages = view.messages ?? [];
|
||||||
|
let rollbackMode = "";
|
||||||
|
let rollbackId = "";
|
||||||
|
if (pack.user) {
|
||||||
|
rollbackMode = "pending-input";
|
||||||
|
rollbackId = pack.user.id;
|
||||||
|
} else if (last) {
|
||||||
|
const nextId = getRollbackMessageId(allMessages, last.id);
|
||||||
|
if (nextId) {
|
||||||
|
rollbackMode = "keep";
|
||||||
|
rollbackId = nextId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const inCount = pack.user ? 1 : 0;
|
||||||
|
const outCount = pack.outputs.length;
|
||||||
|
const meta = pending ? "待输入" : `${inCount} 入 · ${outCount} 出`;
|
||||||
|
const rollbackAttr = rollbackId
|
||||||
|
? ` data-rollback-message-id="${esc(rollbackId)}" data-rollback-mode="${esc(rollbackMode)}"`
|
||||||
|
: "";
|
||||||
|
const restoreAttr =
|
||||||
|
rollbackMode === "pending-input" && userText
|
||||||
|
? ` data-restore-draft="${escAttr(userText)}"`
|
||||||
|
: "";
|
||||||
|
const originalAttr = userText ? ` data-original-text="${escAttr(userText)}"` : "";
|
||||||
|
const readOnlyAttr = opts.traceOnly ? ` data-read-only="1"` : "";
|
||||||
|
const tracedOutputCount = pack.outputs.filter((msg) => msg.contextTrace).length;
|
||||||
|
let requestIndex = 0;
|
||||||
|
const lines = [
|
||||||
|
pack.user
|
||||||
|
? renderCoordLine(pack.user, view, {
|
||||||
|
inPack: true,
|
||||||
|
traceOnly: opts.traceOnly || opts.hiddenIds?.has(pack.user.id),
|
||||||
|
})
|
||||||
|
: "",
|
||||||
|
...pack.outputs.map((msg) => {
|
||||||
|
const thisRequest =
|
||||||
|
tracedOutputCount > 1 && msg.contextTrace ? ++requestIndex : 0;
|
||||||
|
return renderCoordLine(msg, view, {
|
||||||
|
inPack: true,
|
||||||
|
traceOnly: opts.traceOnly || opts.hiddenIds?.has(msg.id),
|
||||||
|
requestIndex: thisRequest || undefined,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
].join("");
|
||||||
|
return `<article class="coord-pack${pending ? " coord-pack--pending" : ""}" data-message-id="${esc(anchor.id)}"${rollbackAttr}${restoreAttr}${originalAttr}${readOnlyAttr}>
|
||||||
|
<button type="button" class="coord-line-menu" data-msg-menu-trigger aria-label="打开这一轮的操作菜单" title="更多操作">⋯</button>
|
||||||
|
<header class="coord-pack-head">
|
||||||
|
<span class="coord-pack-meta">${esc(meta)}</span>
|
||||||
|
</header>
|
||||||
|
${lines}
|
||||||
|
</article>`;
|
||||||
|
}
|
||||||
|
|
||||||
function getRollbackMessageId(messages, messageId) {
|
function getRollbackMessageId(messages, messageId) {
|
||||||
const messageIndex = messages.findIndex((item) => item.id === messageId);
|
const messageIndex = messages.findIndex((item) => item.id === messageId);
|
||||||
return messageIndex >= 0 ? messages[messageIndex + 1]?.id ?? null : null;
|
return messageIndex >= 0 ? messages[messageIndex + 1]?.id ?? null : null;
|
||||||
@@ -3255,7 +3653,7 @@ function getRollbackMessageId(messages, messageId) {
|
|||||||
|
|
||||||
/** @returns {"speak"|"answer"|"review"|"busy"|null} */
|
/** @returns {"speak"|"answer"|"review"|"busy"|null} */
|
||||||
function resolveDesignSurface(view, loading) {
|
function resolveDesignSurface(view, loading) {
|
||||||
if (view.lifecycleStage === "play") return null;
|
if (isPlayView(view)) return null;
|
||||||
if (view.phase === "done") return null;
|
if (view.phase === "done") return null;
|
||||||
const busy = Boolean(loading || (view.phase === "running" && !view.waitingReason));
|
const busy = Boolean(loading || (view.phase === "running" && !view.waitingReason));
|
||||||
if (busy) return "busy";
|
if (busy) return "busy";
|
||||||
@@ -3300,6 +3698,12 @@ function proposedOutputCopy(proposed) {
|
|||||||
typeof target === "string" || typeof target === "number"
|
typeof target === "string" || typeof target === "number"
|
||||||
? String(target).trim()
|
? String(target).trim()
|
||||||
: "";
|
: "";
|
||||||
|
if (proposed?.mode === "revise") {
|
||||||
|
if (targetText) {
|
||||||
|
return `将在既有「${targetText}」的${proposed.name}上继续改,不是从零再生成。`;
|
||||||
|
}
|
||||||
|
return `将在既有「${proposed?.name || "该步"}」上继续改,不是从零再生成。`;
|
||||||
|
}
|
||||||
if (targetText) {
|
if (targetText) {
|
||||||
return `将生成「${targetText}」的${proposed.name}。`;
|
return `将生成「${targetText}」的${proposed.name}。`;
|
||||||
}
|
}
|
||||||
@@ -3347,17 +3751,24 @@ function renderSpeakWorkspace(view) {
|
|||||||
hint = "写清楚要改哪里;发送后在现有产物上修改。";
|
hint = "写清楚要改哪里;发送后在现有产物上修改。";
|
||||||
} else if (wr?.kind === "next_intent") {
|
} else if (wr?.kind === "next_intent") {
|
||||||
title = "下一步想写什么";
|
title = "下一步想写什么";
|
||||||
hint = "说说接下来想做什么;可留空,发送后会展示下一节点供确认。";
|
hint = "说说接下来想做什么;可留空,也可说回头改某步。发送后会展示下一节点供确认。";
|
||||||
} else if (wr?.kind === "approve_step") {
|
} else if (wr?.kind === "approve_step") {
|
||||||
title = view.proposedNextStep?.name
|
title = view.proposedNextStep?.name
|
||||||
? `接下来生成 · ${view.proposedNextStep.name}`
|
? view.proposedNextStep.mode === "revise"
|
||||||
|
? `回头修改 · ${view.proposedNextStep.name}`
|
||||||
|
: `接下来生成 · ${view.proposedNextStep.name}`
|
||||||
: "确认下一步";
|
: "确认下一步";
|
||||||
hint = view.proposedNextStep
|
hint = view.proposedNextStep
|
||||||
? proposedOutputCopy(view.proposedNextStep)
|
? proposedOutputCopy(view.proposedNextStep)
|
||||||
: view.focus?.detail || "确认执行,或在底栏说明意见。";
|
: view.focus?.detail || "确认执行,或在底栏说明意见。";
|
||||||
} else if (wr?.kind === "input") {
|
} else if (wr?.kind === "input") {
|
||||||
|
if (wr.message && /创作已收口/.test(wr.message)) {
|
||||||
|
title = "创作已收口";
|
||||||
|
hint = wr.message;
|
||||||
|
} else {
|
||||||
title = "继续说";
|
title = "继续说";
|
||||||
hint = view.hints?.[0] || wr.message || "直接输入你的想法或补充。";
|
hint = view.hints?.[0] || wr.message || "直接输入你的想法或补充。";
|
||||||
|
}
|
||||||
} else if (view.uiPrompt && !hasUserMessages(view)) {
|
} else if (view.uiPrompt && !hasUserMessages(view)) {
|
||||||
title = "描述你想创作什么";
|
title = "描述你想创作什么";
|
||||||
hint = String(view.uiPrompt).trim().slice(0, 280);
|
hint = String(view.uiPrompt).trim().slice(0, 280);
|
||||||
@@ -3400,19 +3811,25 @@ function fillCoordRail(visible, view, handlers) {
|
|||||||
return visibleIds.has(message.id) || Boolean(message.contextTrace);
|
return visibleIds.has(message.id) || Boolean(message.contextTrace);
|
||||||
});
|
});
|
||||||
const traceCount = history.filter((message) => message.contextTrace).length;
|
const traceCount = history.filter((message) => message.contextTrace).length;
|
||||||
|
const packs = groupCoordTurnPacks(history);
|
||||||
|
const hiddenIds = new Set(
|
||||||
|
history.filter((message) => !visibleIds.has(message.id)).map((message) => message.id),
|
||||||
|
);
|
||||||
coordRail.hidden = false;
|
coordRail.hidden = false;
|
||||||
if (drawerBody) {
|
if (drawerBody) {
|
||||||
drawerBody.innerHTML = history.length
|
drawerBody.innerHTML = packs.length
|
||||||
? history
|
? packs.map((pack) => renderCoordPack(pack, view, { hiddenIds })).join("")
|
||||||
.map((m) => renderCoordLine(m, view, { traceOnly: !visibleIds.has(m.id) }))
|
|
||||||
.join("")
|
|
||||||
: `<p class="coord-empty">暂无对话摘要</p>`;
|
: `<p class="coord-empty">暂无对话摘要</p>`;
|
||||||
wireMessageFeedActions(drawerBody, handlers);
|
wireMessageFeedActions(drawerBody, handlers);
|
||||||
wireMessageContextMenu(drawerBody, handlers);
|
wireMessageContextMenu(drawerBody, handlers);
|
||||||
}
|
}
|
||||||
if (drawerCount) {
|
if (drawerCount) {
|
||||||
drawerCount.textContent = history.length ? String(history.length) : "";
|
drawerCount.textContent = packs.length ? String(packs.length) : "";
|
||||||
drawerCount.title = traceCount ? `${traceCount} 条保留了完整请求上下文` : "";
|
drawerCount.title = traceCount
|
||||||
|
? `${packs.length} 轮,其中 ${traceCount} 条保留了完整请求上下文`
|
||||||
|
: packs.length
|
||||||
|
? `${packs.length} 轮对话`
|
||||||
|
: "";
|
||||||
}
|
}
|
||||||
syncCoordRailChrome();
|
syncCoordRailChrome();
|
||||||
}
|
}
|
||||||
@@ -3444,6 +3861,7 @@ function mountReviewWorkspace(stage, view, handlers, opts = {}) {
|
|||||||
}) + askSlot;
|
}) + askSlot;
|
||||||
wireMessageFeedActions(stage, handlers);
|
wireMessageFeedActions(stage, handlers);
|
||||||
wireMessageContextMenu(stage, handlers);
|
wireMessageContextMenu(stage, handlers);
|
||||||
|
wireOpeningPicker();
|
||||||
if (opts.mountAskCard) {
|
if (opts.mountAskCard) {
|
||||||
const askHost = document.getElementById("workspace-review-ask");
|
const askHost = document.getElementById("workspace-review-ask");
|
||||||
if (askHost) ensureQuestionsHostIn(askHost);
|
if (askHost) ensureQuestionsHostIn(askHost);
|
||||||
@@ -3466,13 +3884,16 @@ export function renderMessageFeed(view, loading, handlers = {}) {
|
|||||||
const surface = resolveDesignSurface(view, loading);
|
const surface = resolveDesignSurface(view, loading);
|
||||||
const designWorkspace = surface != null;
|
const designWorkspace = surface != null;
|
||||||
const reviewingNow =
|
const reviewingNow =
|
||||||
view.waitingReason?.kind === "review_artifact" && Boolean(view.reviewArtifact);
|
!isPlayView(view) &&
|
||||||
|
view.waitingReason?.kind === "review_artifact" &&
|
||||||
|
Boolean(view.reviewArtifact);
|
||||||
|
|
||||||
document.body.classList.toggle("is-reviewing", reviewingNow);
|
document.body.classList.toggle("is-reviewing", reviewingNow);
|
||||||
document.body.classList.toggle("design-workspace", designWorkspace);
|
document.body.classList.toggle("design-workspace", designWorkspace);
|
||||||
if (reviewingNow) {
|
if (reviewingNow) {
|
||||||
document.body.dataset.reviewKind = reviewComposerCopy(
|
document.body.dataset.reviewKind = reviewComposerCopy(
|
||||||
view.reviewArtifact?.workerId,
|
view.reviewArtifact?.workerId,
|
||||||
|
{ outputTags: view.reviewArtifact?.outputTags },
|
||||||
).kind;
|
).kind;
|
||||||
} else {
|
} else {
|
||||||
delete document.body.dataset.reviewKind;
|
delete document.body.dataset.reviewKind;
|
||||||
@@ -3529,9 +3950,11 @@ export function renderMessageFeed(view, loading, handlers = {}) {
|
|||||||
<header class="workspace-pending-head">
|
<header class="workspace-pending-head">
|
||||||
<span class="msg-tag">${esc(label)}</span>
|
<span class="msg-tag">${esc(label)}</span>
|
||||||
<span class="msg-live-indicator">流式输出中</span>
|
<span class="msg-live-indicator">流式输出中</span>
|
||||||
|
${renderRetryRunButton()}
|
||||||
</header>
|
</header>
|
||||||
<div class="msg-body msg-live-body">${renderLiveStreamBody(live, view)}</div>
|
<div class="msg-body msg-live-body">${renderLiveStreamBody(live, view)}</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
wireRetryRun(stage, handlers);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3565,8 +3988,8 @@ export function renderMessageFeed(view, loading, handlers = {}) {
|
|||||||
if (!visible.length && !loading) {
|
if (!visible.length && !loading) {
|
||||||
const p = document.createElement("p");
|
const p = document.createElement("p");
|
||||||
p.className = "empty";
|
p.className = "empty";
|
||||||
if (view.lifecycleStage === "play") {
|
if (isPlayView(view)) {
|
||||||
p.textContent = "游玩模式:Agent 将按 Worker 集调度,推进世界与叙事。";
|
p.textContent = "开场已就绪。在下方说你要做什么。";
|
||||||
} else if (view.uiPrompt) {
|
} else if (view.uiPrompt) {
|
||||||
const recipeLine = view.selectedRecipe?.name
|
const recipeLine = view.selectedRecipe?.name
|
||||||
? `\n\n已选配方:${view.selectedRecipe.name}`
|
? `\n\n已选配方:${view.selectedRecipe.name}`
|
||||||
@@ -3621,31 +4044,48 @@ export function renderMessageFeed(view, loading, handlers = {}) {
|
|||||||
|
|
||||||
const questionsActive = kind === "worker_questions" && Boolean(activeQuestions);
|
const questionsActive = kind === "worker_questions" && Boolean(activeQuestions);
|
||||||
const showThinking =
|
const showThinking =
|
||||||
!isUser && msg.thinking && view.lifecycleStage === "play";
|
!isUser && msg.thinking && isPlayView(view);
|
||||||
const bodyHtml = questionsActive
|
const innerBody = questionsActive
|
||||||
? `<p class="msg-q-index">${esc(body)}</p>`
|
? `<p class="msg-q-index">${esc(body)}</p>`
|
||||||
: kind === "worker_questions"
|
: kind === "worker_questions"
|
||||||
? formatQuestionsHtml(body)
|
? formatQuestionsHtml(body)
|
||||||
: kind === "worker_output"
|
|
||||||
? formatArtifactBodyHtml(body)
|
|
||||||
: shouldRenderPlayPresent(msg, view)
|
: shouldRenderPlayPresent(msg, view)
|
||||||
? formatPlayPresentHtml(body, view)
|
? formatPlayPresentHtml(body, view)
|
||||||
|
: kind === "worker_output"
|
||||||
|
? formatArtifactBodyHtml(body)
|
||||||
: esc(body);
|
: esc(body);
|
||||||
|
|
||||||
|
const process = !isUser && isPlayProcessMessage(msg, view);
|
||||||
|
const playFinal = !isUser && isPlayFinalReply(msg, view);
|
||||||
|
if (process) {
|
||||||
|
card.classList.add("msg-play-process-row");
|
||||||
|
card.innerHTML = `
|
||||||
|
<details class="msg-play-process">
|
||||||
|
<summary>
|
||||||
|
<span class="msg-tag">${esc(label)}</span>
|
||||||
|
${showSubtitle ? `<span class="msg-subtitle">${esc(subtitle)}</span>` : ""}
|
||||||
|
<span class="msg-time">${fmtTime(msg.createdAt)}</span>
|
||||||
|
</summary>
|
||||||
|
${showThinking ? renderThinkingBlock(msg.thinking) : ""}
|
||||||
|
<div class="msg-body">${innerBody}</div>
|
||||||
|
</details>`;
|
||||||
|
} else {
|
||||||
|
if (playFinal) card.classList.add("msg-play-final");
|
||||||
card.innerHTML = `
|
card.innerHTML = `
|
||||||
<div class="msg-bubble${questionsActive ? " msg-bubble-index" : ""}">
|
<div class="msg-bubble${questionsActive ? " msg-bubble-index" : ""}">
|
||||||
${isUser ? "" : `<header class="msg-head">${headInner}${menuTrigger}</header>`}
|
${isUser ? "" : `<header class="msg-head">${headInner}${menuTrigger}</header>`}
|
||||||
${showThinking ? renderThinkingBlock(msg.thinking) : ""}
|
${showThinking ? renderThinkingBlock(msg.thinking) : ""}
|
||||||
<div class="msg-body">${bodyHtml}</div>
|
<div class="msg-body">${innerBody}</div>
|
||||||
${isUser ? `<footer class="msg-foot">${headInner}${menuTrigger}</footer>` : ""}
|
${isUser ? `<footer class="msg-foot">${headInner}${menuTrigger}</footer>` : ""}
|
||||||
</div>`;
|
</div>`;
|
||||||
|
}
|
||||||
feed.appendChild(card);
|
feed.appendChild(card);
|
||||||
}
|
}
|
||||||
|
|
||||||
wireMessageFeedActions(feed, handlers);
|
wireMessageFeedActions(feed, handlers);
|
||||||
wireMessageContextMenu(feed, handlers);
|
wireMessageContextMenu(feed, handlers);
|
||||||
|
|
||||||
if (reviewingNow && view.reviewArtifact && !loading && stage) {
|
if (reviewingNow && view.reviewArtifact && !loading && stage && !isPlayView(view)) {
|
||||||
const hasHungAsk = Boolean(activeQuestions?.questions?.length);
|
const hasHungAsk = Boolean(activeQuestions?.questions?.length);
|
||||||
mountReviewWorkspace(stage, view, handlers, {
|
mountReviewWorkspace(stage, view, handlers, {
|
||||||
hideAskSidecar: hasHungAsk,
|
hideAskSidecar: hasHungAsk,
|
||||||
@@ -3661,10 +4101,11 @@ export function renderMessageFeed(view, loading, handlers = {}) {
|
|||||||
const label = live?.label ?? view.focus?.action ?? "处理中";
|
const label = live?.label ?? view.focus?.action ?? "处理中";
|
||||||
pending.innerHTML = `
|
pending.innerHTML = `
|
||||||
<div class="msg-bubble">
|
<div class="msg-bubble">
|
||||||
<header class="msg-head"><span class="msg-tag">${esc(label)}</span><span class="msg-live-indicator">流式输出中</span></header>
|
<header class="msg-head"><span class="msg-tag">${esc(label)}</span><span class="msg-live-indicator">流式输出中</span>${renderRetryRunButton()}</header>
|
||||||
<div class="msg-body msg-live-body">${renderLiveStreamBody(live, view)}</div>
|
<div class="msg-body msg-live-body">${renderLiveStreamBody(live, view)}</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
feed.appendChild(pending);
|
feed.appendChild(pending);
|
||||||
|
wireRetryRun(pending, handlers);
|
||||||
}
|
}
|
||||||
|
|
||||||
feed.scrollTop = feed.scrollHeight;
|
feed.scrollTop = feed.scrollHeight;
|
||||||
@@ -3682,7 +4123,7 @@ export function updateLiveStreamPanel(view) {
|
|||||||
if (labelEl && live?.label) labelEl.textContent = live.label;
|
if (labelEl && live?.label) labelEl.textContent = live.label;
|
||||||
const next = renderLiveStreamBody(live, view);
|
const next = renderLiveStreamBody(live, view);
|
||||||
const hadThinking = Boolean(body.querySelector(".msg-live-pre"));
|
const hadThinking = Boolean(body.querySelector(".msg-live-pre"));
|
||||||
if (next === "…" && hadThinking) return;
|
if (next.includes("msg-live-empty") && hadThinking) return;
|
||||||
body.innerHTML = next;
|
body.innerHTML = next;
|
||||||
const pre = body.querySelector(".msg-live-pre:last-of-type");
|
const pre = body.querySelector(".msg-live-pre:last-of-type");
|
||||||
if (pre) pre.scrollTop = pre.scrollHeight;
|
if (pre) pre.scrollTop = pre.scrollHeight;
|
||||||
|
|||||||
+460
-271
File diff suppressed because it is too large
Load Diff
@@ -101,6 +101,10 @@ export function formatWorkerDisplayTitle(workerId, action = null) {
|
|||||||
export function reviewComposerCopy(workerId, opts = {}) {
|
export function reviewComposerCopy(workerId, opts = {}) {
|
||||||
const hasQuestions = Boolean(opts.hasQuestions);
|
const hasQuestions = Boolean(opts.hasQuestions);
|
||||||
const id = String(workerId ?? "").trim();
|
const id = String(workerId ?? "").trim();
|
||||||
|
const openingReview =
|
||||||
|
id === "opening-generator" ||
|
||||||
|
(Array.isArray(opts.outputTags) &&
|
||||||
|
opts.outputTags.includes("设计.开场白与开场变量"));
|
||||||
|
|
||||||
if (id === "design-flow") {
|
if (id === "design-flow") {
|
||||||
return {
|
return {
|
||||||
@@ -120,7 +124,7 @@ export function reviewComposerCopy(workerId, opts = {}) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (id === "opening-generator") {
|
if (id === "opening-generator" || openingReview) {
|
||||||
return {
|
return {
|
||||||
chip: "开场",
|
chip: "开场",
|
||||||
kicker: "待选定",
|
kicker: "待选定",
|
||||||
|
|||||||
+22
-32
@@ -35,7 +35,6 @@
|
|||||||
|
|
||||||
<div class="rail-panels">
|
<div class="rail-panels">
|
||||||
<div class="rail-tab-panel active" id="rail-panel-books" role="tabpanel" data-rail-panel="books">
|
<div class="rail-tab-panel active" id="rail-panel-books" role="tabpanel" data-rail-panel="books">
|
||||||
<nav class="path-bar" id="book-path-bar" aria-label="面包屑" hidden></nav>
|
|
||||||
<div class="book-list" id="book-list"></div>
|
<div class="book-list" id="book-list"></div>
|
||||||
<div id="book-box-overlay" class="book-box-overlay" hidden aria-hidden="true"></div>
|
<div id="book-box-overlay" class="book-box-overlay" hidden aria-hidden="true"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -48,39 +47,11 @@
|
|||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<main class="panel panel-main">
|
<main class="panel panel-main">
|
||||||
<header class="main-head" id="main-head">
|
|
||||||
<div class="main-head-left">
|
|
||||||
<h1 class="work-title" id="work-title">未打开作品</h1>
|
|
||||||
<span class="work-meta" id="work-meta"></span>
|
|
||||||
</div>
|
|
||||||
<div class="main-head-center">
|
|
||||||
<div class="lifecycle-toggle" id="lifecycle-toggle" role="group" aria-label="创作 / 游玩">
|
|
||||||
<button type="button" class="lifecycle-btn active" data-stage="design">创作</button>
|
|
||||||
<button type="button" class="lifecycle-btn" data-stage="play" disabled>游玩</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="main-head-right">
|
|
||||||
<button type="button" class="btn-sm" id="btn-save-instance" hidden>保存定稿</button>
|
|
||||||
<button type="button" class="btn-sm" id="btn-save-play" hidden>保存游玩</button>
|
|
||||||
<button type="button" class="btn-sm" id="btn-export" disabled>导出</button>
|
|
||||||
<span class="status" id="status-pill"><span class="status-dot" id="status-dot"></span><span id="status-text">—</span></span>
|
|
||||||
<details class="more-menu">
|
|
||||||
<summary class="btn-sm more-menu-sum" title="更多页面">更多</summary>
|
|
||||||
<div class="more-menu-panel" role="menu">
|
|
||||||
<a class="more-menu-item" href="/modules.html" role="menuitem">技能</a>
|
|
||||||
<a class="more-menu-item" href="/shells.html" role="menuitem">呈现壳</a>
|
|
||||||
<a class="more-menu-item" href="/stats.html" role="menuitem">Token</a>
|
|
||||||
<a class="more-menu-item" href="/settings.html" role="menuitem">设置</a>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div class="main-body">
|
<div class="main-body">
|
||||||
<section class="panel-feed" id="panel-feed">
|
<section class="panel-feed" id="panel-feed">
|
||||||
<div id="skill-picker" class="skill-picker" hidden></div>
|
<div id="skill-picker" class="skill-picker" hidden></div>
|
||||||
<div id="message-feed" class="message-feed">
|
<div id="message-feed" class="message-feed">
|
||||||
<p class="empty">点击左侧 + 新建作品</p>
|
<p class="empty">正在打开作品…</p>
|
||||||
</div>
|
</div>
|
||||||
<div id="workspace-stage" class="workspace-stage" hidden aria-label="验收工作面"></div>
|
<div id="workspace-stage" class="workspace-stage" hidden aria-label="验收工作面"></div>
|
||||||
<div id="questions-card-host" class="questions-card-host" hidden></div>
|
<div id="questions-card-host" class="questions-card-host" hidden></div>
|
||||||
@@ -106,7 +77,22 @@
|
|||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer class="composer" id="composer"></footer>
|
<footer class="composer" id="composer">
|
||||||
|
<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>
|
||||||
|
<details class="more-menu">
|
||||||
|
<summary class="btn-sm more-menu-sum" title="更多">更多</summary>
|
||||||
|
<div class="more-menu-panel" role="menu">
|
||||||
|
<button type="button" class="more-menu-item" id="btn-save-instance" hidden role="menuitem">保存定稿</button>
|
||||||
|
<button type="button" class="more-menu-item" id="btn-save-play" hidden role="menuitem">保存游玩</button>
|
||||||
|
<button type="button" class="more-menu-item" id="btn-export" disabled role="menuitem">导出</button>
|
||||||
|
<a class="more-menu-item" href="/modules.html" role="menuitem">技能</a>
|
||||||
|
<a class="more-menu-item" href="/shells.html" role="menuitem">呈现壳</a>
|
||||||
|
<a class="more-menu-item" href="/stats.html" role="menuitem">Token</a>
|
||||||
|
<a class="more-menu-item" href="/settings.html" role="menuitem">设置</a>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</footer>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -133,7 +119,11 @@
|
|||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
<menu id="book-action-menu" class="context-menu" hidden>
|
<menu id="book-action-menu" class="context-menu" hidden>
|
||||||
<button type="button" class="context-menu-item" id="book-menu-open" data-action="open">打开</button>
|
<button type="button" class="context-menu-item" id="book-menu-open" data-action="open">打开创作</button>
|
||||||
|
<button type="button" class="context-menu-item" id="book-menu-play" data-action="play">进入游玩</button>
|
||||||
|
<button type="button" class="context-menu-item" id="book-menu-save-instance" data-action="save-instance">保存定稿</button>
|
||||||
|
<button type="button" class="context-menu-item" id="book-menu-save-play" data-action="save-play">保存游玩</button>
|
||||||
|
<button type="button" class="context-menu-item" id="book-menu-export" data-action="export">导出</button>
|
||||||
<button type="button" class="context-menu-item" id="book-menu-rename" data-action="rename">重命名</button>
|
<button type="button" class="context-menu-item" id="book-menu-rename" data-action="rename">重命名</button>
|
||||||
<button type="button" class="context-menu-item" id="book-menu-duplicate" data-action="duplicate">复制备份</button>
|
<button type="button" class="context-menu-item" id="book-menu-duplicate" data-action="duplicate">复制备份</button>
|
||||||
<button type="button" class="context-menu-item" id="book-menu-select" data-action="select-mode">多选</button>
|
<button type="button" class="context-menu-item" id="book-menu-select" data-action="select-mode">多选</button>
|
||||||
|
|||||||
+16
-1
@@ -82,6 +82,20 @@ function formatBlockContent(value) {
|
|||||||
return String(value);
|
return String(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 模型常把 tag 名写进正文开头;展示前剥掉 */
|
||||||
|
export function stripPresentSourceFences(raw) {
|
||||||
|
let s = String(raw ?? "").trim();
|
||||||
|
for (let i = 0; i < 3; i += 1) {
|
||||||
|
const next = s
|
||||||
|
.replace(/^(#{1,6}\s*)?输出[.::]?\s*(用户展示|开场白)(?:\s*\n+|\s*$)/u, "")
|
||||||
|
.replace(/^【\s*输出[.::]?\s*(用户展示|开场白)\s*】(?:\s*\n+|\s*$)/u, "")
|
||||||
|
.trim();
|
||||||
|
if (next === s) break;
|
||||||
|
s = next;
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeBlocks(raw) {
|
function normalizeBlocks(raw) {
|
||||||
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {};
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {};
|
||||||
const out = {};
|
const out = {};
|
||||||
@@ -90,6 +104,7 @@ function normalizeBlocks(raw) {
|
|||||||
}
|
}
|
||||||
if (out.body === undefined && raw.正文 !== undefined) out.body = raw.正文;
|
if (out.body === undefined && raw.正文 !== undefined) out.body = raw.正文;
|
||||||
if (out.monitor === undefined && raw.监控 !== undefined) out.monitor = raw.监控;
|
if (out.monitor === undefined && raw.监控 !== undefined) out.monitor = raw.监控;
|
||||||
|
if (typeof out.body === "string") out.body = stripPresentSourceFences(out.body);
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,7 +177,7 @@ export function presentFromPlain(text, shell = "prose") {
|
|||||||
return {
|
return {
|
||||||
schema: "present.v1",
|
schema: "present.v1",
|
||||||
shell: isPresentShellId(shell) ? shell : "prose",
|
shell: isPresentShellId(shell) ? shell : "prose",
|
||||||
blocks: { body: text ?? "" },
|
blocks: { body: stripPresentSourceFences(text ?? "") },
|
||||||
meta: { suggested_actions: [] },
|
meta: { suggested_actions: [] },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-34
@@ -4,6 +4,7 @@
|
|||||||
* 点发送即答复 → 收起本轮卡(卡上无独立发送钮)。
|
* 点发送即答复 → 收起本轮卡(卡上无独立发送钮)。
|
||||||
* 产物验收挂载题:选项 + 底栏意见一并随「按意见修改」写回产物;空内容点该钮不等于接受。
|
* 产物验收挂载题:选项 + 底栏意见一并随「按意见修改」写回产物;空内容点该钮不等于接受。
|
||||||
* 分页 / 跳过为悬浮控件,不占独立 header/footer 行。
|
* 分页 / 跳过为悬浮控件,不占独立 header/footer 行。
|
||||||
|
* 点顶栏横条折叠成一行,再点展开。
|
||||||
*
|
*
|
||||||
* 能力「默认问题」(id=module-opening) 不是追问:对齐美学纲领开局,走说话面 + openingGuide。
|
* 能力「默认问题」(id=module-opening) 不是追问:对齐美学纲领开局,走说话面 + openingGuide。
|
||||||
*/
|
*/
|
||||||
@@ -259,7 +260,7 @@ export function clearQuestionCardState(host, view) {
|
|||||||
host._qState = null;
|
host._qState = null;
|
||||||
host.hidden = true;
|
host.hidden = true;
|
||||||
host.innerHTML = "";
|
host.innerHTML = "";
|
||||||
host.classList.remove("is-open", "is-expanded", "has-pager", "has-skip");
|
host.classList.remove("is-open", "is-expanded", "is-collapsed", "has-pager", "has-skip");
|
||||||
host.removeAttribute("role");
|
host.removeAttribute("role");
|
||||||
host.removeAttribute("aria-label");
|
host.removeAttribute("aria-label");
|
||||||
}
|
}
|
||||||
@@ -275,7 +276,7 @@ export function renderQuestionsCard(host, view, _handlers = {}) {
|
|||||||
if (!active?.questions?.length) {
|
if (!active?.questions?.length) {
|
||||||
host.hidden = true;
|
host.hidden = true;
|
||||||
host.innerHTML = "";
|
host.innerHTML = "";
|
||||||
host.classList.remove("is-open", "is-expanded", "has-pager", "has-skip");
|
host.classList.remove("is-open", "is-expanded", "is-collapsed", "has-pager", "has-skip");
|
||||||
host.removeAttribute("role");
|
host.removeAttribute("role");
|
||||||
host.removeAttribute("aria-label");
|
host.removeAttribute("aria-label");
|
||||||
host._qDismissed = null;
|
host._qDismissed = null;
|
||||||
@@ -290,7 +291,7 @@ export function renderQuestionsCard(host, view, _handlers = {}) {
|
|||||||
) {
|
) {
|
||||||
host.hidden = true;
|
host.hidden = true;
|
||||||
host.innerHTML = "";
|
host.innerHTML = "";
|
||||||
host.classList.remove("is-open", "is-expanded", "has-pager", "has-skip");
|
host.classList.remove("is-open", "is-expanded", "is-collapsed", "has-pager", "has-skip");
|
||||||
host.removeAttribute("role");
|
host.removeAttribute("role");
|
||||||
host.removeAttribute("aria-label");
|
host.removeAttribute("aria-label");
|
||||||
return false;
|
return false;
|
||||||
@@ -305,14 +306,14 @@ export function renderQuestionsCard(host, view, _handlers = {}) {
|
|||||||
sessionId: view.id,
|
sessionId: view.id,
|
||||||
key: qKey,
|
key: qKey,
|
||||||
page: 0,
|
page: 0,
|
||||||
expanded: false,
|
collapsed: false,
|
||||||
// questionId -> { optionId?, text, otherText? }
|
// questionId -> { optionId?, text, otherText? }
|
||||||
answers: {},
|
answers: {},
|
||||||
drafts: {}, // option key -> edited label
|
drafts: {}, // option key -> edited label
|
||||||
};
|
};
|
||||||
if (typeof state.expanded !== "boolean") state.expanded = false;
|
if (typeof state.collapsed !== "boolean") state.collapsed = false;
|
||||||
host._qState = state;
|
host._qState = state;
|
||||||
host.classList.toggle("is-expanded", state.expanded);
|
host.classList.toggle("is-collapsed", state.collapsed);
|
||||||
|
|
||||||
const { questions, pageSize, assessment, optional, skipLabel, dismissOnAccept } =
|
const { questions, pageSize, assessment, optional, skipLabel, dismissOnAccept } =
|
||||||
active;
|
active;
|
||||||
@@ -362,8 +363,13 @@ export function renderQuestionsCard(host, view, _handlers = {}) {
|
|||||||
${pagerHtml}
|
${pagerHtml}
|
||||||
${skipHtml}
|
${skipHtml}
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="qcard-top" data-qtop title="${state.expanded ? "点击收起" : "点击展开"}" aria-label="${state.expanded ? "收起询问卡" : "展开询问卡"}">
|
<button type="button" class="qcard-top" data-qtop title="${state.collapsed ? "展开追问" : "折叠追问"}" aria-expanded="${state.collapsed ? "false" : "true"}" aria-label="${state.collapsed ? "展开追问" : "折叠追问"}">
|
||||||
<span class="qcard-top-grip" aria-hidden="true"></span>
|
<span class="qcard-top-grip" aria-hidden="true"></span>
|
||||||
|
<span class="qcard-collapsed-meta">${esc(
|
||||||
|
pageCount > 1
|
||||||
|
? `追问 ${state.page + 1}/${pageCount} · ${pageQs[0]?.prompt ?? "询问"}`
|
||||||
|
: `追问 · ${pageQs[0]?.prompt ?? "询问"}`,
|
||||||
|
)}</span>
|
||||||
</button>
|
</button>
|
||||||
<div class="qcard-body">
|
<div class="qcard-body">
|
||||||
${dismissNote}
|
${dismissNote}
|
||||||
@@ -414,40 +420,21 @@ export function renderQuestionsCard(host, view, _handlers = {}) {
|
|||||||
.join("")}
|
.join("")}
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
const setExpanded = (next) => {
|
const setCollapsed = (next) => {
|
||||||
state.expanded = next;
|
state.collapsed = next;
|
||||||
host.classList.toggle("is-expanded", next);
|
host.classList.toggle("is-collapsed", next);
|
||||||
const top = host.querySelector("[data-qtop]");
|
const top = host.querySelector("[data-qtop]");
|
||||||
if (top) {
|
if (top) {
|
||||||
top.title = next ? "点击收起" : "点击展开";
|
top.title = next ? "展开追问" : "折叠追问";
|
||||||
top.setAttribute("aria-label", next ? "收起询问卡" : "展开询问卡");
|
top.setAttribute("aria-expanded", next ? "false" : "true");
|
||||||
|
top.setAttribute("aria-label", next ? "展开追问" : "折叠追问");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
host.querySelector("[data-qtop]")?.addEventListener("click", (e) => {
|
host.querySelector("[data-qtop]")?.addEventListener("click", (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setExpanded(!state.expanded);
|
setCollapsed(!state.collapsed);
|
||||||
});
|
|
||||||
|
|
||||||
host.querySelector(".qcard-body")?.addEventListener("click", (e) => {
|
|
||||||
const t = e.target;
|
|
||||||
if (!(t instanceof Element)) return;
|
|
||||||
if (
|
|
||||||
t.closest(
|
|
||||||
".qcard-letter, .qcard-other-input, [contenteditable=true], .qcard-nav, .qcard-skip, .qcard-pager",
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 展开后点题干 / 评估区收起;收起时点内容区展开
|
|
||||||
if (state.expanded) {
|
|
||||||
if (t.closest(".qcard-prompt, .qcard-assessment, .qcard-optional-note")) {
|
|
||||||
setExpanded(false);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setExpanded(true);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
host.querySelector("[data-qskip]")?.addEventListener("click", () => {
|
host.querySelector("[data-qskip]")?.addEventListener("click", () => {
|
||||||
@@ -488,7 +475,6 @@ export function renderQuestionsCard(host, view, _handlers = {}) {
|
|||||||
section.querySelectorAll(".qcard-opt").forEach((li) => {
|
section.querySelectorAll(".qcard-opt").forEach((li) => {
|
||||||
li.classList.toggle("is-selected", li.getAttribute("data-oid") === oid);
|
li.classList.toggle("is-selected", li.getAttribute("data-oid") === oid);
|
||||||
});
|
});
|
||||||
if (!state.expanded) setExpanded(true);
|
|
||||||
_handlers.onAnswersChange?.();
|
_handlers.onAnswersChange?.();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -800,6 +800,226 @@ code {
|
|||||||
background: rgba(24, 24, 27, 0.4);
|
background: rgba(24, 24, 27, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* —— Preset probe —— */
|
||||||
|
.probe-dialog {
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 0;
|
||||||
|
background: var(--st-surface);
|
||||||
|
color: var(--st-text);
|
||||||
|
width: min(1080px, calc(100% - 24px));
|
||||||
|
height: min(780px, calc(100% - 24px));
|
||||||
|
max-width: none;
|
||||||
|
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.probe-dialog[open] {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.probe-dialog::backdrop {
|
||||||
|
background: rgba(24, 24, 27, 0.45);
|
||||||
|
}
|
||||||
|
|
||||||
|
.probe-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 16px 18px 12px;
|
||||||
|
border-bottom: 1px solid var(--st-border);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.probe-head h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 650;
|
||||||
|
}
|
||||||
|
|
||||||
|
.probe-sub {
|
||||||
|
margin: 4px 0 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--st-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.probe-body {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(260px, 320px) minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.probe-composer,
|
||||||
|
.probe-inspect {
|
||||||
|
min-height: 0;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 16px 18px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.probe-composer {
|
||||||
|
border-right: 1px solid var(--st-border);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.probe-composer label,
|
||||||
|
.probe-extras label {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--st-muted);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.probe-composer textarea,
|
||||||
|
.probe-extras textarea {
|
||||||
|
width: 100%;
|
||||||
|
resize: vertical;
|
||||||
|
padding: 9px 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid var(--st-border-strong);
|
||||||
|
background: var(--st-bg);
|
||||||
|
color: var(--st-text);
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.probe-composer textarea:focus,
|
||||||
|
.probe-extras textarea:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #93c5fd;
|
||||||
|
box-shadow: 0 0 0 3px var(--st-accent-soft);
|
||||||
|
background: var(--st-surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
.probe-extras {
|
||||||
|
border: 1px solid var(--st-border);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px 10px 10px;
|
||||||
|
background: var(--st-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.probe-extras summary {
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--st-text);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.probe-extras[open] summary {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.probe-composer-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.probe-status {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--st-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.probe-inspect h4 {
|
||||||
|
margin: 0 0 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 650;
|
||||||
|
color: var(--st-muted);
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.probe-empty {
|
||||||
|
margin: 8px 0;
|
||||||
|
color: var(--st-faint);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.probe-messages {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.probe-msg {
|
||||||
|
border: 1px solid var(--st-border);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--st-bg);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.probe-msg header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-bottom: 1px solid var(--st-border);
|
||||||
|
background: var(--st-surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
.probe-role {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--st-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.probe-role.user {
|
||||||
|
color: #0f766e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.probe-role.assistant {
|
||||||
|
color: #7c3aed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.probe-index {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--st-faint);
|
||||||
|
}
|
||||||
|
|
||||||
|
.probe-msg pre,
|
||||||
|
.probe-reply pre {
|
||||||
|
margin: 0;
|
||||||
|
padding: 10px 12px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
font-family: ui-monospace, "Cascadia Code", Consolas, monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
.probe-reply {
|
||||||
|
margin-top: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 800px) {
|
||||||
|
.probe-dialog {
|
||||||
|
height: calc(100% - 16px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.probe-body {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
grid-template-rows: auto 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.probe-composer {
|
||||||
|
border-right: none;
|
||||||
|
border-bottom: 1px solid var(--st-border);
|
||||||
|
max-height: 42vh;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.profile-dialog form {
|
.profile-dialog form {
|
||||||
padding: 22px;
|
padding: 22px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -78,6 +78,57 @@
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<dialog id="preset-probe-dialog" class="probe-dialog">
|
||||||
|
<header class="probe-head">
|
||||||
|
<div>
|
||||||
|
<h3>试跑预设</h3>
|
||||||
|
<p class="probe-sub" id="probe-preset-name"></p>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn-secondary" id="probe-close">关闭</button>
|
||||||
|
</header>
|
||||||
|
<div class="probe-body">
|
||||||
|
<section class="probe-composer">
|
||||||
|
<label>
|
||||||
|
本轮输入
|
||||||
|
<textarea id="probe-message" rows="4" placeholder="发给模型的这一轮"></textarea>
|
||||||
|
</label>
|
||||||
|
<details class="probe-extras">
|
||||||
|
<summary>试跑用设定 / 历史</summary>
|
||||||
|
<label>
|
||||||
|
历史前(设定)
|
||||||
|
<textarea id="probe-lore-before" rows="3"></textarea>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
对话历史
|
||||||
|
<textarea id="probe-history" rows="4"></textarea>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
历史后(状态)
|
||||||
|
<textarea id="probe-lore-after" rows="3"></textarea>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
本轮后(可选)
|
||||||
|
<textarea id="probe-post-turn" rows="2"></textarea>
|
||||||
|
</label>
|
||||||
|
</details>
|
||||||
|
<div class="probe-composer-actions">
|
||||||
|
<button type="button" class="btn-primary" id="probe-send">发送一轮</button>
|
||||||
|
<p class="probe-status" id="probe-status" hidden></p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section class="probe-inspect" aria-live="polite">
|
||||||
|
<h4>发出的上下文</h4>
|
||||||
|
<div id="probe-messages" class="probe-messages">
|
||||||
|
<p class="probe-empty">发送后显示实际拼进请求的消息。</p>
|
||||||
|
</div>
|
||||||
|
<div id="probe-reply-wrap" class="probe-reply" hidden>
|
||||||
|
<h4>模型回复</h4>
|
||||||
|
<pre id="probe-reply"></pre>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
|
||||||
<dialog id="profile-dialog" class="profile-dialog">
|
<dialog id="profile-dialog" class="profile-dialog">
|
||||||
<form id="profile-form" method="dialog">
|
<form id="profile-form" method="dialog">
|
||||||
<h3 id="profile-dialog-title">新增 API</h3>
|
<h3 id="profile-dialog-title">新增 API</h3>
|
||||||
|
|||||||
+140
@@ -28,6 +28,29 @@ const sectionStorageEl = document.getElementById("section-storage");
|
|||||||
const activeSettingsBarEl = document.getElementById("active-settings-bar");
|
const activeSettingsBarEl = document.getElementById("active-settings-bar");
|
||||||
const settingsToastEl = document.getElementById("settings-toast");
|
const settingsToastEl = document.getElementById("settings-toast");
|
||||||
const contextTraceKeepEl = document.getElementById("context-trace-keep");
|
const contextTraceKeepEl = document.getElementById("context-trace-keep");
|
||||||
|
const probeDialog = document.getElementById("preset-probe-dialog");
|
||||||
|
const probePresetNameEl = document.getElementById("probe-preset-name");
|
||||||
|
const probeMessageEl = document.getElementById("probe-message");
|
||||||
|
const probeLoreBeforeEl = document.getElementById("probe-lore-before");
|
||||||
|
const probeHistoryEl = document.getElementById("probe-history");
|
||||||
|
const probeLoreAfterEl = document.getElementById("probe-lore-after");
|
||||||
|
const probePostTurnEl = document.getElementById("probe-post-turn");
|
||||||
|
const probeSendEl = document.getElementById("probe-send");
|
||||||
|
const probeStatusEl = document.getElementById("probe-status");
|
||||||
|
const probeMessagesEl = document.getElementById("probe-messages");
|
||||||
|
const probeReplyWrapEl = document.getElementById("probe-reply-wrap");
|
||||||
|
const probeReplyEl = document.getElementById("probe-reply");
|
||||||
|
|
||||||
|
const DEFAULT_PROBE_CONTEXT = {
|
||||||
|
message: "我推开门。",
|
||||||
|
loreBefore: "一座雨夜的港口旅馆。柜台点着油灯。",
|
||||||
|
history: "店员:今晚只剩阁楼那间。\n你:好。",
|
||||||
|
loreAfter: "时间:深夜。地点:旅馆大厅。",
|
||||||
|
postTurn: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
let probingPresetId = null;
|
||||||
|
let probeBusy = false;
|
||||||
|
|
||||||
async function api(path, options = {}) {
|
async function api(path, options = {}) {
|
||||||
const res = await fetch(path, {
|
const res = await fetch(path, {
|
||||||
@@ -192,6 +215,7 @@ function renderPresets() {
|
|||||||
</div>
|
</div>
|
||||||
<div class="config-card-actions">
|
<div class="config-card-actions">
|
||||||
${activateButtonHtml(active, p.id, "activate-preset")}
|
${activateButtonHtml(active, p.id, "activate-preset")}
|
||||||
|
<button type="button" data-action="probe-preset" data-id="${p.id}">试跑</button>
|
||||||
<button type="button" data-action="toggle-preset-entries" data-id="${p.id}">
|
<button type="button" data-action="toggle-preset-entries" data-id="${p.id}">
|
||||||
${expanded ? "收起条目" : "编辑条目"}
|
${expanded ? "收起条目" : "编辑条目"}
|
||||||
</button>
|
</button>
|
||||||
@@ -476,6 +500,8 @@ presetsListEl.addEventListener("click", async (e) => {
|
|||||||
try {
|
try {
|
||||||
if (action === "activate-preset") {
|
if (action === "activate-preset") {
|
||||||
await activatePreset(id);
|
await activatePreset(id);
|
||||||
|
} else if (action === "probe-preset") {
|
||||||
|
openProbeDialog(id);
|
||||||
} else if (action === "toggle-preset-entries") {
|
} else if (action === "toggle-preset-entries") {
|
||||||
await togglePresetEntries(id);
|
await togglePresetEntries(id);
|
||||||
} else if (action === "delete-preset") {
|
} else if (action === "delete-preset") {
|
||||||
@@ -573,6 +599,120 @@ presetFileEl.addEventListener("change", async () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function resetProbeForm() {
|
||||||
|
probeMessageEl.value = DEFAULT_PROBE_CONTEXT.message;
|
||||||
|
probeLoreBeforeEl.value = DEFAULT_PROBE_CONTEXT.loreBefore;
|
||||||
|
probeHistoryEl.value = DEFAULT_PROBE_CONTEXT.history;
|
||||||
|
probeLoreAfterEl.value = DEFAULT_PROBE_CONTEXT.loreAfter;
|
||||||
|
probePostTurnEl.value = DEFAULT_PROBE_CONTEXT.postTurn;
|
||||||
|
probeStatusEl.hidden = true;
|
||||||
|
probeStatusEl.textContent = "";
|
||||||
|
probeReplyWrapEl.hidden = true;
|
||||||
|
probeReplyEl.textContent = "";
|
||||||
|
probeMessagesEl.innerHTML =
|
||||||
|
'<p class="probe-empty">发送后显示实际拼进请求的消息。</p>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function openProbeDialog(presetId) {
|
||||||
|
const preset = state.presets.find((p) => p.id === presetId);
|
||||||
|
probingPresetId = presetId;
|
||||||
|
probePresetNameEl.textContent = preset?.name ?? presetId;
|
||||||
|
resetProbeForm();
|
||||||
|
probeDialog.showModal();
|
||||||
|
probeMessageEl.focus();
|
||||||
|
probeMessageEl.select();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderProbeMessages(messages) {
|
||||||
|
if (!messages?.length) {
|
||||||
|
probeMessagesEl.innerHTML =
|
||||||
|
'<p class="probe-empty">这次没有拼出任何消息。</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
probeMessagesEl.innerHTML = messages
|
||||||
|
.map(
|
||||||
|
(m, i) => `
|
||||||
|
<article class="probe-msg">
|
||||||
|
<header>
|
||||||
|
<span class="probe-role ${escapeHtml(m.role)}">${escapeHtml(m.role)}</span>
|
||||||
|
<span class="probe-index">#${i + 1}</span>
|
||||||
|
</header>
|
||||||
|
<pre>${escapeHtml(m.content)}</pre>
|
||||||
|
</article>`,
|
||||||
|
)
|
||||||
|
.join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
function setProbeBusy(busy) {
|
||||||
|
probeBusy = busy;
|
||||||
|
probeSendEl.disabled = busy;
|
||||||
|
probeSendEl.textContent = busy ? "发送中…" : "发送一轮";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendProbe() {
|
||||||
|
if (probeBusy || !probingPresetId) return;
|
||||||
|
const message = probeMessageEl.value.trim();
|
||||||
|
if (!message) {
|
||||||
|
probeStatusEl.hidden = false;
|
||||||
|
probeStatusEl.textContent = "请填写本轮输入";
|
||||||
|
probeMessageEl.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setProbeBusy(true);
|
||||||
|
probeStatusEl.hidden = false;
|
||||||
|
probeStatusEl.textContent = "正在拼装上下文…";
|
||||||
|
probeReplyWrapEl.hidden = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await api(`/api/presets/${probingPresetId}/probe`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
message,
|
||||||
|
loreBefore: probeLoreBeforeEl.value,
|
||||||
|
history: probeHistoryEl.value,
|
||||||
|
loreAfter: probeLoreAfterEl.value,
|
||||||
|
postTurn: probePostTurnEl.value,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
renderProbeMessages(result.messages);
|
||||||
|
if (result.reply) {
|
||||||
|
probeReplyWrapEl.hidden = false;
|
||||||
|
probeReplyEl.textContent = result.reasoning
|
||||||
|
? `${result.reasoning}\n\n——\n\n${result.reply}`
|
||||||
|
: result.reply;
|
||||||
|
}
|
||||||
|
if (result.error) {
|
||||||
|
probeStatusEl.textContent = `上下文已拼装,生成失败:${result.error}`;
|
||||||
|
} else if (result.skipReason) {
|
||||||
|
probeStatusEl.textContent = result.skipReason;
|
||||||
|
} else if (result.completed) {
|
||||||
|
probeStatusEl.textContent = `已发送 ${result.messages.length} 条`;
|
||||||
|
} else {
|
||||||
|
probeStatusEl.textContent = `已拼装 ${result.messages.length} 条`;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
probeStatusEl.textContent = err.message;
|
||||||
|
} finally {
|
||||||
|
setProbeBusy(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById("probe-close")?.addEventListener("click", () => {
|
||||||
|
probeDialog.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
probeSendEl?.addEventListener("click", () => {
|
||||||
|
sendProbe().catch((err) => showToast(err.message, true));
|
||||||
|
});
|
||||||
|
|
||||||
|
probeMessageEl?.addEventListener("keydown", (e) => {
|
||||||
|
if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) {
|
||||||
|
e.preventDefault();
|
||||||
|
sendProbe().catch((err) => showToast(err.message, true));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
document.getElementById("btn-save-context-keep")?.addEventListener("click", async () => {
|
document.getElementById("btn-save-context-keep")?.addEventListener("click", async () => {
|
||||||
try {
|
try {
|
||||||
const n = Number(contextTraceKeepEl?.value ?? 5);
|
const n = Number(contextTraceKeepEl?.value ?? 5);
|
||||||
|
|||||||
+1
-1
@@ -32,7 +32,7 @@
|
|||||||
<p class="st-rail-hint">按「要同时看见什么」选,不是按好看选。</p>
|
<p class="st-rail-hint">按「要同时看见什么」选,不是按好看选。</p>
|
||||||
<nav class="shells-list" id="shell-list" role="tablist"></nav>
|
<nav class="shells-list" id="shell-list" role="tablist"></nav>
|
||||||
<p class="st-rail-note">
|
<p class="st-rail-note">
|
||||||
主任务永远是读正文(常 600~3000 字)。侧栏只换壳;长度/语气在预览顶栏。
|
主任务永远是读正文(常 500~2000 字)。侧栏只换壳;长度/语气在预览顶栏。
|
||||||
</p>
|
</p>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* 呈现壳预览:主工作面 = 壳画布。
|
* 呈现壳预览:主工作面 = 壳画布。
|
||||||
* 按「要同时看见什么」选壳;正文按 600~3000 字撑高滚动。
|
* 按「要同时看见什么」选壳;正文按 500~2000 字撑高滚动。
|
||||||
*/
|
*/
|
||||||
import {
|
import {
|
||||||
PRESENT_SHELL_IDS,
|
PRESENT_SHELL_IDS,
|
||||||
@@ -56,7 +56,7 @@ const TONES = [
|
|||||||
const LENGTHS = [
|
const LENGTHS = [
|
||||||
{ id: "short", label: "短" },
|
{ id: "short", label: "短" },
|
||||||
{ id: "mid", label: "中 ·800" },
|
{ id: "mid", label: "中 ·800" },
|
||||||
{ id: "long", label: "长 ·2500" },
|
{ id: "long", label: "长 ·2000" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const BODY_MID = `雨还在下。你把伞往她那边偏了偏,她没说话,只是把购物袋往怀里收紧了一点。水从伞骨边缘连成线,砸在柏油路上,溅起细碎的白点。
|
const BODY_MID = `雨还在下。你把伞往她那边偏了偏,她没说话,只是把购物袋往怀里收紧了一点。水从伞骨边缘连成线,砸在柏油路上,溅起细碎的白点。
|
||||||
|
|||||||
+331
-45
@@ -18,8 +18,8 @@
|
|||||||
--workspace-pad-x: clamp(24px, 4.5vw, 72px);
|
--workspace-pad-x: clamp(24px, 4.5vw, 72px);
|
||||||
--workspace-pad-y: clamp(20px, 3vh, 40px);
|
--workspace-pad-y: clamp(20px, 3vh, 40px);
|
||||||
--head-h: 48px;
|
--head-h: 48px;
|
||||||
--composer-min: 56px;
|
--composer-min: 0px;
|
||||||
--composer-control-h: 40px;
|
--composer-control-h: 36px;
|
||||||
--font-ui: "Noto Sans SC", "PingFang SC", "Microsoft YaHei UI", system-ui, sans-serif;
|
--font-ui: "Noto Sans SC", "PingFang SC", "Microsoft YaHei UI", system-ui, sans-serif;
|
||||||
--font-read: "Noto Serif SC", "Songti SC", "STSong", "Source Han Serif SC", Georgia, serif;
|
--font-read: "Noto Serif SC", "Songti SC", "STSong", "Source Han Serif SC", Georgia, serif;
|
||||||
font-family: var(--font-ui);
|
font-family: var(--font-ui);
|
||||||
@@ -201,7 +201,7 @@ body.rail-collapsed .panel-rail { align-items: stretch; }
|
|||||||
/* composer æå
容èªéåºï¼ä¸é´åºåæå©ä½é«åº¦ï¼æ´é¡µéå¨è§å£å
*/
|
/* composer æå
容èªéåºï¼ä¸é´åºåæå©ä½é«åº¦ï¼æ´é¡µéå¨è§å£å
*/
|
||||||
.panel-main {
|
.panel-main {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: var(--head-h) minmax(0, 1fr) auto;
|
grid-template-rows: minmax(0, 1fr) auto;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -435,7 +435,7 @@ body:not(.coord-rail-collapsed) .coord-rail-toggle {
|
|||||||
padding: 8px;
|
padding: 8px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 4px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
.coord-empty {
|
.coord-empty {
|
||||||
margin: 16px 8px;
|
margin: 16px 8px;
|
||||||
@@ -443,6 +443,55 @@ body:not(.coord-rail-collapsed) .coord-rail-toggle {
|
|||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
.coord-pack {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
padding: 4px 4px 6px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 10px;
|
||||||
|
background: var(--bg);
|
||||||
|
min-width: 0;
|
||||||
|
cursor: context-menu;
|
||||||
|
}
|
||||||
|
.coord-pack:hover {
|
||||||
|
border-color: color-mix(in srgb, var(--accent) 28%, var(--border));
|
||||||
|
background: color-mix(in srgb, var(--accent-soft) 28%, var(--bg));
|
||||||
|
}
|
||||||
|
.coord-pack--pending {
|
||||||
|
border-color: color-mix(in srgb, var(--accent) 42%, var(--border));
|
||||||
|
}
|
||||||
|
.coord-pack-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 24px;
|
||||||
|
padding: 2px 28px 2px 8px;
|
||||||
|
}
|
||||||
|
.coord-pack-meta {
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
.coord-pack--pending .coord-pack-meta {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
.coord-pack > .coord-line-menu {
|
||||||
|
top: 4px;
|
||||||
|
right: 4px;
|
||||||
|
}
|
||||||
|
.coord-pack:hover > .coord-line-menu,
|
||||||
|
.coord-pack:focus-within > .coord-line-menu {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.coord-pack .coord-line {
|
||||||
|
background: transparent;
|
||||||
|
padding: 6px 8px;
|
||||||
|
}
|
||||||
|
.coord-pack .coord-line:hover {
|
||||||
|
background: color-mix(in srgb, var(--accent-soft) 35%, transparent);
|
||||||
|
}
|
||||||
.coord-line {
|
.coord-line {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -565,6 +614,10 @@ body:not(.coord-rail-collapsed) .coord-rail-toggle {
|
|||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
.coord-context-request {
|
||||||
|
color: var(--text);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
.coord-trace-meta {
|
.coord-trace-meta {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@@ -596,6 +649,14 @@ body.design-workspace .workspace-pending {
|
|||||||
margin-bottom: 6px;
|
margin-bottom: 6px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
.workspace-pending-head .msg-action {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
.msg-live-empty {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
body.is-reviewing .message-feed {
|
body.is-reviewing .message-feed {
|
||||||
flex: 0 1 auto;
|
flex: 0 1 auto;
|
||||||
@@ -752,6 +813,33 @@ body.design-workspace[data-design-surface="answer"] .questions-card-host {
|
|||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
/* 答题/验收工作面已是 surface,询问不再套圆角卡片 */
|
||||||
|
body.design-workspace[data-design-surface="answer"] .questions-card-host.is-open:not([hidden]),
|
||||||
|
.workspace-review-ask .questions-card-host.is-open:not([hidden]) {
|
||||||
|
margin: 0;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0;
|
||||||
|
background: transparent;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
body.design-workspace[data-design-surface="answer"] .qcard-body,
|
||||||
|
.workspace-review-ask .qcard-body {
|
||||||
|
padding-left: 0;
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
body.design-workspace[data-design-surface="answer"] .qcard-top,
|
||||||
|
.workspace-review-ask .qcard-top,
|
||||||
|
body.design-workspace[data-design-surface="answer"] .questions-card-host.is-collapsed .qcard-top,
|
||||||
|
.workspace-review-ask .questions-card-host.is-collapsed .qcard-top {
|
||||||
|
padding-left: 0;
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
body.design-workspace[data-design-surface="answer"] .qcard-skip,
|
||||||
|
.workspace-review-ask .qcard-skip,
|
||||||
|
body.design-workspace[data-design-surface="answer"] .qcard-pager,
|
||||||
|
.workspace-review-ask .qcard-pager {
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
body.design-workspace[data-design-surface="speak"] .workspace-surface-kicker {
|
body.design-workspace[data-design-surface="speak"] .workspace-surface-kicker {
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
}
|
}
|
||||||
@@ -766,25 +854,23 @@ body.design-workspace[data-design-surface="review"][data-review-kind="opening"]
|
|||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 挂载追问嵌在产物验收面下方,与产物同一工作面 */
|
/* 挂载追问嵌在产物验收面下方:与产物同宽,随主柱动态适应 */
|
||||||
.workspace-review-ask {
|
.workspace-review-ask {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
max-width: var(--workspace-max, 920px);
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
align-self: center;
|
max-width: none;
|
||||||
padding: 0 var(--workspace-pad-x, 24px) var(--workspace-pad-y, 16px);
|
align-self: stretch;
|
||||||
|
padding: 8px var(--workspace-pad-x, 24px) var(--workspace-pad-y, 16px);
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
border-top: 1px solid color-mix(in srgb, var(--border) 70%, transparent);
|
||||||
}
|
}
|
||||||
.workspace-review-ask .questions-card-host {
|
.workspace-review-ask .questions-card-host {
|
||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
padding-right: 0;
|
padding-right: 0;
|
||||||
border-top: none;
|
border-top: none;
|
||||||
}
|
}
|
||||||
.workspace-review-ask .questions-card-host.is-open:not([hidden]) {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.design-workspace .ws-creation-flow h4 { display: none; }
|
body.design-workspace .ws-creation-flow h4 { display: none; }
|
||||||
body.design-workspace .flow-brief {
|
body.design-workspace .flow-brief {
|
||||||
@@ -1058,6 +1144,32 @@ body.design-workspace .flow-step[data-run="done"] .flow-name {
|
|||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
.explorer-row.has-twist {
|
||||||
|
grid-template-columns: 16px minmax(0, 1fr) auto;
|
||||||
|
}
|
||||||
|
.explorer-twist {
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: 1 / span 2;
|
||||||
|
align-self: center;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 10px;
|
||||||
|
line-height: 1;
|
||||||
|
color: var(--muted);
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
.explorer-row.has-twist .explorer-main { grid-column: 2; }
|
||||||
|
.explorer-row.has-twist .explorer-del { grid-column: 3; }
|
||||||
|
.explorer-row.child {
|
||||||
|
padding-left: 28px;
|
||||||
|
min-height: 28px;
|
||||||
|
padding-top: 5px;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
}
|
||||||
|
.explorer-row.child .explorer-name { font-size: 12px; font-weight: 400; }
|
||||||
.explorer-row:hover { background: var(--bg); }
|
.explorer-row:hover { background: var(--bg); }
|
||||||
.explorer-row.active { background: var(--accent-soft); }
|
.explorer-row.active { background: var(--accent-soft); }
|
||||||
.explorer-row.active .explorer-name { color: var(--accent); font-weight: 600; }
|
.explorer-row.active .explorer-name { color: var(--accent); font-weight: 600; }
|
||||||
@@ -1119,7 +1231,7 @@ body.design-workspace .flow-step[data-run="done"] .flow-name {
|
|||||||
color: var(--muted); letter-spacing: 0.04em;
|
color: var(--muted); letter-spacing: 0.04em;
|
||||||
}
|
}
|
||||||
.explorer-hint {
|
.explorer-hint {
|
||||||
margin: 4px 8px; font-size: 11px; color: var(--muted); line-height: 1.4;
|
margin: 4px 8px 4px 28px; font-size: 11px; color: var(--muted); line-height: 1.4;
|
||||||
}
|
}
|
||||||
.sidebar-empty { padding: 12px; color: var(--muted); font-size: 13px; text-align: center; }
|
.sidebar-empty { padding: 12px; color: var(--muted); font-size: 13px; text-align: center; }
|
||||||
|
|
||||||
@@ -1214,6 +1326,9 @@ body.rail-collapsed .message-feed {
|
|||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
.msg-head [data-act="retry_run"] {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
.msg-foot {
|
.msg-foot {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
@@ -1339,6 +1454,41 @@ body.rail-collapsed .message-feed {
|
|||||||
max-height: 240px;
|
max-height: 240px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
.msg-play-process-row {
|
||||||
|
margin: 4px 0;
|
||||||
|
}
|
||||||
|
.msg-play-process {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--muted);
|
||||||
|
border: 1px dashed var(--border);
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #fafafa;
|
||||||
|
}
|
||||||
|
.msg-play-process > summary {
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
user-select: none;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
.msg-play-process > summary::-webkit-details-marker { display: none; }
|
||||||
|
.msg-play-process > summary::before {
|
||||||
|
content: "▸";
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
.msg-play-process[open] > summary::before { content: "▾"; }
|
||||||
|
.msg-play-process .msg-tag {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
.msg-play-process .msg-body {
|
||||||
|
padding: 0 10px 10px;
|
||||||
|
max-height: 320px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.msg-live-indicator {
|
.msg-live-indicator {
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
@@ -1519,7 +1669,7 @@ body.rail-collapsed .message-feed {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
right: 0;
|
right: 0;
|
||||||
top: calc(100% + 4px);
|
top: calc(100% + 4px);
|
||||||
z-index: 40;
|
z-index: 80;
|
||||||
min-width: 132px;
|
min-width: 132px;
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
@@ -1530,13 +1680,28 @@ body.rail-collapsed .message-feed {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1px;
|
gap: 1px;
|
||||||
}
|
}
|
||||||
|
.composer .more-menu-panel {
|
||||||
|
top: auto;
|
||||||
|
bottom: calc(100% + 4px);
|
||||||
|
}
|
||||||
.more-menu-item {
|
.more-menu-item {
|
||||||
display: block;
|
display: block;
|
||||||
|
width: 100%;
|
||||||
padding: 7px 10px;
|
padding: 7px 10px;
|
||||||
|
border: none;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
|
background: transparent;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
text-align: left;
|
||||||
|
font: inherit;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.more-menu-item[hidden] { display: none !important; }
|
||||||
|
.more-menu-item:disabled {
|
||||||
|
opacity: 0.45;
|
||||||
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
.more-menu-item:hover {
|
.more-menu-item:hover {
|
||||||
background: var(--bg);
|
background: var(--bg);
|
||||||
@@ -1567,27 +1732,50 @@ body.rail-collapsed .message-feed {
|
|||||||
.msg-q-list li { margin: 0.35rem 0; }
|
.msg-q-list li { margin: 0.35rem 0; }
|
||||||
.msg-q-lead { margin: 0; line-height: 1.55; font-size: 14px; }
|
.msg-q-lead { margin: 0; line-height: 1.55; font-size: 14px; }
|
||||||
|
|
||||||
/* 游玩:阅读柱略抬权重 */
|
/* 游玩:每轮终稿仍是气泡(方便扫上下文),正文在气泡里撑高 */
|
||||||
body[data-lifecycle="play"] .message-feed {
|
body[data-lifecycle="play"] .message-feed {
|
||||||
padding: 14px 18px 18px;
|
padding: 16px 18px 22px;
|
||||||
gap: 14px;
|
gap: 16px;
|
||||||
}
|
}
|
||||||
body[data-lifecycle="play"] .msg-assistant-row .msg-bubble {
|
body[data-lifecycle="play"] .msg-assistant-row .msg-bubble {
|
||||||
width: min(100%, 44rem);
|
width: min(100%, 44rem);
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
border-color: color-mix(in srgb, var(--border) 70%, transparent);
|
border-color: color-mix(in srgb, var(--border) 70%, transparent);
|
||||||
box-shadow: none;
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
|
||||||
}
|
}
|
||||||
body[data-lifecycle="play"] .msg-assistant-row .msg-head {
|
body[data-lifecycle="play"] .msg-assistant-row .msg-head {
|
||||||
padding-top: 6px;
|
padding-top: 6px;
|
||||||
}
|
}
|
||||||
body[data-lifecycle="play"] .present-shell {
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
body[data-lifecycle="play"] .msg-user-row .msg-bubble {
|
body[data-lifecycle="play"] .msg-user-row .msg-bubble {
|
||||||
width: min(100%, 28rem);
|
width: min(100%, 28rem);
|
||||||
max-width: 90%;
|
max-width: 90%;
|
||||||
}
|
}
|
||||||
|
body[data-lifecycle="play"] .msg-play-final .msg-bubble {
|
||||||
|
width: min(100%, 46rem);
|
||||||
|
}
|
||||||
|
body[data-lifecycle="play"] .msg-play-final .present-shell {
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
background: transparent;
|
||||||
|
padding: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
body[data-lifecycle="play"] .msg-play-final .present-shell-badge {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
body[data-lifecycle="play"] .msg-play-final .present-stage .present-region--body .present-region-body,
|
||||||
|
body[data-lifecycle="play"] .msg-play-final .present-stage.present-region--body .present-region-body {
|
||||||
|
font-size: 18.5px;
|
||||||
|
line-height: 1.88;
|
||||||
|
max-width: none;
|
||||||
|
max-height: none;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
body[data-lifecycle="play"] .msg-play-final .present-shell--chat_monitor .present-stage {
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
/* 产物紧凑卡:横向主轴,预览一行 */
|
/* 产物紧凑卡:横向主轴,预览一行 */
|
||||||
.artifact-compact-list {
|
.artifact-compact-list {
|
||||||
@@ -2455,7 +2643,8 @@ body.design-workspace .af-mosaic {
|
|||||||
color: var(--text);
|
color: var(--text);
|
||||||
line-height: 1.45;
|
line-height: 1.45;
|
||||||
}
|
}
|
||||||
.propose-sync-hint {
|
.propose-sync-hint,
|
||||||
|
.propose-plan-hint {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
@@ -2544,6 +2733,9 @@ body.design-workspace .af-mosaic {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
|
.flow-occ-revise {
|
||||||
|
color: var(--text, #222);
|
||||||
|
}
|
||||||
.flow-order {
|
.flow-order {
|
||||||
grid-row: 1 / -1;
|
grid-row: 1 / -1;
|
||||||
align-self: center;
|
align-self: center;
|
||||||
@@ -2685,10 +2877,9 @@ body.design-workspace .af-mosaic {
|
|||||||
box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent, #2a6) 20%, transparent);
|
box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent, #2a6) 20%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 询问卡:host 本身即卡片(不再套内层 .qcard) */
|
/* 询问卡:对话流里 host 即卡片;创作工作面上剥壳,见上方 flatten 规则 */
|
||||||
.questions-card-host {
|
.questions-card-host {
|
||||||
--qcard-max: 48vh;
|
--qcard-max: 48vh;
|
||||||
--qcard-max-expanded: 82vh;
|
|
||||||
flex: 0 1 auto;
|
flex: 0 1 auto;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
@@ -2710,16 +2901,17 @@ body.design-workspace .af-mosaic {
|
|||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
background: var(--surface, #fff);
|
background: var(--surface, #fff);
|
||||||
transition: max-height 0.22s ease;
|
transition: max-height 0.18s ease;
|
||||||
}
|
}
|
||||||
.questions-card-host.is-open.is-expanded:not([hidden]) {
|
.questions-card-host.is-open.is-collapsed:not([hidden]) {
|
||||||
max-height: var(--qcard-max-expanded);
|
max-height: none;
|
||||||
}
|
}
|
||||||
.qcard-top {
|
.qcard-top {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
gap: 10px;
|
||||||
min-height: 18px;
|
min-height: 18px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 4px 12px 0;
|
padding: 4px 12px 0;
|
||||||
@@ -2734,18 +2926,40 @@ body.design-workspace .af-mosaic {
|
|||||||
.qcard-top-grip {
|
.qcard-top-grip {
|
||||||
width: 28px;
|
width: 28px;
|
||||||
height: 3px;
|
height: 3px;
|
||||||
|
flex-shrink: 0;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
background: color-mix(in srgb, var(--muted, #888) 35%, transparent);
|
background: color-mix(in srgb, var(--muted, #888) 35%, transparent);
|
||||||
}
|
}
|
||||||
.questions-card-host.is-expanded .qcard-top {
|
.qcard-top:hover .qcard-top-grip {
|
||||||
min-height: 22px;
|
|
||||||
padding-top: 6px;
|
|
||||||
}
|
|
||||||
.questions-card-host.is-expanded .qcard-top:hover .qcard-top-grip {
|
|
||||||
background: color-mix(in srgb, var(--muted, #888) 55%, transparent);
|
background: color-mix(in srgb, var(--muted, #888) 55%, transparent);
|
||||||
}
|
}
|
||||||
.questions-card-host:not(.is-expanded) .qcard-body {
|
.qcard-collapsed-meta {
|
||||||
cursor: pointer;
|
display: none;
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 550;
|
||||||
|
line-height: 1.35;
|
||||||
|
color: var(--text);
|
||||||
|
text-align: left;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.questions-card-host.is-collapsed .qcard-top {
|
||||||
|
min-height: 44px;
|
||||||
|
padding: 0 12px;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
.questions-card-host.is-collapsed .qcard-top:hover .qcard-top-grip {
|
||||||
|
background: color-mix(in srgb, var(--muted, #888) 55%, transparent);
|
||||||
|
}
|
||||||
|
.questions-card-host.is-collapsed .qcard-collapsed-meta {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.questions-card-host.is-collapsed .qcard-body,
|
||||||
|
.questions-card-host.is-collapsed .qcard-float {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
/* 悬浮控件层:不占文档流高度 */
|
/* 悬浮控件层:不占文档流高度 */
|
||||||
.qcard-float {
|
.qcard-float {
|
||||||
@@ -3472,6 +3686,7 @@ body.design-workspace .af-mosaic {
|
|||||||
border: none; background: transparent; padding: 8px 12px;
|
border: none; background: transparent; padding: 8px 12px;
|
||||||
font-size: 13px; cursor: pointer; border-radius: 4px;
|
font-size: 13px; cursor: pointer; border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
.context-menu-item[hidden] { display: none; }
|
||||||
.context-menu-item:hover { background: var(--bg); }
|
.context-menu-item:hover { background: var(--bg); }
|
||||||
.context-menu-separator-before { border-top: 1px solid var(--border); }
|
.context-menu-separator-before { border-top: 1px solid var(--border); }
|
||||||
.context-menu-danger { color: var(--danger); }
|
.context-menu-danger { color: var(--danger); }
|
||||||
@@ -3491,18 +3706,46 @@ body.design-workspace .af-mosaic {
|
|||||||
.timeline-item { padding: 6px 0; border-bottom: 1px solid var(--border); }
|
.timeline-item { padding: 6px 0; border-bottom: 1px solid var(--border); }
|
||||||
.timeline-empty { color: var(--muted); }
|
.timeline-empty { color: var(--muted); }
|
||||||
|
|
||||||
/* Composer — 单层输入壳;底栏本身不再套第二层卡片 */
|
/* Composer — 单行:状态 / 输入 / 更多;菜单向上弹出不被裁切 */
|
||||||
.composer {
|
.composer {
|
||||||
border-top: 1px solid var(--border);
|
border-top: 1px solid var(--border);
|
||||||
padding: 10px var(--workspace-pad-x, 16px) 12px;
|
padding: 6px var(--workspace-pad-x, 16px) 8px;
|
||||||
min-height: var(--composer-min);
|
min-height: var(--composer-min);
|
||||||
max-height: min(50vh, 420px);
|
overflow: visible;
|
||||||
overflow: hidden;
|
|
||||||
background: var(--bg);
|
background: var(--bg);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
position: relative;
|
||||||
|
z-index: 20;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: row;
|
||||||
justify-content: flex-end;
|
align-items: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.composer > .status {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
align-self: center;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.composer:has(.composer-mode-chip) > .status {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
#composer-main {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
|
max-height: min(50vh, 420px);
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
.composer > .more-menu {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
align-self: flex-end;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
.composer .more-menu-sum {
|
||||||
|
height: calc(var(--composer-control-h) - 8px);
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
/* 唯一描边:输入 + 操作同框 */
|
/* 唯一描边:输入 + 操作同框 */
|
||||||
.composer-input-shell {
|
.composer-input-shell {
|
||||||
@@ -3584,7 +3827,7 @@ body.design-workspace .af-mosaic {
|
|||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
padding: 8px 4px;
|
padding: 6px 4px;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
@@ -3603,7 +3846,16 @@ body.design-workspace .af-mosaic {
|
|||||||
margin-right: 2px;
|
margin-right: 2px;
|
||||||
}
|
}
|
||||||
.composer-waiting, .composer-idle {
|
.composer-waiting, .composer-idle {
|
||||||
padding: 12px; text-align: center; color: var(--muted); font-size: 13px;
|
padding: 6px 0; text-align: left; color: var(--muted); font-size: 13px;
|
||||||
|
}
|
||||||
|
.composer-waiting {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.composer-waiting .btn {
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
.composer-hint { margin: 0 0 8px; font-size: 12px; color: var(--muted); }
|
.composer-hint { margin: 0 0 8px; font-size: 12px; color: var(--muted); }
|
||||||
.composer-form {
|
.composer-form {
|
||||||
@@ -3656,7 +3908,7 @@ body.design-workspace .af-mosaic {
|
|||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
padding: 8px 4px;
|
padding: 6px 4px;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
field-sizing: content;
|
field-sizing: content;
|
||||||
@@ -3666,7 +3918,7 @@ body.design-workspace .af-mosaic {
|
|||||||
border: none;
|
border: none;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
.composer-actions { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 8px; }
|
.composer-actions { display: flex; gap: 8px; flex-wrap: wrap; margin: 0; }
|
||||||
.composer-actions .btn-primary { flex: 1; }
|
.composer-actions .btn-primary { flex: 1; }
|
||||||
|
|
||||||
/* Intake (from intake-ui.js) */
|
/* Intake (from intake-ui.js) */
|
||||||
@@ -3729,7 +3981,7 @@ body.design-workspace .af-mosaic {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* —— 游玩呈现壳 present.v1 ——
|
/* —— 游玩呈现壳 present.v1 ——
|
||||||
* 主工作面 = body(随 600~3000 字撑高)。
|
* 主工作面 = body(随 500~2000 字撑高)。
|
||||||
* 视觉:少盒子、少渐变;层级靠字号/间距/对比,不靠装饰抢注意力。
|
* 视觉:少盒子、少渐变;层级靠字号/间距/对比,不靠装饰抢注意力。
|
||||||
*/
|
*/
|
||||||
.present-shell {
|
.present-shell {
|
||||||
@@ -3793,7 +4045,7 @@ body.design-workspace .af-mosaic {
|
|||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
/* P0:正文舞台 —— 随字数伸展(600~3000 靠外层滚动,不塞进小框) */
|
/* P0:正文舞台 —— 随字数伸展(500~2000 靠外层滚动,不塞进小框) */
|
||||||
.present-stage {
|
.present-stage {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
@@ -4085,3 +4337,37 @@ body.design-workspace .af-mosaic {
|
|||||||
color: #e7e5e4;
|
color: #e7e5e4;
|
||||||
background: #3f3f46;
|
background: #3f3f46;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.opening-setup-body {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.opening-picker {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.opening-picker-nav {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.opening-picker-btn {
|
||||||
|
font: inherit;
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background: var(--bg-elev, transparent);
|
||||||
|
color: inherit;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.opening-picker-btn:hover {
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
.opening-picker-pos {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--muted, #888);
|
||||||
|
min-width: 4.5em;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user