Steer generation rules toward direction pools and readable fills.
Direction-first pools, recipes, and grade manifestation replace enum-tag stuffing; spawned products can be deleted and in-flight runs aborted. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -58,7 +58,7 @@
|
|||||||
**存盘**:三块都在同一 `正文` 里。**注入**:默认 `full` 时整份正文都会进模型——若日后只要左栏进游玩,再改投影,不靠藏 UI。
|
**存盘**:三块都在同一 `正文` 里。**注入**:默认 `full` 时整份正文都会进模型——若日后只要左栏进游玩,再改投影,不靠藏 UI。
|
||||||
实现机制正文:`依据的核心体验` + `支撑点[]` + `支撑点关系[]` + `覆盖检验`(见 `mechanism`)。
|
实现机制正文:`依据的核心体验` + `支撑点[]` + `支撑点关系[]` + `覆盖检验`(见 `mechanism`)。
|
||||||
舞台骨架正文:尺度 / 基底与变造 / **社会结构** / **世界状况** / 关键舞台区 / 未展开范围(见 `world-blueprint`)。
|
舞台骨架正文:尺度 / 基底与变造 / **社会结构** / **世界状况** / 关键舞台区 / 未展开范围(见 `world-blueprint`)。
|
||||||
生成规则正文:必要性判断 + `rules[]`(生成与描写 + **单层**产物格式 + 可选多池绑顶层字段 + **数量**与**一次生成**(单条|同批,同批须钉记录间关系));自评=必要性/属性妥当/格式准确。
|
生成规则正文:必要性判断 + `rules[]`(设计判断 + 生成与描写含**配方/表现** + **分级表现** + **单层**产物格式 + 可选池:**方向池**以方向概括为主、例子极少,**元素池**仅封闭集合,旁观抽样只抽元素池 + **数量**与**一次生成**);自评=必要性/属性妥当/材料得当/格式准确。
|
||||||
美学纲领自评仍为:交互范式 / 美学纲领 / 整体协调(勿与生成规则三维混用)。
|
美学纲领自评仍为:交互范式 / 美学纲领 / 整体协调(勿与生成规则三维混用)。
|
||||||
叙事指南与故事推进正文:一份全文(纲领/遣词/笔墨/禁忌/推进等);**整份**双挂转述与主世界层,不为省 token 拆两套投影。artifact=`设计.叙事指南与故事推进`。
|
叙事指南与故事推进正文:一份全文(纲领/遣词/笔墨/禁忌/推进等);**整份**双挂转述与主世界层,不为省 token 拆两套投影。artifact=`设计.叙事指南与故事推进`。
|
||||||
回复呈现:每轮终稿给用户看什么(正文/字数/日期/变量);artifact=`设计.监控栏`(旧称设计监控栏/状态栏)。
|
回复呈现:每轮终稿给用户看什么(正文/字数/日期/变量);artifact=`设计.监控栏`(旧称设计监控栏/状态栏)。
|
||||||
@@ -144,7 +144,7 @@ API:`POST /api/sessions/:id/context-order`(`move` / `set_projection` / `set_
|
|||||||
|--------|----------|
|
|--------|----------|
|
||||||
| `context-fragment.v1` 外壳 | brief / 挂载 / **自评十分制(x/10)** / 追问 |
|
| `context-fragment.v1` 外壳 | brief / 挂载 / **自评十分制(x/10)** / 追问 |
|
||||||
| 美学纲领与交互范式 | mosaic(设定逻辑 / 交互范式 / 美学纲领) |
|
| 美学纲领与交互范式 | mosaic(设定逻辑 / 交互范式 / 美学纲领) |
|
||||||
| 生成规则 | 必要性 + 规则卡(方法、单层字段表、池) |
|
| 生成规则 | 必要性 + 规则卡(配方/表现、分级表象、单层字段表、方向池/元素池) |
|
||||||
| 舞台骨架 | 尺度 / 社会 / 状况 / 关键区实体卡 |
|
| 舞台骨架 | 尺度 / 社会 / 状况 / 关键区实体卡 |
|
||||||
| 实现机制 | 支撑点卡 + 关系 + 覆盖检验 |
|
| 实现机制 | 支撑点卡 + 关系 + 覆盖检验 |
|
||||||
| 其它技能正文 | 通用结构化(KV);**应补专用卡** |
|
| 其它技能正文 | 通用结构化(KV);**应补专用卡** |
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
| 块 id | 必填 | 用途 |
|
| 块 id | 必填 | 用途 |
|
||||||
|-------|------|------|
|
|-------|------|------|
|
||||||
| `meta` | 建议 | YAML:name / id / artifact / declaration / when / when_not / boundary |
|
| `meta` | 建议 | YAML:name / id / artifact / declaration / when / when_not / boundary |
|
||||||
| `opening` | 可选 | **默认问题**正文。开局:配方 `steps` 已预置进「设计.创作流程」,用户首句即坐在 DAG 第一步;若「用户.需求」已有内容,程序**跳过**再抛 opening,直接进本步 LLM |
|
| `opening` | 可选 | **默认问题**正文。点进节点时由程序发给用户(不经本步 LLM),用户先答一轮再产出。整块可省略 = 本步直接 LLM |
|
||||||
| `task` | 是 | 本步任务与验收边界 |
|
| `task` | 是 | 本步任务与验收边界 |
|
||||||
| `principles` | 建议 | 原则 |
|
| `principles` | 建议 | 原则 |
|
||||||
| `probe` | 建议 | 追问策略 |
|
| `probe` | 建议 | 追问策略 |
|
||||||
@@ -173,7 +173,7 @@ 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`;宽原则 + 严合同键;可反复;**〔先验产物〕**;规划字段 `target`;挂 gm+auditor;**专用卡已接** |
|
| 生成规则 | `generation-rules` | **已写**:`context-fragment.v1`;方向池优先、元素池仅封闭集合;可反复;**〔先验产物〕**;规划字段 `target`;挂 gm+auditor;**专用卡已接** |
|
||||||
| 具体实例 | `concrete-instances` | **已写**:`context-fragment.v1`;只按规则执行;可反复;**〔先验产物〕**;规划字段 `rule_id` |
|
| 具体实例 | `concrete-instances` | **已写**:`context-fragment.v1`;只按规则执行;可反复;**〔先验产物〕**;规划字段 `rule_id` |
|
||||||
| 叙事指南与故事推进 | `narrative` | **已写**:一份全文双挂;**不**为省 token 拆投影;结构化卡 |
|
| 叙事指南与故事推进 | `narrative` | **已写**:一份全文双挂;**不**为省 token 拆投影;结构化卡 |
|
||||||
| 拓扑图谱 | `topology` | 必须生成且不适合走生成规则→实例时,用拓扑结构写出(升级路径、地图、人物关系等) |
|
| 拓扑图谱 | `topology` | 必须生成且不适合走生成规则→实例时,用拓扑结构写出(升级路径、地图、人物关系等) |
|
||||||
|
|||||||
@@ -57,8 +57,7 @@ feeds: design_only
|
|||||||
一次 design-step、一场对话、一次验收。参与方式与美学内容在同一步内交叉追问,不要先交「半份交互」再开「半份美学」。
|
一次 design-step、一场对话、一次验收。参与方式与美学内容在同一步内交叉追问,不要先交「半份交互」再开「半份美学」。
|
||||||
用户已答过追问:把答复写入正文三块,`追问.题目` 优先 `[]`;禁止在「交互范式」与「美学纲领」之间轮流补洞拖验收。核心体验与权限边界够用即可收。
|
用户已答过追问:把答复写入正文三块,`追问.题目` 优先 `[]`;禁止在「交互范式」与「美学纲领」之间轮流补洞拖验收。核心体验与权限边界够用即可收。
|
||||||
|
|
||||||
若程序已发出默认问题:用户首答在「用户.worker答复」,开场白在「创作.能力开场白」。禁止重复同一开场;在首答基础上补洞、追问、产出。
|
若程序已发出默认问题:用户首答在「用户.worker答复」,开场白在「创作.能力开场白」。禁止重复同一开场;在首答基础上补洞、追问、产出。黑板「用户.需求」里已有的句子当作补充材料采纳,不代替本步引导作答。
|
||||||
开局通常已由配方写入「设计.创作流程」第一步;若「用户.需求」已有首句:程序会跳过默认问题——首句即本步对话,只补真正缺口。
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## principles
|
## principles
|
||||||
@@ -79,7 +78,7 @@ feeds: design_only
|
|||||||
```probe
|
```probe
|
||||||
本步只钉三件:站位(代入 / 镜头看哪 / 爽从哪来)、核心感觉(反复感受到什么)、轮转边界(何时等用户)。
|
本步只钉三件:站位(代入 / 镜头看哪 / 爽从哪来)、核心感觉(反复感受到什么)、轮转边界(何时等用户)。
|
||||||
|
|
||||||
用户首句或黑板已有的直接采纳。默认问题已覆盖变造、代入、核心感觉;开场若已跳过,把「用户.需求」首句当答复。已答清的不重问。
|
用户首句或黑板已有的直接采纳。默认问题已覆盖变造、代入、核心感觉。已答清的不重问。
|
||||||
|
|
||||||
缺其中一件:在产物「追问」写 1~2 点(建议选项 + 短场景);顶层 askUser 为 null。三件够验收:`追问.题目` 为 `[]`,内容维度等其余键写「未定」或省略。
|
缺其中一件:在产物「追问」写 1~2 点(建议选项 + 短场景);顶层 askUser 为 null。三件够验收:`追问.题目` 为 `[]`,内容维度等其余键写「未定」或省略。
|
||||||
|
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ modules:
|
|||||||
repeatable: true
|
repeatable: true
|
||||||
declaration: >-
|
declaration: >-
|
||||||
要精准格式,或本局原创导致知识中无可参考对象、或难以在无指导情况下生成足够可信内容且故事里还要生成时:写出该对象族怎么生成;
|
要精准格式,或本局原创导致知识中无可参考对象、或难以在无指导情况下生成足够可信内容且故事里还要生成时:写出该对象族怎么生成;
|
||||||
不填最终实例;可按对象反复
|
创意字段用方向池,不堆枚举词;不填最终实例;可按对象反复
|
||||||
artifact: 设计.生成规则
|
artifact: 设计.生成规则
|
||||||
params:
|
params:
|
||||||
- key: target
|
- key: target
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ when_not: |
|
|||||||
规则是 `runtime_only` → 不排,游玩期再生成。
|
规则是 `runtime_only` → 不排,游玩期再生成。
|
||||||
只要规则不要预写实例 → 不排。
|
只要规则不要预写实例 → 不排。
|
||||||
boundary: |
|
boundary: |
|
||||||
本技能只做一件事:按指定规则生成 records。规则说怎么写、什么格式、如何用池,就照做。
|
本技能只做一件事:按指定规则生成 records。规则说怎么写、什么格式、如何用池、填到多厚,就照做。
|
||||||
生成规则:合同制定方——本步不重论证、不修改。
|
生成规则:合同制定方——本步不重论证、不修改。
|
||||||
feeds: gm
|
feeds: gm
|
||||||
```
|
```
|
||||||
@@ -49,8 +49,14 @@ feeds: gm
|
|||||||
- `单条`:本批 `records` 恰好 1 条;规划 `count` 即使写成多条也忽略。
|
- `单条`:本批 `records` 恰好 1 条;规划 `count` 即使写成多条也忽略。
|
||||||
- `同批`:一次交齐互有关系的多条。条数:规划 `count` 有数字用数字,否则用规则「数量」。生成时先对齐「一次生成.关系」(各条如何互指、对立、同场),再填各条字段。
|
- `同批`:一次交齐互有关系的多条。条数:规划 `count` 有数字用数字,否则用规则「数量」。生成时先对齐「一次生成.关系」(各条如何互指、对立、同场),再填各条字段。
|
||||||
`batch_goal` 只作不违反规则的筛选。缺「一次生成」时按「数量」/ `count` 交 `records[]`(可多条)。
|
`batch_goal` 只作不违反规则的筛选。缺「一次生成」时按「数量」/ `count` 交 `records[]`(可多条)。
|
||||||
3. 完全按规则的「生成与描写」+「产物格式」+「池」生成 `records`(始终是数组,单条也是长度为 1 的数组;键=schema 顶层字段)。
|
3. 完全按规则的「生成与描写」+「产物格式」+「池」+「分级表现」生成 `records`(始终是数组,单条也是长度为 1 的数组;键=schema 顶层字段)。
|
||||||
4. 按规则自检;不合格内部重做。已有产物则追加批次(除非用户要求替换)。
|
池的用法以规则 `池型` 为准:
|
||||||
|
- **方向**:以方向为主。粒度是词条/方向时,字段值可以就是方向名。粒度是句子时,按该方向写开(方向名可作标签,后面仍要达到地板)。方向说明里的专名不是选项库,不要按专名点菜。
|
||||||
|
- **元素**:从 `条目` 里抽取或必选,写入绑定字段。
|
||||||
|
有 **配方**:按配方从各组件取方向,拼进目标字段那一格。
|
||||||
|
有 **分级表现**:实例只填档位短值;不要把整段表象复制进 record,除非 schema 单独设了表象字段。叙事时按规则展开。
|
||||||
|
无池的描写字段按 `粒度`/`地板`/`表现`/`工作流` 写到够厚。
|
||||||
|
4. 按规则自检(含地板:句子级不得只用一个词交差);不合格内部重做。已有产物则追加批次(除非用户要求替换)。
|
||||||
5. 输出 JSON;追问通常 `[]`。
|
5. 输出 JSON;追问通常 `[]`。
|
||||||
|
|
||||||
summary:`具体实例 · {对象} · {n}条`
|
summary:`具体实例 · {对象} · {n}条`
|
||||||
@@ -59,10 +65,11 @@ summary:`具体实例 · {对象} · {n}条`
|
|||||||
## principles
|
## principles
|
||||||
|
|
||||||
```principles
|
```principles
|
||||||
1. 只按规则来:方法、格式、池、数量、一次生成、约束——规则有什么用什么;没有的不发明。
|
1. 只按规则来:方法、配方、表现、分级表现、格式、池型、数量、一次生成、约束、地板——规则有什么用什么;没有的不发明。
|
||||||
2. 同批先钉记录间关系再填各条;单条不虚构同场关系。始终交 `records[]`。
|
2. 同批先钉记录间关系再填各条;单条不虚构同场关系。始终交 `records[]`。
|
||||||
3. 只交 records:单层 JSON;不交解释、草稿、schema 副本、整池粘贴。
|
3. 只交 records:单层 JSON;不交解释、草稿、schema 副本、整池粘贴。方向以方向为主;说明里的专名不是选项。
|
||||||
4. 要改合同 → 回「生成规则」,本步不加字段、不改枚举。
|
4. 句子级描写必须达到该字段 `地板`;词条/档位/数字可以短。句子级不要只丢一个词交差;方向名可以当选项或标签。
|
||||||
|
5. 要改合同 → 回「生成规则」,本步不加字段、不改枚举、不改池型。
|
||||||
```
|
```
|
||||||
|
|
||||||
## probe
|
## probe
|
||||||
@@ -98,7 +105,7 @@ summary:`具体实例 · {对象} · {n}条`
|
|||||||
{
|
{
|
||||||
"名": "合规模",
|
"名": "合规模",
|
||||||
"分数": 8,
|
"分数": 8,
|
||||||
"说明": "是否严格按该规则的方法、单层格式、池与约束生成"
|
"说明": "是否按方法/配方/表现/单层格式/池型(方向为主,元素=抽取)/分级表现/地板生成;句子级是否达到粒度"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"薄弱点": "一句话;无则空字符串"
|
"薄弱点": "一句话;无则空字符串"
|
||||||
@@ -123,7 +130,7 @@ summary:`具体实例 · {对象} · {n}条`
|
|||||||
```checklist
|
```checklist
|
||||||
- [ ] 按本步认定的 rule_id(规划或步内钉)的预生成规则执行?
|
- [ ] 按本步认定的 rule_id(规划或步内钉)的预生成规则执行?
|
||||||
- [ ] 一次生成已遵守(单条=1 条;同批=多条且关系对齐)?records 始终是数组?
|
- [ ] 一次生成已遵守(单条=1 条;同批=多条且关系对齐)?records 始终是数组?
|
||||||
- [ ] records 单层、键值符合产物格式与全部约束?
|
- [ ] records 单层、键值符合产物格式与全部约束?句子级是否达到地板?有配方是否组装进目标字段?方向是否以方向为主(专名不当选项库)?
|
||||||
- [ ] 自评为合规模?未改合同?
|
- [ ] 自评为合规模?未改合同?
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -132,6 +139,9 @@ summary:`具体实例 · {对象} · {n}条`
|
|||||||
```examples
|
```examples
|
||||||
好:规则一次生成=同批、关系=开局仇敌二人 → 本批 2 条,对立在两条里对齐。
|
好:规则一次生成=同批、关系=开局仇敌二人 → 本批 2 条,对立在两条里对齐。
|
||||||
好:规则一次生成=单条 → 本批 1 条;还要更多就再点原型。
|
好:规则一次生成=单条 → 本批 1 条;还要更多就再点原型。
|
||||||
|
好:方向「冷焰」作标签,能力描述按该方向写开到可感知;或粒度=方向时字段值就是「冷焰」。
|
||||||
|
好:阶层字段填「金丹」,表象留在规则的分级表现里,不把整段对照复制进每条 record。
|
||||||
坏:同批规则却只交 1 条,或分多步把有关系的人拆开写。
|
坏:同批规则却只交 1 条,或分多步把有关系的人拆开写。
|
||||||
坏:加规则没有的字段;输出一段「我是怎么想的」却没有 records;本步改 schema。
|
坏:加规则没有的字段;输出一段「我是怎么想的」却没有 records;本步改 schema。
|
||||||
|
坏:句子级只填「阴郁」;或按方向说明里那个专名点菜交差。
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
> 技能文档。程序只切割下方 **fence 块**;`##` 标题仅供人读。
|
> 技能文档。程序只切割下方 **fence 块**;`##` 标题仅供人读。
|
||||||
> 产物外壳:`docs/context-fragment-design.md`;范例:`aesthetics-interaction` / `mechanism`。
|
> 产物外壳:`docs/context-fragment-design.md`;范例:`aesthetics-interaction` / `mechanism`。
|
||||||
> 正文高度自定义(描写文案、schema 字段、池条目随对象族而变);外壳与规则键必须严。
|
> 正文高度自定义(描写文案、schema 字段、池随对象族而变);外壳与规则键必须严。
|
||||||
|
|
||||||
## meta
|
## meta
|
||||||
|
|
||||||
@@ -11,7 +11,8 @@ name: 生成规则
|
|||||||
id: generation-rules
|
id: generation-rules
|
||||||
artifact: 设计.生成规则
|
artifact: 设计.生成规则
|
||||||
declaration: >
|
declaration: >
|
||||||
要精准格式,或本局原创听了会雾且故事里还要生成时:写出该对象族怎么生成(描写/格式/可选池);不填最终实例;可按对象反复
|
要精准格式,或本局原创听了会雾且故事里还要生成时:写出该对象族怎么生成(描写/格式/可选池);
|
||||||
|
创意字段用方向池给创作范围,不堆枚举词;不填最终实例;可按对象反复
|
||||||
when: |
|
when: |
|
||||||
已有「美学纲领与交互范式」或等价整体感觉,且下面两条满足一条就排(可按对象族反复):
|
已有「美学纲领与交互范式」或等价整体感觉,且下面两条满足一条就排(可按对象族反复):
|
||||||
a. 生成物必须有精准格式。例:武功典籍要有名字、效果、阶层。
|
a. 生成物必须有精准格式。例:武功典籍要有名字、效果、阶层。
|
||||||
@@ -22,12 +23,12 @@ when_not: |
|
|||||||
只要几张具名名片、不必按规则生成 → 不排;已有规则再要名片 → 「具体实例」。
|
只要几张具名名片、不必按规则生成 → 不排;已有规则再要名片 → 「具体实例」。
|
||||||
跨轮记住什么 → 「变量设计与更新规则」。故事怎么往前写 → 「叙事指南与故事推进」。
|
跨轮记住什么 → 「变量设计与更新规则」。故事怎么往前写 → 「叙事指南与故事推进」。
|
||||||
boundary: |
|
boundary: |
|
||||||
本技能:一条规则=怎么描写/生成 + 单层 JSON 产物格式 + 可选多池(各绑一个顶层字段)。
|
本技能:一条规则=怎么描写/生成 + 单层 JSON 产物格式 + 可选池(方向池优先;元素池仅封闭集合)。
|
||||||
具体实例:按规则填最终 records;本步不写最终实例(池条目≠实例)。
|
具体实例:按规则填最终 records;本步不写最终实例(池≠实例)。
|
||||||
舞台骨架 / 实现机制:可引用,不重写百科、不重钉既成法则。
|
舞台骨架 / 实现机制:可引用,不重写百科、不重钉既成法则。
|
||||||
变量*:游玩期状态更新——本步只约束生成时的类型与合法初值范围。
|
变量*:游玩期状态更新——本步只约束生成时的类型与合法初值范围。
|
||||||
随机范围整理:把本步(及其他上游)里需要的掷骰/检定范围收成插入表——本步可写合法区间,不代替那份汇总。
|
随机范围整理:把本步(及其他上游)里需要的掷骰/检定范围收成插入表——本步可写合法区间,不代替那份汇总。
|
||||||
旁观维护:可读合同(rule_id、产物格式、何时可 need_generate);不依赖整份描写长文。
|
旁观维护:可读合同(rule_id、产物格式、何时可 need_generate);程序抽样只抽元素池;方向池由填写方按规则自创。
|
||||||
feeds: gm,auditor
|
feeds: gm,auditor
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -43,7 +44,7 @@ feeds: gm,auditor
|
|||||||
2. 这是不是本局原创、听了会一头雾水、还要在故事里随机生成的东西?
|
2. 这是不是本局原创、听了会一头雾水、还要在故事里随机生成的东西?
|
||||||
3. 更需要:开局先钉一批、游玩中再随机补,还是只在游玩期动态生成?
|
3. 更需要:开局先钉一批、游玩中再随机补,还是只在游玩期动态生成?
|
||||||
4. 这类实例彼此有没有必须同场钉死的关系(如互为亲属、对立、同场搭档)?有 → 一次同批生成;彼此独立 → 一次一条。
|
4. 这类实例彼此有没有必须同场钉死的关系(如互为亲属、对立、同场搭档)?有 → 一次同批生成;彼此独立 → 一次一条。
|
||||||
5. 哪些产物字段需要「池」?(池挂在 JSON 字段上,如气质标签池→`气质`;可多个;没有就说没有)
|
5. 有没有必须全员一致、能数完的标签(阶层、强度档、武器种类)?名字、能力、气质、外貌这类写不完的,后面用「方向」划范围(一句话概括即可,少举例)。阶层这类光看名字不懂的,后面会写成能看见/听见的对照,不要只留阶名。
|
||||||
|
|
||||||
想到什么写什么;完整规则在正式产物里钉。
|
想到什么写什么;完整规则在正式产物里钉。
|
||||||
```
|
```
|
||||||
@@ -53,11 +54,10 @@ feeds: gm,auditor
|
|||||||
```task
|
```task
|
||||||
你正在执行「生成规则」。产物必须是 **context-fragment.v1** JSON,写入「设计.生成规则」。
|
你正在执行「生成规则」。产物必须是 **context-fragment.v1** JSON,写入「设计.生成规则」。
|
||||||
|
|
||||||
本步写出的最终物是**规则**,不是实例。一条规则必须说清两件事,并可附第三件:
|
本步写出的最终物是**规则**,不是实例。规则是创造这类东西的方法:空白表格 + 怎么填厚 + 必要时给方向。一条规则必须说清两件事,并可附第三件:
|
||||||
1. **生成与描写**:应该怎么写、怎么生成(步骤、依据、语气/切面、约束)。
|
1. **生成与描写**:按什么顺序写、多方向如何拼进一格、每格填到多厚、分级如何被感知。
|
||||||
2. **产物格式**:生成结果必须遵循的可校验格式(schema)。
|
2. **产物格式**:生成结果必须遵循的可校验格式(schema)。描写字段要有粒度和地板。
|
||||||
3. **池**(可选):每个池挂在产物格式里的某个字段(变量)上,供该字段抽取/组装/枚举;
|
3. **池**(可选,默认无):优先 **方向池**(方向名 + 一句概括;默认不举例,至多点 1 个专名);**元素池**只给能数完、必须全员一致的封闭集合。无则 `池: []`。
|
||||||
一条规则可多个池;无则 `池: []`。
|
|
||||||
|
|
||||||
本步是〔先验产物〕:先定本轮对象族,再写规则。读【本步对象】与【原型建议】;在本步钉「写什么」。禁止顺手扩到其它族。
|
本步是〔先验产物〕:先定本轮对象族,再写规则。读【本步对象】与【原型建议】;在本步钉「写什么」。禁止顺手扩到其它族。
|
||||||
|
|
||||||
@@ -70,12 +70,19 @@ feeds: gm,auditor
|
|||||||
- `seed_and_runtime`:先种子实例;规则与实例都保留,游玩期仍可按同规则生成。
|
- `seed_and_runtime`:先种子实例;规则与实例都保留,游玩期仍可按同规则生成。
|
||||||
4. 钉「数量」与「一次生成」(两件不同的事):
|
4. 钉「数量」与「一次生成」(两件不同的事):
|
||||||
- **数量**:这类对象总共要有几条(fixed / range / contextual)。
|
- **数量**:这类对象总共要有几条(fixed / range / contextual)。
|
||||||
- **一次生成**:点一次「具体实例」交几条。彼此有必须同场对齐的关系 → `同批`(一次交出并对齐关系);彼此独立 → `单条`(一次只交 1 条,要更多就再点原型)。产物形状始终是 `records[]`,不因单条改成单对象。
|
- **一次生成**:点一次「具体实例」交几条。彼此有必须同场对齐的关系 → `同批`;彼此独立 → `单条`。产物形状始终是 `records[]`。
|
||||||
5. 写满规则三块:生成与描写(可较长)+ 产物格式(**单层 JSON**:每条产物是一层键值,无嵌套对象)+ 池(需要才填;绑顶层字段)。
|
5. 写字段前先做设计判断(写入该 rule 的 `设计判断`):
|
||||||
6. 逐字段过筛:每个属性都要能回答「对本对象族是否必要」。冗余字段删掉(如女主规则里的「性别」);对象族必需字段不得漏(如 D&D 怪物的等级、攻击骰)。
|
- 列出 **不可穷举项**(姓名、绰号、能力概念、外貌手感等理论上写不完的)→ 这些用方向池或自由写,禁止做成条目枚举。
|
||||||
7. `rule_id`:规划已给则用;否则稳定 kebab-case。已有产物按 rule_id 增量。
|
- 列出 **封闭集合**(阶层、强度档、有限类型名)→ 这些才用 `allowed_values` 或一个元素池。
|
||||||
|
- 标签本身看不懂的分级(境界、品阶、强度档、好感档等)→ 必须另写 `分级表现`,把每档写成可感知的详细表象,不要只留字母/阶名。
|
||||||
|
- 从本对象族真正用得上的面挑 3~6 个正交字段(是什么 / 能做什么 / 和谁关联 / 怎么被感知),不要摊通用名片。
|
||||||
|
- 外貌、服装、仪式等由几面拼成一格的 → 写 `配方`(多方向组装进同一个顶层字段),不要拆成嵌套对象,也不要为每一面各建元素池。
|
||||||
|
6. 写满规则:生成与描写(含配方、表现、工作流、反模式)+ 产物格式(**单层 JSON**)+ 池(需要才填)+ 需要时的分级表现。
|
||||||
|
每个描写类 string 字段必须有 `粒度`(句子)和 `地板`(填到什么才算够)。`示例形状` 必须达到地板,禁止用单标签当示例。
|
||||||
|
7. 逐字段过筛:每个属性都要能回答「对本对象族是否必要」。冗余字段删掉(如女主规则里的「性别」);玩法接口不得漏(如 D&D 怪物的等级、攻击骰)。
|
||||||
|
8. `rule_id`:规划已给则用;否则稳定 kebab-case。已有产物按 rule_id 增量。
|
||||||
若本步对象与规划不一致,以最新对象为准并重钉对应英文 rule_id,禁止沿用旧 id。
|
若本步对象与规划不一致,以最新对象为准并重钉对应英文 rule_id,禁止沿用旧 id。
|
||||||
8. 按自评三维打分并填追问;输出 JSON。禁止夹带最终实例 records。
|
9. 按自评四维打分并填追问;输出 JSON。禁止夹带最终实例 records。
|
||||||
|
|
||||||
若程序已发默认问题:禁止重复开场;在首答上补洞。
|
若程序已发默认问题:禁止重复开场;在首答上补洞。
|
||||||
summary:建立 → `生成规则 · {对象} · {仅动态|仅预生成|预生成并动态} · {单条|同批}`;跳过 → `生成规则 · {对象} · 无需专门规则`。
|
summary:建立 → `生成规则 · {对象} · {仅动态|仅预生成|预生成并动态} · {单条|同批}`;跳过 → `生成规则 · {对象} · 无需专门规则`。
|
||||||
@@ -86,15 +93,21 @@ summary:建立 → `生成规则 · {对象} · {仅动态|仅预生成|预生
|
|||||||
```principles
|
```principles
|
||||||
1. 整条规则先问必要性:时机 a(精准格式)或时机 b(原创会雾且还要生成)是否成立?两条都不成立则跳过,勿硬造表。
|
1. 整条规则先问必要性:时机 a(精准格式)或时机 b(原创会雾且还要生成)是否成立?两条都不成立则跳过,勿硬造表。
|
||||||
2. 一调用一对象族;格式或用途不同则分次编排。
|
2. 一调用一对象族;格式或用途不同则分次编排。
|
||||||
3. 规则 = 描写/生成方法 + 产物格式;池是可选附属(可多池),不是必填第三设计。
|
3. 规则 = 描写/生成方法 + 产物格式。池不是必填第三设计;默认 `池: []`。宁少池,勿为每个字段挂一套词表。
|
||||||
4. 属性逐个过筛:每个字段必须对本对象族有意义;禁塞通用名片字段,也禁漏掉玩法接口字段。
|
4. 属性逐个过筛:每个字段必须对本对象族有意义;禁塞通用名片字段,也禁漏掉玩法接口字段。
|
||||||
5. 产物格式 = **单层 JSON**:一条产物是一层扁平对象;值仅为标量或「标量数组」。
|
5. 产物格式 = **单层 JSON**:一条产物是一层扁平对象;值仅为标量或「标量数组」。
|
||||||
禁止嵌套 object、禁止 object 数组。此格式要对齐动态表格列、旁观者→主世界层生成、以及投影取字段。
|
禁止嵌套 object、禁止 object 数组。此格式要对齐动态表格列、旁观者→主世界层生成、以及投影取字段。
|
||||||
类型/必填/枚举/上下界必须可校验;禁止只给示例让下游猜类型。
|
类型/必填/枚举/上下界必须可校验;禁止只给示例让下游猜类型。
|
||||||
6. 池绑顶层字段名(如 `气质`、`攻击花招`);一条规则可多池。池条目是该字段材料,不是整条 record。
|
6. 池分两种,先判断能不能数完:
|
||||||
7. 生命周期先行;未定前不假装可执行「具体实例」。
|
- **方向池**(默认):选项理论上无限。给 4~8 个方向;每条以**方向名 + 一句概括**为主。默认不举例;需要手感时至多点 1 个专名,禁止把方向写成作品名/例句清单。填写以方向为主:方向名可以当选项或标签;句子级字段按该方向写开。程序不抽方向池。
|
||||||
8. **一次生成**看关系,不看「想不想一次多写」:有必须同场钉死的关系(亲属、对立、同场搭档等)→ `同批`;彼此独立 → `单条`。`同批` 至少 2 条,否则改 `单条`。不要把「总共要几个」(数量)和「这一次交几条」(一次生成)混成一件事。
|
- **元素池**:选项有限且必须全员一致(阶层、武器种类、强度档字母)。条目可穷举;旁观 `need_generate` 只抽这类池。
|
||||||
9. 不写变量更新公式、不写剧情推进、不填最终实例。
|
姓名、绰号、能力概念、外貌、气质(作描写时)禁止元素池,也禁止用 `allowed_values` 假穷举。同一封闭集合不要既写枚举又再建元素池。
|
||||||
|
7. 填值要有地板。骰子、是否可破坏可以短。外貌、能力现象、限制、互动手感必须写到可感知。`示例形状` 是地板的样板,不是「阴郁」这种标签。
|
||||||
|
分级标签本身看不懂时:实例字段仍填短档位;详细对照写在 `分级表现`(每档 = 一句概括 + 一段可感知表象,相邻档必须能分清)。不要只丢「A / 金丹」让人猜。
|
||||||
|
8. 几面拼成一格用 **配方**:点名目标字段、用到哪些方向组件、怎么拼进这一格。表现类要点用 **表现**:一句概括 + 至多 1 条例子;例子是密度样板,不是选项库。
|
||||||
|
9. 生命周期先行;未定前不假装可执行「具体实例」。
|
||||||
|
10. **一次生成**看关系,不看「想不想一次多写」:有必须同场钉死的关系 → `同批`;彼此独立 → `单条`。`同批` 至少 2 条。不要把「总共要几个」和「这一次交几条」混成一件事。
|
||||||
|
11. 不写变量更新公式、不写剧情推进、不填最终实例。
|
||||||
```
|
```
|
||||||
|
|
||||||
## probe
|
## probe
|
||||||
@@ -102,9 +115,9 @@ summary:建立 → `生成规则 · {对象} · {仅动态|仅预生成|预生
|
|||||||
```probe
|
```probe
|
||||||
每轮 1~2 点。规划已钉且用户未改对象时,禁止重问 target / rule_id。规划为空或用户要换对象时,先问本轮生成什么。
|
每轮 1~2 点。规划已钉且用户未改对象时,禁止重问 target / rule_id。规划为空或用户要换对象时,先问本轮生成什么。
|
||||||
|
|
||||||
优先:整条规则是否必要;实例彼此有无必须同场对齐的关系(一次生成:同批 vs 单条);某字段该不该进表(冗余 vs 漏接口);单层格式类型/枚举/边界;哪些字段要池。
|
优先:整条规则是否必要;实例彼此有无必须同场对齐的关系;某字段该不该进表;不可穷举项有没有被错做成枚举词表;分级标签够不够让人看懂;描写字段地板是否够用。
|
||||||
若时机 a/b 都不成立 → 建议跳过,停追问字段与格式。
|
若时机 a/b 都不成立 → 建议跳过,停追问字段与格式。
|
||||||
不追问:最终实例正文、变量增减公式、其它对象族、完整世界百科。
|
不追问:最终实例正文、变量增减公式、其它对象族、完整世界百科、再给哪个字段加一套枚举池。
|
||||||
```
|
```
|
||||||
|
|
||||||
## output
|
## output
|
||||||
@@ -113,7 +126,7 @@ summary:建立 → `生成规则 · {对象} · {仅动态|仅预生成|预生
|
|||||||
{
|
{
|
||||||
"schema": "context-fragment.v1",
|
"schema": "context-fragment.v1",
|
||||||
"技能": "生成规则",
|
"技能": "生成规则",
|
||||||
"brief": "一句话:对象族 + 建立规则|无需专门规则 + 生命周期(若建立)+ 有池|无池",
|
"brief": "一句话:对象族 + 建立规则|无需专门规则 + 生命周期(若建立)+ 方向池|元素池|无池",
|
||||||
"mount": ["world-simulator", "auditor"],
|
"mount": ["world-simulator", "auditor"],
|
||||||
"稳变": "stable",
|
"稳变": "stable",
|
||||||
"正文": {
|
"正文": {
|
||||||
@@ -149,16 +162,50 @@ summary:建立 → `生成规则 · {对象} · {仅动态|仅预生成|预生
|
|||||||
"模式": "单条|同批",
|
"模式": "单条|同批",
|
||||||
"关系": "同批必填:本批记录之间必须一起钉的联系;单条写「彼此独立」"
|
"关系": "同批必填:本批记录之间必须一起钉的联系;单条写「彼此独立」"
|
||||||
},
|
},
|
||||||
|
"设计判断": {
|
||||||
|
"不可穷举项": ["姓名", "能力概念", "外貌"],
|
||||||
|
"封闭集合": ["类型", "阶层"],
|
||||||
|
"正交字段依据": "一句话:为何是这几个字段而不是通用名片"
|
||||||
|
},
|
||||||
"生成与描写": {
|
"生成与描写": {
|
||||||
"依据": ["可较长;引用上游产物或用户要求"],
|
"依据": ["可较长;引用上游产物或用户要求"],
|
||||||
"方法": ["可较长:生成顺序、如何组装、描写口径、语气/切面、如何用池"],
|
"配方": [
|
||||||
|
{
|
||||||
|
"目标字段": "外貌",
|
||||||
|
"组件": ["轮廓方向", "质感方向"],
|
||||||
|
"组装": "各取一个方向,拼进外貌这一格;不要拆成嵌套对象"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"表现": [
|
||||||
|
{
|
||||||
|
"针对": "能力现象",
|
||||||
|
"概括": "写到旁人能看见或听见的变化,以及对手能抓住的空隙",
|
||||||
|
"例子": ["至多 1 条密度样板;没有就不写"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"工作流": ["按配方取方向并组装", "封闭档位对照分级表现", "按地板写描写格"],
|
||||||
|
"方法": ["描写口径;方向为主(方向名可作选项/标签,句子级按该方向写开);元素池如何抽取"],
|
||||||
"硬约束": ["任何产物不得违反"],
|
"硬约束": ["任何产物不得违反"],
|
||||||
"字段间约束": ["例:阵营=A 时权限≥3"],
|
"字段间约束": ["有就写;无则 []"],
|
||||||
"变化维度": ["允许随机或变化的部分;无则 []"],
|
"变化维度": ["允许随机或变化的部分;无则 []"],
|
||||||
"禁止项": ["不得出现的组合或内容"],
|
"禁止项": ["不得出现的组合或内容"],
|
||||||
"去重规则": ["同批/跨批"],
|
"去重规则": ["同批/跨批"],
|
||||||
"校验": ["类型、枚举、边界、世界与体验一致性;以及是否按方法生成"]
|
"反模式": ["为名字/能力/外貌建枚举条目池", "描写字段只填一个词", "方向池堆作品名或例句当选项", "分级只写阶名没有表象"],
|
||||||
|
"校验": ["类型、枚举、边界、地板、配方是否组装进目标字段、分级表现是否可分档"]
|
||||||
},
|
},
|
||||||
|
"分级表现": [
|
||||||
|
{
|
||||||
|
"字段": "阶层",
|
||||||
|
"说明": "实例只填档位值;叙事按本表展开。无此类分级则 []",
|
||||||
|
"档位": [
|
||||||
|
{
|
||||||
|
"值": "档位名",
|
||||||
|
"概括": "一句:这档能做到什么量级",
|
||||||
|
"表象": "可感知的详细对照(看见/听见/碰到什么;须能和上下档分清)"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
"产物格式": {
|
"产物格式": {
|
||||||
"格式": "单层JSON",
|
"格式": "单层JSON",
|
||||||
"批量时": "array of flat object(每条记录单层)|单条则一个 flat object",
|
"批量时": "array of flat object(每条记录单层)|单条则一个 flat object",
|
||||||
@@ -167,8 +214,11 @@ summary:建立 → `生成规则 · {对象} · {仅动态|仅预生成|预生
|
|||||||
"type": "string|number|integer|boolean|enum|array",
|
"type": "string|number|integer|boolean|enum|array",
|
||||||
"required": true,
|
"required": true,
|
||||||
"description": "语义 + 为何本对象族需要此字段;可较长",
|
"description": "语义 + 为何本对象族需要此字段;可较长",
|
||||||
|
"粒度": "词条|方向|句子",
|
||||||
|
"创作": "自由|约束|参考方向|必选元素",
|
||||||
|
"地板": "描写类必填:填到什么才算够;词条/数字可省略",
|
||||||
"allowed_values": [
|
"allowed_values": [
|
||||||
{ "value": "枚举值", "meaning": "含义" }
|
{ "value": "仅封闭集合", "meaning": "含义" }
|
||||||
],
|
],
|
||||||
"minimum": 0,
|
"minimum": 0,
|
||||||
"maximum": 100,
|
"maximum": 100,
|
||||||
@@ -178,28 +228,32 @@ summary:建立 → `生成规则 · {对象} · {仅动态|仅预生成|预生
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"示例形状": {
|
"示例形状": {
|
||||||
"名称": "……",
|
"名称": "达到地板的名字,不是「某某」",
|
||||||
"等级": 3,
|
"阶层": "金丹",
|
||||||
"攻击骰": "2d6+2",
|
"外貌": "按配方拼好的一段可感知外貌,不是「黑发高个」",
|
||||||
"气质": "阴郁"
|
"能力描述": "写清现象与可被抓住的空隙;禁止一个词交差"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"池": [
|
"池": [
|
||||||
{
|
{
|
||||||
"pool_id": "英文 kebab-case;同规则内唯一",
|
"pool_id": "英文 kebab-case;同规则内唯一",
|
||||||
"名称": "人话名",
|
"名称": "人话名",
|
||||||
"绑定字段": "单层 schema 的顶层字段名,例:气质 / 攻击花招",
|
"池型": "方向|元素",
|
||||||
"用途": "灵感组装|随机抽取|枚举生成|其它",
|
"绑定字段": "单层 schema 的顶层字段名",
|
||||||
"说明": "该字段如何用本池;与「生成与描写.方法」如何配合;可较长",
|
"用途": "方向:方向为主的创作范围|元素:随机抽取或必选",
|
||||||
|
"说明": "该字段如何用本池;多池绑同一字段时看配方",
|
||||||
|
"方向": {
|
||||||
|
"方向名": "一句概括。默认到此为止;至多再点 1 个专名。仅池型=方向时使用"
|
||||||
|
},
|
||||||
"条目": [
|
"条目": [
|
||||||
{
|
{
|
||||||
"id": "可选稳定 id",
|
"id": "可选稳定 id",
|
||||||
"内容": "材料正文或短标签;结构随池自定义,但同池宜一致",
|
"内容": "仅池型=元素:可穷举的标签或短词",
|
||||||
"权重": 1,
|
"权重": 1,
|
||||||
"标签": ["可选分类标签"]
|
"标签": ["可选分类标签"]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"抽取或组装规则": "可较长;无特殊则写「均匀随机」或「按方法写入绑定字段」"
|
"抽取或组装规则": "方向池写「以方向为主;句子级按该方向写开」;多池同字段看配方;元素池写抽取方式,默认均匀随机"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -216,12 +270,17 @@ summary:建立 → `生成规则 · {对象} · {仅动态|仅预生成|预生
|
|||||||
{
|
{
|
||||||
"名": "属性妥当",
|
"名": "属性妥当",
|
||||||
"分数": 8,
|
"分数": 8,
|
||||||
"说明": "逐字段:有无冗余(如女主规则的性别)或缺失(如 D&D 怪物缺等级/攻击骰);跳过时写 N/A"
|
"说明": "逐字段:有无冗余或缺失玩法接口;跳过时写 N/A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"名": "材料得当",
|
||||||
|
"分数": 8,
|
||||||
|
"说明": "不可穷举项是否做成方向池/自由而非假穷举;方向是否以概括为主、例子极少;元素池是否仅封闭集合;该细的分级是否有表象;描写字段地板是否够密;跳过时写 N/A"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"名": "格式准确",
|
"名": "格式准确",
|
||||||
"分数": 8,
|
"分数": 8,
|
||||||
"说明": "是否严格单层 JSON、类型/必填/枚举/边界可对接动态表格·旁观者生成·投影;跳过时写 N/A"
|
"说明": "是否严格单层 JSON、类型/必填可对接动态表格·旁观者生成·投影;跳过时写 N/A"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"薄弱点": "一句话"
|
"薄弱点": "一句话"
|
||||||
@@ -242,22 +301,26 @@ summary:建立 → `生成规则 · {对象} · {仅动态|仅预生成|预生
|
|||||||
|
|
||||||
硬规则(公共三段 + 本技正文):
|
硬规则(公共三段 + 本技正文):
|
||||||
1. 合法 JSON;必含 `schema` / `技能` / `brief` / `正文` / `自评` / `追问`(题目可 `[]`)。
|
1. 合法 JSON;必含 `schema` / `技能` / `brief` / `正文` / `自评` / `追问`(题目可 `[]`)。
|
||||||
2. **公共三段**:正文=规则主体;自评=必要性/属性妥当/格式准确(**分数 0–10 十分制**);追问=导语+建议选项。
|
2. **公共三段**:正文=规则主体;自评=必要性/属性妥当/材料得当/格式准确(**分数 0–10 十分制**);追问=导语+建议选项。
|
||||||
3. **正文键固定**:本步参数、必要性判断、rules、增量说明。
|
3. **正文键固定**:本步参数、必要性判断、rules、增量说明。
|
||||||
4. **每条 rule 必含**:生成与描写、产物格式、数量、一次生成;**池可空数组**。有池时 `绑定字段` 必须是单层 schema 的顶层键。`一次生成.模式` 为同批时 `关系` 必填且本批至少 2 条;为单条时一次只交 1 条。
|
4. **每条 rule 必含**:设计判断、生成与描写、产物格式、数量、一次生成;**池可空数组**。`生成与描写.配方` / `表现` 无则 `[]`。标签看不懂的分级必须有 `分级表现`(每档概括+表象,相邻可分清);无此类则 `分级表现: []`。有池时 `绑定字段` 必须是单层 schema 的顶层键,且必须标 `池型`。多个方向池绑同一字段时配方必须写清怎么拼进该格。`一次生成.模式` 为同批时 `关系` 必填且本批至少 2 条;为单条时一次只交 1 条。
|
||||||
5. **产物格式强制单层**:`schema` 的 `type` 不得为 `object`;`array` 的 `items` 不得为 object。禁止 `外貌.发色` 这类嵌套路径。
|
5. **产物格式强制单层**:`schema` 的 `type` 不得为 `object`;`array` 的 `items` 不得为 object。禁止 `外貌.发色` 这类嵌套路径。描写类 string 字段必须有 `粒度` 与 `地板`;`示例形状` 必须达到地板。
|
||||||
6. 结论为「无需专门规则」时:`rules` 必须 `[]`。上下文策略布尔须与生命周期一致。
|
6. **池型**:`方向` 必须有 `方向`(对象:方向名→一句概括,4~8 条;默认不举例,至多 1 个专名),`条目` 可 `[]` 或省略。`元素` 必须有可穷举 `条目`,不要写 `方向`。不可穷举项禁止 `池型=元素`,也禁止用 `allowed_values` 冒充。
|
||||||
7. 池条目≠最终实例;禁止输出具体实例 `records`、变量更新规则、执行单元列表。
|
7. 结论为「无需专门规则」时:`rules` 必须 `[]`。上下文策略布尔须与生命周期一致。
|
||||||
8. `mount` 默认主世界层 + 旁观维护(旁观只需合同键:rule_id/格式/必要性;描写长文可在投影时 summary);**禁止**写 `order`。summary 见 task。
|
8. 池≠最终实例;禁止输出具体实例 `records`、变量更新规则、执行单元列表。
|
||||||
|
9. `mount` 默认主世界层 + 旁观维护(旁观只需合同键:rule_id/格式/必要性;描写长文可在投影时 summary);**禁止**写 `order`。summary 见 task。
|
||||||
|
|
||||||
## checklist
|
## checklist
|
||||||
|
|
||||||
```checklist
|
```checklist
|
||||||
- [ ] 含 schema / brief / 正文 / 自评 / 追问?
|
- [ ] 含 schema / brief / 正文 / 自评 / 追问?
|
||||||
- [ ] 自评为必要性 / 属性妥当 / 格式准确?
|
- [ ] 自评为必要性 / 属性妥当 / 材料得当 / 格式准确?
|
||||||
- [ ] 整条规则必要性有举证?不成立时 rules=[]?
|
- [ ] 整条规则必要性有举证?不成立时 rules=[]?
|
||||||
- [ ] 建立时:逐字段无冗余无漏接口?产物为单层 JSON 且类型可校验?数量与一次生成已分清(总共几个 vs 这一次交几条)?同批有关系、单条写彼此独立?
|
- [ ] 建立时:有设计判断(不可穷举 vs 封闭集合)?逐字段无冗余无漏接口?产物为单层 JSON?数量与一次生成已分清?
|
||||||
- [ ] 池:无则 [];有则绑顶层字段、未写成最终实例表?
|
- [ ] 描写字段有粒度+地板,示例形状不是单标签?
|
||||||
|
- [ ] 该细的分级有概括+可感知表象,不是只留阶名?
|
||||||
|
- [ ] 多方向拼一格时有配方?表现是概括+极少例子?
|
||||||
|
- [ ] 池:无则 [];有则标池型;方向池以概括为主、例子极少;元素池仅封闭集合;未把名字/能力做成枚举词表?
|
||||||
- [ ] 未夹带最终实例、变量公式、其它对象族、百科?
|
- [ ] 未夹带最终实例、变量公式、其它对象族、百科?
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -265,19 +328,21 @@ summary:建立 → `生成规则 · {对象} · {仅动态|仅预生成|预生
|
|||||||
|
|
||||||
```examples
|
```examples
|
||||||
好:
|
好:
|
||||||
- 武功典籍:单层含 名字/效果/阶层(时机 a)。
|
- 武功典籍:单层含 名字/效果/阶层。阶层短填 + `分级表现` 把每阶写成可感知对照;名字与效果用方向池(方向名+一句概括)。
|
||||||
- 「心之彩」:本局原创、听了会雾,故事里还要随机生成(时机 b)。
|
- 外貌:轮廓方向 × 质感方向,配方拼进 `外貌` 一格,不要 `{发色:{}}`。
|
||||||
- D&D 怪物:单层含 名称/等级/攻击骰/……;池可绑 `气质`;自评属性妥当高(未漏战斗接口)。
|
- 「心之彩」:颜色/性格用方向池(如「冷焰:静、刺、余烬不散」),不要列出 20 种色号,也不要堆作品名。
|
||||||
- 女主规则:不设「性别」字段;关系切面进表;池可空。
|
- D&D 怪物:名称/等级/攻击骰齐全;等级若只写数字看不懂,补分级表现;气质用方向池+句子地板。
|
||||||
- 互为仇敌的两名开局 NPC:一次生成=同批,关系写清对立如何对齐;数量=2。
|
- 女主规则:不设「性别」;池可空;内在冲突自由写并给地板。
|
||||||
- 可互换的酒馆路人:一次生成=单条,关系写「彼此独立」;要 5 个就点 5 次。
|
- 互为仇敌的两名开局 NPC:一次生成=同批;可互换的酒馆路人:一次生成=单条。
|
||||||
- 事件钩子:池绑顶层 `钩子`;方法写抽 1~2 条写入该字段。
|
- 现实名车、西幻常见种族 → 无需专门规则。
|
||||||
- 现实名车、西幻常见种族 → 无需专门规则(常识能推且不需精准格式)。
|
|
||||||
|
|
||||||
坏:
|
坏:
|
||||||
- 产物写成 `{ "外貌": { "发色": "…" } }` → 非单层,前端表格/投影难对接。
|
- 为气质/花招/绰号/能力各建一个 8~20 词的元素池,填出来全是标签。
|
||||||
|
- 方向池每条挂 3 个作品名或例句,变成假选项库。
|
||||||
|
- 产物写成 `{ "外貌": { "发色": "…" } }` → 非单层。
|
||||||
|
- `示例形状` 里 `能力描述: "时空斩"` 或 `气质: "阴郁"` —— 地板被示例教坏。
|
||||||
|
- 阶层/强度只有「A」「金丹」,没有表象,叙事时没法写。
|
||||||
- 女主规则塞「性别」;或怪物规则只有「名字+性格」没有等级/攻击骰。
|
- 女主规则塞「性别」;或怪物规则只有「名字+性格」没有等级/攻击骰。
|
||||||
- 有亲属关系的三人却标成单条,分三次各写一条,关系对不上。
|
- 有亲属关系的三人却标成单条;或把「总共要 5 个」写成同批但彼此无必须同场的关系。
|
||||||
- 把「总共要 5 个」写成一次生成=同批,但彼此并无必须同场的关系。
|
|
||||||
- 把写好的 NPC 整份塞进池当最终实例。
|
- 把写好的 NPC 整份塞进池当最终实例。
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -32,4 +32,5 @@ prompt_excerpt: |
|
|||||||
多数轮保持空:need_generate=false、table_ops=[]、notes=[]。
|
多数轮保持空:need_generate=false、table_ops=[]、notes=[]。
|
||||||
table_ops 仅在确需改「变量.当前」等表字段时使用(op=set|delta)。
|
table_ops 仅在确需改「变量.当前」等表字段时使用(op=set|delta)。
|
||||||
need_generate 仅在确需按某条 rule_id 补实例时写成 { "rule_id", "reason" }。
|
need_generate 仅在确需按某条 rule_id 补实例时写成 { "rule_id", "reason" }。
|
||||||
|
程序只会抽该规则里的元素池;方向池由主世界层按规则自创,不要指望抽签出标签。
|
||||||
禁止编造长对话续写;禁止输出用户可见正文;禁止修改未点名的无关字段。
|
禁止编造长对话续写;禁止输出用户可见正文;禁止修改未点名的无关字段。
|
||||||
|
|||||||
@@ -88,12 +88,12 @@ export class Blackboard {
|
|||||||
this.writeSeq += 1;
|
this.writeSeq += 1;
|
||||||
const existing = this.getLatestByTag(input.tag);
|
const existing = this.getLatestByTag(input.tag);
|
||||||
const item: BlackboardItem = {
|
const item: BlackboardItem = {
|
||||||
id: randomUUID(),
|
id: existing?.id ?? randomUUID(),
|
||||||
tag: input.tag,
|
tag: input.tag,
|
||||||
content: input.content,
|
content: input.content,
|
||||||
source: input.source,
|
source: input.source,
|
||||||
scope: input.scope ?? existing?.scope,
|
scope: input.scope ?? existing?.scope,
|
||||||
createdAt: now,
|
createdAt: existing?.createdAt ?? now,
|
||||||
updatedAt: `${now}#${this.writeSeq}`,
|
updatedAt: `${now}#${this.writeSeq}`,
|
||||||
dependencies: input.dependencies,
|
dependencies: input.dependencies,
|
||||||
metadata: input.metadata,
|
metadata: input.metadata,
|
||||||
|
|||||||
@@ -421,7 +421,8 @@ export function canApplyEvent(
|
|||||||
reason?.kind === "approve_step" ||
|
reason?.kind === "approve_step" ||
|
||||||
reason?.kind === "worker_questions" ||
|
reason?.kind === "worker_questions" ||
|
||||||
reason?.kind === "input" ||
|
reason?.kind === "input" ||
|
||||||
reason?.kind === "revision"
|
reason?.kind === "revision" ||
|
||||||
|
reason?.kind === "review_artifact"
|
||||||
);
|
);
|
||||||
case "creation_step_pick_awaited":
|
case "creation_step_pick_awaited":
|
||||||
return session.phase === "running";
|
return session.phase === "running";
|
||||||
|
|||||||
+346
-71
@@ -55,7 +55,10 @@ import {
|
|||||||
extractUnitContentFromDraft,
|
extractUnitContentFromDraft,
|
||||||
isDesignDiskWorker,
|
isDesignDiskWorker,
|
||||||
isDesignUnitArtifact,
|
isDesignUnitArtifact,
|
||||||
|
normalizeCreationUnitId,
|
||||||
|
parseAcceptedContentStore,
|
||||||
parseAcceptedUnits,
|
parseAcceptedUnits,
|
||||||
|
dropAcceptedUnitContent,
|
||||||
upsertAcceptedUnitContent,
|
upsertAcceptedUnitContent,
|
||||||
} from "../skills/creation-units.js";
|
} from "../skills/creation-units.js";
|
||||||
import {
|
import {
|
||||||
@@ -80,6 +83,8 @@ import {
|
|||||||
spawnRepeatableCreationStep,
|
spawnRepeatableCreationStep,
|
||||||
spawnInstanceFromPrototype,
|
spawnInstanceFromPrototype,
|
||||||
removeUnstartedInstance,
|
removeUnstartedInstance,
|
||||||
|
removeRepeatableInstance,
|
||||||
|
pruneRepeatableArtifactContent,
|
||||||
isPrototypeStep,
|
isPrototypeStep,
|
||||||
isPrototypeSelectable,
|
isPrototypeSelectable,
|
||||||
isInstanceStep,
|
isInstanceStep,
|
||||||
@@ -93,7 +98,6 @@ import {
|
|||||||
patchCreationFlowStepParams,
|
patchCreationFlowStepParams,
|
||||||
pickRecordedStepId,
|
pickRecordedStepId,
|
||||||
resolveDesignStepBinding,
|
resolveDesignStepBinding,
|
||||||
shouldSkipModuleOpening,
|
|
||||||
isProgressPointerTag,
|
isProgressPointerTag,
|
||||||
missingRequiredStepParams,
|
missingRequiredStepParams,
|
||||||
stepUnitId,
|
stepUnitId,
|
||||||
@@ -101,6 +105,7 @@ import {
|
|||||||
stringifyModuleOpeningState,
|
stringifyModuleOpeningState,
|
||||||
finalizeCreationFlow,
|
finalizeCreationFlow,
|
||||||
type CreationFlow,
|
type CreationFlow,
|
||||||
|
type CreationFlowStep,
|
||||||
type CreationFlowStepParams,
|
type CreationFlowStepParams,
|
||||||
type ModuleCatalog,
|
type ModuleCatalog,
|
||||||
type ModuleCatalogEntry,
|
type ModuleCatalogEntry,
|
||||||
@@ -185,6 +190,7 @@ import type {
|
|||||||
AcceptanceMode,
|
AcceptanceMode,
|
||||||
ActiveSkillSnapshot,
|
ActiveSkillSnapshot,
|
||||||
ApplyEventResult,
|
ApplyEventResult,
|
||||||
|
ArtifactRecord,
|
||||||
MainAgentDecision,
|
MainAgentDecision,
|
||||||
PhaseEffect,
|
PhaseEffect,
|
||||||
RuntimeEvent,
|
RuntimeEvent,
|
||||||
@@ -339,6 +345,71 @@ export class PhaseRuntime {
|
|||||||
this.lastWorkerRunSnapshot = null;
|
this.lastWorkerRunSnapshot = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提前停止当前生成:还原开跑快照,回到生成前的等待面(或分层图)。
|
||||||
|
* 不重跑。
|
||||||
|
*/
|
||||||
|
abortCurrentRun(): RuntimeSession {
|
||||||
|
const snap = this.lastWorkerRunSnapshot;
|
||||||
|
if (snap) {
|
||||||
|
this.restoreFromCheckpoint(snap.runtimeSession, snap.blackboardItems);
|
||||||
|
}
|
||||||
|
return this.settleAfterAbort();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 停止后落到可操作等待态,避免停在 running 空转。 */
|
||||||
|
private settleAfterAbort(): RuntimeSession {
|
||||||
|
const s = this.session;
|
||||||
|
if (s.waitingReason) return s;
|
||||||
|
const pendingId = s.pendingArtifactId?.trim();
|
||||||
|
const revisionTarget =
|
||||||
|
typeof s.slots.revisionTargetArtifactId === "string"
|
||||||
|
? s.slots.revisionTargetArtifactId.trim()
|
||||||
|
: "";
|
||||||
|
const reviewId =
|
||||||
|
pendingId && s.artifacts.some((a) => a.id === pendingId)
|
||||||
|
? pendingId
|
||||||
|
: revisionTarget && s.artifacts.some((a) => a.id === revisionTarget)
|
||||||
|
? revisionTarget
|
||||||
|
: "";
|
||||||
|
if (reviewId) {
|
||||||
|
this.session = {
|
||||||
|
...s,
|
||||||
|
phase: "waiting_user",
|
||||||
|
waitingReason: { kind: "review_artifact", artifactId: reviewId },
|
||||||
|
pendingArtifactId: reviewId,
|
||||||
|
currentWorkerId: undefined,
|
||||||
|
pendingDecision: undefined,
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
return this.session;
|
||||||
|
}
|
||||||
|
if (this.hasCreationFlow()) {
|
||||||
|
this.clearProposedStep();
|
||||||
|
this.session = {
|
||||||
|
...s,
|
||||||
|
phase: "waiting_user",
|
||||||
|
waitingReason: { kind: "pick_creation_step" },
|
||||||
|
pendingDecision: undefined,
|
||||||
|
currentWorkerId: undefined,
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
return this.session;
|
||||||
|
}
|
||||||
|
if (this.recoverOrphanedRun()) return this.session;
|
||||||
|
if (s.phase === "running") {
|
||||||
|
this.session = {
|
||||||
|
...s,
|
||||||
|
phase: "waiting_user",
|
||||||
|
waitingReason: { kind: "input", message: "已停止生成,可继续说明。" },
|
||||||
|
currentWorkerId: undefined,
|
||||||
|
pendingDecision: undefined,
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return this.session;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 执行中卡住:有进行中的 worker 则按开跑快照重跑;否则重跑总管。
|
* 执行中卡住:有进行中的 worker 则按开跑快照重跑;否则重跑总管。
|
||||||
*/
|
*/
|
||||||
@@ -711,12 +782,19 @@ export class PhaseRuntime {
|
|||||||
* 可增殖节点刚增殖出、尚未验收的空实例从图上撤掉。
|
* 可增殖节点刚增殖出、尚未验收的空实例从图上撤掉。
|
||||||
*/
|
*/
|
||||||
async leaveCreationStep(): Promise<RuntimeSession> {
|
async leaveCreationStep(): Promise<RuntimeSession> {
|
||||||
|
if (this.session.phase === "running" && !this.session.waitingReason) {
|
||||||
|
this.abortCurrentRun();
|
||||||
|
}
|
||||||
|
if (this.session.waitingReason?.kind === "pick_creation_step") {
|
||||||
|
return this.session;
|
||||||
|
}
|
||||||
const waiting = this.session.waitingReason?.kind;
|
const waiting = this.session.waitingReason?.kind;
|
||||||
const allowed =
|
const allowed =
|
||||||
waiting === "worker_questions" ||
|
waiting === "worker_questions" ||
|
||||||
waiting === "input" ||
|
waiting === "input" ||
|
||||||
waiting === "revision" ||
|
waiting === "revision" ||
|
||||||
waiting === "approve_step";
|
waiting === "approve_step" ||
|
||||||
|
waiting === "review_artifact";
|
||||||
if (!allowed) {
|
if (!allowed) {
|
||||||
throw new Error("当前不能返回节点选择");
|
throw new Error("当前不能返回节点选择");
|
||||||
}
|
}
|
||||||
@@ -724,6 +802,7 @@ export class PhaseRuntime {
|
|||||||
throw new Error("游玩中不能返回创作节点");
|
throw new Error("游玩中不能返回创作节点");
|
||||||
}
|
}
|
||||||
const stepId = this.readPinnedStepId();
|
const stepId = this.readPinnedStepId();
|
||||||
|
this.forgetUnansweredModuleOpening(stepId);
|
||||||
const removed = this.unspawnCurrentInstanceIfEmpty();
|
const removed = this.unspawnCurrentInstanceIfEmpty();
|
||||||
this.clearProposedStep();
|
this.clearProposedStep();
|
||||||
this.clearCurrentStepPointers();
|
this.clearCurrentStepPointers();
|
||||||
@@ -768,6 +847,42 @@ export class PhaseRuntime {
|
|||||||
return { id: step.id, name: step.name };
|
return { id: step.id, name: step.name };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 离开节点时若默认问题只展示过、还没答:清状态,下次点进再出引导。 */
|
||||||
|
private forgetUnansweredModuleOpening(stepId: string): void {
|
||||||
|
const id = stepId.trim();
|
||||||
|
if (!id) return;
|
||||||
|
const flow = parseCreationFlow(
|
||||||
|
this.blackboard.getContentByTag(CREATION_FLOW_TAG),
|
||||||
|
);
|
||||||
|
const step = flow ? findStepByRef(flow, id) : null;
|
||||||
|
const key = step ? stepUnitId(step) : id;
|
||||||
|
const state = parseModuleOpeningState(
|
||||||
|
this.session.slots[SLOT_CREATION_MODULE_OPENING_STATE] ??
|
||||||
|
this.blackboard.getContentByTag(CREATION_MODULE_OPENING_STATE_TAG),
|
||||||
|
);
|
||||||
|
if (state[key] !== "shown") return;
|
||||||
|
delete state[key];
|
||||||
|
const raw = stringifyModuleOpeningState(state);
|
||||||
|
this.blackboard.write({
|
||||||
|
tag: CREATION_MODULE_OPENING_STATE_TAG,
|
||||||
|
content: raw,
|
||||||
|
source: "runtime",
|
||||||
|
});
|
||||||
|
this.blackboard.write({
|
||||||
|
tag: CREATION_MODULE_OPENING_TAG,
|
||||||
|
content: "",
|
||||||
|
source: "runtime",
|
||||||
|
});
|
||||||
|
this.session = {
|
||||||
|
...this.session,
|
||||||
|
slots: {
|
||||||
|
...this.session.slots,
|
||||||
|
[SLOT_CREATION_MODULE_OPENING_STATE]: raw,
|
||||||
|
[CREATION_MODULE_OPENING_TAG]: undefined,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private clearCurrentStepPointers(): void {
|
private clearCurrentStepPointers(): void {
|
||||||
this.blackboard.write({
|
this.blackboard.write({
|
||||||
tag: CREATION_CURRENT_STEP_TAG,
|
tag: CREATION_CURRENT_STEP_TAG,
|
||||||
@@ -856,6 +971,85 @@ export class PhaseRuntime {
|
|||||||
return this.session;
|
return this.session;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 图上删除可增殖产物(已验收也可)。 */
|
||||||
|
async deleteCreationStep(stepId: string): Promise<RuntimeSession> {
|
||||||
|
const id = stepId.trim();
|
||||||
|
if (!id) throw new Error("请指定要删除的产物");
|
||||||
|
this.syncSlotsToBlackboard(this.session);
|
||||||
|
const catalog = await this.loadActiveModuleCatalog();
|
||||||
|
const flow = this.readNormalizedFlow(catalog);
|
||||||
|
if (!flow) throw new Error("还没有工作流计划");
|
||||||
|
const result = removeRepeatableInstance({ flow, stepId: id, catalog });
|
||||||
|
if ("error" in result) throw new Error(result.error);
|
||||||
|
this.writeCreationFlow(result.flow);
|
||||||
|
this.forgetDeletedCreationStep(result.removed, result.flow, catalog);
|
||||||
|
this.onMessage(`已删除「${result.removed.name}」产物 ${result.removed.id}。`);
|
||||||
|
debugLog("step", `删除产物 ${result.removed.id}`);
|
||||||
|
return this.session;
|
||||||
|
}
|
||||||
|
|
||||||
|
private forgetDeletedCreationStep(
|
||||||
|
step: CreationFlowStep,
|
||||||
|
flow: CreationFlow,
|
||||||
|
catalog: ModuleCatalog | null,
|
||||||
|
): void {
|
||||||
|
const unitId = step.id;
|
||||||
|
const accepted = this.readAcceptedStepIds().filter((item) => item !== unitId);
|
||||||
|
const acceptedRaw = JSON.stringify(accepted);
|
||||||
|
this.blackboard.write({
|
||||||
|
tag: CREATION_ACCEPTED_UNITS_TAG,
|
||||||
|
content: acceptedRaw,
|
||||||
|
source: "runtime",
|
||||||
|
});
|
||||||
|
const contentRaw = dropAcceptedUnitContent(
|
||||||
|
this.blackboard.getContentByTag(CREATION_ACCEPTED_CONTENT_TAG),
|
||||||
|
unitId,
|
||||||
|
);
|
||||||
|
this.blackboard.write({
|
||||||
|
tag: CREATION_ACCEPTED_CONTENT_TAG,
|
||||||
|
content: contentRaw,
|
||||||
|
source: "runtime",
|
||||||
|
});
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
const artifacts = this.session.artifacts.map((a) =>
|
||||||
|
a.stepId === unitId && a.status !== "rejected"
|
||||||
|
? { ...a, status: "rejected" as const, updatedAt: now }
|
||||||
|
: a,
|
||||||
|
);
|
||||||
|
const remainingSameModule = accepted.filter((item) => {
|
||||||
|
const other = findStepByRef(flow, item);
|
||||||
|
return other?.name === step.name;
|
||||||
|
}).length;
|
||||||
|
const tag = findModuleByName(catalog, step.name)?.artifact?.trim();
|
||||||
|
if (tag) {
|
||||||
|
const pruned = pruneRepeatableArtifactContent({
|
||||||
|
raw: this.blackboard.getContentByTag(tag) ?? "",
|
||||||
|
step,
|
||||||
|
remainingSameModule,
|
||||||
|
});
|
||||||
|
this.blackboard.write({
|
||||||
|
tag,
|
||||||
|
content: pruned,
|
||||||
|
source: "runtime",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const pinned = this.readPinnedStepId();
|
||||||
|
this.session = {
|
||||||
|
...this.session,
|
||||||
|
artifacts,
|
||||||
|
currentStepId: pinned === unitId ? undefined : this.session.currentStepId,
|
||||||
|
slots: {
|
||||||
|
...this.session.slots,
|
||||||
|
[SLOT_CREATION_ACCEPTED_UNITS]: accepted,
|
||||||
|
[CREATION_ACCEPTED_UNITS_TAG]: acceptedRaw,
|
||||||
|
[CREATION_ACCEPTED_CONTENT_TAG]: contentRaw,
|
||||||
|
...(tag ? { [tag]: this.blackboard.getContentByTag(tag) ?? "" } : {}),
|
||||||
|
},
|
||||||
|
updatedAt: now,
|
||||||
|
};
|
||||||
|
if (pinned === unitId) this.clearCurrentStepPointers();
|
||||||
|
}
|
||||||
|
|
||||||
private writeCreationFlow(flow: CreationFlow): void {
|
private writeCreationFlow(flow: CreationFlow): void {
|
||||||
const content = stringifyCreationFlow(flow);
|
const content = stringifyCreationFlow(flow);
|
||||||
this.blackboard.write({
|
this.blackboard.write({
|
||||||
@@ -1164,7 +1358,7 @@ export class PhaseRuntime {
|
|||||||
case "run_worker":
|
case "run_worker":
|
||||||
if (
|
if (
|
||||||
effect.workerId === DESIGN_STEP_WORKER_ID &&
|
effect.workerId === DESIGN_STEP_WORKER_ID &&
|
||||||
!this.hasPinnedReadyStep()
|
!this.canRunPinnedDesignStep()
|
||||||
) {
|
) {
|
||||||
await this.awaitCreationStepPick();
|
await this.awaitCreationStepPick();
|
||||||
break;
|
break;
|
||||||
@@ -1273,6 +1467,29 @@ export class PhaseRuntime {
|
|||||||
return isStepReady(flow, step, this.readAcceptedStepIds());
|
return isStepReady(flow, step, this.readAcceptedStepIds());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 已验收节点回头改:isStepReady 为 false,但仍须跑 worker,不能弹回分层图。 */
|
||||||
|
private canRunPinnedDesignStep(): boolean {
|
||||||
|
if (this.hasPinnedReadyStep()) return true;
|
||||||
|
if (typeof this.session.slots.revisionTargetArtifactId === "string") {
|
||||||
|
return Boolean(this.session.slots.revisionTargetArtifactId.trim());
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
isInheritExistingFlag(
|
||||||
|
this.blackboard.getContentByTag(CREATION_INHERIT_EXISTING_TAG) ??
|
||||||
|
this.session.slots[CREATION_INHERIT_EXISTING_TAG],
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const flow = parseCreationFlow(
|
||||||
|
this.blackboard.getContentByTag(CREATION_FLOW_TAG),
|
||||||
|
);
|
||||||
|
const step = flow
|
||||||
|
? findStepByRef(flow, this.readPinnedStepId())
|
||||||
|
: null;
|
||||||
|
return Boolean(step && isReviseStep(step));
|
||||||
|
}
|
||||||
|
|
||||||
private pinCreationStepSnapshot(params: {
|
private pinCreationStepSnapshot(params: {
|
||||||
stepId: string;
|
stepId: string;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -1371,15 +1588,24 @@ export class PhaseRuntime {
|
|||||||
};
|
};
|
||||||
flow = spawned.flow;
|
flow = spawned.flow;
|
||||||
step = spawned.step;
|
step = spawned.step;
|
||||||
} else if (reenter) {
|
} else if (!reenter && !isStepReady(flow, step, accepted)) {
|
||||||
if (!isStepAccepted(step, accepted)) {
|
|
||||||
throw new Error(`「${step.name}」还没完成,请直接进入而不是回头修改`);
|
|
||||||
}
|
|
||||||
} else if (!isStepReady(flow, step, accepted)) {
|
|
||||||
throw new Error(`「${step.name}」的前置节点还没完成,不能开始`);
|
throw new Error(`「${step.name}」的前置节点还没完成,不能开始`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const pendingModule = findModuleByName(catalog, step.name);
|
const pendingModule = findModuleByName(catalog, step.name);
|
||||||
|
if (reenter) {
|
||||||
|
const artTag = pendingModule?.artifact?.trim();
|
||||||
|
const hasArt = Boolean(
|
||||||
|
artTag && this.blackboard.getContentByTag(artTag)?.trim(),
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
!isStepAccepted(step, accepted) &&
|
||||||
|
!hasArt &&
|
||||||
|
!this.findReenterArtifact(step.id, artTag)
|
||||||
|
) {
|
||||||
|
throw new Error(`「${step.name}」还没完成,请直接进入而不是回头修改`);
|
||||||
|
}
|
||||||
|
}
|
||||||
const paramSpecs = pendingModule?.params ?? [];
|
const paramSpecs = pendingModule?.params ?? [];
|
||||||
const auto = pendingModule?.auto === true;
|
const auto = pendingModule?.auto === true;
|
||||||
if (opts?.stepParams && Object.keys(opts.stepParams).length > 0) {
|
if (opts?.stepParams && Object.keys(opts.stepParams).length > 0) {
|
||||||
@@ -1417,6 +1643,21 @@ export class PhaseRuntime {
|
|||||||
const waitingPick =
|
const waitingPick =
|
||||||
this.session.waitingReason?.kind === "pick_creation_step";
|
this.session.waitingReason?.kind === "pick_creation_step";
|
||||||
const needsParamForm = paramsMissing.length > 0 && !auto;
|
const needsParamForm = paramsMissing.length > 0 && !auto;
|
||||||
|
|
||||||
|
if (reenter) {
|
||||||
|
const existing = this.ensureReenterArtifact(
|
||||||
|
pinned.id,
|
||||||
|
pendingModule?.artifact,
|
||||||
|
);
|
||||||
|
if (existing) {
|
||||||
|
this.reopenArtifactForRevision(existing, pinned);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new Error(
|
||||||
|
`找不到「${pinned.name}」的已有产物,无法回头修改。要新开一条请点原型上的「增殖」。`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (needsParamForm) {
|
if (needsParamForm) {
|
||||||
const decision = createDecision({
|
const decision = createDecision({
|
||||||
action: "run_worker",
|
action: "run_worker",
|
||||||
@@ -1453,6 +1694,101 @@ export class PhaseRuntime {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 回头修改:打开该步已有产物,让用户像未验收时一样写意见再改。 */
|
||||||
|
private findReenterArtifact(
|
||||||
|
stepId: string,
|
||||||
|
artifactTag?: string,
|
||||||
|
): ArtifactRecord | undefined {
|
||||||
|
const id = stepId.trim();
|
||||||
|
const tag = artifactTag?.trim();
|
||||||
|
if (!id && !tag) return undefined;
|
||||||
|
const usable = this.session.artifacts.filter(
|
||||||
|
(a) => a.workerId === DESIGN_STEP_WORKER_ID && a.status !== "rejected",
|
||||||
|
);
|
||||||
|
const byStep = id ? usable.filter((a) => a.stepId === id) : [];
|
||||||
|
const byTag = tag
|
||||||
|
? usable.filter((a) => a.outputTags.includes(tag))
|
||||||
|
: [];
|
||||||
|
const matches = byStep.length ? byStep : byTag;
|
||||||
|
const accepted = matches.filter((a) => a.status === "accepted");
|
||||||
|
const pool = accepted.length ? accepted : matches;
|
||||||
|
return pool.at(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ensureReenterArtifact(
|
||||||
|
stepId: string,
|
||||||
|
artifactTag?: string,
|
||||||
|
): ArtifactRecord | undefined {
|
||||||
|
const found = this.findReenterArtifact(stepId, artifactTag);
|
||||||
|
if (found) return found;
|
||||||
|
const tag = artifactTag?.trim();
|
||||||
|
let content = tag
|
||||||
|
? this.blackboard.getContentByTag(tag)?.trim() || ""
|
||||||
|
: "";
|
||||||
|
if (!content) {
|
||||||
|
content = this.readAcceptedUnitSlice(stepId);
|
||||||
|
if (content && tag) {
|
||||||
|
this.blackboard.write({
|
||||||
|
tag,
|
||||||
|
content,
|
||||||
|
source: "runtime",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!tag || !content) return undefined;
|
||||||
|
const artifact = createArtifact({
|
||||||
|
workerId: DESIGN_STEP_WORKER_ID,
|
||||||
|
stepId,
|
||||||
|
outputTags: [tag],
|
||||||
|
summary: "既有产物",
|
||||||
|
});
|
||||||
|
this.session = {
|
||||||
|
...this.session,
|
||||||
|
artifacts: [...this.session.artifacts, artifact],
|
||||||
|
};
|
||||||
|
return artifact;
|
||||||
|
}
|
||||||
|
|
||||||
|
private readAcceptedUnitSlice(stepId: string): string {
|
||||||
|
const store = parseAcceptedContentStore(
|
||||||
|
this.blackboard.getContentByTag(CREATION_ACCEPTED_CONTENT_TAG),
|
||||||
|
);
|
||||||
|
const entry = store.units[normalizeCreationUnitId(stepId)];
|
||||||
|
if (!entry || entry.content == null) return "";
|
||||||
|
return typeof entry.content === "string"
|
||||||
|
? entry.content.trim()
|
||||||
|
: JSON.stringify(entry.content, null, 2).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private reopenArtifactForRevision(
|
||||||
|
artifact: ArtifactRecord,
|
||||||
|
step: { id: string; name: string },
|
||||||
|
): void {
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
this.session = {
|
||||||
|
...this.session,
|
||||||
|
phase: "waiting_user",
|
||||||
|
waitingReason: { kind: "review_artifact", artifactId: artifact.id },
|
||||||
|
pendingArtifactId: artifact.id,
|
||||||
|
currentWorkerId: DESIGN_STEP_WORKER_ID,
|
||||||
|
currentStepId: step.id,
|
||||||
|
pendingDecision: undefined,
|
||||||
|
artifacts: this.session.artifacts.map((a) =>
|
||||||
|
a.id === artifact.id
|
||||||
|
? { ...a, status: "under_review", updatedAt: now }
|
||||||
|
: a,
|
||||||
|
),
|
||||||
|
slots: {
|
||||||
|
...this.session.slots,
|
||||||
|
revisionTargetArtifactId: artifact.id,
|
||||||
|
},
|
||||||
|
updatedAt: now,
|
||||||
|
};
|
||||||
|
this.onMessage(
|
||||||
|
`已回到「${step.name}」,可按意见修改现有产物(继承原文),或返回节点再选别的步骤。`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 验收后回到分层图:点节点即确认并进入,底栏写意见再发即改编排。
|
* 验收后回到分层图:点节点即确认并进入,底栏写意见再发即改编排。
|
||||||
*/
|
*/
|
||||||
@@ -2204,7 +2540,7 @@ export class PhaseRuntime {
|
|||||||
});
|
});
|
||||||
if (!result) {
|
if (!result) {
|
||||||
this.onMessage(
|
this.onMessage(
|
||||||
`[旁观维护] need_generate=${need.rule_id}:未找到可抽样的池(请检查设计.生成规则)`,
|
`[旁观维护] need_generate=${need.rule_id}:无可程序抽样的元素池(方向池由填写方按规则自创)`,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -2350,48 +2686,6 @@ export class PhaseRuntime {
|
|||||||
|
|
||||||
if (phase === "answered") return false;
|
if (phase === "answered") return false;
|
||||||
|
|
||||||
// 配方开局模块 + 用户首句已在「用户.需求」→ 默认视为 opening 已做完,直接进 LLM
|
|
||||||
const demand = (
|
|
||||||
this.blackboard.getContentByTag("用户.需求") ??
|
|
||||||
this.session.slots["用户.需求"] ??
|
|
||||||
""
|
|
||||||
)
|
|
||||||
.toString()
|
|
||||||
.trim();
|
|
||||||
if (
|
|
||||||
shouldSkipModuleOpening({
|
|
||||||
demand,
|
|
||||||
dependsOn: binding.step.depends_on,
|
|
||||||
phase,
|
|
||||||
})
|
|
||||||
) {
|
|
||||||
state[stepKey] = "answered";
|
|
||||||
const raw = stringifyModuleOpeningState(state);
|
|
||||||
this.blackboard.write({
|
|
||||||
tag: CREATION_MODULE_OPENING_STATE_TAG,
|
|
||||||
content: raw,
|
|
||||||
source: "runtime",
|
|
||||||
});
|
|
||||||
this.blackboard.write({
|
|
||||||
tag: CREATION_MODULE_OPENING_TAG,
|
|
||||||
content: "(已跳过默认问题:用户首句见「用户.需求」)",
|
|
||||||
source: "runtime",
|
|
||||||
});
|
|
||||||
this.session = {
|
|
||||||
...this.session,
|
|
||||||
slots: {
|
|
||||||
...this.session.slots,
|
|
||||||
[SLOT_CREATION_MODULE_OPENING_STATE]: raw,
|
|
||||||
[CREATION_MODULE_OPENING_TAG]:
|
|
||||||
"(已跳过默认问题:用户首句见「用户.需求」)",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
this.onMessage(
|
|
||||||
`[系统] ${binding.module.name} · 已坐在配方开局步,首句见「用户.需求」,跳过默认问题`,
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (phase === "shown" && this.session.resumeContext) {
|
if (phase === "shown" && this.session.resumeContext) {
|
||||||
// 用户刚答完默认问题 → 标记已答,继续走 LLM
|
// 用户刚答完默认问题 → 标记已答,继续走 LLM
|
||||||
state[stepKey] = "answered";
|
state[stepKey] = "answered";
|
||||||
@@ -2412,26 +2706,7 @@ export class PhaseRuntime {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (phase === "shown" && !this.session.resumeContext) {
|
// 首次,或开场已发出但用户还没答(离开节点 / 恢复丢了 resume):展示引导,等用户先说一轮
|
||||||
// 异常重入:开场已发过但未走 resume;避免死循环,直接进 LLM
|
|
||||||
state[stepKey] = "answered";
|
|
||||||
const raw = stringifyModuleOpeningState(state);
|
|
||||||
this.blackboard.write({
|
|
||||||
tag: CREATION_MODULE_OPENING_STATE_TAG,
|
|
||||||
content: raw,
|
|
||||||
source: "runtime",
|
|
||||||
});
|
|
||||||
this.session = {
|
|
||||||
...this.session,
|
|
||||||
slots: {
|
|
||||||
...this.session.slots,
|
|
||||||
[SLOT_CREATION_MODULE_OPENING_STATE]: raw,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 首次:发出默认问题
|
|
||||||
state[stepKey] = "shown";
|
state[stepKey] = "shown";
|
||||||
const raw = stringifyModuleOpeningState(state);
|
const raw = stringifyModuleOpeningState(state);
|
||||||
this.blackboard.write({
|
this.blackboard.write({
|
||||||
|
|||||||
@@ -1410,6 +1410,31 @@ export class SessionManager {
|
|||||||
return this.toView(id);
|
return this.toView(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 中止当前生成,不重跑;回到生成前的等待面或分层图。 */
|
||||||
|
async abortRun(id: string): Promise<SessionView> {
|
||||||
|
const s = this.require(id);
|
||||||
|
const session = s.runtime.getSession();
|
||||||
|
if (session.phase !== "running" || session.waitingReason) {
|
||||||
|
return this.toView(id);
|
||||||
|
}
|
||||||
|
const cutoff = s.runMessageCutoff ?? s.messages.length;
|
||||||
|
s.messages = s.messages.slice(0, cutoff);
|
||||||
|
try {
|
||||||
|
const owned = await this.runExclusive(id, s, async () => {
|
||||||
|
s.runtime.abortCurrentRun();
|
||||||
|
});
|
||||||
|
if (!owned) return this.toView(id);
|
||||||
|
} catch (err) {
|
||||||
|
const detail = err instanceof Error ? err.message : String(err);
|
||||||
|
console.error("[会话] 停止生成失败", detail);
|
||||||
|
await this.recoverFailedRun(s, detail);
|
||||||
|
}
|
||||||
|
s.messages.push(this.msg("system", "已停止生成。"));
|
||||||
|
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);
|
||||||
@@ -1686,6 +1711,27 @@ export class SessionManager {
|
|||||||
return this.toView(id);
|
return this.toView(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async deleteStep(id: string, stepId: string): Promise<SessionView> {
|
||||||
|
const s = this.require(id);
|
||||||
|
try {
|
||||||
|
const owned = await this.runExclusive(id, s, async () => {
|
||||||
|
await this.confirmPendingFlowPlan(s);
|
||||||
|
const waiting = s.runtime.getSession().waitingReason?.kind;
|
||||||
|
if (waiting !== "pick_creation_step") {
|
||||||
|
throw new Error("当前不是删除产物的时机");
|
||||||
|
}
|
||||||
|
await s.runtime.deleteCreationStep(stepId);
|
||||||
|
});
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
/** 已点进技能步、尚未验收:可退回分层图(误点可增殖节点时用) */
|
/** 已点进技能步、尚未验收:可退回分层图(误点可增殖节点时用) */
|
||||||
private canOfferLeaveCreationStep(
|
private canOfferLeaveCreationStep(
|
||||||
s: ManagedSession,
|
s: ManagedSession,
|
||||||
@@ -1699,10 +1745,21 @@ export class SessionManager {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const kind = reason?.kind;
|
const kind = reason?.kind;
|
||||||
|
if (session.phase === "running" && !kind) {
|
||||||
|
return Boolean(
|
||||||
|
s.runtime.getBlackboard().getContentByTag(CREATION_FLOW_TAG)?.trim(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (kind === "review_artifact") {
|
||||||
|
return Boolean(
|
||||||
|
s.runtime.getBlackboard().getContentByTag(CREATION_FLOW_TAG)?.trim(),
|
||||||
|
);
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
kind !== "worker_questions" &&
|
kind !== "worker_questions" &&
|
||||||
kind !== "input" &&
|
kind !== "input" &&
|
||||||
kind !== "revision"
|
kind !== "revision" &&
|
||||||
|
kind !== "approve_step"
|
||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -515,6 +515,12 @@ const server = createServer(async (req, res) => {
|
|||||||
typeof body.moduleName === "string" ? body.moduleName : "",
|
typeof body.moduleName === "string" ? body.moduleName : "",
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
case "delete_step":
|
||||||
|
view = await sessionManager.deleteStep(
|
||||||
|
sessionId,
|
||||||
|
typeof body.stepId === "string" ? body.stepId : "",
|
||||||
|
);
|
||||||
|
break;
|
||||||
case "run_outline":
|
case "run_outline":
|
||||||
view = await sessionManager.runOutline(sessionId);
|
view = await sessionManager.runOutline(sessionId);
|
||||||
break;
|
break;
|
||||||
@@ -524,6 +530,9 @@ const server = createServer(async (req, res) => {
|
|||||||
case "retry_run":
|
case "retry_run":
|
||||||
view = await sessionManager.abortAndRetry(sessionId);
|
view = await sessionManager.abortAndRetry(sessionId);
|
||||||
break;
|
break;
|
||||||
|
case "abort_run":
|
||||||
|
view = await sessionManager.abortRun(sessionId);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
json(res, 400, { error: "未知 action" });
|
json(res, 400, { error: "未知 action" });
|
||||||
return;
|
return;
|
||||||
|
|||||||
+95
-17
@@ -1224,21 +1224,6 @@ export function creationFlowFromRecipeSeed(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 已坐在配方开局步(无 depends_on)且「用户.需求」有首句时:
|
|
||||||
* 跳过程序再抛模块 opening——首句即该步对话,不是「先开场再进 DAG」。
|
|
||||||
*/
|
|
||||||
export function shouldSkipModuleOpening(opts: {
|
|
||||||
demand: string;
|
|
||||||
dependsOn?: string[] | null;
|
|
||||||
phase?: "shown" | "answered" | null;
|
|
||||||
}): boolean {
|
|
||||||
if (opts.phase === "shown" || opts.phase === "answered") return false;
|
|
||||||
if (!String(opts.demand ?? "").trim()) return false;
|
|
||||||
const deps = opts.dependsOn ?? [];
|
|
||||||
return deps.length === 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 无 id 时的兜底(目录仍应显式写 id) */
|
/** 无 id 时的兜底(目录仍应显式写 id) */
|
||||||
function slugFromName(name: string): string {
|
function slugFromName(name: string): string {
|
||||||
const map: Record<string, string> = {
|
const map: Record<string, string> = {
|
||||||
@@ -2623,17 +2608,110 @@ export function removeUnstartedInstance(
|
|||||||
if (!step || !isInstanceStep(step) || isStepAccepted(step, acceptedStepIds)) {
|
if (!step || !isInstanceStep(step) || isStepAccepted(step, acceptedStepIds)) {
|
||||||
return flow;
|
return flow;
|
||||||
}
|
}
|
||||||
|
return dropInstanceFromFlow(flow, step.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function dropInstanceFromFlow(flow: CreationFlow, stepId: string): CreationFlow {
|
||||||
return {
|
return {
|
||||||
...flow,
|
...flow,
|
||||||
steps: flow.steps
|
steps: flow.steps
|
||||||
.filter((s) => s.id !== step.id)
|
.filter((s) => s.id !== stepId)
|
||||||
.map((s) => ({
|
.map((s) => ({
|
||||||
...s,
|
...s,
|
||||||
depends_on: s.depends_on.filter((d) => d !== step.id),
|
depends_on: s.depends_on.filter((d) => d !== stepId),
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function repeatableInstanceLabel(step: CreationFlowStep): string {
|
||||||
|
const target = titleFromStepParams(step.params);
|
||||||
|
if (target && target !== step.name) return `${step.name} · ${target}`;
|
||||||
|
return step.id !== step.name ? `${step.name}(${step.id})` : step.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 图上删除可增殖产物:已验收也可删。
|
||||||
|
* 另有实例依赖这条时拒绝;收口等一次性节点只解开依赖。
|
||||||
|
*/
|
||||||
|
export function removeRepeatableInstance(params: {
|
||||||
|
flow: CreationFlow;
|
||||||
|
stepId: string;
|
||||||
|
catalog?: ModuleCatalog | null;
|
||||||
|
}): { flow: CreationFlow; removed: CreationFlowStep } | { error: string } {
|
||||||
|
const step = findStepByRef(params.flow, params.stepId.trim());
|
||||||
|
if (!step) return { error: "找不到节点" };
|
||||||
|
if (!isInstanceStep(step)) {
|
||||||
|
return { error: "只能删除可增殖能力长出来的产物" };
|
||||||
|
}
|
||||||
|
const mod = findModuleByName(params.catalog, step.name);
|
||||||
|
if (mod && mod.repeatable !== true) {
|
||||||
|
return { error: `「${step.name}」不是可增殖能力,不能从图上删` };
|
||||||
|
}
|
||||||
|
if (!mod && !step.from) {
|
||||||
|
return { error: "只能删除可增殖能力长出来的产物" };
|
||||||
|
}
|
||||||
|
const blockers = params.flow.steps.filter(
|
||||||
|
(s) =>
|
||||||
|
s.id !== step.id &&
|
||||||
|
isInstanceStep(s) &&
|
||||||
|
s.depends_on.includes(step.id),
|
||||||
|
);
|
||||||
|
if (blockers.length) {
|
||||||
|
return {
|
||||||
|
error: `还有产物依赖这条:${blockers.map(repeatableInstanceLabel).join("、")}。请先删那些产物。`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { flow: dropInstanceFromFlow(params.flow, step.id), removed: step };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 共享产物 tag 里按 rule_id 抽掉已删实例;同能力已无实例则清空。 */
|
||||||
|
export function pruneRepeatableArtifactContent(params: {
|
||||||
|
raw: string;
|
||||||
|
step: CreationFlowStep;
|
||||||
|
remainingSameModule: number;
|
||||||
|
}): string {
|
||||||
|
if (params.remainingSameModule <= 0) return "";
|
||||||
|
const stripped = stripFragmentRowsMatchingStep(params.raw, params.step);
|
||||||
|
return stripped ?? params.raw;
|
||||||
|
}
|
||||||
|
|
||||||
|
function stripFragmentRowsMatchingStep(
|
||||||
|
raw: string,
|
||||||
|
step: CreationFlowStep,
|
||||||
|
): string | null {
|
||||||
|
const parsed = tryParseJsonDoc(raw);
|
||||||
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const doc = structuredClone(parsed) as Record<string, unknown>;
|
||||||
|
const bodyRaw = doc.正文;
|
||||||
|
if (!bodyRaw || typeof bodyRaw !== "object" || Array.isArray(bodyRaw)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const body = bodyRaw as Record<string, unknown>;
|
||||||
|
const ruleId = flowParamText(step.params, "rule_id");
|
||||||
|
if (!ruleId) return null;
|
||||||
|
let changed = false;
|
||||||
|
for (const key of ["rules", "records"] as const) {
|
||||||
|
const rows = body[key];
|
||||||
|
if (!Array.isArray(rows)) continue;
|
||||||
|
const next = rows.filter((item) => {
|
||||||
|
if (!item || typeof item !== "object" || Array.isArray(item)) return true;
|
||||||
|
return (
|
||||||
|
String((item as Record<string, unknown>).rule_id ?? "").trim() !==
|
||||||
|
ruleId
|
||||||
|
);
|
||||||
|
});
|
||||||
|
if (next.length !== rows.length) {
|
||||||
|
body[key] = next;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!changed) return null;
|
||||||
|
doc.正文 = body;
|
||||||
|
return JSON.stringify(doc, null, 2);
|
||||||
|
}
|
||||||
|
|
||||||
/** 实例步 depends_on:具体实例钉最近已验收规则实例;其余跟原型。 */
|
/** 实例步 depends_on:具体实例钉最近已验收规则实例;其余跟原型。 */
|
||||||
function resolveInstanceDependsOn(params: {
|
function resolveInstanceDependsOn(params: {
|
||||||
flow: CreationFlow;
|
flow: CreationFlow;
|
||||||
|
|||||||
@@ -622,6 +622,17 @@ export function extractUnitContentFromDraft(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 从图上删除产物时拿掉该单位的验收切片 */
|
||||||
|
export function dropAcceptedUnitContent(
|
||||||
|
existingRaw: unknown,
|
||||||
|
unitId: string,
|
||||||
|
): string {
|
||||||
|
const store = parseAcceptedContentStore(existingRaw);
|
||||||
|
const id = normalizeCreationUnitId(unitId);
|
||||||
|
if (id) delete store.units[id];
|
||||||
|
return JSON.stringify(store, null, 2);
|
||||||
|
}
|
||||||
|
|
||||||
/** 写入/覆盖某一单位的最后验收内容 */
|
/** 写入/覆盖某一单位的最后验收内容 */
|
||||||
export function upsertAcceptedUnitContent(
|
export function upsertAcceptedUnitContent(
|
||||||
existingRaw: unknown,
|
existingRaw: unknown,
|
||||||
|
|||||||
@@ -13,6 +13,36 @@ export const MAINTAIN_GENERATE_TAG = "运行.本轮.旁观.生成抽样";
|
|||||||
|
|
||||||
type PoolEntry = { id?: string; 内容?: string; 权重?: number; weight?: number };
|
type PoolEntry = { id?: string; 内容?: string; 权重?: number; weight?: number };
|
||||||
|
|
||||||
|
const DIRECTION_KINDS = new Set(["方向", "方向池", "type", "类型", "类型池"]);
|
||||||
|
const ELEMENT_KINDS = new Set(["元素", "元素池", "enum", "枚举", "枚举池"]);
|
||||||
|
|
||||||
|
function poolKindToken(pool: Record<string, unknown>): string | undefined {
|
||||||
|
return (
|
||||||
|
asString(pool.池型) ??
|
||||||
|
asString(pool.kind) ??
|
||||||
|
asString(pool.类型) ??
|
||||||
|
asString(pool.pool_type)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 方向池给 LLM 当创作范围,不能程序加权抽。 */
|
||||||
|
export function isElementPool(pool: Record<string, unknown>): boolean {
|
||||||
|
const kind = poolKindToken(pool);
|
||||||
|
if (kind && DIRECTION_KINDS.has(kind)) return false;
|
||||||
|
if (kind && ELEMENT_KINDS.has(kind)) return true;
|
||||||
|
const dirs = pool.方向;
|
||||||
|
const hasDirection =
|
||||||
|
(dirs != null &&
|
||||||
|
typeof dirs === "object" &&
|
||||||
|
!Array.isArray(dirs) &&
|
||||||
|
Object.keys(dirs as object).length > 0) ||
|
||||||
|
(Array.isArray(dirs) && dirs.length > 0);
|
||||||
|
const entries = pool.条目 ?? pool.entries;
|
||||||
|
const hasEntries = Array.isArray(entries) && entries.length > 0;
|
||||||
|
if (hasDirection && !hasEntries) return false;
|
||||||
|
return hasEntries;
|
||||||
|
}
|
||||||
|
|
||||||
function asString(v: unknown): string | undefined {
|
function asString(v: unknown): string | undefined {
|
||||||
if (typeof v !== "string") return undefined;
|
if (typeof v !== "string") return undefined;
|
||||||
const t = v.trim();
|
const t = v.trim();
|
||||||
@@ -61,6 +91,7 @@ export function buildChanceBatchFromGenerationRule(
|
|||||||
for (const poolRaw of pools) {
|
for (const poolRaw of pools) {
|
||||||
if (!poolRaw || typeof poolRaw !== "object" || Array.isArray(poolRaw)) continue;
|
if (!poolRaw || typeof poolRaw !== "object" || Array.isArray(poolRaw)) continue;
|
||||||
const pool = poolRaw as Record<string, unknown>;
|
const pool = poolRaw as Record<string, unknown>;
|
||||||
|
if (!isElementPool(pool)) continue;
|
||||||
const poolId =
|
const poolId =
|
||||||
asString(pool.pool_id) ?? asString(pool.池id) ?? asString(pool.名称);
|
asString(pool.pool_id) ?? asString(pool.池id) ?? asString(pool.名称);
|
||||||
const entries = (pool.条目 ?? pool.entries) as PoolEntry[] | undefined;
|
const entries = (pool.条目 ?? pool.entries) as PoolEntry[] | undefined;
|
||||||
|
|||||||
+89
-26
@@ -28,6 +28,8 @@ import {
|
|||||||
hasSelectableCreationWork,
|
hasSelectableCreationWork,
|
||||||
hasAcceptedCloserStep,
|
hasAcceptedCloserStep,
|
||||||
removeUnstartedInstance,
|
removeUnstartedInstance,
|
||||||
|
removeRepeatableInstance,
|
||||||
|
pruneRepeatableArtifactContent,
|
||||||
looksLikeCreationFlowDoc,
|
looksLikeCreationFlowDoc,
|
||||||
parseCreationFlow,
|
parseCreationFlow,
|
||||||
pickRecordedStepId,
|
pickRecordedStepId,
|
||||||
@@ -37,7 +39,6 @@ import {
|
|||||||
parseRecipeYaml,
|
parseRecipeYaml,
|
||||||
parseSelectedRecipeRef,
|
parseSelectedRecipeRef,
|
||||||
resolveDesignStepBinding,
|
resolveDesignStepBinding,
|
||||||
shouldSkipModuleOpening,
|
|
||||||
isProgressPointerTag,
|
isProgressPointerTag,
|
||||||
stringifyCreationFlow,
|
stringifyCreationFlow,
|
||||||
stripReviseSteps,
|
stripReviseSteps,
|
||||||
@@ -776,31 +777,6 @@ modules:
|
|||||||
美学纲领与交互范式: "shown",
|
美学纲领与交互范式: "shown",
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(
|
|
||||||
shouldSkipModuleOpening({
|
|
||||||
demand: "丧尸世界只有我不会被感染",
|
|
||||||
dependsOn: [],
|
|
||||||
}),
|
|
||||||
).toBe(true);
|
|
||||||
expect(
|
|
||||||
shouldSkipModuleOpening({
|
|
||||||
demand: "丧尸世界只有我不会被感染",
|
|
||||||
dependsOn: ["美学纲领与交互范式"],
|
|
||||||
}),
|
|
||||||
).toBe(false);
|
|
||||||
expect(
|
|
||||||
shouldSkipModuleOpening({
|
|
||||||
demand: "",
|
|
||||||
dependsOn: [],
|
|
||||||
}),
|
|
||||||
).toBe(false);
|
|
||||||
expect(
|
|
||||||
shouldSkipModuleOpening({
|
|
||||||
demand: "已有需求",
|
|
||||||
dependsOn: [],
|
|
||||||
phase: "shown",
|
|
||||||
}),
|
|
||||||
).toBe(false);
|
|
||||||
expect(isProgressPointerTag("创作.当前步骤")).toBe(true);
|
expect(isProgressPointerTag("创作.当前步骤")).toBe(true);
|
||||||
expect(isProgressPointerTag("设计.美学纲领与交互范式")).toBe(false);
|
expect(isProgressPointerTag("设计.美学纲领与交互范式")).toBe(false);
|
||||||
expect(isProgressPointerTag("设计.创作流程")).toBe(false);
|
expect(isProgressPointerTag("设计.创作流程")).toBe(false);
|
||||||
@@ -1253,6 +1229,93 @@ modules:
|
|||||||
expect(accepted.steps.some((s) => s.id === "生成规则#1")).toBe(true);
|
expect(accepted.steps.some((s) => s.id === "生成规则#1")).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("removeRepeatableInstance drops accepted spawn and closer dep", () => {
|
||||||
|
const flow = parseCreationFlow(`{
|
||||||
|
"status": "open",
|
||||||
|
"steps": [
|
||||||
|
{ "name": "美学纲领与交互范式", "depends_on": [] },
|
||||||
|
{ "id": "生成规则", "name": "生成规则", "role": "prototype", "depends_on": ["美学纲领与交互范式"] },
|
||||||
|
{ "id": "生成规则#1", "name": "生成规则", "role": "instance", "from": "生成规则", "depends_on": ["美学纲领与交互范式"], "params": { "rule_id": "core-female-roles" } },
|
||||||
|
{ "name": "开场白与开场变量", "depends_on": ["美学纲领与交互范式", "生成规则#1"] }
|
||||||
|
]
|
||||||
|
}`)!;
|
||||||
|
const dropped = removeRepeatableInstance({
|
||||||
|
flow,
|
||||||
|
stepId: "生成规则#1",
|
||||||
|
catalog: sampleCatalog,
|
||||||
|
});
|
||||||
|
expect("error" in dropped).toBe(false);
|
||||||
|
if ("error" in dropped) return;
|
||||||
|
expect(dropped.flow.steps.map((s) => s.id)).toEqual([
|
||||||
|
"美学纲领与交互范式",
|
||||||
|
"生成规则",
|
||||||
|
"开场白与开场变量",
|
||||||
|
]);
|
||||||
|
expect(
|
||||||
|
dropped.flow.steps.find((s) => s.name === "开场白与开场变量")?.depends_on,
|
||||||
|
).toEqual(["美学纲领与交互范式"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("removeRepeatableInstance blocks when another instance depends on it", () => {
|
||||||
|
const flow = parseCreationFlow(`{
|
||||||
|
"steps": [
|
||||||
|
{ "name": "美学纲领与交互范式", "depends_on": [] },
|
||||||
|
{ "id": "生成规则", "name": "生成规则", "role": "prototype", "depends_on": ["美学纲领与交互范式"] },
|
||||||
|
{ "id": "生成规则#1", "name": "生成规则", "role": "instance", "from": "生成规则", "depends_on": ["美学纲领与交互范式"], "params": { "target": "4位核心女性", "rule_id": "core-female-roles" } },
|
||||||
|
{ "id": "具体实例", "name": "具体实例", "role": "prototype", "depends_on": ["生成规则"] },
|
||||||
|
{ "id": "具体实例#1", "name": "具体实例", "role": "instance", "from": "具体实例", "depends_on": ["生成规则#1"] }
|
||||||
|
]
|
||||||
|
}`)!;
|
||||||
|
const blocked = removeRepeatableInstance({
|
||||||
|
flow,
|
||||||
|
stepId: "生成规则#1",
|
||||||
|
catalog: sampleCatalog,
|
||||||
|
});
|
||||||
|
expect(blocked).toMatchObject({ error: expect.stringContaining("具体实例") });
|
||||||
|
const ok = removeRepeatableInstance({
|
||||||
|
flow,
|
||||||
|
stepId: "具体实例#1",
|
||||||
|
catalog: sampleCatalog,
|
||||||
|
});
|
||||||
|
expect("error" in ok).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("pruneRepeatableArtifactContent strips the deleted rule_id", () => {
|
||||||
|
const raw = JSON.stringify({
|
||||||
|
schema: "context-fragment.v1",
|
||||||
|
技能: "生成规则",
|
||||||
|
brief: "两条规则",
|
||||||
|
正文: {
|
||||||
|
rules: [
|
||||||
|
{ rule_id: "core-female-roles", 对象: "女臣" },
|
||||||
|
{ rule_id: "zombies", 对象: "丧尸" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const step = {
|
||||||
|
id: "生成规则#1",
|
||||||
|
name: "生成规则",
|
||||||
|
role: "instance" as const,
|
||||||
|
depends_on: [],
|
||||||
|
params: { rule_id: "core-female-roles" },
|
||||||
|
};
|
||||||
|
const pruned = JSON.parse(
|
||||||
|
pruneRepeatableArtifactContent({
|
||||||
|
raw,
|
||||||
|
step,
|
||||||
|
remainingSameModule: 1,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(pruned.正文.rules.map((r) => r.rule_id)).toEqual(["zombies"]);
|
||||||
|
expect(
|
||||||
|
pruneRepeatableArtifactContent({
|
||||||
|
raw,
|
||||||
|
step,
|
||||||
|
remainingSameModule: 0,
|
||||||
|
}),
|
||||||
|
).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
it("blocks 具体实例 spawn until a 生成规则 instance is accepted", () => {
|
it("blocks 具体实例 spawn until a 生成规则 instance is accepted", () => {
|
||||||
const flow = parseCreationFlow(`{
|
const flow = parseCreationFlow(`{
|
||||||
"steps": [
|
"steps": [
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import {
|
import {
|
||||||
buildChanceBatchFromGenerationRule,
|
buildChanceBatchFromGenerationRule,
|
||||||
|
isElementPool,
|
||||||
runMaintainNeedGenerateSampling,
|
runMaintainNeedGenerateSampling,
|
||||||
} from "../src/skills/maintain-need-generate.js";
|
} from "../src/skills/maintain-need-generate.js";
|
||||||
|
|
||||||
@@ -40,4 +41,61 @@ describe("maintain need_generate sampling", () => {
|
|||||||
expect(result?.schema).toBe("chance.batch.v1");
|
expect(result?.schema).toBe("chance.batch.v1");
|
||||||
expect(result?.results[0]?.id).toBe("pool:mood");
|
expect(result?.results[0]?.id).toBe("pool:mood");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("treats 方向池 as non-element even if leftover 条目 exist", () => {
|
||||||
|
expect(
|
||||||
|
isElementPool({
|
||||||
|
池型: "方向",
|
||||||
|
方向: { 冷焰系: "静、刺、余烬不散,如…" },
|
||||||
|
条目: [{ id: "should-not-draw" }],
|
||||||
|
}),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("skips 方向池 when building chance batch", () => {
|
||||||
|
const rules = JSON.stringify({
|
||||||
|
正文: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
rule_id: "stand",
|
||||||
|
池: [
|
||||||
|
{
|
||||||
|
pool_id: "ability",
|
||||||
|
池型: "方向",
|
||||||
|
方向: { 时空操作系: "暂停、加速、回溯。例如 The World" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pool_id: "stand-type",
|
||||||
|
池型: "元素",
|
||||||
|
条目: [{ id: "close-range", 权重: 1 }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const batch = buildChanceBatchFromGenerationRule(rules, "stand");
|
||||||
|
expect(batch).toHaveLength(1);
|
||||||
|
expect(batch![0]!.id).toBe("pool:stand-type");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns null when a rule only has 方向池", () => {
|
||||||
|
const rules = JSON.stringify({
|
||||||
|
正文: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
rule_id: "nickname",
|
||||||
|
池: [
|
||||||
|
{
|
||||||
|
pool_id: "nick-style",
|
||||||
|
池型: "方向",
|
||||||
|
方向: { 自然现象系: "以天气、地理命名" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(buildChanceBatchFromGenerationRule(rules, "nickname")).toBeNull();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -658,6 +658,32 @@ describe("phase machine", () => {
|
|||||||
expect(left.effects).toEqual([]);
|
expect(left.effects).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("leaving from review_artifact returns to the pick graph", () => {
|
||||||
|
let session = createSession("default");
|
||||||
|
const artifact = createArtifact({
|
||||||
|
workerId: "design-step",
|
||||||
|
stepId: "生成规则#1",
|
||||||
|
outputTags: ["设计.生成规则"],
|
||||||
|
});
|
||||||
|
session = {
|
||||||
|
...session,
|
||||||
|
phase: "waiting_user",
|
||||||
|
waitingReason: { kind: "review_artifact", artifactId: artifact.id },
|
||||||
|
pendingArtifactId: artifact.id,
|
||||||
|
artifacts: [artifact],
|
||||||
|
currentWorkerId: "design-step",
|
||||||
|
currentStepId: "生成规则#1",
|
||||||
|
};
|
||||||
|
const left = applyEvent(session, {
|
||||||
|
type: "user_left_creation_step",
|
||||||
|
payload: {},
|
||||||
|
});
|
||||||
|
expect(left.session.phase).toBe("waiting_user");
|
||||||
|
expect(left.session.waitingReason?.kind).toBe("pick_creation_step");
|
||||||
|
expect(left.session.pendingArtifactId).toBeUndefined();
|
||||||
|
expect(left.session.currentStepId).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
it("replan from the pick graph runs design-flow", () => {
|
it("replan from the pick graph runs design-flow", () => {
|
||||||
let session = createSession("default");
|
let session = createSession("default");
|
||||||
session = {
|
session = {
|
||||||
|
|||||||
+468
-1
@@ -3,12 +3,36 @@ import {
|
|||||||
createMockMainAgentResponse,
|
createMockMainAgentResponse,
|
||||||
createMockToolCall,
|
createMockToolCall,
|
||||||
MockLlmProvider,
|
MockLlmProvider,
|
||||||
|
type LlmProvider,
|
||||||
} 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 { createArtifact, createSession } from "../src/runtime/phase-machine.js";
|
||||||
import { Blackboard } from "../src/blackboard/blackboard.js";
|
import { Blackboard } from "../src/blackboard/blackboard.js";
|
||||||
import { parseCreationFlow } from "../src/skills/creation-flow.js";
|
import { parseCreationFlow } from "../src/skills/creation-flow.js";
|
||||||
|
|
||||||
|
function countLlmCalls(llm: MockLlmProvider): { calls: () => number } {
|
||||||
|
let n = 0;
|
||||||
|
const bump = () => {
|
||||||
|
n += 1;
|
||||||
|
};
|
||||||
|
const origComplete = llm.complete.bind(llm);
|
||||||
|
llm.complete = (async (messages, options) => {
|
||||||
|
bump();
|
||||||
|
return origComplete(messages, options);
|
||||||
|
}) as LlmProvider["complete"];
|
||||||
|
const origStream = llm.completeStream.bind(llm);
|
||||||
|
llm.completeStream = (async (messages, options, callbacks) => {
|
||||||
|
bump();
|
||||||
|
return origStream(messages, options, callbacks);
|
||||||
|
}) as LlmProvider["completeStream"];
|
||||||
|
const origTools = llm.completeWithTools.bind(llm);
|
||||||
|
llm.completeWithTools = (async (messages, options) => {
|
||||||
|
bump();
|
||||||
|
return origTools(messages, options);
|
||||||
|
}) as LlmProvider["completeWithTools"];
|
||||||
|
return { calls: () => n };
|
||||||
|
}
|
||||||
|
|
||||||
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 () => {
|
||||||
const runtime = new PhaseRuntime();
|
const runtime = new PhaseRuntime();
|
||||||
@@ -178,6 +202,120 @@ describe("phase runtime", () => {
|
|||||||
expect(runtime.getSession().waitingReason?.kind).toBe("pick_creation_step");
|
expect(runtime.getSession().waitingReason?.kind).toBe("pick_creation_step");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("picking a node shows module opening instead of calling LLM", async () => {
|
||||||
|
const llm = new MockLlmProvider([
|
||||||
|
JSON.stringify({
|
||||||
|
outputs: { "设计.舞台骨架": { 范围: "不该到这一步" } },
|
||||||
|
summary: "不应生成",
|
||||||
|
askUser: null,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
const llmCalls = countLlmCalls(llm);
|
||||||
|
const runtime = new PhaseRuntime({ llm });
|
||||||
|
await runtime.start();
|
||||||
|
runtime.getBlackboard().write({
|
||||||
|
tag: "设计.创作流程",
|
||||||
|
content: JSON.stringify({
|
||||||
|
version: 1,
|
||||||
|
status: "open",
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
id: "舞台骨架",
|
||||||
|
name: "舞台骨架",
|
||||||
|
depends_on: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
source: "runtime",
|
||||||
|
});
|
||||||
|
await runtime.enterSeededCreationPick();
|
||||||
|
await runtime.pickCreationStep("舞台骨架");
|
||||||
|
const session = runtime.getSession();
|
||||||
|
expect(session.phase).toBe("waiting_user");
|
||||||
|
expect(session.waitingReason?.kind).toBe("worker_questions");
|
||||||
|
if (session.waitingReason?.kind === "worker_questions") {
|
||||||
|
expect(session.waitingReason.questions[0]?.id).toBe("module-opening");
|
||||||
|
expect(session.waitingReason.questions[0]?.prompt).toContain(
|
||||||
|
"玩家实际会待着玩的地方",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
expect(llmCalls.calls()).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("picking a root node still shows opening even if 用户.需求 already has text", async () => {
|
||||||
|
const llm = new MockLlmProvider([
|
||||||
|
JSON.stringify({
|
||||||
|
outputs: { "设计.舞台骨架": { 范围: "不该到这一步" } },
|
||||||
|
summary: "不应生成",
|
||||||
|
askUser: null,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
const llmCalls = countLlmCalls(llm);
|
||||||
|
const runtime = new PhaseRuntime({ llm });
|
||||||
|
await runtime.start();
|
||||||
|
runtime.getBlackboard().write({
|
||||||
|
tag: "设计.创作流程",
|
||||||
|
content: JSON.stringify({
|
||||||
|
version: 1,
|
||||||
|
status: "open",
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
id: "舞台骨架",
|
||||||
|
name: "舞台骨架",
|
||||||
|
depends_on: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
source: "runtime",
|
||||||
|
});
|
||||||
|
runtime.getBlackboard().write({
|
||||||
|
tag: "用户.需求",
|
||||||
|
content: "丧尸世界只有我不会被感染",
|
||||||
|
source: "user",
|
||||||
|
});
|
||||||
|
runtime.getSession().slots["用户.需求"] = "丧尸世界只有我不会被感染";
|
||||||
|
await runtime.enterSeededCreationPick();
|
||||||
|
await runtime.pickCreationStep("舞台骨架");
|
||||||
|
expect(runtime.getSession().waitingReason?.kind).toBe("worker_questions");
|
||||||
|
expect(llmCalls.calls()).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("leaving during opening and picking again shows opening instead of calling LLM", async () => {
|
||||||
|
const llm = new MockLlmProvider([
|
||||||
|
JSON.stringify({
|
||||||
|
outputs: { "设计.舞台骨架": { 范围: "不该到这一步" } },
|
||||||
|
summary: "不应生成",
|
||||||
|
askUser: null,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
const llmCalls = countLlmCalls(llm);
|
||||||
|
const runtime = new PhaseRuntime({ llm });
|
||||||
|
await runtime.start();
|
||||||
|
runtime.getBlackboard().write({
|
||||||
|
tag: "设计.创作流程",
|
||||||
|
content: JSON.stringify({
|
||||||
|
version: 1,
|
||||||
|
status: "open",
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
id: "舞台骨架",
|
||||||
|
name: "舞台骨架",
|
||||||
|
depends_on: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
source: "runtime",
|
||||||
|
});
|
||||||
|
await runtime.enterSeededCreationPick();
|
||||||
|
await runtime.pickCreationStep("舞台骨架");
|
||||||
|
expect(runtime.getSession().waitingReason?.kind).toBe("worker_questions");
|
||||||
|
await runtime.leaveCreationStep();
|
||||||
|
expect(runtime.getSession().waitingReason?.kind).toBe("pick_creation_step");
|
||||||
|
await runtime.pickCreationStep("舞台骨架");
|
||||||
|
expect(runtime.getSession().waitingReason?.kind).toBe("worker_questions");
|
||||||
|
expect(llmCalls.calls()).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
it("unusable main agent output without DAG starts design-flow", async () => {
|
it("unusable main agent output without DAG starts design-flow", async () => {
|
||||||
const llm = new MockLlmProvider(["Let me think about routing"]);
|
const llm = new MockLlmProvider(["Let me think about routing"]);
|
||||||
const runtime = new PhaseRuntime({ llm, autoStubWorker: true });
|
const runtime = new PhaseRuntime({ llm, autoStubWorker: true });
|
||||||
@@ -294,6 +432,335 @@ describe("phase runtime", () => {
|
|||||||
).toEqual(["美学纲领与交互范式"]);
|
).toEqual(["美学纲领与交互范式"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("leaveCreationStep from review_artifact returns to pick without unsawning a filled instance", async () => {
|
||||||
|
const artifact = createArtifact({
|
||||||
|
workerId: "design-step",
|
||||||
|
stepId: "生成规则#1",
|
||||||
|
outputTags: ["设计.生成规则"],
|
||||||
|
});
|
||||||
|
const session = createSession("default");
|
||||||
|
session.phase = "waiting_user";
|
||||||
|
session.waitingReason = { kind: "review_artifact", artifactId: artifact.id };
|
||||||
|
session.pendingArtifactId = artifact.id;
|
||||||
|
session.currentWorkerId = "design-step";
|
||||||
|
session.currentStepId = "生成规则#1";
|
||||||
|
session.artifacts = [artifact];
|
||||||
|
session.slots = { startupCompleted: true };
|
||||||
|
const blackboard = new Blackboard();
|
||||||
|
blackboard.write({
|
||||||
|
tag: "设计.创作流程",
|
||||||
|
content: JSON.stringify({
|
||||||
|
version: 1,
|
||||||
|
status: "open",
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
id: "美学纲领与交互范式",
|
||||||
|
name: "美学纲领与交互范式",
|
||||||
|
depends_on: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "生成规则",
|
||||||
|
name: "生成规则",
|
||||||
|
role: "prototype",
|
||||||
|
depends_on: ["美学纲领与交互范式"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "生成规则#1",
|
||||||
|
name: "生成规则",
|
||||||
|
role: "instance",
|
||||||
|
from: "生成规则",
|
||||||
|
depends_on: ["美学纲领与交互范式"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
source: "runtime",
|
||||||
|
});
|
||||||
|
blackboard.write({
|
||||||
|
tag: "创作.当前步骤",
|
||||||
|
content: "生成规则#1",
|
||||||
|
source: "runtime",
|
||||||
|
});
|
||||||
|
const runtime = new PhaseRuntime({
|
||||||
|
initialSession: session,
|
||||||
|
initialBlackboardItems: blackboard.exportItems(),
|
||||||
|
});
|
||||||
|
await runtime.leaveCreationStep();
|
||||||
|
expect(runtime.getSession().waitingReason?.kind).toBe("pick_creation_step");
|
||||||
|
expect(runtime.getSession().pendingArtifactId).toBeUndefined();
|
||||||
|
const flow = parseCreationFlow(
|
||||||
|
runtime.getBlackboard().getContentByTag("设计.创作流程"),
|
||||||
|
);
|
||||||
|
expect(flow?.steps.map((s) => s.id)).toContain("生成规则#1");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("abortCurrentRun restores the snapshot and waits instead of rerunning", 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 artifactsAtSnapshot = snap!.runtimeSession.artifacts.length;
|
||||||
|
|
||||||
|
const session = runtime.getSession();
|
||||||
|
session.phase = "running";
|
||||||
|
session.waitingReason = undefined;
|
||||||
|
session.currentWorkerId = "design-flow";
|
||||||
|
|
||||||
|
runtime.abortCurrentRun();
|
||||||
|
expect(runtime.getSession().phase).toBe("waiting_user");
|
||||||
|
expect(runtime.getSession().currentWorkerId).toBeUndefined();
|
||||||
|
expect(runtime.getSession().artifacts.length).toBe(artifactsAtSnapshot);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reentering an accepted instance reopens its artifact for revision", async () => {
|
||||||
|
const artifact = {
|
||||||
|
...createArtifact({
|
||||||
|
workerId: "design-step",
|
||||||
|
stepId: "生成规则#1",
|
||||||
|
outputTags: ["设计.生成规则"],
|
||||||
|
}),
|
||||||
|
status: "accepted",
|
||||||
|
};
|
||||||
|
const session = createSession("default");
|
||||||
|
session.phase = "waiting_user";
|
||||||
|
session.waitingReason = { kind: "pick_creation_step" };
|
||||||
|
session.slots = {
|
||||||
|
startupCompleted: true,
|
||||||
|
creationAcceptedUnits: ["生成规则#1"],
|
||||||
|
};
|
||||||
|
session.artifacts = [artifact];
|
||||||
|
const blackboard = new Blackboard();
|
||||||
|
blackboard.write({
|
||||||
|
tag: "设计.创作流程",
|
||||||
|
content: JSON.stringify({
|
||||||
|
version: 1,
|
||||||
|
status: "open",
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
id: "美学纲领与交互范式",
|
||||||
|
name: "美学纲领与交互范式",
|
||||||
|
depends_on: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "生成规则",
|
||||||
|
name: "生成规则",
|
||||||
|
role: "prototype",
|
||||||
|
depends_on: ["美学纲领与交互范式"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "生成规则#1",
|
||||||
|
name: "生成规则",
|
||||||
|
role: "instance",
|
||||||
|
from: "生成规则",
|
||||||
|
depends_on: ["美学纲领与交互范式"],
|
||||||
|
params: { target: "4位核心女性" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
source: "runtime",
|
||||||
|
});
|
||||||
|
blackboard.write({
|
||||||
|
tag: "创作.已验收单位",
|
||||||
|
content: JSON.stringify(["生成规则#1"]),
|
||||||
|
source: "runtime",
|
||||||
|
});
|
||||||
|
blackboard.write({
|
||||||
|
tag: "设计.生成规则",
|
||||||
|
content: "已有规则正文",
|
||||||
|
source: "runtime",
|
||||||
|
});
|
||||||
|
const runtime = new PhaseRuntime({
|
||||||
|
initialSession: session,
|
||||||
|
initialBlackboardItems: blackboard.exportItems(),
|
||||||
|
});
|
||||||
|
await runtime.pickCreationStep("生成规则#1", undefined, { reenter: true });
|
||||||
|
const next = runtime.getSession();
|
||||||
|
expect(next.waitingReason?.kind).toBe("review_artifact");
|
||||||
|
if (next.waitingReason?.kind === "review_artifact") {
|
||||||
|
expect(next.waitingReason.artifactId).toBe(artifact.id);
|
||||||
|
}
|
||||||
|
expect(next.pendingArtifactId).toBe(artifact.id);
|
||||||
|
expect(next.currentStepId).toBe("生成规则#1");
|
||||||
|
expect(next.phase).toBe("waiting_user");
|
||||||
|
expect(runtime.getBlackboard().getContentByTag("创作.继承修改")).toBe("1");
|
||||||
|
expect(next.slots.revisionTargetArtifactId).toBe(artifact.id);
|
||||||
|
expect(
|
||||||
|
next.artifacts.find((a) => a.id === artifact.id)?.status,
|
||||||
|
).toBe("under_review");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("revising a reentered accepted instance does not bounce to the node graph", async () => {
|
||||||
|
const artifact = {
|
||||||
|
...createArtifact({
|
||||||
|
workerId: "design-step",
|
||||||
|
stepId: "生成规则#1",
|
||||||
|
outputTags: ["设计.生成规则"],
|
||||||
|
}),
|
||||||
|
status: "accepted",
|
||||||
|
};
|
||||||
|
const session = createSession("default");
|
||||||
|
session.phase = "waiting_user";
|
||||||
|
session.waitingReason = { kind: "pick_creation_step" };
|
||||||
|
session.slots = {
|
||||||
|
startupCompleted: true,
|
||||||
|
creationAcceptedUnits: ["生成规则#1"],
|
||||||
|
};
|
||||||
|
session.artifacts = [artifact];
|
||||||
|
const blackboard = new Blackboard();
|
||||||
|
blackboard.write({
|
||||||
|
tag: "设计.创作流程",
|
||||||
|
content: JSON.stringify({
|
||||||
|
version: 1,
|
||||||
|
status: "open",
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
id: "美学纲领与交互范式",
|
||||||
|
name: "美学纲领与交互范式",
|
||||||
|
depends_on: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "生成规则",
|
||||||
|
name: "生成规则",
|
||||||
|
role: "prototype",
|
||||||
|
depends_on: ["美学纲领与交互范式"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "生成规则#1",
|
||||||
|
name: "生成规则",
|
||||||
|
role: "instance",
|
||||||
|
from: "生成规则",
|
||||||
|
depends_on: ["美学纲领与交互范式"],
|
||||||
|
params: { target: "4位核心女性" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
source: "runtime",
|
||||||
|
});
|
||||||
|
blackboard.write({
|
||||||
|
tag: "创作.已验收单位",
|
||||||
|
content: JSON.stringify(["生成规则#1"]),
|
||||||
|
source: "runtime",
|
||||||
|
});
|
||||||
|
blackboard.write({
|
||||||
|
tag: "设计.生成规则",
|
||||||
|
content: "已有规则正文",
|
||||||
|
source: "runtime",
|
||||||
|
});
|
||||||
|
const runtime = new PhaseRuntime({
|
||||||
|
initialSession: session,
|
||||||
|
initialBlackboardItems: blackboard.exportItems(),
|
||||||
|
});
|
||||||
|
await runtime.pickCreationStep("生成规则#1", undefined, { reenter: true });
|
||||||
|
await runtime.rejectArtifact("把对象改成四位男性");
|
||||||
|
const next = runtime.getSession();
|
||||||
|
expect(next.waitingReason?.kind).not.toBe("pick_creation_step");
|
||||||
|
expect(next.phase).toBe("running");
|
||||||
|
expect(next.currentWorkerId).toBe("design-step");
|
||||||
|
expect(next.slots.revisionTargetArtifactId).toBe(artifact.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("deleteCreationStep removes an accepted repeatable instance", async () => {
|
||||||
|
const artifact = {
|
||||||
|
...createArtifact({
|
||||||
|
workerId: "design-step",
|
||||||
|
stepId: "生成规则#1",
|
||||||
|
outputTags: ["设计.生成规则"],
|
||||||
|
}),
|
||||||
|
status: "accepted",
|
||||||
|
};
|
||||||
|
const session = createSession("default");
|
||||||
|
session.phase = "waiting_user";
|
||||||
|
session.waitingReason = { kind: "pick_creation_step" };
|
||||||
|
session.slots = {
|
||||||
|
startupCompleted: true,
|
||||||
|
creationAcceptedUnits: ["美学纲领与交互范式", "生成规则#1"],
|
||||||
|
};
|
||||||
|
session.artifacts = [artifact];
|
||||||
|
const blackboard = new Blackboard();
|
||||||
|
blackboard.write({
|
||||||
|
tag: "设计.创作流程",
|
||||||
|
content: JSON.stringify({
|
||||||
|
version: 1,
|
||||||
|
status: "open",
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
id: "美学纲领与交互范式",
|
||||||
|
name: "美学纲领与交互范式",
|
||||||
|
depends_on: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "生成规则",
|
||||||
|
name: "生成规则",
|
||||||
|
role: "prototype",
|
||||||
|
depends_on: ["美学纲领与交互范式"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "生成规则#1",
|
||||||
|
name: "生成规则",
|
||||||
|
role: "instance",
|
||||||
|
from: "生成规则",
|
||||||
|
depends_on: ["美学纲领与交互范式"],
|
||||||
|
params: { target: "4位核心女性", rule_id: "core-female-roles" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "开场白与开场变量",
|
||||||
|
name: "开场白与开场变量",
|
||||||
|
depends_on: ["美学纲领与交互范式", "生成规则#1"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
source: "runtime",
|
||||||
|
});
|
||||||
|
blackboard.write({
|
||||||
|
tag: "创作.已验收单位",
|
||||||
|
content: JSON.stringify(["美学纲领与交互范式", "生成规则#1"]),
|
||||||
|
source: "runtime",
|
||||||
|
});
|
||||||
|
blackboard.write({
|
||||||
|
tag: "设计.生成规则",
|
||||||
|
content: JSON.stringify({
|
||||||
|
schema: "context-fragment.v1",
|
||||||
|
技能: "生成规则",
|
||||||
|
brief: "女臣",
|
||||||
|
正文: { rules: [{ rule_id: "core-female-roles", 对象: "女臣" }] },
|
||||||
|
}),
|
||||||
|
source: "runtime",
|
||||||
|
});
|
||||||
|
const runtime = new PhaseRuntime({
|
||||||
|
initialSession: session,
|
||||||
|
initialBlackboardItems: blackboard.exportItems(),
|
||||||
|
});
|
||||||
|
await runtime.deleteCreationStep("生成规则#1");
|
||||||
|
const flow = parseCreationFlow(
|
||||||
|
runtime.getBlackboard().getContentByTag("设计.创作流程"),
|
||||||
|
);
|
||||||
|
expect(flow?.steps.map((s) => s.id)).toEqual([
|
||||||
|
"美学纲领与交互范式",
|
||||||
|
"生成规则",
|
||||||
|
"开场白与开场变量",
|
||||||
|
]);
|
||||||
|
expect(
|
||||||
|
flow?.steps.find((s) => s.name === "开场白与开场变量")?.depends_on,
|
||||||
|
).toEqual(["美学纲领与交互范式"]);
|
||||||
|
expect(runtime.getSession().slots.creationAcceptedUnits).toEqual([
|
||||||
|
"美学纲领与交互范式",
|
||||||
|
]);
|
||||||
|
expect(
|
||||||
|
runtime.getSession().artifacts.find((a) => a.id === artifact.id)?.status,
|
||||||
|
).toBe("rejected");
|
||||||
|
await expect(
|
||||||
|
runtime.deleteCreationStep("美学纲领与交互范式"),
|
||||||
|
).rejects.toThrow(/可增殖/);
|
||||||
|
});
|
||||||
|
|
||||||
it("prepareEnterPlay seals when closer is already accepted", () => {
|
it("prepareEnterPlay seals when closer is already accepted", () => {
|
||||||
const session = createSession("default");
|
const session = createSession("default");
|
||||||
const blackboard = new Blackboard();
|
const blackboard = new Blackboard();
|
||||||
|
|||||||
+469
-53
@@ -197,8 +197,10 @@ function hideFlowNodeMenu() {
|
|||||||
const flowMenuState = {
|
const flowMenuState = {
|
||||||
stepId: null,
|
stepId: null,
|
||||||
name: "",
|
name: "",
|
||||||
|
label: "",
|
||||||
hasArtifact: false,
|
hasArtifact: false,
|
||||||
canReenter: false,
|
canReenter: false,
|
||||||
|
canDelete: false,
|
||||||
node: null,
|
node: null,
|
||||||
handlers: null,
|
handlers: null,
|
||||||
};
|
};
|
||||||
@@ -224,12 +226,16 @@ function showFlowNodeMenu(node, x, y, handlers) {
|
|||||||
if (!stepId) return;
|
if (!stepId) return;
|
||||||
flowMenuState.stepId = stepId;
|
flowMenuState.stepId = stepId;
|
||||||
flowMenuState.name = node.getAttribute("data-flow-done-name") || "";
|
flowMenuState.name = node.getAttribute("data-flow-done-name") || "";
|
||||||
|
flowMenuState.label =
|
||||||
|
node.getAttribute("data-flow-label") || flowMenuState.name || stepId;
|
||||||
flowMenuState.hasArtifact = node.getAttribute("data-has-artifact") === "1";
|
flowMenuState.hasArtifact = node.getAttribute("data-has-artifact") === "1";
|
||||||
flowMenuState.canReenter = node.getAttribute("data-can-reenter") === "1";
|
flowMenuState.canReenter = node.getAttribute("data-can-reenter") === "1";
|
||||||
|
flowMenuState.canDelete = node.getAttribute("data-can-delete") === "1";
|
||||||
flowMenuState.node = node;
|
flowMenuState.node = node;
|
||||||
flowMenuState.handlers = handlers;
|
flowMenuState.handlers = handlers;
|
||||||
const viewBtn = document.getElementById("flow-menu-view");
|
const viewBtn = document.getElementById("flow-menu-view");
|
||||||
const reenterBtn = document.getElementById("flow-menu-reenter");
|
const reenterBtn = document.getElementById("flow-menu-reenter");
|
||||||
|
const deleteBtn = document.getElementById("flow-menu-delete");
|
||||||
if (viewBtn) {
|
if (viewBtn) {
|
||||||
viewBtn.disabled = !flowMenuState.hasArtifact;
|
viewBtn.disabled = !flowMenuState.hasArtifact;
|
||||||
viewBtn.title = flowMenuState.hasArtifact ? "" : "这一步还没有产物";
|
viewBtn.title = flowMenuState.hasArtifact ? "" : "这一步还没有产物";
|
||||||
@@ -237,6 +243,9 @@ function showFlowNodeMenu(node, x, y, handlers) {
|
|||||||
if (reenterBtn) {
|
if (reenterBtn) {
|
||||||
reenterBtn.toggleAttribute("hidden", !flowMenuState.canReenter);
|
reenterBtn.toggleAttribute("hidden", !flowMenuState.canReenter);
|
||||||
}
|
}
|
||||||
|
if (deleteBtn) {
|
||||||
|
deleteBtn.toggleAttribute("hidden", !flowMenuState.canDelete);
|
||||||
|
}
|
||||||
placeMenu(menu, x, y);
|
placeMenu(menu, x, y);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -248,11 +257,13 @@ function wireFlowNodeMenu() {
|
|||||||
const action = e.target.closest("[data-flow-menu-action]")?.getAttribute("data-flow-menu-action");
|
const action = e.target.closest("[data-flow-menu-action]")?.getAttribute("data-flow-menu-action");
|
||||||
const stepId = flowMenuState.stepId;
|
const stepId = flowMenuState.stepId;
|
||||||
const name = flowMenuState.name;
|
const name = flowMenuState.name;
|
||||||
|
const label = flowMenuState.label || name;
|
||||||
const handlers = flowMenuState.handlers;
|
const handlers = flowMenuState.handlers;
|
||||||
hideFlowNodeMenu();
|
hideFlowNodeMenu();
|
||||||
if (!stepId || !action) return;
|
if (!stepId || !action) return;
|
||||||
if (action === "view") handlers?.onViewStepArtifact?.(stepId, name);
|
if (action === "view") handlers?.onViewStepArtifact?.(stepId, name);
|
||||||
if (action === "reenter") handlers?.onReenterCreationStep?.(stepId);
|
if (action === "reenter") handlers?.onReenterCreationStep?.(stepId);
|
||||||
|
if (action === "delete") handlers?.onDeleteCreationStep?.(stepId, label);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2406,6 +2417,17 @@ function renderOneGenerationRuleCard(rule, index) {
|
|||||||
</header>`;
|
</header>`;
|
||||||
|
|
||||||
const blocks = [];
|
const blocks = [];
|
||||||
|
if (rule.设计判断 && typeof rule.设计判断 === "object") {
|
||||||
|
const d = rule.设计判断;
|
||||||
|
const unlist = (v) =>
|
||||||
|
Array.isArray(v) && v.length ? v.map(String).join("、") : "";
|
||||||
|
blocks.push(
|
||||||
|
`<div class="skill-sub"><div class="skill-sub-title">设计判断</div>${skillChipRow([
|
||||||
|
unlist(d.不可穷举项) && `不可穷举 · ${unlist(d.不可穷举项)}`,
|
||||||
|
unlist(d.封闭集合) && `封闭 · ${unlist(d.封闭集合)}`,
|
||||||
|
])}${d.正交字段依据 ? `<p class="skill-pool-desc">${esc(String(d.正交字段依据))}</p>` : ""}</div>`,
|
||||||
|
);
|
||||||
|
}
|
||||||
if (rule.上下文策略 && typeof rule.上下文策略 === "object") {
|
if (rule.上下文策略 && typeof rule.上下文策略 === "object") {
|
||||||
const flags = Object.entries(rule.上下文策略)
|
const flags = Object.entries(rule.上下文策略)
|
||||||
.map(([k, v]) => `<span class="ws-slot-chip ${v ? "ws-slot-on" : "ws-slot-off"}">${esc(k)}</span>`)
|
.map(([k, v]) => `<span class="ws-slot-chip ${v ? "ws-slot-on" : "ws-slot-off"}">${esc(k)}</span>`)
|
||||||
@@ -2424,9 +2446,13 @@ function renderOneGenerationRuleCard(rule, index) {
|
|||||||
}
|
}
|
||||||
if (rule.生成与描写 && typeof rule.生成与描写 === "object") {
|
if (rule.生成与描写 && typeof rule.生成与描写 === "object") {
|
||||||
const g = rule.生成与描写;
|
const g = rule.生成与描写;
|
||||||
const methodKeys = ["依据", "方法", "硬约束", "字段间约束", "变化维度", "禁止项", "去重规则", "校验"];
|
const methodKeys = ["依据", "工作流", "方法", "硬约束", "字段间约束", "变化维度", "禁止项", "去重规则", "反模式", "校验"];
|
||||||
|
const extra = [
|
||||||
|
renderGenerationRecipeHtml(g.配方),
|
||||||
|
renderGenerationExpressHtml(g.表现),
|
||||||
|
].filter(Boolean);
|
||||||
blocks.push(
|
blocks.push(
|
||||||
`<div class="skill-sub"><div class="skill-sub-title">生成与描写</div>${methodKeys
|
`<div class="skill-sub"><div class="skill-sub-title">生成与描写</div>${extra.join("")}${methodKeys
|
||||||
.filter((k) => Array.isArray(g[k]) && g[k].length)
|
.filter((k) => Array.isArray(g[k]) && g[k].length)
|
||||||
.map(
|
.map(
|
||||||
(k) =>
|
(k) =>
|
||||||
@@ -2435,6 +2461,9 @@ function renderOneGenerationRuleCard(rule, index) {
|
|||||||
.join("")}</div>`,
|
.join("")}</div>`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (Array.isArray(rule.分级表现) && rule.分级表现.length) {
|
||||||
|
blocks.push(renderGenerationGradeHtml(rule.分级表现));
|
||||||
|
}
|
||||||
if (rule.产物格式 && typeof rule.产物格式 === "object") {
|
if (rule.产物格式 && typeof rule.产物格式 === "object") {
|
||||||
blocks.push(renderProductSchemaCard(rule.产物格式));
|
blocks.push(renderProductSchemaCard(rule.产物格式));
|
||||||
}
|
}
|
||||||
@@ -2442,19 +2471,7 @@ function renderOneGenerationRuleCard(rule, index) {
|
|||||||
if (pools.length) {
|
if (pools.length) {
|
||||||
blocks.push(
|
blocks.push(
|
||||||
`<div class="skill-sub"><div class="skill-sub-title">池(${pools.length})</div><div class="skill-pool-grid">${pools
|
`<div class="skill-sub"><div class="skill-sub-title">池(${pools.length})</div><div class="skill-pool-grid">${pools
|
||||||
.map((p) => {
|
.map((p) => renderGenerationPoolCard(p))
|
||||||
if (!p || typeof p !== "object") return "";
|
|
||||||
const n = Array.isArray(p.条目) ? p.条目.length : 0;
|
|
||||||
return `<article class="skill-pool-card">
|
|
||||||
<div class="skill-pool-name">${esc(String(p.名称 || p.pool_id || "池"))}</div>
|
|
||||||
${skillChipRow([
|
|
||||||
p.绑定字段 && `绑 · ${p.绑定字段}`,
|
|
||||||
p.用途 && String(p.用途),
|
|
||||||
n ? `${n} 条` : "",
|
|
||||||
])}
|
|
||||||
${p.说明 ? `<p class="skill-pool-desc">${esc(clampPreviewText(String(p.说明), 120))}</p>` : ""}
|
|
||||||
</article>`;
|
|
||||||
})
|
|
||||||
.join("")}</div></div>`,
|
.join("")}</div></div>`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -2462,6 +2479,137 @@ function renderOneGenerationRuleCard(rule, index) {
|
|||||||
return `<article class="skill-rule-card">${head}${blocks.join("")}</article>`;
|
return `<article class="skill-rule-card">${head}${blocks.join("")}</article>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderGenerationRecipeHtml(rows) {
|
||||||
|
if (!Array.isArray(rows) || !rows.length) return "";
|
||||||
|
const cards = rows
|
||||||
|
.map((row) => {
|
||||||
|
if (!row || typeof row !== "object") return "";
|
||||||
|
const comps = Array.isArray(row.组件) ? row.组件.map(String).join(" × ") : "";
|
||||||
|
return `<article class="skill-pool-card is-direction">
|
||||||
|
<div class="skill-pool-name">配方 · ${esc(String(row.目标字段 || "字段"))}</div>
|
||||||
|
${skillChipRow([comps && `组件 · ${comps}`])}
|
||||||
|
${row.组装 ? `<p class="skill-pool-desc">${esc(String(row.组装))}</p>` : ""}
|
||||||
|
</article>`;
|
||||||
|
})
|
||||||
|
.filter(Boolean);
|
||||||
|
return cards.length
|
||||||
|
? `<div class="skill-method"><span class="skill-method-label">配方</span><div class="skill-pool-grid">${cards.join("")}</div></div>`
|
||||||
|
: "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderGenerationExpressHtml(rows) {
|
||||||
|
if (!Array.isArray(rows) || !rows.length) return "";
|
||||||
|
const cards = rows
|
||||||
|
.map((row) => {
|
||||||
|
if (!row || typeof row !== "object") return "";
|
||||||
|
const ex = Array.isArray(row.例子) ? row.例子.filter(Boolean) : [];
|
||||||
|
return `<article class="skill-pool-card is-direction">
|
||||||
|
<div class="skill-pool-name">${esc(String(row.针对 || "表现"))}</div>
|
||||||
|
${row.概括 ? `<p class="skill-pool-desc">${esc(String(row.概括))}</p>` : ""}
|
||||||
|
${
|
||||||
|
ex.length
|
||||||
|
? `<ul class="artifact-list">${ex.map((x) => `<li>${esc(String(x))}</li>`).join("")}</ul>`
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
</article>`;
|
||||||
|
})
|
||||||
|
.filter(Boolean);
|
||||||
|
return cards.length
|
||||||
|
? `<div class="skill-method"><span class="skill-method-label">表现</span><div class="skill-pool-grid">${cards.join("")}</div></div>`
|
||||||
|
: "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderGenerationGradeHtml(rows) {
|
||||||
|
const blocks = rows
|
||||||
|
.map((row) => {
|
||||||
|
if (!row || typeof row !== "object") return "";
|
||||||
|
const grades = Array.isArray(row.档位) ? row.档位 : [];
|
||||||
|
const tr = grades
|
||||||
|
.map((g) => {
|
||||||
|
if (!g || typeof g !== "object") return "";
|
||||||
|
return `<tr>
|
||||||
|
<td><code>${esc(String(g.值 || ""))}</code></td>
|
||||||
|
<td>${esc(String(g.概括 || ""))}</td>
|
||||||
|
<td>${esc(String(g.表象 || ""))}</td>
|
||||||
|
</tr>`;
|
||||||
|
})
|
||||||
|
.join("");
|
||||||
|
return `<div class="skill-sub">
|
||||||
|
<div class="skill-sub-title">分级表现 · ${esc(String(row.字段 || ""))}</div>
|
||||||
|
${row.说明 ? `<p class="skill-pool-desc">${esc(String(row.说明))}</p>` : ""}
|
||||||
|
${
|
||||||
|
tr
|
||||||
|
? `<div class="skill-table-wrap"><table class="skill-field-table"><thead><tr><th>档</th><th>概括</th><th>表象</th></tr></thead><tbody>${tr}</tbody></table></div>`
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
</div>`;
|
||||||
|
})
|
||||||
|
.filter(Boolean);
|
||||||
|
return blocks.join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
function generationPoolKind(pool) {
|
||||||
|
const raw = String(pool?.池型 || pool?.kind || pool?.类型 || "").trim();
|
||||||
|
if (/方向|类型|type/i.test(raw)) return "方向";
|
||||||
|
if (/元素|枚举|enum/i.test(raw)) return "元素";
|
||||||
|
if (pool?.方向 && typeof pool.方向 === "object") return "方向";
|
||||||
|
if (Array.isArray(pool?.条目) && pool.条目.length) return "元素";
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function generationPoolDirections(pool) {
|
||||||
|
const dirs = pool?.方向;
|
||||||
|
if (!dirs) return [];
|
||||||
|
if (Array.isArray(dirs)) {
|
||||||
|
return dirs
|
||||||
|
.map((row) => {
|
||||||
|
if (typeof row === "string") return { name: row, text: "" };
|
||||||
|
if (!row || typeof row !== "object") return null;
|
||||||
|
return {
|
||||||
|
name: String(row.名 || row.name || row.方向 || "").trim(),
|
||||||
|
text: String(row.说明 || row.description || row.内容 || "").trim(),
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter((x) => x && (x.name || x.text));
|
||||||
|
}
|
||||||
|
if (typeof dirs === "object") {
|
||||||
|
return Object.entries(dirs).map(([name, text]) => ({
|
||||||
|
name,
|
||||||
|
text: text == null ? "" : String(text),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderGenerationPoolCard(p) {
|
||||||
|
if (!p || typeof p !== "object") return "";
|
||||||
|
const kind = generationPoolKind(p);
|
||||||
|
const dirs = generationPoolDirections(p);
|
||||||
|
const n = Array.isArray(p.条目) ? p.条目.length : 0;
|
||||||
|
const dirHtml = dirs.length
|
||||||
|
? `<dl class="skill-pool-dirs">${dirs
|
||||||
|
.map(
|
||||||
|
(d) =>
|
||||||
|
`<div class="skill-pool-dir"><dt>${esc(d.name || "方向")}</dt>${
|
||||||
|
d.text ? `<dd>${esc(clampPreviewText(d.text, 160))}</dd>` : ""
|
||||||
|
}</div>`,
|
||||||
|
)
|
||||||
|
.join("")}</dl>`
|
||||||
|
: "";
|
||||||
|
return `<article class="skill-pool-card${kind === "方向" ? " is-direction" : ""}">
|
||||||
|
<div class="skill-pool-name">${esc(String(p.名称 || p.pool_id || "池"))}</div>
|
||||||
|
${skillChipRow([
|
||||||
|
kind && `${kind}池`,
|
||||||
|
p.绑定字段 && `绑 · ${p.绑定字段}`,
|
||||||
|
p.用途 && String(p.用途),
|
||||||
|
kind === "元素" && n ? `${n} 条` : "",
|
||||||
|
kind === "方向" && dirs.length ? `${dirs.length} 向` : "",
|
||||||
|
])}
|
||||||
|
${p.说明 ? `<p class="skill-pool-desc">${esc(clampPreviewText(String(p.说明), 160))}</p>` : ""}
|
||||||
|
${dirHtml}
|
||||||
|
</article>`;
|
||||||
|
}
|
||||||
|
|
||||||
function renderProductSchemaCard(fmt) {
|
function renderProductSchemaCard(fmt) {
|
||||||
const schema = fmt.schema && typeof fmt.schema === "object" ? fmt.schema : null;
|
const schema = fmt.schema && typeof fmt.schema === "object" ? fmt.schema : null;
|
||||||
const fields = schema
|
const fields = schema
|
||||||
@@ -2474,6 +2622,9 @@ function renderProductSchemaCard(fmt) {
|
|||||||
const req = spec.required === true ? "必填" : spec.required === false ? "可选" : "—";
|
const req = spec.required === true ? "必填" : spec.required === false ? "可选" : "—";
|
||||||
const desc = spec.description != null ? clampPreviewText(String(spec.description), 80) : "";
|
const desc = spec.description != null ? clampPreviewText(String(spec.description), 80) : "";
|
||||||
const extra = [];
|
const extra = [];
|
||||||
|
if (spec.粒度) extra.push(`粒度 ${spec.粒度}`);
|
||||||
|
if (spec.创作) extra.push(`创作 ${spec.创作}`);
|
||||||
|
if (spec.地板) extra.push(clampPreviewText(String(spec.地板), 60));
|
||||||
if (Array.isArray(spec.allowed_values) && spec.allowed_values.length) {
|
if (Array.isArray(spec.allowed_values) && spec.allowed_values.length) {
|
||||||
extra.push(
|
extra.push(
|
||||||
`枚举 ${spec.allowed_values
|
`枚举 ${spec.allowed_values
|
||||||
@@ -3576,6 +3727,7 @@ function renderCreationFlowView(flowView, opts = {}) {
|
|||||||
<div class="flow-dag" data-flow-dag>
|
<div class="flow-dag" data-flow-dag>
|
||||||
<svg class="flow-dag-edges" aria-hidden="true"></svg>
|
<svg class="flow-dag-edges" aria-hidden="true"></svg>
|
||||||
<div class="flow-dag-rows">${rows}</div>
|
<div class="flow-dag-rows">${rows}</div>
|
||||||
|
<aside class="flow-dag-hint" hidden role="status"></aside>
|
||||||
</div>
|
</div>
|
||||||
${suggestionHtml}
|
${suggestionHtml}
|
||||||
</section>`;
|
</section>`;
|
||||||
@@ -3670,6 +3822,11 @@ function renderFlowGraphNode(s, interactive, opts = {}) {
|
|||||||
const selectable = Boolean(s.selectable) && interactive;
|
const selectable = Boolean(s.selectable) && interactive;
|
||||||
const doneMenu = !isProto && runState === "done";
|
const doneMenu = !isProto && runState === "done";
|
||||||
const canReenter = doneMenu && interactive;
|
const canReenter = doneMenu && interactive;
|
||||||
|
const canDelete =
|
||||||
|
doneMenu &&
|
||||||
|
interactive &&
|
||||||
|
s.role === "instance" &&
|
||||||
|
Boolean(s.repeatable);
|
||||||
const runLabel = isProto
|
const runLabel = isProto
|
||||||
? selectable
|
? selectable
|
||||||
? "可增殖"
|
? "可增殖"
|
||||||
@@ -3685,6 +3842,8 @@ function renderFlowGraphNode(s, interactive, opts = {}) {
|
|||||||
? isProto
|
? isProto
|
||||||
? "增殖"
|
? "增殖"
|
||||||
: "进入"
|
: "进入"
|
||||||
|
: canReenter
|
||||||
|
? "进入"
|
||||||
: "";
|
: "";
|
||||||
const actionHtml = actionLabel
|
const actionHtml = actionLabel
|
||||||
? `<span class="flow-action">${esc(actionLabel)}</span>`
|
? `<span class="flow-action">${esc(actionLabel)}</span>`
|
||||||
@@ -3692,15 +3851,19 @@ function renderFlowGraphNode(s, interactive, opts = {}) {
|
|||||||
const doneMenuBtn = doneMenu
|
const doneMenuBtn = doneMenu
|
||||||
? `<span class="flow-node-menu-btn" data-flow-menu-trigger aria-hidden="true">⋯</span>`
|
? `<span class="flow-node-menu-btn" data-flow-menu-trigger aria-hidden="true">⋯</span>`
|
||||||
: "";
|
: "";
|
||||||
|
const blockedBy = Array.isArray(s.blockedBy) ? s.blockedBy : [];
|
||||||
|
const blockedAttr = blockedBy.length
|
||||||
|
? ` data-flow-blocked="${escAttr(JSON.stringify(blockedBy))}"`
|
||||||
|
: "";
|
||||||
const blocked =
|
const blocked =
|
||||||
!selectable && !doneMenu && s.blockedBy?.length
|
!selectable && !doneMenu && blockedBy.length
|
||||||
? ` title="还差:${esc(s.blockedBy.join("、"))}"`
|
? ` title="还差:${esc(blockedBy.join("、"))}"`
|
||||||
: isProto && selectable
|
: isProto && selectable
|
||||||
? ` title="确认当前编排,点一下新开一条产物再进入"`
|
? ` title="确认当前编排,点一下新开一条产物再进入"`
|
||||||
: selectable
|
: selectable
|
||||||
? ` title="确认当前编排并进入"`
|
? ` title="确认当前编排并进入"`
|
||||||
: doneMenu
|
: canReenter
|
||||||
? ` title="查看产物或重新进入修改"`
|
? ` title="进入这条产物继续改,继承原文;新开一条请点原型上的「增殖」"`
|
||||||
: "";
|
: "";
|
||||||
const currentAttr = runState === "current" ? ' aria-current="step"' : "";
|
const currentAttr = runState === "current" ? ' aria-current="step"' : "";
|
||||||
const clickable = selectable || doneMenu;
|
const clickable = selectable || doneMenu;
|
||||||
@@ -3708,9 +3871,9 @@ function renderFlowGraphNode(s, interactive, opts = {}) {
|
|||||||
const typeAttr = clickable ? ' type="button"' : "";
|
const typeAttr = clickable ? ' type="button"' : "";
|
||||||
const pickAttr = selectable ? ` data-flow-pick="${esc(s.id)}"` : "";
|
const pickAttr = selectable ? ` data-flow-pick="${esc(s.id)}"` : "";
|
||||||
const doneAttr = doneMenu
|
const doneAttr = doneMenu
|
||||||
? ` data-flow-done="${esc(s.id)}" data-flow-done-name="${escAttr(s.name)}" data-has-artifact="${s.hasArtifact ? "1" : "0"}" data-can-reenter="${canReenter ? "1" : "0"}" aria-haspopup="menu" aria-label="已完成:${escAttr(s.title || s.name)}"`
|
? ` data-flow-done="${esc(s.id)}" data-flow-done-name="${escAttr(s.name)}" data-flow-label="${escAttr([s.name, s.title].filter(Boolean).join(" · ") || s.id)}" data-has-artifact="${s.hasArtifact ? "1" : "0"}" data-can-reenter="${canReenter ? "1" : "0"}" data-can-delete="${canDelete ? "1" : "0"}" aria-haspopup="menu" aria-label="${canReenter ? "进入继续修改" : "已完成"}:${escAttr(s.title || s.name)}"`
|
||||||
: "";
|
: "";
|
||||||
const clickableClass = selectable
|
const clickableClass = selectable || canReenter
|
||||||
? " is-selectable"
|
? " is-selectable"
|
||||||
: doneMenu
|
: doneMenu
|
||||||
? " is-done-menu"
|
? " is-done-menu"
|
||||||
@@ -3724,9 +3887,9 @@ function renderFlowGraphNode(s, interactive, opts = {}) {
|
|||||||
const originAttr =
|
const originAttr =
|
||||||
origin?.fromId ? ` data-flow-origin="${escAttr(origin.fromId)}"` : "";
|
origin?.fromId ? ` data-flow-origin="${escAttr(origin.fromId)}"` : "";
|
||||||
const metaHtml = compact
|
const metaHtml = compact
|
||||||
? `<span class="flow-spawn-meta">${actionHtml || doneMenuBtn || `<span class="flow-run">${esc(runLabel)}</span>`}</span>`
|
? `<span class="flow-spawn-meta">${actionHtml}${doneMenuBtn || (!actionHtml ? `<span class="flow-run">${esc(runLabel)}</span>` : "")}</span>`
|
||||||
: `${actionHtml}${doneMenuBtn}`;
|
: `${actionHtml}${doneMenuBtn}`;
|
||||||
return `<${tag} class="flow-node${protoClass}${spawnClass}${clickableClass}${lockedClass}" data-flow-id="${escAttr(s.id)}" data-flow-name="${escAttr(s.name)}" data-flow-deps="${depsJson}" data-run="${escAttr(runState)}"${fromAttr}${originAttr}${currentAttr}${typeAttr}${pickAttr}${doneAttr}${blocked}>
|
return `<${tag} class="flow-node${protoClass}${spawnClass}${clickableClass}${lockedClass}" data-flow-id="${escAttr(s.id)}" data-flow-name="${escAttr(s.name)}" data-flow-deps="${depsJson}" data-run="${escAttr(runState)}"${fromAttr}${originAttr}${blockedAttr}${currentAttr}${typeAttr}${pickAttr}${doneAttr}${blocked}>
|
||||||
<span class="flow-node-main">
|
<span class="flow-node-main">
|
||||||
${compact ? "" : `<span class="flow-run">${esc(runLabel)}</span>`}
|
${compact ? "" : `<span class="flow-run">${esc(runLabel)}</span>`}
|
||||||
<span class="flow-name">${nameLabel}${occ}</span>
|
<span class="flow-name">${nameLabel}${occ}</span>
|
||||||
@@ -3741,6 +3904,47 @@ function renderFlowGraphNode(s, interactive, opts = {}) {
|
|||||||
</${tag}>`;
|
</${tag}>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseFlowJsonList(raw) {
|
||||||
|
try {
|
||||||
|
const list = JSON.parse(raw || "[]");
|
||||||
|
return Array.isArray(list) ? list.map(String).filter(Boolean) : [];
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function flowNodeHintName(el) {
|
||||||
|
if (!el) return "";
|
||||||
|
return (
|
||||||
|
el.getAttribute("data-flow-label") ||
|
||||||
|
el.querySelector(".flow-title")?.textContent?.trim() ||
|
||||||
|
el.getAttribute("data-flow-name") ||
|
||||||
|
el.getAttribute("data-flow-id") ||
|
||||||
|
""
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function flowNodeRunLabel(el) {
|
||||||
|
const run = el?.getAttribute("data-run");
|
||||||
|
if (run === "done") return "已完成";
|
||||||
|
if (run === "current") return "进行中";
|
||||||
|
if (el?.classList.contains("is-selectable")) return "可进入";
|
||||||
|
if (el?.classList.contains("is-locked")) return "待前置";
|
||||||
|
return "未执行";
|
||||||
|
}
|
||||||
|
|
||||||
|
function flowDagNodeIndex(dag) {
|
||||||
|
const byId = new Map();
|
||||||
|
const byName = new Map();
|
||||||
|
for (const el of dag.querySelectorAll("[data-flow-id]")) {
|
||||||
|
const id = el.getAttribute("data-flow-id");
|
||||||
|
const name = el.getAttribute("data-flow-name");
|
||||||
|
if (id) byId.set(id, el);
|
||||||
|
if (name && !byName.has(name)) byName.set(name, el);
|
||||||
|
}
|
||||||
|
return (ref) => byId.get(String(ref)) || byName.get(String(ref)) || null;
|
||||||
|
}
|
||||||
|
|
||||||
function paintFlowDagEdges(dag) {
|
function paintFlowDagEdges(dag) {
|
||||||
if (!dag) return;
|
if (!dag) return;
|
||||||
const svg = dag.querySelector(".flow-dag-edges");
|
const svg = dag.querySelector(".flow-dag-edges");
|
||||||
@@ -3750,15 +3954,6 @@ function paintFlowDagEdges(dag) {
|
|||||||
svg.replaceChildren();
|
svg.replaceChildren();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const byId = new Map();
|
|
||||||
const byName = new Map();
|
|
||||||
for (const el of nodes) {
|
|
||||||
const id = el.getAttribute("data-flow-id");
|
|
||||||
const name = el.getAttribute("data-flow-name");
|
|
||||||
if (id) byId.set(id, el);
|
|
||||||
if (name && !byName.has(name)) byName.set(name, el);
|
|
||||||
}
|
|
||||||
const findNode = (ref) => byId.get(ref) || byName.get(ref) || null;
|
|
||||||
const dagRect = dag.getBoundingClientRect();
|
const dagRect = dag.getBoundingClientRect();
|
||||||
const w = Math.max(dag.scrollWidth, dag.clientWidth, 1);
|
const w = Math.max(dag.scrollWidth, dag.clientWidth, 1);
|
||||||
const h = Math.max(dag.scrollHeight, dag.clientHeight, 1);
|
const h = Math.max(dag.scrollHeight, dag.clientHeight, 1);
|
||||||
@@ -3766,20 +3961,17 @@ function paintFlowDagEdges(dag) {
|
|||||||
svg.setAttribute("height", String(h));
|
svg.setAttribute("height", String(h));
|
||||||
svg.setAttribute("viewBox", `0 0 ${w} ${h}`);
|
svg.setAttribute("viewBox", `0 0 ${w} ${h}`);
|
||||||
|
|
||||||
|
const findNode = flowDagNodeIndex(dag);
|
||||||
const paths = [];
|
const paths = [];
|
||||||
for (const el of nodes) {
|
for (const el of nodes) {
|
||||||
let deps = [];
|
const deps = parseFlowJsonList(el.getAttribute("data-flow-deps"));
|
||||||
try {
|
if (!deps.length) continue;
|
||||||
deps = JSON.parse(el.getAttribute("data-flow-deps") || "[]");
|
const toId = el.getAttribute("data-flow-id") || "";
|
||||||
} catch {
|
|
||||||
deps = [];
|
|
||||||
}
|
|
||||||
if (!Array.isArray(deps) || !deps.length) continue;
|
|
||||||
const toRect = el.getBoundingClientRect();
|
const toRect = el.getBoundingClientRect();
|
||||||
const x2 = toRect.left - dagRect.left + toRect.width / 2 + dag.scrollLeft;
|
const x2 = toRect.left - dagRect.left + toRect.width / 2 + dag.scrollLeft;
|
||||||
const y2 = toRect.top - dagRect.top + dag.scrollTop;
|
const y2 = toRect.top - dagRect.top + dag.scrollTop;
|
||||||
for (const ref of deps) {
|
for (const ref of deps) {
|
||||||
const from = findNode(String(ref));
|
const from = findNode(ref);
|
||||||
if (!from || from === el) continue;
|
if (!from || from === el) continue;
|
||||||
const fromId = from.getAttribute("data-flow-id") || "";
|
const fromId = from.getAttribute("data-flow-id") || "";
|
||||||
if (el.getAttribute("data-flow-from") === fromId) continue;
|
if (el.getAttribute("data-flow-from") === fromId) continue;
|
||||||
@@ -3787,19 +3979,15 @@ function paintFlowDagEdges(dag) {
|
|||||||
const x1 =
|
const x1 =
|
||||||
fromRect.left - dagRect.left + fromRect.width / 2 + dag.scrollLeft;
|
fromRect.left - dagRect.left + fromRect.width / 2 + dag.scrollLeft;
|
||||||
const y1 = fromRect.bottom - dagRect.top + dag.scrollTop;
|
const y1 = fromRect.bottom - dagRect.top + dag.scrollTop;
|
||||||
const dy = Math.max(24, (y2 - y1) * 0.45);
|
const dy = Math.max(28, (y2 - y1) * 0.42);
|
||||||
const d = `M ${x1.toFixed(1)} ${y1.toFixed(1)} C ${x1.toFixed(1)} ${(y1 + dy).toFixed(1)}, ${x2.toFixed(1)} ${(y2 - dy).toFixed(1)}, ${x2.toFixed(1)} ${y2.toFixed(1)}`;
|
const d = `M ${x1.toFixed(1)} ${y1.toFixed(1)} C ${x1.toFixed(1)} ${(y1 + dy).toFixed(1)}, ${x2.toFixed(1)} ${(y2 - dy).toFixed(1)}, ${x2.toFixed(1)} ${y2.toFixed(1)}`;
|
||||||
const belong = el.getAttribute("data-flow-origin") === fromId;
|
const belong = el.getAttribute("data-flow-origin") === fromId;
|
||||||
const active =
|
const cls = `flow-dag-edge${belong ? " flow-dag-edge--belong" : ""}`;
|
||||||
from.classList.contains("is-selectable") ||
|
|
||||||
el.classList.contains("is-selectable") ||
|
|
||||||
from.getAttribute("data-run") === "done";
|
|
||||||
const cls = `flow-dag-edge${active ? " is-lit" : ""}${belong ? " flow-dag-edge--belong" : ""}`;
|
|
||||||
const marker = belong
|
const marker = belong
|
||||||
? "url(#flow-dag-arrow-belong)"
|
? "url(#flow-dag-arrow-belong)"
|
||||||
: "url(#flow-dag-arrow)";
|
: "url(#flow-dag-arrow)";
|
||||||
paths.push(
|
paths.push(
|
||||||
`<path class="${cls}" d="${d}" fill="none" marker-end="${marker}" />`,
|
`<path class="${cls}" d="${d}" fill="none" marker-end="${marker}" data-from="${escAttr(fromId)}" data-to="${escAttr(toId)}" />`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3807,10 +3995,210 @@ function paintFlowDagEdges(dag) {
|
|||||||
<marker id="flow-dag-arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
<marker id="flow-dag-arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||||
<path d="M 0 1 L 8 5 L 0 9 z" class="flow-dag-arrowhead" />
|
<path d="M 0 1 L 8 5 L 0 9 z" class="flow-dag-arrowhead" />
|
||||||
</marker>
|
</marker>
|
||||||
|
<marker id="flow-dag-arrow-in" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="8" markerHeight="8" orient="auto-start-reverse">
|
||||||
|
<path d="M 0 1 L 8 5 L 0 9 z" class="flow-dag-arrowhead flow-dag-arrowhead--in" />
|
||||||
|
</marker>
|
||||||
|
<marker id="flow-dag-arrow-out" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="8" markerHeight="8" orient="auto-start-reverse">
|
||||||
|
<path d="M 0 1 L 8 5 L 0 9 z" class="flow-dag-arrowhead flow-dag-arrowhead--out" />
|
||||||
|
</marker>
|
||||||
<marker id="flow-dag-arrow-belong" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
<marker id="flow-dag-arrow-belong" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
||||||
<path d="M 0 1 L 8 5 L 0 9 z" class="flow-dag-arrowhead flow-dag-arrowhead--belong" />
|
<path d="M 0 1 L 8 5 L 0 9 z" class="flow-dag-arrowhead flow-dag-arrowhead--belong" />
|
||||||
</marker>
|
</marker>
|
||||||
</defs>${paths.join("")}`;
|
</defs>${paths.join("")}`;
|
||||||
|
|
||||||
|
const traceId = dag.getAttribute("data-flow-trace");
|
||||||
|
if (traceId) {
|
||||||
|
const node = nodes.find((el) => el.getAttribute("data-flow-id") === traceId);
|
||||||
|
if (node) applyFlowDagTrace(dag, node);
|
||||||
|
else clearFlowDagTrace(dag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function flowHintItemsHtml(items, emptyText) {
|
||||||
|
if (!items.length) {
|
||||||
|
return `<p class="flow-dag-hint-empty">${esc(emptyText)}</p>`;
|
||||||
|
}
|
||||||
|
return `<ul class="flow-dag-hint-list">${items
|
||||||
|
.map(
|
||||||
|
(item) =>
|
||||||
|
`<li><span class="flow-dag-hint-item-name">${esc(item.name)}</span><span class="flow-dag-hint-item-run">${esc(item.run)}</span></li>`,
|
||||||
|
)
|
||||||
|
.join("")}</ul>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function placeFlowDagHint(dag, node) {
|
||||||
|
const hint = dag.querySelector(".flow-dag-hint");
|
||||||
|
if (!hint || hint.hidden) return;
|
||||||
|
const dagRect = dag.getBoundingClientRect();
|
||||||
|
const nodeRect = node.getBoundingClientRect();
|
||||||
|
const hintW = hint.offsetWidth;
|
||||||
|
const hintH = hint.offsetHeight;
|
||||||
|
const pad = 8;
|
||||||
|
let left = nodeRect.right - dagRect.left + dag.scrollLeft + 12;
|
||||||
|
let top = nodeRect.top - dagRect.top + dag.scrollTop;
|
||||||
|
const maxLeft = Math.max(pad, dag.scrollWidth - hintW - pad);
|
||||||
|
if (left > maxLeft) {
|
||||||
|
left = nodeRect.left - dagRect.left + dag.scrollLeft - hintW - 12;
|
||||||
|
}
|
||||||
|
left = Math.min(maxLeft, Math.max(pad, left));
|
||||||
|
const maxTop = Math.max(pad, dag.scrollHeight - hintH - pad);
|
||||||
|
if (top > maxTop) top = maxTop;
|
||||||
|
if (top < pad) top = pad;
|
||||||
|
hint.style.left = `${left}px`;
|
||||||
|
hint.style.top = `${top}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyFlowDagTrace(dag, node) {
|
||||||
|
const findNode = flowDagNodeIndex(dag);
|
||||||
|
const nodeId = node.getAttribute("data-flow-id") || "";
|
||||||
|
const nodeName = node.getAttribute("data-flow-name") || "";
|
||||||
|
const refs = new Set([nodeId, nodeName].filter(Boolean));
|
||||||
|
const depRefs = parseFlowJsonList(node.getAttribute("data-flow-deps"));
|
||||||
|
const blocked = new Set(parseFlowJsonList(node.getAttribute("data-flow-blocked")));
|
||||||
|
const relatedIds = new Set([nodeId]);
|
||||||
|
|
||||||
|
dag.querySelectorAll(".flow-dag-edge").forEach((path) => {
|
||||||
|
const fromId = path.getAttribute("data-from") || "";
|
||||||
|
const toId = path.getAttribute("data-to") || "";
|
||||||
|
const isIn = toId === nodeId;
|
||||||
|
const isOut = fromId === nodeId;
|
||||||
|
path.classList.toggle("is-trace-in", isIn);
|
||||||
|
path.classList.toggle("is-trace-out", isOut && !isIn);
|
||||||
|
if (isIn) {
|
||||||
|
path.setAttribute("marker-end", "url(#flow-dag-arrow-in)");
|
||||||
|
relatedIds.add(fromId);
|
||||||
|
} else if (isOut) {
|
||||||
|
path.setAttribute("marker-end", "url(#flow-dag-arrow-out)");
|
||||||
|
relatedIds.add(toId);
|
||||||
|
} else {
|
||||||
|
path.setAttribute(
|
||||||
|
"marker-end",
|
||||||
|
path.classList.contains("flow-dag-edge--belong")
|
||||||
|
? "url(#flow-dag-arrow-belong)"
|
||||||
|
: "url(#flow-dag-arrow)",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
dag.querySelectorAll("[data-flow-id]").forEach((el) => {
|
||||||
|
const id = el.getAttribute("data-flow-id") || "";
|
||||||
|
el.classList.toggle("is-trace-root", el === node);
|
||||||
|
el.classList.toggle("is-trace-related", el !== node && relatedIds.has(id));
|
||||||
|
});
|
||||||
|
|
||||||
|
const deps = depRefs
|
||||||
|
.map((ref) => {
|
||||||
|
const el = findNode(ref);
|
||||||
|
if (el && el.getAttribute("data-flow-from") === nodeId) return null;
|
||||||
|
return {
|
||||||
|
name: el ? flowNodeHintName(el) : ref,
|
||||||
|
run: el
|
||||||
|
? flowNodeRunLabel(el)
|
||||||
|
: blocked.has(ref)
|
||||||
|
? "未齐"
|
||||||
|
: "",
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
const downstream = [...dag.querySelectorAll("[data-flow-id]")]
|
||||||
|
.filter((el) => {
|
||||||
|
if (el === node) return false;
|
||||||
|
if (el.getAttribute("data-flow-from") === nodeId) return false;
|
||||||
|
return parseFlowJsonList(el.getAttribute("data-flow-deps")).some((ref) =>
|
||||||
|
refs.has(ref),
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.map((el) => ({
|
||||||
|
name: flowNodeHintName(el),
|
||||||
|
run: flowNodeRunLabel(el),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const hint = dag.querySelector(".flow-dag-hint");
|
||||||
|
if (hint) {
|
||||||
|
hint.innerHTML = `<p class="flow-dag-hint-kicker">依赖关系</p>
|
||||||
|
<p class="flow-dag-hint-name">${esc(flowNodeHintName(node))}</p>
|
||||||
|
<div class="flow-dag-hint-sec">
|
||||||
|
<div class="flow-dag-hint-label">依赖</div>
|
||||||
|
${flowHintItemsHtml(deps, "无前置")}
|
||||||
|
</div>
|
||||||
|
<div class="flow-dag-hint-sec">
|
||||||
|
<div class="flow-dag-hint-label">下游</div>
|
||||||
|
${flowHintItemsHtml(downstream, "无下游")}
|
||||||
|
</div>`;
|
||||||
|
hint.hidden = false;
|
||||||
|
placeFlowDagHint(dag, node);
|
||||||
|
}
|
||||||
|
dag.classList.add("is-tracing");
|
||||||
|
dag.setAttribute("data-flow-trace", nodeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearFlowDagTrace(dag) {
|
||||||
|
if (!dag) return;
|
||||||
|
dag.classList.remove("is-tracing");
|
||||||
|
dag.removeAttribute("data-flow-trace");
|
||||||
|
dag.querySelectorAll(".flow-dag-edge").forEach((path) => {
|
||||||
|
path.classList.remove("is-trace-in", "is-trace-out");
|
||||||
|
path.setAttribute(
|
||||||
|
"marker-end",
|
||||||
|
path.classList.contains("flow-dag-edge--belong")
|
||||||
|
? "url(#flow-dag-arrow-belong)"
|
||||||
|
: "url(#flow-dag-arrow)",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
dag.querySelectorAll("[data-flow-id]").forEach((el) => {
|
||||||
|
el.classList.remove("is-trace-root", "is-trace-related");
|
||||||
|
});
|
||||||
|
const hint = dag.querySelector(".flow-dag-hint");
|
||||||
|
if (hint) {
|
||||||
|
hint.hidden = true;
|
||||||
|
hint.replaceChildren();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setFlowDagTrace(dag, node) {
|
||||||
|
if (!dag || !node) return;
|
||||||
|
if (dag.getAttribute("data-flow-trace") === node.getAttribute("data-flow-id")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
applyFlowDagTrace(dag, node);
|
||||||
|
}
|
||||||
|
|
||||||
|
function wireFlowDagTrace(dag) {
|
||||||
|
dag.addEventListener("pointerover", (e) => {
|
||||||
|
const node = e.target.closest("[data-flow-id]");
|
||||||
|
if (node && dag.contains(node)) setFlowDagTrace(dag, node);
|
||||||
|
});
|
||||||
|
dag.addEventListener("pointerleave", () => {
|
||||||
|
const focused = dag.querySelector("[data-flow-id]:focus");
|
||||||
|
if (focused) {
|
||||||
|
applyFlowDagTrace(dag, focused);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
clearFlowDagTrace(dag);
|
||||||
|
});
|
||||||
|
dag.addEventListener("focusin", (e) => {
|
||||||
|
const node = e.target.closest("[data-flow-id]");
|
||||||
|
if (node && dag.contains(node)) setFlowDagTrace(dag, node);
|
||||||
|
});
|
||||||
|
dag.addEventListener("focusout", (e) => {
|
||||||
|
const next = e.relatedTarget;
|
||||||
|
if (next && dag.contains(next) && next.closest("[data-flow-id]")) return;
|
||||||
|
if (dag.matches(":hover")) return;
|
||||||
|
clearFlowDagTrace(dag);
|
||||||
|
});
|
||||||
|
dag.addEventListener(
|
||||||
|
"scroll",
|
||||||
|
() => {
|
||||||
|
const id = dag.getAttribute("data-flow-trace");
|
||||||
|
if (!id) return;
|
||||||
|
const node = [...dag.querySelectorAll("[data-flow-id]")].find(
|
||||||
|
(el) => el.getAttribute("data-flow-id") === id,
|
||||||
|
);
|
||||||
|
if (node) placeFlowDagHint(dag, node);
|
||||||
|
},
|
||||||
|
{ passive: true },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function wireCreationFlowGraph(root, handlers) {
|
function wireCreationFlowGraph(root, handlers) {
|
||||||
@@ -3825,18 +4213,42 @@ function wireCreationFlowGraph(root, handlers) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
root.querySelectorAll("[data-flow-done]").forEach((el) => {
|
root.querySelectorAll("[data-flow-done]").forEach((el) => {
|
||||||
|
const stepId = el.getAttribute("data-flow-done");
|
||||||
|
const canReenter = el.getAttribute("data-can-reenter") === "1";
|
||||||
const openMenu = (e) => {
|
const openMenu = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
showFlowNodeMenu(el, e.clientX, e.clientY, handlers);
|
showFlowNodeMenu(el, e.clientX, e.clientY, handlers);
|
||||||
};
|
};
|
||||||
el.addEventListener("click", openMenu);
|
el.addEventListener("click", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
if (e.target.closest("[data-flow-menu-trigger]")) {
|
||||||
|
openMenu(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (canReenter && stepId) {
|
||||||
|
handlers.onReenterCreationStep?.(stepId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
openMenu(e);
|
||||||
|
});
|
||||||
el.addEventListener("contextmenu", openMenu);
|
el.addEventListener("contextmenu", openMenu);
|
||||||
el.addEventListener("keydown", (e) => {
|
el.addEventListener("keydown", (e) => {
|
||||||
if (e.key !== "Enter" && e.key !== " " && e.key !== "ContextMenu") return;
|
if (e.key === "ContextMenu") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const rect = el.getBoundingClientRect();
|
const rect = el.getBoundingClientRect();
|
||||||
showFlowNodeMenu(el, rect.left, rect.bottom + 4, handlers);
|
showFlowNodeMenu(el, rect.left, rect.bottom + 4, handlers);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (e.key !== "Enter" && e.key !== " ") return;
|
||||||
|
e.preventDefault();
|
||||||
|
if (canReenter && stepId) {
|
||||||
|
handlers.onReenterCreationStep?.(stepId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const rect = el.getBoundingClientRect();
|
||||||
|
showFlowNodeMenu(el, rect.left, rect.bottom + 4, handlers);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
root.querySelectorAll("[data-flow-spawn]").forEach((el) => {
|
root.querySelectorAll("[data-flow-spawn]").forEach((el) => {
|
||||||
@@ -3863,6 +4275,7 @@ function wireCreationFlowGraph(root, handlers) {
|
|||||||
dag.querySelectorAll(".flow-family").forEach((n) => ro.observe(n));
|
dag.querySelectorAll(".flow-family").forEach((n) => ro.observe(n));
|
||||||
}
|
}
|
||||||
window.addEventListener("resize", paint, { passive: true });
|
window.addEventListener("resize", paint, { passive: true });
|
||||||
|
wireFlowDagTrace(dag);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3995,6 +4408,7 @@ function renderReviewWorkspace(review, opts = {}) {
|
|||||||
return `
|
return `
|
||||||
<section class="workspace-review" data-review="1" data-review-kind="${esc(copy.kind)}" data-message-id="${esc(contextId)}" data-can-edit="0" data-has-context="${hasContext}">
|
<section class="workspace-review" data-review="1" data-review-kind="${esc(copy.kind)}" data-message-id="${esc(contextId)}" data-can-edit="0" data-has-context="${hasContext}">
|
||||||
<header class="workspace-review-head workspace-review-head--slim">
|
<header class="workspace-review-head workspace-review-head--slim">
|
||||||
|
${opts.leaveStepHtml || ""}
|
||||||
<div class="workspace-review-titles">
|
<div class="workspace-review-titles">
|
||||||
<span class="workspace-review-kicker">${esc(copy.kicker)}</span>
|
<span class="workspace-review-kicker">${esc(copy.kicker)}</span>
|
||||||
<h2 class="workspace-review-title">${esc(label)}</h2>
|
<h2 class="workspace-review-title">${esc(label)}</h2>
|
||||||
@@ -4512,6 +4926,7 @@ function renderSpeakWorkspace(view) {
|
|||||||
: "";
|
: "";
|
||||||
return `<section class="workspace-intent">
|
return `<section class="workspace-intent">
|
||||||
<div class="workspace-intent-head">
|
<div class="workspace-intent-head">
|
||||||
|
${leaveStepControlHtml(view)}
|
||||||
${
|
${
|
||||||
flowInteractive
|
flowInteractive
|
||||||
? workspaceViewSwitchHtml("flow")
|
? workspaceViewSwitchHtml("flow")
|
||||||
@@ -4519,7 +4934,6 @@ function renderSpeakWorkspace(view) {
|
|||||||
canOfferSaveProduct(view) ? "落档" : "说话"
|
canOfferSaveProduct(view) ? "落档" : "说话"
|
||||||
}</span>`
|
}</span>`
|
||||||
}
|
}
|
||||||
${leaveStepControlHtml(view)}
|
|
||||||
</div>
|
</div>
|
||||||
<h2 class="workspace-intent-title">${esc(title)}</h2>
|
<h2 class="workspace-intent-title">${esc(title)}</h2>
|
||||||
<p class="workspace-intent-hint">${esc(hint)}</p>
|
<p class="workspace-intent-hint">${esc(hint)}</p>
|
||||||
@@ -4534,9 +4948,9 @@ function renderSpeakWorkspace(view) {
|
|||||||
function renderAnswerWorkspaceShell(view) {
|
function renderAnswerWorkspaceShell(view) {
|
||||||
return `<section class="workspace-answer" id="workspace-answer-slot">
|
return `<section class="workspace-answer" id="workspace-answer-slot">
|
||||||
<header class="workspace-answer-head workspace-answer-head--slim">
|
<header class="workspace-answer-head workspace-answer-head--slim">
|
||||||
|
${leaveStepControlHtml(view)}
|
||||||
<span class="workspace-surface-kicker">答题</span>
|
<span class="workspace-surface-kicker">答题</span>
|
||||||
<p class="workspace-answer-hint">点字母选中,文案可改;点卡片展开,点顶条收起。</p>
|
<p class="workspace-answer-hint">点字母选中,文案可改;点卡片展开,点顶条收起。</p>
|
||||||
${leaveStepControlHtml(view)}
|
|
||||||
</header>
|
</header>
|
||||||
</section>`;
|
</section>`;
|
||||||
}
|
}
|
||||||
@@ -4602,10 +5016,12 @@ function mountReviewWorkspace(stage, view, handlers, opts = {}) {
|
|||||||
renderReviewWorkspace(view.reviewArtifact, {
|
renderReviewWorkspace(view.reviewArtifact, {
|
||||||
hideAskSidecar: opts.hideAskSidecar === true,
|
hideAskSidecar: opts.hideAskSidecar === true,
|
||||||
sourceMessage: resolveReviewSourceMessage(view),
|
sourceMessage: resolveReviewSourceMessage(view),
|
||||||
|
leaveStepHtml: leaveStepControlHtml(view),
|
||||||
}) + askSlot;
|
}) + askSlot;
|
||||||
wireMessageFeedActions(stage, handlers);
|
wireMessageFeedActions(stage, handlers);
|
||||||
wireMessageContextMenu(stage, handlers);
|
wireMessageContextMenu(stage, handlers);
|
||||||
wireCreationFlowGraph(stage, handlers);
|
wireCreationFlowGraph(stage, handlers);
|
||||||
|
wireLeaveStepControl(stage, handlers);
|
||||||
wireOpeningPicker();
|
wireOpeningPicker();
|
||||||
if (opts.mountAskCard) {
|
if (opts.mountAskCard) {
|
||||||
const askHost = document.getElementById("workspace-review-ask");
|
const askHost = document.getElementById("workspace-review-ask");
|
||||||
@@ -4696,13 +5112,13 @@ export function renderMessageFeed(view, loading, handlers = {}) {
|
|||||||
stage.innerHTML = `
|
stage.innerHTML = `
|
||||||
<div class="workspace-pending" id="msg-live-pending">
|
<div class="workspace-pending" id="msg-live-pending">
|
||||||
<header class="workspace-pending-head">
|
<header class="workspace-pending-head">
|
||||||
|
${leaveStepControlHtml(view)}
|
||||||
<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);
|
wireLeaveStepControl(stage, handlers);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+38
-22
@@ -546,7 +546,7 @@ function resolveComposer(view, loading) {
|
|||||||
|
|
||||||
function composerModeChip(spec) {
|
function composerModeChip(spec) {
|
||||||
const id = spec.task?.id;
|
const id = spec.task?.id;
|
||||||
if (id !== "speak" && id !== "answer" && id !== "review") return "";
|
if (id !== "speak" && id !== "answer" && id !== "review" && id !== "busy") return "";
|
||||||
const label = spec.task.label || "";
|
const label = spec.task.label || "";
|
||||||
const tip = [spec.taskTitle, spec.taskHint].filter(Boolean).join(" — ");
|
const tip = [spec.taskTitle, spec.taskHint].filter(Boolean).join(" — ");
|
||||||
const tone = spec.acceptAction?.tone ? ` data-tone="${esc(spec.acceptAction.tone)}"` : "";
|
const tone = spec.acceptAction?.tone ? ` data-tone="${esc(spec.acceptAction.tone)}"` : "";
|
||||||
@@ -586,13 +586,6 @@ function syncThemeMenuUi() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function leaveStepButtonHtml(spec) {
|
|
||||||
if (!spec.leaveStep) return "";
|
|
||||||
return `<button type="button" class="btn" data-act="leave_step" title="${esc(
|
|
||||||
spec.leaveStep.title || "先不写这一步,回到节点选择",
|
|
||||||
)}">${esc(spec.leaveStep.label)}</button>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function stashComposerDraftFromDom() {
|
function stashComposerDraftFromDom() {
|
||||||
if (skipComposerStash) {
|
if (skipComposerStash) {
|
||||||
skipComposerStash = false;
|
skipComposerStash = false;
|
||||||
@@ -650,20 +643,22 @@ function renderComposer(view, loading) {
|
|||||||
host._enterHandler = null;
|
host._enterHandler = null;
|
||||||
}
|
}
|
||||||
const spec = resolveComposer(view, loading);
|
const spec = resolveComposer(view, loading);
|
||||||
const leaveAct = view.actions?.find((a) => a.type === "leave_step");
|
|
||||||
if (leaveAct && spec.mode !== "waiting" && spec.mode !== "idle") {
|
|
||||||
spec.leaveStep = {
|
|
||||||
label: leaveAct.label || "返回节点",
|
|
||||||
title: "先不写这一步,回到节点选择",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (spec.mode === "waiting") {
|
if (spec.mode === "waiting") {
|
||||||
root.innerHTML = `<div class="composer-waiting">
|
const shell = composerInputShell(
|
||||||
<span>${esc(spec.text)}</span>
|
{ ...spec, task: { ...(spec.task || {}), id: "busy", label: spec.task?.label || "生成中" } },
|
||||||
<button type="button" class="btn" data-act="retry_run" title="停止当前生成并重试">停止并重试</button>
|
{
|
||||||
</div>`;
|
textareaHtml: `<textarea id="composer-input" rows="1" placeholder="生成中…可提前停止或重roll"></textarea>`,
|
||||||
|
trailing: `<button type="button" class="btn composer-btn-icon" data-act="abort_run" title="提前停止" aria-label="提前停止">□</button>
|
||||||
|
<button type="button" class="btn composer-btn-icon" data-act="retry_run" title="重roll" aria-label="重roll">🔄</button>`,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
root.innerHTML = `<form class="composer-form" id="composer-form">${shell}</form>`;
|
||||||
|
root.querySelector("#composer-form")?.addEventListener("submit", (e) => e.preventDefault());
|
||||||
|
root.querySelector("[data-act=abort_run]")?.addEventListener("click", () => abortRun());
|
||||||
root.querySelector("[data-act=retry_run]")?.addEventListener("click", () => retryRun());
|
root.querySelector("[data-act=retry_run]")?.addEventListener("click", () => retryRun());
|
||||||
|
applyPendingComposerDraft();
|
||||||
|
const input = $("composer-input");
|
||||||
|
if (input) autosizeComposerInput(input);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (spec.mode === "idle") {
|
if (spec.mode === "idle") {
|
||||||
@@ -878,7 +873,7 @@ function renderComposer(view, loading) {
|
|||||||
: spec.placeholder;
|
: spec.placeholder;
|
||||||
const shell = composerInputShell(spec, {
|
const shell = composerInputShell(spec, {
|
||||||
textareaHtml: `<textarea id="composer-input" rows="1" placeholder="${esc(placeholder)}"></textarea>`,
|
textareaHtml: `<textarea id="composer-input" rows="1" placeholder="${esc(placeholder)}"></textarea>`,
|
||||||
trailing: `${leaveStepButtonHtml(spec)}<button type="button" class="btn composer-btn${
|
trailing: `<button type="button" class="btn composer-btn${
|
||||||
sendIsPrimary ? " btn-primary" : ""
|
sendIsPrimary ? " btn-primary" : ""
|
||||||
}" data-act="revise" title="${esc(
|
}" data-act="revise" title="${esc(
|
||||||
acceptOnEmpty
|
acceptOnEmpty
|
||||||
@@ -889,7 +884,6 @@ function renderComposer(view, loading) {
|
|||||||
root.innerHTML = `<form class="composer-form" id="composer-form">${shell}</form>`;
|
root.innerHTML = `<form class="composer-form" id="composer-form">${shell}</form>`;
|
||||||
wireComposerForm({ acceptOnEmpty });
|
wireComposerForm({ acceptOnEmpty });
|
||||||
root.querySelector("[data-act=accept]")?.addEventListener("click", () => runAction("accept"));
|
root.querySelector("[data-act=accept]")?.addEventListener("click", () => runAction("accept"));
|
||||||
root.querySelector("[data-act=leave_step]")?.addEventListener("click", () => runAction("leave_step"));
|
|
||||||
applyPendingComposerDraft();
|
applyPendingComposerDraft();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1953,6 +1947,23 @@ async function retryRun() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function abortRun() {
|
||||||
|
if (!sessionId) return;
|
||||||
|
const seq = beginUiRequest();
|
||||||
|
try {
|
||||||
|
renderSession(lastView, true);
|
||||||
|
const view = await api(`/api/sessions/${encodeURIComponent(sessionId)}/actions`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({ action: "abort_run" }),
|
||||||
|
});
|
||||||
|
if (isStaleUiRequest(seq)) return;
|
||||||
|
renderSession(view, false);
|
||||||
|
} catch (err) {
|
||||||
|
if (isStaleUiRequest(seq)) return;
|
||||||
|
if (lastView) renderSession({ ...lastView, hints: [err.message] }, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function startLivePoll() {
|
function startLivePoll() {
|
||||||
stopLivePoll();
|
stopLivePoll();
|
||||||
if (!sessionId) return;
|
if (!sessionId) return;
|
||||||
@@ -2009,6 +2020,11 @@ function renderSession(view, loading = false) {
|
|||||||
onReenterCreationStep: (stepId) =>
|
onReenterCreationStep: (stepId) =>
|
||||||
runAction("pick_step", { stepId, reenter: true }),
|
runAction("pick_step", { stepId, reenter: true }),
|
||||||
onViewStepArtifact: (stepId, name) => showStepArtifactDialog(stepId, name),
|
onViewStepArtifact: (stepId, name) => showStepArtifactDialog(stepId, name),
|
||||||
|
onDeleteCreationStep: (stepId, name) => {
|
||||||
|
const label = name ? `${name}` : stepId;
|
||||||
|
if (!confirm(`删除「${label}」这条产物?会从图上拿掉,不可恢复。`)) return;
|
||||||
|
void runAction("delete_step", { stepId });
|
||||||
|
},
|
||||||
onSpawnCreationStep: (moduleName) => runAction("spawn_step", { moduleName }),
|
onSpawnCreationStep: (moduleName) => runAction("spawn_step", { moduleName }),
|
||||||
onLeaveCreationStep: () => runAction("leave_step"),
|
onLeaveCreationStep: () => runAction("leave_step"),
|
||||||
onEnterPlay: () => enterPlayNow(),
|
onEnterPlay: () => enterPlayNow(),
|
||||||
|
|||||||
+2
-1
@@ -233,7 +233,8 @@
|
|||||||
|
|
||||||
<menu id="flow-node-menu" class="context-menu" hidden>
|
<menu id="flow-node-menu" class="context-menu" hidden>
|
||||||
<button type="button" class="context-menu-item" id="flow-menu-view" data-flow-menu-action="view">查看产物</button>
|
<button type="button" class="context-menu-item" id="flow-menu-view" data-flow-menu-action="view">查看产物</button>
|
||||||
<button type="button" class="context-menu-item" id="flow-menu-reenter" data-flow-menu-action="reenter">重新进入修改</button>
|
<button type="button" class="context-menu-item" id="flow-menu-reenter" data-flow-menu-action="reenter">进入继续修改</button>
|
||||||
|
<button type="button" class="context-menu-item context-menu-danger context-menu-separator-before" id="flow-menu-delete" data-flow-menu-action="delete">删除产物</button>
|
||||||
</menu>
|
</menu>
|
||||||
|
|
||||||
<dialog id="dialog-step-artifact" class="dialog dialog-wide" aria-labelledby="step-artifact-title">
|
<dialog id="dialog-step-artifact" class="dialog dialog-wide" aria-labelledby="step-artifact-title">
|
||||||
|
|||||||
+168
-21
@@ -479,21 +479,26 @@ body.design-workspace.coord-rail-collapsed .main-body {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
padding-bottom: 10px;
|
padding-bottom: 10px;
|
||||||
border-bottom: 1px solid var(--border);
|
border-bottom: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
|
.workspace-review-head .workspace-leave-step {
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-right: 2px;
|
||||||
|
}
|
||||||
.workspace-review-titles {
|
.workspace-review-titles {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
flex: 1 1 auto;
|
||||||
}
|
}
|
||||||
.workspace-review-nav {
|
.workspace-review-nav {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
.workspace-review-nav .msg-variant-nav {
|
.workspace-review-nav .msg-variant-nav {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -860,6 +865,9 @@ body.design-workspace .workspace-pending {
|
|||||||
margin-bottom: 6px;
|
margin-bottom: 6px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
.workspace-pending-head .workspace-leave-step {
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
.workspace-pending-head .msg-action {
|
.workspace-pending-head .msg-action {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
@@ -948,7 +956,7 @@ body.design-workspace .review-worker-set {
|
|||||||
.workspace-intent-head {
|
.workspace-intent-head {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
justify-content: space-between;
|
justify-content: flex-start;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
max-width: 46rem;
|
max-width: 46rem;
|
||||||
@@ -979,7 +987,7 @@ body.design-workspace .review-worker-set {
|
|||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
}
|
}
|
||||||
.workspace-answer-head--slim .workspace-leave-step {
|
.workspace-answer-head--slim .workspace-leave-step {
|
||||||
margin-left: auto;
|
margin-right: 4px;
|
||||||
}
|
}
|
||||||
.workspace-intent,
|
.workspace-intent,
|
||||||
.workspace-answer {
|
.workspace-answer {
|
||||||
@@ -1213,7 +1221,7 @@ body.design-workspace .flow-status {
|
|||||||
body.design-workspace .flow-dag {
|
body.design-workspace .flow-dag {
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
padding: 4px 2px 8px;
|
padding: 8px 8px 16px;
|
||||||
}
|
}
|
||||||
body.design-workspace .flow-dag-edges {
|
body.design-workspace .flow-dag-edges {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -1225,23 +1233,41 @@ body.design-workspace .flow-dag-edges {
|
|||||||
overflow: visible;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
body.design-workspace .flow-dag-edge {
|
body.design-workspace .flow-dag-edge {
|
||||||
stroke: color-mix(in srgb, var(--border) 80%, var(--muted));
|
stroke: color-mix(in srgb, var(--text) 32%, var(--border));
|
||||||
stroke-width: 1.5;
|
stroke-width: 1.5;
|
||||||
opacity: 0.85;
|
opacity: 0.55;
|
||||||
}
|
stroke-linecap: round;
|
||||||
body.design-workspace .flow-dag-edge.is-lit {
|
|
||||||
stroke: color-mix(in srgb, var(--accent) 55%, var(--border));
|
|
||||||
opacity: 1;
|
|
||||||
}
|
}
|
||||||
body.design-workspace .flow-dag-arrowhead {
|
body.design-workspace .flow-dag-arrowhead {
|
||||||
fill: color-mix(in srgb, var(--accent) 50%, var(--border));
|
fill: color-mix(in srgb, var(--text) 38%, var(--border));
|
||||||
|
}
|
||||||
|
body.design-workspace .flow-dag.is-tracing .flow-dag-edge {
|
||||||
|
opacity: 0.12;
|
||||||
|
}
|
||||||
|
body.design-workspace .flow-dag.is-tracing .flow-dag-edge.is-trace-in,
|
||||||
|
body.design-workspace .flow-dag.is-tracing .flow-dag-edge.is-trace-out {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
body.design-workspace .flow-dag-edge.is-trace-in {
|
||||||
|
stroke: var(--accent);
|
||||||
|
stroke-width: 2.35;
|
||||||
|
}
|
||||||
|
body.design-workspace .flow-dag-edge.is-trace-out {
|
||||||
|
stroke: color-mix(in srgb, var(--text) 72%, var(--accent));
|
||||||
|
stroke-width: 2.1;
|
||||||
|
}
|
||||||
|
body.design-workspace .flow-dag-arrowhead--in {
|
||||||
|
fill: var(--accent);
|
||||||
|
}
|
||||||
|
body.design-workspace .flow-dag-arrowhead--out {
|
||||||
|
fill: color-mix(in srgb, var(--text) 72%, var(--accent));
|
||||||
}
|
}
|
||||||
body.design-workspace .flow-dag-rows {
|
body.design-workspace .flow-dag-rows {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 28px;
|
gap: 44px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
body.design-workspace .flow-dag-row {
|
body.design-workspace .flow-dag-row {
|
||||||
@@ -1262,8 +1288,96 @@ body.design-workspace .flow-dag-row-nodes {
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: 12px;
|
gap: 24px 104px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
padding: 0 28px;
|
||||||
|
}
|
||||||
|
body.design-workspace .flow-dag.is-tracing .flow-node {
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
body.design-workspace .flow-dag.is-tracing .flow-node.is-trace-root,
|
||||||
|
body.design-workspace .flow-dag.is-tracing .flow-node.is-trace-related,
|
||||||
|
body.design-workspace .flow-dag.is-tracing .flow-node.is-locked.is-trace-root,
|
||||||
|
body.design-workspace .flow-dag.is-tracing .flow-node.is-locked.is-trace-related {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
body.design-workspace .flow-dag.is-tracing .flow-node.is-trace-root {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
border-color: var(--accent);
|
||||||
|
box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent) 22%, transparent);
|
||||||
|
}
|
||||||
|
body.design-workspace .flow-dag-hint {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 4;
|
||||||
|
width: max-content;
|
||||||
|
max-width: min(240px, calc(100% - 24px));
|
||||||
|
padding: 8px 10px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--surface);
|
||||||
|
box-shadow: 0 8px 22px color-mix(in srgb, var(--text) 12%, transparent);
|
||||||
|
pointer-events: none;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
body.design-workspace .flow-dag-hint[hidden] {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
body.design-workspace .flow-dag-hint-kicker {
|
||||||
|
margin: 0 0 2px;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 650;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
body.design-workspace .flow-dag-hint-name {
|
||||||
|
margin: 0 0 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 650;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
body.design-workspace .flow-dag-hint-sec + .flow-dag-hint-sec {
|
||||||
|
margin-top: 8px;
|
||||||
|
padding-top: 8px;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
body.design-workspace .flow-dag-hint-label {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 650;
|
||||||
|
color: var(--muted);
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
body.design-workspace .flow-dag-hint-empty {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
body.design-workspace .flow-dag-hint-list {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
body.design-workspace .flow-dag-hint-list li {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
body.design-workspace .flow-dag-hint-item-name {
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
body.design-workspace .flow-dag-hint-item-run {
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
body.design-workspace .flow-node {
|
body.design-workspace .flow-node {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -1321,10 +1435,13 @@ body.design-workspace .flow-node-menu-btn {
|
|||||||
line-height: 1;
|
line-height: 1;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
opacity: 0.55;
|
opacity: 0.55;
|
||||||
pointer-events: none;
|
pointer-events: auto;
|
||||||
|
cursor: context-menu;
|
||||||
}
|
}
|
||||||
body.design-workspace .flow-node.is-done-menu:hover .flow-node-menu-btn,
|
body.design-workspace .flow-node.is-done-menu:hover .flow-node-menu-btn,
|
||||||
body.design-workspace .flow-node.is-done-menu:focus-visible .flow-node-menu-btn {
|
body.design-workspace .flow-node.is-done-menu:focus-visible .flow-node-menu-btn,
|
||||||
|
body.design-workspace .flow-node.is-selectable:hover .flow-node-menu-btn,
|
||||||
|
body.design-workspace .flow-node.is-selectable:focus-visible .flow-node-menu-btn {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
.step-artifact-body {
|
.step-artifact-body {
|
||||||
@@ -1349,14 +1466,12 @@ body.design-workspace .flow-node.is-selectable {
|
|||||||
0 4px 12px color-mix(in srgb, var(--accent) 12%, transparent);
|
0 4px 12px color-mix(in srgb, var(--accent) 12%, transparent);
|
||||||
}
|
}
|
||||||
body.design-workspace .flow-node.is-selectable:hover {
|
body.design-workspace .flow-node.is-selectable:hover {
|
||||||
transform: translateY(-2px);
|
|
||||||
background: color-mix(in srgb, var(--accent) 10%, var(--surface));
|
background: color-mix(in srgb, var(--accent) 10%, var(--surface));
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 0 0 1px color-mix(in srgb, var(--accent) 28%, transparent),
|
0 0 0 1px color-mix(in srgb, var(--accent) 28%, transparent),
|
||||||
0 8px 18px color-mix(in srgb, var(--accent) 16%, transparent);
|
0 8px 18px color-mix(in srgb, var(--accent) 16%, transparent);
|
||||||
}
|
}
|
||||||
body.design-workspace .flow-node.is-selectable:active {
|
body.design-workspace .flow-node.is-selectable:active {
|
||||||
transform: translateY(0) scale(0.98);
|
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 0 0 1px color-mix(in srgb, var(--accent) 35%, transparent),
|
0 0 0 1px color-mix(in srgb, var(--accent) 35%, transparent),
|
||||||
0 2px 6px color-mix(in srgb, var(--accent) 12%, transparent);
|
0 2px 6px color-mix(in srgb, var(--accent) 12%, transparent);
|
||||||
@@ -1465,6 +1580,7 @@ body.design-workspace .flow-spawn-meta {
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
}
|
}
|
||||||
body.design-workspace .flow-spawn-meta .flow-action {
|
body.design-workspace .flow-spawn-meta .flow-action {
|
||||||
min-height: 24px;
|
min-height: 24px;
|
||||||
@@ -1495,7 +1611,11 @@ body.design-workspace .flow-dag-edge--belong {
|
|||||||
stroke-width: 1.25;
|
stroke-width: 1.25;
|
||||||
}
|
}
|
||||||
body.design-workspace .flow-dag-arrowhead--belong {
|
body.design-workspace .flow-dag-arrowhead--belong {
|
||||||
fill: color-mix(in srgb, var(--accent) 38%, var(--border));
|
fill: color-mix(in srgb, var(--text) 28%, var(--border));
|
||||||
|
}
|
||||||
|
body.design-workspace .flow-dag.is-tracing .flow-dag-edge--belong.is-trace-in,
|
||||||
|
body.design-workspace .flow-dag.is-tracing .flow-dag-edge--belong.is-trace-out {
|
||||||
|
stroke-dasharray: 4 4;
|
||||||
}
|
}
|
||||||
body.design-workspace .flow-suggestion-inline {
|
body.design-workspace .flow-suggestion-inline {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
@@ -1604,6 +1724,8 @@ body.design-workspace .flow-spawn.is-blocked {
|
|||||||
body.design-workspace .flow-dag-row-nodes {
|
body.design-workspace .flow-dag-row-nodes {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 0;
|
||||||
}
|
}
|
||||||
body.design-workspace .flow-node,
|
body.design-workspace .flow-node,
|
||||||
body.design-workspace .flow-family {
|
body.design-workspace .flow-family {
|
||||||
@@ -3559,8 +3681,21 @@ body.design-workspace .af-mosaic {
|
|||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
.skill-pool-desc {
|
.skill-pool-card.is-direction {
|
||||||
margin: 6px 0 0;
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
.skill-pool-dirs {
|
||||||
|
margin: 8px 0 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.skill-pool-dir dt {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.skill-pool-dir dd {
|
||||||
|
margin: 2px 0 0;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
@@ -4821,7 +4956,7 @@ body.design-workspace .af-mosaic {
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 10px;
|
||||||
padding-bottom: 2px;
|
padding-bottom: 2px;
|
||||||
}
|
}
|
||||||
.composer-mode-chip {
|
.composer-mode-chip {
|
||||||
@@ -4861,6 +4996,10 @@ body.design-workspace .af-mosaic {
|
|||||||
.composer-input-shell[data-task="review"][data-tone="plan"] .composer-mode-chip {
|
.composer-input-shell[data-task="review"][data-tone="plan"] .composer-mode-chip {
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
}
|
}
|
||||||
|
.composer-mode-chip[data-task="busy"],
|
||||||
|
.composer-input-shell[data-task="busy"] .composer-mode-chip {
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
.composer-input-shell textarea {
|
.composer-input-shell textarea {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
@@ -4917,6 +5056,7 @@ body.design-workspace .af-mosaic {
|
|||||||
.composer-shell-actions > .btn-primary,
|
.composer-shell-actions > .btn-primary,
|
||||||
.composer-shell-actions > .composer-btn,
|
.composer-shell-actions > .composer-btn,
|
||||||
.composer-shell-actions > .composer-btn-accept,
|
.composer-shell-actions > .composer-btn-accept,
|
||||||
|
.composer-shell-actions > .composer-btn-icon,
|
||||||
.composer-form > .btn,
|
.composer-form > .btn,
|
||||||
.composer-form > .btn-primary,
|
.composer-form > .btn-primary,
|
||||||
.composer-form > .composer-btn,
|
.composer-form > .composer-btn,
|
||||||
@@ -4933,6 +5073,13 @@ body.design-workspace .af-mosaic {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
.composer-btn-icon {
|
||||||
|
width: calc(var(--composer-control-h) - 8px);
|
||||||
|
min-width: calc(var(--composer-control-h) - 8px);
|
||||||
|
padding: 0;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
/* 验收态:主按钮承接原「批阅」语义 */
|
/* 验收态:主按钮承接原「批阅」语义 */
|
||||||
.composer-btn-review {
|
.composer-btn-review {
|
||||||
background: #15803d;
|
background: #15803d;
|
||||||
|
|||||||
Reference in New Issue
Block a user