Add capability-probed structured outputs and a prototype/instance creation graph.
Probe API profiles for json_schema/tools, flatten worker artifacts onto a strict schema, and let users spawn or revise graph nodes instead of a one-shot DAG. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
| 产物/技能 | 美化程度 | 渲染入口 |
|
| 产物/技能 | 美化程度 | 渲染入口 |
|
||||||
|-----------|----------|----------|
|
|-----------|----------|----------|
|
||||||
| 美学纲领与交互范式 | **左右分栏**:左契约(美学纲领+交互范式)/ 右诊断(设定逻辑) | `renderSpecialtyBodyHtml` → aesthetics |
|
| 美学纲领与交互范式 | **左右分栏**(左契约可读 / 右诊断密);无工程 banner | `af-split--aesthetics` + `renderAestheticsNodeHtml` |
|
||||||
| 实现机制 | **专用卡**(支撑点) | `renderMechanismBodyHtml` |
|
| 实现机制 | **专用卡**(支撑点) | `renderMechanismBodyHtml` |
|
||||||
| 舞台骨架 | **专用卡** | `renderWorldBlueprintBodyHtml` |
|
| 舞台骨架 | **专用卡** | `renderWorldBlueprintBodyHtml` |
|
||||||
| 生成规则 | **专用卡** | `renderGenerationRulesBodyHtml` |
|
| 生成规则 | **专用卡** | `renderGenerationRulesBodyHtml` |
|
||||||
|
|||||||
@@ -121,8 +121,7 @@
|
|||||||
"steps": [
|
"steps": [
|
||||||
{ "id": "美学纲领与交互范式", "name": "美学纲领与交互范式", "depends_on": [] },
|
{ "id": "美学纲领与交互范式", "name": "美学纲领与交互范式", "depends_on": [] },
|
||||||
{ "id": "生成规则", "name": "生成规则", "depends_on": ["美学纲领与交互范式"] },
|
{ "id": "生成规则", "name": "生成规则", "depends_on": ["美学纲领与交互范式"] },
|
||||||
{ "id": "生成规则#2", "name": "生成规则", "depends_on": ["生成规则"] },
|
{ "id": "生成规则#2", "name": "生成规则", "depends_on": ["生成规则"] }
|
||||||
{ "id": "美学纲领与交互范式·改", "name": "美学纲领与交互范式", "mode": "revise", "revises": "美学纲领与交互范式", "depends_on": ["生成规则#2"] }
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -134,12 +133,10 @@
|
|||||||
| `id` | 本局步骤唯一键;同技能多次必须不同 |
|
| `id` | 本局步骤唯一键;同技能多次必须不同 |
|
||||||
| `name` | 必须与技能目录中的**固定中文名**完全一致(可重复) |
|
| `name` | 必须与技能目录中的**固定中文名**完全一致(可重复) |
|
||||||
| `depends_on` | 依赖的其它步骤 **id**(若某 name 在本流程唯一,也可写 name) |
|
| `depends_on` | 依赖的其它步骤 **id**(若某 name 在本流程唯一,也可写 name) |
|
||||||
| `mode` | 可选。缺省 / `fresh` = 从零新建;`revise` = 回头修改,继承 `revises` 所指那步的既有产物 |
|
|
||||||
| `revises` | `mode=revise` 时必填:被改的既有步骤 id(须更前、同 name) |
|
|
||||||
| `params` | 可选。普通节点=编排参数。〔先验产物〕=编排器规划的产物内容(可空;执行注入【规划产物】) |
|
| `params` | 可选。普通节点=编排参数。〔先验产物〕=编排器规划的产物内容(可空;执行注入【规划产物】) |
|
||||||
|
|
||||||
程序用中文 `name` 映射到 `artifact` tag;流程 JSON **不写**英文模块 id / tag。
|
程序用中文 `name` 映射到 `artifact` tag;流程 JSON **不写**英文模块 id / tag。
|
||||||
**禁止**一次排死全程固定长链;「生成规则」「具体实例」等标了 `repeatable` 的技能应允许多次**新建**编入。已完成节点要改用 `mode=revise`,不要再排一条 fresh。
|
**禁止**一次排死全程固定长链;「生成规则」「具体实例」等标了 `repeatable` 的技能应允许多次**新建**编入。已完成节点要改:用户在图上点该节点重进,不要排 `mode=revise` 新步。
|
||||||
〔先验产物〕节点可提前规划多条「写什么」写入 params;规划可空,「生成什么」在步内钉。
|
〔先验产物〕节点可提前规划多条「写什么」写入 params;规划可空,「生成什么」在步内钉。
|
||||||
|
|
||||||
## 2.5 已合并技能(勿拆回)
|
## 2.5 已合并技能(勿拆回)
|
||||||
|
|||||||
@@ -29,9 +29,13 @@
|
|||||||
推荐游玩调度:`gm → perspective? → narrator → auditor`。
|
推荐游玩调度:`gm → perspective? → narrator → auditor`。
|
||||||
旁观维护与叙事转述默认**不注入**「对话.历史」;主世界层按投影排序表裁剪历史。
|
旁观维护与叙事转述默认**不注入**「对话.历史」;主世界层按投影排序表裁剪历史。
|
||||||
|
|
||||||
## 3. 上下文片段:`context-fragment.v1`
|
## 3. 上下文片段:`context-fragment.v1` / 扁形 `artifact.flat.v1`
|
||||||
|
|
||||||
公共外壳(**所有上下文创作技能相同**):
|
**投递**(全局):按 API profile 探测能力选 `json_schema` → `forced_tool` → `json_object`,与具体技能无关。
|
||||||
|
|
||||||
|
**技能稿默认 `context-fragment.v1`**:`正文` 为对象树,深度约 **2 层分组 + 叶子**(短字段、string[],或诊断 `{结论, 依据, 完备度}`)。不要把整柱收成一段散文。扁形 `artifact.flat.v1` 仅为投递兼容;若 `sections[].text` 是 JSON 对象字符串,程序会还原进正文键。
|
||||||
|
|
||||||
|
公共外壳:
|
||||||
|
|
||||||
| 段 | 含义 |
|
| 段 | 含义 |
|
||||||
|----|------|
|
|----|------|
|
||||||
@@ -54,7 +58,7 @@
|
|||||||
**存盘**:三块都在同一 `正文` 里。**注入**:默认 `full` 时整份正文都会进模型——若日后只要左栏进游玩,再改投影,不靠藏 UI。
|
**存盘**:三块都在同一 `正文` 里。**注入**:默认 `full` 时整份正文都会进模型——若日后只要左栏进游玩,再改投影,不靠藏 UI。
|
||||||
实现机制正文:`依据的核心体验` + `支撑点[]` + `支撑点关系[]` + `覆盖检验`(见 `mechanism`)。
|
实现机制正文:`依据的核心体验` + `支撑点[]` + `支撑点关系[]` + `覆盖检验`(见 `mechanism`)。
|
||||||
舞台骨架正文:尺度 / 基底与变造 / **社会结构** / **世界状况** / 关键舞台区 / 未展开范围(见 `world-blueprint`)。
|
舞台骨架正文:尺度 / 基底与变造 / **社会结构** / **世界状况** / 关键舞台区 / 未展开范围(见 `world-blueprint`)。
|
||||||
生成规则正文:必要性判断 + `rules[]`(生成与描写 + **单层**产物格式 + 可选多池绑顶层字段);自评=必要性/属性妥当/格式准确。
|
生成规则正文:必要性判断 + `rules[]`(生成与描写 + **单层**产物格式 + 可选多池绑顶层字段 + **数量**与**一次生成**(单条|同批,同批须钉记录间关系));自评=必要性/属性妥当/格式准确。
|
||||||
美学纲领自评仍为:交互范式 / 美学纲领 / 整体协调(勿与生成规则三维混用)。
|
美学纲领自评仍为:交互范式 / 美学纲领 / 整体协调(勿与生成规则三维混用)。
|
||||||
叙事指南与故事推进正文:一份全文(纲领/遣词/笔墨/禁忌/推进等);**整份**双挂转述与主世界层,不为省 token 拆两套投影。artifact=`设计.叙事指南与故事推进`。
|
叙事指南与故事推进正文:一份全文(纲领/遣词/笔墨/禁忌/推进等);**整份**双挂转述与主世界层,不为省 token 拆两套投影。artifact=`设计.叙事指南与故事推进`。
|
||||||
正文组成:**基于固定呈现壳适配微调**(选 `shell_id` + 微调轴;块只挂壳已有区域);正文含 **`示例`**(`present.v1` 灌数,验收壳与美化;非正史,文笔不重要);artifact=`设计.正文组成`。见 `docs/play-presentation-shells.md`。
|
正文组成:**基于固定呈现壳适配微调**(选 `shell_id` + 微调轴;块只挂壳已有区域);正文含 **`示例`**(`present.v1` 灌数,验收壳与美化;非正史,文笔不重要);artifact=`设计.正文组成`。见 `docs/play-presentation-shells.md`。
|
||||||
@@ -65,7 +69,7 @@
|
|||||||
开场白与开场变量:开场正文(守正文组成)+ 开场初值;可经 opening-generator 落库。
|
开场白与开场变量:开场正文(守正文组成)+ 开场初值;可经 opening-generator 落库。
|
||||||
|
|
||||||
|
|
||||||
具体实例正文:`rule_id` + `records[]`(单层,键随规则);自评=合规模(只按规则执行)。
|
具体实例正文:`rule_id` + `records[]`(始终数组;单层,键随规则;条数服从规则「一次生成」);自评=合规模(只按规则执行)。
|
||||||
其它技能换掉正文内块名与自评维度名即可,三段外壳不要拆。
|
其它技能换掉正文内块名与自评维度名即可,三段外壳不要拆。
|
||||||
## 4. 投影排序:`context-order.v1`(扁平序)
|
## 4. 投影排序:`context-order.v1`(扁平序)
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ Book 跨 Session:过程、资产、游玩
|
|||||||
新建作品 → UI 选配方 → 用户首句
|
新建作品 → UI 选配方 → 用户首句
|
||||||
→ design-flow:排出近期 设计.创作流程(工作流计划 / 增量 DAG,status=open|closed)
|
→ design-flow:排出近期 设计.创作流程(工作流计划 / 增量 DAG,status=open|closed)
|
||||||
→ 反复 design-step:注入当前技能 modules/{id}/prompt.md + 依赖产物
|
→ 反复 design-step:注入当前技能 modules/{id}/prompt.md + 依赖产物
|
||||||
→ 不够则再 design-flow(可追加 / 反复调用 repeatable 技能作**新建**;已完成节点用回头修改继承旧稿)→ closed
|
→ 不够则再 design-flow(可追加 / 反复调用 repeatable 技能作**新建**)→ closed
|
||||||
→ 收成 设计.worker集(运行规格)→ 验收后才可进 play
|
→ 收成 设计.worker集(运行规格)→ 验收后才可进 play
|
||||||
→ 开场白与开场变量(若编排):1~多条开场 → 选定 → 程序落库、关 DAG、保存 instance
|
→ 开场白与开场变量(若编排):1~多条开场 → 选定 → 程序落库、关 DAG、保存 instance
|
||||||
→ (无开场白节点时可选)opening-generator 兜底落库
|
→ (无开场白节点时可选)opening-generator 兜底落库
|
||||||
|
|||||||
+6
-3
@@ -35,7 +35,7 @@
|
|||||||
|
|
||||||
**一层选型**:新建作品只选**配方**,不要再叠「能力包 + 配方」两层。
|
**一层选型**:新建作品只选**配方**,不要再叠「能力包 + 配方」两层。
|
||||||
|
|
||||||
**调味**:配方给出的建议步骤是**近期起点**(horizon);编排时可增量追加、可反复调用标了可反复的技能(每次=彻底新建)。已完成节点要改用**回头修改**(继承旧产物),不必整局重开。验收的是本局【工作流计划】与【运行规格】,不是磁盘上的静态菜谱。
|
**调味**:配方给出的建议步骤是**近期起点**(horizon);编排时可增量追加、可反复调用标了可反复的技能(每次=彻底新建)。已完成节点要改:在图上点该节点重进(继承旧产物),不必整局重开。验收的是本局【工作流计划】与【运行规格】,不是磁盘上的静态菜谱。
|
||||||
|
|
||||||
**剧本拆分**:旧称「剧本」同时罩住流程与规格,易混。现固定拆成:
|
**剧本拆分**:旧称「剧本」同时罩住流程与规格,易混。现固定拆成:
|
||||||
- **工作流计划** = 怎么排技能步骤(DAG)
|
- **工作流计划** = 怎么排技能步骤(DAG)
|
||||||
@@ -181,12 +181,15 @@
|
|||||||
|----------------------|----------------|----------|
|
|----------------------|----------------|----------|
|
||||||
| `intake` | 描述需求 | 发送 |
|
| `intake` | 描述需求 | 发送 |
|
||||||
| `input` | 补充说明 | 发送 / 补充并发送 |
|
| `input` | 补充说明 | 发送 / 补充并发送 |
|
||||||
| `worker_questions` | 答题 | 补充并发送 |
|
| `worker_questions` | 答题 | 补充并发送;已进技能步时旁挂「返回节点」 |
|
||||||
| `approve_step` | 确认 | 同意并开始;回头修改时文案为「回头修改 · {技能名}」 |
|
| `approve_step` | 确认 | 同意并开始;重进已完成节点时文案为「回头修改 · {技能名}」;次要为「返回节点」 |
|
||||||
|
| `pick_creation_step` | 选节点 | 点可进入的节点即开始;要改排在底栏写意见再发 |
|
||||||
| `review_artifact` | 视产物(勿一律「验收」) | **流程编排** → 确认编排;技能步 → 接受;开场 → 选定此开场。旁挂「按意见改编排 / 按意见修改」 |
|
| `review_artifact` | 视产物(勿一律「验收」) | **流程编排** → 确认编排;技能步 → 接受;开场 → 选定此开场。旁挂「按意见改编排 / 按意见修改」 |
|
||||||
| `next_intent` | 说话 | 继续(可留空) |
|
| `next_intent` | 说话 | 继续(可留空) |
|
||||||
| `revision` | 说话 | 按意见修改 |
|
| `revision` | 说话 | 按意见修改 |
|
||||||
|
|
||||||
|
**返回节点**:已点进某步(尤其可增殖原型刚增殖出的实例)但还没写完时,退回分层图。未验收的空实例从图上撤掉,避免误点留下一条空槽。已完成节点重进后返回则保留该节点。勿写成「取消作品」或浏览器后退。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 8. 禁止出现在用户主路径上的写法
|
## 8. 禁止出现在用户主路径上的写法
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ declaration: …
|
|||||||
→ 循环直到「接受」
|
→ 循环直到「接受」
|
||||||
→ 直接提案下一节点并确认开干(普通节点可在此钉编排参数;〔先验产物〕不在此钉「写什么」;〔程序步〕如投影排序跳过确认直接执行)
|
→ 直接提案下一节点并确认开干(普通节点可在此钉编排参数;〔先验产物〕不在此钉「写什么」;〔程序步〕如投影排序跳过确认直接执行)
|
||||||
· 可反复技能再来一次 = 彻底新建(新 id / 新 params)
|
· 可反复技能再来一次 = 彻底新建(新 id / 新 params)
|
||||||
· 已完成节点要改 = 回头修改(mode=revise,继承旧产物)
|
· 已完成节点要改 = 用户在图上点该节点(查看产物 / 重新进入修改),不在 DAG 里加修订步
|
||||||
→ 下一节点 opening / design-step …
|
→ 下一节点 opening / design-step …
|
||||||
直到「开场白与开场变量」(〔收口〕)
|
直到「开场白与开场变量」(〔收口〕)
|
||||||
→ 产出 1~多条开场白 → 选定 → 程序落库、关闭 DAG、保存创作定稿
|
→ 产出 1~多条开场白 → 选定 → 程序落库、关闭 DAG、保存创作定稿
|
||||||
@@ -135,7 +135,7 @@ declaration: …
|
|||||||
|
|
||||||
「按意见修改」只重跑当前节点,不得跳到「下一步想写什么」。底栏空且未选追问时 Enter=接受;已选追问时 Enter=按意见修改。点按钮仍只做按钮自己的事。
|
「按意见修改」只重跑当前节点,不得跳到「下一步想写什么」。底栏空且未选追问时 Enter=接受;已选追问时 Enter=按意见修改。点按钮仍只做按钮自己的事。
|
||||||
|
|
||||||
「回头修改」是编排层能力:给已完成步骤追加一条 `mode=revise` 的新步,执行时注入既有产物继续改。它**不是**〔可反复〕再来一次——后者从零新建。漏了前面的节点、或谈完才发现要改上游,用回头修改,不必整局重开。
|
「回头修改」是已完成节点上的动作,不是 DAG 节点:用户点该格,菜单里回看产物或重进继承旧稿。它**不是**〔可反复〕再来一次——后者从零新建。漏了前面的节点、或谈完才发现要改上游,点那个已完成节点即可,不必整局重开。
|
||||||
|
|
||||||
〔先验产物〕节点(生成规则、具体实例):编排器可提前规划「写什么」写入 params——提前规划多条是正确的。确认开干不补「生成什么」;执行时注入【规划产物】,步内钉或修订。
|
〔先验产物〕节点(生成规则、具体实例):编排器可提前规划「写什么」写入 params——提前规划多条是正确的。确认开干不补「生成什么」;执行时注入【规划产物】,步内钉或修订。
|
||||||
|
|
||||||
@@ -188,6 +188,6 @@ recipes/world-simulator|expand-assistant/recipe.yaml
|
|||||||
1. 只选配方 → 出**近期**创作流程(`status=open`)
|
1. 只选配方 → 出**近期**创作流程(`status=open`)
|
||||||
2. design-step 能切割出 `opening`/`task`/…
|
2. design-step 能切割出 `opening`/`task`/…
|
||||||
3. 有 `opening` 时先程序开场再 LLM
|
3. 有 `opening` 时先程序开场再 LLM
|
||||||
4. 可追加同能力多次(不同 step.id + params,**彻底新建**);也可 `mode=revise` 回头修改已完成节点(继承旧产物);收成前 `status=closed`
|
4. 可追加同能力多次(不同 step.id + params,**彻底新建**);已完成节点由用户在图上点选重进(继承旧产物);收成前 `status=closed`
|
||||||
5. UI 用编排术语;有编排参数的步骤须展示 params;回头修改在流程上标「回头修改」,确认开干文案与「接下来生成」区分
|
5. UI 用编排术语;有编排参数的步骤须展示 params;已完成节点菜单区分「查看产物」与「重新进入修改」
|
||||||
6. 缺必填 params 的**普通**步骤不得视为可执行(校验失败 / 编排先 askUser);〔先验产物〕规划可空,不拦确认开干;回头修改可继承原步 params
|
6. 缺必填 params 的**普通**步骤不得视为可执行(校验失败 / 编排先 askUser);〔先验产物〕规划可空,不拦确认开干;重进已完成节点可继承原步 params
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ worker-templates/
|
|||||||
```text
|
```text
|
||||||
选导演(recipes)→ design-flow → 验收近期 设计.创作流程(status=open)
|
选导演(recipes)→ design-flow → 验收近期 设计.创作流程(status=open)
|
||||||
→ 反复 design-step(注入 modules/{id}/prompt.md)
|
→ 反复 design-step(注入 modules/{id}/prompt.md)
|
||||||
→ 不够则再 design-flow(可追加 repeatable 新建,或 mode=revise 回头修改)→ closed
|
→ 不够则再 design-flow(可追加 repeatable 新建)→ closed
|
||||||
→ (可选)opening-generator → 手动进 play
|
→ (可选)opening-generator → 手动进 play
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,8 @@ feeds: design_only
|
|||||||
与用户一起钉清一份「体验契约」:给玩家什么感受、如何参与、正文如何呈现、何时等待用户。
|
与用户一起钉清一份「体验契约」:给玩家什么感受、如何参与、正文如何呈现、何时等待用户。
|
||||||
|
|
||||||
产物必须是 **context-fragment.v1** JSON,写入 tag「设计.美学纲领与交互范式」。
|
产物必须是 **context-fragment.v1** JSON,写入 tag「设计.美学纲领与交互范式」。
|
||||||
业务字段全部放在「正文」内;须含 schema / 技能 / brief;建议填 mount 与稳变(本步不写数字 order)。
|
业务字段全部放在「正文」内(三柱为**对象**,不要写成一段散文);须含 schema / 技能 / brief;建议填 mount 与稳变(本步不写数字 order)。
|
||||||
|
由运行时按模型能力做 schema/tool 投递,不要再口头包一层 outputs 字符串。
|
||||||
|
|
||||||
一次 design-step、一场对话、一次验收。参与方式与美学内容在同一步内交叉追问,不要先交「半份交互」再开「半份美学」。
|
一次 design-step、一场对话、一次验收。参与方式与美学内容在同一步内交叉追问,不要先交「半份交互」再开「半份美学」。
|
||||||
用户已答过追问:把答复写入正文三块,`追问.题目` 优先 `[]`;禁止在「交互范式」与「美学纲领」之间轮流补洞拖验收。核心体验与权限边界够用即可收。
|
用户已答过追问:把答复写入正文三块,`追问.题目` 优先 `[]`;禁止在「交互范式」与「美学纲领」之间轮流补洞拖验收。核心体验与权限边界够用即可收。
|
||||||
@@ -193,17 +194,18 @@ feeds: design_only
|
|||||||
|
|
||||||
硬规则(公共三段 + 本技正文):
|
硬规则(公共三段 + 本技正文):
|
||||||
1. 合法 JSON;必含 `schema` / `技能` / `brief` / `正文` / `自评` / `追问`(题目可 `[]`)。
|
1. 合法 JSON;必含 `schema` / `技能` / `brief` / `正文` / `自评` / `追问`(题目可 `[]`)。
|
||||||
2. **公共三段**:`正文`=产物主体;`自评`=完备度打分(**分数 0–10 十分制,禁止百分数**);`追问`=导语+题目(建议选项/示例)。禁止用散文代替这三段。
|
2. **公共三段**:`正文`=产物主体;`自评`=打分(**分数 0–10 十分制,禁止百分数**);`追问`=导语+题目(建议选项/示例)。禁止用散文代替这三段。
|
||||||
3. **本技正文三块**:`设定逻辑`(参与/内容维度/区域/复杂性/完备度)+ `交互范式` + `美学纲领`。未知省略或「未定」,禁止编造百科。
|
3. **正文深度**:三柱(设定逻辑 / 交互范式 / 美学纲领)为对象。柱下到「条目」为止:条目是短字段、string[],或诊断三元组 `{结论, 依据, 完备度}` / `{已知, 待探, 完备度}`。诊断里的结论/依据/已知/待探必须是字符串。禁止把整柱写成一段 text;禁止在诊断或短字段里再套对象。
|
||||||
4. `mount` 建议挂主世界层+转述;**禁止**写数字 `order`。`稳变` 定稿后多为 `stable`。
|
4. **本技三柱**:`设定逻辑`(变造 + 参与方式 + 内容维度 + 区域/复杂性/完备度)+ `交互范式`(前置配置/视角/权限/后果)+ `美学纲领`(体验内核 + 呈现要点)。未知省略或「未定」,禁止编造百科。
|
||||||
5. `追问.题目` 一次宜 1~3 题;有建议选项与示例;允许「其它」。已答清的不要再问。题目只写在「追问」,askUser 为 null。
|
5. `mount` 建议挂主世界层+转述;**禁止**写数字 `order`。`稳变` 定稿后多为 `stable`。
|
||||||
6. summary:`美学纲领与交互范式 · {brief 缩略}`。
|
6. `追问.题目` 一次宜 1~3 题;有建议选项与示例;允许「其它」。已答清的不要再问。题目只写在「追问」,askUser 为 null。
|
||||||
|
7. summary:`美学纲领与交互范式 · {brief 缩略}`。
|
||||||
|
|
||||||
## checklist
|
## checklist
|
||||||
|
|
||||||
```checklist
|
```checklist
|
||||||
- [ ] 含 schema / brief / 正文 / 自评 / 追问 三段外壳?
|
- [ ] 含 schema / brief / 正文 / 自评 / 追问 三段外壳?
|
||||||
- [ ] 正文含设定逻辑 + 交互范式 + 美学纲领?自评有交互/美学/协调维度?
|
- [ ] 正文三柱是对象(非一段散文)?设定逻辑里诊断有结论/依据/完备度?自评有交互/美学/协调维度?
|
||||||
- [ ] 未写 order、未输出执行单元列表?
|
- [ ] 未写 order、未输出执行单元列表?
|
||||||
- [ ] brief 能点题核心体验?变造/代入/满足来源清楚或已标未定?
|
- [ ] brief 能点题核心体验?变造/代入/满足来源清楚或已标未定?
|
||||||
- [ ] 交互范式权限与美学纲领互相保护、不矛盾?
|
- [ ] 交互范式权限与美学纲领互相保护、不矛盾?
|
||||||
@@ -215,11 +217,12 @@ feeds: design_only
|
|||||||
|
|
||||||
```examples
|
```examples
|
||||||
好:
|
好:
|
||||||
- brief 点题「垄断生存资源的绝对掌控感」;正文三块齐(设定逻辑含完备度/待探、交互范式锁权限、美学纲领写体验内核);自评打分;追问带建议选项打薄弱点。
|
- brief 点题「垄断生存资源的绝对掌控感」;正文三柱为对象(设定逻辑含诊断三元组、交互范式锁权限、美学纲领写体验内核);自评打分;追问带建议选项打薄弱点。
|
||||||
- 内容质感可参考「权力孤岛」类长描写,但必须落在上述 JSON 键里,不要输出 XML/自定义标签壳。
|
- 内容质感可参考「权力孤岛」类长描写,但必须落在上述 JSON 键里,不要输出 XML/自定义标签壳,也不要把三柱收成一段 text。
|
||||||
|
|
||||||
坏:
|
坏:
|
||||||
- 只有散文纲领,无 JSON / 无自评 / 无追问结构
|
- 只有散文纲领,无 JSON / 无自评 / 无追问结构
|
||||||
|
- 三柱是 sections[].text 一段话,验收卡无法切开诊断与契约
|
||||||
- 百科式背景堆砌,无满足来源与权限边界
|
- 百科式背景堆砌,无满足来源与权限边界
|
||||||
- 输出 workers[] 或 order 表
|
- 输出 workers[] 或 order 表
|
||||||
- 追问空洞「你想要什么体验?」无选项
|
- 追问空洞「你想要什么体验?」无选项
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
# declaration = catalog 短声明(可被 prompt.md ```meta 覆盖)
|
# declaration = catalog 短声明(可被 prompt.md ```meta 覆盖)
|
||||||
# artifact = 执行期产物 tag(程序映射;不写进流程 JSON)
|
# artifact = 执行期产物 tag(程序映射;不写进流程 JSON)
|
||||||
# repeatable = true 时允许同能力多次**新建**编入增量 DAG(如生成规则、具体实例)。
|
# repeatable = true 时允许同能力多次**新建**编入增量 DAG(如生成规则、具体实例)。
|
||||||
# 回头修改已完成节点不是 repeatable:DAG 用 mode=revise + revises,继承旧产物。
|
# 回头修改已完成节点不是 repeatable:用户在图上点该节点重进,不要排 mode=revise。
|
||||||
# closer = true 时为创作终节点:选定后程序落库、关 DAG、保存定稿
|
# closer = true 时为创作终节点:选定后程序落库、关 DAG、保存定稿
|
||||||
# auto = true 时为程序步:不抛默认问题、不经「同意并开始」,提案后直接执行(产物仍验收)
|
# auto = true 时为程序步:不抛默认问题、不经「同意并开始」,提案后直接执行(产物仍验收)
|
||||||
# kind = 节点特性。缺省=普通执行步。prior-artifact=先验产物(须先定写什么;params 是规划产物,不拦执行)
|
# kind = 节点特性。缺省=普通执行步。prior-artifact=先验产物(须先定写什么;params 是规划产物,不拦执行)
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
#
|
#
|
||||||
# 世界模拟器常用能力见下「体验→世界→机制→呈现→收成」;编排按需选用,勿默认全选。
|
# 世界模拟器常用能力见下「体验→世界→机制→呈现→收成」;编排按需选用,勿默认全选。
|
||||||
# 流程是可变增量 DAG:勿一次排完全程;可反复调用标了 repeatable 的能力(每次=彻底新建)。
|
# 流程是可变增量 DAG:勿一次排完全程;可反复调用标了 repeatable 的能力(每次=彻底新建)。
|
||||||
# 已完成节点要改:编排 mode=revise(继承旧稿),不要再排一条 fresh。
|
# 已完成节点要改:用户在图上点该节点重进(继承旧稿),不要再排一条 fresh 或 mode=revise。
|
||||||
# 有编排参数的普通能力:DAG 步骤必须是带 params 的具体调用,禁止空壳进 design-step。
|
# 有编排参数的普通能力:DAG 步骤必须是带 params 的具体调用,禁止空壳进 design-step。
|
||||||
# 〔先验产物〕例外:params 是规划产物,可空;「写什么」在步内钉。
|
# 〔先验产物〕例外:params 是规划产物,可空;「写什么」在步内钉。
|
||||||
|
|
||||||
@@ -77,7 +77,7 @@ modules:
|
|||||||
hint: 本批要锚定什么(如开局重要NPC)
|
hint: 本批要锚定什么(如开局重要NPC)
|
||||||
- key: count
|
- key: count
|
||||||
label: 数量
|
label: 数量
|
||||||
hint: 数字或「按规则」;contextual 时写清规模依据
|
hint: 同批时本批条数;规则「一次生成」为单条时忽略(本步只交 1 条)
|
||||||
|
|
||||||
- id: narrative
|
- id: narrative
|
||||||
name: 叙事指南与故事推进
|
name: 叙事指南与故事推进
|
||||||
|
|||||||
@@ -28,10 +28,10 @@ feeds: gm
|
|||||||
|
|
||||||
```opening
|
```opening
|
||||||
先认定本批按哪条规则生成:
|
先认定本批按哪条规则生成:
|
||||||
- 若【规划产物】已有 rule_id,以此为起点;要换规则就在本步改(须仍是已验收的预生成规则)。
|
- 若【本步对象】已有 rule_id,以此为起点;要换规则就在本步改(须仍是已验收的预生成规则)。
|
||||||
- 若规划为空,从已验收生成规则里选出一条预生成规则。
|
- 若规划为空,从已验收生成规则里选出一条预生成规则。
|
||||||
|
|
||||||
有批次目标或数量偏好可一句带过;否则直接「按规则生成」。
|
读规则「一次生成」:同批则本步一次交齐互有关系的多条;单条则本步只交 1 条。有批次目标可一句带过。
|
||||||
```
|
```
|
||||||
|
|
||||||
## task
|
## task
|
||||||
@@ -41,12 +41,15 @@ feeds: gm
|
|||||||
|
|
||||||
本步 = 规则执行器。禁止重议对象族、生命周期、schema 或描写方法。
|
本步 = 规则执行器。禁止重议对象族、生命周期、schema 或描写方法。
|
||||||
|
|
||||||
本步是〔先验产物〕:先定调用哪条规则,再生成 records。读【规划产物】当起点;规划可空。
|
本步是〔先验产物〕:先定调用哪条规则,再生成 records。读【本步对象】与【原型建议】;在本步钉「写什么」。
|
||||||
|
|
||||||
执行顺序:
|
执行顺序:
|
||||||
1. 认定 `rule_id`:规划有则采用(用户要换则以最新为准,须仍是已验收的预生成规则);空则先从依赖产物里选出。不要停下来让编排器补参。非预生成生命周期或缺信息 → 停止并指出缺口。
|
1. 认定 `rule_id`:规划有则采用(用户要换则以最新为准,须仍是已验收的预生成规则);空则先从依赖产物里选出。不要停下来让编排器补参。非预生成生命周期或缺信息 → 停止并指出缺口。
|
||||||
2. 数量:规划 `count` 有数字用数字,否则用规则「数量」;`batch_goal` 只作不违反规则的筛选。
|
2. 条数:先读规则「一次生成」。
|
||||||
3. 完全按规则的「生成与描写」+「产物格式」+「池」生成 `records`(单层 JSON;键=schema 顶层字段)。
|
- `单条`:本批 `records` 恰好 1 条;规划 `count` 即使写成多条也忽略。
|
||||||
|
- `同批`:一次交齐互有关系的多条。条数:规划 `count` 有数字用数字,否则用规则「数量」。生成时先对齐「一次生成.关系」(各条如何互指、对立、同场),再填各条字段。
|
||||||
|
`batch_goal` 只作不违反规则的筛选。缺「一次生成」时按「数量」/ `count` 交 `records[]`(可多条)。
|
||||||
|
3. 完全按规则的「生成与描写」+「产物格式」+「池」生成 `records`(始终是数组,单条也是长度为 1 的数组;键=schema 顶层字段)。
|
||||||
4. 按规则自检;不合格内部重做。已有产物则追加批次(除非用户要求替换)。
|
4. 按规则自检;不合格内部重做。已有产物则追加批次(除非用户要求替换)。
|
||||||
5. 输出 JSON;追问通常 `[]`。
|
5. 输出 JSON;追问通常 `[]`。
|
||||||
|
|
||||||
@@ -56,15 +59,16 @@ summary:`具体实例 · {对象} · {n}条`
|
|||||||
## principles
|
## principles
|
||||||
|
|
||||||
```principles
|
```principles
|
||||||
1. 只按规则来:方法、格式、池、数量、约束——规则有什么用什么;没有的不发明。
|
1. 只按规则来:方法、格式、池、数量、一次生成、约束——规则有什么用什么;没有的不发明。
|
||||||
2. 只交 records:单层 JSON;不交解释、草稿、schema 副本、整池粘贴。
|
2. 同批先钉记录间关系再填各条;单条不虚构同场关系。始终交 `records[]`。
|
||||||
3. 要改合同 → 回「生成规则」,本步不加字段、不改枚举。
|
3. 只交 records:单层 JSON;不交解释、草稿、schema 副本、整池粘贴。
|
||||||
|
4. 要改合同 → 回「生成规则」,本步不加字段、不改枚举。
|
||||||
```
|
```
|
||||||
|
|
||||||
## probe
|
## probe
|
||||||
|
|
||||||
```probe
|
```probe
|
||||||
通常不追问。仅当 contextual 数量算不出时问一句规模依据。
|
通常不追问。仅当 contextual 数量算不出、或同批「关系」写了但本批对象对不齐时,问一句。
|
||||||
用户要 schema 外内容 → 提示回「生成规则」。
|
用户要 schema 外内容 → 提示回「生成规则」。
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -110,7 +114,7 @@ summary:`具体实例 · {对象} · {n}条`
|
|||||||
硬规则:
|
硬规则:
|
||||||
1. 合法 JSON;含 `schema` / `技能` / `brief` / `正文` / `自评` / `追问`。
|
1. 合法 JSON;含 `schema` / `技能` / `brief` / `正文` / `自评` / `追问`。
|
||||||
2. 正文键固定:`rule_id` / `本批数量` / `批次条件` / `batch_id` / `records` / `增量说明`。
|
2. 正文键固定:`rule_id` / `本批数量` / `批次条件` / `batch_id` / `records` / `增量说明`。
|
||||||
3. `records` 每条必须是规则约定的单层 JSON;`本批数量 === records.length`。
|
3. `records` 始终为数组;每条必须是规则约定的单层 JSON;`本批数量 === records.length`。规则「一次生成」为单条时长度为 1;为同批时 ≥2 且关系对齐。
|
||||||
4. 自评仅「合规模」一维。禁止改规则、写 `order`、输出执行单元列表。
|
4. 自评仅「合规模」一维。禁止改规则、写 `order`、输出执行单元列表。
|
||||||
5. summary:`具体实例 · {对象} · {n}条`。
|
5. summary:`具体实例 · {对象} · {n}条`。
|
||||||
|
|
||||||
@@ -118,6 +122,7 @@ summary:`具体实例 · {对象} · {n}条`
|
|||||||
|
|
||||||
```checklist
|
```checklist
|
||||||
- [ ] 按本步认定的 rule_id(规划或步内钉)的预生成规则执行?
|
- [ ] 按本步认定的 rule_id(规划或步内钉)的预生成规则执行?
|
||||||
|
- [ ] 一次生成已遵守(单条=1 条;同批=多条且关系对齐)?records 始终是数组?
|
||||||
- [ ] records 单层、键值符合产物格式与全部约束?
|
- [ ] records 单层、键值符合产物格式与全部约束?
|
||||||
- [ ] 自评为合规模?未改合同?
|
- [ ] 自评为合规模?未改合同?
|
||||||
```
|
```
|
||||||
@@ -125,6 +130,8 @@ summary:`具体实例 · {对象} · {n}条`
|
|||||||
## examples
|
## examples
|
||||||
|
|
||||||
```examples
|
```examples
|
||||||
好:规则要 3 条怪物 → 交出 3 条单层 record,字段与池用法全服从规则。
|
好:规则一次生成=同批、关系=开局仇敌二人 → 本批 2 条,对立在两条里对齐。
|
||||||
|
好:规则一次生成=单条 → 本批 1 条;还要更多就再点原型。
|
||||||
|
坏:同批规则却只交 1 条,或分多步把有关系的人拆开写。
|
||||||
坏:加规则没有的字段;输出一段「我是怎么想的」却没有 records;本步改 schema。
|
坏:加规则没有的字段;输出一段「我是怎么想的」却没有 records;本步改 schema。
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -37,13 +37,14 @@ feeds: gm,auditor
|
|||||||
|
|
||||||
```opening
|
```opening
|
||||||
先认定本轮规则写哪一类内容:
|
先认定本轮规则写哪一类内容:
|
||||||
- 若【规划产物】已有对象,以此为起点;要改对象就在本步改。
|
- 若【本步对象】/【原型建议】已有对象提示,以此为起点;要改对象就在本步改。
|
||||||
- 若规划为空,先钉对象族,再谈规则。
|
- 若规划为空,先钉对象族,再谈规则。
|
||||||
|
|
||||||
然后谈:
|
然后谈:
|
||||||
1. 若不做专门规则、只靠世界设定临场写,最怕出现什么不可接受的结果?
|
1. 若不做专门规则、只靠世界设定临场写,最怕出现什么不可接受的结果?
|
||||||
2. 更需要:开局先钉一批、游玩中再随机补,还是只在游玩期动态生成?
|
2. 更需要:开局先钉一批、游玩中再随机补,还是只在游玩期动态生成?
|
||||||
3. 哪些产物字段需要「池」?(池挂在 JSON 字段上,如气质标签池→`气质`;可多个;没有就说没有)
|
3. 这类实例彼此有没有必须同场钉死的关系(如互为亲属、对立、同场搭档)?有 → 一次同批生成;彼此独立 → 一次一条。
|
||||||
|
4. 哪些产物字段需要「池」?(池挂在 JSON 字段上,如气质标签池→`气质`;可多个;没有就说没有)
|
||||||
|
|
||||||
想到什么写什么;完整规则在正式产物里钉。
|
想到什么写什么;完整规则在正式产物里钉。
|
||||||
```
|
```
|
||||||
@@ -59,7 +60,7 @@ feeds: gm,auditor
|
|||||||
3. **池**(可选):每个池挂在产物格式里的某个字段(变量)上,供该字段抽取/组装/枚举;
|
3. **池**(可选):每个池挂在产物格式里的某个字段(变量)上,供该字段抽取/组装/枚举;
|
||||||
一条规则可多个池;无则 `池: []`。
|
一条规则可多个池;无则 `池: []`。
|
||||||
|
|
||||||
本步是〔先验产物〕:先定本轮对象族,再写规则。读【规划产物】当起点;规划可空。禁止顺手扩到其它族。
|
本步是〔先验产物〕:先定本轮对象族,再写规则。读【本步对象】与【原型建议】;在本步钉「写什么」。禁止顺手扩到其它族。
|
||||||
|
|
||||||
执行顺序:
|
执行顺序:
|
||||||
1. 认定对象族:规划有 `target` 则采用(用户要改则以最新为准);规划为空则先钉对象。不要停下来让编排器补参。
|
1. 认定对象族:规划有 `target` 则采用(用户要改则以最新为准);规划为空则先钉对象。不要停下来让编排器补参。
|
||||||
@@ -68,14 +69,17 @@ feeds: gm,auditor
|
|||||||
- `runtime_only`:仅游玩期动态生成;不要求「具体实例」。
|
- `runtime_only`:仅游玩期动态生成;不要求「具体实例」。
|
||||||
- `seed_only`:由「具体实例」预生成;规则用完可丢。
|
- `seed_only`:由「具体实例」预生成;规则用完可丢。
|
||||||
- `seed_and_runtime`:先种子实例;规则与实例都保留,游玩期仍可按同规则生成。
|
- `seed_and_runtime`:先种子实例;规则与实例都保留,游玩期仍可按同规则生成。
|
||||||
4. 写满规则三块:生成与描写(可较长)+ 产物格式(**单层 JSON**:每条产物是一层键值,无嵌套对象)+ 池(需要才填;绑顶层字段)。
|
4. 钉「数量」与「一次生成」(两件不同的事):
|
||||||
5. 逐字段过筛:每个属性都要能回答「对本对象族是否必要」。冗余字段删掉(如女主规则里的「性别」);对象族必需字段不得漏(如 D&D 怪物的等级、攻击骰)。
|
- **数量**:这类对象总共要有几条(fixed / range / contextual)。
|
||||||
6. `rule_id`:规划已给则用;否则稳定 kebab-case。已有产物按 rule_id 增量。
|
- **一次生成**:点一次「具体实例」交几条。彼此有必须同场对齐的关系 → `同批`(一次交出并对齐关系);彼此独立 → `单条`(一次只交 1 条,要更多就再点原型)。产物形状始终是 `records[]`,不因单条改成单对象。
|
||||||
|
5. 写满规则三块:生成与描写(可较长)+ 产物格式(**单层 JSON**:每条产物是一层键值,无嵌套对象)+ 池(需要才填;绑顶层字段)。
|
||||||
|
6. 逐字段过筛:每个属性都要能回答「对本对象族是否必要」。冗余字段删掉(如女主规则里的「性别」);对象族必需字段不得漏(如 D&D 怪物的等级、攻击骰)。
|
||||||
|
7. `rule_id`:规划已给则用;否则稳定 kebab-case。已有产物按 rule_id 增量。
|
||||||
若本步对象与规划不一致,以最新对象为准并重钉对应英文 rule_id,禁止沿用旧 id。
|
若本步对象与规划不一致,以最新对象为准并重钉对应英文 rule_id,禁止沿用旧 id。
|
||||||
7. 按自评三维打分并填追问;输出 JSON。禁止夹带最终实例 records。
|
8. 按自评三维打分并填追问;输出 JSON。禁止夹带最终实例 records。
|
||||||
|
|
||||||
若程序已发默认问题:禁止重复开场;在首答上补洞。
|
若程序已发默认问题:禁止重复开场;在首答上补洞。
|
||||||
summary:建立 → `生成规则 · {对象} · {仅动态|仅预生成|预生成并动态}`;跳过 → `生成规则 · {对象} · 无需专门规则`。
|
summary:建立 → `生成规则 · {对象} · {仅动态|仅预生成|预生成并动态} · {单条|同批}`;跳过 → `生成规则 · {对象} · 无需专门规则`。
|
||||||
```
|
```
|
||||||
|
|
||||||
## principles
|
## principles
|
||||||
@@ -90,7 +94,8 @@ summary:建立 → `生成规则 · {对象} · {仅动态|仅预生成|预生
|
|||||||
类型/必填/枚举/上下界必须可校验;禁止只给示例让下游猜类型。
|
类型/必填/枚举/上下界必须可校验;禁止只给示例让下游猜类型。
|
||||||
6. 池绑顶层字段名(如 `气质`、`攻击花招`);一条规则可多池。池条目是该字段材料,不是整条 record。
|
6. 池绑顶层字段名(如 `气质`、`攻击花招`);一条规则可多池。池条目是该字段材料,不是整条 record。
|
||||||
7. 生命周期先行;未定前不假装可执行「具体实例」。
|
7. 生命周期先行;未定前不假装可执行「具体实例」。
|
||||||
8. 不写变量更新公式、不写剧情推进、不填最终实例。
|
8. **一次生成**看关系,不看「想不想一次多写」:有必须同场钉死的关系(亲属、对立、同场搭档等)→ `同批`;彼此独立 → `单条`。`同批` 至少 2 条,否则改 `单条`。不要把「总共要几个」(数量)和「这一次交几条」(一次生成)混成一件事。
|
||||||
|
9. 不写变量更新公式、不写剧情推进、不填最终实例。
|
||||||
```
|
```
|
||||||
|
|
||||||
## probe
|
## probe
|
||||||
@@ -98,7 +103,7 @@ summary:建立 → `生成规则 · {对象} · {仅动态|仅预生成|预生
|
|||||||
```probe
|
```probe
|
||||||
每轮 1~2 点。规划已钉且用户未改对象时,禁止重问 target / rule_id。规划为空或用户要换对象时,先问本轮生成什么。
|
每轮 1~2 点。规划已钉且用户未改对象时,禁止重问 target / rule_id。规划为空或用户要换对象时,先问本轮生成什么。
|
||||||
|
|
||||||
优先:整条规则是否必要;某字段该不该进表(冗余 vs 漏接口);单层格式类型/枚举/边界;哪些字段要池。
|
优先:整条规则是否必要;实例彼此有无必须同场对齐的关系(一次生成:同批 vs 单条);某字段该不该进表(冗余 vs 漏接口);单层格式类型/枚举/边界;哪些字段要池。
|
||||||
若双门槛不成立 → 建议跳过,停追问字段与格式。
|
若双门槛不成立 → 建议跳过,停追问字段与格式。
|
||||||
不追问:最终实例正文、变量增减公式、其它对象族、完整世界百科。
|
不追问:最终实例正文、变量增减公式、其它对象族、完整世界百科。
|
||||||
```
|
```
|
||||||
@@ -141,6 +146,10 @@ summary:建立 → `生成规则 · {对象} · {仅动态|仅预生成|预生
|
|||||||
"最大": 1,
|
"最大": 1,
|
||||||
"决定规则": "contextual 时必填,可较长"
|
"决定规则": "contextual 时必填,可较长"
|
||||||
},
|
},
|
||||||
|
"一次生成": {
|
||||||
|
"模式": "单条|同批",
|
||||||
|
"关系": "同批必填:本批记录之间必须一起钉的联系;单条写「彼此独立」"
|
||||||
|
},
|
||||||
"生成与描写": {
|
"生成与描写": {
|
||||||
"依据": ["可较长;引用上游产物或用户要求"],
|
"依据": ["可较长;引用上游产物或用户要求"],
|
||||||
"方法": ["可较长:生成顺序、如何组装、描写口径、语气/切面、如何用池"],
|
"方法": ["可较长:生成顺序、如何组装、描写口径、语气/切面、如何用池"],
|
||||||
@@ -236,7 +245,7 @@ summary:建立 → `生成规则 · {对象} · {仅动态|仅预生成|预生
|
|||||||
1. 合法 JSON;必含 `schema` / `技能` / `brief` / `正文` / `自评` / `追问`(题目可 `[]`)。
|
1. 合法 JSON;必含 `schema` / `技能` / `brief` / `正文` / `自评` / `追问`(题目可 `[]`)。
|
||||||
2. **公共三段**:正文=规则主体;自评=必要性/属性妥当/格式准确(**分数 0–10 十分制**);追问=导语+建议选项。
|
2. **公共三段**:正文=规则主体;自评=必要性/属性妥当/格式准确(**分数 0–10 十分制**);追问=导语+建议选项。
|
||||||
3. **正文键固定**:本步参数、必要性判断、rules、增量说明。
|
3. **正文键固定**:本步参数、必要性判断、rules、增量说明。
|
||||||
4. **每条 rule 必含**:生成与描写、产物格式;**池可空数组**。有池时 `绑定字段` 必须是单层 schema 的顶层键。
|
4. **每条 rule 必含**:生成与描写、产物格式、数量、一次生成;**池可空数组**。有池时 `绑定字段` 必须是单层 schema 的顶层键。`一次生成.模式` 为同批时 `关系` 必填且本批至少 2 条;为单条时一次只交 1 条。
|
||||||
5. **产物格式强制单层**:`schema` 的 `type` 不得为 `object`;`array` 的 `items` 不得为 object。禁止 `外貌.发色` 这类嵌套路径。
|
5. **产物格式强制单层**:`schema` 的 `type` 不得为 `object`;`array` 的 `items` 不得为 object。禁止 `外貌.发色` 这类嵌套路径。
|
||||||
6. 结论为「无需专门规则」时:`rules` 必须 `[]`。上下文策略布尔须与生命周期一致。
|
6. 结论为「无需专门规则」时:`rules` 必须 `[]`。上下文策略布尔须与生命周期一致。
|
||||||
7. 池条目≠最终实例;禁止输出具体实例 `records`、变量更新规则、执行单元列表。
|
7. 池条目≠最终实例;禁止输出具体实例 `records`、变量更新规则、执行单元列表。
|
||||||
@@ -248,7 +257,7 @@ summary:建立 → `生成规则 · {对象} · {仅动态|仅预生成|预生
|
|||||||
- [ ] 含 schema / brief / 正文 / 自评 / 追问?
|
- [ ] 含 schema / brief / 正文 / 自评 / 追问?
|
||||||
- [ ] 自评为必要性 / 属性妥当 / 格式准确?
|
- [ ] 自评为必要性 / 属性妥当 / 格式准确?
|
||||||
- [ ] 整条规则必要性有举证?不成立时 rules=[]?
|
- [ ] 整条规则必要性有举证?不成立时 rules=[]?
|
||||||
- [ ] 建立时:逐字段无冗余无漏接口?产物为单层 JSON 且类型可校验?
|
- [ ] 建立时:逐字段无冗余无漏接口?产物为单层 JSON 且类型可校验?数量与一次生成已分清(总共几个 vs 这一次交几条)?同批有关系、单条写彼此独立?
|
||||||
- [ ] 池:无则 [];有则绑顶层字段、未写成最终实例表?
|
- [ ] 池:无则 [];有则绑顶层字段、未写成最终实例表?
|
||||||
- [ ] 未夹带最终实例、变量公式、其它对象族、百科?
|
- [ ] 未夹带最终实例、变量公式、其它对象族、百科?
|
||||||
```
|
```
|
||||||
@@ -259,11 +268,15 @@ summary:建立 → `生成规则 · {对象} · {仅动态|仅预生成|预生
|
|||||||
好:
|
好:
|
||||||
- D&D 怪物:单层含 名称/等级/攻击骰/……;池可绑 `气质`;自评属性妥当高(未漏战斗接口)。
|
- D&D 怪物:单层含 名称/等级/攻击骰/……;池可绑 `气质`;自评属性妥当高(未漏战斗接口)。
|
||||||
- 女主规则:不设「性别」字段;关系切面进表;池可空。
|
- 女主规则:不设「性别」字段;关系切面进表;池可空。
|
||||||
|
- 互为仇敌的两名开局 NPC:一次生成=同批,关系写清对立如何对齐;数量=2。
|
||||||
|
- 可互换的酒馆路人:一次生成=单条,关系写「彼此独立」;要 5 个就点 5 次。
|
||||||
- 事件钩子:池绑顶层 `钩子`;方法写抽 1~2 条写入该字段。
|
- 事件钩子:池绑顶层 `钩子`;方法写抽 1~2 条写入该字段。
|
||||||
- 普通古董 → 无需专门规则(必要性维度说明跳过理由)。
|
- 普通古董 → 无需专门规则(必要性维度说明跳过理由)。
|
||||||
|
|
||||||
坏:
|
坏:
|
||||||
- 产物写成 `{ "外貌": { "发色": "…" } }` → 非单层,前端表格/投影难对接。
|
- 产物写成 `{ "外貌": { "发色": "…" } }` → 非单层,前端表格/投影难对接。
|
||||||
- 女主规则塞「性别」;或怪物规则只有「名字+性格」没有等级/攻击骰。
|
- 女主规则塞「性别」;或怪物规则只有「名字+性格」没有等级/攻击骰。
|
||||||
|
- 有亲属关系的三人却标成单条,分三次各写一条,关系对不上。
|
||||||
|
- 把「总共要 5 个」写成一次生成=同批,但彼此并无必须同场的关系。
|
||||||
- 把写好的 NPC 整份塞进池当最终实例。
|
- 把写好的 NPC 整份塞进池当最终实例。
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ uiPrompt: |
|
|||||||
|
|
||||||
你只负责读取 `设计.创作流程` DAG,并选择下一个创作节点:
|
你只负责读取 `设计.创作流程` DAG,并选择下一个创作节点:
|
||||||
|
|
||||||
- 没有 DAG 或 DAG 需要追加节点:`design-flow`
|
- 没有 DAG、需要追加节点,或用户要求补前序节点(即使下一步已是收口):`design-flow`
|
||||||
- DAG 已给出下一个可执行节点:`design-step`
|
- DAG 已给出下一个可执行节点、且用户没有要求改排:`design-step`
|
||||||
|
|
||||||
节点如何执行、何时等待、验收、返工、结束和运行状态变化均由 Runtime 与节点自身处理。
|
节点如何执行、何时等待、验收、返工、结束和运行状态变化均由 Runtime 与节点自身处理。
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ id: design-flow
|
|||||||
skill: world-simulator
|
skill: world-simulator
|
||||||
name: 创作 · 流程编排
|
name: 创作 · 流程编排
|
||||||
description: >-
|
description: >-
|
||||||
【编排】以用户已选配方为起点,从能力池排出近期工序与依赖,
|
【编排】以用户已选配方为起点,从能力池排出近期 DAG 骨架(节点 + 依赖 + 原型槽位 + 建议)。
|
||||||
产出/修订可变增量 DAG(设计.创作流程)。本步不写美学/机制正文,不写 Worker 集。
|
不决定执行顺序;用户在图上点选。本步不写美学/机制正文。
|
||||||
version: 1
|
version: 1
|
||||||
stage: design
|
stage: design
|
||||||
inputTags:
|
inputTags:
|
||||||
@@ -40,70 +40,101 @@ contextSegments:
|
|||||||
|
|
||||||
# 创作 · 流程编排
|
# 创作 · 流程编排
|
||||||
|
|
||||||
你只做一件事:根据【用户要求】编排或**增量修订**一份剧本流程(可变 DAG)。要求按先后都有效,冲突时以较新的为准。
|
你只做一件事:根据【用户要求】编排或**增量修订**一份 **DAG 骨架**。要求按先后都有效,冲突时以较新的为准。
|
||||||
|
|
||||||
|
**你不决定**用户下一步先跑哪个——用户在分层图上点选。你只产出:有哪些节点、depends_on、原型槽位(repeatable 能力)、以及建议。
|
||||||
|
|
||||||
上下文里会有:
|
上下文里会有:
|
||||||
|
|
||||||
1. **【流程进度】**(程序钉死):已完成哪些步、草案里已有哪些步、哪些能力才允许再编入。必须先读完再排。
|
1. **【流程进度】**(程序钉死):已完成哪些步、草案里已有哪些步、原型槽位。必须先读完再排。
|
||||||
2. **【用户已选配方】**:该方法的适用、核心思路、设计流程与原则 + 近期起点 steps——**不是**锁死流水线;**禁止**替用户改选其它配方
|
2. **【用户已选配方】**:方法论 + 近期起点——**不是**锁死流水线
|
||||||
3. **【能力 · 可选工序】**:固定中文名 + 声明 + **何时用 / 何时不用 / 边界**(来自各能力 meta)+(若有)编排参数——步骤只能从这里选;标〔可反复〕的可多次**新建**编入;已完成节点要改用回头修改(mode=revise)
|
3. **【能力 · 可选工序】**:步骤只能从这里选
|
||||||
4. **【已有剧本草案】/【已验收步骤】**:若有,在其上追加或改未验收步,**不要**推倒重来
|
4. **【已有剧本草案】/【已验收步骤】**:在其上追加,**不要**推倒重来
|
||||||
|
|
||||||
开局说明:程序可能已按配方 `steps` 写入近期起点(用户已坐在 DAG 第一步,例如「美学纲领与交互范式」)。此时**不要**把开局步再规划一遍;仅追加缺口。listed 步已齐仍需扩写时,保留已完成 id,只加新步。
|
|
||||||
|
|
||||||
## 增量 DAG(核心)
|
## 增量 DAG(核心)
|
||||||
|
|
||||||
**禁止**一次排完全程固定长链。每次只排出**近期要做**的步骤(通常 1~4 步),`status` 默认 `"open"`。
|
**禁止**一次排完全程固定长链。每次只排出**近期 horizon**(通常 1~4 个节点 + 若干原型),`status` 默认 `"open"`。
|
||||||
|
|
||||||
选型规则:
|
### 节点类型
|
||||||
|
|
||||||
- **跟配方方法论**:用配方的 core / process / principles 理解整局怎么设计、何时收成。
|
| 类型 | JSON | 谁跑 |
|
||||||
- **跟能力 meta**:某步该不该排,看能力的「何时用 / 何时不用 / 边界」;**不要**在配方里找各能力调用条件,也不要凭题材默认全选。
|
|------|------|------|
|
||||||
- 〔先验产物〕(生成规则、具体实例):可提前规划「写什么」写入 params;执行时程序注入【规划产物】。规划可空——对象在步内钉。提前规划多条(不同对象/批次)是正确的。**禁止**为钉「生成什么 / 调用哪个规则」而 askUser 拦在确认开干前。
|
| 普通一步 | 缺省(无 role) | 用户点上直接 design-step |
|
||||||
- 其它有「编排参数」的能力:写入 DAG 时可先留空壳;**确认开干前**必须钉齐必填 params。
|
| **原型槽位** | `"role": "prototype"` | 用户点上 → 程序增殖 instance → design-step |
|
||||||
- 用户先认可**下一节点**,再进入执行;用户提出新要求时,再调本 worker 修订未验收步或追加新步。
|
| **实例** | `"role": "instance"`, `"from": "<原型 id>"` | 仅程序增殖,**禁止**你在编排层预排 |
|
||||||
|
|
||||||
同能力**可以**多次出现。必须分清两种:
|
**禁止**排 `mode=revise` / 回头修改节点。已完成节点要改:用户在图上点该节点重进,不在 DAG 里加修订步。
|
||||||
|
|
||||||
- **彻底新建**(〔可反复〕如生成规则、具体实例):`mode` 缺省或 `"fresh"`,新 id + 新 params。每一次都是新对象/新批次,**不要**改旧条。id 应尽量带上调用目标,如 `生成规则·怪物`,避免无信息的 `#2`。
|
### 〔可反复〕= 原型,不是多条 instance
|
||||||
- **回头修改**:用户要改**已经做过**的节点(漏了、谈浅了、后来发现要改)→ 追加新 id,`"mode": "revise"`,`"revises": "<原步 id>"`。程序会注入既有产物,在旧稿上改。非反复技能**禁止**再排一条 fresh;只能 revise。插在最近已验收步之后、尚未执行的步之前。
|
|
||||||
|
生成规则、具体实例等 catalog 标〔可反复〕的能力:
|
||||||
|
|
||||||
|
- 在 `steps` 里写 **`role: "prototype"`** 一条即可(id 通常与 name 相同,如 `"生成规则"`)
|
||||||
|
- 写 **`suggestion`**:建议这条原型将来生成什么(短句,给用户和实例步参考)
|
||||||
|
- **禁止**在原型上写 `params`
|
||||||
|
- **禁止**预排 `role: "instance"` 或 `生成规则#2` 这类实例——用户在图上点原型,程序增殖
|
||||||
|
|
||||||
|
### `suggestions`(流程级)
|
||||||
|
|
||||||
|
除 `steps` 里的原型外,可在顶层写 **`suggestions`**:建议用户**还可以**追加哪些原型(尚未编入 steps 时展示在图上)。
|
||||||
|
|
||||||
|
```json
|
||||||
|
"suggestions": [
|
||||||
|
{
|
||||||
|
"name": "生成规则",
|
||||||
|
"suggestion": "怪物种群与 NPC 两套规则,各开一条",
|
||||||
|
"depends_on": ["美学纲领与交互范式"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
若已把该能力编入 `steps` 为 prototype,不必在 suggestions 重复。
|
||||||
|
|
||||||
|
### 〔先验产物〕
|
||||||
|
|
||||||
|
生成规则、具体实例:**「写什么」在实例步内钉**,不在编排层 params。原型只带 `suggestion`。
|
||||||
|
|
||||||
|
### 选型规则
|
||||||
|
|
||||||
|
- 跟配方 core / process / principles
|
||||||
|
- 跟能力 meta 的何时用/不用
|
||||||
|
- 用户要补节点 → 插在未验收步之前、收口之前,`status: "open"`
|
||||||
|
|
||||||
## 产出(唯一)
|
## 产出(唯一)
|
||||||
|
|
||||||
写入 tag `设计.创作流程`,必须是 JSON 对象,形状:
|
写入 tag `设计.创作流程`。根对象就是下面这份 JSON(必须含 `steps`),不要改写成 `artifact.flat.v1` 的 sections。
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"brief": "一句话复述用户要的体验(可选)",
|
"brief": "一句话体验(可选)",
|
||||||
"status": "open",
|
"status": "open",
|
||||||
|
"suggestions": [
|
||||||
|
{
|
||||||
|
"name": "具体实例",
|
||||||
|
"suggestion": "开局 2~3 个重要 NPC",
|
||||||
|
"depends_on": ["生成规则"]
|
||||||
|
}
|
||||||
|
],
|
||||||
"steps": [
|
"steps": [
|
||||||
{ "id": "美学纲领与交互范式", "name": "美学纲领与交互范式", "depends_on": [] },
|
{ "id": "美学纲领与交互范式", "name": "美学纲领与交互范式", "depends_on": [] },
|
||||||
{
|
{
|
||||||
"id": "生成规则·怪物",
|
"id": "生成规则",
|
||||||
"name": "生成规则",
|
"name": "生成规则",
|
||||||
"params": {
|
"role": "prototype",
|
||||||
"target": "怪物",
|
"suggestion": "先写怪物种群生成规则",
|
||||||
"rule_id": "monsters",
|
|
||||||
"lifecycle_intent": "runtime_only"
|
|
||||||
},
|
|
||||||
"depends_on": ["美学纲领与交互范式"]
|
"depends_on": ["美学纲领与交互范式"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "具体实例·重要NPC·开局",
|
"id": "具体实例",
|
||||||
"name": "具体实例",
|
"name": "具体实例",
|
||||||
"params": {
|
"role": "prototype",
|
||||||
"rule_id": "major-npcs",
|
"suggestion": "锚定 1~2 个开局 NPC",
|
||||||
"batch_goal": "开局重要NPC",
|
"depends_on": ["生成规则"]
|
||||||
"count": "按规则"
|
|
||||||
},
|
|
||||||
"depends_on": ["生成规则·重要NPC"]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "美学纲领与交互范式·改",
|
"id": "开场白与开场变量",
|
||||||
"name": "美学纲领与交互范式",
|
"name": "开场白与开场变量",
|
||||||
"mode": "revise",
|
"depends_on": ["美学纲领与交互范式", "具体实例"]
|
||||||
"revises": "美学纲领与交互范式",
|
|
||||||
"depends_on": ["实现机制"]
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -111,44 +142,20 @@ contextSegments:
|
|||||||
|
|
||||||
规则:
|
规则:
|
||||||
|
|
||||||
1. `steps` 数组顺序 = **建议执行顺序**(依赖须指向更前的步骤)
|
1. `steps` 顺序 = 建议阅读顺序;`depends_on` 须指向更前步骤 id
|
||||||
2. `name` = 能力池里的固定中文名(可重复)
|
2. `name` = 能力池固定中文名
|
||||||
3. `id` = 本局步骤唯一键;同 name 多次时必须不同;新建优先语义化(`生成规则·{对象}`);回头修改用 `{原id}·改`
|
3. `id` = 本局唯一;原型 id 稳定(如 `生成规则`);实例 id 由程序增殖(`生成规则#1`…)
|
||||||
4. `depends_on` = 其它步骤的 **id**(若某 name 在本流程唯一,也可写该 name)
|
4. **禁止**在编排层为 repeatable 预排 instance 或写 params
|
||||||
5. `params` = 本步调用参数(对象)。〔先验产物〕:此处是规划产物(可空;已知对象就填,便于区分多条)。其它能力目录声明了编排参数时,**必填项必须写出**;可选则能推断就写。回头修改可省略,程序会从 `revises` 那步继承
|
5. 已验收 id **必须保留**
|
||||||
6. `status`:`"open"` = 还可能追加;`"closed"` = 不再扩步(可走收成)
|
6. 非反复已完成 → 禁止再排新建,**禁止**排 `mode=revise`
|
||||||
7. 已验收步骤的 id **必须保留**;只能追加新步,或改未验收步的依赖/顺序/params
|
7. 收口「开场白与开场变量」排最后;选定前用户补节点 → 插收口前、`status: "open"`
|
||||||
8. 已完成的**非反复**技能禁止再排一条 `mode=fresh`;要改旧稿只能追加 `mode=revise` + `revises`。〔可反复〕再编入新 id 且 fresh = 彻底新建(新对象/新批次),不是改旧条;改旧条同样用 revise
|
8. askUser 只问排布:补哪个原型、改依赖。**禁止**问体验正文,**禁止**问要不要回头修改
|
||||||
9. 配方建议 steps 只作近期起点;按能力 meta 按需选用,勿默认全选、勿一次排满
|
9. `summary`:`流程 · N 步 · open|closed · …`
|
||||||
10. **禁止**把「交互」与「美学」拆成两步
|
|
||||||
11. **禁止**在本步写能力正文、自由 Worker 列表、表结构
|
|
||||||
12. **禁止**编排「反复发明执行单元」;收成链:确认「游玩拓扑」→(上下文已多时)「上下文投影排序」→「细化终稿」→(可开玩时)「开场白与开场变量」。挂谁可早、排第几须晚,勿让中段技能写死 order
|
|
||||||
13. 「开场白与开场变量」标〔收口〕:排在细化终稿之后作为最后一步;排入后不要再追加其它步,可将 `status` 设为 `closed`。选定开场由程序落库并保存定稿,不必再调 opening-generator
|
|
||||||
14. 参数或选型不够时,用 askUser 问 1~2 点(**优先带 options,并允许其它**)。
|
|
||||||
只许问**排布层**的事:下一步排哪个能力、普通节点的必填 params 取什么值、是新建还是回头修改哪一步。
|
|
||||||
〔先验产物〕的「生成什么 / 调用哪个规则」在步内钉,**不要**为此 askUser。
|
|
||||||
**禁止**问体验偏好、设定细节、口味倾向等能力正文问题——那是 design-step 的活;
|
|
||||||
已验收步骤里问过的题,**禁止**再抛一遍(用户会读成「那一步要重做」)
|
|
||||||
15. 依赖真实产物的步骤(如具体实例依赖某条生成规则)须等上游验收后再编排,并从产物中列出可选项供用户选
|
|
||||||
16. `summary`:`流程 · N 步 · open|closed · …`
|
|
||||||
17. **改对象必须改后台 id**:用户把「生成规则」从女租客改成丧尸怪物时(编排修订或步内改规划),须同步改
|
|
||||||
`params.target`、`params.rule_id`(对应英文 kebab-case,如 `zombies`)、以及步骤 `id`(如 `生成规则·丧尸怪物`)。
|
|
||||||
**禁止**只改中文对象却沿用旧 `rule_id` / 旧步骤 id。
|
|
||||||
这是**改这条规则的对象**(回头修改或改未验收步),与「再来一条新的生成规则」不是一回事。
|
|
||||||
|
|
||||||
## 自检
|
## 自检
|
||||||
|
|
||||||
- 用户是否已选配方?(未选则不要硬编)
|
- 只排了骨架,没有替用户决定顺序?
|
||||||
- 是否只排了近期 horizon,而不是假固定全图?
|
- repeatable 是否都是 `role=prototype` + suggestion,没有 instance/params?
|
||||||
- 是否误排了多次「发明 Worker」?应改为至多一次「游玩拓扑」
|
- suggestions 是否简短、可执行?
|
||||||
- 每步是否按能力 meta 的何时用/不用判断过,而非题材默认?
|
- depends_on 是否合法(具体实例原型依赖生成规则原型 = 至少一条规则实例验收后才可增殖)?
|
||||||
- 每步 name 都在【能力】里?同名多次是否都有不同 id?
|
- 已验收 id 是否都在?
|
||||||
- 有编排参数的普通步骤,必填 params 是否已钉齐?缺参是否应先 askUser 而不是产出空壳?
|
|
||||||
- 〔先验产物〕是否误把「生成什么」当成确认开干前必填?规划可空;已知对象才写入 params
|
|
||||||
- depends_on 是否都指向更靠前的步骤 id?
|
|
||||||
- 已验收 id 是否都还在?
|
|
||||||
- 【流程进度】里已完成的非反复技能,是否误当成 fresh 又排了一次?要改旧稿应 mode=revise
|
|
||||||
- askUser 是否只问了排哪步 / 填哪个参数 / 新建还是回头修改,而不是体验偏好等正文问题?
|
|
||||||
- 需要反复补同类内容时,是否用了新 id + 新 params + fresh 追加(彻底新建),而不是改写旧步?
|
|
||||||
- 用户要改已完成节点时,是否用了 mode=revise + revises,而不是再排一条 fresh?
|
|
||||||
- 收成前是否把 `status` 设为 `closed`?
|
|
||||||
|
|||||||
@@ -42,14 +42,14 @@ contextSegments:
|
|||||||
同能力可多次出现(不同 id):
|
同能力可多次出现(不同 id):
|
||||||
|
|
||||||
- **新建**(mode 缺省 / fresh):只写这一次应增量补上的新内容;可合并既有同 tag 内容,但不改旧条、不假装在做别的步骤。〔可反复〕的「再来一次」属于这种。
|
- **新建**(mode 缺省 / fresh):只写这一次应增量补上的新内容;可合并既有同 tag 内容,但不改旧条、不假装在做别的步骤。〔可反复〕的「再来一次」属于这种。
|
||||||
- **回头修改**(mode=revise):以【既有产物 · 继承修改】为底稿,改用户点名的部分,保留其余;不从零另起,不再走开场询问。
|
- **回头修改**(程序注入【既有产物 · 继承修改】):用户从图上点已完成节点重进。以底稿继续改用户点名的部分,保留其余;不从零另起,不再走开场询问。
|
||||||
- **按意见修改**(验收底栏):以【待改底稿】(最新产物)为底。用户要求按先后都有效,冲突时以较新的为准;未点名推翻的旧条仍须留在产物里。禁止原样交回。
|
- **按意见修改**(验收底栏):以【待改底稿】(最新产物)为底。用户要求按先后都有效,冲突时以较新的为准;未点名推翻的旧条仍须留在产物里。禁止原样交回。
|
||||||
|
|
||||||
## 执行
|
## 执行
|
||||||
|
|
||||||
1. 认定本步要钉的事项:读【本步方法】里 `task` 开头(钉什么、写到哪个 tag)和 `meta.boundary` 的「本技能:」行。
|
1. 认定本步要钉的事项:读【本步方法】里 `task` 开头(钉什么、写到哪个 tag)和 `meta.boundary` 的「本技能:」行。
|
||||||
2. 读【用户要求】与依赖产物。若有默认问题:程序已发出,首答在「用户.worker答复」——在其答复上继续,不重复开场白。
|
2. 读【用户要求】与依赖产物。若有默认问题:程序已发出,首答在「用户.worker答复」——在其答复上继续,不重复开场白。
|
||||||
3. 按方法正文填产物。〔先验产物〕读【规划产物】当起点(可空、可在步内修订);其它节点编排参数见【本步参数】,直接采用。
|
3. 按方法正文填产物。〔先验产物〕读【本步对象】在本步与用户钉「写什么」;可参考【原型建议】。其它节点编排参数见【本步参数】,直接采用。
|
||||||
4. 产物用简洁 JSON 或结构化中文,少写机器变量名。`summary`:`{本步能力名} · …`
|
4. 产物用简洁 JSON 或结构化中文,少写机器变量名。`summary`:`{本步能力名} · …`
|
||||||
|
|
||||||
## 缺料时
|
## 缺料时
|
||||||
@@ -62,4 +62,4 @@ contextSegments:
|
|||||||
- 〔先验产物〕规划为空:先在本步钉「写什么」,再填产物。不要退回编排器补参。
|
- 〔先验产物〕规划为空:先在本步钉「写什么」,再填产物。不要退回编排器补参。
|
||||||
- 普通节点【本步参数】缺必填项或与目录不符:停止产出,提示返回流程编排补参。
|
- 普通节点【本步参数】缺必填项或与目录不符:停止产出,提示返回流程编排补参。
|
||||||
|
|
||||||
需要追加「再来一次」或回头修改其它步时,由编排器再调流程编排。
|
需要追加「再来一次」时,由编排器再调流程编排。已完成节点要改:用户自己在图上点该节点,不要让编排器加修订步。
|
||||||
|
|||||||
+79
-28
@@ -1,6 +1,12 @@
|
|||||||
import { randomUUID } from "node:crypto";
|
import { randomUUID } from "node:crypto";
|
||||||
import { readFileSync, writeFileSync } from "node:fs";
|
import { readFileSync, writeFileSync } from "node:fs";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
import {
|
||||||
|
normalizeCapabilities,
|
||||||
|
probeProfileCapabilities,
|
||||||
|
type ProfileCapabilities,
|
||||||
|
} from "../llm/capabilities.js";
|
||||||
|
import { sanitizeReasoningEffort } from "../llm/generation-compat.js";
|
||||||
import type { LlmConfig } from "./env.js";
|
import type { LlmConfig } from "./env.js";
|
||||||
import { loadLlmConfigOptional } from "./env.js";
|
import { loadLlmConfigOptional } from "./env.js";
|
||||||
import { ensureUserDataDirs, getUserDataDir } from "./user-data-dir.js";
|
import { ensureUserDataDirs, getUserDataDir } from "./user-data-dir.js";
|
||||||
@@ -11,8 +17,15 @@ export type ApiProfile = {
|
|||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
apiKey: string;
|
apiKey: string;
|
||||||
model: string;
|
model: string;
|
||||||
|
/**
|
||||||
|
* 该配置默认思考强度(写入请求的 reasoning_effort)。
|
||||||
|
* 省略 / auto = 不发送;探测永远用 low,不受此项影响。
|
||||||
|
*/
|
||||||
|
reasoningEffort?: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
|
/** 能力探测结果;改 baseUrl/model 后应重测 */
|
||||||
|
capabilities?: ProfileCapabilities;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ApiProfilesFile = {
|
type ApiProfilesFile = {
|
||||||
@@ -45,6 +58,16 @@ function writeFile(data: ApiProfilesFile): void {
|
|||||||
writeFileSync(profilesPath(), JSON.stringify(data, null, 2), "utf8");
|
writeFileSync(profilesPath(), JSON.stringify(data, null, 2), "utf8");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 存盘值:合法档位原样保留;空 / auto / 非法 → undefined(不发送) */
|
||||||
|
export function normalizeProfileReasoningEffort(
|
||||||
|
value: unknown,
|
||||||
|
): string | undefined {
|
||||||
|
if (typeof value !== "string") return undefined;
|
||||||
|
const trimmed = value.trim();
|
||||||
|
if (!trimmed || trimmed.toLowerCase() === "auto") return undefined;
|
||||||
|
return sanitizeReasoningEffort(trimmed);
|
||||||
|
}
|
||||||
|
|
||||||
function seedFromEnvIfEmpty(data: ApiProfilesFile): ApiProfilesFile {
|
function seedFromEnvIfEmpty(data: ApiProfilesFile): ApiProfilesFile {
|
||||||
if (data.profiles.length > 0) return data;
|
if (data.profiles.length > 0) return data;
|
||||||
const env = loadLlmConfigOptional();
|
const env = loadLlmConfigOptional();
|
||||||
@@ -79,15 +102,18 @@ export function createApiProfile(input: {
|
|||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
apiKey: string;
|
apiKey: string;
|
||||||
model: string;
|
model: string;
|
||||||
|
reasoningEffort?: string;
|
||||||
}): ApiProfile {
|
}): ApiProfile {
|
||||||
const data = readFile();
|
const data = readFile();
|
||||||
const now = new Date().toISOString();
|
const now = new Date().toISOString();
|
||||||
|
const reasoningEffort = normalizeProfileReasoningEffort(input.reasoningEffort);
|
||||||
const profile: ApiProfile = {
|
const profile: ApiProfile = {
|
||||||
id: randomUUID(),
|
id: randomUUID(),
|
||||||
name: input.name.trim() || "未命名",
|
name: input.name.trim() || "未命名",
|
||||||
baseUrl: input.baseUrl.trim() || "https://api.deepseek.com",
|
baseUrl: input.baseUrl.trim() || "https://api.deepseek.com",
|
||||||
apiKey: input.apiKey.trim(),
|
apiKey: input.apiKey.trim(),
|
||||||
model: input.model.trim() || "deepseek-v4-pro",
|
model: input.model.trim() || "deepseek-v4-pro",
|
||||||
|
...(reasoningEffort ? { reasoningEffort } : {}),
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
};
|
};
|
||||||
@@ -98,20 +124,52 @@ export function createApiProfile(input: {
|
|||||||
|
|
||||||
export function updateApiProfile(
|
export function updateApiProfile(
|
||||||
id: string,
|
id: string,
|
||||||
input: Partial<Pick<ApiProfile, "name" | "baseUrl" | "apiKey" | "model">>,
|
input: Partial<
|
||||||
|
Pick<ApiProfile, "name" | "baseUrl" | "apiKey" | "model" | "reasoningEffort">
|
||||||
|
>,
|
||||||
): ApiProfile {
|
): ApiProfile {
|
||||||
const data = readFile();
|
const data = readFile();
|
||||||
const index = data.profiles.findIndex((p) => p.id === id);
|
const index = data.profiles.findIndex((p) => p.id === id);
|
||||||
if (index < 0) throw new Error("API 配置不存在");
|
if (index < 0) throw new Error("API 配置不存在");
|
||||||
const current = data.profiles[index];
|
const current = data.profiles[index];
|
||||||
|
const nextBase = input.baseUrl?.trim() || current.baseUrl;
|
||||||
|
const nextModel = input.model?.trim() || current.model;
|
||||||
|
const endpointChanged =
|
||||||
|
nextBase !== current.baseUrl || nextModel !== current.model;
|
||||||
|
const reasoningEffort =
|
||||||
|
input.reasoningEffort !== undefined
|
||||||
|
? normalizeProfileReasoningEffort(input.reasoningEffort)
|
||||||
|
: current.reasoningEffort;
|
||||||
const updated: ApiProfile = {
|
const updated: ApiProfile = {
|
||||||
...current,
|
...current,
|
||||||
name: input.name?.trim() || current.name,
|
name: input.name?.trim() || current.name,
|
||||||
baseUrl: input.baseUrl?.trim() || current.baseUrl,
|
baseUrl: nextBase,
|
||||||
apiKey: input.apiKey !== undefined && input.apiKey.trim() !== ""
|
apiKey: input.apiKey !== undefined && input.apiKey.trim() !== ""
|
||||||
? input.apiKey.trim()
|
? input.apiKey.trim()
|
||||||
: current.apiKey,
|
: current.apiKey,
|
||||||
model: input.model?.trim() || current.model,
|
model: nextModel,
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
// 端点或模型变了,旧探测作废
|
||||||
|
...(endpointChanged ? { capabilities: undefined } : {}),
|
||||||
|
};
|
||||||
|
if (reasoningEffort) updated.reasoningEffort = reasoningEffort;
|
||||||
|
else delete updated.reasoningEffort;
|
||||||
|
data.profiles[index] = updated;
|
||||||
|
writeFile(data);
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function saveProfileCapabilities(
|
||||||
|
id: string,
|
||||||
|
capabilities: ProfileCapabilities,
|
||||||
|
): ApiProfile {
|
||||||
|
const data = readFile();
|
||||||
|
const index = data.profiles.findIndex((p) => p.id === id);
|
||||||
|
if (index < 0) throw new Error("API 配置不存在");
|
||||||
|
const current = data.profiles[index]!;
|
||||||
|
const updated: ApiProfile = {
|
||||||
|
...current,
|
||||||
|
capabilities: normalizeCapabilities(capabilities) ?? capabilities,
|
||||||
updatedAt: new Date().toISOString(),
|
updatedAt: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
data.profiles[index] = updated;
|
data.profiles[index] = updated;
|
||||||
@@ -119,6 +177,20 @@ export function updateApiProfile(
|
|||||||
return updated;
|
return updated;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 探测并持久化;返回更新后的 profile */
|
||||||
|
export async function probeAndSaveProfileCapabilities(
|
||||||
|
id: string,
|
||||||
|
): Promise<ApiProfile> {
|
||||||
|
const profile = getApiProfile(id);
|
||||||
|
if (!profile) throw new Error("API 配置不存在");
|
||||||
|
const capabilities = await probeProfileCapabilities({
|
||||||
|
baseUrl: profile.baseUrl,
|
||||||
|
apiKey: profile.apiKey,
|
||||||
|
model: profile.model,
|
||||||
|
});
|
||||||
|
return saveProfileCapabilities(id, capabilities);
|
||||||
|
}
|
||||||
|
|
||||||
export function deleteApiProfile(id: string): void {
|
export function deleteApiProfile(id: string): void {
|
||||||
const data = readFile();
|
const data = readFile();
|
||||||
data.profiles = data.profiles.filter((p) => p.id !== id);
|
data.profiles = data.profiles.filter((p) => p.id !== id);
|
||||||
@@ -130,30 +202,9 @@ export function profileToLlmConfig(profile: ApiProfile): LlmConfig {
|
|||||||
baseUrl: profile.baseUrl,
|
baseUrl: profile.baseUrl,
|
||||||
apiKey: profile.apiKey,
|
apiKey: profile.apiKey,
|
||||||
model: profile.model,
|
model: profile.model,
|
||||||
|
...(profile.reasoningEffort
|
||||||
|
? { reasoningEffort: profile.reasoningEffort }
|
||||||
|
: {}),
|
||||||
|
capabilities: normalizeCapabilities(profile.capabilities),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function testApiProfile(profile: ApiProfile): Promise<{
|
|
||||||
ok: boolean;
|
|
||||||
message: string;
|
|
||||||
}> {
|
|
||||||
const url = `${profile.baseUrl.replace(/\/$/, "")}/models`;
|
|
||||||
try {
|
|
||||||
const response = await fetch(url, {
|
|
||||||
headers: { Authorization: `Bearer ${profile.apiKey}` },
|
|
||||||
});
|
|
||||||
if (response.ok) {
|
|
||||||
return { ok: true, message: "连接成功" };
|
|
||||||
}
|
|
||||||
const body = await response.text();
|
|
||||||
return {
|
|
||||||
ok: false,
|
|
||||||
message: `HTTP ${response.status}: ${body.slice(0, 200)}`,
|
|
||||||
};
|
|
||||||
} catch (err) {
|
|
||||||
return {
|
|
||||||
ok: false,
|
|
||||||
message: err instanceof Error ? err.message : "连接失败",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,10 +1,19 @@
|
|||||||
import { existsSync, readFileSync } from "node:fs";
|
import { existsSync, readFileSync } from "node:fs";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
|
||||||
|
import type { ProfileCapabilities } from "../llm/capabilities.js";
|
||||||
|
|
||||||
export type LlmConfig = {
|
export type LlmConfig = {
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
apiKey: string;
|
apiKey: string;
|
||||||
model: string;
|
model: string;
|
||||||
|
/**
|
||||||
|
* API 配置上的默认思考强度;generation 未指定时写入 reasoning_effort。
|
||||||
|
* 空 / auto = 不发送,走供应商默认。
|
||||||
|
*/
|
||||||
|
reasoningEffort?: string;
|
||||||
|
/** 来自 API profile 探测结果;决定 structured 投递选路 */
|
||||||
|
capabilities?: ProfileCapabilities;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function loadLlmConfig(): LlmConfig {
|
export function loadLlmConfig(): LlmConfig {
|
||||||
|
|||||||
@@ -0,0 +1,364 @@
|
|||||||
|
/**
|
||||||
|
* API profile 能力探测与 structured 投递选路。
|
||||||
|
* 优先 response json_schema;不可用则 forced tool;再退 json_object。
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
applyRejectedGenerationField,
|
||||||
|
parseRejectedGenerationField,
|
||||||
|
} from "./generation-compat.js";
|
||||||
|
|
||||||
|
export type CapabilityStatus = "ok" | "fail" | "unknown";
|
||||||
|
|
||||||
|
export type StructuredDeliveryMode =
|
||||||
|
| "json_schema"
|
||||||
|
| "forced_tool"
|
||||||
|
| "json_object"
|
||||||
|
| "text";
|
||||||
|
|
||||||
|
export type ProfileCapabilities = {
|
||||||
|
/** 普通聊天 /models 或极简 completion */
|
||||||
|
chat: CapabilityStatus;
|
||||||
|
/** response_format: json_object */
|
||||||
|
jsonObject: CapabilityStatus;
|
||||||
|
/** response_format: json_schema(真 schema 约束) */
|
||||||
|
jsonSchema: CapabilityStatus;
|
||||||
|
/** tools + 返回 tool_calls */
|
||||||
|
tools: CapabilityStatus;
|
||||||
|
/** tool_choice=required(或指名函数)能强制提交 */
|
||||||
|
forcedTool: CapabilityStatus;
|
||||||
|
/** tools[].function.strict(若探测时带了) */
|
||||||
|
strictTool: CapabilityStatus;
|
||||||
|
testedAt?: string;
|
||||||
|
notes?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const UNKNOWN_CAPABILITIES: ProfileCapabilities = {
|
||||||
|
chat: "unknown",
|
||||||
|
jsonObject: "unknown",
|
||||||
|
jsonSchema: "unknown",
|
||||||
|
tools: "unknown",
|
||||||
|
forcedTool: "unknown",
|
||||||
|
strictTool: "unknown",
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 探测固定 low,避免 always-on 思考把 max_tokens 吃光导致误判 */
|
||||||
|
export const CAPABILITY_PROBE_REASONING_EFFORT = "low" as const;
|
||||||
|
|
||||||
|
const PROBE_SCHEMA = {
|
||||||
|
type: "object",
|
||||||
|
additionalProperties: false,
|
||||||
|
required: ["ping", "n"],
|
||||||
|
properties: {
|
||||||
|
ping: { type: "string" },
|
||||||
|
n: { type: "number" },
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const PROBE_TOOL = {
|
||||||
|
type: "function" as const,
|
||||||
|
function: {
|
||||||
|
name: "submit_probe",
|
||||||
|
description: "Submit the probe result. Always call this tool.",
|
||||||
|
parameters: {
|
||||||
|
type: "object",
|
||||||
|
additionalProperties: false,
|
||||||
|
required: ["ping", "n"],
|
||||||
|
properties: {
|
||||||
|
ping: { type: "string" },
|
||||||
|
n: { type: "number" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const PROBE_TOOL_STRICT = {
|
||||||
|
type: "function" as const,
|
||||||
|
function: {
|
||||||
|
...PROBE_TOOL.function,
|
||||||
|
strict: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export function normalizeCapabilities(
|
||||||
|
raw: unknown,
|
||||||
|
): ProfileCapabilities | undefined {
|
||||||
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return undefined;
|
||||||
|
const row = raw as Record<string, unknown>;
|
||||||
|
const read = (k: string): CapabilityStatus => {
|
||||||
|
const v = row[k];
|
||||||
|
if (v === "ok" || v === "fail" || v === "unknown") return v;
|
||||||
|
return "unknown";
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
chat: read("chat"),
|
||||||
|
jsonObject: read("jsonObject"),
|
||||||
|
jsonSchema: read("jsonSchema"),
|
||||||
|
tools: read("tools"),
|
||||||
|
forcedTool: read("forcedTool"),
|
||||||
|
strictTool: read("strictTool"),
|
||||||
|
testedAt: typeof row.testedAt === "string" ? row.testedAt : undefined,
|
||||||
|
notes: Array.isArray(row.notes)
|
||||||
|
? row.notes.filter((x): x is string => typeof x === "string")
|
||||||
|
: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 选路:schema → forced tool → json_object → text */
|
||||||
|
export function resolveStructuredDeliveryMode(
|
||||||
|
caps: ProfileCapabilities | null | undefined,
|
||||||
|
): StructuredDeliveryMode {
|
||||||
|
if (!caps) return "json_object";
|
||||||
|
if (caps.jsonSchema === "ok") return "json_schema";
|
||||||
|
if (caps.forcedTool === "ok") return "forced_tool";
|
||||||
|
if (caps.jsonObject === "ok") return "json_object";
|
||||||
|
return "text";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deliveryModeLabel(mode: StructuredDeliveryMode): string {
|
||||||
|
switch (mode) {
|
||||||
|
case "json_schema":
|
||||||
|
return "JSON Schema";
|
||||||
|
case "forced_tool":
|
||||||
|
return "强制 Tool";
|
||||||
|
case "json_object":
|
||||||
|
return "JSON Object";
|
||||||
|
default:
|
||||||
|
return "纯文本";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProbeConfig = {
|
||||||
|
baseUrl: string;
|
||||||
|
apiKey: string;
|
||||||
|
model: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
async function postOnce(
|
||||||
|
config: ProbeConfig,
|
||||||
|
body: Record<string, unknown>,
|
||||||
|
): Promise<{ ok: boolean; status: number; json: unknown; text: string }> {
|
||||||
|
const url = `${config.baseUrl.replace(/\/$/, "")}/chat/completions`;
|
||||||
|
try {
|
||||||
|
const res = await fetch(url, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${config.apiKey}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ model: config.model, ...body }),
|
||||||
|
});
|
||||||
|
const text = await res.text();
|
||||||
|
let json: unknown = null;
|
||||||
|
try {
|
||||||
|
json = JSON.parse(text) as unknown;
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
return { ok: res.ok, status: res.status, json, text };
|
||||||
|
} catch (err) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
status: 0,
|
||||||
|
json: null,
|
||||||
|
text: err instanceof Error ? err.message : String(err),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 探测请求始终带 reasoning_effort=low。
|
||||||
|
* 若供应商拒收该字段,剥掉后再试一次。
|
||||||
|
*/
|
||||||
|
async function postChat(
|
||||||
|
config: ProbeConfig,
|
||||||
|
body: Record<string, unknown>,
|
||||||
|
): Promise<{ ok: boolean; status: number; json: unknown; text: string }> {
|
||||||
|
const withEffort: Record<string, unknown> = {
|
||||||
|
...body,
|
||||||
|
reasoning_effort: CAPABILITY_PROBE_REASONING_EFFORT,
|
||||||
|
};
|
||||||
|
const first = await postOnce(config, withEffort);
|
||||||
|
if (first.ok || first.status !== 400) return first;
|
||||||
|
|
||||||
|
const rejected = parseRejectedGenerationField(first.text);
|
||||||
|
if (rejected !== "reasoning_effort") return first;
|
||||||
|
|
||||||
|
const retryBody = { ...withEffort };
|
||||||
|
const tried = new Set<string>();
|
||||||
|
if (!applyRejectedGenerationField(retryBody, "reasoning_effort", tried)) {
|
||||||
|
return first;
|
||||||
|
}
|
||||||
|
return postOnce(config, retryBody);
|
||||||
|
}
|
||||||
|
|
||||||
|
function assistantMessage(json: unknown): Record<string, unknown> | null {
|
||||||
|
if (!json || typeof json !== "object") return null;
|
||||||
|
const choices = (json as { choices?: unknown }).choices;
|
||||||
|
if (!Array.isArray(choices) || !choices[0] || typeof choices[0] !== "object") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const msg = (choices[0] as { message?: unknown }).message;
|
||||||
|
if (!msg || typeof msg !== "object") return null;
|
||||||
|
return msg as Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasToolCallNamed(json: unknown, name: string): boolean {
|
||||||
|
const msg = assistantMessage(json);
|
||||||
|
if (!msg) return false;
|
||||||
|
const calls = msg.tool_calls;
|
||||||
|
if (!Array.isArray(calls)) return false;
|
||||||
|
return calls.some((c) => {
|
||||||
|
if (!c || typeof c !== "object") return false;
|
||||||
|
const fn = (c as { function?: unknown }).function;
|
||||||
|
if (!fn || typeof fn !== "object") return false;
|
||||||
|
return (fn as { name?: unknown }).name === name;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function contentLooksLikeJsonObject(json: unknown): boolean {
|
||||||
|
const msg = assistantMessage(json);
|
||||||
|
const content = msg?.content;
|
||||||
|
if (typeof content !== "string" || !content.trim()) return false;
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(content) as unknown;
|
||||||
|
return parsed != null && typeof parsed === "object" && !Array.isArray(parsed);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对单个 profile 跑一轮能力探测(会真实打 API,费用很小)。
|
||||||
|
*/
|
||||||
|
export async function probeProfileCapabilities(
|
||||||
|
config: ProbeConfig,
|
||||||
|
): Promise<ProfileCapabilities> {
|
||||||
|
const notes: string[] = [];
|
||||||
|
const caps: ProfileCapabilities = {
|
||||||
|
...UNKNOWN_CAPABILITIES,
|
||||||
|
testedAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const chat = await postChat(config, {
|
||||||
|
messages: [{ role: "user", content: "Reply with exactly: ok" }],
|
||||||
|
max_tokens: 32,
|
||||||
|
});
|
||||||
|
caps.chat = chat.ok ? "ok" : "fail";
|
||||||
|
if (!chat.ok) {
|
||||||
|
notes.push(`chat: HTTP ${chat.status} ${chat.text.slice(0, 160)}`);
|
||||||
|
caps.notes = notes;
|
||||||
|
return caps;
|
||||||
|
}
|
||||||
|
|
||||||
|
const jsonObject = await postChat(config, {
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
content:
|
||||||
|
'Return a JSON object with keys ping (string "ok") and n (number 1).',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
response_format: { type: "json_object" },
|
||||||
|
max_tokens: 128,
|
||||||
|
});
|
||||||
|
caps.jsonObject =
|
||||||
|
jsonObject.ok && contentLooksLikeJsonObject(jsonObject.json) ? "ok" : "fail";
|
||||||
|
if (!jsonObject.ok) {
|
||||||
|
notes.push(`json_object: HTTP ${jsonObject.status} ${jsonObject.text.slice(0, 120)}`);
|
||||||
|
} else if (caps.jsonObject === "fail") {
|
||||||
|
notes.push("json_object: 返回了但不是可解析对象");
|
||||||
|
}
|
||||||
|
|
||||||
|
const jsonSchema = await postChat(config, {
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
content: "Fill ping=ok and n=1.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
response_format: {
|
||||||
|
type: "json_schema",
|
||||||
|
json_schema: {
|
||||||
|
name: "probe_result",
|
||||||
|
strict: true,
|
||||||
|
schema: PROBE_SCHEMA,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
max_tokens: 128,
|
||||||
|
});
|
||||||
|
caps.jsonSchema =
|
||||||
|
jsonSchema.ok && contentLooksLikeJsonObject(jsonSchema.json) ? "ok" : "fail";
|
||||||
|
if (!jsonSchema.ok) {
|
||||||
|
notes.push(`json_schema: HTTP ${jsonSchema.status} ${jsonSchema.text.slice(0, 120)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const tools = await postChat(config, {
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
content: "Call submit_probe with ping=ok and n=1.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
tools: [PROBE_TOOL],
|
||||||
|
tool_choice: "auto",
|
||||||
|
max_tokens: 256,
|
||||||
|
});
|
||||||
|
caps.tools = tools.ok && hasToolCallNamed(tools.json, "submit_probe") ? "ok" : "fail";
|
||||||
|
if (!tools.ok) {
|
||||||
|
notes.push(`tools: HTTP ${tools.status} ${tools.text.slice(0, 120)}`);
|
||||||
|
} else if (caps.tools === "fail") {
|
||||||
|
notes.push("tools: 请求成功但未返回 submit_probe");
|
||||||
|
}
|
||||||
|
|
||||||
|
const forced = await postChat(config, {
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
content: "Submit the probe.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
tools: [PROBE_TOOL],
|
||||||
|
tool_choice: "required",
|
||||||
|
max_tokens: 256,
|
||||||
|
});
|
||||||
|
let forcedOk =
|
||||||
|
forced.ok && hasToolCallNamed(forced.json, "submit_probe");
|
||||||
|
if (!forcedOk && forced.ok) {
|
||||||
|
const named = await postChat(config, {
|
||||||
|
messages: [{ role: "user", content: "Submit the probe." }],
|
||||||
|
tools: [PROBE_TOOL],
|
||||||
|
tool_choice: {
|
||||||
|
type: "function",
|
||||||
|
function: { name: "submit_probe" },
|
||||||
|
},
|
||||||
|
max_tokens: 256,
|
||||||
|
});
|
||||||
|
forcedOk = named.ok && hasToolCallNamed(named.json, "submit_probe");
|
||||||
|
if (!named.ok) {
|
||||||
|
notes.push(`forced_tool(named): HTTP ${named.status} ${named.text.slice(0, 120)}`);
|
||||||
|
}
|
||||||
|
} else if (!forced.ok) {
|
||||||
|
notes.push(`forced_tool: HTTP ${forced.status} ${forced.text.slice(0, 120)}`);
|
||||||
|
}
|
||||||
|
caps.forcedTool = forcedOk ? "ok" : "fail";
|
||||||
|
|
||||||
|
if (caps.tools === "ok" || caps.forcedTool === "ok") {
|
||||||
|
const strict = await postChat(config, {
|
||||||
|
messages: [{ role: "user", content: "Submit the probe." }],
|
||||||
|
tools: [PROBE_TOOL_STRICT],
|
||||||
|
tool_choice: "required",
|
||||||
|
max_tokens: 256,
|
||||||
|
});
|
||||||
|
caps.strictTool =
|
||||||
|
strict.ok && hasToolCallNamed(strict.json, "submit_probe") ? "ok" : "fail";
|
||||||
|
if (!strict.ok) {
|
||||||
|
notes.push(`strict_tool: HTTP ${strict.status} ${strict.text.slice(0, 120)}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
caps.strictTool = "fail";
|
||||||
|
}
|
||||||
|
|
||||||
|
caps.notes = notes.length ? notes : undefined;
|
||||||
|
return caps;
|
||||||
|
}
|
||||||
+47
-28
@@ -1,11 +1,18 @@
|
|||||||
import type { LlmConfig } from "../config/env.js";
|
import type { LlmConfig } from "../config/env.js";
|
||||||
import type { GenerationParameters } from "../types/preset.js";
|
import type { GenerationParameters } from "../types/preset.js";
|
||||||
|
import {
|
||||||
|
applyOpenAiGeneration,
|
||||||
|
fetchWithGenerationCompat,
|
||||||
|
sanitizeReasoningEffort,
|
||||||
|
} from "./generation-compat.js";
|
||||||
import { consumeOpenAiToolStream } from "./stream-complete.js";
|
import { consumeOpenAiToolStream } from "./stream-complete.js";
|
||||||
import {
|
import {
|
||||||
currentAbortSignal,
|
currentAbortSignal,
|
||||||
throwIfAborted,
|
throwIfAborted,
|
||||||
} from "./run-abort.js";
|
} from "./run-abort.js";
|
||||||
|
|
||||||
|
export { sanitizeReasoningEffort };
|
||||||
|
|
||||||
export type ToolCallPayload = {
|
export type ToolCallPayload = {
|
||||||
id: string;
|
id: string;
|
||||||
type: "function";
|
type: "function";
|
||||||
@@ -30,6 +37,8 @@ export type ToolDefinition = {
|
|||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
parameters: Record<string, unknown>;
|
parameters: Record<string, unknown>;
|
||||||
|
/** 部分供应商支持的严格参数约束 */
|
||||||
|
strict?: boolean;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -57,8 +66,16 @@ export type CompleteResult = {
|
|||||||
model?: string;
|
model?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type JsonSchemaFormat = {
|
||||||
|
name: string;
|
||||||
|
strict?: boolean;
|
||||||
|
schema: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
|
||||||
export type CompleteOptions = {
|
export type CompleteOptions = {
|
||||||
responseFormat?: "json_object" | "text";
|
responseFormat?: "json_object" | "json_schema" | "text";
|
||||||
|
/** responseFormat=json_schema 时使用 */
|
||||||
|
jsonSchema?: JsonSchemaFormat;
|
||||||
generation?: GenerationParameters;
|
generation?: GenerationParameters;
|
||||||
/** 统计用途,如 main_agent / worker:write-rules */
|
/** 统计用途,如 main_agent / worker:write-rules */
|
||||||
caller?: string;
|
caller?: string;
|
||||||
@@ -66,8 +83,15 @@ export type CompleteOptions = {
|
|||||||
signal?: AbortSignal;
|
signal?: AbortSignal;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ToolChoice =
|
||||||
|
| "auto"
|
||||||
|
| "required"
|
||||||
|
| "none"
|
||||||
|
| { type: "function"; function: { name: string } };
|
||||||
|
|
||||||
export type CompleteWithToolsOptions = CompleteOptions & {
|
export type CompleteWithToolsOptions = CompleteOptions & {
|
||||||
tools: ToolDefinition[];
|
tools: ToolDefinition[];
|
||||||
|
toolChoice?: ToolChoice;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CompleteWithToolsResult = {
|
export type CompleteWithToolsResult = {
|
||||||
@@ -104,12 +128,11 @@ export type LlmProvider = {
|
|||||||
): Promise<CompleteWithToolsResult>;
|
): Promise<CompleteWithToolsResult>;
|
||||||
};
|
};
|
||||||
|
|
||||||
function buildRequestBody(
|
export function buildRequestBody(
|
||||||
config: LlmConfig,
|
config: LlmConfig,
|
||||||
messages: ChatMessage[],
|
messages: ChatMessage[],
|
||||||
options?: CompleteOptions & { tools?: ToolDefinition[]; stream?: boolean },
|
options?: CompleteOptions & { tools?: ToolDefinition[]; stream?: boolean },
|
||||||
): Record<string, unknown> {
|
): Record<string, unknown> {
|
||||||
const gen = options?.generation ?? {};
|
|
||||||
const body: Record<string, unknown> = {
|
const body: Record<string, unknown> = {
|
||||||
model: config.model,
|
model: config.model,
|
||||||
messages,
|
messages,
|
||||||
@@ -117,34 +140,28 @@ function buildRequestBody(
|
|||||||
|
|
||||||
if (options?.tools?.length) {
|
if (options?.tools?.length) {
|
||||||
body.tools = options.tools;
|
body.tools = options.tools;
|
||||||
body.tool_choice = "auto";
|
body.tool_choice =
|
||||||
|
(options as CompleteWithToolsOptions).toolChoice ?? "auto";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (gen.temperature !== undefined) body.temperature = gen.temperature;
|
applyOpenAiGeneration(body, options?.generation ?? {}, config.model, {
|
||||||
else body.temperature = 0.2;
|
reasoningEffort: config.reasoningEffort,
|
||||||
|
});
|
||||||
if (gen.topP !== undefined) body.top_p = gen.topP;
|
|
||||||
if (gen.topK !== undefined) body.top_k = gen.topK;
|
|
||||||
if (gen.minP !== undefined) body.min_p = gen.minP;
|
|
||||||
if (gen.frequencyPenalty !== undefined) {
|
|
||||||
body.frequency_penalty = gen.frequencyPenalty;
|
|
||||||
}
|
|
||||||
if (gen.presencePenalty !== undefined) {
|
|
||||||
body.presence_penalty = gen.presencePenalty;
|
|
||||||
}
|
|
||||||
if (gen.repetitionPenalty !== undefined) {
|
|
||||||
body.repetition_penalty = gen.repetitionPenalty;
|
|
||||||
}
|
|
||||||
if (gen.maxOutputTokens !== undefined) {
|
|
||||||
body.max_tokens = gen.maxOutputTokens;
|
|
||||||
}
|
|
||||||
if (gen.seed !== undefined) body.seed = gen.seed;
|
|
||||||
if (gen.reasoningEffort !== undefined) {
|
|
||||||
body.reasoning_effort = gen.reasoningEffort;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options?.responseFormat === "json_object") {
|
if (options?.responseFormat === "json_object") {
|
||||||
body.response_format = { type: "json_object" };
|
body.response_format = { type: "json_object" };
|
||||||
|
} else if (
|
||||||
|
options?.responseFormat === "json_schema" &&
|
||||||
|
options.jsonSchema
|
||||||
|
) {
|
||||||
|
body.response_format = {
|
||||||
|
type: "json_schema",
|
||||||
|
json_schema: {
|
||||||
|
name: options.jsonSchema.name,
|
||||||
|
strict: options.jsonSchema.strict ?? true,
|
||||||
|
schema: options.jsonSchema.schema,
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options?.stream) {
|
if (options?.stream) {
|
||||||
@@ -246,9 +263,10 @@ function extractMessageParts(message: Record<string, unknown> | undefined): {
|
|||||||
async function llmFetch(
|
async function llmFetch(
|
||||||
url: string,
|
url: string,
|
||||||
apiKey: string,
|
apiKey: string,
|
||||||
body: unknown,
|
body: Record<string, unknown>,
|
||||||
options?: CompleteOptions,
|
options?: CompleteOptions,
|
||||||
): Promise<Response> {
|
): Promise<Response> {
|
||||||
|
return fetchWithGenerationCompat(async (payload) => {
|
||||||
throwIfAborted(options?.signal);
|
throwIfAborted(options?.signal);
|
||||||
return fetch(url, {
|
return fetch(url, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -256,9 +274,10 @@ async function llmFetch(
|
|||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
Authorization: `Bearer ${apiKey}`,
|
Authorization: `Bearer ${apiKey}`,
|
||||||
},
|
},
|
||||||
body: JSON.stringify(body),
|
body: JSON.stringify(payload),
|
||||||
signal: options?.signal ?? currentAbortSignal(),
|
signal: options?.signal ?? currentAbortSignal(),
|
||||||
});
|
});
|
||||||
|
}, body);
|
||||||
}
|
}
|
||||||
|
|
||||||
export class OpenAiCompatibleProvider implements LlmProvider {
|
export class OpenAiCompatibleProvider implements LlmProvider {
|
||||||
|
|||||||
@@ -0,0 +1,277 @@
|
|||||||
|
import type { GenerationParameters } from "../types/preset.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OpenAI-compatible Chat Completions 的生成参数兼容层,对齐 SillyTavern Custom 源:
|
||||||
|
* - `auto` / 关闭值不发送
|
||||||
|
* - top_k、min_p、repetition_penalty 不是官方 Chat Completions 字段,默认不发
|
||||||
|
* - 推理模型去掉 temperature / top_p / penalty
|
||||||
|
* - o 系列与 gpt-5 用 max_completion_tokens
|
||||||
|
* - 仍 400 时按错误信息剥掉不支持字段再试
|
||||||
|
*/
|
||||||
|
|
||||||
|
const REASONING_EFFORT_VALUES = [
|
||||||
|
"none",
|
||||||
|
"minimal",
|
||||||
|
"low",
|
||||||
|
"medium",
|
||||||
|
"high",
|
||||||
|
"xhigh",
|
||||||
|
"max",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
type ReasoningEffort = (typeof REASONING_EFFORT_VALUES)[number];
|
||||||
|
|
||||||
|
const REASONING_EFFORT_ALIASES: Record<string, ReasoningEffort> = {
|
||||||
|
min: "minimal",
|
||||||
|
};
|
||||||
|
|
||||||
|
const VERBOSITY_VALUES = ["low", "medium", "high"] as const;
|
||||||
|
type Verbosity = (typeof VERBOSITY_VALUES)[number];
|
||||||
|
|
||||||
|
export const STRIPPABLE_GENERATION_FIELDS = new Set([
|
||||||
|
"temperature",
|
||||||
|
"top_p",
|
||||||
|
"top_k",
|
||||||
|
"min_p",
|
||||||
|
"frequency_penalty",
|
||||||
|
"presence_penalty",
|
||||||
|
"repetition_penalty",
|
||||||
|
"seed",
|
||||||
|
"n",
|
||||||
|
"reasoning_effort",
|
||||||
|
"verbosity",
|
||||||
|
"max_tokens",
|
||||||
|
"max_completion_tokens",
|
||||||
|
"logit_bias",
|
||||||
|
"stop",
|
||||||
|
"response_format",
|
||||||
|
"stream_options",
|
||||||
|
]);
|
||||||
|
|
||||||
|
export type ChatModelCompat = {
|
||||||
|
omitSampling: boolean;
|
||||||
|
useMaxCompletionTokens: boolean;
|
||||||
|
supportsVerbosity: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
function modelLeaf(model: string): string {
|
||||||
|
const trimmed = model.trim().toLowerCase();
|
||||||
|
const slash = trimmed.lastIndexOf("/");
|
||||||
|
return slash >= 0 ? trimmed.slice(slash + 1) : trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function classifyChatModel(model: string): ChatModelCompat {
|
||||||
|
const id = modelLeaf(model);
|
||||||
|
const isOSeries = /^o[134]($|[-.\d])/.test(id);
|
||||||
|
const isGpt5 = id.includes("gpt-5");
|
||||||
|
const isGpt5Chat = isGpt5 && id.includes("chat");
|
||||||
|
const isKimi25 = id.includes("kimi-k2.5");
|
||||||
|
const isDeepseekReasoner = id.includes("deepseek-reasoner");
|
||||||
|
const isGrok3Mini = id.includes("grok-3-mini");
|
||||||
|
|
||||||
|
return {
|
||||||
|
omitSampling:
|
||||||
|
isOSeries ||
|
||||||
|
(isGpt5 && !isGpt5Chat) ||
|
||||||
|
isKimi25 ||
|
||||||
|
isDeepseekReasoner ||
|
||||||
|
isGrok3Mini,
|
||||||
|
useMaxCompletionTokens: isOSeries || isGpt5,
|
||||||
|
supportsVerbosity: isGpt5,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sanitizeReasoningEffort(
|
||||||
|
value: unknown,
|
||||||
|
): ReasoningEffort | undefined {
|
||||||
|
if (typeof value !== "string") return undefined;
|
||||||
|
const key = value.trim().toLowerCase();
|
||||||
|
if (!key || key === "auto") return undefined;
|
||||||
|
const mapped = REASONING_EFFORT_ALIASES[key] ?? key;
|
||||||
|
return (REASONING_EFFORT_VALUES as readonly string[]).includes(mapped)
|
||||||
|
? (mapped as ReasoningEffort)
|
||||||
|
: undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sanitizeVerbosity(value: unknown): Verbosity | undefined {
|
||||||
|
if (typeof value !== "string") return undefined;
|
||||||
|
const key = value.trim().toLowerCase();
|
||||||
|
if (!key || key === "auto") return undefined;
|
||||||
|
return (VERBOSITY_VALUES as readonly string[]).includes(key)
|
||||||
|
? (key as Verbosity)
|
||||||
|
: undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function finiteNumber(value: unknown): number | undefined {
|
||||||
|
if (typeof value !== "number" || !Number.isFinite(value)) return undefined;
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GenerationDefaults = {
|
||||||
|
/** API profile 等来源的默认思考强度;仅当 gen 未指定时生效 */
|
||||||
|
reasoningEffort?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function applyOpenAiGeneration(
|
||||||
|
body: Record<string, unknown>,
|
||||||
|
gen: GenerationParameters,
|
||||||
|
model: string,
|
||||||
|
defaults?: GenerationDefaults,
|
||||||
|
): void {
|
||||||
|
const { omitSampling, useMaxCompletionTokens, supportsVerbosity } =
|
||||||
|
classifyChatModel(model);
|
||||||
|
|
||||||
|
if (!omitSampling) {
|
||||||
|
body.temperature = gen.temperature !== undefined ? gen.temperature : 0.2;
|
||||||
|
const topP = finiteNumber(gen.topP);
|
||||||
|
if (topP !== undefined && topP > 0) body.top_p = topP;
|
||||||
|
const frequencyPenalty = finiteNumber(gen.frequencyPenalty);
|
||||||
|
if (frequencyPenalty !== undefined) {
|
||||||
|
body.frequency_penalty = frequencyPenalty;
|
||||||
|
}
|
||||||
|
const presencePenalty = finiteNumber(gen.presencePenalty);
|
||||||
|
if (presencePenalty !== undefined) {
|
||||||
|
body.presence_penalty = presencePenalty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxOutput = finiteNumber(gen.maxOutputTokens);
|
||||||
|
if (maxOutput !== undefined && maxOutput > 0) {
|
||||||
|
if (useMaxCompletionTokens) body.max_completion_tokens = maxOutput;
|
||||||
|
else body.max_tokens = maxOutput;
|
||||||
|
}
|
||||||
|
|
||||||
|
const seed = finiteNumber(gen.seed);
|
||||||
|
if (seed !== undefined && seed >= 0) body.seed = seed;
|
||||||
|
|
||||||
|
const reasoningEffort =
|
||||||
|
sanitizeReasoningEffort(gen.reasoningEffort) ??
|
||||||
|
sanitizeReasoningEffort(defaults?.reasoningEffort);
|
||||||
|
if (reasoningEffort !== undefined) {
|
||||||
|
body.reasoning_effort = reasoningEffort;
|
||||||
|
}
|
||||||
|
|
||||||
|
const verbosity = sanitizeVerbosity(gen.verbosity);
|
||||||
|
if (verbosity !== undefined && supportsVerbosity) {
|
||||||
|
body.verbosity = verbosity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function readErrorMessage(raw: unknown): string {
|
||||||
|
if (!raw || typeof raw !== "object") return "";
|
||||||
|
const err = (raw as { error?: unknown }).error;
|
||||||
|
if (typeof err === "string") return err;
|
||||||
|
if (err && typeof err === "object") {
|
||||||
|
const message = (err as { message?: unknown }).message;
|
||||||
|
if (typeof message === "string") return message;
|
||||||
|
}
|
||||||
|
const message = (raw as { message?: unknown }).message;
|
||||||
|
return typeof message === "string" ? message : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function readErrorParam(raw: unknown): string | undefined {
|
||||||
|
if (!raw || typeof raw !== "object") return undefined;
|
||||||
|
const err = (raw as { error?: unknown }).error;
|
||||||
|
if (err && typeof err === "object") {
|
||||||
|
const param = (err as { param?: unknown }).param;
|
||||||
|
if (typeof param === "string" && STRIPPABLE_GENERATION_FIELDS.has(param)) {
|
||||||
|
return param;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseRejectedGenerationField(
|
||||||
|
errorText: string,
|
||||||
|
): string | undefined {
|
||||||
|
let parsed: unknown;
|
||||||
|
try {
|
||||||
|
parsed = JSON.parse(errorText);
|
||||||
|
} catch {
|
||||||
|
parsed = undefined;
|
||||||
|
}
|
||||||
|
const fromParam = readErrorParam(parsed);
|
||||||
|
if (fromParam) return fromParam;
|
||||||
|
|
||||||
|
const message = `${readErrorMessage(parsed)}\n${errorText}`;
|
||||||
|
if (/temperature and top_p cannot both/i.test(message)) return "top_p";
|
||||||
|
|
||||||
|
const patterns = [
|
||||||
|
/unsupported parameter:?\s*['"`]?([a-z_]+)/i,
|
||||||
|
/unknown field [`'"]([a-z_]+)[`'"]/i,
|
||||||
|
/([a-z_]+):\s*unknown variant/i,
|
||||||
|
/invalid value[^\n]*for[^\n]*['"`]([a-z_]+)['"`]/i,
|
||||||
|
];
|
||||||
|
for (const pattern of patterns) {
|
||||||
|
const match = message.match(pattern);
|
||||||
|
const field = match?.[1]?.toLowerCase();
|
||||||
|
if (field && STRIPPABLE_GENERATION_FIELDS.has(field)) return field;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyRejectedGenerationField(
|
||||||
|
body: Record<string, unknown>,
|
||||||
|
field: string,
|
||||||
|
tried: Set<string>,
|
||||||
|
): boolean {
|
||||||
|
if (!STRIPPABLE_GENERATION_FIELDS.has(field)) return false;
|
||||||
|
|
||||||
|
if (
|
||||||
|
field === "max_completion_tokens" &&
|
||||||
|
body.max_completion_tokens != null &&
|
||||||
|
!tried.has("max_tokens")
|
||||||
|
) {
|
||||||
|
body.max_tokens = body.max_completion_tokens;
|
||||||
|
delete body.max_completion_tokens;
|
||||||
|
tried.add("max_completion_tokens");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
field === "max_tokens" &&
|
||||||
|
body.max_tokens != null &&
|
||||||
|
!tried.has("max_completion_tokens")
|
||||||
|
) {
|
||||||
|
body.max_completion_tokens = body.max_tokens;
|
||||||
|
delete body.max_tokens;
|
||||||
|
tried.add("max_tokens");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(field in body)) return false;
|
||||||
|
delete body[field];
|
||||||
|
tried.add(field);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function replayResponse(source: Response, text: string): Response {
|
||||||
|
return new Response(text, {
|
||||||
|
status: source.status,
|
||||||
|
statusText: source.statusText,
|
||||||
|
headers: source.headers,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchWithGenerationCompat(
|
||||||
|
send: (body: Record<string, unknown>) => Promise<Response>,
|
||||||
|
initialBody: Record<string, unknown>,
|
||||||
|
): Promise<Response> {
|
||||||
|
let body = initialBody;
|
||||||
|
const tried = new Set<string>();
|
||||||
|
let last: Response | undefined;
|
||||||
|
|
||||||
|
for (let attempt = 0; attempt < 8; attempt++) {
|
||||||
|
last = await send(body);
|
||||||
|
if (last.ok || last.status !== 400) return last;
|
||||||
|
const text = await last.text();
|
||||||
|
const field = parseRejectedGenerationField(text);
|
||||||
|
if (!field) return replayResponse(last, text);
|
||||||
|
const next = { ...body };
|
||||||
|
if (!applyRejectedGenerationField(next, field, tried)) {
|
||||||
|
return replayResponse(last, text);
|
||||||
|
}
|
||||||
|
body = next;
|
||||||
|
}
|
||||||
|
|
||||||
|
return last ?? new Response("LLM request failed", { status: 400 });
|
||||||
|
}
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
/**
|
||||||
|
* 按 profile 能力选路的 structured 完成:json_schema → forced_tool → json_object。
|
||||||
|
*/
|
||||||
|
import {
|
||||||
|
resolveStructuredDeliveryMode,
|
||||||
|
type ProfileCapabilities,
|
||||||
|
type StructuredDeliveryMode,
|
||||||
|
} from "./capabilities.js";
|
||||||
|
import type {
|
||||||
|
ChatMessage,
|
||||||
|
CompleteOptions,
|
||||||
|
CompleteResult,
|
||||||
|
LlmProvider,
|
||||||
|
ToolDefinition,
|
||||||
|
} from "./client.js";
|
||||||
|
import { tryParseJsonDoc } from "../parse/json-doc.js";
|
||||||
|
|
||||||
|
export type StructuredCompleteOptions = CompleteOptions & {
|
||||||
|
/** JSON Schema 对象(json_schema / tool parameters 共用) */
|
||||||
|
schema: Record<string, unknown>;
|
||||||
|
/** schema / tool 名称 */
|
||||||
|
name: string;
|
||||||
|
/** 覆盖自动选路 */
|
||||||
|
mode?: StructuredDeliveryMode;
|
||||||
|
capabilities?: ProfileCapabilities | null;
|
||||||
|
/**
|
||||||
|
* schema 含 additionalProperties:true 等、无法走 strict json_schema 时置 true。
|
||||||
|
* 会自动降级:forced_tool → json_object。
|
||||||
|
*/
|
||||||
|
schemaIsLoose?: boolean;
|
||||||
|
/** forced_tool 时是否尝试 strict(默认:capabilities.strictTool===ok 且非 loose) */
|
||||||
|
preferStrictTool?: boolean;
|
||||||
|
stream?: import("./client.js").StreamCallbacks;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type StructuredCompleteResult = CompleteResult & {
|
||||||
|
parsed: unknown;
|
||||||
|
mode: StructuredDeliveryMode;
|
||||||
|
};
|
||||||
|
|
||||||
|
function parseObjectPayload(raw: string): unknown {
|
||||||
|
const parsed = tryParseJsonDoc(raw);
|
||||||
|
if (parsed != null) return parsed;
|
||||||
|
try {
|
||||||
|
return JSON.parse(raw) as unknown;
|
||||||
|
} catch {
|
||||||
|
throw new Error("structured 输出无法解析为 JSON");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildSubmitTool(
|
||||||
|
name: string,
|
||||||
|
schema: Record<string, unknown>,
|
||||||
|
strict: boolean,
|
||||||
|
): ToolDefinition {
|
||||||
|
return {
|
||||||
|
type: "function",
|
||||||
|
function: {
|
||||||
|
name,
|
||||||
|
description: `Submit the structured result as arguments to ${name}. Always call this tool.`,
|
||||||
|
parameters: schema,
|
||||||
|
...(strict ? { strict: true } : {}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 一次 structured 完成:返回解析后的对象。
|
||||||
|
* forced_tool 路径走 completeWithTools;其余走 complete / completeStream。
|
||||||
|
*/
|
||||||
|
export async function completeStructured(
|
||||||
|
llm: LlmProvider,
|
||||||
|
messages: ChatMessage[],
|
||||||
|
options: StructuredCompleteOptions,
|
||||||
|
): Promise<StructuredCompleteResult> {
|
||||||
|
const requested =
|
||||||
|
options.mode ?? resolveStructuredDeliveryMode(options.capabilities);
|
||||||
|
let mode = requested;
|
||||||
|
if (options.schemaIsLoose && mode === "json_schema") {
|
||||||
|
mode =
|
||||||
|
options.capabilities?.forcedTool === "ok"
|
||||||
|
? "forced_tool"
|
||||||
|
: options.capabilities?.jsonObject === "ok"
|
||||||
|
? "json_object"
|
||||||
|
: "text";
|
||||||
|
}
|
||||||
|
const schemaName = options.name || "submit_result";
|
||||||
|
|
||||||
|
if (mode === "forced_tool") {
|
||||||
|
const strict =
|
||||||
|
!options.schemaIsLoose &&
|
||||||
|
(options.preferStrictTool ??
|
||||||
|
options.capabilities?.strictTool === "ok");
|
||||||
|
const tools = [buildSubmitTool(schemaName, options.schema, strict)];
|
||||||
|
const result = await llm.completeWithTools(messages, {
|
||||||
|
...options,
|
||||||
|
tools,
|
||||||
|
toolChoice: "required",
|
||||||
|
responseFormat: undefined,
|
||||||
|
jsonSchema: undefined,
|
||||||
|
});
|
||||||
|
const call =
|
||||||
|
result.toolCalls.find((t) => t.name === schemaName) ??
|
||||||
|
result.toolCalls[0];
|
||||||
|
if (!call) {
|
||||||
|
throw new Error("forced_tool 模式未返回 tool_calls");
|
||||||
|
}
|
||||||
|
const parsed = parseObjectPayload(call.arguments);
|
||||||
|
return {
|
||||||
|
content: call.arguments,
|
||||||
|
reasoning: result.reasoning,
|
||||||
|
usage: result.usage,
|
||||||
|
model: result.model,
|
||||||
|
parsed,
|
||||||
|
mode,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const completeOpts: CompleteOptions = {
|
||||||
|
...options,
|
||||||
|
responseFormat:
|
||||||
|
mode === "json_schema"
|
||||||
|
? "json_schema"
|
||||||
|
: mode === "json_object"
|
||||||
|
? "json_object"
|
||||||
|
: "text",
|
||||||
|
jsonSchema:
|
||||||
|
mode === "json_schema"
|
||||||
|
? { name: schemaName, strict: true, schema: options.schema }
|
||||||
|
: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
let result: CompleteResult;
|
||||||
|
if (options.stream && llm.completeStream) {
|
||||||
|
result = await llm.completeStream(messages, completeOpts, options.stream);
|
||||||
|
} else {
|
||||||
|
result = await llm.complete(messages, completeOpts);
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = parseObjectPayload(result.content);
|
||||||
|
return { ...result, parsed, mode };
|
||||||
|
}
|
||||||
@@ -46,6 +46,7 @@ const WAIT_LABEL: Record<WaitingReason["kind"], string> = {
|
|||||||
worker_questions: "执行单元提问",
|
worker_questions: "执行单元提问",
|
||||||
revision: "修订",
|
revision: "修订",
|
||||||
next_intent: "下一步意向",
|
next_intent: "下一步意向",
|
||||||
|
pick_creation_step: "点选节点",
|
||||||
};
|
};
|
||||||
|
|
||||||
const EVENT_LABEL: Record<RuntimeEvent["type"], string> = {
|
const EVENT_LABEL: Record<RuntimeEvent["type"], string> = {
|
||||||
@@ -56,9 +57,14 @@ const EVENT_LABEL: Record<RuntimeEvent["type"], string> = {
|
|||||||
main_agent_decision_created: "总管决策",
|
main_agent_decision_created: "总管决策",
|
||||||
user_approved_next_step: "确认下一步",
|
user_approved_next_step: "确认下一步",
|
||||||
user_rejected_next_step: "拒绝下一步",
|
user_rejected_next_step: "拒绝下一步",
|
||||||
|
user_requested_flow_replan: "再编排",
|
||||||
|
user_picked_creation_step: "点选节点",
|
||||||
|
creation_step_pick_awaited: "等待点选节点",
|
||||||
|
user_left_creation_step: "返回节点选择",
|
||||||
worker_started: "执行单元开始",
|
worker_started: "执行单元开始",
|
||||||
worker_completed: "执行单元完成",
|
worker_completed: "执行单元完成",
|
||||||
worker_needs_input: "执行单元提问",
|
worker_needs_input: "执行单元提问",
|
||||||
|
worker_revision_produced_nothing: "修订无新产物",
|
||||||
user_resolved_sidecar_questions: "回答挂载追问",
|
user_resolved_sidecar_questions: "回答挂载追问",
|
||||||
user_accepted_artifact: "验收通过",
|
user_accepted_artifact: "验收通过",
|
||||||
user_rejected_artifact: "打回产物",
|
user_rejected_artifact: "打回产物",
|
||||||
@@ -78,6 +84,7 @@ const EFFECT_LABEL: Record<string, string> = {
|
|||||||
emit_message: "发消息",
|
emit_message: "发消息",
|
||||||
propose_next_creation_step: "提案下一步",
|
propose_next_creation_step: "提案下一步",
|
||||||
seal_creation_opening: "收口开场白",
|
seal_creation_opening: "收口开场白",
|
||||||
|
unseal_creation_opening: "解开开场收口",
|
||||||
run_play_turn: "开游玩回合",
|
run_play_turn: "开游玩回合",
|
||||||
continue_play_turn: "续游玩管线",
|
continue_play_turn: "续游玩管线",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -24,25 +24,26 @@ function buildMainAgentSystemPrompt(
|
|||||||
? workers.map((w) => `- ${w.id}:${w.description}`).join("\n")
|
? workers.map((w) => `- ${w.id}:${w.description}`).join("\n")
|
||||||
: "- (当前 skill 未加载 worker 列表)";
|
: "- (当前 skill 未加载 worker 列表)";
|
||||||
|
|
||||||
return `你是创作节点路由器。只读取「设计.创作流程」DAG,并选择下一个要执行的创作节点。
|
return `你是创作节点路由器。当前没有「设计.创作流程」。本轮调用 run_worker:workerId 为 design-flow,排出近期起点。
|
||||||
|
|
||||||
- DAG 不存在或需要追加节点:调用 design-flow。
|
|
||||||
- DAG 中已有下一个可执行节点:调用 design-step。
|
|
||||||
- 只调度 worker,不创作正文,不规划 DAG 之外的流程。
|
|
||||||
|
|
||||||
当前 skill 可用 worker(workerId 必须与下列 id 完全一致):
|
当前 skill 可用 worker(workerId 必须与下列 id 完全一致):
|
||||||
${workerLines}
|
${workerLines}
|
||||||
|
|
||||||
输出必须是 JSON 对象,字段:
|
结束本轮必须调用 run_worker(design-flow)。reason 用一句话说明根据用户需求开始编排;requiresApproval 为 false。`;
|
||||||
{
|
}
|
||||||
"action": "ask_user" | "run_worker" | "create_temp_worker" | "review_blackboard" | "finish",
|
|
||||||
"reason": "string",
|
/** 总管把推理写成正文、未给出可用工具/JSON 时,运行时改走点选或 design-flow。 */
|
||||||
"assessment": "string | null",
|
export function isUnusableMainAgentOutput(err: unknown): boolean {
|
||||||
"questions": [{ "id": "q1", "prompt": "…", "options": [{ "label": "建议示范…" }] }] | null,
|
const msg = err instanceof Error ? err.message : String(err);
|
||||||
"workerId": "string | null",
|
return (
|
||||||
"requiresApproval": boolean,
|
/invalid JSON/i.test(msg) ||
|
||||||
"workerContext": { "roleId": "string" } | null
|
/no tool calls and no content/i.test(msg) ||
|
||||||
}`;
|
/Invalid action/i.test(msg) ||
|
||||||
|
/must be an object/i.test(msg) ||
|
||||||
|
/requires reason/i.test(msg) ||
|
||||||
|
/multiple terminal tools/i.test(msg) ||
|
||||||
|
/Not a terminal tool/i.test(msg)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MainAgent {
|
export class MainAgent {
|
||||||
|
|||||||
@@ -42,17 +42,12 @@ function buildToolLoopSystemPrompt(
|
|||||||
? workers.map((w) => `- ${w.id}:${w.description}`).join("\n")
|
? workers.map((w) => `- ${w.id}:${w.description}`).join("\n")
|
||||||
: "- (当前 skill 未加载 worker 列表)";
|
: "- (当前 skill 未加载 worker 列表)";
|
||||||
|
|
||||||
return `你是创作节点路由器。只根据「设计.创作流程」DAG 选择下一个创作节点。
|
return `你是创作节点路由器。当前没有「设计.创作流程」。本轮调用 run_worker:workerId 为 design-flow,排出近期起点。
|
||||||
|
|
||||||
1. 用 read_blackboard 读取「设计.创作流程」及其进度 tag。
|
|
||||||
2. 没有 DAG 或 DAG 需要追加节点时,run_worker(design-flow)。
|
|
||||||
3. DAG 已给出下一个可执行节点时,run_worker(design-step)。
|
|
||||||
4. 不创作正文,不改写 DAG,不处理运行状态或游玩流程。
|
|
||||||
|
|
||||||
当前 skill 可用 worker:
|
当前 skill 可用 worker:
|
||||||
${workerLines}
|
${workerLines}
|
||||||
|
|
||||||
选定节点后立即调用 run_worker。`;
|
结束本轮必须调用 run_worker(design-flow)。reason 用一句话说明根据用户需求开始编排;requiresApproval 为 false。`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function executeLoopTool(
|
function executeLoopTool(
|
||||||
|
|||||||
@@ -0,0 +1,226 @@
|
|||||||
|
/** 从模型正文里抽出 JSON:先按原文 parse,再宽松修,避免弯引号/围栏/夹叙把合法稿撕开。 */
|
||||||
|
|
||||||
|
export function stripCodeFences(text: string): string {
|
||||||
|
let t = String(text || "").trim();
|
||||||
|
t = t.replace(/^```(?:json|yaml|yml)?\s*\r?\n?/i, "");
|
||||||
|
t = t.replace(/\r?\n?```\s*$/i, "");
|
||||||
|
const fenced = t.match(/```(?:json|yaml|yml)?\s*\r?\n([\s\S]*?)\r?\n```/i);
|
||||||
|
if (fenced) t = fenced[1]!.trim();
|
||||||
|
return t.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function escapeRawControlsInJsonStrings(s: string): string {
|
||||||
|
let out = "";
|
||||||
|
let inString = false;
|
||||||
|
let escaped = false;
|
||||||
|
for (let i = 0; i < s.length; i++) {
|
||||||
|
const c = s[i]!;
|
||||||
|
if (inString) {
|
||||||
|
if (escaped) {
|
||||||
|
out += c;
|
||||||
|
escaped = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (c === "\\") {
|
||||||
|
out += c;
|
||||||
|
escaped = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (c === '"') {
|
||||||
|
out += c;
|
||||||
|
inString = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (c === "\n") {
|
||||||
|
out += "\\n";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (c === "\r") {
|
||||||
|
out += "\\r";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (c === "\t") {
|
||||||
|
out += "\\t";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
out += c;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (c === '"') {
|
||||||
|
inString = true;
|
||||||
|
out += c;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
out += c;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function convertStructuralSmartQuotes(s: string): string {
|
||||||
|
const text = String(s || "");
|
||||||
|
let out = "";
|
||||||
|
for (let i = 0; i < text.length; i++) {
|
||||||
|
const c = text[i]!;
|
||||||
|
const isOpen = c === "\u201C" || c === "\u201E" || c === "\u201F";
|
||||||
|
const isClose = c === "\u201D" || c === "\u2033" || c === "\u2036";
|
||||||
|
if (!isOpen && !isClose) {
|
||||||
|
out += c;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let prev = i - 1;
|
||||||
|
while (prev >= 0 && /\s/.test(text[prev]!)) prev -= 1;
|
||||||
|
let next = i + 1;
|
||||||
|
while (next < text.length && /\s/.test(text[next]!)) next += 1;
|
||||||
|
const prevCh = prev >= 0 ? text[prev]! : "";
|
||||||
|
const nextCh = next < text.length ? text[next]! : "";
|
||||||
|
const afterOpen = !prevCh || "{[:,[".includes(prevCh);
|
||||||
|
const beforeClose = !nextCh || ":,}]".includes(nextCh);
|
||||||
|
if (isOpen && afterOpen) out += '"';
|
||||||
|
else if (isClose && beforeClose) out += '"';
|
||||||
|
else out += c;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function softenJsonText(s: string): string {
|
||||||
|
let t = String(s || "")
|
||||||
|
.replace(/^\uFEFF/, "")
|
||||||
|
.replace(/\u2026/g, "")
|
||||||
|
.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F]/g, "");
|
||||||
|
t = t.replace(/^\s*\/\/.*$/gm, "");
|
||||||
|
t = t.replace(/\/\*[\s\S]*?\*\//g, "");
|
||||||
|
t = escapeRawControlsInJsonStrings(t);
|
||||||
|
t = t.replace(/,\s*([\]}])/g, "$1");
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function repairTruncatedJsonObject(slice: string): string {
|
||||||
|
let s = String(slice || "").trim();
|
||||||
|
if (!s.startsWith("{")) return s;
|
||||||
|
s = s.replace(/,\s*"[^"]*$/u, "");
|
||||||
|
s = s.replace(/,\s*$/u, "");
|
||||||
|
const opens = (s.match(/\{/g) || []).length;
|
||||||
|
const closes = (s.match(/\}/g) || []).length;
|
||||||
|
const openBrackets = (s.match(/\[/g) || []).length;
|
||||||
|
const closeBrackets = (s.match(/\]/g) || []).length;
|
||||||
|
const quoteCount = (s.match(/(?<!\\)"/g) || []).length;
|
||||||
|
if (quoteCount % 2 === 1) s += '"';
|
||||||
|
if (openBrackets > closeBrackets) s += "]".repeat(openBrackets - closeBrackets);
|
||||||
|
if (opens > closes) s += "}".repeat(opens - closes);
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
function tryParse(s: string): unknown {
|
||||||
|
try {
|
||||||
|
return JSON.parse(s);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const PRODUCT_HINT_KEYS = [
|
||||||
|
"outputs",
|
||||||
|
"schema",
|
||||||
|
"正文",
|
||||||
|
"body",
|
||||||
|
"技能",
|
||||||
|
"workers",
|
||||||
|
"steps",
|
||||||
|
"brief",
|
||||||
|
"追问",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export function looksLikeProductJson(value: unknown): boolean {
|
||||||
|
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
||||||
|
const row = value as Record<string, unknown>;
|
||||||
|
return PRODUCT_HINT_KEYS.some((k) => k in row);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseSlice(slice: string): unknown {
|
||||||
|
return (
|
||||||
|
tryParse(slice) ??
|
||||||
|
tryParse(softenJsonText(slice)) ??
|
||||||
|
tryParse(softenJsonText(convertStructuralSmartQuotes(slice))) ??
|
||||||
|
tryParse(repairTruncatedJsonObject(slice)) ??
|
||||||
|
tryParse(softenJsonText(repairTruncatedJsonObject(slice)))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 思维链末尾常整段 JSON;从后往前找像产物的对象,避免吃进前面的举例 `{ }` */
|
||||||
|
export function extractLastProductJson(text: string): unknown {
|
||||||
|
const s = String(text || "");
|
||||||
|
const starts: number[] = [];
|
||||||
|
for (let i = s.length - 1; i >= 0 && starts.length < 8; i--) {
|
||||||
|
if (s[i] === "{") starts.push(i);
|
||||||
|
}
|
||||||
|
let fallback: unknown = null;
|
||||||
|
for (const start of starts) {
|
||||||
|
const slice = s.slice(start);
|
||||||
|
const end = slice.lastIndexOf("}");
|
||||||
|
if (end < 1) continue;
|
||||||
|
const parsed = parseSlice(slice.slice(0, end + 1));
|
||||||
|
if (parsed == null) continue;
|
||||||
|
if (looksLikeProductJson(parsed)) return parsed;
|
||||||
|
if (fallback == null) fallback = parsed;
|
||||||
|
}
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 模型常把弯引号写进字符串;必须先按原文 parse,再做宽松修复 */
|
||||||
|
export function tryParseJsonDoc(text: string): unknown {
|
||||||
|
const stripped = stripCodeFences(text);
|
||||||
|
const attempts = [
|
||||||
|
stripped,
|
||||||
|
softenJsonText(stripped),
|
||||||
|
softenJsonText(convertStructuralSmartQuotes(stripped)),
|
||||||
|
];
|
||||||
|
for (const candidate of attempts) {
|
||||||
|
const parsed = tryParse(candidate);
|
||||||
|
if (parsed != null) return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cleaned = softenJsonText(convertStructuralSmartQuotes(stripped));
|
||||||
|
if (
|
||||||
|
(cleaned.startsWith('"') && cleaned.endsWith('"')) ||
|
||||||
|
(cleaned.startsWith("'") && cleaned.endsWith("'"))
|
||||||
|
) {
|
||||||
|
const inner = tryParse(cleaned);
|
||||||
|
if (typeof inner === "string") {
|
||||||
|
const parsed = tryParse(softenJsonText(inner)) ?? tryParse(inner);
|
||||||
|
if (parsed != null) return parsed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fromTail = extractLastProductJson(stripped) ?? extractLastProductJson(cleaned);
|
||||||
|
if (fromTail != null) return fromTail;
|
||||||
|
|
||||||
|
const arrStart = cleaned.indexOf("[");
|
||||||
|
const arrEnd = cleaned.lastIndexOf("]");
|
||||||
|
if (arrStart >= 0 && arrEnd > arrStart) {
|
||||||
|
const parsed = tryParse(cleaned.slice(arrStart, arrEnd + 1));
|
||||||
|
if (parsed != null) return parsed;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 正文可能是闲聊/英文收束,JSON 在思维链里。
|
||||||
|
* 能 parse 的一边优先;两边都能 parse 时用更像产物的那边。
|
||||||
|
*/
|
||||||
|
export function selectJsonPayload(content: string, reasoning?: string): string {
|
||||||
|
const body = String(content ?? "").trim();
|
||||||
|
const think = String(reasoning ?? "").trim();
|
||||||
|
const fromBody = body ? tryParseJsonDoc(body) : null;
|
||||||
|
const fromThink = think ? tryParseJsonDoc(think) : null;
|
||||||
|
const bodyOk = fromBody != null && typeof fromBody === "object";
|
||||||
|
const thinkOk = fromThink != null && typeof fromThink === "object";
|
||||||
|
if (bodyOk && thinkOk) {
|
||||||
|
if (looksLikeProductJson(fromThink) && !looksLikeProductJson(fromBody)) {
|
||||||
|
return think;
|
||||||
|
}
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
if (bodyOk) return body;
|
||||||
|
if (thinkOk) return think;
|
||||||
|
return body || think;
|
||||||
|
}
|
||||||
@@ -160,6 +160,9 @@ export class RuntimeOrchestrator {
|
|||||||
case "invoke_main_agent":
|
case "invoke_main_agent":
|
||||||
case "propose_next_creation_step":
|
case "propose_next_creation_step":
|
||||||
case "seal_creation_opening":
|
case "seal_creation_opening":
|
||||||
|
case "unseal_creation_opening":
|
||||||
|
case "run_play_turn":
|
||||||
|
case "continue_play_turn":
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+167
-10
@@ -35,6 +35,11 @@ import {
|
|||||||
SLOT_CREATION_SEALED_BY_OPENING,
|
SLOT_CREATION_SEALED_BY_OPENING,
|
||||||
isOpeningSealArtifact,
|
isOpeningSealArtifact,
|
||||||
} from "../skills/opening-seal.js";
|
} from "../skills/opening-seal.js";
|
||||||
|
import {
|
||||||
|
CREATION_PROPOSED_STEP_TAG,
|
||||||
|
DESIGN_FLOW_WORKER_ID,
|
||||||
|
DESIGN_STEP_WORKER_ID,
|
||||||
|
} from "../skills/creation-flow.js";
|
||||||
import { inferLifecycleStage } from "../skills/worker-declaration.js";
|
import { inferLifecycleStage } from "../skills/worker-declaration.js";
|
||||||
import type { QuestionItem } from "../types/questions.js";
|
import type { QuestionItem } from "../types/questions.js";
|
||||||
|
|
||||||
@@ -146,6 +151,54 @@ function running(session: RuntimeSession): RuntimeSession {
|
|||||||
return touch({ ...session, phase: "running", waitingReason: undefined });
|
return touch({ ...session, phase: "running", waitingReason: undefined });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 回到流程编排:保留已验收步骤,让 design-flow 追加/改排节点 */
|
||||||
|
function applyFlowReplan(
|
||||||
|
session: RuntimeSession,
|
||||||
|
event: RuntimeEvent,
|
||||||
|
reason?: string,
|
||||||
|
): ApplyEventResult {
|
||||||
|
const slots = { ...session.slots };
|
||||||
|
const note = reason?.trim() ?? "";
|
||||||
|
if (note) {
|
||||||
|
slots["用户.下一步意向"] = note;
|
||||||
|
slots["用户.修订说明"] = note;
|
||||||
|
}
|
||||||
|
delete slots[CREATION_PROPOSED_STEP_TAG];
|
||||||
|
delete slots[SLOT_CREATION_SEALED_BY_OPENING];
|
||||||
|
|
||||||
|
let artifacts = session.artifacts;
|
||||||
|
if (session.pendingArtifactId) {
|
||||||
|
artifacts = session.artifacts.map((a) =>
|
||||||
|
a.id === session.pendingArtifactId
|
||||||
|
? { ...a, status: "rejected" as const, updatedAt: nowIso() }
|
||||||
|
: a,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
session: touch(
|
||||||
|
running(
|
||||||
|
appendHistory(
|
||||||
|
{
|
||||||
|
...session,
|
||||||
|
slots,
|
||||||
|
artifacts,
|
||||||
|
pendingDecision: undefined,
|
||||||
|
pendingArtifactId: undefined,
|
||||||
|
currentWorkerId: undefined,
|
||||||
|
resumeContext: undefined,
|
||||||
|
},
|
||||||
|
event,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
effects: [
|
||||||
|
{ type: "unseal_creation_opening" },
|
||||||
|
{ type: "run_worker", workerId: DESIGN_FLOW_WORKER_ID },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/** 游玩连跑下一执行单元;创作则交总管 */
|
/** 游玩连跑下一执行单元;创作则交总管 */
|
||||||
function afterPlayOrMainAgent(
|
function afterPlayOrMainAgent(
|
||||||
session: RuntimeSession,
|
session: RuntimeSession,
|
||||||
@@ -286,6 +339,7 @@ export function getAllowedEvents(
|
|||||||
case "running":
|
case "running":
|
||||||
return [
|
return [
|
||||||
"main_agent_decision_created",
|
"main_agent_decision_created",
|
||||||
|
"creation_step_pick_awaited",
|
||||||
"worker_started",
|
"worker_started",
|
||||||
"worker_completed",
|
"worker_completed",
|
||||||
"worker_needs_input",
|
"worker_needs_input",
|
||||||
@@ -303,6 +357,9 @@ export function getAllowedEvents(
|
|||||||
"user_confirmed_intake",
|
"user_confirmed_intake",
|
||||||
"user_approved_next_step",
|
"user_approved_next_step",
|
||||||
"user_rejected_next_step",
|
"user_rejected_next_step",
|
||||||
|
"user_requested_flow_replan",
|
||||||
|
"user_picked_creation_step",
|
||||||
|
"user_left_creation_step",
|
||||||
"user_accepted_artifact",
|
"user_accepted_artifact",
|
||||||
"user_rejected_artifact",
|
"user_rejected_artifact",
|
||||||
"user_requested_revision",
|
"user_requested_revision",
|
||||||
@@ -341,13 +398,34 @@ export function canApplyEvent(
|
|||||||
reason?.kind === "input" ||
|
reason?.kind === "input" ||
|
||||||
reason?.kind === "worker_questions" ||
|
reason?.kind === "worker_questions" ||
|
||||||
reason?.kind === "revision" ||
|
reason?.kind === "revision" ||
|
||||||
reason?.kind === "next_intent"
|
reason?.kind === "next_intent" ||
|
||||||
|
reason?.kind === "pick_creation_step"
|
||||||
);
|
);
|
||||||
case "user_confirmed_intake":
|
case "user_confirmed_intake":
|
||||||
return reason?.kind === "intake";
|
return reason?.kind === "intake";
|
||||||
case "user_approved_next_step":
|
case "user_approved_next_step":
|
||||||
case "user_rejected_next_step":
|
case "user_rejected_next_step":
|
||||||
return reason?.kind === "approve_step";
|
return reason?.kind === "approve_step";
|
||||||
|
case "user_requested_flow_replan":
|
||||||
|
return (
|
||||||
|
reason?.kind === "approve_step" ||
|
||||||
|
reason?.kind === "review_artifact" ||
|
||||||
|
reason?.kind === "input" ||
|
||||||
|
reason?.kind === "worker_questions" ||
|
||||||
|
reason?.kind === "revision" ||
|
||||||
|
reason?.kind === "pick_creation_step"
|
||||||
|
);
|
||||||
|
case "user_picked_creation_step":
|
||||||
|
return reason?.kind === "pick_creation_step";
|
||||||
|
case "user_left_creation_step":
|
||||||
|
return (
|
||||||
|
reason?.kind === "approve_step" ||
|
||||||
|
reason?.kind === "worker_questions" ||
|
||||||
|
reason?.kind === "input" ||
|
||||||
|
reason?.kind === "revision"
|
||||||
|
);
|
||||||
|
case "creation_step_pick_awaited":
|
||||||
|
return session.phase === "running";
|
||||||
case "user_accepted_artifact":
|
case "user_accepted_artifact":
|
||||||
case "user_rejected_artifact":
|
case "user_rejected_artifact":
|
||||||
return reason?.kind === "review_artifact";
|
return reason?.kind === "review_artifact";
|
||||||
@@ -361,8 +439,12 @@ export function canApplyEvent(
|
|||||||
reason?.kind === "review_artifact" || reason?.kind === "approve_step"
|
reason?.kind === "review_artifact" || reason?.kind === "approve_step"
|
||||||
);
|
);
|
||||||
case "main_agent_decision_created":
|
case "main_agent_decision_created":
|
||||||
// running 时可决策;waiting_user(input) 时允许总管在启动阶段插话
|
// running 时可决策;waiting_user(input/pick) 时允许总管在启动阶段插话或点选后补参确认
|
||||||
return session.phase === "running" || reason?.kind === "input";
|
return (
|
||||||
|
session.phase === "running" ||
|
||||||
|
reason?.kind === "input" ||
|
||||||
|
reason?.kind === "pick_creation_step"
|
||||||
|
);
|
||||||
default:
|
default:
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -420,13 +502,20 @@ function handleWorkerCompleted(
|
|||||||
|
|
||||||
if (mode === "no_confirmation") {
|
if (mode === "no_confirmation") {
|
||||||
next = updateArtifact(next, artifact.id, { status: "accepted" });
|
next = updateArtifact(next, artifact.id, { status: "accepted" });
|
||||||
return afterPlayOrMainAgent(session, {
|
const cleared = {
|
||||||
...next,
|
...next,
|
||||||
pendingArtifactId: undefined,
|
pendingArtifactId: undefined,
|
||||||
currentWorkerId: undefined,
|
currentWorkerId: undefined,
|
||||||
pendingDecision: undefined,
|
pendingDecision: undefined,
|
||||||
resumeContext: undefined,
|
resumeContext: undefined,
|
||||||
});
|
};
|
||||||
|
if (artifact.workerId === DESIGN_FLOW_WORKER_ID) {
|
||||||
|
return {
|
||||||
|
session: touch(running(cleared)),
|
||||||
|
effects: [{ type: "propose_next_creation_step" }],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return afterPlayOrMainAgent(session, cleared);
|
||||||
}
|
}
|
||||||
|
|
||||||
// programmatic_review
|
// programmatic_review
|
||||||
@@ -618,6 +707,10 @@ export function applyEvent(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (session.waitingReason?.kind === "pick_creation_step") {
|
||||||
|
return applyFlowReplan(session, event, text);
|
||||||
|
}
|
||||||
|
|
||||||
if (session.waitingReason?.kind === "revision") {
|
if (session.waitingReason?.kind === "revision") {
|
||||||
const instruction =
|
const instruction =
|
||||||
text.trim() || session.waitingReason.instruction?.trim() || "";
|
text.trim() || session.waitingReason.instruction?.trim() || "";
|
||||||
@@ -836,16 +929,81 @@ export function applyEvent(
|
|||||||
}
|
}
|
||||||
|
|
||||||
case "user_rejected_next_step": {
|
case "user_rejected_next_step": {
|
||||||
|
return applyFlowReplan(session, event, event.payload.reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
case "user_requested_flow_replan": {
|
||||||
|
return applyFlowReplan(session, event, event.payload.reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
case "creation_step_pick_awaited": {
|
||||||
|
return {
|
||||||
|
session: waiting(
|
||||||
|
touch(
|
||||||
|
appendHistory(
|
||||||
|
{
|
||||||
|
...session,
|
||||||
|
currentWorkerId: undefined,
|
||||||
|
pendingDecision: undefined,
|
||||||
|
},
|
||||||
|
event,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
{ kind: "pick_creation_step" },
|
||||||
|
),
|
||||||
|
effects: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
case "user_left_creation_step": {
|
||||||
|
const slots = { ...session.slots };
|
||||||
|
delete slots[CREATION_PROPOSED_STEP_TAG];
|
||||||
|
return {
|
||||||
|
session: waiting(
|
||||||
|
touch(
|
||||||
|
appendHistory(
|
||||||
|
{
|
||||||
|
...session,
|
||||||
|
slots,
|
||||||
|
pendingDecision: undefined,
|
||||||
|
pendingArtifactId: undefined,
|
||||||
|
currentWorkerId: undefined,
|
||||||
|
currentStepId: undefined,
|
||||||
|
resumeContext: undefined,
|
||||||
|
},
|
||||||
|
event,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
{ kind: "pick_creation_step" },
|
||||||
|
),
|
||||||
|
effects: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
case "user_picked_creation_step": {
|
||||||
|
const stepId = event.payload.stepId.trim();
|
||||||
|
if (!stepId) {
|
||||||
|
return fail(session, "pick_creation_step requires stepId");
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
session: touch(
|
session: touch(
|
||||||
running(
|
running(
|
||||||
appendHistory(
|
appendHistory(
|
||||||
{ ...session, pendingDecision: undefined },
|
{
|
||||||
|
...session,
|
||||||
|
currentWorkerId: DESIGN_STEP_WORKER_ID,
|
||||||
|
pendingDecision: undefined,
|
||||||
|
},
|
||||||
event,
|
event,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
effects: [{ type: "invoke_main_agent" }],
|
effects: [
|
||||||
|
{
|
||||||
|
type: "run_worker",
|
||||||
|
workerId: DESIGN_STEP_WORKER_ID,
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1033,9 +1191,8 @@ export function applyEvent(
|
|||||||
if (inferLifecycleStage(session) === "play") {
|
if (inferLifecycleStage(session) === "play") {
|
||||||
return afterPlayOrMainAgent(session, cleared);
|
return afterPlayOrMainAgent(session, cleared);
|
||||||
}
|
}
|
||||||
// 创作步验收后直接提案下一步(有待执行步则确认开干;否则交总管扩步)。
|
// 创作步验收后停在分层图上等点选(可反复能力可当场新开一条)。
|
||||||
// 不再先停在「下一步想写什么」——与「同意并开始」叠成双重确认。
|
// 仅当没有任何可点节点时,propose 才会把控制权交回总管扩步。
|
||||||
// 细化终稿也走提案,而不是 invoke_main_agent(否则容易把刚收下的终稿再跑一遍)。
|
|
||||||
const proposeNext =
|
const proposeNext =
|
||||||
artifact.workerId === "design-step" ||
|
artifact.workerId === "design-step" ||
|
||||||
artifact.workerId === "design-flow";
|
artifact.workerId === "design-flow";
|
||||||
|
|||||||
+626
-127
@@ -17,7 +17,11 @@ import {
|
|||||||
} from "./phase-machine.js";
|
} from "./phase-machine.js";
|
||||||
import { Blackboard } from "../blackboard/blackboard.js";
|
import { Blackboard } from "../blackboard/blackboard.js";
|
||||||
import type { LlmProvider } from "../llm/client.js";
|
import type { LlmProvider } from "../llm/client.js";
|
||||||
import { DEFAULT_WORKERS, MainAgent } from "../main-agent/main-agent.js";
|
import {
|
||||||
|
DEFAULT_WORKERS,
|
||||||
|
isUnusableMainAgentOutput,
|
||||||
|
MainAgent,
|
||||||
|
} from "../main-agent/main-agent.js";
|
||||||
import { listSkills, listWorkerSkills, loadSkill, resolveSkillId } from "../skills/loader.js";
|
import { listSkills, listWorkerSkills, loadSkill, resolveSkillId } from "../skills/loader.js";
|
||||||
import {
|
import {
|
||||||
buildInstanceWorkerDeclaration,
|
buildInstanceWorkerDeclaration,
|
||||||
@@ -58,14 +62,32 @@ import {
|
|||||||
CREATION_FLOW_TAG,
|
CREATION_FLOW_TAG,
|
||||||
CREATION_CURRENT_STEP_TAG,
|
CREATION_CURRENT_STEP_TAG,
|
||||||
CREATION_PROPOSED_STEP_TAG,
|
CREATION_PROPOSED_STEP_TAG,
|
||||||
|
CREATION_INHERIT_EXISTING_TAG,
|
||||||
CREATION_MODULE_OPENING_TAG,
|
CREATION_MODULE_OPENING_TAG,
|
||||||
CREATION_MODULE_OPENING_STATE_TAG,
|
CREATION_MODULE_OPENING_STATE_TAG,
|
||||||
SLOT_CREATION_MODULE_OPENING_STATE,
|
SLOT_CREATION_MODULE_OPENING_STATE,
|
||||||
loadModuleCatalog,
|
loadModuleCatalog,
|
||||||
isReviseStep,
|
isReviseStep,
|
||||||
|
isStepAccepted,
|
||||||
|
isInheritExistingFlag,
|
||||||
mergeCreationFlowPreservingAccepted,
|
mergeCreationFlowPreservingAccepted,
|
||||||
nextPendingStep,
|
nextPendingStep,
|
||||||
|
listReadySteps,
|
||||||
|
listSelectablePrototypes,
|
||||||
|
listRepeatableSpawns,
|
||||||
|
isStepReady,
|
||||||
|
hasSelectableCreationWork,
|
||||||
|
spawnRepeatableCreationStep,
|
||||||
|
spawnInstanceFromPrototype,
|
||||||
|
removeUnstartedInstance,
|
||||||
|
isPrototypeStep,
|
||||||
|
isPrototypeSelectable,
|
||||||
|
isInstanceStep,
|
||||||
|
findStepByRef,
|
||||||
|
findModuleByName,
|
||||||
parseCreationFlow,
|
parseCreationFlow,
|
||||||
|
DESIGN_FLOW_WORKER_ID,
|
||||||
|
DESIGN_STEP_WORKER_ID,
|
||||||
parseModuleOpeningState,
|
parseModuleOpeningState,
|
||||||
patchCreationFlowStepParams,
|
patchCreationFlowStepParams,
|
||||||
pickRecordedStepId,
|
pickRecordedStepId,
|
||||||
@@ -76,7 +98,10 @@ import {
|
|||||||
stepUnitId,
|
stepUnitId,
|
||||||
stringifyCreationFlow,
|
stringifyCreationFlow,
|
||||||
stringifyModuleOpeningState,
|
stringifyModuleOpeningState,
|
||||||
|
finalizeCreationFlow,
|
||||||
|
type CreationFlow,
|
||||||
type CreationFlowStepParams,
|
type CreationFlowStepParams,
|
||||||
|
type ModuleCatalog,
|
||||||
type ModuleCatalogEntry,
|
type ModuleCatalogEntry,
|
||||||
type ModuleParamSpec,
|
type ModuleParamSpec,
|
||||||
} from "../skills/creation-flow.js";
|
} from "../skills/creation-flow.js";
|
||||||
@@ -98,6 +123,7 @@ import {
|
|||||||
OPENING_OUTPUT_TAG,
|
OPENING_OUTPUT_TAG,
|
||||||
OPENING_SETUP_ARTIFACT_TAG,
|
OPENING_SETUP_ARTIFACT_TAG,
|
||||||
parseOpeningSealPayload,
|
parseOpeningSealPayload,
|
||||||
|
reopenCreationFlowRaw,
|
||||||
SLOT_CREATION_SEALED_BY_OPENING,
|
SLOT_CREATION_SEALED_BY_OPENING,
|
||||||
SLOT_OPENING_SELECTED_INDEX,
|
SLOT_OPENING_SELECTED_INDEX,
|
||||||
} from "../skills/opening-seal.js";
|
} from "../skills/opening-seal.js";
|
||||||
@@ -326,6 +352,56 @@ export class PhaseRuntime {
|
|||||||
throw new Error("当前没有可重试的执行");
|
throw new Error("当前没有可重试的执行");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** LLM/解析失败后离开 running,避免界面一直转圈。 */
|
||||||
|
async failRun(reason: string): Promise<RuntimeSession> {
|
||||||
|
if (this.session.phase !== "running") return this.session;
|
||||||
|
const result = this.commitEvent({
|
||||||
|
type: "runtime_failed",
|
||||||
|
payload: { reason },
|
||||||
|
});
|
||||||
|
await this.processEffects(result.effects);
|
||||||
|
return this.session;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 进程里留下的「执行中、没 worker」:配方已预置 DAG 时改回点选等待。
|
||||||
|
* 读档 / 轮询时调用,避免界面一直转圈。
|
||||||
|
*/
|
||||||
|
recoverOrphanedRun(): boolean {
|
||||||
|
if (this.session.phase !== "running" || this.session.waitingReason) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (this.session.currentWorkerId) return false;
|
||||||
|
if (!this.hasCreationFlow()) return false;
|
||||||
|
debugLog("step", "恢复中断的调度,等待点选节点");
|
||||||
|
this.clearProposedStep();
|
||||||
|
this.session = {
|
||||||
|
...this.session,
|
||||||
|
phase: "waiting_user",
|
||||||
|
waitingReason: { kind: "pick_creation_step" },
|
||||||
|
pendingDecision: undefined,
|
||||||
|
currentWorkerId: undefined,
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 配方已预置 DAG:跳过「先描述需求」,直接进入点选图。
|
||||||
|
* 图上节点来自当前剧本 seed,换配方即换开局。
|
||||||
|
*/
|
||||||
|
async enterSeededCreationPick(): Promise<void> {
|
||||||
|
if (!this.hasCreationFlow()) return;
|
||||||
|
this.session = {
|
||||||
|
...this.session,
|
||||||
|
slots: {
|
||||||
|
...this.session.slots,
|
||||||
|
startupCompleted: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
await this.awaitCreationStepPick();
|
||||||
|
}
|
||||||
|
|
||||||
/** 重 roll 指定 worker(刷新 Skill 回复) */
|
/** 重 roll 指定 worker(刷新 Skill 回复) */
|
||||||
async rerunWorker(workerId: string): Promise<RuntimeSession> {
|
async rerunWorker(workerId: string): Promise<RuntimeSession> {
|
||||||
const effect: Extract<PhaseEffect, { type: "run_worker" }> = {
|
const effect: Extract<PhaseEffect, { type: "run_worker" }> = {
|
||||||
@@ -595,7 +671,7 @@ export class PhaseRuntime {
|
|||||||
return this.session;
|
return this.session;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 用户拒绝 pendingDecision,回到总管 */
|
/** 用户拒绝 pendingDecision:回到流程编排,可追加节点 */
|
||||||
async rejectStep(reason?: string): Promise<RuntimeSession> {
|
async rejectStep(reason?: string): Promise<RuntimeSession> {
|
||||||
const id = this.session.pendingDecision?.id;
|
const id = this.session.pendingDecision?.id;
|
||||||
if (!id) throw new Error("当前没有待拒绝的决策");
|
if (!id) throw new Error("当前没有待拒绝的决策");
|
||||||
@@ -606,6 +682,180 @@ export class PhaseRuntime {
|
|||||||
return this.session;
|
return this.session;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 离开当前技能步,回到分层图。
|
||||||
|
* 可增殖节点刚增殖出、尚未验收的空实例从图上撤掉。
|
||||||
|
*/
|
||||||
|
async leaveCreationStep(): Promise<RuntimeSession> {
|
||||||
|
const waiting = this.session.waitingReason?.kind;
|
||||||
|
const allowed =
|
||||||
|
waiting === "worker_questions" ||
|
||||||
|
waiting === "input" ||
|
||||||
|
waiting === "revision" ||
|
||||||
|
waiting === "approve_step";
|
||||||
|
if (!allowed) {
|
||||||
|
throw new Error("当前不能返回节点选择");
|
||||||
|
}
|
||||||
|
if (inferLifecycleStage(this.session) === "play") {
|
||||||
|
throw new Error("游玩中不能返回创作节点");
|
||||||
|
}
|
||||||
|
const stepId = this.readPinnedStepId();
|
||||||
|
const removed = this.unspawnCurrentInstanceIfEmpty();
|
||||||
|
this.clearProposedStep();
|
||||||
|
this.clearCurrentStepPointers();
|
||||||
|
const result = await this.dispatch({
|
||||||
|
type: "user_left_creation_step",
|
||||||
|
payload: {},
|
||||||
|
});
|
||||||
|
if (result.error) {
|
||||||
|
throw new Error(result.error);
|
||||||
|
}
|
||||||
|
const stepName = removed?.name?.trim();
|
||||||
|
this.onMessage(
|
||||||
|
removed
|
||||||
|
? `已取消刚增殖的「${stepName || stepId}」,回到节点选择。`
|
||||||
|
: "已回到节点选择。",
|
||||||
|
);
|
||||||
|
return this.session;
|
||||||
|
}
|
||||||
|
|
||||||
|
private unspawnCurrentInstanceIfEmpty(): { id: string; name: string } | null {
|
||||||
|
const stepId = this.readPinnedStepId();
|
||||||
|
if (!stepId) return null;
|
||||||
|
const flow = parseCreationFlow(
|
||||||
|
this.blackboard.getContentByTag(CREATION_FLOW_TAG),
|
||||||
|
);
|
||||||
|
if (!flow) return null;
|
||||||
|
const step = findStepByRef(flow, stepId);
|
||||||
|
if (!step || !isInstanceStep(step)) return null;
|
||||||
|
const accepted = this.readAcceptedStepIds();
|
||||||
|
if (isStepAccepted(step, accepted)) return null;
|
||||||
|
if (
|
||||||
|
this.session.artifacts.some(
|
||||||
|
(a) => a.stepId === step.id && a.status !== "rejected",
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const next = removeUnstartedInstance(flow, step.id, accepted);
|
||||||
|
if (next.steps.length === flow.steps.length) return null;
|
||||||
|
this.writeCreationFlow(next);
|
||||||
|
debugLog("step", `取消未完成实例 ${step.id}`);
|
||||||
|
return { id: step.id, name: step.name };
|
||||||
|
}
|
||||||
|
|
||||||
|
private clearCurrentStepPointers(): void {
|
||||||
|
this.blackboard.write({
|
||||||
|
tag: CREATION_CURRENT_STEP_TAG,
|
||||||
|
content: "",
|
||||||
|
source: "runtime",
|
||||||
|
});
|
||||||
|
this.blackboard.write({
|
||||||
|
tag: CREATION_INHERIT_EXISTING_TAG,
|
||||||
|
content: "",
|
||||||
|
source: "runtime",
|
||||||
|
});
|
||||||
|
this.session = {
|
||||||
|
...this.session,
|
||||||
|
slots: {
|
||||||
|
...this.session.slots,
|
||||||
|
[CREATION_CURRENT_STEP_TAG]: undefined,
|
||||||
|
[CREATION_INHERIT_EXISTING_TAG]: undefined,
|
||||||
|
[SLOT_CREATION_CURRENT_UNIT]: undefined,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 开场白等步骤上发现漏了前序节点:再进流程编排,不整局重开 */
|
||||||
|
async replanCreationFlow(reason?: string): Promise<RuntimeSession> {
|
||||||
|
const waiting = this.session.waitingReason?.kind;
|
||||||
|
if (waiting === "approve_step") {
|
||||||
|
return this.rejectStep(reason);
|
||||||
|
}
|
||||||
|
await this.dispatch({
|
||||||
|
type: "user_requested_flow_replan",
|
||||||
|
payload: { reason },
|
||||||
|
});
|
||||||
|
return this.session;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 图上点选:普通节点开干;原型槽位先增殖再进去;已完成节点可 reenter 重进 */
|
||||||
|
async pickCreationStep(
|
||||||
|
stepId: string,
|
||||||
|
stepParams?: CreationFlowStepParams,
|
||||||
|
opts?: { reenter?: boolean },
|
||||||
|
): Promise<RuntimeSession> {
|
||||||
|
const id = stepId.trim();
|
||||||
|
this.syncSlotsToBlackboard(this.session);
|
||||||
|
const catalog = await this.loadActiveModuleCatalog();
|
||||||
|
const flow = this.readNormalizedFlow(catalog);
|
||||||
|
const step = flow ? findStepByRef(flow, id) : null;
|
||||||
|
if (flow && step && isPrototypeStep(step)) {
|
||||||
|
if (opts?.reenter) {
|
||||||
|
throw new Error("原型槽位不能回头修改,请点已完成的实例");
|
||||||
|
}
|
||||||
|
const spawned = spawnInstanceFromPrototype({
|
||||||
|
flow,
|
||||||
|
catalog,
|
||||||
|
prototypeId: step.id,
|
||||||
|
acceptedStepIds: this.readAcceptedStepIds(),
|
||||||
|
});
|
||||||
|
if ("error" in spawned) throw new Error(spawned.error);
|
||||||
|
this.writeCreationFlow(spawned.flow);
|
||||||
|
debugLog("step", `点原型 ${step.id} → ${spawned.step.id}`);
|
||||||
|
await this.startCreationStep(spawned.step.id, { stepParams });
|
||||||
|
return this.session;
|
||||||
|
}
|
||||||
|
await this.startCreationStep(id, {
|
||||||
|
stepParams,
|
||||||
|
reenter: opts?.reenter === true,
|
||||||
|
});
|
||||||
|
return this.session;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 可反复能力:新编一条再进去 */
|
||||||
|
async spawnCreationStep(moduleName: string): Promise<RuntimeSession> {
|
||||||
|
this.syncSlotsToBlackboard(this.session);
|
||||||
|
const catalog = await this.loadActiveModuleCatalog();
|
||||||
|
const flow = this.readNormalizedFlow(catalog);
|
||||||
|
if (!flow) throw new Error("还没有工作流计划,无法追加节点");
|
||||||
|
const spawned = spawnRepeatableCreationStep({
|
||||||
|
flow,
|
||||||
|
catalog,
|
||||||
|
moduleName,
|
||||||
|
acceptedStepIds: this.readAcceptedStepIds(),
|
||||||
|
});
|
||||||
|
if ("error" in spawned) throw new Error(spawned.error);
|
||||||
|
this.writeCreationFlow(spawned.flow);
|
||||||
|
debugLog("step", `新开 ${spawned.step.id}(${spawned.step.name})`);
|
||||||
|
await this.startCreationStep(spawned.step.id);
|
||||||
|
return this.session;
|
||||||
|
}
|
||||||
|
|
||||||
|
private writeCreationFlow(flow: CreationFlow): void {
|
||||||
|
const content = stringifyCreationFlow(flow);
|
||||||
|
this.blackboard.write({
|
||||||
|
tag: CREATION_FLOW_TAG,
|
||||||
|
content,
|
||||||
|
source: "user",
|
||||||
|
});
|
||||||
|
this.session = {
|
||||||
|
...this.session,
|
||||||
|
slots: { ...this.session.slots, [CREATION_FLOW_TAG]: content },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private readNormalizedFlow(catalog: ModuleCatalog | null): CreationFlow | null {
|
||||||
|
const flow = parseCreationFlow(
|
||||||
|
this.blackboard.getContentByTag(CREATION_FLOW_TAG),
|
||||||
|
);
|
||||||
|
if (!flow) return null;
|
||||||
|
const next = finalizeCreationFlow(flow, catalog);
|
||||||
|
const changed = JSON.stringify(next.steps) !== JSON.stringify(flow.steps);
|
||||||
|
if (changed) this.writeCreationFlow(next);
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
/** 将实例 Worker 声明写入 slots,便于 session 持久化与调试 */
|
/** 将实例 Worker 声明写入 slots,便于 session 持久化与调试 */
|
||||||
private persistInstanceWorkerDeclaration(): void {
|
private persistInstanceWorkerDeclaration(): void {
|
||||||
const active = this.getActiveSkill();
|
const active = this.getActiveSkill();
|
||||||
@@ -857,6 +1107,13 @@ export class PhaseRuntime {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "run_worker":
|
case "run_worker":
|
||||||
|
if (
|
||||||
|
effect.workerId === DESIGN_STEP_WORKER_ID &&
|
||||||
|
!this.hasPinnedReadyStep()
|
||||||
|
) {
|
||||||
|
await this.awaitCreationStepPick();
|
||||||
|
break;
|
||||||
|
}
|
||||||
if (this.autoStubWorker) {
|
if (this.autoStubWorker) {
|
||||||
await this.runStubWorker(effect);
|
await this.runStubWorker(effect);
|
||||||
} else if (this.llm) {
|
} else if (this.llm) {
|
||||||
@@ -894,6 +1151,9 @@ export class PhaseRuntime {
|
|||||||
case "seal_creation_opening":
|
case "seal_creation_opening":
|
||||||
this.sealCreationOpening();
|
this.sealCreationOpening();
|
||||||
break;
|
break;
|
||||||
|
case "unseal_creation_opening":
|
||||||
|
this.unsealCreationOpening();
|
||||||
|
break;
|
||||||
case "run_play_turn":
|
case "run_play_turn":
|
||||||
await this.startPlayTurn();
|
await this.startPlayTurn();
|
||||||
break;
|
break;
|
||||||
@@ -905,9 +1165,243 @@ export class PhaseRuntime {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 用户填完「下一步想写什么」后:有待执行步则提案并进入确认;否则交总管扩步/收口。
|
* 用户填完「下一步想写什么」后:有可点节点则停在分层图;程序步自动开干;否则交总管扩步。
|
||||||
*/
|
*/
|
||||||
private async proposeNextCreationStep(): Promise<void> {
|
private async proposeNextCreationStep(): Promise<void> {
|
||||||
|
await this.awaitCreationStepPick();
|
||||||
|
}
|
||||||
|
|
||||||
|
private readAcceptedStepIds(): string[] {
|
||||||
|
return parseAcceptedUnits(
|
||||||
|
this.session.slots[SLOT_CREATION_ACCEPTED_UNITS] ??
|
||||||
|
this.blackboard.getContentByTag(CREATION_ACCEPTED_UNITS_TAG),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async loadActiveModuleCatalog(): Promise<ModuleCatalog | null> {
|
||||||
|
const active = this.getActiveSkill();
|
||||||
|
if (!active?.name) return null;
|
||||||
|
try {
|
||||||
|
const skill = await loadSkill(active.name);
|
||||||
|
if (!skill.skillPackRoot) return null;
|
||||||
|
return await loadModuleCatalog(skill.skillPackRoot);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private readPinnedStepId(): string {
|
||||||
|
const fromCurrent = this.blackboard
|
||||||
|
.getContentByTag(CREATION_CURRENT_STEP_TAG)
|
||||||
|
?.trim();
|
||||||
|
if (fromCurrent) return fromCurrent;
|
||||||
|
const raw =
|
||||||
|
(typeof this.session.slots[CREATION_PROPOSED_STEP_TAG] === "string"
|
||||||
|
? String(this.session.slots[CREATION_PROPOSED_STEP_TAG])
|
||||||
|
: null) || this.blackboard.getContentByTag(CREATION_PROPOSED_STEP_TAG);
|
||||||
|
if (!raw?.trim()) return "";
|
||||||
|
try {
|
||||||
|
const doc = JSON.parse(raw) as { stepId?: unknown };
|
||||||
|
return typeof doc.stepId === "string" ? doc.stepId.trim() : "";
|
||||||
|
} catch {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private hasPinnedReadyStep(): boolean {
|
||||||
|
const flow = parseCreationFlow(
|
||||||
|
this.blackboard.getContentByTag(CREATION_FLOW_TAG),
|
||||||
|
);
|
||||||
|
if (!flow) return false;
|
||||||
|
const step = findStepByRef(flow, this.readPinnedStepId());
|
||||||
|
if (!step) return false;
|
||||||
|
return isStepReady(flow, step, this.readAcceptedStepIds());
|
||||||
|
}
|
||||||
|
|
||||||
|
private pinCreationStepSnapshot(params: {
|
||||||
|
stepId: string;
|
||||||
|
name: string;
|
||||||
|
declaration: string;
|
||||||
|
params: Record<string, unknown>;
|
||||||
|
paramsMissing: string[];
|
||||||
|
paramSpecs: ModuleParamSpec[];
|
||||||
|
kind?: ModuleCatalogEntry["kind"];
|
||||||
|
closer?: boolean;
|
||||||
|
revise: boolean;
|
||||||
|
revises?: string;
|
||||||
|
}): void {
|
||||||
|
const intent =
|
||||||
|
typeof this.session.slots["用户.下一步意向"] === "string"
|
||||||
|
? String(this.session.slots["用户.下一步意向"]).trim()
|
||||||
|
: "";
|
||||||
|
const snapshot = {
|
||||||
|
stepId: params.stepId,
|
||||||
|
name: params.name,
|
||||||
|
declaration: params.declaration || undefined,
|
||||||
|
params: params.params,
|
||||||
|
paramsMissing: params.paramsMissing,
|
||||||
|
paramSpecs: params.paramSpecs.map((p) => ({
|
||||||
|
key: p.key,
|
||||||
|
label: p.label,
|
||||||
|
required: Boolean(p.required),
|
||||||
|
hint: p.hint,
|
||||||
|
})),
|
||||||
|
intent: intent || undefined,
|
||||||
|
...(params.kind ? { kind: params.kind } : {}),
|
||||||
|
...(params.closer ? { closer: true } : {}),
|
||||||
|
...(params.revise ? { mode: "revise" as const } : {}),
|
||||||
|
...(params.revises ? { revises: params.revises } : {}),
|
||||||
|
};
|
||||||
|
this.blackboard.write({
|
||||||
|
tag: CREATION_PROPOSED_STEP_TAG,
|
||||||
|
content: JSON.stringify(snapshot),
|
||||||
|
source: "runtime",
|
||||||
|
});
|
||||||
|
this.blackboard.write({
|
||||||
|
tag: CREATION_CURRENT_STEP_TAG,
|
||||||
|
content: params.stepId,
|
||||||
|
source: "runtime",
|
||||||
|
});
|
||||||
|
this.blackboard.write({
|
||||||
|
tag: CREATION_INHERIT_EXISTING_TAG,
|
||||||
|
content: params.revise ? "1" : "",
|
||||||
|
source: "runtime",
|
||||||
|
});
|
||||||
|
this.session = {
|
||||||
|
...this.session,
|
||||||
|
slots: {
|
||||||
|
...this.session.slots,
|
||||||
|
[CREATION_PROPOSED_STEP_TAG]: JSON.stringify(snapshot),
|
||||||
|
[CREATION_CURRENT_STEP_TAG]: params.stepId,
|
||||||
|
[CREATION_INHERIT_EXISTING_TAG]: params.revise ? "1" : "",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async startCreationStep(
|
||||||
|
stepId: string,
|
||||||
|
opts?: { stepParams?: CreationFlowStepParams; reenter?: boolean },
|
||||||
|
): Promise<void> {
|
||||||
|
this.syncSlotsToBlackboard(this.session);
|
||||||
|
let flow = parseCreationFlow(
|
||||||
|
this.blackboard.getContentByTag(CREATION_FLOW_TAG),
|
||||||
|
);
|
||||||
|
if (!flow) throw new Error("还没有工作流计划");
|
||||||
|
let step = findStepByRef(flow, stepId);
|
||||||
|
if (!step) throw new Error(`找不到节点:${stepId}`);
|
||||||
|
const accepted = this.readAcceptedStepIds();
|
||||||
|
const catalog = await this.loadActiveModuleCatalog();
|
||||||
|
const reenter = opts?.reenter === true;
|
||||||
|
|
||||||
|
if (isPrototypeStep(step)) {
|
||||||
|
if (!isPrototypeSelectable(flow, step, accepted)) {
|
||||||
|
throw new Error(`「${step.name}」的前置还没齐,不能增殖`);
|
||||||
|
}
|
||||||
|
const spawned = spawnInstanceFromPrototype({
|
||||||
|
flow,
|
||||||
|
catalog,
|
||||||
|
prototypeId: step.id,
|
||||||
|
acceptedStepIds: accepted,
|
||||||
|
});
|
||||||
|
if ("error" in spawned) throw new Error(spawned.error);
|
||||||
|
const content = stringifyCreationFlow(spawned.flow);
|
||||||
|
this.blackboard.write({
|
||||||
|
tag: CREATION_FLOW_TAG,
|
||||||
|
content,
|
||||||
|
source: "user",
|
||||||
|
});
|
||||||
|
this.session = {
|
||||||
|
...this.session,
|
||||||
|
slots: { ...this.session.slots, [CREATION_FLOW_TAG]: content },
|
||||||
|
};
|
||||||
|
flow = spawned.flow;
|
||||||
|
step = spawned.step;
|
||||||
|
} else if (reenter) {
|
||||||
|
if (!isStepAccepted(step, accepted)) {
|
||||||
|
throw new Error(`「${step.name}」还没完成,请直接进入而不是回头修改`);
|
||||||
|
}
|
||||||
|
} else if (!isStepReady(flow, step, accepted)) {
|
||||||
|
throw new Error(`「${step.name}」的前置节点还没完成,不能开始`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const pendingModule = findModuleByName(catalog, step.name);
|
||||||
|
const paramSpecs = pendingModule?.params ?? [];
|
||||||
|
const auto = pendingModule?.auto === true;
|
||||||
|
if (opts?.stepParams && Object.keys(opts.stepParams).length > 0) {
|
||||||
|
const next = patchCreationFlowStepParams(flow, step.id, opts.stepParams);
|
||||||
|
const content = stringifyCreationFlow(next);
|
||||||
|
this.blackboard.write({
|
||||||
|
tag: CREATION_FLOW_TAG,
|
||||||
|
content,
|
||||||
|
source: "user",
|
||||||
|
});
|
||||||
|
this.session = {
|
||||||
|
...this.session,
|
||||||
|
slots: { ...this.session.slots, [CREATION_FLOW_TAG]: content },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const latest = parseCreationFlow(
|
||||||
|
this.blackboard.getContentByTag(CREATION_FLOW_TAG),
|
||||||
|
);
|
||||||
|
const pinned = latest ? findStepByRef(latest, step.id) ?? step : step;
|
||||||
|
const params = pinned.params ? { ...pinned.params } : {};
|
||||||
|
const paramsMissing = missingRequiredStepParams(pinned, pendingModule);
|
||||||
|
this.pinCreationStepSnapshot({
|
||||||
|
stepId: pinned.id,
|
||||||
|
name: pinned.name,
|
||||||
|
declaration: pendingModule?.declaration?.trim() || "",
|
||||||
|
params,
|
||||||
|
paramsMissing,
|
||||||
|
paramSpecs,
|
||||||
|
kind: pendingModule?.kind,
|
||||||
|
closer: pendingModule?.closer === true,
|
||||||
|
revise: reenter || isReviseStep(pinned),
|
||||||
|
revises: pinned.revises,
|
||||||
|
});
|
||||||
|
|
||||||
|
const waitingPick =
|
||||||
|
this.session.waitingReason?.kind === "pick_creation_step";
|
||||||
|
const needsParamForm = paramsMissing.length > 0 && !auto;
|
||||||
|
if (needsParamForm) {
|
||||||
|
const decision = createDecision({
|
||||||
|
action: "run_worker",
|
||||||
|
workerId: DESIGN_STEP_WORKER_ID,
|
||||||
|
reason: `「${pinned.name}」还缺参数:${paramsMissing.join("、")}`,
|
||||||
|
requiresApproval: true,
|
||||||
|
});
|
||||||
|
await this.dispatch({
|
||||||
|
type: "main_agent_decision_created",
|
||||||
|
payload: { decision },
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (waitingPick) {
|
||||||
|
await this.dispatch({
|
||||||
|
type: "user_picked_creation_step",
|
||||||
|
payload: { stepId: pinned.id },
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const decision = createDecision({
|
||||||
|
action: "run_worker",
|
||||||
|
workerId: DESIGN_STEP_WORKER_ID,
|
||||||
|
reason: reenter || isReviseStep(pinned)
|
||||||
|
? `回头修改 · ${pinned.name}`
|
||||||
|
: `开始:${pinned.name}`,
|
||||||
|
requiresApproval: false,
|
||||||
|
});
|
||||||
|
await this.dispatch({
|
||||||
|
type: "main_agent_decision_created",
|
||||||
|
payload: { decision },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验收后回到分层图:点节点即确认并进入,底栏写意见再发即改编排。
|
||||||
|
*/
|
||||||
|
private async awaitCreationStepPick(): Promise<void> {
|
||||||
if (this.session.slots[SLOT_CREATION_SEALED_BY_OPENING]) {
|
if (this.session.slots[SLOT_CREATION_SEALED_BY_OPENING]) {
|
||||||
this.onMessage(CREATION_SEALED_WAITING_MESSAGE);
|
this.onMessage(CREATION_SEALED_WAITING_MESSAGE);
|
||||||
return;
|
return;
|
||||||
@@ -916,13 +1410,60 @@ export class PhaseRuntime {
|
|||||||
const flow = parseCreationFlow(
|
const flow = parseCreationFlow(
|
||||||
this.blackboard.getContentByTag(CREATION_FLOW_TAG),
|
this.blackboard.getContentByTag(CREATION_FLOW_TAG),
|
||||||
);
|
);
|
||||||
const accepted = parseAcceptedUnits(
|
const accepted = this.readAcceptedStepIds();
|
||||||
this.session.slots[SLOT_CREATION_ACCEPTED_UNITS] ??
|
if (!flow) {
|
||||||
this.blackboard.getContentByTag(CREATION_ACCEPTED_UNITS_TAG),
|
debugLog("step", "暂无可用流程,等待点选或改编排");
|
||||||
|
await this.enterCreationStepPickWait();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const catalog = await this.loadActiveModuleCatalog();
|
||||||
|
const ready = listReadySteps(flow, accepted);
|
||||||
|
const autoReady = ready.filter((step) => {
|
||||||
|
const mod = findModuleByName(catalog, step.name);
|
||||||
|
return (
|
||||||
|
mod?.auto === true &&
|
||||||
|
missingRequiredStepParams(step, mod).length === 0
|
||||||
);
|
);
|
||||||
const pending = nextPendingStep(flow, accepted);
|
});
|
||||||
if (!pending || !flow) {
|
if (autoReady.length > 0 && autoReady.length === ready.length) {
|
||||||
debugLog("step", "暂无下一步,交给总管");
|
const protoReady = listSelectablePrototypes(flow, accepted).length > 0;
|
||||||
|
const spawnReady = catalog
|
||||||
|
? listRepeatableSpawns(flow, catalog, accepted).some((s) => s.ready)
|
||||||
|
: false;
|
||||||
|
if (!protoReady && !spawnReady) {
|
||||||
|
debugLog("step", `程序步直接开始 ${autoReady[0]!.id}`);
|
||||||
|
await this.startCreationStep(autoReady[0]!.id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
debugLog(
|
||||||
|
"step",
|
||||||
|
hasSelectableCreationWork(flow, catalog, accepted)
|
||||||
|
? "等待点选创作节点"
|
||||||
|
: "图上暂无已就绪节点,点节点进入或在底栏写意见改编排",
|
||||||
|
);
|
||||||
|
await this.enterCreationStepPickWait();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async enterCreationStepPickWait(): Promise<void> {
|
||||||
|
this.clearProposedStep();
|
||||||
|
if (this.session.waitingReason?.kind === "pick_creation_step") return;
|
||||||
|
if (this.session.phase === "running") {
|
||||||
|
await this.dispatch({ type: "creation_step_pick_awaited", payload: {} });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.session = {
|
||||||
|
...this.session,
|
||||||
|
phase: "waiting_user",
|
||||||
|
waitingReason: { kind: "pick_creation_step" },
|
||||||
|
pendingDecision: undefined,
|
||||||
|
currentWorkerId: undefined,
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private clearProposedStep(): void {
|
||||||
this.session = {
|
this.session = {
|
||||||
...this.session,
|
...this.session,
|
||||||
slots: {
|
slots: {
|
||||||
@@ -930,107 +1471,6 @@ export class PhaseRuntime {
|
|||||||
[CREATION_PROPOSED_STEP_TAG]: undefined,
|
[CREATION_PROPOSED_STEP_TAG]: undefined,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
if (this.mainAgent) {
|
|
||||||
await this.runMainAgent();
|
|
||||||
} else {
|
|
||||||
this.onMessage(
|
|
||||||
"[阶段机] 暂无下一节点,等待总管编排或补充意向。",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const active = this.getActiveSkill();
|
|
||||||
let paramSpecs: ModuleParamSpec[] = [];
|
|
||||||
let declaration = "";
|
|
||||||
let auto = false;
|
|
||||||
let kind: ModuleCatalogEntry["kind"];
|
|
||||||
let pendingModule: ModuleCatalogEntry | null = null;
|
|
||||||
if (active?.name) {
|
|
||||||
try {
|
|
||||||
const skill = await loadSkill(active.name);
|
|
||||||
if (skill.skillPackRoot) {
|
|
||||||
const catalog = await loadModuleCatalog(skill.skillPackRoot);
|
|
||||||
const mod = catalog?.modules.find((m) => m.name === pending.name);
|
|
||||||
paramSpecs = mod?.params ?? [];
|
|
||||||
declaration = mod?.declaration?.trim() || "";
|
|
||||||
auto = mod?.auto === true;
|
|
||||||
kind = mod?.kind;
|
|
||||||
pendingModule = mod ?? null;
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
/* catalog optional */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
debugLog("step", `提案下一步 ${pending.id}(${pending.name})`);
|
|
||||||
|
|
||||||
const params = pending.params ? { ...pending.params } : {};
|
|
||||||
const paramsMissing = missingRequiredStepParams(pending, pendingModule);
|
|
||||||
|
|
||||||
const intent =
|
|
||||||
typeof this.session.slots["用户.下一步意向"] === "string"
|
|
||||||
? String(this.session.slots["用户.下一步意向"]).trim()
|
|
||||||
: "";
|
|
||||||
const paramHint =
|
|
||||||
Object.keys(params).length > 0
|
|
||||||
? `;参数 ${Object.entries(params)
|
|
||||||
.map(([k, v]) => `${k}=${v}`)
|
|
||||||
.join(" · ")}`
|
|
||||||
: paramsMissing.length
|
|
||||||
? `;待钉参数:${paramsMissing.join("、")}`
|
|
||||||
: "";
|
|
||||||
const revise = isReviseStep(pending);
|
|
||||||
const reason = [
|
|
||||||
revise ? `下一步:回头修改 · ${pending.name}` : `下一步:${pending.name}`,
|
|
||||||
declaration ? `—— ${declaration}` : "",
|
|
||||||
paramHint,
|
|
||||||
intent ? `(你的意向:${intent})` : "",
|
|
||||||
]
|
|
||||||
.filter(Boolean)
|
|
||||||
.join("");
|
|
||||||
|
|
||||||
const snapshot = {
|
|
||||||
stepId: pending.id,
|
|
||||||
name: pending.name,
|
|
||||||
declaration: declaration || undefined,
|
|
||||||
params,
|
|
||||||
paramsMissing,
|
|
||||||
paramSpecs: paramSpecs.map((p) => ({
|
|
||||||
key: p.key,
|
|
||||||
label: p.label,
|
|
||||||
required: Boolean(p.required),
|
|
||||||
hint: p.hint,
|
|
||||||
})),
|
|
||||||
intent: intent || undefined,
|
|
||||||
...(kind ? { kind } : {}),
|
|
||||||
...(revise ? { mode: "revise" as const } : {}),
|
|
||||||
...(pending.revises ? { revises: pending.revises } : {}),
|
|
||||||
};
|
|
||||||
this.blackboard.write({
|
|
||||||
tag: CREATION_PROPOSED_STEP_TAG,
|
|
||||||
content: JSON.stringify(snapshot),
|
|
||||||
source: "runtime",
|
|
||||||
});
|
|
||||||
this.session = {
|
|
||||||
...this.session,
|
|
||||||
slots: {
|
|
||||||
...this.session.slots,
|
|
||||||
[CREATION_PROPOSED_STEP_TAG]: JSON.stringify(snapshot),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const decision = createDecision({
|
|
||||||
action: "run_worker",
|
|
||||||
workerId: "design-step",
|
|
||||||
reason,
|
|
||||||
// 程序步(如投影排序)且参数已齐:跳过「同意并开始」,直接执行。
|
|
||||||
requiresApproval: !(auto && paramsMissing.length === 0),
|
|
||||||
});
|
|
||||||
await this.dispatch({
|
|
||||||
type: "main_agent_decision_created",
|
|
||||||
payload: { decision },
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1108,6 +1548,24 @@ export class PhaseRuntime {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 再编排:解开开场收口,让 design-flow 能在收口前插入节点 */
|
||||||
|
private unsealCreationOpening(): void {
|
||||||
|
const reopened = reopenCreationFlowRaw(
|
||||||
|
this.blackboard.getContentByTag(CREATION_FLOW_TAG),
|
||||||
|
);
|
||||||
|
if (reopened) {
|
||||||
|
this.blackboard.write({
|
||||||
|
tag: CREATION_FLOW_TAG,
|
||||||
|
content: reopened,
|
||||||
|
source: "runtime",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const slots = { ...this.session.slots };
|
||||||
|
delete slots[SLOT_CREATION_SEALED_BY_OPENING];
|
||||||
|
if (reopened) slots[CREATION_FLOW_TAG] = reopened;
|
||||||
|
this.session = { ...this.session, slots };
|
||||||
|
}
|
||||||
|
|
||||||
private applyProposedStepParams(params: CreationFlowStepParams): void {
|
private applyProposedStepParams(params: CreationFlowStepParams): void {
|
||||||
const raw =
|
const raw =
|
||||||
(typeof this.session.slots[CREATION_PROPOSED_STEP_TAG] === "string"
|
(typeof this.session.slots[CREATION_PROPOSED_STEP_TAG] === "string"
|
||||||
@@ -1807,6 +2265,10 @@ export class PhaseRuntime {
|
|||||||
flowRaw,
|
flowRaw,
|
||||||
currentStepName,
|
currentStepName,
|
||||||
acceptedStepNames: accepted,
|
acceptedStepNames: accepted,
|
||||||
|
inheritExisting: isInheritExistingFlag(
|
||||||
|
this.blackboard.getContentByTag(CREATION_INHERIT_EXISTING_TAG) ??
|
||||||
|
this.session.slots[CREATION_INHERIT_EXISTING_TAG],
|
||||||
|
),
|
||||||
});
|
});
|
||||||
if (!binding?.opening?.trim()) return false;
|
if (!binding?.opening?.trim()) return false;
|
||||||
|
|
||||||
@@ -1952,7 +2414,9 @@ export class PhaseRuntime {
|
|||||||
const flow = parseCreationFlow(
|
const flow = parseCreationFlow(
|
||||||
this.blackboard.getContentByTag(CREATION_FLOW_TAG),
|
this.blackboard.getContentByTag(CREATION_FLOW_TAG),
|
||||||
);
|
);
|
||||||
const pending = nextPendingStep(flow, accepted);
|
const pinnedId = this.readPinnedStepId();
|
||||||
|
const pinned = flow && pinnedId ? findStepByRef(flow, pinnedId) : null;
|
||||||
|
const pending = pinned ?? nextPendingStep(flow, accepted);
|
||||||
current = pending ? stepUnitId(pending) : current || "(无待执行步骤)";
|
current = pending ? stepUnitId(pending) : current || "(无待执行步骤)";
|
||||||
this.blackboard.write({
|
this.blackboard.write({
|
||||||
tag: CREATION_CURRENT_STEP_TAG,
|
tag: CREATION_CURRENT_STEP_TAG,
|
||||||
@@ -2209,14 +2673,44 @@ export class PhaseRuntime {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private hasCreationFlow(): boolean {
|
||||||
|
return Boolean(
|
||||||
|
parseCreationFlow(this.blackboard.getContentByTag(CREATION_FLOW_TAG)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async commitMainAgentDecision(
|
||||||
|
decision: MainAgentDecision,
|
||||||
|
): Promise<void> {
|
||||||
|
debugLog(
|
||||||
|
"step",
|
||||||
|
`总管决策 ${labelAction(decision.action)}${decision.workerId ? ` ${decision.workerId}` : ""}`,
|
||||||
|
);
|
||||||
|
this.onMessage(`[总管] ${decision.action}: ${decision.reason}`);
|
||||||
|
const result = this.commitEvent({
|
||||||
|
type: "main_agent_decision_created",
|
||||||
|
payload: { decision },
|
||||||
|
});
|
||||||
|
if (result.error) this.onMessage(result.error);
|
||||||
|
await this.processEffects(result.effects);
|
||||||
|
await this.maybeRunMainAgent(result.effects);
|
||||||
|
}
|
||||||
|
|
||||||
private async runMainAgent(): Promise<void> {
|
private async runMainAgent(): Promise<void> {
|
||||||
if (!this.mainAgent || this.session.phase !== "running") {
|
if (!this.mainAgent || this.session.phase !== "running") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.hasCreationFlow()) {
|
||||||
|
debugLog("step", "已有创作流程,等待点选节点");
|
||||||
|
await this.awaitCreationStepPick();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
debugLog("step", "总管开始");
|
debugLog("step", "总管开始");
|
||||||
const availableWorkers = await this.resolveAvailableWorkers();
|
const availableWorkers = await this.resolveAvailableWorkers();
|
||||||
|
|
||||||
|
try {
|
||||||
const decision = await this.mainAgent.runToolLoop(
|
const decision = await this.mainAgent.runToolLoop(
|
||||||
{
|
{
|
||||||
session: this.session,
|
session: this.session,
|
||||||
@@ -2250,24 +2744,29 @@ export class PhaseRuntime {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
await this.commitMainAgentDecision(decision);
|
||||||
debugLog(
|
} catch (err) {
|
||||||
"step",
|
const reason = err instanceof Error ? err.message : String(err);
|
||||||
`总管决策 ${labelAction(decision.action)}${decision.workerId ? ` ${decision.workerId}` : ""}`,
|
if (this.hasCreationFlow()) {
|
||||||
|
debugLog("step", "总管无可用决策,改为等待点选");
|
||||||
|
await this.awaitCreationStepPick();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isUnusableMainAgentOutput(err)) {
|
||||||
|
debugLog("step", "总管未给出工具调用,改为启动 design-flow");
|
||||||
|
await this.commitMainAgentDecision(
|
||||||
|
createDecision({
|
||||||
|
action: "run_worker",
|
||||||
|
workerId: DESIGN_FLOW_WORKER_ID,
|
||||||
|
reason: "尚无创作流程,开始编排近期起点",
|
||||||
|
requiresApproval: false,
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
this.onMessage(`[总管] ${decision.action}: ${decision.reason}`);
|
return;
|
||||||
|
}
|
||||||
const result = this.commitEvent({
|
debugLog("step", `总管失败 ${reason.slice(0, 120)}`);
|
||||||
type: "main_agent_decision_created",
|
await this.failRun(reason);
|
||||||
payload: { decision },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.error) {
|
|
||||||
this.onMessage(result.error);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.processEffects(result.effects);
|
|
||||||
await this.maybeRunMainAgent(result.effects);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 总管 read_blackboard tool:按 tag 或模式读取正文 */
|
/** 总管 read_blackboard tool:按 tag 或模式读取正文 */
|
||||||
|
|||||||
@@ -435,6 +435,15 @@ export function buildFocus(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (reason?.kind === "pick_creation_step") {
|
||||||
|
return {
|
||||||
|
actorType: "user",
|
||||||
|
actorLabel: "你",
|
||||||
|
action: "点选要做的节点",
|
||||||
|
detail: "点可进入的节点即确认并开始;虚线原型点一下增殖再进去。要改排在底栏写意见再发",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (reason?.kind === "next_intent") {
|
if (reason?.kind === "next_intent") {
|
||||||
return {
|
return {
|
||||||
actorType: "user",
|
actorType: "user",
|
||||||
@@ -471,6 +480,14 @@ export function buildFocus(
|
|||||||
|
|
||||||
if (reason?.kind === "review_artifact") {
|
if (reason?.kind === "review_artifact") {
|
||||||
const art = session.artifacts.find((a) => a.id === session.pendingArtifactId);
|
const art = session.artifacts.find((a) => a.id === session.pendingArtifactId);
|
||||||
|
if (art?.workerId === "design-flow") {
|
||||||
|
return {
|
||||||
|
actorType: "user",
|
||||||
|
actorLabel: "你",
|
||||||
|
action: "点选要做的节点",
|
||||||
|
detail: "点可进入的节点即确认并开始;虚线原型点一下增殖再进去。要改排在底栏写意见再发",
|
||||||
|
};
|
||||||
|
}
|
||||||
const copy = reviewComposerCopy(art?.workerId, {
|
const copy = reviewComposerCopy(art?.workerId, {
|
||||||
hasQuestions: Boolean(reason.questions?.length),
|
hasQuestions: Boolean(reason.questions?.length),
|
||||||
outputTags: art?.outputTags,
|
outputTags: art?.outputTags,
|
||||||
|
|||||||
@@ -123,6 +123,17 @@ export function formatAgentDisplayTitle(detail?: string): string {
|
|||||||
return "编排器";
|
return "编排器";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 流程编排产物待确认:图上点节点即确认,不走验收双按钮 */
|
||||||
|
export function isFlowPlanReview(view: {
|
||||||
|
waitingReason?: { kind?: string } | null;
|
||||||
|
reviewArtifact?: { workerId?: string | null } | null;
|
||||||
|
} | null | undefined): boolean {
|
||||||
|
return (
|
||||||
|
view?.waitingReason?.kind === "review_artifact" &&
|
||||||
|
view?.reviewArtifact?.workerId === "design-flow"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/** 底栏双按钮 + 工作面 kicker:随 waiting 产物种类变化 */
|
/** 底栏双按钮 + 工作面 kicker:随 waiting 产物种类变化 */
|
||||||
export type ReviewComposerCopy = {
|
export type ReviewComposerCopy = {
|
||||||
/** 底栏短芯片 */
|
/** 底栏短芯片 */
|
||||||
@@ -179,8 +190,8 @@ export function reviewComposerCopy(
|
|||||||
taskLabel: "开场",
|
taskLabel: "开场",
|
||||||
taskTitle: "选定开场白",
|
taskTitle: "选定开场白",
|
||||||
hint: hasQuestions
|
hint: hasQuestions
|
||||||
? "同意就选定此开场;要改就写意见。下方追问可选答。"
|
? "同意就选定此开场;要改开场白就写意见。下方追问可选答。"
|
||||||
: "同意就选定此开场;要改就写意见。",
|
: "同意就选定此开场;要改开场白就写意见。",
|
||||||
submitLabel: "按意见修改",
|
submitLabel: "按意见修改",
|
||||||
acceptLabel: "选定此开场",
|
acceptLabel: "选定此开场",
|
||||||
placeholder: "修改意见…",
|
placeholder: "修改意见…",
|
||||||
|
|||||||
+317
-25
@@ -81,8 +81,10 @@ import { Blackboard } from "../blackboard/blackboard.js";
|
|||||||
import { parseWorkerSetYaml } from "../skills/worker-set-parse.js";
|
import { parseWorkerSetYaml } from "../skills/worker-set-parse.js";
|
||||||
import { parseShellAdaptationFromReplyFormat } from "../skills/present-packet.js";
|
import { parseShellAdaptationFromReplyFormat } from "../skills/present-packet.js";
|
||||||
import {
|
import {
|
||||||
|
CREATION_ACCEPTED_CONTENT_TAG,
|
||||||
CREATION_ACCEPTED_UNITS_TAG,
|
CREATION_ACCEPTED_UNITS_TAG,
|
||||||
isDesignDiskWorker,
|
isDesignDiskWorker,
|
||||||
|
parseAcceptedContentStore,
|
||||||
parseAcceptedUnits,
|
parseAcceptedUnits,
|
||||||
SLOT_CREATION_ACCEPTED_UNITS,
|
SLOT_CREATION_ACCEPTED_UNITS,
|
||||||
} from "../skills/creation-units.js";
|
} from "../skills/creation-units.js";
|
||||||
@@ -104,18 +106,26 @@ import {
|
|||||||
CREATION_FLOW_TAG,
|
CREATION_FLOW_TAG,
|
||||||
CREATION_PROPOSED_STEP_TAG,
|
CREATION_PROPOSED_STEP_TAG,
|
||||||
CREATION_SELECTED_RECIPE_TAG,
|
CREATION_SELECTED_RECIPE_TAG,
|
||||||
|
DESIGN_FLOW_WORKER_ID,
|
||||||
creationFlowFromRecipeSeed,
|
creationFlowFromRecipeSeed,
|
||||||
findRecipeCatalogEntry,
|
findRecipeCatalogEntry,
|
||||||
formatCreationFlowForUser,
|
formatCreationFlowForUser,
|
||||||
|
findModuleByName,
|
||||||
|
findStepByRef,
|
||||||
|
type FlowStepTitleHint,
|
||||||
isProgressPointerTag,
|
isProgressPointerTag,
|
||||||
loadRecipeCatalog,
|
loadRecipeCatalog,
|
||||||
loadRecipeDetail,
|
loadRecipeDetail,
|
||||||
|
loadModuleCatalog,
|
||||||
parseCreationFlow,
|
parseCreationFlow,
|
||||||
parseSelectedRecipeRef,
|
parseSelectedRecipeRef,
|
||||||
resolveDesignStepBinding,
|
resolveDesignStepBinding,
|
||||||
stepUnitId,
|
stepUnitId,
|
||||||
stringifyCreationFlow,
|
stringifyCreationFlow,
|
||||||
|
stripReviseSteps,
|
||||||
|
type CreationFlow,
|
||||||
type CreationFlowUserView,
|
type CreationFlowUserView,
|
||||||
|
type ModuleCatalog,
|
||||||
type RecipeCatalogEntry,
|
type RecipeCatalogEntry,
|
||||||
} from "../skills/creation-flow.js";
|
} from "../skills/creation-flow.js";
|
||||||
import { loadSkill } from "../skills/loader.js";
|
import { loadSkill } from "../skills/loader.js";
|
||||||
@@ -260,6 +270,8 @@ export type SessionAction =
|
|||||||
| { type: "approve"; label: string }
|
| { type: "approve"; label: string }
|
||||||
| { type: "accept"; label: string }
|
| { type: "accept"; label: string }
|
||||||
| { type: "reject"; label: string }
|
| { type: "reject"; label: string }
|
||||||
|
| { type: "replan"; label: string }
|
||||||
|
| { type: "leave_step"; label: string }
|
||||||
| { type: "run_outline"; label: string }
|
| { type: "run_outline"; label: string }
|
||||||
| { type: "finish"; label: string };
|
| { type: "finish"; label: string };
|
||||||
|
|
||||||
@@ -297,6 +309,8 @@ export type ProposedNextStepView = {
|
|||||||
/** 步骤怎么跑:revise = 回头修改既有产物 */
|
/** 步骤怎么跑:revise = 回头修改既有产物 */
|
||||||
mode?: "fresh" | "revise";
|
mode?: "fresh" | "revise";
|
||||||
revises?: string;
|
revises?: string;
|
||||||
|
/** 收口节点(开场白):确认开干前仍可改编排追加前序节点 */
|
||||||
|
closer?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ReviewArtifactView = {
|
export type ReviewArtifactView = {
|
||||||
@@ -325,6 +339,8 @@ type ManagedSession = {
|
|||||||
branchState: MessageBranchState;
|
branchState: MessageBranchState;
|
||||||
/** 当前能力包可选初始配方(用户手动选) */
|
/** 当前能力包可选初始配方(用户手动选) */
|
||||||
recipeOptions?: RecipeCatalogEntry[];
|
recipeOptions?: RecipeCatalogEntry[];
|
||||||
|
/** 模块目录(分层图 / 可反复增殖) */
|
||||||
|
moduleCatalog?: ModuleCatalog | null;
|
||||||
/** 开局模块 opening,缓存给意图页 */
|
/** 开局模块 opening,缓存给意图页 */
|
||||||
openingGuide?: { stepId: string; stepName: string; text: string } | null;
|
openingGuide?: { stepId: string; stepName: string; text: string } | null;
|
||||||
/** 当前 LLM 请求的取消器(停止并重试) */
|
/** 当前 LLM 请求的取消器(停止并重试) */
|
||||||
@@ -540,6 +556,7 @@ export class SessionManager {
|
|||||||
if (bookId) {
|
if (bookId) {
|
||||||
this.activeBookSessions.set(bookId, id);
|
this.activeBookSessions.set(bookId, id);
|
||||||
appendBookSession(bookId, id);
|
appendBookSession(bookId, id);
|
||||||
|
this.persist(managed);
|
||||||
}
|
}
|
||||||
return this.toView(id);
|
return this.toView(id);
|
||||||
}
|
}
|
||||||
@@ -557,7 +574,9 @@ export class SessionManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get(id: string): SessionView | null {
|
get(id: string): SessionView | null {
|
||||||
if (!this.sessions.has(id)) return null;
|
const s = this.sessions.get(id);
|
||||||
|
if (!s) return null;
|
||||||
|
if (s.runtime.recoverOrphanedRun()) this.persist(s);
|
||||||
return this.toView(id);
|
return this.toView(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -825,7 +844,8 @@ export class SessionManager {
|
|||||||
kind !== "review_artifact" &&
|
kind !== "review_artifact" &&
|
||||||
kind !== "approve_step" &&
|
kind !== "approve_step" &&
|
||||||
kind !== "revision" &&
|
kind !== "revision" &&
|
||||||
kind !== "next_intent"
|
kind !== "next_intent" &&
|
||||||
|
kind !== "pick_creation_step"
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1079,8 +1099,7 @@ export class SessionManager {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
const detail = err instanceof Error ? err.message : String(err);
|
const detail = err instanceof Error ? err.message : String(err);
|
||||||
console.error("[会话] 发送消息失败", detail);
|
console.error("[会话] 发送消息失败", detail);
|
||||||
s.messages.push(this.msg("system", formatRuntimeError(detail)));
|
await this.recoverFailedRun(s, detail);
|
||||||
if (s.bookId) this.syncBookPreview(s.bookId, s.messages);
|
|
||||||
}
|
}
|
||||||
this.syncCreationDialogue(s);
|
this.syncCreationDialogue(s);
|
||||||
this.persist(s);
|
this.persist(s);
|
||||||
@@ -1315,8 +1334,7 @@ export class SessionManager {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
const detail = err instanceof Error ? err.message : String(err);
|
const detail = err instanceof Error ? err.message : String(err);
|
||||||
console.error("[会话] 停止并重试失败", detail);
|
console.error("[会话] 停止并重试失败", detail);
|
||||||
s.messages.push(this.msg("system", formatRuntimeError(detail)));
|
await this.recoverFailedRun(s, detail);
|
||||||
if (s.bookId) this.syncBookPreview(s.bookId, s.messages);
|
|
||||||
}
|
}
|
||||||
this.syncCreationDialogue(s);
|
this.syncCreationDialogue(s);
|
||||||
this.persist(s);
|
this.persist(s);
|
||||||
@@ -1462,7 +1480,7 @@ export class SessionManager {
|
|||||||
try {
|
try {
|
||||||
const owned = await this.runExclusive(id, s, async () => {
|
const owned = await this.runExclusive(id, s, async () => {
|
||||||
if (waiting?.kind === "approve_step") {
|
if (waiting?.kind === "approve_step") {
|
||||||
await s.runtime.rejectStep(reason ?? "用户暂不执行");
|
await s.runtime.leaveCreationStep();
|
||||||
} else if (waiting?.kind === "review_artifact") {
|
} else if (waiting?.kind === "review_artifact") {
|
||||||
await s.runtime.rejectArtifact(reason ?? "用户要求重新来");
|
await s.runtime.rejectArtifact(reason ?? "用户要求重新来");
|
||||||
} else {
|
} else {
|
||||||
@@ -1476,9 +1494,174 @@ export class SessionManager {
|
|||||||
s.messages.push(this.msg("system", formatRuntimeError(detail)));
|
s.messages.push(this.msg("system", formatRuntimeError(detail)));
|
||||||
if (s.bookId) this.syncBookPreview(s.bookId, s.messages);
|
if (s.bookId) this.syncBookPreview(s.bookId, s.messages);
|
||||||
}
|
}
|
||||||
|
this.syncCreationDialogue(s);
|
||||||
|
this.persist(s);
|
||||||
return this.toView(id);
|
return this.toView(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 离开当前技能步,回到分层图点选 */
|
||||||
|
async leaveStep(id: string): Promise<SessionView> {
|
||||||
|
const s = this.require(id);
|
||||||
|
try {
|
||||||
|
const owned = await this.runExclusive(id, s, () =>
|
||||||
|
s.runtime.leaveCreationStep(),
|
||||||
|
);
|
||||||
|
if (!owned) return this.toView(id);
|
||||||
|
} catch (err) {
|
||||||
|
const detail = err instanceof Error ? err.message : String(err);
|
||||||
|
console.error("[会话] 返回节点失败", detail);
|
||||||
|
s.messages.push(this.msg("system", formatRuntimeError(detail)));
|
||||||
|
if (s.bookId) this.syncBookPreview(s.bookId, s.messages);
|
||||||
|
}
|
||||||
|
this.syncCreationDialogue(s);
|
||||||
|
this.persist(s);
|
||||||
|
return this.toView(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 开场白等步骤上发现漏了前序节点:再进流程编排,不整局重开 */
|
||||||
|
async replan(id: string, reason?: string): Promise<SessionView> {
|
||||||
|
const s = this.require(id);
|
||||||
|
const note = reason?.trim() || "回到流程编排,追加或改排节点。";
|
||||||
|
this.clearAgentThinking(id);
|
||||||
|
recordPreMessageCheckpoint(
|
||||||
|
s.branchState,
|
||||||
|
s.messages.length,
|
||||||
|
this.captureCheckpoint(s),
|
||||||
|
);
|
||||||
|
s.messages.push(this.msg("user", note));
|
||||||
|
this.syncCreationDialogue(s);
|
||||||
|
if (s.bookId) this.syncBookPreview(s.bookId, s.messages);
|
||||||
|
try {
|
||||||
|
const owned = await this.runExclusive(id, s, () =>
|
||||||
|
s.runtime.replanCreationFlow(note),
|
||||||
|
);
|
||||||
|
if (!owned) return this.toView(id);
|
||||||
|
} catch (err) {
|
||||||
|
const detail = err instanceof Error ? err.message : String(err);
|
||||||
|
console.error("[会话] 再编排失败", detail);
|
||||||
|
s.messages.push(this.msg("system", formatRuntimeError(detail)));
|
||||||
|
if (s.bookId) this.syncBookPreview(s.bookId, s.messages);
|
||||||
|
}
|
||||||
|
this.syncCreationDialogue(s);
|
||||||
|
this.persist(s);
|
||||||
|
return this.toView(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async pickStep(
|
||||||
|
id: string,
|
||||||
|
stepId: string,
|
||||||
|
stepParams?: Record<string, unknown>,
|
||||||
|
reenter?: boolean,
|
||||||
|
): 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.pickCreationStep(stepId, stepParams, {
|
||||||
|
reenter: reenter === true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getStepArtifact(
|
||||||
|
id: string,
|
||||||
|
stepId: string,
|
||||||
|
): Promise<{
|
||||||
|
stepId: string;
|
||||||
|
name: string;
|
||||||
|
tag: string | null;
|
||||||
|
content: string;
|
||||||
|
}> {
|
||||||
|
const s = this.require(id);
|
||||||
|
const catalog = s.moduleCatalog;
|
||||||
|
const flow = parseCreationFlow(
|
||||||
|
s.runtime.getBlackboard().getContentByTag(CREATION_FLOW_TAG),
|
||||||
|
);
|
||||||
|
if (!flow) throw new Error("还没有工作流计划");
|
||||||
|
const cleaned = stripReviseSteps(flow);
|
||||||
|
const step = findStepByRef(cleaned, stepId.trim());
|
||||||
|
if (!step) throw new Error(`找不到节点:${stepId}`);
|
||||||
|
const mod = findModuleByName(catalog, step.name);
|
||||||
|
const tag = mod?.artifact?.trim() || null;
|
||||||
|
const content = tag
|
||||||
|
? s.runtime.getBlackboard().getContentByTag(tag)?.trim() || ""
|
||||||
|
: "";
|
||||||
|
return { stepId: step.id, name: step.name, tag, content };
|
||||||
|
}
|
||||||
|
|
||||||
|
async spawnStep(id: string, moduleName: 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.spawnCreationStep(moduleName);
|
||||||
|
});
|
||||||
|
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(
|
||||||
|
s: ManagedSession,
|
||||||
|
reason: WaitingReason | undefined,
|
||||||
|
): boolean {
|
||||||
|
const session = s.runtime.getSession();
|
||||||
|
if (
|
||||||
|
isPlayLayerActive(session.slots) ||
|
||||||
|
inferLifecycleStage(session) === "play"
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const kind = reason?.kind;
|
||||||
|
if (
|
||||||
|
kind !== "worker_questions" &&
|
||||||
|
kind !== "input" &&
|
||||||
|
kind !== "revision"
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const stepId =
|
||||||
|
s.runtime.getBlackboard().getContentByTag(CREATION_CURRENT_STEP_TAG)?.trim() ||
|
||||||
|
session.currentStepId?.trim() ||
|
||||||
|
"";
|
||||||
|
return Boolean(stepId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 流程编排还停在核对态时,点节点先收下计划再进入 */
|
||||||
|
private async confirmPendingFlowPlan(s: ManagedSession): Promise<void> {
|
||||||
|
const session = s.runtime.getSession();
|
||||||
|
if (session.waitingReason?.kind !== "review_artifact") return;
|
||||||
|
const art = session.pendingArtifactId
|
||||||
|
? session.artifacts.find((a) => a.id === session.pendingArtifactId)
|
||||||
|
: undefined;
|
||||||
|
if (art?.workerId !== DESIGN_FLOW_WORKER_ID) {
|
||||||
|
throw new Error("当前不是点选节点的时机");
|
||||||
|
}
|
||||||
|
await s.runtime.acceptArtifact();
|
||||||
|
}
|
||||||
|
|
||||||
async runOutline(id: string): Promise<SessionView> {
|
async runOutline(id: string): Promise<SessionView> {
|
||||||
const s = this.require(id);
|
const s = this.require(id);
|
||||||
try {
|
try {
|
||||||
@@ -1586,6 +1769,7 @@ export class SessionManager {
|
|||||||
this.activeBookSessions.set(params.bookId, params.sessionId);
|
this.activeBookSessions.set(params.bookId, params.sessionId);
|
||||||
appendBookSession(params.bookId, params.sessionId);
|
appendBookSession(params.bookId, params.sessionId);
|
||||||
await this.refreshRecipeOptions(managedRef);
|
await this.refreshRecipeOptions(managedRef);
|
||||||
|
runtime.recoverOrphanedRun();
|
||||||
this.persist(managedRef);
|
this.persist(managedRef);
|
||||||
await this.maybeKickAgentAfterDemand(runtime);
|
await this.maybeKickAgentAfterDemand(runtime);
|
||||||
|
|
||||||
@@ -1891,6 +2075,20 @@ export class SessionManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 执行失败后离开 running,避免界面一直转圈。 */
|
||||||
|
private async recoverFailedRun(
|
||||||
|
s: ManagedSession,
|
||||||
|
detail: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const formatted = formatRuntimeError(detail);
|
||||||
|
if (s.runtime.getSession().phase === "running") {
|
||||||
|
await s.runtime.failRun(formatted);
|
||||||
|
} else {
|
||||||
|
s.messages.push(this.msg("system", formatted));
|
||||||
|
}
|
||||||
|
if (s.bookId) this.syncBookPreview(s.bookId, s.messages);
|
||||||
|
}
|
||||||
|
|
||||||
private enrichDisplayMessages(
|
private enrichDisplayMessages(
|
||||||
session: RuntimeSession,
|
session: RuntimeSession,
|
||||||
messages: ChatMessage[],
|
messages: ChatMessage[],
|
||||||
@@ -2202,11 +2400,21 @@ export class SessionManager {
|
|||||||
} else {
|
} else {
|
||||||
actions.push({ type: "approve", label: "确认执行" });
|
actions.push({ type: "approve", label: "确认执行" });
|
||||||
}
|
}
|
||||||
actions.push({ type: "reject", label: "暂不执行" });
|
actions.push({ type: "reject", label: "返回节点" });
|
||||||
actions.push({
|
actions.push({
|
||||||
type: "send_message",
|
type: "send_message",
|
||||||
label: "说明意见",
|
label: "说明意见",
|
||||||
placeholder: "改排、回头改某步,或说明不要这一步…",
|
placeholder: "改排、补节点(如 NPC)、回头改某步,或说明不要这一步…",
|
||||||
|
});
|
||||||
|
} else if (reason?.kind === "pick_creation_step") {
|
||||||
|
hints.push(
|
||||||
|
"点可进入的节点即确认并开始。虚线原型点一下会增殖一条实例再进去。要改排在底栏写意见再发。",
|
||||||
|
);
|
||||||
|
actions.push({ type: "replan", label: "发送" });
|
||||||
|
actions.push({
|
||||||
|
type: "send_message",
|
||||||
|
label: "发送",
|
||||||
|
placeholder: "例如:补舞台骨架、把某步改到开场白之前…",
|
||||||
});
|
});
|
||||||
} else if (reason?.kind === "next_intent") {
|
} else if (reason?.kind === "next_intent") {
|
||||||
hints.push("下一步想写什么?可留空;也可说回头改某步。发送后会展示下一节点供你确认。");
|
hints.push("下一步想写什么?可留空;也可说回头改某步。发送后会展示下一节点供你确认。");
|
||||||
@@ -2220,6 +2428,16 @@ export class SessionManager {
|
|||||||
const copy = reviewComposerCopy(art?.workerId, {
|
const copy = reviewComposerCopy(art?.workerId, {
|
||||||
hasQuestions: Boolean(reason.questions?.length),
|
hasQuestions: Boolean(reason.questions?.length),
|
||||||
});
|
});
|
||||||
|
if (art?.workerId === DESIGN_FLOW_WORKER_ID) {
|
||||||
|
hints.push(
|
||||||
|
"点可进入的节点即确认并开始。虚线原型点一下会增殖一条实例再进去。要改排在底栏写意见再发。",
|
||||||
|
);
|
||||||
|
actions.push({
|
||||||
|
type: "send_message",
|
||||||
|
label: "发送",
|
||||||
|
placeholder: "例如:补舞台骨架、把某步改到开场白之前…",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
actions.push({ type: "accept", label: copy.acceptLabel });
|
actions.push({ type: "accept", label: copy.acceptLabel });
|
||||||
actions.push({ type: "reject", label: "不接受" });
|
actions.push({ type: "reject", label: "不接受" });
|
||||||
actions.push({
|
actions.push({
|
||||||
@@ -2227,8 +2445,13 @@ export class SessionManager {
|
|||||||
label: copy.submitLabel,
|
label: copy.submitLabel,
|
||||||
placeholder: `${copy.placeholder};在现有产物上改,不整份重做…`,
|
placeholder: `${copy.placeholder};在现有产物上改,不整份重做…`,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
if (reason.questions?.length) {
|
if (reason.questions?.length) {
|
||||||
hints.push(`下方追问可选;${copy.acceptLabel}即收起(表示无需再完善)`);
|
hints.push(
|
||||||
|
art?.workerId === DESIGN_FLOW_WORKER_ID
|
||||||
|
? "下方追问可选;点节点进入即收起"
|
||||||
|
: `下方追问可选;${copy.acceptLabel}即收起(表示无需再完善)`,
|
||||||
|
);
|
||||||
actions.push({ type: "answer_questions", label: "提交作答" });
|
actions.push({ type: "answer_questions", label: "提交作答" });
|
||||||
actions.push({ type: "skip_questions", label: "跳过追问" });
|
actions.push({ type: "skip_questions", label: "跳过追问" });
|
||||||
}
|
}
|
||||||
@@ -2281,6 +2504,10 @@ export class SessionManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.canOfferLeaveCreationStep(s, reason)) {
|
||||||
|
actions.push({ type: "leave_step", label: "返回节点" });
|
||||||
|
}
|
||||||
|
|
||||||
if (s.bookId) {
|
if (s.bookId) {
|
||||||
this.persist(s);
|
this.persist(s);
|
||||||
}
|
}
|
||||||
@@ -2330,7 +2557,12 @@ export class SessionManager {
|
|||||||
effectiveStartupMode(activeSnap) === "agent-first"
|
effectiveStartupMode(activeSnap) === "agent-first"
|
||||||
? activeSnap.uiPrompt?.trim() || DEFAULT_UI_PROMPT
|
? activeSnap.uiPrompt?.trim() || DEFAULT_UI_PROMPT
|
||||||
: undefined,
|
: undefined,
|
||||||
reviewArtifact: buildReviewArtifactView(session, messages, s.runtime.getBlackboard()),
|
reviewArtifact: buildReviewArtifactView(
|
||||||
|
session,
|
||||||
|
messages,
|
||||||
|
s.runtime.getBlackboard(),
|
||||||
|
s.moduleCatalog,
|
||||||
|
),
|
||||||
workerSetView: buildWorkerSetUserView(
|
workerSetView: buildWorkerSetUserView(
|
||||||
s.runtime.getBlackboard(),
|
s.runtime.getBlackboard(),
|
||||||
s.runtime.getSession(),
|
s.runtime.getSession(),
|
||||||
@@ -2338,6 +2570,7 @@ export class SessionManager {
|
|||||||
creationFlowView: buildCreationFlowUserView(
|
creationFlowView: buildCreationFlowUserView(
|
||||||
s.runtime.getBlackboard(),
|
s.runtime.getBlackboard(),
|
||||||
session.slots[SLOT_CREATION_ACCEPTED_UNITS],
|
session.slots[SLOT_CREATION_ACCEPTED_UNITS],
|
||||||
|
s.moduleCatalog,
|
||||||
),
|
),
|
||||||
proposedNextStep:
|
proposedNextStep:
|
||||||
reason?.kind === "approve_step"
|
reason?.kind === "approve_step"
|
||||||
@@ -2448,6 +2681,7 @@ export class SessionManager {
|
|||||||
const skillName = s.runtime.getActiveSkill()?.name;
|
const skillName = s.runtime.getActiveSkill()?.name;
|
||||||
if (!skillName) {
|
if (!skillName) {
|
||||||
s.recipeOptions = [];
|
s.recipeOptions = [];
|
||||||
|
s.moduleCatalog = null;
|
||||||
s.openingGuide = null;
|
s.openingGuide = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -2455,13 +2689,19 @@ export class SessionManager {
|
|||||||
const skill = await loadSkill(skillName);
|
const skill = await loadSkill(skillName);
|
||||||
if (!skill.skillPackRoot) {
|
if (!skill.skillPackRoot) {
|
||||||
s.recipeOptions = [];
|
s.recipeOptions = [];
|
||||||
|
s.moduleCatalog = null;
|
||||||
s.openingGuide = null;
|
s.openingGuide = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const catalog = await loadRecipeCatalog(skill.skillPackRoot);
|
const [catalog, modules] = await Promise.all([
|
||||||
|
loadRecipeCatalog(skill.skillPackRoot),
|
||||||
|
loadModuleCatalog(skill.skillPackRoot),
|
||||||
|
]);
|
||||||
s.recipeOptions = catalog?.recipes ?? [];
|
s.recipeOptions = catalog?.recipes ?? [];
|
||||||
|
s.moduleCatalog = modules;
|
||||||
} catch {
|
} catch {
|
||||||
s.recipeOptions = [];
|
s.recipeOptions = [];
|
||||||
|
s.moduleCatalog = null;
|
||||||
}
|
}
|
||||||
await this.ensureRecipeSeededFlow(s);
|
await this.ensureRecipeSeededFlow(s);
|
||||||
await this.refreshOpeningGuide(s);
|
await this.refreshOpeningGuide(s);
|
||||||
@@ -2489,11 +2729,7 @@ export class SessionManager {
|
|||||||
const detail = await loadRecipeDetail(packRoot, entry);
|
const detail = await loadRecipeDetail(packRoot, entry);
|
||||||
const seeded = creationFlowFromRecipeSeed(detail);
|
const seeded = creationFlowFromRecipeSeed(detail);
|
||||||
if (!seeded) return;
|
if (!seeded) return;
|
||||||
board.write({
|
await this.commitSeededCreationFlow(s, seeded);
|
||||||
tag: CREATION_FLOW_TAG,
|
|
||||||
content: stringifyCreationFlow(seeded),
|
|
||||||
source: "runtime",
|
|
||||||
});
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("[会话] 配方预置失败", err);
|
console.error("[会话] 配方预置失败", err);
|
||||||
}
|
}
|
||||||
@@ -2553,6 +2789,7 @@ export class SessionManager {
|
|||||||
s: ManagedSession,
|
s: ManagedSession,
|
||||||
reason: WaitingReason | undefined,
|
reason: WaitingReason | undefined,
|
||||||
): { stepId: string; stepName: string; text: string } | null {
|
): { stepId: string; stepName: string; text: string } | null {
|
||||||
|
if (reason?.kind === "pick_creation_step") return null;
|
||||||
if (reason?.kind === "worker_questions") {
|
if (reason?.kind === "worker_questions") {
|
||||||
const qs = normalizeQuestions(reason.questions);
|
const qs = normalizeQuestions(reason.questions);
|
||||||
if (isModuleOpeningQuestions(qs)) {
|
if (isModuleOpeningQuestions(qs)) {
|
||||||
@@ -2584,6 +2821,44 @@ export class SessionManager {
|
|||||||
return s.openingGuide ?? null;
|
return s.openingGuide ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 写入剧本 seed,并立刻进入点选图(开局节点跟配方走)。 */
|
||||||
|
private async commitSeededCreationFlow(
|
||||||
|
s: ManagedSession,
|
||||||
|
seeded: CreationFlow,
|
||||||
|
): Promise<void> {
|
||||||
|
this.writeSeededCreationFlow(s, seeded);
|
||||||
|
await s.runtime.enterSeededCreationPick();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 配方预置 DAG:写入黑板并留下可回退的编排检查点(不必先跑 LLM) */
|
||||||
|
private writeSeededCreationFlow(s: ManagedSession, seeded: CreationFlow): void {
|
||||||
|
s.runtime.getBlackboard().write({
|
||||||
|
tag: CREATION_FLOW_TAG,
|
||||||
|
content: stringifyCreationFlow(seeded),
|
||||||
|
source: "runtime",
|
||||||
|
});
|
||||||
|
const alreadyRecorded = s.messages.some(
|
||||||
|
(m) =>
|
||||||
|
m.kind === "worker_output" &&
|
||||||
|
m.actor === "design-flow" &&
|
||||||
|
(m.text ?? "").includes("配方预置近期起点"),
|
||||||
|
);
|
||||||
|
if (alreadyRecorded) return;
|
||||||
|
const names = seeded.steps.map((step) => step.name).join(" → ");
|
||||||
|
recordPreMessageCheckpoint(
|
||||||
|
s.branchState,
|
||||||
|
s.messages.length,
|
||||||
|
this.captureCheckpoint(s),
|
||||||
|
);
|
||||||
|
s.messages.push(
|
||||||
|
this.msg(
|
||||||
|
"system",
|
||||||
|
`[Worker] design-flow 已完成\n\n配方预置近期起点${names ? `:${names}` : ""}。\n这是可回退的编排检查点;之后可再编排追加节点(如生成规则 / 具体实例)。`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
this.syncCreationDialogue(s);
|
||||||
|
}
|
||||||
|
|
||||||
private async applySelectedRecipe(
|
private async applySelectedRecipe(
|
||||||
s: ManagedSession,
|
s: ManagedSession,
|
||||||
recipeId: string,
|
recipeId: string,
|
||||||
@@ -2606,7 +2881,7 @@ export class SessionManager {
|
|||||||
source: "user",
|
source: "user",
|
||||||
});
|
});
|
||||||
|
|
||||||
// 剧本已钉死开局模块 → 默认写入近期起点,开局即坐在 DAG 第一步(不必先跑 design-flow)
|
// 开局节点跟剧本 seed:写入近期起点并进入点选图(不必先跑 design-flow / 先描述需求)
|
||||||
const existingFlow = board.getContentByTag(CREATION_FLOW_TAG)?.trim();
|
const existingFlow = board.getContentByTag(CREATION_FLOW_TAG)?.trim();
|
||||||
if (!existingFlow) {
|
if (!existingFlow) {
|
||||||
const skillName = s.runtime.getActiveSkill()?.name;
|
const skillName = s.runtime.getActiveSkill()?.name;
|
||||||
@@ -2618,11 +2893,7 @@ export class SessionManager {
|
|||||||
const detail = await loadRecipeDetail(packRoot, entry);
|
const detail = await loadRecipeDetail(packRoot, entry);
|
||||||
const seeded = creationFlowFromRecipeSeed(detail);
|
const seeded = creationFlowFromRecipeSeed(detail);
|
||||||
if (seeded) {
|
if (seeded) {
|
||||||
board.write({
|
await this.commitSeededCreationFlow(s, seeded);
|
||||||
tag: CREATION_FLOW_TAG,
|
|
||||||
content: stringifyCreationFlow(seeded),
|
|
||||||
source: "runtime",
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -2652,6 +2923,7 @@ function collectFilledTags(blackboard: Blackboard): string[] {
|
|||||||
function buildCreationFlowUserView(
|
function buildCreationFlowUserView(
|
||||||
blackboard: Blackboard,
|
blackboard: Blackboard,
|
||||||
acceptedRaw?: unknown,
|
acceptedRaw?: unknown,
|
||||||
|
catalog?: ModuleCatalog | null,
|
||||||
): CreationFlowUserView | undefined {
|
): CreationFlowUserView | undefined {
|
||||||
const raw = blackboard.getContentByTag(CREATION_FLOW_TAG)?.trim();
|
const raw = blackboard.getContentByTag(CREATION_FLOW_TAG)?.trim();
|
||||||
if (!raw) return undefined;
|
if (!raw) return undefined;
|
||||||
@@ -2662,7 +2934,24 @@ function buildCreationFlowUserView(
|
|||||||
const accepted = parseAcceptedUnits(
|
const accepted = parseAcceptedUnits(
|
||||||
acceptedRaw ?? blackboard.getContentByTag(CREATION_ACCEPTED_UNITS_TAG),
|
acceptedRaw ?? blackboard.getContentByTag(CREATION_ACCEPTED_UNITS_TAG),
|
||||||
);
|
);
|
||||||
return formatCreationFlowForUser(flow, undefined, accepted);
|
const titleHints: Record<string, FlowStepTitleHint> = {};
|
||||||
|
const store = parseAcceptedContentStore(
|
||||||
|
blackboard.getContentByTag(CREATION_ACCEPTED_CONTENT_TAG),
|
||||||
|
);
|
||||||
|
for (const [id, entry] of Object.entries(store.units)) {
|
||||||
|
if (!entry.summary && entry.content == null) continue;
|
||||||
|
titleHints[id] = {
|
||||||
|
...(entry.summary ? { summary: entry.summary } : {}),
|
||||||
|
...(entry.content != null ? { content: entry.content } : {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return formatCreationFlowForUser(
|
||||||
|
flow,
|
||||||
|
catalog,
|
||||||
|
accepted,
|
||||||
|
collectFilledTags(blackboard),
|
||||||
|
titleHints,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function readProposedNextStep(
|
function readProposedNextStep(
|
||||||
@@ -2708,6 +2997,7 @@ function readProposedNextStep(
|
|||||||
intent: typeof doc.intent === "string" ? doc.intent : undefined,
|
intent: typeof doc.intent === "string" ? doc.intent : undefined,
|
||||||
mode: doc.mode === "revise" ? "revise" : undefined,
|
mode: doc.mode === "revise" ? "revise" : undefined,
|
||||||
revises: typeof doc.revises === "string" ? doc.revises : undefined,
|
revises: typeof doc.revises === "string" ? doc.revises : undefined,
|
||||||
|
closer: doc.closer === true ? true : undefined,
|
||||||
};
|
};
|
||||||
} catch {
|
} catch {
|
||||||
return undefined;
|
return undefined;
|
||||||
@@ -2767,6 +3057,7 @@ function buildReviewArtifactView(
|
|||||||
session: RuntimeSession,
|
session: RuntimeSession,
|
||||||
messages: ChatMessage[],
|
messages: ChatMessage[],
|
||||||
blackboard: Blackboard,
|
blackboard: Blackboard,
|
||||||
|
catalog?: ModuleCatalog | null,
|
||||||
): ReviewArtifactView | undefined {
|
): ReviewArtifactView | undefined {
|
||||||
if (session.waitingReason?.kind !== "review_artifact") return undefined;
|
if (session.waitingReason?.kind !== "review_artifact") return undefined;
|
||||||
const art = session.artifacts.find((a) => a.id === session.pendingArtifactId);
|
const art = session.artifacts.find((a) => a.id === session.pendingArtifactId);
|
||||||
@@ -2869,6 +3160,7 @@ function buildReviewArtifactView(
|
|||||||
? buildCreationFlowUserView(
|
? buildCreationFlowUserView(
|
||||||
blackboard,
|
blackboard,
|
||||||
session.slots[SLOT_CREATION_ACCEPTED_UNITS],
|
session.slots[SLOT_CREATION_ACCEPTED_UNITS],
|
||||||
|
catalog,
|
||||||
)
|
)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
@@ -2896,7 +3188,7 @@ function formatRuntimeError(detail: string): string {
|
|||||||
if (detail.includes("401") || detail.toLowerCase().includes("authentication")) {
|
if (detail.includes("401") || detail.toLowerCase().includes("authentication")) {
|
||||||
return (
|
return (
|
||||||
"[请求失败] API Key 无效或未授权。请到「设置 → API 配置」检查 Key 与 Base URL," +
|
"[请求失败] API Key 无效或未授权。请到「设置 → API 配置」检查 Key 与 Base URL," +
|
||||||
"或点击「测试连接」验证。"
|
"或点击「探测」验证。"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (detail.startsWith("LLM request failed")) {
|
if (detail.startsWith("LLM request failed")) {
|
||||||
|
|||||||
@@ -4,9 +4,13 @@ import {
|
|||||||
deleteApiProfile,
|
deleteApiProfile,
|
||||||
getApiProfile,
|
getApiProfile,
|
||||||
listApiProfiles,
|
listApiProfiles,
|
||||||
testApiProfile,
|
probeAndSaveProfileCapabilities,
|
||||||
updateApiProfile,
|
updateApiProfile,
|
||||||
} from "../config/api-profiles.js";
|
} from "../config/api-profiles.js";
|
||||||
|
import {
|
||||||
|
deliveryModeLabel,
|
||||||
|
resolveStructuredDeliveryMode,
|
||||||
|
} from "../llm/capabilities.js";
|
||||||
import {
|
import {
|
||||||
ensureActiveProfileDefault,
|
ensureActiveProfileDefault,
|
||||||
loadAppSettings,
|
loadAppSettings,
|
||||||
@@ -120,12 +124,14 @@ export async function handleSettingsApi(
|
|||||||
baseUrl?: string;
|
baseUrl?: string;
|
||||||
apiKey?: string;
|
apiKey?: string;
|
||||||
model?: string;
|
model?: string;
|
||||||
|
reasoningEffort?: string;
|
||||||
};
|
};
|
||||||
const profile = createApiProfile({
|
const profile = createApiProfile({
|
||||||
name: body.name ?? "新配置",
|
name: body.name ?? "新配置",
|
||||||
baseUrl: body.baseUrl ?? "https://api.deepseek.com",
|
baseUrl: body.baseUrl ?? "https://api.deepseek.com",
|
||||||
apiKey: body.apiKey ?? "",
|
apiKey: body.apiKey ?? "",
|
||||||
model: body.model ?? "deepseek-v4-pro",
|
model: body.model ?? "deepseek-v4-pro",
|
||||||
|
reasoningEffort: body.reasoningEffort,
|
||||||
});
|
});
|
||||||
const activate = (body as { activate?: boolean }).activate === true;
|
const activate = (body as { activate?: boolean }).activate === true;
|
||||||
if (activate) {
|
if (activate) {
|
||||||
@@ -136,19 +142,34 @@ export async function handleSettingsApi(
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const profileMatch = pathname.match(/^\/api\/profiles\/([^/]+)(\/test)?$/);
|
const profileMatch = pathname.match(
|
||||||
|
/^\/api\/profiles\/([^/]+)(\/probe-capabilities)?$/,
|
||||||
|
);
|
||||||
if (profileMatch) {
|
if (profileMatch) {
|
||||||
const id = decodeURIComponent(profileMatch[1]);
|
const id = decodeURIComponent(profileMatch[1]);
|
||||||
const isTest = profileMatch[2] === "/test";
|
const suffix = profileMatch[2];
|
||||||
|
|
||||||
if (isTest && req.method === "POST") {
|
if (suffix === "/probe-capabilities" && req.method === "POST") {
|
||||||
const profile = getApiProfile(id);
|
try {
|
||||||
if (!profile) {
|
const profile = await probeAndSaveProfileCapabilities(id);
|
||||||
json(res, 404, { error: "配置不存在" });
|
const mode = resolveStructuredDeliveryMode(profile.capabilities);
|
||||||
return true;
|
const settings = loadAppSettings();
|
||||||
|
const reloadedSessions =
|
||||||
|
settings.activeProfileId === id
|
||||||
|
? sessionManager.reloadAllLlms()
|
||||||
|
: 0;
|
||||||
|
json(res, 200, {
|
||||||
|
ok: profile.capabilities?.chat === "ok",
|
||||||
|
profile,
|
||||||
|
deliveryMode: mode,
|
||||||
|
deliveryModeLabel: deliveryModeLabel(mode),
|
||||||
|
reloadedSessions,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
json(res, 400, {
|
||||||
|
error: err instanceof Error ? err.message : "能力探测失败",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
const result = await testApiProfile(profile);
|
|
||||||
json(res, 200, result);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,10 +189,16 @@ export async function handleSettingsApi(
|
|||||||
baseUrl?: string;
|
baseUrl?: string;
|
||||||
apiKey?: string;
|
apiKey?: string;
|
||||||
model?: string;
|
model?: string;
|
||||||
|
reasoningEffort?: string;
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
const profile = updateApiProfile(id, body);
|
const profile = updateApiProfile(id, body);
|
||||||
json(res, 200, { profile });
|
const settings = loadAppSettings();
|
||||||
|
const reloadedSessions =
|
||||||
|
settings.activeProfileId === id
|
||||||
|
? sessionManager.reloadAllLlms()
|
||||||
|
: 0;
|
||||||
|
json(res, 200, { profile, reloadedSessions });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
json(res, 404, {
|
json(res, 404, {
|
||||||
error: err instanceof Error ? err.message : "更新失败",
|
error: err instanceof Error ? err.message : "更新失败",
|
||||||
|
|||||||
@@ -123,6 +123,18 @@ const server = createServer(async (req, res) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (req.method === "GET" && sub === "/step-artifact") {
|
||||||
|
try {
|
||||||
|
const stepId = url.searchParams.get("stepId") ?? "";
|
||||||
|
const data = await sessionManager.getStepArtifact(sessionId, stepId);
|
||||||
|
json(res, 200, data);
|
||||||
|
} catch (err) {
|
||||||
|
const msg = err instanceof Error ? err.message : "读取失败";
|
||||||
|
json(res, msg.includes("不存在") ? 404 : 400, { error: msg });
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (req.method === "POST" && sub === "/messages") {
|
if (req.method === "POST" && sub === "/messages") {
|
||||||
const body = JSON.parse(await readBody(req)) as {
|
const body = JSON.parse(await readBody(req)) as {
|
||||||
text?: string;
|
text?: string;
|
||||||
@@ -445,6 +457,10 @@ const server = createServer(async (req, res) => {
|
|||||||
action?: string;
|
action?: string;
|
||||||
stepParams?: Record<string, unknown>;
|
stepParams?: Record<string, unknown>;
|
||||||
openingIndex?: number;
|
openingIndex?: number;
|
||||||
|
reason?: string;
|
||||||
|
stepId?: string;
|
||||||
|
moduleName?: string;
|
||||||
|
reenter?: boolean;
|
||||||
};
|
};
|
||||||
let view;
|
let view;
|
||||||
try {
|
try {
|
||||||
@@ -474,6 +490,31 @@ const server = createServer(async (req, res) => {
|
|||||||
case "reject":
|
case "reject":
|
||||||
view = await sessionManager.reject(sessionId);
|
view = await sessionManager.reject(sessionId);
|
||||||
break;
|
break;
|
||||||
|
case "replan":
|
||||||
|
view = await sessionManager.replan(
|
||||||
|
sessionId,
|
||||||
|
typeof body.reason === "string" ? body.reason : undefined,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case "leave_step":
|
||||||
|
view = await sessionManager.leaveStep(sessionId);
|
||||||
|
break;
|
||||||
|
case "pick_step":
|
||||||
|
view = await sessionManager.pickStep(
|
||||||
|
sessionId,
|
||||||
|
typeof body.stepId === "string" ? body.stepId : "",
|
||||||
|
body.stepParams && typeof body.stepParams === "object"
|
||||||
|
? body.stepParams
|
||||||
|
: undefined,
|
||||||
|
body.reenter === true,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case "spawn_step":
|
||||||
|
view = await sessionManager.spawnStep(
|
||||||
|
sessionId,
|
||||||
|
typeof body.moduleName === "string" ? body.moduleName : "",
|
||||||
|
);
|
||||||
|
break;
|
||||||
case "run_outline":
|
case "run_outline":
|
||||||
view = await sessionManager.runOutline(sessionId);
|
view = await sessionManager.runOutline(sessionId);
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* 上下文片段产物(context-fragment.v1)解析与展示辅助。
|
* 上下文片段产物(context-fragment.v1)解析与展示辅助。
|
||||||
* 规范:docs/context-fragment-design.md
|
* 规范:docs/context-fragment-design.md
|
||||||
*/
|
*/
|
||||||
import { extractJsonObjectText } from "./worker-set-parse.js";
|
import { tryParseJsonDoc } from "../parse/json-doc.js";
|
||||||
import {
|
import {
|
||||||
normalizeQuestions,
|
normalizeQuestions,
|
||||||
type QuestionItem,
|
type QuestionItem,
|
||||||
@@ -75,15 +75,10 @@ function normalizeStability(v: unknown): ContextFragmentStability | undefined {
|
|||||||
export function parseContextFragment(raw: unknown): ContextFragment | undefined {
|
export function parseContextFragment(raw: unknown): ContextFragment | undefined {
|
||||||
let row: Record<string, unknown> | null = null;
|
let row: Record<string, unknown> | null = null;
|
||||||
if (typeof raw === "string") {
|
if (typeof raw === "string") {
|
||||||
const text = extractJsonObjectText(raw) ?? raw.trim();
|
const parsed = tryParseJsonDoc(raw);
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(text) as unknown;
|
|
||||||
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
||||||
row = parsed as Record<string, unknown>;
|
row = parsed as Record<string, unknown>;
|
||||||
}
|
}
|
||||||
} catch {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
} else if (raw && typeof raw === "object" && !Array.isArray(raw)) {
|
} else if (raw && typeof raw === "object" && !Array.isArray(raw)) {
|
||||||
row = raw as Record<string, unknown>;
|
row = raw as Record<string, unknown>;
|
||||||
}
|
}
|
||||||
@@ -278,15 +273,10 @@ export function extractFragmentAskSidecar(raw: unknown): {
|
|||||||
} {
|
} {
|
||||||
let row: Record<string, unknown> | null = null;
|
let row: Record<string, unknown> | null = null;
|
||||||
if (typeof raw === "string") {
|
if (typeof raw === "string") {
|
||||||
const text = extractJsonObjectText(raw) ?? raw.trim();
|
const parsed = tryParseJsonDoc(raw);
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(text) as unknown;
|
|
||||||
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
||||||
row = parsed as Record<string, unknown>;
|
row = parsed as Record<string, unknown>;
|
||||||
}
|
}
|
||||||
} catch {
|
|
||||||
return { questions: [], assessment: "" };
|
|
||||||
}
|
|
||||||
} else if (raw && typeof raw === "object" && !Array.isArray(raw)) {
|
} else if (raw && typeof raw === "object" && !Array.isArray(raw)) {
|
||||||
row = raw as Record<string, unknown>;
|
row = raw as Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|||||||
+1029
-76
File diff suppressed because it is too large
Load Diff
@@ -44,7 +44,8 @@ type WorkerTemplateDoc = {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 解析本次 worker 的验收模式(创作 / run 共用入口)。
|
* 解析本次 worker 的验收模式(创作 / run 共用入口)。
|
||||||
* - design-* / opening-generator 磁盘创作 worker → 始终 user_confirmed
|
* - design-flow → no_confirmation(图上点节点即确认;底栏输入即改编排)
|
||||||
|
* - design-step / opening-generator 磁盘创作 worker → 始终 user_confirmed
|
||||||
* - 游玩回合:一律 no_confirmation(一句输入连跑,不穿插验收)
|
* - 游玩回合:一律 no_confirmation(一句输入连跑,不穿插验收)
|
||||||
* - Worker 集 acceptance: review → user_confirmed;continue → no_confirmation
|
* - Worker 集 acceptance: review → user_confirmed;continue → no_confirmation
|
||||||
* - 未声明 acceptance:创作默认确认
|
* - 未声明 acceptance:创作默认确认
|
||||||
@@ -55,6 +56,7 @@ export function resolveAcceptanceModeForWorker(params: {
|
|||||||
workerId: string;
|
workerId: string;
|
||||||
}): AcceptanceMode {
|
}): AcceptanceMode {
|
||||||
const workerId = params.workerId.trim();
|
const workerId = params.workerId.trim();
|
||||||
|
if (workerId === "design-flow") return "no_confirmation";
|
||||||
if (isDesignDiskWorker(workerId) || workerId === "opening-generator") {
|
if (isDesignDiskWorker(workerId) || workerId === "opening-generator") {
|
||||||
return "user_confirmed";
|
return "user_confirmed";
|
||||||
}
|
}
|
||||||
@@ -95,7 +97,7 @@ export async function resolveRunnableWorker(params: {
|
|||||||
const acceptedRaw =
|
const acceptedRaw =
|
||||||
params.session.slots?.creationAcceptedUnits ??
|
params.session.slots?.creationAcceptedUnits ??
|
||||||
params.blackboard.getContentByTag("创作.已验收单位");
|
params.blackboard.getContentByTag("创作.已验收单位");
|
||||||
const { parseAcceptedSteps } = await import("./creation-flow.js");
|
const { parseAcceptedSteps, isInheritExistingFlag } = await import("./creation-flow.js");
|
||||||
const withCtx = await loadWorkerSkillWithContext(
|
const withCtx = await loadWorkerSkillWithContext(
|
||||||
params.skillPackName,
|
params.skillPackName,
|
||||||
params.workerId,
|
params.workerId,
|
||||||
@@ -106,6 +108,9 @@ export async function resolveRunnableWorker(params: {
|
|||||||
acceptedStepNames: parseAcceptedSteps(acceptedRaw),
|
acceptedStepNames: parseAcceptedSteps(acceptedRaw),
|
||||||
selectedRecipeRef,
|
selectedRecipeRef,
|
||||||
filledArtifactTags: params.blackboard.listTagIndex().map((item) => item.tag),
|
filledArtifactTags: params.blackboard.listTagIndex().map((item) => item.tag),
|
||||||
|
inheritExisting: isInheritExistingFlag(
|
||||||
|
params.blackboard.getContentByTag("创作.继承修改"),
|
||||||
|
),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -0,0 +1,258 @@
|
|||||||
|
/**
|
||||||
|
* 扁平产物契约(wire format):机器字段薄,正文用 sections[] 字符串块。
|
||||||
|
* 渲染与旧 context-fragment.v1 深树通过 normalize 互转。
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const FLAT_ARTIFACT_SCHEMA_ID = "artifact.flat.v1" as const;
|
||||||
|
|
||||||
|
export type FlatArtifactQuestion = {
|
||||||
|
id: string;
|
||||||
|
prompt: string;
|
||||||
|
options: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FlatArtifactSection = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
text: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 所有需程序校验的创作产物尽量收成此形 */
|
||||||
|
export type FlatArtifact = {
|
||||||
|
schema: typeof FLAT_ARTIFACT_SCHEMA_ID | string;
|
||||||
|
skill: string;
|
||||||
|
brief: string;
|
||||||
|
mount: string[];
|
||||||
|
stability: "stable" | "semi" | "volatile" | "";
|
||||||
|
sections: FlatArtifactSection[];
|
||||||
|
open_questions: string[];
|
||||||
|
questions: FlatArtifactQuestion[];
|
||||||
|
summary: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** OpenAI/DeepSeek strict 友好的 JSON Schema(全 required + additionalProperties:false) */
|
||||||
|
export const FLAT_ARTIFACT_JSON_SCHEMA: Record<string, unknown> = {
|
||||||
|
type: "object",
|
||||||
|
additionalProperties: false,
|
||||||
|
required: [
|
||||||
|
"schema",
|
||||||
|
"skill",
|
||||||
|
"brief",
|
||||||
|
"mount",
|
||||||
|
"stability",
|
||||||
|
"sections",
|
||||||
|
"open_questions",
|
||||||
|
"questions",
|
||||||
|
"summary",
|
||||||
|
],
|
||||||
|
properties: {
|
||||||
|
schema: { type: "string" },
|
||||||
|
skill: { type: "string" },
|
||||||
|
brief: { type: "string" },
|
||||||
|
mount: { type: "array", items: { type: "string" } },
|
||||||
|
stability: {
|
||||||
|
type: "string",
|
||||||
|
enum: ["stable", "semi", "volatile", ""],
|
||||||
|
},
|
||||||
|
sections: {
|
||||||
|
type: "array",
|
||||||
|
items: {
|
||||||
|
type: "object",
|
||||||
|
additionalProperties: false,
|
||||||
|
required: ["id", "title", "text"],
|
||||||
|
properties: {
|
||||||
|
id: { type: "string" },
|
||||||
|
title: { type: "string" },
|
||||||
|
text: { type: "string" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
open_questions: { type: "array", items: { type: "string" } },
|
||||||
|
questions: {
|
||||||
|
type: "array",
|
||||||
|
items: {
|
||||||
|
type: "object",
|
||||||
|
additionalProperties: false,
|
||||||
|
required: ["id", "prompt", "options"],
|
||||||
|
properties: {
|
||||||
|
id: { type: "string" },
|
||||||
|
prompt: { type: "string" },
|
||||||
|
options: { type: "array", items: { type: "string" } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
summary: { type: "string" },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export function isFlatArtifact(raw: unknown): raw is FlatArtifact {
|
||||||
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return false;
|
||||||
|
const row = raw as Record<string, unknown>;
|
||||||
|
return Array.isArray(row.sections);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 扁形 → 旧 fragment 外壳,便于现有渲染;正文按 section id 堆对象。
|
||||||
|
* section.text 若是 JSON 对象/数组字符串,解析回结构,避免专用卡吃到整段 stringify。
|
||||||
|
*/
|
||||||
|
export function flatArtifactToFragmentDoc(flat: FlatArtifact): Record<string, unknown> {
|
||||||
|
const 正文: Record<string, unknown> = {};
|
||||||
|
for (const s of flat.sections) {
|
||||||
|
const key = s.id || s.title;
|
||||||
|
if (!key) continue;
|
||||||
|
正文[key] = coerceSectionText(s.text);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
schema: "context-fragment.v1",
|
||||||
|
技能: flat.skill,
|
||||||
|
brief: flat.brief,
|
||||||
|
mount: flat.mount,
|
||||||
|
...(flat.stability ? { 稳变: flat.stability } : {}),
|
||||||
|
正文,
|
||||||
|
开放问题: flat.open_questions,
|
||||||
|
追问: {
|
||||||
|
导语: "",
|
||||||
|
题目: flat.questions.map((q) => ({
|
||||||
|
问: q.prompt,
|
||||||
|
建议选项: q.options,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
...(flat.summary ? { summary: flat.summary } : {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** section 文本:纯散文保留;JSON 对象/数组还原为结构 */
|
||||||
|
export function coerceSectionText(text: string): unknown {
|
||||||
|
const raw = String(text ?? "").trim();
|
||||||
|
if (!raw) return "";
|
||||||
|
if (
|
||||||
|
!(
|
||||||
|
(raw.startsWith("{") && raw.endsWith("}")) ||
|
||||||
|
(raw.startsWith("[") && raw.endsWith("]"))
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return String(text ?? "");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return JSON.parse(raw) as unknown;
|
||||||
|
} catch {
|
||||||
|
return String(text ?? "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 旧深树 / 任意对象 → 尽量收成扁形(保存与投递归一) */
|
||||||
|
export function coerceToFlatArtifact(raw: unknown): FlatArtifact | null {
|
||||||
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
||||||
|
const row = raw as Record<string, unknown>;
|
||||||
|
|
||||||
|
// DAG / worker 集 / 上下文排序:不是扁形正文稿,不能靠 brief 误收
|
||||||
|
if (
|
||||||
|
Array.isArray(row.steps) ||
|
||||||
|
Array.isArray(row.workers) ||
|
||||||
|
Array.isArray(row.agents) ||
|
||||||
|
Array.isArray(row.slots)
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isFlatArtifact(raw)) {
|
||||||
|
return {
|
||||||
|
schema:
|
||||||
|
typeof row.schema === "string" ? row.schema : FLAT_ARTIFACT_SCHEMA_ID,
|
||||||
|
skill: String(row.skill ?? row.技能 ?? ""),
|
||||||
|
brief: String(row.brief ?? ""),
|
||||||
|
mount: Array.isArray(row.mount)
|
||||||
|
? row.mount.filter((x): x is string => typeof x === "string")
|
||||||
|
: [],
|
||||||
|
stability:
|
||||||
|
row.stability === "stable" ||
|
||||||
|
row.stability === "semi" ||
|
||||||
|
row.stability === "volatile"
|
||||||
|
? row.stability
|
||||||
|
: row.稳变 === "stable" ||
|
||||||
|
row.稳变 === "semi" ||
|
||||||
|
row.稳变 === "volatile"
|
||||||
|
? row.稳变
|
||||||
|
: "",
|
||||||
|
sections: (row.sections as FlatArtifactSection[]).map((s) => ({
|
||||||
|
id: String(s?.id ?? ""),
|
||||||
|
title: String(s?.title ?? ""),
|
||||||
|
text: String(s?.text ?? ""),
|
||||||
|
})),
|
||||||
|
open_questions: Array.isArray(row.open_questions)
|
||||||
|
? row.open_questions.filter((x): x is string => typeof x === "string")
|
||||||
|
: Array.isArray(row.开放问题)
|
||||||
|
? row.开放问题.filter((x): x is string => typeof x === "string")
|
||||||
|
: [],
|
||||||
|
questions: Array.isArray(row.questions)
|
||||||
|
? (row.questions as FlatArtifactQuestion[]).map((q, i) => ({
|
||||||
|
id: String(q?.id ?? `q${i + 1}`),
|
||||||
|
prompt: String(q?.prompt ?? ""),
|
||||||
|
options: Array.isArray(q?.options)
|
||||||
|
? q.options.filter((x): x is string => typeof x === "string")
|
||||||
|
: [],
|
||||||
|
}))
|
||||||
|
: [],
|
||||||
|
summary: String(row.summary ?? ""),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const 正文 = row.正文 ?? row.body;
|
||||||
|
const sections: FlatArtifactSection[] = [];
|
||||||
|
if (正文 && typeof 正文 === "object" && !Array.isArray(正文)) {
|
||||||
|
for (const [key, val] of Object.entries(正文 as Record<string, unknown>)) {
|
||||||
|
sections.push({
|
||||||
|
id: key,
|
||||||
|
title: key,
|
||||||
|
text:
|
||||||
|
typeof val === "string"
|
||||||
|
? val
|
||||||
|
: JSON.stringify(val, null, 2),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (typeof 正文 === "string" && 正文.trim()) {
|
||||||
|
sections.push({ id: "body", title: "正文", text: 正文 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const probe = row.追问;
|
||||||
|
const questions: FlatArtifactQuestion[] = [];
|
||||||
|
if (probe && typeof probe === "object" && !Array.isArray(probe)) {
|
||||||
|
const topics = (probe as { 题目?: unknown }).题目;
|
||||||
|
if (Array.isArray(topics)) {
|
||||||
|
topics.forEach((t, i) => {
|
||||||
|
if (!t || typeof t !== "object") return;
|
||||||
|
const item = t as Record<string, unknown>;
|
||||||
|
questions.push({
|
||||||
|
id: `q${i + 1}`,
|
||||||
|
prompt: String(item.问 ?? item.prompt ?? ""),
|
||||||
|
options: Array.isArray(item.建议选项)
|
||||||
|
? item.建议选项.filter((x): x is string => typeof x === "string")
|
||||||
|
: Array.isArray(item.options)
|
||||||
|
? item.options.filter((x): x is string => typeof x === "string")
|
||||||
|
: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sections.length) return null;
|
||||||
|
|
||||||
|
const stab = row.稳变 ?? row.stability;
|
||||||
|
return {
|
||||||
|
schema: FLAT_ARTIFACT_SCHEMA_ID,
|
||||||
|
skill: String(row.技能 ?? row.skill ?? ""),
|
||||||
|
brief: String(row.brief ?? ""),
|
||||||
|
mount: Array.isArray(row.mount)
|
||||||
|
? row.mount.filter((x): x is string => typeof x === "string")
|
||||||
|
: [],
|
||||||
|
stability:
|
||||||
|
stab === "stable" || stab === "semi" || stab === "volatile" ? stab : "",
|
||||||
|
sections,
|
||||||
|
open_questions: Array.isArray(row.开放问题)
|
||||||
|
? row.开放问题.filter((x): x is string => typeof x === "string")
|
||||||
|
: Array.isArray(row.open_questions)
|
||||||
|
? row.open_questions.filter((x): x is string => typeof x === "string")
|
||||||
|
: [],
|
||||||
|
questions,
|
||||||
|
summary: String(row.summary ?? ""),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -475,6 +475,8 @@ export async function loadWorkerSkillWithContext(
|
|||||||
selectedRecipeRef?: string | null;
|
selectedRecipeRef?: string | null;
|
||||||
/** 黑板已有产物 tag,供编排器判断哪些技能已经做过 */
|
/** 黑板已有产物 tag,供编排器判断哪些技能已经做过 */
|
||||||
filledArtifactTags?: readonly string[];
|
filledArtifactTags?: readonly string[];
|
||||||
|
/** 用户点已完成节点重进 */
|
||||||
|
inheritExisting?: boolean;
|
||||||
},
|
},
|
||||||
): Promise<{ worker: ParsedWorkerSkill; sharedContext: string | null; promptBody: string }> {
|
): Promise<{ worker: ParsedWorkerSkill; sharedContext: string | null; promptBody: string }> {
|
||||||
const worker = await loadWorkerSkill(skillIdOrName, workerId, skillsRoot);
|
const worker = await loadWorkerSkill(skillIdOrName, workerId, skillsRoot);
|
||||||
@@ -526,6 +528,7 @@ export async function loadWorkerSkillWithContext(
|
|||||||
currentStepName: opts?.currentStepName,
|
currentStepName: opts?.currentStepName,
|
||||||
acceptedStepNames: opts?.acceptedStepNames,
|
acceptedStepNames: opts?.acceptedStepNames,
|
||||||
skillsRoot,
|
skillsRoot,
|
||||||
|
inheritExisting: opts?.inheritExisting,
|
||||||
});
|
});
|
||||||
if (binding) {
|
if (binding) {
|
||||||
const inheritTag = binding.inheritTag?.trim() || "";
|
const inheritTag = binding.inheritTag?.trim() || "";
|
||||||
@@ -536,8 +539,11 @@ export async function loadWorkerSkillWithContext(
|
|||||||
? `\n\n【程序开场】若黑板有「${CREATION_MODULE_OPENING_TAG}」,该默认问题已由程序发给用户(不经 LLM);用户首答在「用户.worker答复」。勿重复同一开场白,在其答复与提示词基础上继续追问或产出。`
|
? `\n\n【程序开场】若黑板有「${CREATION_MODULE_OPENING_TAG}」,该默认问题已由程序发给用户(不经 LLM);用户首答在「用户.worker答复」。勿重复同一开场白,在其答复与提示词基础上继续追问或产出。`
|
||||||
: "";
|
: "";
|
||||||
const paramsBlock = isPriorArtifactModule(binding.module)
|
const paramsBlock = isPriorArtifactModule(binding.module)
|
||||||
? formatPriorArtifactContext(binding.step.params)
|
? formatPriorArtifactContext(
|
||||||
: `## 【本步参数】(编排期已钉;直接按此执行,勿再问「生成什么 / 调用哪个规则」)\n\n${formatStepParamsForPrompt(binding.step.params)}`;
|
binding.step.params,
|
||||||
|
binding.prototypeSuggestion,
|
||||||
|
)
|
||||||
|
: `## 【本步参数】(编排期已钉;直接按此执行,勿再问编排层已定的参数)\n\n${formatStepParamsForPrompt(binding.step.params)}`;
|
||||||
let rosterBlock = "";
|
let rosterBlock = "";
|
||||||
const depProjection =
|
const depProjection =
|
||||||
binding.module.id === "context-order" ? "index" : undefined;
|
binding.module.id === "context-order" ? "index" : undefined;
|
||||||
|
|||||||
@@ -198,3 +198,11 @@ export function closeCreationFlowRaw(raw: string | null | undefined): string | n
|
|||||||
if (flow.status === "closed") return stringifyCreationFlow(flow);
|
if (flow.status === "closed") return stringifyCreationFlow(flow);
|
||||||
return stringifyCreationFlow({ ...flow, status: "closed" });
|
return stringifyCreationFlow({ ...flow, status: "closed" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 再编排:收口前补节点时把 DAG 改回 open。 */
|
||||||
|
export function reopenCreationFlowRaw(raw: string | null | undefined): string | null {
|
||||||
|
const flow = parseCreationFlow(raw);
|
||||||
|
if (!flow) return raw?.trim() ? raw : null;
|
||||||
|
if (flow.status !== "closed") return stringifyCreationFlow(flow);
|
||||||
|
return stringifyCreationFlow({ ...flow, status: "open" });
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { parse as parseYaml } from "yaml";
|
import { parse as parseYaml } from "yaml";
|
||||||
|
import { tryParseJsonDoc } from "../parse/json-doc.js";
|
||||||
import {
|
import {
|
||||||
inferPlaySlotsFromWorkers,
|
inferPlaySlotsFromWorkers,
|
||||||
mergeWorkersWithPlaySlots,
|
mergeWorkersWithPlaySlots,
|
||||||
@@ -400,6 +401,10 @@ export function parseWorkerSetYaml(raw: string | undefined): ParsedWorkerSet | n
|
|||||||
|
|
||||||
/** 从纯 JSON、```json 围栏或夹杂说明的文本中抽出对象字面量 */
|
/** 从纯 JSON、```json 围栏或夹杂说明的文本中抽出对象字面量 */
|
||||||
export function extractJsonObjectText(raw: string): string | null {
|
export function extractJsonObjectText(raw: string): string | null {
|
||||||
|
const parsed = tryParseJsonDoc(raw);
|
||||||
|
if (parsed != null && typeof parsed === "object") {
|
||||||
|
return JSON.stringify(parsed);
|
||||||
|
}
|
||||||
const text = raw.trim();
|
const text = raw.trim();
|
||||||
if (!text) return null;
|
if (!text) return null;
|
||||||
if (text.startsWith("{")) {
|
if (text.startsWith("{")) {
|
||||||
|
|||||||
@@ -72,9 +72,16 @@ export function buildContextTrace(params: {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 编排检查点:不随「只留最新 N 条」丢掉,否则开场白阶段无法回看/回退到流程编排 */
|
||||||
|
export function isPinnedContextTraceCaller(caller: string | undefined): boolean {
|
||||||
|
const id = (caller ?? "").trim();
|
||||||
|
return id === "worker:design-flow" || id.endsWith(":design-flow");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 只保留「带 contextTrace 的消息」中最新 keepLatest 条的全文;
|
* 只保留「带 contextTrace 的消息」中最新 keepLatest 条的全文;
|
||||||
* 更早的消息删除 contextTrace 字段(消息本身保留)。
|
* 更早的消息删除 contextTrace 字段(消息本身保留)。
|
||||||
|
* design-flow 痕迹钉住不删(keep=0 仍全部清除)。
|
||||||
*/
|
*/
|
||||||
export function pruneContextTraces<T extends { contextTrace?: LlmContextTrace }>(
|
export function pruneContextTraces<T extends { contextTrace?: LlmContextTrace }>(
|
||||||
messages: T[],
|
messages: T[],
|
||||||
@@ -88,12 +95,15 @@ export function pruneContextTraces<T extends { contextTrace?: LlmContextTrace }>
|
|||||||
return rest as T;
|
return rest as T;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const withTraceIdx: number[] = [];
|
const droppableIdx: number[] = [];
|
||||||
for (let i = 0; i < messages.length; i++) {
|
for (let i = 0; i < messages.length; i++) {
|
||||||
if (messages[i].contextTrace) withTraceIdx.push(i);
|
const trace = messages[i].contextTrace;
|
||||||
|
if (!trace) continue;
|
||||||
|
if (isPinnedContextTraceCaller(trace.caller)) continue;
|
||||||
|
droppableIdx.push(i);
|
||||||
}
|
}
|
||||||
if (withTraceIdx.length <= keep) return messages;
|
if (droppableIdx.length <= keep) return messages;
|
||||||
const drop = new Set(withTraceIdx.slice(0, withTraceIdx.length - keep));
|
const drop = new Set(droppableIdx.slice(0, droppableIdx.length - keep));
|
||||||
return messages.map((m, i) => {
|
return messages.map((m, i) => {
|
||||||
if (!drop.has(i) || !m.contextTrace) return m;
|
if (!drop.has(i) || !m.contextTrace) return m;
|
||||||
const { contextTrace: _drop, ...rest } = m as T & { contextTrace?: LlmContextTrace };
|
const { contextTrace: _drop, ...rest } = m as T & { contextTrace?: LlmContextTrace };
|
||||||
|
|||||||
+30
-1
@@ -60,6 +60,13 @@ export type WaitingReason =
|
|||||||
kind: "next_intent";
|
kind: "next_intent";
|
||||||
afterWorkerId?: string;
|
afterWorkerId?: string;
|
||||||
afterUnitId?: string;
|
afterUnitId?: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
/**
|
||||||
|
* DAG 已在:按依赖层级点选节点。依赖未齐的不能点;
|
||||||
|
* 可反复能力以 role=prototype 槽位呈现;点原型增殖 instance 再进去。
|
||||||
|
*/
|
||||||
|
kind: "pick_creation_step";
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 与 src/skills/types 对齐的最小 skill 索引字段,避免 runtime 强依赖 skills 模块 */
|
/** 与 src/skills/types 对齐的最小 skill 索引字段,避免 runtime 强依赖 skills 模块 */
|
||||||
@@ -232,6 +239,26 @@ export type RuntimeEvent =
|
|||||||
type: "user_rejected_next_step";
|
type: "user_rejected_next_step";
|
||||||
payload: { decisionId: string; reason?: string };
|
payload: { decisionId: string; reason?: string };
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
/** 回到流程编排:在已验收步骤上追加/改排节点(不整局重开) */
|
||||||
|
type: "user_requested_flow_replan";
|
||||||
|
payload: { reason?: string };
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
/** DAG 图上点选一条已就绪的步骤,进入 design-step */
|
||||||
|
type: "user_picked_creation_step";
|
||||||
|
payload: { stepId: string };
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
/** 验收后 / 总管误调 design-step:停在分层图上等用户点选 */
|
||||||
|
type: "creation_step_pick_awaited";
|
||||||
|
payload?: Record<string, never>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
/** 离开当前技能步,回到分层图点选(误点增殖节点时用) */
|
||||||
|
type: "user_left_creation_step";
|
||||||
|
payload?: Record<string, never>;
|
||||||
|
}
|
||||||
| {
|
| {
|
||||||
type: "worker_started";
|
type: "worker_started";
|
||||||
payload: {
|
payload: {
|
||||||
@@ -331,10 +358,12 @@ export type PhaseEffect =
|
|||||||
| { type: "resume_worker" }
|
| { type: "resume_worker" }
|
||||||
| { type: "run_programmatic_review"; artifactId: string }
|
| { type: "run_programmatic_review"; artifactId: string }
|
||||||
| { type: "emit_message"; message: string }
|
| { type: "emit_message"; message: string }
|
||||||
/** 根据流程与用户下一步意向,提案下一节点并进入确认 */
|
/** 根据流程与用户下一步意向,提案下一节点或停在分层图上等点选 */
|
||||||
| { type: "propose_next_creation_step" }
|
| { type: "propose_next_creation_step" }
|
||||||
/** 选定开场白:落库、关 DAG、标记创作收口 */
|
/** 选定开场白:落库、关 DAG、标记创作收口 */
|
||||||
| { type: "seal_creation_opening" }
|
| { type: "seal_creation_opening" }
|
||||||
|
/** 再编排:解开开场收口、把 DAG 改回 open */
|
||||||
|
| { type: "unseal_creation_opening" }
|
||||||
/** 游玩:按运行规格开一轮(旁观 → 主世界层 → 转述) */
|
/** 游玩:按运行规格开一轮(旁观 → 主世界层 → 转述) */
|
||||||
| { type: "run_play_turn" }
|
| { type: "run_play_turn" }
|
||||||
/** 游玩:跑管线里的下一个执行单元;队空则等下一条用户输入 */
|
/** 游玩:跑管线里的下一个执行单元;队空则等下一条用户输入 */
|
||||||
|
|||||||
+126
-63
@@ -1,6 +1,7 @@
|
|||||||
import type { Blackboard } from "../blackboard/blackboard.js";
|
import type { Blackboard } from "../blackboard/blackboard.js";
|
||||||
import type { LlmProvider, StreamCallbacks, ChatMessage } from "../llm/client.js";
|
import type { LlmProvider, StreamCallbacks, ChatMessage } from "../llm/client.js";
|
||||||
import { supportsContentStream } from "../llm/stream-complete.js";
|
import { completeStructured } from "../llm/structured-complete.js";
|
||||||
|
import { resolveActiveProfile } from "../config/settings.js";
|
||||||
import { loadWorkerSkillWithContext } from "../skills/loader.js";
|
import { loadWorkerSkillWithContext } from "../skills/loader.js";
|
||||||
import { filterInputsForRolePerspective } from "../skills/worker-llm.js";
|
import { filterInputsForRolePerspective } from "../skills/worker-llm.js";
|
||||||
import { collectUserInputTranscript } from "../intake/intake.js";
|
import { collectUserInputTranscript } from "../intake/intake.js";
|
||||||
@@ -14,6 +15,10 @@ import {
|
|||||||
looksLikeProseNotSpec,
|
looksLikeProseNotSpec,
|
||||||
parseWorkerSetYaml,
|
parseWorkerSetYaml,
|
||||||
} from "../skills/worker-set-parse.js";
|
} from "../skills/worker-set-parse.js";
|
||||||
|
import {
|
||||||
|
selectJsonPayload,
|
||||||
|
tryParseJsonDoc,
|
||||||
|
} from "../parse/json-doc.js";
|
||||||
import { assembleWorkerContext } from "../skills/context-segments.js";
|
import { assembleWorkerContext } from "../skills/context-segments.js";
|
||||||
import { isPlayLayerActive } from "../skills/play-turn.js";
|
import { isPlayLayerActive } from "../skills/play-turn.js";
|
||||||
import { loadAppSettings } from "../config/settings.js";
|
import { loadAppSettings } from "../config/settings.js";
|
||||||
@@ -21,6 +26,10 @@ import { resolveActivePreset } from "../preset/store.js";
|
|||||||
import { assemblePlayWorkerMessages } from "../preset/play-frame.js";
|
import { assemblePlayWorkerMessages } from "../preset/play-frame.js";
|
||||||
import { worldInfoPackFromSegments } from "../preset/world-info-pack.js";
|
import { worldInfoPackFromSegments } from "../preset/world-info-pack.js";
|
||||||
import { PRESENT_TAG } from "../skills/present-packet.js";
|
import { PRESENT_TAG } from "../skills/present-packet.js";
|
||||||
|
import {
|
||||||
|
coerceToFlatArtifact,
|
||||||
|
flatArtifactToFragmentDoc,
|
||||||
|
} from "../skills/flat-artifact.js";
|
||||||
import {
|
import {
|
||||||
gmChanceToolsEnabled,
|
gmChanceToolsEnabled,
|
||||||
GM_CHANCE_HARNESS_INSTRUCTION,
|
GM_CHANCE_HARNESS_INSTRUCTION,
|
||||||
@@ -36,7 +45,10 @@ import {
|
|||||||
isUsableContextFragment,
|
isUsableContextFragment,
|
||||||
looksLikeFragmentDoc,
|
looksLikeFragmentDoc,
|
||||||
} from "../skills/context-fragment.js";
|
} from "../skills/context-fragment.js";
|
||||||
import { isProgressPointerTag } from "../skills/creation-flow.js";
|
import {
|
||||||
|
isProgressPointerTag,
|
||||||
|
looksLikeCreationFlowDoc,
|
||||||
|
} from "../skills/creation-flow.js";
|
||||||
import {
|
import {
|
||||||
CONTEXT_ORDER_TAG,
|
CONTEXT_ORDER_TAG,
|
||||||
parseContextOrder,
|
parseContextOrder,
|
||||||
@@ -93,28 +105,19 @@ const WORKER_OUTPUT_INSTRUCTION = `
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 运行时输出协议(必须遵守)
|
## 运行时输出协议
|
||||||
|
|
||||||
请输出 **单个 JSON 对象**(不要 markdown 代码块),字段:
|
输出 **一个 JSON 对象**(由程序按模型能力用 schema / tool / json_object 约束投递)。
|
||||||
{
|
|
||||||
"outputs": { "<outputTag>": "<内容字符串>" },
|
|
||||||
"summary": "50字以内产物摘要",
|
|
||||||
"askUser": null 或 问题数组
|
|
||||||
}
|
|
||||||
|
|
||||||
askUser 每项可为:
|
按本步 SKILL 规定的产物形状写。有正文的技能稿按该步 \`正文\` 对象写(柱下到条目/诊断三元组),不要把业务字段改成一段 \`sections[].text\`。
|
||||||
- 字符串:"需要用户补充的问题"
|
「设计.创作流程」必须是带 \`steps\` 数组的 DAG,worker 集必须是带 \`workers\` 的规格。
|
||||||
- 或结构化:{ "id": "q1", "prompt": "问题", "options": [{ "id": "A", "label": "可编辑完整句选项" }], "allowOther": true }
|
|
||||||
|
|
||||||
- outputs 的 key 必须是要求的 outputTags
|
也兼容:
|
||||||
- **设计.worker集 / 设计.worker集.草稿**:value 必须是 JSON 对象文本(以 { 开头),禁止中文说明、元叙述、提问长文;禁止 YAML
|
1. 直接输出本步产物对象(创作流程 / context-fragment.v1 / worker 集等)
|
||||||
- **context-fragment.v1**:必须放在 outputs["<本步 artifact tag>"],禁止把片段当根对象。题目只写在产物「追问」(建议选项 + 示例);顶层 askUser 必须为 **null**。程序会把「追问」挂到询问卡。禁止同一问再抄一份 askUser。禁止写入「创作.当前步骤」等进度指针
|
2. \`{ "outputs": { "<本步 tag>": { … } }, "summary":"…", "askUser": null }\`(values 用对象)
|
||||||
- **其它产物**:优先同时给 outputs + askUser;有产物时追问挂在产物下(用户可直接接受而不作答)
|
|
||||||
- **残稿也要交**:JSON 不完美、字段不全、截断,只要有可展示正文就必须写入 outputs,交给用户验收(可打回重生成,也可接着改)。不要因为渲染不出就把稿扔掉改成提问
|
不要写入进度指针。追问用 \`questions\`(或旧 \`追问.题目\`);\`questions: []\` 表示可验收。
|
||||||
- 仅当完全没有可展示正文、只有提问时,才留空 outputs、只填 askUser
|
「开放问题」留给后续步。残稿也要交。summary 用于界面展示。`;
|
||||||
- 能推断选项时 **必须**给 options(完整句、可改写);不要只丢裸问题逼用户写长段
|
|
||||||
- 若 inputs 中 \`用户.需求\` / \`用户.博弈需求\`(或 book.brief)已有实质内容,禁止 askUser 要求用户重复提供其中已写明的情境、角色、规则等;仅对 genuinely 缺失且无法推断的要点提问
|
|
||||||
- summary 用于界面展示`;
|
|
||||||
|
|
||||||
function slotValueForTag(
|
function slotValueForTag(
|
||||||
tag: string,
|
tag: string,
|
||||||
@@ -193,6 +196,13 @@ function assignOutput(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 模型常把片段当根对象,或写进「创作.当前步骤」 */
|
/** 模型常把片段当根对象,或写进「创作.当前步骤」 */
|
||||||
|
function existingOutputIsProduct(text: string | undefined): boolean {
|
||||||
|
if (!text?.trim()) return false;
|
||||||
|
if (isUsableContextFragment(text)) return true;
|
||||||
|
const parsed = tryParseJsonDoc(text);
|
||||||
|
return looksLikeCreationFlowDoc(parsed) || looksLikeFragmentDoc(parsed);
|
||||||
|
}
|
||||||
|
|
||||||
function recoverFragmentOutputs(
|
function recoverFragmentOutputs(
|
||||||
obj: Record<string, unknown>,
|
obj: Record<string, unknown>,
|
||||||
outputs: Record<string, string>,
|
outputs: Record<string, string>,
|
||||||
@@ -200,7 +210,7 @@ function recoverFragmentOutputs(
|
|||||||
): void {
|
): void {
|
||||||
const target = productOutputTags(outputTags)[0];
|
const target = productOutputTags(outputTags)[0];
|
||||||
if (!target) return;
|
if (!target) return;
|
||||||
if (outputs[target] && isUsableContextFragment(outputs[target])) return;
|
if (existingOutputIsProduct(outputs[target])) return;
|
||||||
|
|
||||||
const outputsRaw =
|
const outputsRaw =
|
||||||
obj.outputs && typeof obj.outputs === "object" && !Array.isArray(obj.outputs)
|
obj.outputs && typeof obj.outputs === "object" && !Array.isArray(obj.outputs)
|
||||||
@@ -212,7 +222,11 @@ function recoverFragmentOutputs(
|
|||||||
if (key === target) continue;
|
if (key === target) continue;
|
||||||
const text = stringifyOutputValue(val);
|
const text = stringifyOutputValue(val);
|
||||||
if (!text) continue;
|
if (!text) continue;
|
||||||
if (isUsableContextFragment(text) || looksLikeFragmentDoc(val)) {
|
if (
|
||||||
|
isUsableContextFragment(text) ||
|
||||||
|
looksLikeFragmentDoc(val) ||
|
||||||
|
looksLikeCreationFlowDoc(val)
|
||||||
|
) {
|
||||||
outputs[target] = text;
|
outputs[target] = text;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -227,6 +241,13 @@ function recoverFragmentOutputs(
|
|||||||
if (looksLikeFragmentDoc(obj) || looksLikeFragmentDoc(rest)) {
|
if (looksLikeFragmentDoc(obj) || looksLikeFragmentDoc(rest)) {
|
||||||
const source = looksLikeFragmentDoc(obj) ? obj : rest;
|
const source = looksLikeFragmentDoc(obj) ? obj : rest;
|
||||||
outputs[target] = JSON.stringify(source);
|
outputs[target] = JSON.stringify(source);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (looksLikeCreationFlowDoc(obj) || looksLikeCreationFlowDoc(rest)) {
|
||||||
|
const source = looksLikeCreationFlowDoc(obj) ? obj : rest;
|
||||||
|
if (!outputs[target]?.trim()) {
|
||||||
|
outputs[target] = JSON.stringify(source);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -283,14 +304,26 @@ const INCOMPLETE_FRAGMENT_ASK = normalizeQuestions([
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
function lookupOutputValue(
|
||||||
|
raw: Record<string, unknown>,
|
||||||
|
tag: string,
|
||||||
|
): unknown {
|
||||||
|
if (raw[tag] != null) return raw[tag];
|
||||||
|
const leaf = tag.includes(".") ? tag.slice(tag.lastIndexOf(".") + 1) : tag;
|
||||||
|
if (!leaf) return undefined;
|
||||||
|
for (const [key, val] of Object.entries(raw)) {
|
||||||
|
if (val == null) continue;
|
||||||
|
if (key === leaf || key.endsWith(`.${leaf}`)) return val;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
function parseWorkerResponse(
|
function parseWorkerResponse(
|
||||||
raw: string,
|
raw: string,
|
||||||
outputTags: string[],
|
outputTags: string[],
|
||||||
): WorkerRunResult {
|
): WorkerRunResult {
|
||||||
let parsed: unknown;
|
const parsed = tryParseJsonDoc(raw);
|
||||||
try {
|
if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
||||||
parsed = JSON.parse(raw);
|
|
||||||
} catch {
|
|
||||||
const target = productOutputTags(outputTags)[0] ?? outputTags[0];
|
const target = productOutputTags(outputTags)[0] ?? outputTags[0];
|
||||||
if (target && looksLikeStructuredDraft(raw)) {
|
if (target && looksLikeStructuredDraft(raw)) {
|
||||||
return finalizeParsedOutputs(
|
return finalizeParsedOutputs(
|
||||||
@@ -322,17 +355,43 @@ function parseWorkerResponse(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!parsed || typeof parsed !== "object") {
|
const obj = parsed as Record<string, unknown>;
|
||||||
throw new Error("Worker 返回了无效 JSON");
|
|
||||||
|
// 创作流程 DAG:根对象就是产物,不能先收成扁形(brief 会被当成稿、steps 丢掉)
|
||||||
|
if (looksLikeCreationFlowDoc(obj)) {
|
||||||
|
const target = productOutputTags(outputTags)[0] ?? outputTags[0];
|
||||||
|
if (target) {
|
||||||
|
return finalizeParsedOutputs(
|
||||||
|
{ [target]: JSON.stringify(obj) },
|
||||||
|
obj,
|
||||||
|
outputTags,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 扁形产物 → 旧 fragment 外壳。已是 context-fragment 深树则原样收下(保留自评与嵌套正文)。
|
||||||
|
if (!looksLikeFragmentDoc(obj)) {
|
||||||
|
const asFlat = coerceToFlatArtifact(obj);
|
||||||
|
if (asFlat) {
|
||||||
|
const frag = flatArtifactToFragmentDoc(asFlat);
|
||||||
|
const target = productOutputTags(outputTags)[0] ?? outputTags[0];
|
||||||
|
if (target) {
|
||||||
|
return finalizeParsedOutputs(
|
||||||
|
{ [target]: JSON.stringify(frag) },
|
||||||
|
{ ...frag, summary: asFlat.summary || obj.summary },
|
||||||
|
outputTags,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const obj = parsed as Record<string, unknown>;
|
|
||||||
const outputsRaw = obj.outputs;
|
const outputsRaw = obj.outputs;
|
||||||
const outputs: Record<string, string> = {};
|
const outputs: Record<string, string> = {};
|
||||||
|
|
||||||
if (outputsRaw && typeof outputsRaw === "object") {
|
if (outputsRaw && typeof outputsRaw === "object" && !Array.isArray(outputsRaw)) {
|
||||||
|
const bag = outputsRaw as Record<string, unknown>;
|
||||||
for (const tag of productOutputTags(outputTags)) {
|
for (const tag of productOutputTags(outputTags)) {
|
||||||
assignOutput(outputs, tag, (outputsRaw as Record<string, unknown>)[tag]);
|
assignOutput(outputs, tag, lookupOutputValue(bag, tag));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
recoverFragmentOutputs(obj, outputs, outputTags);
|
recoverFragmentOutputs(obj, outputs, outputTags);
|
||||||
@@ -484,25 +543,6 @@ function extractQuestionsFromText(text: string): string[] {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
async function completeWorkerPreferStream(
|
|
||||||
llm: LlmProvider,
|
|
||||||
messages: Parameters<LlmProvider["complete"]>[0],
|
|
||||||
options: Parameters<LlmProvider["complete"]>[1],
|
|
||||||
stream?: WorkerStreamCallbacks,
|
|
||||||
): Promise<Awaited<ReturnType<LlmProvider["complete"]>>> {
|
|
||||||
const callbacks: StreamCallbacks = {
|
|
||||||
onReasoningDelta: (delta) => stream?.onThinkingDelta?.(delta),
|
|
||||||
onContentDelta: (delta) => stream?.onOutputDelta?.(delta),
|
|
||||||
};
|
|
||||||
if (supportsContentStream(llm)) {
|
|
||||||
return llm.completeStream!(messages, options, callbacks);
|
|
||||||
}
|
|
||||||
const result = await llm.complete(messages, options);
|
|
||||||
if (result.reasoning) callbacks.onReasoningDelta?.(result.reasoning);
|
|
||||||
if (result.content) callbacks.onContentDelta?.(result.content);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function runWorkerSkill(params: WorkerRunParams): Promise<WorkerRunResult> {
|
export async function runWorkerSkill(params: WorkerRunParams): Promise<WorkerRunResult> {
|
||||||
const workerId = resolveWorkerId(params.workerId);
|
const workerId = resolveWorkerId(params.workerId);
|
||||||
let worker: ParsedWorkerSkill;
|
let worker: ParsedWorkerSkill;
|
||||||
@@ -519,9 +559,15 @@ export async function runWorkerSkill(params: WorkerRunParams): Promise<WorkerRun
|
|||||||
params.slots.creationAcceptedUnits ??
|
params.slots.creationAcceptedUnits ??
|
||||||
params.blackboard.getContentByTag("创作.已验收单位");
|
params.blackboard.getContentByTag("创作.已验收单位");
|
||||||
let acceptedStepNames: string[] = [];
|
let acceptedStepNames: string[] = [];
|
||||||
|
let inheritExisting = false;
|
||||||
try {
|
try {
|
||||||
const { parseAcceptedSteps } = await import("../skills/creation-flow.js");
|
const { parseAcceptedSteps, isInheritExistingFlag } = await import(
|
||||||
|
"../skills/creation-flow.js"
|
||||||
|
);
|
||||||
acceptedStepNames = parseAcceptedSteps(acceptedRaw);
|
acceptedStepNames = parseAcceptedSteps(acceptedRaw);
|
||||||
|
inheritExisting = isInheritExistingFlag(
|
||||||
|
params.blackboard.getContentByTag("创作.继承修改"),
|
||||||
|
);
|
||||||
} catch {
|
} catch {
|
||||||
acceptedStepNames = [];
|
acceptedStepNames = [];
|
||||||
}
|
}
|
||||||
@@ -538,6 +584,7 @@ export async function runWorkerSkill(params: WorkerRunParams): Promise<WorkerRun
|
|||||||
acceptedStepNames,
|
acceptedStepNames,
|
||||||
selectedRecipeRef,
|
selectedRecipeRef,
|
||||||
filledArtifactTags,
|
filledArtifactTags,
|
||||||
|
inheritExisting,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
worker = loaded.worker;
|
worker = loaded.worker;
|
||||||
@@ -616,20 +663,36 @@ export async function runWorkerSkill(params: WorkerRunParams): Promise<WorkerRun
|
|||||||
stream: params.stream,
|
stream: params.stream,
|
||||||
caller: `worker:${worker.id}`,
|
caller: `worker:${worker.id}`,
|
||||||
});
|
});
|
||||||
return parseWorkerResponse(harness.content, worker.outputTags);
|
return parseWorkerResponse(selectJsonPayload(harness.content), worker.outputTags);
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await completeWorkerPreferStream(
|
const streamCallbacks: StreamCallbacks | undefined = params.stream
|
||||||
params.llm,
|
? {
|
||||||
messages,
|
onReasoningDelta: (delta) => params.stream?.onThinkingDelta?.(delta),
|
||||||
{
|
onContentDelta: (delta) => params.stream?.onOutputDelta?.(delta),
|
||||||
responseFormat: "json_object",
|
}
|
||||||
caller: `worker:${worker.id}`,
|
: undefined;
|
||||||
},
|
|
||||||
params.stream,
|
|
||||||
);
|
|
||||||
|
|
||||||
return parseWorkerResponse(result.content, worker.outputTags);
|
const result = await completeStructured(params.llm, messages, {
|
||||||
|
schema: {
|
||||||
|
type: "object",
|
||||||
|
additionalProperties: true,
|
||||||
|
},
|
||||||
|
name: "submit_worker_result",
|
||||||
|
schemaIsLoose: true,
|
||||||
|
capabilities: resolveActiveProfile()?.capabilities,
|
||||||
|
caller: `worker:${worker.id}`,
|
||||||
|
stream: streamCallbacks,
|
||||||
|
});
|
||||||
|
|
||||||
|
const raw =
|
||||||
|
typeof result.parsed === "string"
|
||||||
|
? result.parsed
|
||||||
|
: JSON.stringify(result.parsed);
|
||||||
|
return parseWorkerResponse(
|
||||||
|
selectJsonPayload(raw, result.reasoning) || raw,
|
||||||
|
worker.outputTags,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @internal 供单测 */
|
/** @internal 供单测 */
|
||||||
|
|||||||
@@ -0,0 +1,134 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import {
|
||||||
|
CAPABILITY_PROBE_REASONING_EFFORT,
|
||||||
|
resolveStructuredDeliveryMode,
|
||||||
|
type ProfileCapabilities,
|
||||||
|
} from "../src/llm/capabilities.js";
|
||||||
|
import { normalizeProfileReasoningEffort } from "../src/config/api-profiles.js";
|
||||||
|
import {
|
||||||
|
coerceToFlatArtifact,
|
||||||
|
flatArtifactToFragmentDoc,
|
||||||
|
FLAT_ARTIFACT_SCHEMA_ID,
|
||||||
|
} from "../src/skills/flat-artifact.js";
|
||||||
|
|
||||||
|
describe("capability probe defaults", () => {
|
||||||
|
it("always probes with low reasoning effort", () => {
|
||||||
|
expect(CAPABILITY_PROBE_REASONING_EFFORT).toBe("low");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("normalizeProfileReasoningEffort", () => {
|
||||||
|
it("keeps valid effort and drops auto/empty", () => {
|
||||||
|
expect(normalizeProfileReasoningEffort("low")).toBe("low");
|
||||||
|
expect(normalizeProfileReasoningEffort("MAX")).toBe("max");
|
||||||
|
expect(normalizeProfileReasoningEffort("auto")).toBeUndefined();
|
||||||
|
expect(normalizeProfileReasoningEffort("")).toBeUndefined();
|
||||||
|
expect(normalizeProfileReasoningEffort("nope")).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("resolveStructuredDeliveryMode", () => {
|
||||||
|
it("prefers json_schema then forced_tool then json_object", () => {
|
||||||
|
const base: ProfileCapabilities = {
|
||||||
|
chat: "ok",
|
||||||
|
jsonObject: "ok",
|
||||||
|
jsonSchema: "fail",
|
||||||
|
tools: "ok",
|
||||||
|
forcedTool: "ok",
|
||||||
|
strictTool: "fail",
|
||||||
|
};
|
||||||
|
expect(resolveStructuredDeliveryMode(base)).toBe("forced_tool");
|
||||||
|
expect(
|
||||||
|
resolveStructuredDeliveryMode({ ...base, jsonSchema: "ok" }),
|
||||||
|
).toBe("json_schema");
|
||||||
|
expect(
|
||||||
|
resolveStructuredDeliveryMode({
|
||||||
|
...base,
|
||||||
|
jsonSchema: "fail",
|
||||||
|
forcedTool: "fail",
|
||||||
|
}),
|
||||||
|
).toBe("json_object");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("flat artifact", () => {
|
||||||
|
it("round-trips deep fragment body into sections", () => {
|
||||||
|
const flat = coerceToFlatArtifact({
|
||||||
|
schema: "context-fragment.v1",
|
||||||
|
技能: "美学纲领与交互范式",
|
||||||
|
brief: "点题",
|
||||||
|
mount: ["narrator"],
|
||||||
|
稳变: "stable",
|
||||||
|
正文: {
|
||||||
|
设定逻辑: { 变造: "丧尸" },
|
||||||
|
美学纲领: "出租屋",
|
||||||
|
},
|
||||||
|
开放问题: ["后宫"],
|
||||||
|
追问: {
|
||||||
|
题目: [{ 问: "身份?", 建议选项: ["幸存者", "其他"] }],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(flat?.sections.map((s) => s.id)).toEqual(["设定逻辑", "美学纲领"]);
|
||||||
|
const frag = flatArtifactToFragmentDoc(flat!);
|
||||||
|
expect(frag.schema).toBe("context-fragment.v1");
|
||||||
|
expect(frag.brief).toBe("点题");
|
||||||
|
expect((frag.追问 as { 题目: unknown[] }).题目[0]).toMatchObject({
|
||||||
|
问: "身份?",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not coerce a creation-flow DAG (brief + steps) into a flat artifact", () => {
|
||||||
|
const flow = {
|
||||||
|
brief: "单角代入",
|
||||||
|
status: "open",
|
||||||
|
steps: [{ name: "美学纲领与交互范式", depends_on: [] }],
|
||||||
|
};
|
||||||
|
expect(coerceToFlatArtifact(flow)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not coerce a brief-only object into an empty flat artifact", () => {
|
||||||
|
expect(coerceToFlatArtifact({ brief: "点题" })).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("accepts native flat wire format", () => {
|
||||||
|
const flat = coerceToFlatArtifact({
|
||||||
|
schema: FLAT_ARTIFACT_SCHEMA_ID,
|
||||||
|
skill: "x",
|
||||||
|
brief: "b",
|
||||||
|
mount: [],
|
||||||
|
stability: "semi",
|
||||||
|
sections: [{ id: "a", title: "A", text: "hello" }],
|
||||||
|
open_questions: [],
|
||||||
|
questions: [],
|
||||||
|
summary: "s",
|
||||||
|
});
|
||||||
|
expect(flat?.sections[0]?.text).toBe("hello");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("round-trips nested body objects through flat stringify + fragment parse", () => {
|
||||||
|
const nested = {
|
||||||
|
schema: "context-fragment.v1",
|
||||||
|
技能: "美学纲领与交互范式",
|
||||||
|
brief: "点题",
|
||||||
|
mount: [],
|
||||||
|
正文: {
|
||||||
|
交互范式: { 前置配置: { 用户角色: "唯一免疫者" } },
|
||||||
|
美学纲领: { 体验内核: "报复爽", 呈现要点: "内外对比" },
|
||||||
|
设定逻辑: { 变造: { 原型: "丧尸" } },
|
||||||
|
},
|
||||||
|
开放问题: [],
|
||||||
|
追问: { 题目: [] },
|
||||||
|
};
|
||||||
|
const flat = coerceToFlatArtifact(nested);
|
||||||
|
expect(flat).not.toBeNull();
|
||||||
|
const frag = flatArtifactToFragmentDoc(flat!);
|
||||||
|
expect(frag.技能).toBe("美学纲领与交互范式");
|
||||||
|
expect((frag.正文 as Record<string, unknown>).交互范式).toEqual({
|
||||||
|
前置配置: { 用户角色: "唯一免疫者" },
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
((frag.正文 as Record<string, unknown>).美学纲领 as Record<string, unknown>)
|
||||||
|
.体验内核,
|
||||||
|
).toBe("报复爽");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -48,6 +48,29 @@ describe("pruneContextTraces", () => {
|
|||||||
expect(pruned.map((m) => m.id)).toEqual(["a", "b"]);
|
expect(pruned.map((m) => m.id)).toEqual(["a", "b"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("pins design-flow traces while pruning others", () => {
|
||||||
|
const messages = [
|
||||||
|
{
|
||||||
|
id: "flow",
|
||||||
|
contextTrace: buildContextTrace({
|
||||||
|
caller: "worker:design-flow",
|
||||||
|
messages: [{ role: "user", content: "plan" }],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
msg("1", true),
|
||||||
|
msg("2", true),
|
||||||
|
msg("3", true),
|
||||||
|
];
|
||||||
|
const pruned = pruneContextTraces(messages, 1);
|
||||||
|
expect(pruned[0]?.contextTrace?.caller).toBe("worker:design-flow");
|
||||||
|
expect(pruned.map((m) => Boolean(m.contextTrace))).toEqual([
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
it("is a no-op when under the limit", () => {
|
it("is a no-op when under the limit", () => {
|
||||||
const messages = [msg("1", true), msg("2", true)];
|
const messages = [msg("1", true), msg("2", true)];
|
||||||
const pruned = pruneContextTraces(messages, 5);
|
const pruned = pruneContextTraces(messages, 5);
|
||||||
|
|||||||
+393
-34
@@ -6,6 +6,8 @@ import {
|
|||||||
artifactTagForStep,
|
artifactTagForStep,
|
||||||
extractModuleOpening,
|
extractModuleOpening,
|
||||||
formatCreationFlowForUser,
|
formatCreationFlowForUser,
|
||||||
|
deriveFlowStepTitle,
|
||||||
|
titleFromSuggestion,
|
||||||
formatFlowProgressForAgent,
|
formatFlowProgressForAgent,
|
||||||
formatModuleCatalogForAgent,
|
formatModuleCatalogForAgent,
|
||||||
formatRecipeCatalogForAgent,
|
formatRecipeCatalogForAgent,
|
||||||
@@ -18,6 +20,13 @@ import {
|
|||||||
mergeCreationFlowPreservingAccepted,
|
mergeCreationFlowPreservingAccepted,
|
||||||
needsFlowExpansion,
|
needsFlowExpansion,
|
||||||
nextPendingStep,
|
nextPendingStep,
|
||||||
|
listReadySteps,
|
||||||
|
computeStepLayers,
|
||||||
|
spawnRepeatableCreationStep,
|
||||||
|
spawnInstanceFromPrototype,
|
||||||
|
hasSelectableCreationWork,
|
||||||
|
removeUnstartedInstance,
|
||||||
|
looksLikeCreationFlowDoc,
|
||||||
parseCreationFlow,
|
parseCreationFlow,
|
||||||
pickRecordedStepId,
|
pickRecordedStepId,
|
||||||
parseModuleCatalog,
|
parseModuleCatalog,
|
||||||
@@ -29,6 +38,7 @@ import {
|
|||||||
shouldSkipModuleOpening,
|
shouldSkipModuleOpening,
|
||||||
isProgressPointerTag,
|
isProgressPointerTag,
|
||||||
stringifyCreationFlow,
|
stringifyCreationFlow,
|
||||||
|
stripReviseSteps,
|
||||||
creationFlowFromRecipeSeed,
|
creationFlowFromRecipeSeed,
|
||||||
validateCreationFlow,
|
validateCreationFlow,
|
||||||
validateStepReadyToRun,
|
validateStepReadyToRun,
|
||||||
@@ -51,6 +61,10 @@ modules:
|
|||||||
declaration: 锚定实例
|
declaration: 锚定实例
|
||||||
artifact: 设计.具体实例
|
artifact: 设计.具体实例
|
||||||
repeatable: true
|
repeatable: true
|
||||||
|
- name: 开场白与开场变量
|
||||||
|
declaration: 收口
|
||||||
|
artifact: 设计.开场白与开场变量
|
||||||
|
closer: true
|
||||||
`)!;
|
`)!;
|
||||||
|
|
||||||
describe("creation-flow", () => {
|
describe("creation-flow", () => {
|
||||||
@@ -145,7 +159,44 @@ describe("creation-flow", () => {
|
|||||||
acceptedStepIds: ["美学纲领与交互范式"],
|
acceptedStepIds: ["美学纲领与交互范式"],
|
||||||
});
|
});
|
||||||
expect(merged.restored).toEqual([]);
|
expect(merged.restored).toEqual([]);
|
||||||
expect(merged.raw).toBe(rewritten);
|
expect(parseCreationFlow(merged.raw)!.steps.map((s) => s.id)).toEqual([
|
||||||
|
"美学纲领与交互范式",
|
||||||
|
"实现机制",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("drops planner revise steps when merging flow", () => {
|
||||||
|
const prev = `{
|
||||||
|
"status": "open",
|
||||||
|
"steps": [
|
||||||
|
{ "id": "美学纲领与交互范式", "name": "美学纲领与交互范式", "depends_on": [] }
|
||||||
|
]
|
||||||
|
}`;
|
||||||
|
const rewritten = `{
|
||||||
|
"status": "open",
|
||||||
|
"steps": [
|
||||||
|
{ "id": "美学纲领与交互范式", "name": "美学纲领与交互范式", "depends_on": [] },
|
||||||
|
{
|
||||||
|
"id": "美学纲领与交互范式·改",
|
||||||
|
"name": "美学纲领与交互范式",
|
||||||
|
"mode": "revise",
|
||||||
|
"revises": "美学纲领与交互范式",
|
||||||
|
"depends_on": ["美学纲领与交互范式"]
|
||||||
|
},
|
||||||
|
{ "id": "实现机制", "name": "实现机制", "depends_on": ["美学纲领与交互范式·改"] }
|
||||||
|
]
|
||||||
|
}`;
|
||||||
|
const merged = mergeCreationFlowPreservingAccepted({
|
||||||
|
prevRaw: prev,
|
||||||
|
nextRaw: rewritten,
|
||||||
|
acceptedStepIds: ["美学纲领与交互范式"],
|
||||||
|
});
|
||||||
|
const flow = parseCreationFlow(merged.raw)!;
|
||||||
|
expect(flow.steps.map((s) => s.id)).toEqual([
|
||||||
|
"美学纲领与交互范式",
|
||||||
|
"实现机制",
|
||||||
|
]);
|
||||||
|
expect(flow.steps[1]?.depends_on).toEqual(["美学纲领与交互范式"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("allows duplicate capability names with distinct ids", () => {
|
it("allows duplicate capability names with distinct ids", () => {
|
||||||
@@ -185,6 +236,24 @@ describe("creation-flow", () => {
|
|||||||
expect(flow?.steps[0]?.name).toBe("美学纲领与交互范式");
|
expect(flow?.steps[0]?.name).toBe("美学纲领与交互范式");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("extracts JSON from a code fence", () => {
|
||||||
|
const flow = parseCreationFlow(
|
||||||
|
"```json\n{\"brief\":\"单角代入\",\"steps\":[{\"name\":\"美学纲领与交互范式\",\"depends_on\":[]}]}\n```",
|
||||||
|
);
|
||||||
|
expect(flow?.brief).toBe("单角代入");
|
||||||
|
expect(flow?.steps[0]?.name).toBe("美学纲领与交互范式");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("recognizes a DAG object and ignores brief-only objects", () => {
|
||||||
|
expect(
|
||||||
|
looksLikeCreationFlowDoc({
|
||||||
|
brief: "单角代入",
|
||||||
|
steps: [{ name: "美学纲领与交互范式", depends_on: [] }],
|
||||||
|
}),
|
||||||
|
).toBe(true);
|
||||||
|
expect(looksLikeCreationFlowDoc({ brief: "单角代入" })).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
it("validates deps must appear earlier", () => {
|
it("validates deps must appear earlier", () => {
|
||||||
const bad = parseCreationFlow(`{
|
const bad = parseCreationFlow(`{
|
||||||
"steps": [
|
"steps": [
|
||||||
@@ -250,13 +319,93 @@ describe("creation-flow", () => {
|
|||||||
depends_on: [],
|
depends_on: [],
|
||||||
declaration: "站位与体验契约",
|
declaration: "站位与体验契约",
|
||||||
});
|
});
|
||||||
expect(view.steps[2]?.occurrence).toBe(2);
|
expect(view.steps[2]?.role).toBe("instance");
|
||||||
|
expect(view.steps[2]?.occurrence).toBe(1);
|
||||||
|
expect(view.steps[1]?.role).toBe("prototype");
|
||||||
expect(view.steps[2]?.repeatable).toBe(true);
|
expect(view.steps[2]?.repeatable).toBe(true);
|
||||||
expect(view.steps[0]?.runState).toBe("current");
|
expect(view.steps[0]?.runState).toBe("pending");
|
||||||
|
expect(view.steps[0]?.layer).toBe(0);
|
||||||
|
expect(view.steps[0]?.ready).toBe(true);
|
||||||
|
expect(view.steps[0]?.selectable).toBe(true);
|
||||||
expect(view.steps[1]?.runState).toBe("pending");
|
expect(view.steps[1]?.runState).toBe("pending");
|
||||||
|
expect(view.steps[1]?.layer).toBe(1);
|
||||||
|
expect(view.steps[1]?.ready).toBe(false);
|
||||||
expect(view.steps[2]?.runState).toBe("pending");
|
expect(view.steps[2]?.runState).toBe("pending");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("shows a short title for 生成规则 / 具体实例 prototypes and instances", () => {
|
||||||
|
const flow = parseCreationFlow(`{
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"id": "生成规则",
|
||||||
|
"name": "生成规则",
|
||||||
|
"role": "prototype",
|
||||||
|
"suggestion": "幸存者生成规则:锚定出租屋内的女性租客",
|
||||||
|
"depends_on": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "生成规则#1",
|
||||||
|
"name": "生成规则",
|
||||||
|
"role": "instance",
|
||||||
|
"from": "生成规则",
|
||||||
|
"depends_on": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "具体实例",
|
||||||
|
"name": "具体实例",
|
||||||
|
"role": "prototype",
|
||||||
|
"suggestion": "按幸存者规则生成开局女租客",
|
||||||
|
"depends_on": ["生成规则"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "具体实例#1",
|
||||||
|
"name": "具体实例",
|
||||||
|
"role": "instance",
|
||||||
|
"from": "具体实例",
|
||||||
|
"params": { "batch_goal": "开局女租客", "rule_id": "survivors" },
|
||||||
|
"depends_on": ["生成规则#1"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`)!;
|
||||||
|
const untitled = formatCreationFlowForUser(flow, sampleCatalog);
|
||||||
|
expect(untitled.steps.find((s) => s.id === "生成规则")?.title).toBe(
|
||||||
|
"幸存者生成规则",
|
||||||
|
);
|
||||||
|
expect(untitled.steps.find((s) => s.id === "生成规则#1")?.title).toBe(
|
||||||
|
"幸存者生成规则",
|
||||||
|
);
|
||||||
|
expect(untitled.steps.find((s) => s.id === "具体实例")?.title).toBe(
|
||||||
|
"按幸存者规则生成开局女租客",
|
||||||
|
);
|
||||||
|
expect(untitled.steps.find((s) => s.id === "具体实例#1")?.title).toBe(
|
||||||
|
"开局女租客",
|
||||||
|
);
|
||||||
|
|
||||||
|
const accepted = formatCreationFlowForUser(flow, sampleCatalog, [], [], {
|
||||||
|
"生成规则#1": { summary: "生成规则 · 幸存者 · 预生成并动态" },
|
||||||
|
"具体实例#1": { summary: "具体实例 · 女租客 · 2条" },
|
||||||
|
});
|
||||||
|
expect(accepted.steps.find((s) => s.id === "生成规则#1")?.title).toBe(
|
||||||
|
"幸存者",
|
||||||
|
);
|
||||||
|
expect(accepted.steps.find((s) => s.id === "具体实例#1")?.title).toBe(
|
||||||
|
"女租客",
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
deriveFlowStepTitle({
|
||||||
|
name: "生成规则",
|
||||||
|
hint: {
|
||||||
|
content: {
|
||||||
|
正文: { 本步参数: { target: "女租客" } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).toBe("女租客");
|
||||||
|
expect(titleFromSuggestion("怪物生成规则:按族群抽样")).toBe(
|
||||||
|
"怪物生成规则",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it("marks accepted steps done and the next pending current", () => {
|
it("marks accepted steps done and the next pending current", () => {
|
||||||
const flow = parseCreationFlow(`{
|
const flow = parseCreationFlow(`{
|
||||||
"brief": "网恋回合",
|
"brief": "网恋回合",
|
||||||
@@ -272,9 +421,10 @@ describe("creation-flow", () => {
|
|||||||
]);
|
]);
|
||||||
expect(view.steps.map((s) => s.runState)).toEqual([
|
expect(view.steps.map((s) => s.runState)).toEqual([
|
||||||
"done",
|
"done",
|
||||||
"current",
|
"pending",
|
||||||
"pending",
|
"pending",
|
||||||
]);
|
]);
|
||||||
|
expect(view.steps[1]?.selectable).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("loads world-simulator catalog and formats for agent", async () => {
|
it("loads world-simulator catalog and formats for agent", async () => {
|
||||||
@@ -317,7 +467,7 @@ describe("creation-flow", () => {
|
|||||||
expect(block).toContain("上下文投影排序〔程序步〕");
|
expect(block).toContain("上下文投影排序〔程序步〕");
|
||||||
expect(block).toContain("何时用");
|
expect(block).toContain("何时用");
|
||||||
expect(block).toContain("何时不用");
|
expect(block).toContain("何时不用");
|
||||||
expect(block).toContain("规划产物字段");
|
expect(block).toContain("role=prototype");
|
||||||
expect(block).toContain("编排参数");
|
expect(block).toContain("编排参数");
|
||||||
expect(block).not.toContain("设计.美学纲领与交互范式");
|
expect(block).not.toContain("设计.美学纲领与交互范式");
|
||||||
});
|
});
|
||||||
@@ -403,6 +553,22 @@ steps:
|
|||||||
).toBeNull();
|
).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("creationFlowFromRecipeSeed follows the recipe steps, not a fixed first node", () => {
|
||||||
|
const detail = parseRecipeYaml(
|
||||||
|
`
|
||||||
|
when: 其它剧本
|
||||||
|
steps:
|
||||||
|
- name: 舞台骨架
|
||||||
|
depends_on: []
|
||||||
|
- name: 实现机制
|
||||||
|
depends_on: ["舞台骨架"]
|
||||||
|
`,
|
||||||
|
{ id: "other", name: "其它", declaration: "测" },
|
||||||
|
);
|
||||||
|
const seeded = creationFlowFromRecipeSeed(detail);
|
||||||
|
expect(seeded?.steps.map((s) => s.name)).toEqual(["舞台骨架", "实现机制"]);
|
||||||
|
});
|
||||||
|
|
||||||
it("falls back to legacy hint when methodology absent", () => {
|
it("falls back to legacy hint when methodology absent", () => {
|
||||||
const detail = parseRecipeYaml(
|
const detail = parseRecipeYaml(
|
||||||
`
|
`
|
||||||
@@ -478,7 +644,7 @@ recipes:
|
|||||||
expect(loaded.promptBody).toContain("美学纲领与交互范式");
|
expect(loaded.promptBody).toContain("美学纲领与交互范式");
|
||||||
expect(loaded.promptBody).toContain("已验收");
|
expect(loaded.promptBody).toContain("已验收");
|
||||||
expect(loaded.promptBody).toContain("禁止再排新建");
|
expect(loaded.promptBody).toContain("禁止再排新建");
|
||||||
expect(loaded.promptBody).toContain("mode=revise");
|
expect(loaded.promptBody).toContain("禁止排 mode=revise");
|
||||||
expect(loaded.promptBody).toContain("生成规则");
|
expect(loaded.promptBody).toContain("生成规则");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -496,7 +662,36 @@ recipes:
|
|||||||
expect(text).toContain("草案已有、尚未验收");
|
expect(text).toContain("草案已有、尚未验收");
|
||||||
expect(text).toContain("美学纲领与交互范式");
|
expect(text).toContain("美学纲领与交互范式");
|
||||||
expect(text).toContain("不要当作新规划再写一遍");
|
expect(text).toContain("不要当作新规划再写一遍");
|
||||||
expect(text).toContain("可反复追加:生成规则、具体实例");
|
expect(text).toContain("目录〔可反复〕:生成规则、具体实例");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("tells design-flow to insert nodes before an unaccepted closer", () => {
|
||||||
|
const catalog = parseModuleCatalog(`
|
||||||
|
modules:
|
||||||
|
- name: 美学纲领与交互范式
|
||||||
|
declaration: 站位
|
||||||
|
artifact: 设计.美学纲领与交互范式
|
||||||
|
- name: 开场白与开场变量
|
||||||
|
declaration: 收口
|
||||||
|
artifact: 设计.开场白与开场变量
|
||||||
|
closer: true
|
||||||
|
`)!;
|
||||||
|
const flow = parseCreationFlow(`{
|
||||||
|
"status": "closed",
|
||||||
|
"steps": [
|
||||||
|
{ "name": "美学纲领与交互范式", "depends_on": [] },
|
||||||
|
{ "name": "开场白与开场变量", "depends_on": ["美学纲领与交互范式"] }
|
||||||
|
]
|
||||||
|
}`)!;
|
||||||
|
const text = formatFlowProgressForAgent({
|
||||||
|
flow,
|
||||||
|
acceptedStepIds: ["美学纲领与交互范式"],
|
||||||
|
catalog,
|
||||||
|
});
|
||||||
|
expect(text).toContain("〔收口〕");
|
||||||
|
expect(text).toContain("开场白与开场变量");
|
||||||
|
expect(text).toContain("插在收口之前");
|
||||||
|
expect(text).toContain("status 改回 open");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("injects aesthetics-interaction module prompt into design-step", async () => {
|
it("injects aesthetics-interaction module prompt into design-step", async () => {
|
||||||
@@ -637,7 +832,7 @@ recipes:
|
|||||||
expect(missingView.steps[0]?.paramsMissing).toBeUndefined();
|
expect(missingView.steps[0]?.paramsMissing).toBeUndefined();
|
||||||
|
|
||||||
const block = formatModuleCatalogForAgent(catalog!);
|
const block = formatModuleCatalogForAgent(catalog!);
|
||||||
expect(block).toContain("规划产物字段");
|
expect(block).toContain("role=prototype");
|
||||||
expect(block).toContain("target");
|
expect(block).toContain("target");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -662,8 +857,7 @@ recipes:
|
|||||||
acceptedStepNames: [],
|
acceptedStepNames: [],
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
expect(loaded.promptBody).toContain("【规划产物】");
|
expect(loaded.promptBody).toContain("【本步对象】");
|
||||||
expect(loaded.promptBody).not.toContain("勿再问「生成什么");
|
|
||||||
expect(loaded.promptBody).toContain("target: 怪物");
|
expect(loaded.promptBody).toContain("target: 怪物");
|
||||||
expect(loaded.promptBody).toContain("rule_id: monsters");
|
expect(loaded.promptBody).toContain("rule_id: monsters");
|
||||||
expect(loaded.worker.outputTags).toContain("设计.生成规则");
|
expect(loaded.worker.outputTags).toContain("设计.生成规则");
|
||||||
@@ -693,14 +887,22 @@ modules:
|
|||||||
undefined,
|
undefined,
|
||||||
{
|
{
|
||||||
flowRaw: JSON.stringify({
|
flowRaw: JSON.stringify({
|
||||||
steps: [{ id: "生成规则", name: "生成规则", depends_on: [] }],
|
steps: [
|
||||||
|
{
|
||||||
|
id: "生成规则#1",
|
||||||
|
name: "生成规则",
|
||||||
|
role: "instance",
|
||||||
|
from: "生成规则",
|
||||||
|
depends_on: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
}),
|
}),
|
||||||
currentStepName: "生成规则",
|
currentStepName: "生成规则#1",
|
||||||
acceptedStepNames: [],
|
acceptedStepNames: [],
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
expect(loaded.promptBody).toContain("【规划产物】");
|
expect(loaded.promptBody).toContain("【本步对象】");
|
||||||
expect(loaded.promptBody).toContain("尚未规划本步具体写什么");
|
expect(loaded.promptBody).toContain("尚未钉本步具体写什么");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("nextPendingStep respects deps and accepted by id", () => {
|
it("nextPendingStep respects deps and accepted by id", () => {
|
||||||
@@ -780,7 +982,7 @@ modules:
|
|||||||
expect(isCreationFlowComplete(legacy, ["美学纲领与交互范式"])).toBe(true);
|
expect(isCreationFlowComplete(legacy, ["美学纲领与交互范式"])).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("allows revising a non-repeatable skill and infers revises + id", () => {
|
it("parses legacy revise steps then strips them from the user graph", () => {
|
||||||
const flow = parseCreationFlow(`{
|
const flow = parseCreationFlow(`{
|
||||||
"status": "open",
|
"status": "open",
|
||||||
"steps": [
|
"steps": [
|
||||||
@@ -795,10 +997,35 @@ modules:
|
|||||||
revises: "实现机制",
|
revises: "实现机制",
|
||||||
});
|
});
|
||||||
expect(validateCreationFlow(flow, sampleCatalog).ok).toBe(true);
|
expect(validateCreationFlow(flow, sampleCatalog).ok).toBe(true);
|
||||||
|
const stripped = stripReviseSteps(flow);
|
||||||
|
expect(stripped.steps.map((s) => s.id)).toEqual(["实现机制"]);
|
||||||
const view = formatCreationFlowForUser(flow, sampleCatalog, ["实现机制"]);
|
const view = formatCreationFlowForUser(flow, sampleCatalog, ["实现机制"]);
|
||||||
expect(view.steps[1]?.mode).toBe("revise");
|
expect(view.steps).toHaveLength(1);
|
||||||
expect(view.steps[1]?.occurrence).toBeUndefined();
|
expect(view.steps[0]?.id).toBe("实现机制");
|
||||||
expect(view.steps[1]?.runState).toBe("current");
|
expect(view.steps[0]?.runState).toBe("done");
|
||||||
|
expect(view.steps[0]?.mode).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("stripReviseSteps remaps dependents back to the origin step", () => {
|
||||||
|
const flow = parseCreationFlow(`{
|
||||||
|
"steps": [
|
||||||
|
{ "id": "美学纲领与交互范式", "name": "美学纲领与交互范式", "depends_on": [] },
|
||||||
|
{
|
||||||
|
"id": "美学纲领与交互范式·改",
|
||||||
|
"name": "美学纲领与交互范式",
|
||||||
|
"mode": "revise",
|
||||||
|
"revises": "美学纲领与交互范式",
|
||||||
|
"depends_on": ["美学纲领与交互范式"]
|
||||||
|
},
|
||||||
|
{ "id": "正文组成", "name": "实现机制", "depends_on": ["美学纲领与交互范式·改"] }
|
||||||
|
]
|
||||||
|
}`)!;
|
||||||
|
const stripped = stripReviseSteps(flow);
|
||||||
|
expect(stripped.steps.map((s) => s.id)).toEqual([
|
||||||
|
"美学纲领与交互范式",
|
||||||
|
"正文组成",
|
||||||
|
]);
|
||||||
|
expect(stripped.steps[1]?.depends_on).toEqual(["美学纲领与交互范式"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("still rejects two fresh copies of a non-repeatable skill", () => {
|
it("still rejects two fresh copies of a non-repeatable skill", () => {
|
||||||
@@ -849,7 +1076,7 @@ modules:
|
|||||||
expect(validateCreationFlow(flow, sampleCatalog).ok).toBe(true);
|
expect(validateCreationFlow(flow, sampleCatalog).ok).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("nextPendingStep prefers a ready revise over a later fresh step", () => {
|
it("nextPendingStep ignores orchestrated revise steps", () => {
|
||||||
const flow = parseCreationFlow(`{
|
const flow = parseCreationFlow(`{
|
||||||
"status": "open",
|
"status": "open",
|
||||||
"steps": [
|
"steps": [
|
||||||
@@ -863,31 +1090,26 @@ modules:
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}`)!;
|
}`)!;
|
||||||
expect(nextPendingStep(flow, ["美学纲领与交互范式"])?.mode).toBe("revise");
|
expect(nextPendingStep(flow, ["美学纲领与交互范式"])?.mode).toBeUndefined();
|
||||||
expect(nextPendingStep(flow, ["美学纲领与交互范式"])?.id).toBe(
|
expect(nextPendingStep(flow, ["美学纲领与交互范式"])?.id).toBe("生成规则");
|
||||||
"美学纲领与交互范式·改",
|
expect(
|
||||||
);
|
listReadySteps(flow, ["美学纲领与交互范式"]).map((s) => s.id),
|
||||||
|
).toEqual(["生成规则"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("injects inherit draft and skips opening on a revise design-step", async () => {
|
it("inherits existing artifact when reentering an accepted step", async () => {
|
||||||
const flowRaw = JSON.stringify({
|
const flowRaw = JSON.stringify({
|
||||||
status: "open",
|
status: "open",
|
||||||
steps: [
|
steps: [
|
||||||
{ id: "美学纲领与交互范式", name: "美学纲领与交互范式", depends_on: [] },
|
{ id: "美学纲领与交互范式", name: "美学纲领与交互范式", depends_on: [] },
|
||||||
{
|
|
||||||
id: "美学纲领与交互范式·改",
|
|
||||||
name: "美学纲领与交互范式",
|
|
||||||
mode: "revise",
|
|
||||||
revises: "美学纲领与交互范式",
|
|
||||||
depends_on: ["美学纲领与交互范式"],
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
const binding = await resolveDesignStepBinding({
|
const binding = await resolveDesignStepBinding({
|
||||||
skillPackRoot: "dialogue/world-simulator",
|
skillPackRoot: "dialogue/world-simulator",
|
||||||
flowRaw,
|
flowRaw,
|
||||||
currentStepName: "美学纲领与交互范式·改",
|
currentStepName: "美学纲领与交互范式",
|
||||||
acceptedStepNames: ["美学纲领与交互范式"],
|
acceptedStepNames: ["美学纲领与交互范式"],
|
||||||
|
inheritExisting: true,
|
||||||
});
|
});
|
||||||
expect(binding?.opening).toBeNull();
|
expect(binding?.opening).toBeNull();
|
||||||
expect(binding?.inheritTag).toBe("设计.美学纲领与交互范式");
|
expect(binding?.inheritTag).toBe("设计.美学纲领与交互范式");
|
||||||
@@ -898,16 +1120,153 @@ modules:
|
|||||||
undefined,
|
undefined,
|
||||||
{
|
{
|
||||||
flowRaw,
|
flowRaw,
|
||||||
currentStepName: "美学纲领与交互范式·改",
|
currentStepName: "美学纲领与交互范式",
|
||||||
acceptedStepNames: ["美学纲领与交互范式"],
|
acceptedStepNames: ["美学纲领与交互范式"],
|
||||||
|
inheritExisting: true,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
expect(loaded.promptBody).toContain("【回头修改】");
|
expect(loaded.promptBody).toContain("【回头修改】");
|
||||||
expect(loaded.promptBody).not.toContain("程序开场");
|
expect(loaded.promptBody).not.toContain("程序开场");
|
||||||
expect(loaded.worker.name).toContain("回头修改");
|
expect(loaded.worker.name).toContain("回头修改");
|
||||||
expect(loaded.worker.inputTags).toContain("设计.美学纲领与交互范式");
|
expect(loaded.worker.inputTags).toContain("设计.美学纲领与交互范式");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("layers follow longest dependency path", () => {
|
||||||
|
const flow = parseCreationFlow(`{
|
||||||
|
"steps": [
|
||||||
|
{ "name": "美学纲领与交互范式", "depends_on": [] },
|
||||||
|
{ "name": "生成规则", "depends_on": ["美学纲领与交互范式"] },
|
||||||
|
{ "name": "具体实例", "depends_on": ["生成规则"] },
|
||||||
|
{ "name": "开场白与开场变量", "depends_on": ["美学纲领与交互范式", "具体实例"] }
|
||||||
|
]
|
||||||
|
}`)!;
|
||||||
|
const layers = computeStepLayers(flow);
|
||||||
|
expect(layers.get("美学纲领与交互范式")).toBe(0);
|
||||||
|
expect(layers.get("生成规则")).toBe(1);
|
||||||
|
expect(layers.get("具体实例")).toBe(2);
|
||||||
|
expect(layers.get("开场白与开场变量")).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("listReadySteps returns every unblocked pending step", () => {
|
||||||
|
const flow = parseCreationFlow(`{
|
||||||
|
"steps": [
|
||||||
|
{ "name": "美学纲领与交互范式", "depends_on": [] },
|
||||||
|
{ "name": "生成规则", "depends_on": ["美学纲领与交互范式"] },
|
||||||
|
{ "id": "实现机制", "name": "实现机制", "depends_on": ["美学纲领与交互范式"] }
|
||||||
|
]
|
||||||
|
}`)!;
|
||||||
|
expect(listReadySteps(flow, []).map((s) => s.id)).toEqual([
|
||||||
|
"美学纲领与交互范式",
|
||||||
|
]);
|
||||||
expect(
|
expect(
|
||||||
loaded.worker.contextSegments?.some((s) => s.id === "inherit-existing"),
|
listReadySteps(flow, ["美学纲领与交互范式"]).map((s) => s.id),
|
||||||
|
).toEqual(["生成规则", "实现机制"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("spawns instance from prototype before closer and hangs instance on closer deps", () => {
|
||||||
|
const flow = parseCreationFlow(`{
|
||||||
|
"status": "open",
|
||||||
|
"steps": [
|
||||||
|
{ "name": "美学纲领与交互范式", "depends_on": [] },
|
||||||
|
{ "name": "开场白与开场变量", "depends_on": ["美学纲领与交互范式"] }
|
||||||
|
]
|
||||||
|
}`)!;
|
||||||
|
expect(
|
||||||
|
hasSelectableCreationWork(flow, sampleCatalog, ["美学纲领与交互范式"]),
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
|
const spawned = spawnRepeatableCreationStep({
|
||||||
|
flow,
|
||||||
|
catalog: sampleCatalog,
|
||||||
|
moduleName: "生成规则",
|
||||||
|
acceptedStepIds: ["美学纲领与交互范式"],
|
||||||
|
});
|
||||||
|
expect("error" in spawned).toBe(false);
|
||||||
|
if ("error" in spawned) return;
|
||||||
|
expect(spawned.step.id).toBe("生成规则#1");
|
||||||
|
expect(spawned.step.role).toBe("instance");
|
||||||
|
expect(spawned.step.depends_on).toEqual(["美学纲领与交互范式"]);
|
||||||
|
expect(spawned.flow.steps.map((s) => s.id)).toEqual([
|
||||||
|
"美学纲领与交互范式",
|
||||||
|
"生成规则",
|
||||||
|
"生成规则#1",
|
||||||
|
"开场白与开场变量",
|
||||||
|
]);
|
||||||
|
expect(
|
||||||
|
spawned.flow.steps.find((s) => s.name === "开场白与开场变量")?.depends_on,
|
||||||
|
).toEqual(["美学纲领与交互范式", "生成规则#1"]);
|
||||||
|
|
||||||
|
const second = spawnRepeatableCreationStep({
|
||||||
|
flow: spawned.flow,
|
||||||
|
catalog: sampleCatalog,
|
||||||
|
moduleName: "生成规则",
|
||||||
|
acceptedStepIds: ["美学纲领与交互范式"],
|
||||||
|
});
|
||||||
|
expect("error" in second).toBe(false);
|
||||||
|
if ("error" in second) return;
|
||||||
|
expect(second.step.id).toBe("生成规则#2");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("removeUnstartedInstance drops empty 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": ["美学纲领与交互范式"] },
|
||||||
|
{ "name": "开场白与开场变量", "depends_on": ["美学纲领与交互范式", "生成规则#1"] }
|
||||||
|
]
|
||||||
|
}`)!;
|
||||||
|
const next = removeUnstartedInstance(flow, "生成规则#1", []);
|
||||||
|
expect(next.steps.map((s) => s.id)).toEqual([
|
||||||
|
"美学纲领与交互范式",
|
||||||
|
"生成规则",
|
||||||
|
"开场白与开场变量",
|
||||||
|
]);
|
||||||
|
expect(
|
||||||
|
next.steps.find((s) => s.name === "开场白与开场变量")?.depends_on,
|
||||||
|
).toEqual(["美学纲领与交互范式"]);
|
||||||
|
const kept = removeUnstartedInstance(next, "美学纲领与交互范式", []);
|
||||||
|
expect(kept.steps.map((s) => s.id)).toEqual(next.steps.map((s) => s.id));
|
||||||
|
const accepted = removeUnstartedInstance(flow, "生成规则#1", ["生成规则#1"]);
|
||||||
|
expect(accepted.steps.some((s) => s.id === "生成规则#1")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("blocks 具体实例 spawn until a 生成规则 instance is accepted", () => {
|
||||||
|
const flow = parseCreationFlow(`{
|
||||||
|
"steps": [
|
||||||
|
{ "name": "美学纲领与交互范式", "depends_on": [] },
|
||||||
|
{
|
||||||
|
"id": "生成规则",
|
||||||
|
"name": "生成规则",
|
||||||
|
"role": "prototype",
|
||||||
|
"depends_on": ["美学纲领与交互范式"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`)!;
|
||||||
|
const blocked = spawnRepeatableCreationStep({
|
||||||
|
flow,
|
||||||
|
catalog: sampleCatalog,
|
||||||
|
moduleName: "具体实例",
|
||||||
|
acceptedStepIds: ["美学纲领与交互范式"],
|
||||||
|
});
|
||||||
|
expect(blocked).toMatchObject({ error: "先完成一条生成规则" });
|
||||||
|
const ruleSpawn = spawnInstanceFromPrototype({
|
||||||
|
flow,
|
||||||
|
catalog: sampleCatalog,
|
||||||
|
prototypeId: "生成规则",
|
||||||
|
acceptedStepIds: ["美学纲领与交互范式"],
|
||||||
|
});
|
||||||
|
expect("error" in ruleSpawn).toBe(false);
|
||||||
|
if ("error" in ruleSpawn) return;
|
||||||
|
const withRule = ruleSpawn.flow;
|
||||||
|
const ok = spawnRepeatableCreationStep({
|
||||||
|
flow: withRule,
|
||||||
|
catalog: sampleCatalog,
|
||||||
|
moduleName: "具体实例",
|
||||||
|
acceptedStepIds: ["美学纲领与交互范式", "生成规则#1"],
|
||||||
|
});
|
||||||
|
expect("error" in ok).toBe(false);
|
||||||
|
if ("error" in ok) return;
|
||||||
|
expect(ok.step.depends_on).toEqual(["生成规则#1"]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,12 +8,21 @@ import {
|
|||||||
import { parseWorkerSetYaml } from "../src/skills/worker-set-parse.js";
|
import { parseWorkerSetYaml } from "../src/skills/worker-set-parse.js";
|
||||||
|
|
||||||
describe("resolveAcceptanceModeForWorker", () => {
|
describe("resolveAcceptanceModeForWorker", () => {
|
||||||
it("always confirms design-flow", () => {
|
it("auto-continues design-flow without a confirm step", () => {
|
||||||
const mode = resolveAcceptanceModeForWorker({
|
const mode = resolveAcceptanceModeForWorker({
|
||||||
session: createSession(),
|
session: createSession(),
|
||||||
blackboard: new Blackboard(),
|
blackboard: new Blackboard(),
|
||||||
workerId: "design-flow",
|
workerId: "design-flow",
|
||||||
});
|
});
|
||||||
|
expect(mode).toBe("no_confirmation");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("still confirms design-step", () => {
|
||||||
|
const mode = resolveAcceptanceModeForWorker({
|
||||||
|
session: createSession(),
|
||||||
|
blackboard: new Blackboard(),
|
||||||
|
workerId: "design-step",
|
||||||
|
});
|
||||||
expect(mode).toBe("user_confirmed");
|
expect(mode).toBe("user_confirmed");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
displayStageLabel,
|
displayStageLabel,
|
||||||
displayWorkerLabel,
|
displayWorkerLabel,
|
||||||
formatWorkerDisplayTitle,
|
formatWorkerDisplayTitle,
|
||||||
|
isFlowPlanReview,
|
||||||
reviewComposerCopy,
|
reviewComposerCopy,
|
||||||
} from "../src/server/display-labels.js";
|
} from "../src/server/display-labels.js";
|
||||||
|
|
||||||
@@ -52,4 +53,25 @@ describe("display-labels", () => {
|
|||||||
expect(setup.acceptLabel).toBe("选定此开场");
|
expect(setup.acceptLabel).toBe("选定此开场");
|
||||||
expect(setup.kind).toBe("opening");
|
expect(setup.kind).toBe("opening");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("treats design-flow review as the pick-node surface", () => {
|
||||||
|
expect(
|
||||||
|
isFlowPlanReview({
|
||||||
|
waitingReason: { kind: "review_artifact" },
|
||||||
|
reviewArtifact: { workerId: "design-flow" },
|
||||||
|
}),
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
isFlowPlanReview({
|
||||||
|
waitingReason: { kind: "review_artifact" },
|
||||||
|
reviewArtifact: { workerId: "design-step" },
|
||||||
|
}),
|
||||||
|
).toBe(false);
|
||||||
|
expect(
|
||||||
|
isFlowPlanReview({
|
||||||
|
waitingReason: { kind: "pick_creation_step" },
|
||||||
|
reviewArtifact: { workerId: "design-flow" },
|
||||||
|
}),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,249 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import {
|
||||||
|
applyOpenAiGeneration,
|
||||||
|
applyRejectedGenerationField,
|
||||||
|
classifyChatModel,
|
||||||
|
fetchWithGenerationCompat,
|
||||||
|
parseRejectedGenerationField,
|
||||||
|
sanitizeReasoningEffort,
|
||||||
|
sanitizeVerbosity,
|
||||||
|
} from "../src/llm/generation-compat.js";
|
||||||
|
import { buildRequestBody } from "../src/llm/client.js";
|
||||||
|
import type { LlmConfig } from "../src/config/env.js";
|
||||||
|
|
||||||
|
const stGeneration = {
|
||||||
|
temperature: 1,
|
||||||
|
topP: 0.95,
|
||||||
|
topK: 40,
|
||||||
|
minP: 0.05,
|
||||||
|
frequencyPenalty: 0,
|
||||||
|
presencePenalty: 0,
|
||||||
|
repetitionPenalty: 1.05,
|
||||||
|
maxOutputTokens: 60000,
|
||||||
|
seed: -1,
|
||||||
|
reasoningEffort: "auto",
|
||||||
|
verbosity: "auto",
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("sanitizeReasoningEffort", () => {
|
||||||
|
it("omits SillyTavern auto and empty values", () => {
|
||||||
|
expect(sanitizeReasoningEffort("auto")).toBeUndefined();
|
||||||
|
expect(sanitizeReasoningEffort("AUTO")).toBeUndefined();
|
||||||
|
expect(sanitizeReasoningEffort("")).toBeUndefined();
|
||||||
|
expect(sanitizeReasoningEffort(" ")).toBeUndefined();
|
||||||
|
expect(sanitizeReasoningEffort(undefined)).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps values the provider enum accepts", () => {
|
||||||
|
expect(sanitizeReasoningEffort("none")).toBe("none");
|
||||||
|
expect(sanitizeReasoningEffort("minimal")).toBe("minimal");
|
||||||
|
expect(sanitizeReasoningEffort("low")).toBe("low");
|
||||||
|
expect(sanitizeReasoningEffort("medium")).toBe("medium");
|
||||||
|
expect(sanitizeReasoningEffort("high")).toBe("high");
|
||||||
|
expect(sanitizeReasoningEffort("xhigh")).toBe("xhigh");
|
||||||
|
expect(sanitizeReasoningEffort("max")).toBe("max");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("maps SillyTavern min to minimal and ignores unknown values", () => {
|
||||||
|
expect(sanitizeReasoningEffort("min")).toBe("minimal");
|
||||||
|
expect(sanitizeReasoningEffort("MIN")).toBe("minimal");
|
||||||
|
expect(sanitizeReasoningEffort("unknown")).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("sanitizeVerbosity", () => {
|
||||||
|
it("omits auto and unknown values", () => {
|
||||||
|
expect(sanitizeVerbosity("auto")).toBeUndefined();
|
||||||
|
expect(sanitizeVerbosity("loud")).toBeUndefined();
|
||||||
|
expect(sanitizeVerbosity("medium")).toBe("medium");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("classifyChatModel", () => {
|
||||||
|
it("treats o-series, gpt-5, kimi-k2.5 and deepseek-reasoner as no-sampling", () => {
|
||||||
|
expect(classifyChatModel("o3-mini").omitSampling).toBe(true);
|
||||||
|
expect(classifyChatModel("openai/o4-mini").omitSampling).toBe(true);
|
||||||
|
expect(classifyChatModel("gpt-5").omitSampling).toBe(true);
|
||||||
|
expect(classifyChatModel("deepseek-reasoner").omitSampling).toBe(true);
|
||||||
|
expect(classifyChatModel("kimi-k2.5").omitSampling).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps sampling for ordinary chat models", () => {
|
||||||
|
expect(classifyChatModel("gpt-4o").omitSampling).toBe(false);
|
||||||
|
expect(classifyChatModel("deepseek-v4-pro").omitSampling).toBe(false);
|
||||||
|
expect(classifyChatModel("gpt-5-chat-latest").omitSampling).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("applyOpenAiGeneration", () => {
|
||||||
|
it("does not send ST extras or auto sentinels on a chat model", () => {
|
||||||
|
const body: Record<string, unknown> = { model: "deepseek-v4-pro" };
|
||||||
|
applyOpenAiGeneration(body, stGeneration, "deepseek-v4-pro");
|
||||||
|
expect(body.temperature).toBe(1);
|
||||||
|
expect(body.top_p).toBe(0.95);
|
||||||
|
expect(body.max_tokens).toBe(60000);
|
||||||
|
expect(body.top_k).toBeUndefined();
|
||||||
|
expect(body.min_p).toBeUndefined();
|
||||||
|
expect(body.repetition_penalty).toBeUndefined();
|
||||||
|
expect(body.reasoning_effort).toBeUndefined();
|
||||||
|
expect(body.verbosity).toBeUndefined();
|
||||||
|
expect(body.seed).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("drops sampling params for reasoning models and uses max_completion_tokens", () => {
|
||||||
|
const body: Record<string, unknown> = { model: "o3-mini" };
|
||||||
|
applyOpenAiGeneration(body, stGeneration, "o3-mini");
|
||||||
|
expect(body.temperature).toBeUndefined();
|
||||||
|
expect(body.top_p).toBeUndefined();
|
||||||
|
expect(body.frequency_penalty).toBeUndefined();
|
||||||
|
expect(body.presence_penalty).toBeUndefined();
|
||||||
|
expect(body.max_tokens).toBeUndefined();
|
||||||
|
expect(body.max_completion_tokens).toBe(60000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("parseRejectedGenerationField", () => {
|
||||||
|
it("reads OpenAI unsupported parameter errors", () => {
|
||||||
|
expect(
|
||||||
|
parseRejectedGenerationField(
|
||||||
|
JSON.stringify({
|
||||||
|
error: {
|
||||||
|
message: "Unsupported parameter: 'temperature' is not supported with this model.",
|
||||||
|
param: "temperature",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
).toBe("temperature");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reads strict JSON deserializer unknown variant errors", () => {
|
||||||
|
expect(
|
||||||
|
parseRejectedGenerationField(
|
||||||
|
JSON.stringify({
|
||||||
|
error: {
|
||||||
|
message:
|
||||||
|
"Failed to deserialize the JSON body into the target type: reasoning_effort: unknown variant `auto`, expected one of `none`, `minimal`",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
).toBe("reasoning_effort");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("drops top_p when temperature and top_p are mutually exclusive", () => {
|
||||||
|
expect(
|
||||||
|
parseRejectedGenerationField(
|
||||||
|
"temperature and top_p cannot both be specified for this model",
|
||||||
|
),
|
||||||
|
).toBe("top_p");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("applyRejectedGenerationField", () => {
|
||||||
|
it("renames max_completion_tokens to max_tokens once", () => {
|
||||||
|
const body: Record<string, unknown> = { max_completion_tokens: 100 };
|
||||||
|
const tried = new Set<string>();
|
||||||
|
expect(applyRejectedGenerationField(body, "max_completion_tokens", tried)).toBe(
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
expect(body.max_tokens).toBe(100);
|
||||||
|
expect(body.max_completion_tokens).toBeUndefined();
|
||||||
|
expect(applyRejectedGenerationField(body, "max_tokens", tried)).toBe(true);
|
||||||
|
expect(body.max_completion_tokens).toBeUndefined();
|
||||||
|
expect(body.max_tokens).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("fetchWithGenerationCompat", () => {
|
||||||
|
it("retries a 400 after stripping the rejected field", async () => {
|
||||||
|
const sent: Array<Record<string, unknown>> = [];
|
||||||
|
const response = await fetchWithGenerationCompat(async (body) => {
|
||||||
|
sent.push({ ...body });
|
||||||
|
if ("temperature" in body) {
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
error: {
|
||||||
|
message: "Unsupported parameter: 'temperature'",
|
||||||
|
param: "temperature",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{ status: 400 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return new Response(JSON.stringify({ ok: true }), { status: 200 });
|
||||||
|
}, { model: "o3", temperature: 1, messages: [] });
|
||||||
|
|
||||||
|
expect(response.ok).toBe(true);
|
||||||
|
expect(sent).toHaveLength(2);
|
||||||
|
expect(sent[0].temperature).toBe(1);
|
||||||
|
expect(sent[1].temperature).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("buildRequestBody", () => {
|
||||||
|
const messages = [{ role: "user" as const, content: "hi" }];
|
||||||
|
|
||||||
|
it("does not send reasoning_effort when the preset says auto", () => {
|
||||||
|
const config: LlmConfig = {
|
||||||
|
baseUrl: "https://example.test/v1",
|
||||||
|
apiKey: "k",
|
||||||
|
model: "deepseek-v4-pro",
|
||||||
|
};
|
||||||
|
const body = buildRequestBody(config, messages, {
|
||||||
|
generation: { reasoningEffort: "auto", temperature: 0.5 },
|
||||||
|
});
|
||||||
|
expect(body.reasoning_effort).toBeUndefined();
|
||||||
|
expect(body.temperature).toBe(0.5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sends a supported effort value", () => {
|
||||||
|
const config: LlmConfig = {
|
||||||
|
baseUrl: "https://example.test/v1",
|
||||||
|
apiKey: "k",
|
||||||
|
model: "gpt-5",
|
||||||
|
};
|
||||||
|
const body = buildRequestBody(config, messages, {
|
||||||
|
generation: { reasoningEffort: "High" },
|
||||||
|
});
|
||||||
|
expect(body.reasoning_effort).toBe("high");
|
||||||
|
expect(body.temperature).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses profile default reasoningEffort when generation omits it", () => {
|
||||||
|
const config: LlmConfig = {
|
||||||
|
baseUrl: "https://example.test/v1",
|
||||||
|
apiKey: "k",
|
||||||
|
model: "glm-5.3",
|
||||||
|
reasoningEffort: "high",
|
||||||
|
};
|
||||||
|
const body = buildRequestBody(config, messages, {
|
||||||
|
generation: { temperature: 1 },
|
||||||
|
});
|
||||||
|
expect(body.reasoning_effort).toBe("high");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("lets generation reasoningEffort override the profile default", () => {
|
||||||
|
const config: LlmConfig = {
|
||||||
|
baseUrl: "https://example.test/v1",
|
||||||
|
apiKey: "k",
|
||||||
|
model: "glm-5.3",
|
||||||
|
reasoningEffort: "max",
|
||||||
|
};
|
||||||
|
const body = buildRequestBody(config, messages, {
|
||||||
|
generation: { reasoningEffort: "low" },
|
||||||
|
});
|
||||||
|
expect(body.reasoning_effort).toBe("low");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not forward top_k from an imported ST preset", () => {
|
||||||
|
const config: LlmConfig = {
|
||||||
|
baseUrl: "https://example.test/v1",
|
||||||
|
apiKey: "k",
|
||||||
|
model: "deepseek-v4-pro",
|
||||||
|
};
|
||||||
|
const body = buildRequestBody(config, messages, {
|
||||||
|
generation: stGeneration,
|
||||||
|
});
|
||||||
|
expect(body.top_k).toBeUndefined();
|
||||||
|
expect(body.min_p).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import {
|
||||||
|
convertStructuralSmartQuotes,
|
||||||
|
tryParseJsonDoc,
|
||||||
|
} from "../web/json-doc.js";
|
||||||
|
|
||||||
|
describe("tryParseJsonDoc", () => {
|
||||||
|
it("keeps interior Chinese quotes in an otherwise valid fragment", () => {
|
||||||
|
const raw = JSON.stringify({
|
||||||
|
schema: "context-fragment.v1",
|
||||||
|
技能: "美学纲领与交互范式",
|
||||||
|
正文: {
|
||||||
|
设定逻辑: {
|
||||||
|
参与方式: {
|
||||||
|
用户与user关系: {
|
||||||
|
依据: "用户明确“只有我不会被感染”,且为爽文主角",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const parsed = tryParseJsonDoc(`## 设计.美学纲领与交互范式\n\n${raw}`);
|
||||||
|
expect(parsed?.技能).toBe("美学纲领与交互范式");
|
||||||
|
expect(parsed?.正文?.设定逻辑?.参与方式?.用户与user关系?.依据).toContain(
|
||||||
|
"只有我不会被感染",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("parses JSON that uses smart quotes as delimiters", () => {
|
||||||
|
const raw = "{“schema”:“context-fragment.v1”,“brief”:“点题”}";
|
||||||
|
expect(convertStructuralSmartQuotes(raw)).toBe(
|
||||||
|
'{"schema":"context-fragment.v1","brief":"点题"}',
|
||||||
|
);
|
||||||
|
expect(tryParseJsonDoc(raw)).toEqual({
|
||||||
|
schema: "context-fragment.v1",
|
||||||
|
brief: "点题",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
isOpeningSealArtifact,
|
isOpeningSealArtifact,
|
||||||
parseOpeningSealPayload,
|
parseOpeningSealPayload,
|
||||||
openingVariablesToTableDoc,
|
openingVariablesToTableDoc,
|
||||||
|
reopenCreationFlowRaw,
|
||||||
} from "../src/skills/opening-seal.js";
|
} from "../src/skills/opening-seal.js";
|
||||||
|
|
||||||
const fragment = JSON.stringify({
|
const fragment = JSON.stringify({
|
||||||
@@ -71,4 +72,14 @@ describe("opening-seal", () => {
|
|||||||
const closed = JSON.parse(closeCreationFlowRaw(raw)!);
|
const closed = JSON.parse(closeCreationFlowRaw(raw)!);
|
||||||
expect(closed.status).toBe("closed");
|
expect(closed.status).toBe("closed");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("reopens a closed creation flow", () => {
|
||||||
|
const raw = JSON.stringify({
|
||||||
|
version: 1,
|
||||||
|
status: "closed",
|
||||||
|
steps: [{ id: "开场白与开场变量", name: "开场白与开场变量", depends_on: [] }],
|
||||||
|
});
|
||||||
|
const opened = JSON.parse(reopenCreationFlowRaw(raw)!);
|
||||||
|
expect(opened.status).toBe("open");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import {
|
||||||
|
extractLastProductJson,
|
||||||
|
selectJsonPayload,
|
||||||
|
tryParseJsonDoc,
|
||||||
|
} from "../src/parse/json-doc.js";
|
||||||
|
|
||||||
|
describe("selectJsonPayload", () => {
|
||||||
|
it("keeps content when it is already the product", () => {
|
||||||
|
const json = '{"schema":"context-fragment.v1","brief":"a","正文":{}}';
|
||||||
|
expect(selectJsonPayload(json, "thinking...")).toBe(json);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("falls back to reasoning when content is not JSON", () => {
|
||||||
|
const json = '{"schema":"context-fragment.v1","brief":"a","正文":{}}';
|
||||||
|
expect(selectJsonPayload("Done.", json)).toBe(json);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("extractLastProductJson", () => {
|
||||||
|
it("skips an earlier example object and takes the fragment", () => {
|
||||||
|
const raw = `example { "foo": 1 } then ${JSON.stringify({
|
||||||
|
schema: "context-fragment.v1",
|
||||||
|
brief: "点题",
|
||||||
|
正文: { x: 1 },
|
||||||
|
})}`;
|
||||||
|
const parsed = extractLastProductJson(raw) as { brief?: string };
|
||||||
|
expect(parsed?.brief).toBe("点题");
|
||||||
|
expect(tryParseJsonDoc(raw)).toMatchObject({ brief: "点题" });
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -541,4 +541,160 @@ describe("phase machine", () => {
|
|||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("rejecting the next step goes back to design-flow instead of the main agent", () => {
|
||||||
|
let session = createSession("default");
|
||||||
|
const decision = {
|
||||||
|
id: "d1",
|
||||||
|
action: "run_worker" as const,
|
||||||
|
reason: "下一步开场白",
|
||||||
|
workerId: "design-step",
|
||||||
|
requiresApproval: true,
|
||||||
|
statePatchAllowed: false as const,
|
||||||
|
};
|
||||||
|
session = {
|
||||||
|
...session,
|
||||||
|
phase: "waiting_user",
|
||||||
|
waitingReason: { kind: "approve_step", decisionId: decision.id },
|
||||||
|
pendingDecision: decision,
|
||||||
|
};
|
||||||
|
|
||||||
|
const rejected = applyEvent(session, {
|
||||||
|
type: "user_rejected_next_step",
|
||||||
|
payload: { decisionId: decision.id, reason: "还没做 NPC" },
|
||||||
|
});
|
||||||
|
expect(rejected.session.phase).toBe("running");
|
||||||
|
expect(rejected.session.slots["用户.下一步意向"]).toBe("还没做 NPC");
|
||||||
|
expect(rejected.session.pendingDecision).toBeUndefined();
|
||||||
|
expect(rejected.effects).toEqual([
|
||||||
|
{ type: "unseal_creation_opening" },
|
||||||
|
{ type: "run_worker", workerId: "design-flow" },
|
||||||
|
]);
|
||||||
|
expect(rejected.effects.some((e) => e.type === "invoke_main_agent")).toBe(
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("replan from opening review runs design-flow without revising the opening", () => {
|
||||||
|
let session = createSession("default");
|
||||||
|
const artifact = createArtifact({
|
||||||
|
workerId: "design-step",
|
||||||
|
stepId: "开场白与开场变量",
|
||||||
|
outputTags: ["设计.开场白与开场变量"],
|
||||||
|
});
|
||||||
|
session = {
|
||||||
|
...session,
|
||||||
|
artifacts: [artifact],
|
||||||
|
pendingArtifactId: artifact.id,
|
||||||
|
phase: "waiting_user",
|
||||||
|
waitingReason: { kind: "review_artifact", artifactId: artifact.id },
|
||||||
|
slots: { creationSealedByOpening: true },
|
||||||
|
};
|
||||||
|
|
||||||
|
const replanned = applyEvent(session, {
|
||||||
|
type: "user_requested_flow_replan",
|
||||||
|
payload: { reason: "漏了开局 NPC" },
|
||||||
|
});
|
||||||
|
expect(replanned.session.phase).toBe("running");
|
||||||
|
expect(replanned.session.pendingArtifactId).toBeUndefined();
|
||||||
|
expect(replanned.session.slots.creationSealedByOpening).toBeUndefined();
|
||||||
|
expect(replanned.session.artifacts[0]?.status).toBe("rejected");
|
||||||
|
expect(replanned.session.slots["用户.修订说明"]).toBe("漏了开局 NPC");
|
||||||
|
expect(replanned.effects).toEqual([
|
||||||
|
{ type: "unseal_creation_opening" },
|
||||||
|
{ type: "run_worker", workerId: "design-flow" },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("awaits pick on the layered graph and starts the clicked step", () => {
|
||||||
|
let session = createSession("default");
|
||||||
|
session = {
|
||||||
|
...session,
|
||||||
|
phase: "running",
|
||||||
|
};
|
||||||
|
const awaited = applyEvent(session, {
|
||||||
|
type: "creation_step_pick_awaited",
|
||||||
|
payload: {},
|
||||||
|
});
|
||||||
|
expect(awaited.session.phase).toBe("waiting_user");
|
||||||
|
expect(awaited.session.waitingReason?.kind).toBe("pick_creation_step");
|
||||||
|
expect(awaited.session.currentWorkerId).toBeUndefined();
|
||||||
|
|
||||||
|
const picked = applyEvent(awaited.session, {
|
||||||
|
type: "user_picked_creation_step",
|
||||||
|
payload: { stepId: "生成规则" },
|
||||||
|
});
|
||||||
|
expect(picked.session.phase).toBe("running");
|
||||||
|
expect(picked.session.currentWorkerId).toBe("design-step");
|
||||||
|
expect(picked.effects).toEqual([
|
||||||
|
{ type: "run_worker", workerId: "design-step" },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("leaving a creation step returns to the pick graph", () => {
|
||||||
|
let session = createSession("default");
|
||||||
|
session = {
|
||||||
|
...session,
|
||||||
|
phase: "waiting_user",
|
||||||
|
waitingReason: {
|
||||||
|
kind: "worker_questions",
|
||||||
|
workerId: "design-step",
|
||||||
|
questions: [{ id: "module-opening", prompt: "引导" }],
|
||||||
|
},
|
||||||
|
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.currentWorkerId).toBeUndefined();
|
||||||
|
expect(left.session.currentStepId).toBeUndefined();
|
||||||
|
expect(left.session.resumeContext).toBeUndefined();
|
||||||
|
expect(left.effects).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("replan from the pick graph runs design-flow", () => {
|
||||||
|
let session = createSession("default");
|
||||||
|
session = {
|
||||||
|
...session,
|
||||||
|
phase: "waiting_user",
|
||||||
|
waitingReason: { kind: "pick_creation_step" },
|
||||||
|
};
|
||||||
|
const replanned = applyEvent(session, {
|
||||||
|
type: "user_requested_flow_replan",
|
||||||
|
payload: { reason: "补舞台骨架" },
|
||||||
|
});
|
||||||
|
expect(replanned.session.phase).toBe("running");
|
||||||
|
expect(replanned.session.slots["用户.下一步意向"]).toBe("补舞台骨架");
|
||||||
|
expect(replanned.effects).toEqual([
|
||||||
|
{ type: "unseal_creation_opening" },
|
||||||
|
{ type: "run_worker", workerId: "design-flow" },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("design-flow without confirmation proposes the pick graph", () => {
|
||||||
|
let session = createSession("default");
|
||||||
|
session = {
|
||||||
|
...session,
|
||||||
|
phase: "running",
|
||||||
|
acceptanceMode: "no_confirmation",
|
||||||
|
};
|
||||||
|
const artifact = createArtifact({
|
||||||
|
workerId: "design-flow",
|
||||||
|
outputTags: ["设计.创作流程"],
|
||||||
|
});
|
||||||
|
session = { ...session, artifacts: [artifact] };
|
||||||
|
const completed = applyEvent(session, {
|
||||||
|
type: "worker_completed",
|
||||||
|
payload: { artifactId: artifact.id },
|
||||||
|
});
|
||||||
|
expect(completed.session.artifacts[0]?.status).toBe("accepted");
|
||||||
|
expect(completed.session.pendingArtifactId).toBeUndefined();
|
||||||
|
expect(completed.effects).toEqual([
|
||||||
|
{ type: "propose_next_creation_step" },
|
||||||
|
]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
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 { 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";
|
||||||
|
|
||||||
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 () => {
|
||||||
@@ -129,4 +130,167 @@ describe("phase runtime", () => {
|
|||||||
await runtime.start();
|
await runtime.start();
|
||||||
await expect(runtime.retryStuckRun()).rejects.toThrow("当前没有可重试的执行");
|
await expect(runtime.retryStuckRun()).rejects.toThrow("当前没有可重试的执行");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("first input with seeded DAG waits for node pick instead of calling main agent", async () => {
|
||||||
|
const llm = new MockLlmProvider(["Let me analyze the DAG in English prose"]);
|
||||||
|
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.submitInput("丧尸世界,但只有我不会被感染");
|
||||||
|
expect(runtime.getSession().phase).toBe("waiting_user");
|
||||||
|
expect(runtime.getSession().waitingReason?.kind).toBe("pick_creation_step");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("enterSeededCreationPick skips first demand and waits on the seeded graph", async () => {
|
||||||
|
const runtime = new PhaseRuntime();
|
||||||
|
await runtime.start();
|
||||||
|
runtime.getBlackboard().write({
|
||||||
|
tag: "设计.创作流程",
|
||||||
|
content: JSON.stringify({
|
||||||
|
version: 1,
|
||||||
|
status: "open",
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
id: "舞台骨架",
|
||||||
|
name: "舞台骨架",
|
||||||
|
depends_on: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
source: "runtime",
|
||||||
|
});
|
||||||
|
await runtime.enterSeededCreationPick();
|
||||||
|
expect(runtime.getSession().slots.startupCompleted).toBe(true);
|
||||||
|
expect(runtime.getSession().waitingReason?.kind).toBe("pick_creation_step");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("unusable main agent output without DAG starts design-flow", async () => {
|
||||||
|
const llm = new MockLlmProvider(["Let me think about routing"]);
|
||||||
|
const runtime = new PhaseRuntime({ llm, autoStubWorker: true });
|
||||||
|
await runtime.start();
|
||||||
|
await runtime.submitInput("网恋对象对话");
|
||||||
|
expect(runtime.getSession().phase).not.toBe("running");
|
||||||
|
expect(
|
||||||
|
runtime.getSession().artifacts.some((a) => a.workerId === "design-flow"),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("failRun leaves error phase instead of running", async () => {
|
||||||
|
const runtime = new PhaseRuntime();
|
||||||
|
await runtime.start();
|
||||||
|
await runtime.submitInput("随便写点");
|
||||||
|
expect(runtime.getSession().phase).toBe("running");
|
||||||
|
await runtime.failRun("fetch failed");
|
||||||
|
expect(runtime.getSession().phase).toBe("error");
|
||||||
|
expect(runtime.getSession().waitingReason).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("recoverOrphanedRun turns stuck running into node pick when DAG exists", async () => {
|
||||||
|
const runtime = new PhaseRuntime();
|
||||||
|
await runtime.start();
|
||||||
|
runtime.getBlackboard().write({
|
||||||
|
tag: "设计.创作流程",
|
||||||
|
content: JSON.stringify({
|
||||||
|
version: 1,
|
||||||
|
status: "open",
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
id: "美学纲领与交互范式",
|
||||||
|
name: "美学纲领与交互范式",
|
||||||
|
depends_on: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
source: "runtime",
|
||||||
|
});
|
||||||
|
const session = runtime.getSession();
|
||||||
|
session.phase = "running";
|
||||||
|
session.waitingReason = undefined;
|
||||||
|
expect(runtime.recoverOrphanedRun()).toBe(true);
|
||||||
|
expect(runtime.getSession().phase).toBe("waiting_user");
|
||||||
|
expect(runtime.getSession().waitingReason?.kind).toBe("pick_creation_step");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("leaveCreationStep returns to pick and unsawns an empty instance", async () => {
|
||||||
|
const session = createSession("default");
|
||||||
|
session.phase = "waiting_user";
|
||||||
|
session.waitingReason = {
|
||||||
|
kind: "worker_questions",
|
||||||
|
workerId: "design-step",
|
||||||
|
questions: [{ id: "module-opening", prompt: "生成规则引导" }],
|
||||||
|
};
|
||||||
|
session.currentWorkerId = "design-step";
|
||||||
|
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: ["美学纲领与交互范式"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "开场白与开场变量",
|
||||||
|
name: "开场白与开场变量",
|
||||||
|
depends_on: ["美学纲领与交互范式", "生成规则#1"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
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().currentWorkerId).toBeUndefined();
|
||||||
|
const flow = parseCreationFlow(
|
||||||
|
runtime.getBlackboard().getContentByTag("设计.创作流程"),
|
||||||
|
);
|
||||||
|
expect(flow?.steps.map((s) => s.id)).toEqual([
|
||||||
|
"美学纲领与交互范式",
|
||||||
|
"生成规则",
|
||||||
|
"开场白与开场变量",
|
||||||
|
]);
|
||||||
|
expect(
|
||||||
|
flow?.steps.find((s) => s.name === "开场白与开场变量")?.depends_on,
|
||||||
|
).toEqual(["美学纲领与交互范式"]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -127,4 +127,32 @@ describe("session persistence", () => {
|
|||||||
|
|
||||||
deleteBook(book.id);
|
deleteBook(book.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("createForBook with recipe lands on that recipe's seeded pick graph", async () => {
|
||||||
|
const { createBook, deleteBook } = await import("../src/book/store.js");
|
||||||
|
const { SessionManager } = await import("../src/server/session-manager.js");
|
||||||
|
|
||||||
|
const book = createBook({ title: "配方开局" });
|
||||||
|
const mgr = new SessionManager();
|
||||||
|
const view = await mgr.createForBook(
|
||||||
|
book.id,
|
||||||
|
"world-simulator",
|
||||||
|
"world-simulator",
|
||||||
|
);
|
||||||
|
expect(view.waitingReason?.kind).toBe("pick_creation_step");
|
||||||
|
expect(view.startupCompleted).toBe(true);
|
||||||
|
expect(view.uiPrompt).toBeUndefined();
|
||||||
|
expect(view.openingGuide).toBeNull();
|
||||||
|
const { loadAllRecipeDetails } = await import(
|
||||||
|
"../src/skills/creation-flow.js"
|
||||||
|
);
|
||||||
|
const details = await loadAllRecipeDetails("dialogue/world-simulator");
|
||||||
|
const seedNames = details
|
||||||
|
.find((d) => d.id === "world-simulator")
|
||||||
|
?.seed?.steps.map((s) => s.name);
|
||||||
|
expect(seedNames?.length).toBeGreaterThan(0);
|
||||||
|
expect(view.creationFlowView?.steps.map((s) => s.name)).toEqual(seedNames);
|
||||||
|
|
||||||
|
deleteBook(book.id);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
createMockToolCall,
|
createMockToolCall,
|
||||||
MockLlmProvider,
|
MockLlmProvider,
|
||||||
} from "../src/llm/client.js";
|
} from "../src/llm/client.js";
|
||||||
|
import { isUnusableMainAgentOutput } from "../src/main-agent/main-agent.js";
|
||||||
import { runMainAgentToolLoop } from "../src/main-agent/tool-loop.js";
|
import { runMainAgentToolLoop } from "../src/main-agent/tool-loop.js";
|
||||||
import { MAIN_AGENT_TOOL_DEFINITIONS } from "../src/main-agent/tools.js";
|
import { MAIN_AGENT_TOOL_DEFINITIONS } from "../src/main-agent/tools.js";
|
||||||
import { toolCallToDecision } from "../src/runtime/tool-registry.js";
|
import { toolCallToDecision } from "../src/runtime/tool-registry.js";
|
||||||
@@ -29,6 +30,15 @@ describe("main agent tool loop", () => {
|
|||||||
).toEqual(["read_blackboard", "run_worker"]);
|
).toEqual(["read_blackboard", "run_worker"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("detects unusable main agent output", () => {
|
||||||
|
expect(
|
||||||
|
isUnusableMainAgentOutput(
|
||||||
|
new Error("Main Agent returned invalid JSON: Let me analyze"),
|
||||||
|
),
|
||||||
|
).toBe(true);
|
||||||
|
expect(isUnusableMainAgentOutput(new Error("fetch failed"))).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
it("runs loop tools then returns terminal decision", async () => {
|
it("runs loop tools then returns terminal decision", async () => {
|
||||||
const llm = new MockLlmProvider([
|
const llm = new MockLlmProvider([
|
||||||
createMockToolCall("read_blackboard", { tags: ["book.brief"] }),
|
createMockToolCall("read_blackboard", { tags: ["book.brief"] }),
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import {
|
|||||||
parseWorkerResponseForTest,
|
parseWorkerResponseForTest,
|
||||||
sanitizeWorkerSetOutputs,
|
sanitizeWorkerSetOutputs,
|
||||||
} from "../src/worker/executor.js";
|
} from "../src/worker/executor.js";
|
||||||
|
import { selectJsonPayload } from "../src/parse/json-doc.js";
|
||||||
|
import { parseCreationFlow } from "../src/skills/creation-flow.js";
|
||||||
|
|
||||||
describe("worker-set prose rejection", () => {
|
describe("worker-set prose rejection", () => {
|
||||||
it("does not YAML-crash on Chinese prose; returns clear parseError", () => {
|
it("does not YAML-crash on Chinese prose; returns clear parseError", () => {
|
||||||
@@ -155,6 +157,39 @@ describe("sanitizeWorkerSetOutputs", () => {
|
|||||||
expect(result.askUser?.[0]?.prompt).not.toContain("为贴近你要的质感");
|
expect(result.askUser?.[0]?.prompt).not.toContain("为贴近你要的质感");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps nested aesthetics 正文 objects for specialty cards", () => {
|
||||||
|
const frag = {
|
||||||
|
schema: "context-fragment.v1",
|
||||||
|
技能: "美学纲领与交互范式",
|
||||||
|
brief: "孤立免疫",
|
||||||
|
正文: {
|
||||||
|
设定逻辑: {
|
||||||
|
参与方式: {
|
||||||
|
用户与user关系: {
|
||||||
|
结论: "完全代入",
|
||||||
|
完备度: "80%",
|
||||||
|
依据: "用户明确只有我不会被感染",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
交互范式: { 前置配置: { 人称: "你" } },
|
||||||
|
美学纲领: { 体验内核: "特权与惊惶" },
|
||||||
|
},
|
||||||
|
自评: {
|
||||||
|
维度: [{ 名: "美学纲领", 分数: 8, 说明: "点题" }],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const result = parseWorkerResponseForTest(JSON.stringify(frag), [
|
||||||
|
"设计.美学纲领与交互范式",
|
||||||
|
]);
|
||||||
|
const parsed = JSON.parse(result.outputs["设计.美学纲领与交互范式"]!);
|
||||||
|
const body = parsed.正文;
|
||||||
|
expect(typeof body.设定逻辑).toBe("object");
|
||||||
|
expect(body.设定逻辑.参与方式.用户与user关系.结论).toBe("完全代入");
|
||||||
|
expect(body.美学纲领.体验内核).toBe("特权与惊惶");
|
||||||
|
expect(parsed.自评.维度[0].分数).toBe(8);
|
||||||
|
});
|
||||||
|
|
||||||
it("recovers a root-level context-fragment into the artifact tag", () => {
|
it("recovers a root-level context-fragment into the artifact tag", () => {
|
||||||
const frag = {
|
const frag = {
|
||||||
schema: "context-fragment.v1",
|
schema: "context-fragment.v1",
|
||||||
@@ -173,6 +208,46 @@ describe("sanitizeWorkerSetOutputs", () => {
|
|||||||
expect(result.askUser).toBeUndefined();
|
expect(result.askUser).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("accepts unprefixed outputs keys and JSON sitting after thinking prose", () => {
|
||||||
|
const frag = {
|
||||||
|
schema: "context-fragment.v1",
|
||||||
|
技能: "美学纲领与交互范式",
|
||||||
|
brief: "孤立免疫",
|
||||||
|
正文: { 美学纲领: { 体验内核: "人人录我" } },
|
||||||
|
};
|
||||||
|
const prefixed = parseWorkerResponseForTest(
|
||||||
|
JSON.stringify({
|
||||||
|
outputs: { "美学纲领与交互范式": frag },
|
||||||
|
summary: "ok",
|
||||||
|
}),
|
||||||
|
["设计.美学纲领与交互范式"],
|
||||||
|
);
|
||||||
|
expect(prefixed.outputs["设计.美学纲领与交互范式"]).toContain("人人录我");
|
||||||
|
|
||||||
|
const mixed = parseWorkerResponseForTest(
|
||||||
|
`We must output JSON. Example { "foo": 1 }.\n${JSON.stringify(frag)}`,
|
||||||
|
["设计.美学纲领与交互范式"],
|
||||||
|
);
|
||||||
|
expect(mixed.outputs["设计.美学纲领与交互范式"]).toContain("人人录我");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses product JSON in reasoning when content is a closing sentence", () => {
|
||||||
|
const frag = {
|
||||||
|
schema: "context-fragment.v1",
|
||||||
|
技能: "美学纲领与交互范式",
|
||||||
|
brief: "点题",
|
||||||
|
正文: { 美学纲领: { 体验内核: "内核" } },
|
||||||
|
};
|
||||||
|
const payload = selectJsonPayload(
|
||||||
|
"The artifact is ready.",
|
||||||
|
JSON.stringify(frag),
|
||||||
|
);
|
||||||
|
const result = parseWorkerResponseForTest(payload, [
|
||||||
|
"设计.美学纲领与交互范式",
|
||||||
|
]);
|
||||||
|
expect(result.outputs["设计.美学纲领与交互范式"]).toContain("内核");
|
||||||
|
});
|
||||||
|
|
||||||
it("moves a fragment written to 创作.当前步骤 onto the artifact tag", () => {
|
it("moves a fragment written to 创作.当前步骤 onto the artifact tag", () => {
|
||||||
const frag = {
|
const frag = {
|
||||||
schema: "context-fragment.v1",
|
schema: "context-fragment.v1",
|
||||||
@@ -257,6 +332,51 @@ describe("sanitizeWorkerSetOutputs", () => {
|
|||||||
expect(doc.play_slots.gm).toBe(true);
|
expect(doc.play_slots.gm).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps a root-level creation flow DAG on 设计.创作流程", () => {
|
||||||
|
const dag = {
|
||||||
|
brief: "单角代入",
|
||||||
|
status: "open",
|
||||||
|
steps: [
|
||||||
|
{ id: "美学纲领与交互范式", name: "美学纲领与交互范式", depends_on: [] },
|
||||||
|
{
|
||||||
|
id: "生成规则",
|
||||||
|
name: "生成规则",
|
||||||
|
role: "prototype",
|
||||||
|
depends_on: ["美学纲领与交互范式"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const result = parseWorkerResponseForTest(JSON.stringify(dag), [
|
||||||
|
"设计.创作流程",
|
||||||
|
]);
|
||||||
|
const parsed = parseCreationFlow(result.outputs["设计.创作流程"]);
|
||||||
|
expect(parsed?.brief).toBe("单角代入");
|
||||||
|
expect(parsed?.steps.map((s) => s.name)).toEqual([
|
||||||
|
"美学纲领与交互范式",
|
||||||
|
"生成规则",
|
||||||
|
]);
|
||||||
|
expect(result.outputs["设计.创作流程"]).not.toContain("context-fragment");
|
||||||
|
expect(result.askUser).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps creation flow written under outputs bag", () => {
|
||||||
|
const result = parseWorkerResponseForTest(
|
||||||
|
JSON.stringify({
|
||||||
|
outputs: {
|
||||||
|
"设计.创作流程": {
|
||||||
|
status: "open",
|
||||||
|
steps: [{ name: "美学纲领与交互范式", depends_on: [] }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
summary: "流程 · 1 步 · open",
|
||||||
|
}),
|
||||||
|
["设计.创作流程"],
|
||||||
|
);
|
||||||
|
expect(parseCreationFlow(result.outputs["设计.创作流程"])?.steps[0]?.name).toBe(
|
||||||
|
"美学纲领与交互范式",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it("keeps a whole-response truncated JSON as the artifact draft", () => {
|
it("keeps a whole-response truncated JSON as the artifact draft", () => {
|
||||||
const raw =
|
const raw =
|
||||||
'{"outputs":{"设计.上下文投影排序":{"schema":"context-order.v1","brief":"旁观+转述","agents":[';
|
'{"outputs":{"设计.上下文投影排序":{"schema":"context-order.v1","brief":"旁观+转述","agents":[';
|
||||||
|
|||||||
+799
-175
File diff suppressed because it is too large
Load Diff
+259
-65
@@ -1,7 +1,7 @@
|
|||||||
import { renderWorkspace, updateLiveStreamPanel, resetRailChrome } from "./agent-ui.js";
|
import { renderWorkspace, updateLiveStreamPanel, resetRailChrome, fillStepArtifactDialog } from "./agent-ui.js";
|
||||||
import { downloadMarkdown, sessionToMarkdown } from "./export.js";
|
import { downloadMarkdown, sessionToMarkdown } from "./export.js";
|
||||||
import { renderIntakePanel } from "./intake-ui.js";
|
import { renderIntakePanel } from "./intake-ui.js";
|
||||||
import { displaySkillPackLabel, reviewComposerCopy } from "./display-labels.js";
|
import { displaySkillPackLabel, isFlowPlanReview, reviewComposerCopy } from "./display-labels.js";
|
||||||
import {
|
import {
|
||||||
clearQuestionCardState,
|
clearQuestionCardState,
|
||||||
collectQuestionAnswers,
|
collectQuestionAnswers,
|
||||||
@@ -17,9 +17,11 @@ let books = [];
|
|||||||
let composerForceInput = false;
|
let composerForceInput = false;
|
||||||
let pendingComposerDraft = "";
|
let pendingComposerDraft = "";
|
||||||
let pendingComposerDraftSeq = 0;
|
let pendingComposerDraftSeq = 0;
|
||||||
|
/** 正在提交底栏:重绘时不要把已捕获的正文再塞进 pending */
|
||||||
|
let skipComposerStash = false;
|
||||||
|
let sidebarNav = { level: "root" };
|
||||||
let bookSelectMode = false;
|
let bookSelectMode = false;
|
||||||
let selectedBookIds = new Set();
|
let selectedBookIds = new Set();
|
||||||
const expandedBookIds = new Set();
|
|
||||||
let activePlaySaveId = null;
|
let activePlaySaveId = null;
|
||||||
const playSavesByBook = new Map();
|
const playSavesByBook = new Map();
|
||||||
const playSavesLoading = new Set();
|
const playSavesLoading = new Set();
|
||||||
@@ -97,6 +99,15 @@ function resolveUserTask(view, loading) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isFlowPlanReview(view) || wr?.kind === "pick_creation_step") {
|
||||||
|
return {
|
||||||
|
id: "speak",
|
||||||
|
label: "选节点",
|
||||||
|
title: "选要做的节点",
|
||||||
|
hint: "点可进入的节点即确认并开始;要改排在底栏写意见再发。",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (wr?.kind === "review_artifact") {
|
if (wr?.kind === "review_artifact") {
|
||||||
const copy = reviewCopyFromView(view);
|
const copy = reviewCopyFromView(view);
|
||||||
return {
|
return {
|
||||||
@@ -388,6 +399,22 @@ function resolveComposer(view, loading) {
|
|||||||
const send = view.actions?.find((a) => a.type === "send_message");
|
const send = view.actions?.find((a) => a.type === "send_message");
|
||||||
const hasQuestionsCard = Boolean(getActiveQuestions(view)?.questions?.length);
|
const hasQuestionsCard = Boolean(getActiveQuestions(view)?.questions?.length);
|
||||||
|
|
||||||
|
if (
|
||||||
|
(view.waitingReason?.kind === "pick_creation_step" || isFlowPlanReview(view)) &&
|
||||||
|
!composerForceInput
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
mode: "pick_step",
|
||||||
|
task,
|
||||||
|
taskTitle: "选要做的节点",
|
||||||
|
taskHint: "点可进入的节点即确认并开始;虚线原型点一下增殖。要改排在底栏写意见再发。",
|
||||||
|
placeholder: send?.placeholder ?? "例如:补舞台骨架、把某步改到开场白之前…",
|
||||||
|
hint: null,
|
||||||
|
submitLabel: "发送",
|
||||||
|
requiresText: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (view.waitingReason?.kind === "review_artifact") {
|
if (view.waitingReason?.kind === "review_artifact") {
|
||||||
const parseBroken = Boolean(view.reviewArtifact?.workerSetView?.parseError);
|
const parseBroken = Boolean(view.reviewArtifact?.workerSetView?.parseError);
|
||||||
const copy = reviewCopyFromView(view);
|
const copy = reviewCopyFromView(view);
|
||||||
@@ -432,6 +459,7 @@ function resolveComposer(view, loading) {
|
|||||||
view.waitingReason?.kind === "worker_questions" ||
|
view.waitingReason?.kind === "worker_questions" ||
|
||||||
view.waitingReason?.kind === "revision" ||
|
view.waitingReason?.kind === "revision" ||
|
||||||
view.waitingReason?.kind === "next_intent" ||
|
view.waitingReason?.kind === "next_intent" ||
|
||||||
|
view.waitingReason?.kind === "pick_creation_step" ||
|
||||||
composerForceInput
|
composerForceInput
|
||||||
) {
|
) {
|
||||||
const wr = view.waitingReason;
|
const wr = view.waitingReason;
|
||||||
@@ -442,7 +470,7 @@ function resolveComposer(view, loading) {
|
|||||||
submitLabel = "继续";
|
submitLabel = "继续";
|
||||||
} else if (wr?.kind === "approve_step" && composerForceInput) {
|
} else if (wr?.kind === "approve_step" && composerForceInput) {
|
||||||
placeholder =
|
placeholder =
|
||||||
"例如:改成丧尸怪物规则(后台 id 也换成 zombies)…";
|
"例如:补开局 NPC、改成丧尸怪物规则(后台 id 也换成 zombies)…";
|
||||||
submitLabel = "发送意见";
|
submitLabel = "发送意见";
|
||||||
} else if (wr?.kind === "revision") {
|
} else if (wr?.kind === "revision") {
|
||||||
placeholder = "说明要改哪里…";
|
placeholder = "说明要改哪里…";
|
||||||
@@ -507,7 +535,38 @@ function composerInputShell(spec, { textareaHtml, trailing = "" } = {}) {
|
|||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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() {
|
||||||
|
if (skipComposerStash) return;
|
||||||
|
const input = $("composer-input");
|
||||||
|
if (!input) return;
|
||||||
|
const v = String(input.value ?? "");
|
||||||
|
if (v) {
|
||||||
|
pendingComposerDraft = v;
|
||||||
|
pendingComposerDraftSeq = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function keepComposerDraft(text) {
|
||||||
|
const v = String(text ?? "");
|
||||||
|
if (!v) return;
|
||||||
|
pendingComposerDraft = v;
|
||||||
|
pendingComposerDraftSeq = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function dropComposerDraft() {
|
||||||
|
pendingComposerDraft = "";
|
||||||
|
pendingComposerDraftSeq = 0;
|
||||||
|
}
|
||||||
|
|
||||||
function renderComposer(view, loading) {
|
function renderComposer(view, loading) {
|
||||||
|
stashComposerDraftFromDom();
|
||||||
if (pendingComposerDraft) composerForceInput = true;
|
if (pendingComposerDraft) composerForceInput = true;
|
||||||
const host = $("composer");
|
const host = $("composer");
|
||||||
const root = $("composer-main") || host;
|
const root = $("composer-main") || host;
|
||||||
@@ -517,6 +576,13 @@ 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">
|
root.innerHTML = `<div class="composer-waiting">
|
||||||
@@ -556,7 +622,7 @@ function renderComposer(view, loading) {
|
|||||||
|
|
||||||
if (spec.mode === "action") {
|
if (spec.mode === "action") {
|
||||||
const reject = spec.showReject
|
const reject = spec.showReject
|
||||||
? `<button type="button" class="btn" data-act="reject">${spec.primaryType === "approve" ? "暂不" : "不接受"}</button>
|
? `<button type="button" class="btn" data-act="reject">${spec.primaryType === "approve" ? "返回节点" : "不接受"}</button>
|
||||||
<button type="button" class="btn" data-act="force-input">说明意见</button>`
|
<button type="button" class="btn" data-act="force-input">说明意见</button>`
|
||||||
: "";
|
: "";
|
||||||
const chip = composerModeChip(spec);
|
const chip = composerModeChip(spec);
|
||||||
@@ -611,6 +677,9 @@ function renderComposer(view, loading) {
|
|||||||
: "具体写什么在步骤内确定。"
|
: "具体写什么在步骤内确定。"
|
||||||
}</p>`
|
}</p>`
|
||||||
: "";
|
: "";
|
||||||
|
const closerHint = p.closer
|
||||||
|
? `<p class="propose-plan-hint">这是收口步。若还没做 NPC 等前序节点,点「改意见」说明要补什么,会回到流程编排。</p>`
|
||||||
|
: "";
|
||||||
root.innerHTML = `
|
root.innerHTML = `
|
||||||
<div class="propose-step" id="propose-step" data-step-id="${esc(p.stepId)}">
|
<div class="propose-step" id="propose-step" data-step-id="${esc(p.stepId)}">
|
||||||
<div class="propose-step-head">
|
<div class="propose-step-head">
|
||||||
@@ -619,11 +688,12 @@ function renderComposer(view, loading) {
|
|||||||
</div>
|
</div>
|
||||||
<p class="propose-output" data-propose-output>${esc(proposedOutputCopy(p))}</p>
|
<p class="propose-output" data-propose-output>${esc(proposedOutputCopy(p))}</p>
|
||||||
${planHint}
|
${planHint}
|
||||||
|
${closerHint}
|
||||||
${fields ? `<div class="propose-fields">${fields}</div>` : ""}
|
${fields ? `<div class="propose-fields">${fields}</div>` : ""}
|
||||||
${syncHint}
|
${syncHint}
|
||||||
<div class="composer-actions">
|
<div class="composer-actions">
|
||||||
<button type="button" class="btn btn-primary" data-act="approve">${esc(spec.primary.label)}</button>
|
<button type="button" class="btn btn-primary" data-act="approve">${esc(spec.primary.label)}</button>
|
||||||
<button type="button" class="btn" data-act="reject">暂不</button>
|
<button type="button" class="btn" data-act="reject" title="先不写这一步,回到节点选择">返回节点</button>
|
||||||
<button type="button" class="btn" data-act="force-input">改意见</button>
|
<button type="button" class="btn" data-act="force-input">改意见</button>
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
@@ -687,6 +757,17 @@ function renderComposer(view, loading) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (spec.mode === "pick_step") {
|
||||||
|
const shell = composerInputShell(spec, {
|
||||||
|
textareaHtml: `<textarea id="composer-input" rows="1" placeholder="${esc(spec.placeholder)}"></textarea>`,
|
||||||
|
trailing: `<button type="submit" class="btn btn-primary composer-btn" data-act="pick-replan" disabled title="写下意见后再发;点图上节点即确认并进入">${esc(spec.submitLabel || "发送")}</button>`,
|
||||||
|
});
|
||||||
|
root.innerHTML = `<form class="composer-form" id="composer-form">${shell}</form>`;
|
||||||
|
wireComposerForm({ requiresText: true });
|
||||||
|
applyPendingComposerDraft();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const acceptOnEmpty = Boolean(spec.acceptAction);
|
const acceptOnEmpty = Boolean(spec.acceptAction);
|
||||||
const acceptTone = spec.acceptAction?.tone || "accept";
|
const acceptTone = spec.acceptAction?.tone || "accept";
|
||||||
const acceptBtn = spec.acceptAction
|
const acceptBtn = spec.acceptAction
|
||||||
@@ -705,7 +786,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: `<button type="button" class="btn composer-btn${
|
trailing: `${leaveStepButtonHtml(spec)}<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
|
||||||
@@ -716,6 +797,7 @@ 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();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -752,6 +834,7 @@ function applyPendingComposerDraft() {
|
|||||||
pendingComposerDraft = "";
|
pendingComposerDraft = "";
|
||||||
pendingComposerDraftSeq = 0;
|
pendingComposerDraftSeq = 0;
|
||||||
autosizeComposerInput(input);
|
autosizeComposerInput(input);
|
||||||
|
syncPickReplanButton(input);
|
||||||
input.focus();
|
input.focus();
|
||||||
refreshReviewComposerChrome();
|
refreshReviewComposerChrome();
|
||||||
}
|
}
|
||||||
@@ -804,10 +887,21 @@ function syncDualComposerPrimary(input, acceptBtn, sendBtn) {
|
|||||||
sendBtn.classList.toggle("btn-primary", revisePrimary);
|
sendBtn.classList.toggle("btn-primary", revisePrimary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function syncPickReplanButton(input) {
|
||||||
|
const btn = $("composer-form")?.querySelector("[data-act=pick-replan]");
|
||||||
|
if (!btn) return;
|
||||||
|
const hasText = Boolean(String(input?.value ?? "").trim());
|
||||||
|
btn.disabled = !hasText;
|
||||||
|
btn.title = hasText
|
||||||
|
? "发送改编排意见"
|
||||||
|
: "写下意见后再发;点图上节点即确认并进入";
|
||||||
|
}
|
||||||
|
|
||||||
function wireComposerForm(opts = {}) {
|
function wireComposerForm(opts = {}) {
|
||||||
const form = $("composer-form");
|
const form = $("composer-form");
|
||||||
const input = $("composer-input");
|
const input = $("composer-input");
|
||||||
const acceptOnEmpty = opts.acceptOnEmpty === true;
|
const acceptOnEmpty = opts.acceptOnEmpty === true;
|
||||||
|
const requiresText = opts.requiresText === true;
|
||||||
const sendBtn = form?.querySelector("[data-act=revise]");
|
const sendBtn = form?.querySelector("[data-act=revise]");
|
||||||
const root = $("composer");
|
const root = $("composer");
|
||||||
if (root?._enterHandler) {
|
if (root?._enterHandler) {
|
||||||
@@ -816,12 +910,12 @@ function wireComposerForm(opts = {}) {
|
|||||||
}
|
}
|
||||||
form?.addEventListener("submit", async (e) => {
|
form?.addEventListener("submit", async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
await submitComposer(input?.value ?? "", { acceptOnEmpty });
|
await submitComposer(input?.value ?? "", { acceptOnEmpty, requiresText });
|
||||||
});
|
});
|
||||||
input?.addEventListener("keydown", (e) => {
|
input?.addEventListener("keydown", (e) => {
|
||||||
if (e.key === "Enter" && !e.shiftKey && !e.isComposing) {
|
if (e.key === "Enter" && !e.shiftKey && !e.isComposing) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
void submitComposer(input?.value ?? "", { acceptOnEmpty });
|
void submitComposer(input?.value ?? "", { acceptOnEmpty, requiresText });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
sendBtn?.addEventListener("click", () => {
|
sendBtn?.addEventListener("click", () => {
|
||||||
@@ -846,6 +940,7 @@ function wireComposerForm(opts = {}) {
|
|||||||
const grow = () => {
|
const grow = () => {
|
||||||
autosizeComposerInput(input);
|
autosizeComposerInput(input);
|
||||||
if (acceptOnEmpty) refreshReviewComposerChrome();
|
if (acceptOnEmpty) refreshReviewComposerChrome();
|
||||||
|
if (requiresText) syncPickReplanButton(input);
|
||||||
};
|
};
|
||||||
input?.addEventListener("input", grow);
|
input?.addEventListener("input", grow);
|
||||||
input?.addEventListener("change", grow);
|
input?.addEventListener("change", grow);
|
||||||
@@ -868,12 +963,6 @@ function bookPlayReady(bookId) {
|
|||||||
return saves.some((s) => s.kind === "instance" || s.kind === "run");
|
return saves.some((s) => s.kind === "instance" || s.kind === "run");
|
||||||
}
|
}
|
||||||
|
|
||||||
function bookHasPlayBranch(bookId) {
|
|
||||||
if (bookPlayReady(bookId)) return true;
|
|
||||||
if (playSavesLoading.has(bookId) && !playSavesByBook.has(bookId)) return true;
|
|
||||||
return playRunsForBook(bookId).length > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatSaveWhen(createdAt) {
|
function formatSaveWhen(createdAt) {
|
||||||
const when = new Date(createdAt);
|
const when = new Date(createdAt);
|
||||||
if (Number.isNaN(when.getTime())) return "";
|
if (Number.isNaN(when.getTime())) return "";
|
||||||
@@ -900,6 +989,7 @@ async function fetchPlaySavesForBook(bookId, force = false) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setBookSelectMode(on, { keepSelection = false } = {}) {
|
function setBookSelectMode(on, { keepSelection = false } = {}) {
|
||||||
|
if (on && sidebarNav.level === "book") return;
|
||||||
bookSelectMode = on;
|
bookSelectMode = on;
|
||||||
if (!on) selectedBookIds = new Set();
|
if (!on) selectedBookIds = new Set();
|
||||||
else if (!keepSelection) selectedBookIds = new Set();
|
else if (!keepSelection) selectedBookIds = new Set();
|
||||||
@@ -962,8 +1052,50 @@ function showBookMenu(bookId, x, y) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function expandBook(bookId) {
|
function isCatalogNav() {
|
||||||
if (bookId) expandedBookIds.add(bookId);
|
return sidebarNav.level !== "book";
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNavBook() {
|
||||||
|
if (sidebarNav.level !== "book") return null;
|
||||||
|
return books.find((b) => b.id === sidebarNav.bookId) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncRailContext() {
|
||||||
|
const el = $("rail-context");
|
||||||
|
if (!el) return;
|
||||||
|
const book = getNavBook();
|
||||||
|
if (sidebarNav.level === "book" && book) {
|
||||||
|
el.textContent = book.title;
|
||||||
|
el.title = book.title;
|
||||||
|
} else {
|
||||||
|
el.textContent = "作品";
|
||||||
|
el.removeAttribute("title");
|
||||||
|
}
|
||||||
|
$("rail-tabs")?.toggleAttribute("hidden", sidebarNav.level !== "book");
|
||||||
|
}
|
||||||
|
|
||||||
|
function navigateToBook(bookId) {
|
||||||
|
if (!bookId) return;
|
||||||
|
sidebarNav = { level: "book", bookId };
|
||||||
|
document.body.dataset.navLevel = "book";
|
||||||
|
bookSelectMode = false;
|
||||||
|
selectedBookIds = new Set();
|
||||||
|
syncRailContext();
|
||||||
|
renderBookList();
|
||||||
|
void fetchPlaySavesForBook(bookId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showCatalog() {
|
||||||
|
beginUiRequest();
|
||||||
|
stopLivePoll();
|
||||||
|
sidebarNav = { level: "root" };
|
||||||
|
document.body.dataset.navLevel = "root";
|
||||||
|
bookSelectMode = false;
|
||||||
|
selectedBookIds = new Set();
|
||||||
|
closeWorkspace();
|
||||||
|
syncRailContext();
|
||||||
|
renderBookList();
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderExplorerRow({
|
function renderExplorerRow({
|
||||||
@@ -1025,7 +1157,7 @@ function renderExplorerRow({
|
|||||||
return row;
|
return row;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderBookBranch(book, list) {
|
function renderBookBranch(book, list, { nested = false } = {}) {
|
||||||
const loading = playSavesLoading.has(book.id);
|
const loading = playSavesLoading.has(book.id);
|
||||||
const saves = playSavesByBook.get(book.id);
|
const saves = playSavesByBook.get(book.id);
|
||||||
const ready = bookPlayReady(book.id);
|
const ready = bookPlayReady(book.id);
|
||||||
@@ -1045,7 +1177,7 @@ function renderBookBranch(book, list) {
|
|||||||
name: "当前",
|
name: "当前",
|
||||||
meta: playHere && !activePlaySaveId ? "进行中" : "工作副本",
|
meta: playHere && !activePlaySaveId ? "进行中" : "工作副本",
|
||||||
active: playHere && !activePlaySaveId,
|
active: playHere && !activePlaySaveId,
|
||||||
child: true,
|
child: nested,
|
||||||
onClick: () => void openCurrentPlay(book.id),
|
onClick: () => void openCurrentPlay(book.id),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -1058,7 +1190,7 @@ function renderBookBranch(book, list) {
|
|||||||
name: s.label,
|
name: s.label,
|
||||||
meta: when ? `游玩 · ${when}` : "游玩进度",
|
meta: when ? `游玩 · ${when}` : "游玩进度",
|
||||||
active: playHere && activePlaySaveId === s.id,
|
active: playHere && activePlaySaveId === s.id,
|
||||||
child: true,
|
child: nested,
|
||||||
actionClass: "explorer-file",
|
actionClass: "explorer-file",
|
||||||
onClick: () => void loadPlaySave(book.id, s.id),
|
onClick: () => void loadPlaySave(book.id, s.id),
|
||||||
onDelete: () => void deletePlaySave(book.id, s.id, s.label),
|
onDelete: () => void deletePlaySave(book.id, s.id, s.label),
|
||||||
@@ -1071,16 +1203,30 @@ function renderBookBranch(book, list) {
|
|||||||
renderExplorerRow({
|
renderExplorerRow({
|
||||||
name: "+ 新开一局",
|
name: "+ 新开一局",
|
||||||
actionClass: "explorer-action",
|
actionClass: "explorer-action",
|
||||||
child: true,
|
child: nested,
|
||||||
onClick: () => void startNewPlayForBook(book.id),
|
onClick: () => void startNewPlayForBook(book.id),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderBookContents(book, list) {
|
||||||
|
const skill = book.activeSkillName ?? book.activeSkillId ?? book.orchestratorName ?? "实例设计";
|
||||||
|
list.appendChild(
|
||||||
|
renderExplorerRow({
|
||||||
|
name: "创作",
|
||||||
|
meta: skill,
|
||||||
|
active: book.id === activeBookId && !isPlayNow(),
|
||||||
|
onClick: () => void openBookDesign(book.id),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
renderBookBranch(book, list, { nested: false });
|
||||||
|
}
|
||||||
|
|
||||||
function renderBookList() {
|
function renderBookList() {
|
||||||
const list = $("book-list");
|
const list = $("book-list");
|
||||||
if (!list) return;
|
if (!list) return;
|
||||||
|
syncRailContext();
|
||||||
if (!books.length) {
|
if (!books.length) {
|
||||||
list.innerHTML = `<p class="sidebar-empty">暂无作品<br><button type="button" class="btn-sm" id="btn-new-inline">+ 新建</button></p>`;
|
list.innerHTML = `<p class="sidebar-empty">暂无作品<br><button type="button" class="btn-sm" id="btn-new-inline">+ 新建</button></p>`;
|
||||||
$("btn-new-inline")?.addEventListener("click", openNewBookDialog);
|
$("btn-new-inline")?.addEventListener("click", openNewBookDialog);
|
||||||
@@ -1088,39 +1234,36 @@ function renderBookList() {
|
|||||||
}
|
}
|
||||||
list.innerHTML = "";
|
list.innerHTML = "";
|
||||||
|
|
||||||
|
if (sidebarNav.level === "book") {
|
||||||
|
const book = getNavBook();
|
||||||
|
if (book) {
|
||||||
|
renderBookContents(book, list);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sidebarNav = { level: "root" };
|
||||||
|
document.body.dataset.navLevel = "root";
|
||||||
|
syncRailContext();
|
||||||
|
}
|
||||||
|
|
||||||
for (const book of books) {
|
for (const book of books) {
|
||||||
const skill = book.activeSkillName ?? book.activeSkillId ?? book.orchestratorName ?? "实例设计";
|
const skill = book.activeSkillName ?? book.activeSkillId ?? book.orchestratorName ?? "实例设计";
|
||||||
const selected = selectedBookIds.has(book.id);
|
const selected = selectedBookIds.has(book.id);
|
||||||
const expanded = expandedBookIds.has(book.id);
|
|
||||||
const hasBranch = bookHasPlayBranch(book.id);
|
|
||||||
const bookActive = book.id === activeBookId && !isPlayNow() && !selected;
|
|
||||||
list.appendChild(
|
list.appendChild(
|
||||||
renderExplorerRow({
|
renderExplorerRow({
|
||||||
name: book.title,
|
name: book.title,
|
||||||
meta: skill,
|
meta: skill,
|
||||||
active: bookActive,
|
|
||||||
selected,
|
selected,
|
||||||
bookId: book.id,
|
bookId: book.id,
|
||||||
actionClass: "explorer-folder",
|
actionClass: "explorer-folder",
|
||||||
twistie: hasBranch,
|
|
||||||
expanded,
|
|
||||||
onTwistie: () => {
|
|
||||||
if (expandedBookIds.has(book.id)) expandedBookIds.delete(book.id);
|
|
||||||
else expandedBookIds.add(book.id);
|
|
||||||
renderBookList();
|
|
||||||
if (!expandedBookIds.has(book.id)) return;
|
|
||||||
void fetchPlaySavesForBook(book.id);
|
|
||||||
},
|
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
if (bookSelectMode) {
|
if (bookSelectMode) {
|
||||||
toggleBookSelection(book.id);
|
toggleBookSelection(book.id);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
void openBookDesign(book.id);
|
void openBook(book.id);
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
if (expanded && hasBranch) renderBookBranch(book, list);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1200,7 +1343,7 @@ async function deleteSelectedBooks() {
|
|||||||
books = books.filter((b) => !deleted.has(b.id));
|
books = books.filter((b) => !deleted.has(b.id));
|
||||||
setBookSelectMode(false);
|
setBookSelectMode(false);
|
||||||
if (activeBookId && deleted.has(activeBookId)) {
|
if (activeBookId && deleted.has(activeBookId)) {
|
||||||
if (books.length) await openBook(books[0].id);
|
if (books.length) showCatalog();
|
||||||
else renderEmpty();
|
else renderEmpty();
|
||||||
} else {
|
} else {
|
||||||
renderBookList();
|
renderBookList();
|
||||||
@@ -1229,9 +1372,9 @@ function setupBookBoxSelect() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
list.addEventListener("mousedown", (e) => {
|
list.addEventListener("mousedown", (e) => {
|
||||||
if (e.button !== 0) return;
|
if (sidebarNav.level !== "root" || e.button !== 0) return;
|
||||||
if (e.target.closest("#book-action-menu")) return;
|
if (e.target.closest("#book-action-menu")) return;
|
||||||
if (e.target.closest(".explorer-row.child")) return;
|
if (e.target.closest(".explorer-row")) return;
|
||||||
dragging = true;
|
dragging = true;
|
||||||
start = { x: e.clientX, y: e.clientY };
|
start = { x: e.clientX, y: e.clientY };
|
||||||
overlay.hidden = true;
|
overlay.hidden = true;
|
||||||
@@ -1282,8 +1425,8 @@ function setupBookBoxSelect() {
|
|||||||
|
|
||||||
async function loadPlaySave(bookId, saveId) {
|
async function loadPlaySave(bookId, saveId) {
|
||||||
try {
|
try {
|
||||||
expandBook(bookId);
|
navigateToBook(bookId);
|
||||||
if (lastView) renderSession(lastView, true);
|
if (lastView?.bookId === bookId) renderSession(lastView, true);
|
||||||
const data = await api(
|
const data = await api(
|
||||||
`/api/books/${encodeURIComponent(bookId)}/saves/${encodeURIComponent(saveId)}/load`,
|
`/api/books/${encodeURIComponent(bookId)}/saves/${encodeURIComponent(saveId)}/load`,
|
||||||
{ method: "POST" },
|
{ method: "POST" },
|
||||||
@@ -1316,8 +1459,8 @@ async function startNewPlayForBook(bookId) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
expandBook(bookId);
|
navigateToBook(bookId);
|
||||||
if (lastView) renderSession(lastView, true);
|
if (lastView?.bookId === bookId) renderSession(lastView, true);
|
||||||
const data = await api(`/api/books/${encodeURIComponent(bookId)}/play/new`, { method: "POST" });
|
const data = await api(`/api/books/${encodeURIComponent(bookId)}/play/new`, { method: "POST" });
|
||||||
activePlaySaveId = null;
|
activePlaySaveId = null;
|
||||||
renderSession(data.session, false);
|
renderSession(data.session, false);
|
||||||
@@ -1328,7 +1471,7 @@ async function startNewPlayForBook(bookId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function openBookDesign(bookId) {
|
async function openBookDesign(bookId) {
|
||||||
expandBook(bookId);
|
navigateToBook(bookId);
|
||||||
activePlaySaveId = null;
|
activePlaySaveId = null;
|
||||||
if (activeBookId !== bookId || !sessionId) {
|
if (activeBookId !== bookId || !sessionId) {
|
||||||
await openBook(bookId);
|
await openBook(bookId);
|
||||||
@@ -1341,7 +1484,7 @@ async function openBookDesign(bookId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function openCurrentPlay(bookId) {
|
async function openCurrentPlay(bookId) {
|
||||||
expandBook(bookId);
|
navigateToBook(bookId);
|
||||||
const alreadyHere =
|
const alreadyHere =
|
||||||
activeBookId === bookId &&
|
activeBookId === bookId &&
|
||||||
isPlayNow() &&
|
isPlayNow() &&
|
||||||
@@ -1390,12 +1533,12 @@ function renderHeader(view, loading) {
|
|||||||
if (btnExport) btnExport.disabled = !(view.messages?.length);
|
if (btnExport) btnExport.disabled = !(view.messages?.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderEmpty() {
|
function closeWorkspace({ emptyCopy = "" } = {}) {
|
||||||
|
stopLivePoll();
|
||||||
sessionId = null;
|
sessionId = null;
|
||||||
lastView = null;
|
lastView = null;
|
||||||
activeBookId = null;
|
activeBookId = null;
|
||||||
activePlaySaveId = null;
|
activePlaySaveId = null;
|
||||||
setBookSelectMode(false);
|
|
||||||
composerForceInput = false;
|
composerForceInput = false;
|
||||||
document.title = "Writing Agent";
|
document.title = "Writing Agent";
|
||||||
$("status-dot").className = "status-dot";
|
$("status-dot").className = "status-dot";
|
||||||
@@ -1406,7 +1549,7 @@ function renderEmpty() {
|
|||||||
if (btnInst) btnInst.hidden = true;
|
if (btnInst) btnInst.hidden = true;
|
||||||
const btnExport = $("btn-export");
|
const btnExport = $("btn-export");
|
||||||
if (btnExport) btnExport.disabled = true;
|
if (btnExport) btnExport.disabled = true;
|
||||||
$("message-feed").innerHTML = `<p class="empty">点击左侧 + 新建作品</p>`;
|
$("message-feed").innerHTML = emptyCopy ? `<p class="empty">${emptyCopy}</p>` : "";
|
||||||
const stage = $("workspace-stage");
|
const stage = $("workspace-stage");
|
||||||
if (stage) {
|
if (stage) {
|
||||||
stage.hidden = true;
|
stage.hidden = true;
|
||||||
@@ -1439,6 +1582,14 @@ function renderEmpty() {
|
|||||||
document.body.dataset.lifecycle = "design";
|
document.body.dataset.lifecycle = "design";
|
||||||
const composerMain = $("composer-main");
|
const composerMain = $("composer-main");
|
||||||
if (composerMain) composerMain.innerHTML = `<div class="composer-idle">暂无打开的作品</div>`;
|
if (composerMain) composerMain.innerHTML = `<div class="composer-idle">暂无打开的作品</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderEmpty() {
|
||||||
|
sidebarNav = { level: "root" };
|
||||||
|
document.body.dataset.navLevel = "root";
|
||||||
|
bookSelectMode = false;
|
||||||
|
selectedBookIds = new Set();
|
||||||
|
closeWorkspace({ emptyCopy: "点击左侧 + 新建作品" });
|
||||||
renderBookList();
|
renderBookList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1490,12 +1641,14 @@ function startLivePoll() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderSession(view, loading = false) {
|
function renderSession(view, loading = false) {
|
||||||
|
if (isCatalogNav()) return;
|
||||||
lastView = view;
|
lastView = view;
|
||||||
sessionId = view.id;
|
sessionId = view.id;
|
||||||
activeBookId = view.bookId ?? activeBookId;
|
activeBookId = view.bookId ?? activeBookId;
|
||||||
if (
|
if (
|
||||||
!loading &&
|
!loading &&
|
||||||
view.waitingReason?.kind !== "approve_step" &&
|
view.waitingReason?.kind !== "approve_step" &&
|
||||||
|
view.waitingReason?.kind !== "pick_creation_step" &&
|
||||||
view.waitingReason?.kind !== "review_artifact" &&
|
view.waitingReason?.kind !== "review_artifact" &&
|
||||||
view.waitingReason?.kind !== "worker_questions" &&
|
view.waitingReason?.kind !== "worker_questions" &&
|
||||||
!(view.waitingReason?.kind === "input" && view.waitingReason?.questions?.length)
|
!(view.waitingReason?.kind === "input" && view.waitingReason?.questions?.length)
|
||||||
@@ -1524,6 +1677,12 @@ function renderSession(view, loading = false) {
|
|||||||
onSwitchVariant: (messageId, direction) =>
|
onSwitchVariant: (messageId, direction) =>
|
||||||
messageAction("variant", messageId, { direction }),
|
messageAction("variant", messageId, { direction }),
|
||||||
onRetryRun: () => retryRun(),
|
onRetryRun: () => retryRun(),
|
||||||
|
onPickCreationStep: (stepId) => runAction("pick_step", { stepId }),
|
||||||
|
onReenterCreationStep: (stepId) =>
|
||||||
|
runAction("pick_step", { stepId, reenter: true }),
|
||||||
|
onViewStepArtifact: (stepId, name) => showStepArtifactDialog(stepId, name),
|
||||||
|
onSpawnCreationStep: (moduleName) => runAction("spawn_step", { moduleName }),
|
||||||
|
onLeaveCreationStep: () => runAction("leave_step"),
|
||||||
});
|
});
|
||||||
renderComposer(view, loading);
|
renderComposer(view, loading);
|
||||||
if (loading) startLivePoll();
|
if (loading) startLivePoll();
|
||||||
@@ -1573,8 +1732,12 @@ function selectedQuestionAnswers(view) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 空 Enter=接受;有字或已选追问则按意见改。点「按意见修改」不走这条。 */
|
/** 空 Enter=接受;有字或已选追问则按意见改。点「按意见修改」不走这条。 */
|
||||||
async function submitComposer(text, { acceptOnEmpty } = {}) {
|
async function submitComposer(text, { acceptOnEmpty, requiresText } = {}) {
|
||||||
const trimmed = String(text ?? "").trim();
|
const trimmed = String(text ?? "").trim();
|
||||||
|
if (requiresText && !trimmed) {
|
||||||
|
alert("点图上节点即确认并进入;要改编排请先写下意见。");
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (acceptOnEmpty && !trimmed) {
|
if (acceptOnEmpty && !trimmed) {
|
||||||
const { answered } = selectedQuestionAnswers(lastView);
|
const { answered } = selectedQuestionAnswers(lastView);
|
||||||
if (answered.length === 0) {
|
if (answered.length === 0) {
|
||||||
@@ -1609,7 +1772,11 @@ async function sendText(text) {
|
|||||||
if (reviewing) {
|
if (reviewing) {
|
||||||
const { collected, answered } = selectedQuestionAnswers(lastView);
|
const { collected, answered } = selectedQuestionAnswers(lastView);
|
||||||
if (!trimmed && answered.length === 0) {
|
if (!trimmed && answered.length === 0) {
|
||||||
alert("请选择追问选项或填写修改意见;满意请点「接受」收下产物。");
|
alert(
|
||||||
|
isFlowPlanReview(lastView)
|
||||||
|
? "点图上节点即确认并进入;要改编排请先写下意见。"
|
||||||
|
: "请选择追问选项或填写修改意见;满意请点「接受」收下产物。",
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -1681,6 +1848,13 @@ async function sendText(text) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!trimmed) {
|
if (!trimmed) {
|
||||||
|
if (
|
||||||
|
lastView?.waitingReason?.kind === "pick_creation_step" ||
|
||||||
|
isFlowPlanReview(lastView)
|
||||||
|
) {
|
||||||
|
alert("点图上节点即确认并进入;要改编排请先写下意见。");
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
lastView?.waitingReason?.kind === "next_intent" ||
|
lastView?.waitingReason?.kind === "next_intent" ||
|
||||||
resolveComposer(lastView, false)?.allowEmpty
|
resolveComposer(lastView, false)?.allowEmpty
|
||||||
@@ -1789,7 +1963,6 @@ async function loadBooks() {
|
|||||||
const data = await api("/api/books");
|
const data = await api("/api/books");
|
||||||
books = data.books ?? [];
|
books = data.books ?? [];
|
||||||
renderBookList();
|
renderBookList();
|
||||||
for (const book of books) void fetchPlaySavesForBook(book.id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function openNewBookDialog() {
|
function openNewBookDialog() {
|
||||||
@@ -1865,7 +2038,7 @@ async function createBook() {
|
|||||||
$("dialog-new-book").close();
|
$("dialog-new-book").close();
|
||||||
if (data.book) books.unshift(data.book);
|
if (data.book) books.unshift(data.book);
|
||||||
activeBookId = data.book?.id;
|
activeBookId = data.book?.id;
|
||||||
expandBook(data.book.id);
|
navigateToBook(data.book.id);
|
||||||
renderSession(data.session, false);
|
renderSession(data.session, false);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert(err.message);
|
alert(err.message);
|
||||||
@@ -1875,19 +2048,22 @@ async function createBook() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function openBook(bookId) {
|
async function openBook(bookId) {
|
||||||
|
const seq = beginUiRequest();
|
||||||
activeBookId = bookId;
|
activeBookId = bookId;
|
||||||
expandBook(bookId);
|
navigateToBook(bookId);
|
||||||
renderBookList();
|
if (lastView?.bookId === bookId) renderSession(lastView, true);
|
||||||
if (lastView) renderSession(lastView, true);
|
|
||||||
try {
|
try {
|
||||||
const data = await api(`/api/books/${encodeURIComponent(bookId)}/open`, { method: "POST" });
|
const data = await api(`/api/books/${encodeURIComponent(bookId)}/open`, { method: "POST" });
|
||||||
|
if (isStaleUiRequest(seq)) return;
|
||||||
if (data.book) {
|
if (data.book) {
|
||||||
const i = books.findIndex((b) => b.id === bookId);
|
const i = books.findIndex((b) => b.id === bookId);
|
||||||
if (i >= 0) books[i] = { ...books[i], ...data.book };
|
if (i >= 0) books[i] = { ...books[i], ...data.book };
|
||||||
}
|
}
|
||||||
renderSession(data.session, false);
|
renderSession(data.session, false);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (lastView) renderSession({ ...lastView, hints: [err.message] }, false);
|
if (isStaleUiRequest(seq)) return;
|
||||||
|
if (lastView?.bookId === bookId) renderSession({ ...lastView, hints: [err.message] }, false);
|
||||||
|
else alert(err.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1899,16 +2075,13 @@ async function deleteBookById(bookId) {
|
|||||||
await api(`/api/books/${encodeURIComponent(bookId)}`, { method: "DELETE" });
|
await api(`/api/books/${encodeURIComponent(bookId)}`, { method: "DELETE" });
|
||||||
books = books.filter((b) => b.id !== bookId);
|
books = books.filter((b) => b.id !== bookId);
|
||||||
playSavesByBook.delete(bookId);
|
playSavesByBook.delete(bookId);
|
||||||
expandedBookIds.delete(bookId);
|
|
||||||
selectedBookIds.delete(bookId);
|
selectedBookIds.delete(bookId);
|
||||||
if (activeBookId === bookId) activePlaySaveId = null;
|
if (activeBookId === bookId) activePlaySaveId = null;
|
||||||
hideBookMenu();
|
hideBookMenu();
|
||||||
if (activeBookId === bookId) {
|
const wasHere = activeBookId === bookId || sidebarNav.bookId === bookId;
|
||||||
if (books.length) {
|
if (wasHere) {
|
||||||
await openBook(books[0].id);
|
if (books.length) showCatalog();
|
||||||
} else {
|
else renderEmpty();
|
||||||
renderEmpty();
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
renderBookList();
|
renderBookList();
|
||||||
}
|
}
|
||||||
@@ -1952,6 +2125,21 @@ async function saveCurrentInstance() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function showStepArtifactDialog(stepId, name) {
|
||||||
|
if (!sessionId || !stepId) return;
|
||||||
|
try {
|
||||||
|
const data = await api(
|
||||||
|
`/api/sessions/${encodeURIComponent(sessionId)}/step-artifact?stepId=${encodeURIComponent(stepId)}`,
|
||||||
|
);
|
||||||
|
fillStepArtifactDialog({
|
||||||
|
name: data.name || name,
|
||||||
|
content: data.content,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
alert(err.message || "无法读取产物");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function showContextTraceDialog(messageId) {
|
function showContextTraceDialog(messageId) {
|
||||||
const msg = (lastView?.messages ?? []).find((m) => m.id === messageId);
|
const msg = (lastView?.messages ?? []).find((m) => m.id === messageId);
|
||||||
const review = lastView?.reviewArtifact;
|
const review = lastView?.reviewArtifact;
|
||||||
@@ -2102,7 +2290,13 @@ $("btn-save-instance")?.addEventListener("click", () => {
|
|||||||
|
|
||||||
window.addEventListener("wa:session-updated", (e) => {
|
window.addEventListener("wa:session-updated", (e) => {
|
||||||
const view = e.detail;
|
const view = e.detail;
|
||||||
if (view?.id) renderSession(view, false);
|
if (!view?.id || isCatalogNav() || !sessionId) return;
|
||||||
|
if (view.id !== sessionId) return;
|
||||||
|
renderSession(view, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener("wa:nav-root", () => {
|
||||||
|
showCatalog();
|
||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener("click", (e) => {
|
document.addEventListener("click", (e) => {
|
||||||
@@ -2116,7 +2310,7 @@ async function init() {
|
|||||||
void populateDirectorSelect();
|
void populateDirectorSelect();
|
||||||
await loadBooks();
|
await loadBooks();
|
||||||
if (books.length) {
|
if (books.length) {
|
||||||
await openBook(books[0].id);
|
showCatalog();
|
||||||
} else {
|
} else {
|
||||||
renderEmpty();
|
renderEmpty();
|
||||||
openNewBookDialog();
|
openNewBookDialog();
|
||||||
|
|||||||
+10
-2
@@ -94,6 +94,14 @@ export function formatWorkerDisplayTitle(workerId, action = null) {
|
|||||||
return base;
|
return base;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 流程编排产物待确认:图上点节点即确认,不走验收双按钮 */
|
||||||
|
export function isFlowPlanReview(view) {
|
||||||
|
return (
|
||||||
|
view?.waitingReason?.kind === "review_artifact" &&
|
||||||
|
view?.reviewArtifact?.workerId === "design-flow"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* `review_artifact` 底栏 / 工作面文案。流程编排核对工作流计划,不是「验收通过」。
|
* `review_artifact` 底栏 / 工作面文案。流程编排核对工作流计划,不是「验收通过」。
|
||||||
* 须与 src/server/display-labels.ts `reviewComposerCopy` 保持一致。
|
* 须与 src/server/display-labels.ts `reviewComposerCopy` 保持一致。
|
||||||
@@ -131,8 +139,8 @@ export function reviewComposerCopy(workerId, opts = {}) {
|
|||||||
taskLabel: "开场",
|
taskLabel: "开场",
|
||||||
taskTitle: "选定开场白",
|
taskTitle: "选定开场白",
|
||||||
hint: hasQuestions
|
hint: hasQuestions
|
||||||
? "同意就选定此开场;要改就写意见。下方追问可选答。"
|
? "同意就选定此开场;要改开场白就写意见。下方追问可选答。"
|
||||||
: "同意就选定此开场;要改就写意见。",
|
: "同意就选定此开场;要改开场白就写意见。",
|
||||||
submitLabel: "按意见修改",
|
submitLabel: "按意见修改",
|
||||||
acceptLabel: "选定此开场",
|
acceptLabel: "选定此开场",
|
||||||
placeholder: "修改意见…",
|
placeholder: "修改意见…",
|
||||||
|
|||||||
+28
-12
@@ -12,25 +12,28 @@
|
|||||||
/>
|
/>
|
||||||
<link rel="stylesheet" href="/styles.css" />
|
<link rel="stylesheet" href="/styles.css" />
|
||||||
</head>
|
</head>
|
||||||
<body data-lifecycle="design">
|
<body data-lifecycle="design" data-nav-level="root">
|
||||||
<div class="layout">
|
<div class="layout">
|
||||||
<aside class="panel panel-rail" id="panel-books">
|
<aside class="panel panel-rail" id="panel-books">
|
||||||
<header class="panel-head rail-head">
|
<header class="panel-head rail-head">
|
||||||
<button type="button" class="rail-toggle btn-sm" id="btn-rail-toggle" title="展开 / 收起侧栏" aria-label="展开或收起侧栏">‹</button>
|
<div class="rail-head-row">
|
||||||
<nav class="rail-tabs" id="rail-tabs" role="tablist" aria-label="侧栏">
|
<button type="button" class="rail-toggle btn-sm" id="btn-rail-toggle" title="返回作品目录" aria-label="返回作品目录">‹</button>
|
||||||
<button type="button" class="rail-tab active" data-rail-tab="books" role="tab" aria-selected="true" title="作品">
|
<h1 class="rail-context" id="rail-context">作品</h1>
|
||||||
|
<div class="panel-head-actions rail-head-actions">
|
||||||
|
<button type="button" class="btn-sm" id="btn-new-book" title="新建">+</button>
|
||||||
|
<span class="burst-badge" id="burst-badge" hidden></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<nav class="rail-tabs" id="rail-tabs" role="tablist" aria-label="本书侧栏" hidden>
|
||||||
|
<button type="button" class="rail-tab active" data-rail-tab="books" role="tab" aria-selected="true" title="本书">
|
||||||
<span class="rail-tab-icon" aria-hidden="true">书</span>
|
<span class="rail-tab-icon" aria-hidden="true">书</span>
|
||||||
<span class="rail-tab-label">作品</span>
|
<span class="rail-tab-label">本书</span>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="rail-tab" data-rail-tab="log" role="tab" aria-selected="false" title="调度">
|
<button type="button" class="rail-tab" data-rail-tab="log" role="tab" aria-selected="false" title="调度">
|
||||||
<span class="rail-tab-icon" aria-hidden="true">志</span>
|
<span class="rail-tab-icon" aria-hidden="true">志</span>
|
||||||
<span class="rail-tab-label">调度</span>
|
<span class="rail-tab-label">调度</span>
|
||||||
</button>
|
</button>
|
||||||
</nav>
|
</nav>
|
||||||
<div class="panel-head-actions rail-head-actions">
|
|
||||||
<button type="button" class="btn-sm" id="btn-new-book" title="新建">+</button>
|
|
||||||
<span class="burst-badge" id="burst-badge" hidden></span>
|
|
||||||
</div>
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="rail-panels">
|
<div class="rail-panels">
|
||||||
@@ -50,9 +53,7 @@
|
|||||||
<div class="main-body">
|
<div class="main-body">
|
||||||
<section class="panel-feed" id="panel-feed">
|
<section class="panel-feed" id="panel-feed">
|
||||||
<div id="skill-picker" class="skill-picker" hidden></div>
|
<div id="skill-picker" class="skill-picker" hidden></div>
|
||||||
<div id="message-feed" class="message-feed">
|
<div id="message-feed" class="message-feed"></div>
|
||||||
<p class="empty">正在打开作品…</p>
|
|
||||||
</div>
|
|
||||||
<div id="workspace-stage" class="workspace-stage" hidden aria-label="验收工作面"></div>
|
<div id="workspace-stage" class="workspace-stage" hidden aria-label="验收工作面"></div>
|
||||||
<div id="questions-card-host" class="questions-card-host" hidden></div>
|
<div id="questions-card-host" class="questions-card-host" hidden></div>
|
||||||
</section>
|
</section>
|
||||||
@@ -138,6 +139,21 @@
|
|||||||
<button type="button" class="context-menu-item context-menu-danger" id="msg-menu-delete" data-msg-menu-action="delete">从这里重开</button>
|
<button type="button" class="context-menu-item context-menu-danger" id="msg-menu-delete" data-msg-menu-action="delete">从这里重开</button>
|
||||||
</menu>
|
</menu>
|
||||||
|
|
||||||
|
<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-reenter" data-flow-menu-action="reenter">重新进入修改</button>
|
||||||
|
</menu>
|
||||||
|
|
||||||
|
<dialog id="dialog-step-artifact" class="dialog dialog-wide" aria-labelledby="step-artifact-title">
|
||||||
|
<form method="dialog" class="context-trace-form">
|
||||||
|
<h2 id="step-artifact-title">产物</h2>
|
||||||
|
<div id="step-artifact-body" class="step-artifact-body"></div>
|
||||||
|
<div class="dialog-actions">
|
||||||
|
<button type="submit" class="btn btn-primary" value="close">关闭</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
|
|
||||||
<dialog id="dialog-context-trace" class="dialog dialog-wide" aria-labelledby="context-trace-title">
|
<dialog id="dialog-context-trace" class="dialog dialog-wide" aria-labelledby="context-trace-title">
|
||||||
<form method="dialog" class="context-trace-form">
|
<form method="dialog" class="context-trace-form">
|
||||||
<h2 id="context-trace-title">上下文调用日志</h2>
|
<h2 id="context-trace-title">上下文调用日志</h2>
|
||||||
|
|||||||
+178
@@ -0,0 +1,178 @@
|
|||||||
|
/** 剥 markdown 代码围栏,便于从验收正文里取出 JSON */
|
||||||
|
export function stripCodeFences(text) {
|
||||||
|
let t = String(text || "").trim();
|
||||||
|
t = t.replace(/^```(?:json|yaml|yml)?\s*\r?\n?/i, "");
|
||||||
|
t = t.replace(/\r?\n?```\s*$/i, "");
|
||||||
|
const fenced = t.match(/```(?:json|yaml|yml)?\s*\r?\n([\s\S]*?)\r?\n```/i);
|
||||||
|
if (fenced) t = fenced[1].trim();
|
||||||
|
return t.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 字符串字面量里的裸换行/制表 → 转义(模型常犯) */
|
||||||
|
export function escapeRawControlsInJsonStrings(s) {
|
||||||
|
let out = "";
|
||||||
|
let inString = false;
|
||||||
|
let escaped = false;
|
||||||
|
for (let i = 0; i < s.length; i++) {
|
||||||
|
const c = s[i];
|
||||||
|
if (inString) {
|
||||||
|
if (escaped) {
|
||||||
|
out += c;
|
||||||
|
escaped = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (c === "\\") {
|
||||||
|
out += c;
|
||||||
|
escaped = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (c === '"') {
|
||||||
|
out += c;
|
||||||
|
inString = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (c === "\n") {
|
||||||
|
out += "\\n";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (c === "\r") {
|
||||||
|
out += "\\r";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (c === "\t") {
|
||||||
|
out += "\\t";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
out += c;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (c === '"') {
|
||||||
|
inString = true;
|
||||||
|
out += c;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
out += c;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 只把「像 JSON 定界符」的弯引号换成 ASCII。
|
||||||
|
* 字符串内部的 “对话/强调” 必须保留,否则会把合法 JSON 撕开。
|
||||||
|
*/
|
||||||
|
export function convertStructuralSmartQuotes(s) {
|
||||||
|
const text = String(s || "");
|
||||||
|
let out = "";
|
||||||
|
for (let i = 0; i < text.length; i++) {
|
||||||
|
const c = text[i];
|
||||||
|
const isOpen = c === "\u201C" || c === "\u201E" || c === "\u201F";
|
||||||
|
const isClose = c === "\u201D" || c === "\u2033" || c === "\u2036";
|
||||||
|
if (!isOpen && !isClose) {
|
||||||
|
out += c;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let prev = i - 1;
|
||||||
|
while (prev >= 0 && /\s/.test(text[prev])) prev -= 1;
|
||||||
|
let next = i + 1;
|
||||||
|
while (next < text.length && /\s/.test(text[next])) next += 1;
|
||||||
|
const prevCh = prev >= 0 ? text[prev] : "";
|
||||||
|
const nextCh = next < text.length ? text[next] : "";
|
||||||
|
const afterOpen = !prevCh || "{[:,[".includes(prevCh);
|
||||||
|
const beforeClose = !nextCh || ":,}]".includes(nextCh);
|
||||||
|
if (isOpen && afterOpen) out += '"';
|
||||||
|
else if (isClose && beforeClose) out += '"';
|
||||||
|
else out += c;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 轻度修复模型常出的尾逗号 / 裸换行;不要全局替换弯引号 */
|
||||||
|
export function softenJsonText(s) {
|
||||||
|
let t = String(s || "")
|
||||||
|
.replace(/^\uFEFF/, "")
|
||||||
|
.replace(/\u2026/g, "")
|
||||||
|
.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F]/g, "");
|
||||||
|
t = t.replace(/^\s*\/\/.*$/gm, "");
|
||||||
|
t = t.replace(/\/\*[\s\S]*?\*\//g, "");
|
||||||
|
t = escapeRawControlsInJsonStrings(t);
|
||||||
|
t = t.replace(/,\s*([\]}])/g, "$1");
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 截断的 {… 补齐引号/括号,便于验收卡仍能出 mosaic */
|
||||||
|
export function repairTruncatedJsonObject(slice) {
|
||||||
|
let s = String(slice || "").trim();
|
||||||
|
if (!s.startsWith("{")) return s;
|
||||||
|
s = s.replace(/,\s*"[^"]*$/u, "");
|
||||||
|
s = s.replace(/,\s*$/u, "");
|
||||||
|
const opens = (s.match(/\{/g) || []).length;
|
||||||
|
const closes = (s.match(/\}/g) || []).length;
|
||||||
|
const openBrackets = (s.match(/\[/g) || []).length;
|
||||||
|
const closeBrackets = (s.match(/\]/g) || []).length;
|
||||||
|
const quoteCount = (s.match(/(?<!\\)"/g) || []).length;
|
||||||
|
if (quoteCount % 2 === 1) s += '"';
|
||||||
|
if (openBrackets > closeBrackets) s += "]".repeat(openBrackets - closeBrackets);
|
||||||
|
if (opens > closes) s += "}".repeat(opens - closes);
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
function tryParse(s) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(s);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 模型常把弯引号写进字符串;必须先按原文 parse,再做宽松修复 */
|
||||||
|
export function tryParseJsonDoc(text) {
|
||||||
|
const stripped = stripCodeFences(text);
|
||||||
|
const attempts = [
|
||||||
|
stripped,
|
||||||
|
softenJsonText(stripped),
|
||||||
|
softenJsonText(convertStructuralSmartQuotes(stripped)),
|
||||||
|
];
|
||||||
|
for (const candidate of attempts) {
|
||||||
|
const parsed = tryParse(candidate);
|
||||||
|
if (parsed != null) return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cleaned = softenJsonText(convertStructuralSmartQuotes(stripped));
|
||||||
|
if (
|
||||||
|
(cleaned.startsWith('"') && cleaned.endsWith('"')) ||
|
||||||
|
(cleaned.startsWith("'") && cleaned.endsWith("'"))
|
||||||
|
) {
|
||||||
|
const inner = tryParse(cleaned);
|
||||||
|
if (typeof inner === "string") {
|
||||||
|
const parsed = tryParse(softenJsonText(inner)) ?? tryParse(inner);
|
||||||
|
if (parsed != null) return parsed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const objStart = cleaned.indexOf("{");
|
||||||
|
const objEnd = cleaned.lastIndexOf("}");
|
||||||
|
if (objStart >= 0 && objEnd > objStart) {
|
||||||
|
const slice = cleaned.slice(objStart, objEnd + 1);
|
||||||
|
const parsed =
|
||||||
|
tryParse(slice) ??
|
||||||
|
tryParse(softenJsonText(slice)) ??
|
||||||
|
tryParse(repairTruncatedJsonObject(slice)) ??
|
||||||
|
tryParse(softenJsonText(repairTruncatedJsonObject(cleaned.slice(objStart))));
|
||||||
|
if (parsed != null) return parsed;
|
||||||
|
|
||||||
|
const rawSlice = stripped.slice(
|
||||||
|
stripped.indexOf("{"),
|
||||||
|
stripped.lastIndexOf("}") + 1,
|
||||||
|
);
|
||||||
|
const fromRaw = tryParse(rawSlice) ?? tryParse(repairTruncatedJsonObject(rawSlice));
|
||||||
|
if (fromRaw != null) return fromRaw;
|
||||||
|
}
|
||||||
|
|
||||||
|
const arrStart = cleaned.indexOf("[");
|
||||||
|
const arrEnd = cleaned.lastIndexOf("]");
|
||||||
|
if (arrStart >= 0 && arrEnd > arrStart) {
|
||||||
|
const parsed = tryParse(cleaned.slice(arrStart, arrEnd + 1));
|
||||||
|
if (parsed != null) return parsed;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
+47
-2
@@ -432,6 +432,37 @@ code {
|
|||||||
color: var(--st-danger);
|
color: var(--st-danger);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cap-row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
.cap-unknown {
|
||||||
|
color: var(--muted, #888);
|
||||||
|
}
|
||||||
|
.cap-chip {
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 1px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid var(--border, #ddd);
|
||||||
|
}
|
||||||
|
.cap-chip.cap-ok {
|
||||||
|
color: #067647;
|
||||||
|
background: #ecfdf3;
|
||||||
|
border-color: #abefc6;
|
||||||
|
}
|
||||||
|
.cap-chip.cap-fail {
|
||||||
|
color: #b42318;
|
||||||
|
background: #fef3f2;
|
||||||
|
border-color: #fecdca;
|
||||||
|
}
|
||||||
|
.cap-chip.cap-unk {
|
||||||
|
color: #667085;
|
||||||
|
background: #f9fafb;
|
||||||
|
}
|
||||||
|
|
||||||
/* —— Toast —— */
|
/* —— Toast —— */
|
||||||
.settings-toast {
|
.settings-toast {
|
||||||
margin: 10px 28px 0;
|
margin: 10px 28px 0;
|
||||||
@@ -1042,7 +1073,8 @@ code {
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-dialog input {
|
.profile-dialog input,
|
||||||
|
.profile-dialog select {
|
||||||
padding: 9px 12px;
|
padding: 9px 12px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
border: 1px solid var(--st-border-strong);
|
border: 1px solid var(--st-border-strong);
|
||||||
@@ -1051,12 +1083,25 @@ code {
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-dialog input:focus {
|
.profile-dialog select {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-dialog input:focus,
|
||||||
|
.profile-dialog select:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
border-color: #93c5fd;
|
border-color: #93c5fd;
|
||||||
box-shadow: 0 0 0 3px var(--st-accent-soft);
|
box-shadow: 0 0 0 3px var(--st-accent-soft);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.profile-dialog .field-hint {
|
||||||
|
margin-top: 2px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 400;
|
||||||
|
color: var(--st-muted);
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
.dialog-actions {
|
.dialog-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
|||||||
@@ -148,6 +148,17 @@
|
|||||||
Model
|
Model
|
||||||
<input name="model" required placeholder="deepseek-chat" />
|
<input name="model" required placeholder="deepseek-chat" />
|
||||||
</label>
|
</label>
|
||||||
|
<label>
|
||||||
|
思考强度
|
||||||
|
<select name="reasoningEffort">
|
||||||
|
<option value="">默认(不发送)</option>
|
||||||
|
<option value="low">low · 轻量</option>
|
||||||
|
<option value="medium">medium</option>
|
||||||
|
<option value="high">high · 增强</option>
|
||||||
|
<option value="max">max · 深度</option>
|
||||||
|
</select>
|
||||||
|
<span class="field-hint">写入 reasoning_effort;「探测」始终用 low,与此项无关。GLM 建议 low / high / max。</span>
|
||||||
|
</label>
|
||||||
<div class="dialog-actions">
|
<div class="dialog-actions">
|
||||||
<button type="button" id="profile-cancel" class="btn-secondary">取消</button>
|
<button type="button" id="profile-cancel" class="btn-secondary">取消</button>
|
||||||
<button type="button" id="profile-save" class="btn-secondary">仅保存</button>
|
<button type="button" id="profile-save" class="btn-secondary">仅保存</button>
|
||||||
|
|||||||
+53
-7
@@ -150,6 +150,31 @@ function activateButtonHtml(active, id, action) {
|
|||||||
return `<button type="button" class="btn-activate" data-action="${action}" data-id="${id}">选用并生效</button>`;
|
return `<button type="button" class="btn-activate" data-action="${action}" data-id="${id}">选用并生效</button>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function capabilityBadge(caps) {
|
||||||
|
if (!caps || !caps.testedAt) {
|
||||||
|
return `<div class="config-card-meta cap-unknown">能力:未探测(选用前建议点「探测」)</div>`;
|
||||||
|
}
|
||||||
|
const bit = (label, status) => {
|
||||||
|
const cls =
|
||||||
|
status === "ok" ? "cap-ok" : status === "fail" ? "cap-fail" : "cap-unk";
|
||||||
|
return `<span class="cap-chip ${cls}">${escapeHtml(label)}:${escapeHtml(status)}</span>`;
|
||||||
|
};
|
||||||
|
const mode =
|
||||||
|
caps.jsonSchema === "ok"
|
||||||
|
? "JSON Schema"
|
||||||
|
: caps.forcedTool === "ok"
|
||||||
|
? "强制 Tool"
|
||||||
|
: caps.jsonObject === "ok"
|
||||||
|
? "JSON Object"
|
||||||
|
: "纯文本";
|
||||||
|
return `<div class="config-card-meta cap-row">
|
||||||
|
投递:<strong>${escapeHtml(mode)}</strong>
|
||||||
|
${bit("schema", caps.jsonSchema)}
|
||||||
|
${bit("tool", caps.forcedTool)}
|
||||||
|
${bit("json", caps.jsonObject)}
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
function renderProfiles() {
|
function renderProfiles() {
|
||||||
profilesListEl.innerHTML = "";
|
profilesListEl.innerHTML = "";
|
||||||
if (!state.profiles.length) {
|
if (!state.profiles.length) {
|
||||||
@@ -171,13 +196,18 @@ function renderProfiles() {
|
|||||||
${active ? '<span class="badge">当前</span>' : ""}
|
${active ? '<span class="badge">当前</span>' : ""}
|
||||||
</div>
|
</div>
|
||||||
<div class="config-card-meta">${escapeHtml(p.model)} · ${escapeHtml(p.baseUrl)}</div>
|
<div class="config-card-meta">${escapeHtml(p.model)} · ${escapeHtml(p.baseUrl)}</div>
|
||||||
<div class="config-card-meta">Key: ${escapeHtml(maskKey(p.apiKey))}</div>
|
<div class="config-card-meta">Key: ${escapeHtml(maskKey(p.apiKey))}${
|
||||||
|
p.reasoningEffort
|
||||||
|
? ` · 思考: ${escapeHtml(p.reasoningEffort)}`
|
||||||
|
: ""
|
||||||
|
}</div>
|
||||||
|
${capabilityBadge(p.capabilities)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="config-card-actions">
|
<div class="config-card-actions">
|
||||||
${activateButtonHtml(active, p.id, "activate-profile")}
|
${activateButtonHtml(active, p.id, "activate-profile")}
|
||||||
<button type="button" data-action="edit-profile" data-id="${p.id}">编辑</button>
|
<button type="button" data-action="edit-profile" data-id="${p.id}">编辑</button>
|
||||||
<button type="button" data-action="test-profile" data-id="${p.id}">测试连接</button>
|
<button type="button" data-action="probe-capabilities" data-id="${p.id}">探测</button>
|
||||||
<button type="button" class="btn-danger" data-action="delete-profile" data-id="${p.id}">删除</button>
|
<button type="button" class="btn-danger" data-action="delete-profile" data-id="${p.id}">删除</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="test-result" id="test-${p.id}"></div>`;
|
<div class="test-result" id="test-${p.id}"></div>`;
|
||||||
@@ -367,6 +397,7 @@ function openProfileDialog(profile = null) {
|
|||||||
profileForm.baseUrl.value = profile?.baseUrl ?? "https://api.deepseek.com";
|
profileForm.baseUrl.value = profile?.baseUrl ?? "https://api.deepseek.com";
|
||||||
profileForm.apiKey.value = profile?.apiKey ?? "";
|
profileForm.apiKey.value = profile?.apiKey ?? "";
|
||||||
profileForm.model.value = profile?.model ?? "deepseek-v4-pro";
|
profileForm.model.value = profile?.model ?? "deepseek-v4-pro";
|
||||||
|
profileForm.reasoningEffort.value = profile?.reasoningEffort ?? "";
|
||||||
profileDialog.showModal();
|
profileDialog.showModal();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -375,6 +406,7 @@ async function saveProfile(activate) {
|
|||||||
name: profileForm.name.value,
|
name: profileForm.name.value,
|
||||||
baseUrl: profileForm.baseUrl.value,
|
baseUrl: profileForm.baseUrl.value,
|
||||||
model: profileForm.model.value,
|
model: profileForm.model.value,
|
||||||
|
reasoningEffort: profileForm.reasoningEffort.value,
|
||||||
activate,
|
activate,
|
||||||
};
|
};
|
||||||
const apiKey = profileForm.apiKey.value.trim();
|
const apiKey = profileForm.apiKey.value.trim();
|
||||||
@@ -457,13 +489,27 @@ profilesListEl.addEventListener("click", async (e) => {
|
|||||||
await activateProfile(id);
|
await activateProfile(id);
|
||||||
} else if (action === "edit-profile") {
|
} else if (action === "edit-profile") {
|
||||||
openProfileDialog(state.profiles.find((p) => p.id === id));
|
openProfileDialog(state.profiles.find((p) => p.id === id));
|
||||||
} else if (action === "test-profile") {
|
} else if (action === "probe-capabilities") {
|
||||||
const el = document.getElementById(`test-${id}`);
|
const el = document.getElementById(`test-${id}`);
|
||||||
el.textContent = "测试中…";
|
el.textContent = "探测中(含连通性,会打几次短请求)…";
|
||||||
el.className = "test-result";
|
el.className = "test-result";
|
||||||
const result = await api(`/api/profiles/${id}/test`, { method: "POST" });
|
const result = await api(`/api/profiles/${id}/probe-capabilities`, {
|
||||||
el.textContent = result.message;
|
method: "POST",
|
||||||
el.className = `test-result ${result.ok ? "ok" : "fail"}`;
|
});
|
||||||
|
await loadAll();
|
||||||
|
const caps = result.profile?.capabilities;
|
||||||
|
const chatOk = caps?.chat === "ok";
|
||||||
|
const note = !chatOk
|
||||||
|
? `连接失败${caps?.notes?.[0] ? `:${caps.notes[0]}` : ""}`
|
||||||
|
: result.deliveryModeLabel
|
||||||
|
? `连接正常 · 投递将用:${result.deliveryModeLabel}`
|
||||||
|
: "探测完成";
|
||||||
|
const refreshed = document.getElementById(`test-${id}`);
|
||||||
|
if (refreshed) {
|
||||||
|
refreshed.textContent = note;
|
||||||
|
refreshed.className = `test-result ${chatOk ? "ok" : "fail"}`;
|
||||||
|
}
|
||||||
|
showToast(note, !chatOk);
|
||||||
} else if (action === "delete-profile") {
|
} else if (action === "delete-profile") {
|
||||||
if (!confirm("确定删除此 API 配置?")) return;
|
if (!confirm("确定删除此 API 配置?")) return;
|
||||||
await api(`/api/profiles/${id}`, { method: "DELETE" });
|
await api(`/api/profiles/${id}`, { method: "DELETE" });
|
||||||
|
|||||||
+736
-29
@@ -69,12 +69,35 @@ a.link:hover { text-decoration: underline; }
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
.rail-head {
|
.rail-head {
|
||||||
gap: 4px;
|
flex-direction: column;
|
||||||
padding: 6px;
|
align-items: stretch;
|
||||||
flex-wrap: nowrap;
|
justify-content: flex-start;
|
||||||
align-items: center;
|
gap: 0;
|
||||||
|
padding: 0;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
.rail-head-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 40px;
|
||||||
|
padding: 6px 8px 6px 6px;
|
||||||
|
}
|
||||||
|
body[data-nav-level="root"] .rail-head-row {
|
||||||
|
padding-left: 10px;
|
||||||
|
}
|
||||||
|
.rail-context {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-width: 0;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.3;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
.rail-toggle {
|
.rail-toggle {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
width: 28px;
|
width: 28px;
|
||||||
@@ -87,12 +110,19 @@ a.link:hover { text-decoration: underline; }
|
|||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
body.rail-collapsed .rail-toggle { transform: rotate(180deg); }
|
body.rail-collapsed .rail-toggle { transform: rotate(180deg); }
|
||||||
|
body[data-nav-level="root"] .rail-toggle { display: none; }
|
||||||
|
body[data-nav-level="book"] .rail-head-actions #btn-new-book,
|
||||||
|
body[data-nav-level="root"] .burst-badge { display: none; }
|
||||||
.rail-tabs {
|
.rail-tabs {
|
||||||
display: flex;
|
display: none;
|
||||||
flex: 1 1 auto;
|
gap: 0;
|
||||||
gap: 2px;
|
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
padding: 0 8px;
|
||||||
}
|
}
|
||||||
|
body[data-nav-level="book"] .rail-tabs {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.rail-tabs[hidden] { display: none !important; }
|
||||||
.rail-tab {
|
.rail-tab {
|
||||||
flex: 1 1 0;
|
flex: 1 1 0;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
@@ -102,21 +132,28 @@ body.rail-collapsed .rail-toggle { transform: rotate(180deg); }
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
height: 28px;
|
height: 28px;
|
||||||
border: none;
|
border: none;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border-radius: 6px;
|
border-radius: 0;
|
||||||
padding: 0 6px;
|
padding: 0 6px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 600;
|
font-weight: 500;
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
.rail-tab:hover { background: var(--bg); color: var(--text); }
|
.rail-tab:hover { color: var(--text); }
|
||||||
.rail-tab.active {
|
.rail-tab.active {
|
||||||
background: var(--accent-soft);
|
background: transparent;
|
||||||
color: var(--accent);
|
color: var(--text);
|
||||||
|
font-weight: 600;
|
||||||
|
border-bottom-color: var(--text);
|
||||||
|
}
|
||||||
|
.rail-tab:focus-visible {
|
||||||
|
outline: 2px solid color-mix(in srgb, var(--accent) 55%, transparent);
|
||||||
|
outline-offset: -2px;
|
||||||
}
|
}
|
||||||
.rail-tab-icon {
|
.rail-tab-icon {
|
||||||
display: none;
|
display: none;
|
||||||
@@ -177,8 +214,8 @@ body.rail-collapsed .rail-toggle { transform: rotate(180deg); }
|
|||||||
.rail-tab-panel[hidden] { display: none !important; }
|
.rail-tab-panel[hidden] { display: none !important; }
|
||||||
|
|
||||||
body.rail-collapsed .rail-tab-label,
|
body.rail-collapsed .rail-tab-label,
|
||||||
body.rail-collapsed .path-bar,
|
body.rail-collapsed .rail-context,
|
||||||
body.rail-collapsed .rail-head-actions #btn-new-book,
|
body.rail-collapsed .rail-head-actions,
|
||||||
body.rail-collapsed .burst-badge { display: none !important; }
|
body.rail-collapsed .burst-badge { display: none !important; }
|
||||||
body.rail-collapsed .rail-tab-icon { display: block; }
|
body.rail-collapsed .rail-tab-icon { display: block; }
|
||||||
body.rail-collapsed .rail-head {
|
body.rail-collapsed .rail-head {
|
||||||
@@ -187,14 +224,27 @@ body.rail-collapsed .rail-head {
|
|||||||
padding: 6px 4px;
|
padding: 6px 4px;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
}
|
}
|
||||||
body.rail-collapsed .rail-tabs {
|
body.rail-collapsed .rail-head-row {
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
body.rail-collapsed[data-nav-level="book"] .rail-tabs:not([hidden]) {
|
||||||
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
padding: 0;
|
||||||
}
|
}
|
||||||
body.rail-collapsed .rail-tab {
|
body.rail-collapsed .rail-tab {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
border-bottom: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
body.rail-collapsed .rail-tab.active {
|
||||||
|
background: var(--accent-soft);
|
||||||
|
color: var(--accent);
|
||||||
}
|
}
|
||||||
body.rail-collapsed .rail-panels { display: none; }
|
body.rail-collapsed .rail-panels { display: none; }
|
||||||
body.rail-collapsed .panel-rail { align-items: stretch; }
|
body.rail-collapsed .panel-rail { align-items: stretch; }
|
||||||
@@ -734,6 +784,39 @@ body.design-workspace .review-worker-set {
|
|||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
.workspace-intent-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
max-width: 46rem;
|
||||||
|
}
|
||||||
|
.workspace-intent-head .workspace-surface-kicker {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
.workspace-leave-step {
|
||||||
|
appearance: none;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
padding: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--muted);
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: var(--font-ui);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.workspace-leave-step:hover {
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
.workspace-leave-step:focus-visible {
|
||||||
|
outline: 2px solid var(--accent);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
.workspace-answer-head--slim .workspace-leave-step {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
.workspace-intent,
|
.workspace-intent,
|
||||||
.workspace-answer {
|
.workspace-answer {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
@@ -884,6 +967,406 @@ body.design-workspace .flow-status {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
}
|
}
|
||||||
|
body.design-workspace .flow-dag {
|
||||||
|
position: relative;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 4px 2px 8px;
|
||||||
|
}
|
||||||
|
body.design-workspace .flow-dag-edges {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 0;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
body.design-workspace .flow-dag-edge {
|
||||||
|
stroke: color-mix(in srgb, var(--border) 80%, var(--muted));
|
||||||
|
stroke-width: 1.5;
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
fill: color-mix(in srgb, var(--accent) 50%, var(--border));
|
||||||
|
}
|
||||||
|
body.design-workspace .flow-dag-rows {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 28px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
body.design-workspace .flow-dag-row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
body.design-workspace .flow-dag-row-label {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 650;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
color: var(--muted);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
body.design-workspace .flow-dag-row-nodes {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
body.design-workspace .flow-node {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
width: min(100%, 280px);
|
||||||
|
margin: 0;
|
||||||
|
padding: 12px 14px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 10px;
|
||||||
|
background: var(--bg);
|
||||||
|
color: inherit;
|
||||||
|
text-align: left;
|
||||||
|
font: inherit;
|
||||||
|
min-width: 0;
|
||||||
|
box-shadow: 0 1px 0 color-mix(in srgb, var(--border) 70%, transparent);
|
||||||
|
transition:
|
||||||
|
transform 0.12s ease,
|
||||||
|
box-shadow 0.12s ease,
|
||||||
|
border-color 0.12s ease,
|
||||||
|
background 0.12s ease;
|
||||||
|
}
|
||||||
|
body.design-workspace .flow-node-main {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 2px;
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
body.design-workspace .flow-node.is-done-menu {
|
||||||
|
cursor: context-menu;
|
||||||
|
opacity: 0.92;
|
||||||
|
}
|
||||||
|
body.design-workspace .flow-node.is-done-menu:hover {
|
||||||
|
opacity: 1;
|
||||||
|
border-color: color-mix(in srgb, var(--border) 40%, var(--text) 20%);
|
||||||
|
background: var(--surface);
|
||||||
|
}
|
||||||
|
body.design-workspace .flow-node.is-done-menu:focus-visible {
|
||||||
|
outline: 2px solid var(--accent);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
body.design-workspace .flow-node-menu-btn {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
align-self: center;
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1;
|
||||||
|
color: var(--muted);
|
||||||
|
opacity: 0.55;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.step-artifact-body {
|
||||||
|
max-height: min(70vh, 560px);
|
||||||
|
overflow: auto;
|
||||||
|
margin: 8px 0 0;
|
||||||
|
}
|
||||||
|
body.design-workspace .flow-node[data-run="current"] {
|
||||||
|
border-color: var(--accent);
|
||||||
|
background: var(--surface);
|
||||||
|
}
|
||||||
|
body.design-workspace .flow-node.is-locked {
|
||||||
|
opacity: 0.55;
|
||||||
|
}
|
||||||
|
body.design-workspace .flow-node.is-selectable {
|
||||||
|
cursor: pointer;
|
||||||
|
border-color: var(--accent);
|
||||||
|
border-width: 1.5px;
|
||||||
|
background: var(--surface);
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 1px color-mix(in srgb, var(--accent) 18%, transparent),
|
||||||
|
0 4px 12px color-mix(in srgb, var(--accent) 12%, transparent);
|
||||||
|
}
|
||||||
|
body.design-workspace .flow-node.is-selectable:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
background: color-mix(in srgb, var(--accent) 10%, var(--surface));
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 1px color-mix(in srgb, var(--accent) 28%, transparent),
|
||||||
|
0 8px 18px color-mix(in srgb, var(--accent) 16%, transparent);
|
||||||
|
}
|
||||||
|
body.design-workspace .flow-node.is-selectable:active {
|
||||||
|
transform: translateY(0) scale(0.98);
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 1px color-mix(in srgb, var(--accent) 35%, transparent),
|
||||||
|
0 2px 6px color-mix(in srgb, var(--accent) 12%, transparent);
|
||||||
|
}
|
||||||
|
body.design-workspace .flow-node.is-selectable:focus-visible {
|
||||||
|
outline: 2px solid var(--accent);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
body.design-workspace .flow-action {
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
color: var(--accent);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
body.design-workspace .flow-node.is-selectable:hover .flow-action {
|
||||||
|
color: color-mix(in srgb, var(--accent) 80%, #000);
|
||||||
|
}
|
||||||
|
body.design-workspace .flow-node--prototype {
|
||||||
|
border-style: dashed;
|
||||||
|
border-color: color-mix(in srgb, var(--accent) 45%, var(--border));
|
||||||
|
background: color-mix(in srgb, var(--accent) 6%, var(--bg));
|
||||||
|
}
|
||||||
|
body.design-workspace .flow-node--prototype.is-selectable {
|
||||||
|
border-style: dashed;
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
body.design-workspace .flow-node--prototype.is-selectable:hover {
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
body.design-workspace .flow-occ-prototype {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
body.design-workspace .flow-family {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
width: min(100%, 280px);
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
body.design-workspace .flow-family > .flow-node {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
body.design-workspace .flow-family-spawns {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 8px 0 0 22px;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
body.design-workspace .flow-family-spawns::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: 12px;
|
||||||
|
top: 0;
|
||||||
|
bottom: 14px;
|
||||||
|
width: 1px;
|
||||||
|
background: color-mix(in srgb, var(--accent) 32%, var(--border));
|
||||||
|
}
|
||||||
|
body.design-workspace .flow-family-spawn {
|
||||||
|
position: relative;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
body.design-workspace .flow-family-spawn::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: -10px;
|
||||||
|
top: 50%;
|
||||||
|
width: 10px;
|
||||||
|
height: 1px;
|
||||||
|
background: color-mix(in srgb, var(--accent) 32%, var(--border));
|
||||||
|
}
|
||||||
|
body.design-workspace .flow-node--spawn {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 8px 10px;
|
||||||
|
gap: 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: none;
|
||||||
|
background: var(--surface);
|
||||||
|
}
|
||||||
|
body.design-workspace .flow-node--spawn .flow-name {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
body.design-workspace .flow-node--spawn .flow-params,
|
||||||
|
body.design-workspace .flow-node--spawn .flow-params-missing {
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
body.design-workspace .flow-node--spawn.is-selectable {
|
||||||
|
border-width: 1px;
|
||||||
|
box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent) 14%, transparent);
|
||||||
|
}
|
||||||
|
body.design-workspace .flow-node--spawn.is-selectable:hover {
|
||||||
|
transform: none;
|
||||||
|
box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent) 28%, transparent);
|
||||||
|
}
|
||||||
|
body.design-workspace .flow-spawn-meta {
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
body.design-workspace .flow-spawn-meta .flow-action {
|
||||||
|
min-height: 24px;
|
||||||
|
padding: 0 8px;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
body.design-workspace .flow-spawn-count {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--muted);
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
body.design-workspace .flow-origin {
|
||||||
|
display: block;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--muted);
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
body.design-workspace .flow-origin-rule {
|
||||||
|
font-weight: 650;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
body.design-workspace .flow-dag-edge--belong {
|
||||||
|
stroke-dasharray: 3 4;
|
||||||
|
stroke-width: 1.25;
|
||||||
|
}
|
||||||
|
body.design-workspace .flow-dag-arrowhead--belong {
|
||||||
|
fill: color-mix(in srgb, var(--accent) 38%, var(--border));
|
||||||
|
}
|
||||||
|
body.design-workspace .flow-suggestion-inline {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--muted);
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
body.design-workspace .flow-suggestions {
|
||||||
|
margin-top: 12px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border: 1px dashed var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--surface);
|
||||||
|
}
|
||||||
|
body.design-workspace .flow-suggestions-title {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
body.design-workspace .flow-suggestion {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 6px 0;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
body.design-workspace .flow-suggestion:first-of-type {
|
||||||
|
border-top: none;
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
body.design-workspace .flow-suggestion-name {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
body.design-workspace .flow-suggestion-text {
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
body.design-workspace .flow-node .flow-run {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 650;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
body.design-workspace .flow-node[data-run="done"] .flow-run {
|
||||||
|
color: #15803d;
|
||||||
|
}
|
||||||
|
body.design-workspace .flow-node.is-selectable .flow-run {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
body.design-workspace .flow-node .flow-name {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
body.design-workspace .flow-title {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 650;
|
||||||
|
line-height: 1.35;
|
||||||
|
color: var(--text);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
body.design-workspace .flow-node .flow-params,
|
||||||
|
body.design-workspace .flow-node .flow-params-missing {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--muted);
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
body.design-workspace .flow-spawns {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 14px;
|
||||||
|
}
|
||||||
|
body.design-workspace .flow-spawn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 32px;
|
||||||
|
padding: 0 10px;
|
||||||
|
border: 1px dashed var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--muted);
|
||||||
|
font: inherit;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
body.design-workspace .flow-spawn.is-ready {
|
||||||
|
cursor: pointer;
|
||||||
|
border-style: solid;
|
||||||
|
border-color: var(--accent);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
body.design-workspace .flow-spawn.is-ready:hover {
|
||||||
|
background: color-mix(in srgb, var(--accent) 8%, var(--surface));
|
||||||
|
}
|
||||||
|
body.design-workspace .flow-spawn.is-ready:focus-visible {
|
||||||
|
outline: 2px solid var(--accent);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
body.design-workspace .flow-spawn.is-blocked {
|
||||||
|
opacity: 0.65;
|
||||||
|
}
|
||||||
|
@media (max-width: 720px) {
|
||||||
|
body.design-workspace .flow-dag-row-nodes {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
body.design-workspace .flow-node,
|
||||||
|
body.design-workspace .flow-family {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
body.design-workspace .flow-steps {
|
body.design-workspace .flow-steps {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -1004,8 +1487,20 @@ body.design-workspace .flow-step[data-run="done"] .flow-name {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 6px 10px;
|
padding: 6px 10px;
|
||||||
}
|
}
|
||||||
|
.btn:focus,
|
||||||
|
.btn-sm:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
.btn:focus-visible,
|
||||||
|
.btn-sm:focus-visible {
|
||||||
|
outline: 2px solid var(--accent);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
.btn-sm { padding: 4px 8px; font-size: 12px; }
|
.btn-sm { padding: 4px 8px; font-size: 12px; }
|
||||||
.btn-primary { background: var(--accent); color: #fff; border-color: var(--accent); }
|
.btn-primary { background: var(--accent); color: #fff; border-color: var(--accent); }
|
||||||
|
.btn-primary:focus-visible {
|
||||||
|
outline-color: color-mix(in srgb, var(--accent) 50%, #fff);
|
||||||
|
}
|
||||||
.btn-danger { color: var(--danger); border-color: #fecaca; }
|
.btn-danger { color: var(--danger); border-color: #fecaca; }
|
||||||
.btn:disabled, .btn-sm:disabled { opacity: 0.5; cursor: not-allowed; }
|
.btn:disabled, .btn-sm:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||||
|
|
||||||
@@ -1950,7 +2445,7 @@ body.design-workspace .af-mosaic {
|
|||||||
grid-template-columns: repeat(auto-fit, minmax(min(100%, 280px), 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(min(100%, 280px), 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 美学验收:左契约 / 右诊断(整体样式约定) */
|
/* 美学验收:左契约 / 右诊断 */
|
||||||
.af-split {
|
.af-split {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -2002,13 +2497,13 @@ body.design-workspace .af-mosaic {
|
|||||||
background: var(--bg);
|
background: var(--bg);
|
||||||
}
|
}
|
||||||
.af-block-head {
|
.af-block-head {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
.af-block-title {
|
.af-block-title {
|
||||||
margin: 0 0 4px;
|
margin: 0;
|
||||||
font-family: var(--font-ui);
|
font-family: var(--font-ui);
|
||||||
font-size: 15px;
|
font-size: 14px;
|
||||||
font-weight: 700;
|
font-weight: 650;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
letter-spacing: 0.01em;
|
letter-spacing: 0.01em;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -2032,20 +2527,80 @@ body.design-workspace .af-mosaic {
|
|||||||
.af-block-body {
|
.af-block-body {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
.af-block--product .af-block-body .artifact-kernel .artifact-prose,
|
|
||||||
.af-block--product .af-block-body .artifact-prose {
|
/* 美学卡:阅读层级 — 内核最大,范式次之,右栏更密更沉 */
|
||||||
font-family: var(--font-read);
|
.af-split--aesthetics .af-split-product {
|
||||||
font-size: 15.5px;
|
gap: 22px;
|
||||||
line-height: 1.75;
|
|
||||||
max-width: 42rem;
|
|
||||||
}
|
}
|
||||||
.af-block--eval .af-block-title {
|
.af-split--aesthetics .af-block--product .af-block-title {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--muted);
|
||||||
|
text-transform: none;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
}
|
||||||
|
.af-split--aesthetics .af-block--product .af-block-head {
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
.af-split--aesthetics .artifact-kernel.qcard-assessment,
|
||||||
|
.af-split--aesthetics .artifact-kernel {
|
||||||
|
margin: 0 0 14px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid color-mix(in srgb, var(--border) 80%, transparent);
|
||||||
|
background: color-mix(in srgb, var(--surface) 88%, var(--bg));
|
||||||
|
}
|
||||||
|
.af-split--aesthetics .artifact-kernel .artifact-aside-label {
|
||||||
|
margin: 0 0 6px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 650;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
.af-split--aesthetics .artifact-kernel .artifact-prose,
|
||||||
|
.af-split--aesthetics .artifact-kernel .artifact-inline {
|
||||||
|
font-family: var(--font-read);
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.75;
|
||||||
|
max-width: 40rem;
|
||||||
|
}
|
||||||
|
.af-split--aesthetics .af-block--product .af-group-title,
|
||||||
|
.af-split--aesthetics .af-block--product .af-section-title {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 650;
|
||||||
|
color: var(--muted);
|
||||||
|
margin: 0 0 6px;
|
||||||
|
}
|
||||||
|
.af-split--aesthetics .af-block--product .af-stack,
|
||||||
|
.af-split--aesthetics .af-block--product .af-def-list {
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.af-split--aesthetics .af-block--product .af-def-v,
|
||||||
|
.af-split--aesthetics .af-block--product .artifact-prose {
|
||||||
|
font-family: var(--font-read);
|
||||||
|
font-size: 14.5px;
|
||||||
|
line-height: 1.65;
|
||||||
|
max-width: 40rem;
|
||||||
|
}
|
||||||
|
.af-split--aesthetics .af-split-eval {
|
||||||
|
gap: 12px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
.af-block--extra .af-block-title {
|
.af-split--aesthetics .af-block--eval .af-block-title,
|
||||||
|
.af-split--aesthetics .af-block--extra .af-block-title {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
font-weight: 650;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
}
|
}
|
||||||
|
.af-split--aesthetics .af-block--eval .af-block-body,
|
||||||
|
.af-split--aesthetics .af-block--extra .af-block-body {
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: color-mix(in srgb, var(--text) 92%, var(--muted));
|
||||||
|
}
|
||||||
|
.af-split--aesthetics .af-dim-title {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
.af-split-grid {
|
.af-split-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
@@ -2368,6 +2923,145 @@ body.design-workspace .af-mosaic {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 14px;
|
gap: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 美学纲领:主栏契约 / 辅栏诊断(无第二层盒子) */
|
||||||
|
.skill-view.skill-view-aesthetics {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1.55fr) minmax(15.5rem, 0.88fr);
|
||||||
|
gap: 8px 36px;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
.skill-view-aesthetics.af-aes--solo {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
.af-aes-main,
|
||||||
|
.af-aes-diag {
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
.af-aes-diag {
|
||||||
|
padding-left: 22px;
|
||||||
|
border-left: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.af-aes-kernel .artifact-aside-label {
|
||||||
|
display: block;
|
||||||
|
margin: 0 0 8px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
.af-aes-kernel .artifact-prose,
|
||||||
|
.af-aes-kernel .artifact-inline {
|
||||||
|
font-family: var(--font-read);
|
||||||
|
font-size: 15.5px;
|
||||||
|
line-height: 1.78;
|
||||||
|
max-width: 42rem;
|
||||||
|
margin: 0;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
.af-aes-paradigm {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
.af-aes-main > .skill-card-section > h4,
|
||||||
|
.af-aes-paradigm > .skill-card-section > h4 {
|
||||||
|
margin: 0 0 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
.af-aes-diag > .skill-card-section > h4 {
|
||||||
|
margin: 0 0 8px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
.af-aes-diag .af-dim-stack { gap: 10px; }
|
||||||
|
.af-aes-diag .af-dim { padding-bottom: 10px; }
|
||||||
|
.af-aes-diag .af-dim-title { font-size: 13px; }
|
||||||
|
.af-aes-twist {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.af-aes-twist-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 2.5rem minmax(0, 1fr);
|
||||||
|
gap: 8px 10px;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
.af-aes-twist-k {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--muted);
|
||||||
|
padding-top: 2px;
|
||||||
|
}
|
||||||
|
.af-aes-twist-v {
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
.af-aes-twist-infer {
|
||||||
|
margin: 2px 0 0;
|
||||||
|
font-family: var(--font-read);
|
||||||
|
font-size: 13.5px;
|
||||||
|
line-height: 1.55;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
.af-dim-row.is-pending .af-dim-k {
|
||||||
|
color: #a16207;
|
||||||
|
font-weight: 650;
|
||||||
|
}
|
||||||
|
.af-dim-row.is-pending .af-dim-v { color: #854d0e; }
|
||||||
|
@media (max-width: 860px) {
|
||||||
|
.skill-view.skill-view-aesthetics {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
.af-aes-diag {
|
||||||
|
padding-left: 0;
|
||||||
|
border-left: none;
|
||||||
|
padding-top: 16px;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.artifact-compact-body .skill-view.skill-view-aesthetics {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
.artifact-compact-body .af-aes-diag {
|
||||||
|
padding-left: 0;
|
||||||
|
border-left: none;
|
||||||
|
padding-top: 12px;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.ws-focus-body .skill-view.skill-view-aesthetics {
|
||||||
|
grid-template-columns: minmax(0, 1.55fr) minmax(15.5rem, 0.88fr);
|
||||||
|
}
|
||||||
|
.ws-focus-body .af-aes-diag {
|
||||||
|
padding-left: 22px;
|
||||||
|
border-left: 1px solid var(--border);
|
||||||
|
padding-top: 0;
|
||||||
|
border-top: none;
|
||||||
|
}
|
||||||
|
@media (max-width: 860px) {
|
||||||
|
.ws-focus-body .skill-view.skill-view-aesthetics {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
.ws-focus-body .af-aes-diag {
|
||||||
|
padding-left: 0;
|
||||||
|
border-left: none;
|
||||||
|
padding-top: 16px;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.skill-card-section > h4 {
|
.skill-card-section > h4 {
|
||||||
margin: 0 0 8px;
|
margin: 0 0 8px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
@@ -2395,6 +3089,14 @@ body.design-workspace .af-mosaic {
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
line-height: 1.45;
|
line-height: 1.45;
|
||||||
}
|
}
|
||||||
|
.af-aes-main .skill-kv-row,
|
||||||
|
.af-aes-paradigm .skill-kv-row {
|
||||||
|
grid-template-columns: minmax(4.75rem, 7.25rem) 1fr;
|
||||||
|
}
|
||||||
|
.af-aes-main .skill-kv-val,
|
||||||
|
.af-aes-paradigm .skill-kv-val {
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
.skill-kv-row { grid-template-columns: 1fr; gap: 2px; }
|
.skill-kv-row { grid-template-columns: 1fr; gap: 2px; }
|
||||||
}
|
}
|
||||||
@@ -3688,6 +4390,11 @@ body.design-workspace .af-mosaic {
|
|||||||
}
|
}
|
||||||
.context-menu-item[hidden] { display: none; }
|
.context-menu-item[hidden] { display: none; }
|
||||||
.context-menu-item:hover { background: var(--bg); }
|
.context-menu-item:hover { background: var(--bg); }
|
||||||
|
.context-menu-item:disabled {
|
||||||
|
opacity: 0.45;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
.context-menu-item:disabled:hover { background: transparent; }
|
||||||
.context-menu-separator-before { border-top: 1px solid var(--border); }
|
.context-menu-separator-before { border-top: 1px solid var(--border); }
|
||||||
.context-menu-danger { color: var(--danger); }
|
.context-menu-danger { color: var(--danger); }
|
||||||
.context-menu-danger:hover { background: #fef2f2; }
|
.context-menu-danger:hover { background: #fef2f2; }
|
||||||
@@ -3976,7 +4683,7 @@ body.design-workspace .af-mosaic {
|
|||||||
.layout { grid-template-columns: var(--books-w-collapsed) 1fr; }
|
.layout { grid-template-columns: var(--books-w-collapsed) 1fr; }
|
||||||
body:not(.rail-collapsed) { --books-w: var(--books-w-collapsed); }
|
body:not(.rail-collapsed) { --books-w: var(--books-w-collapsed); }
|
||||||
body:not(.rail-collapsed) .rail-tab-label,
|
body:not(.rail-collapsed) .rail-tab-label,
|
||||||
body:not(.rail-collapsed) .path-bar { display: none !important; }
|
body:not(.rail-collapsed) .rail-context { display: none !important; }
|
||||||
body:not(.rail-collapsed) .rail-panels { display: none; }
|
body:not(.rail-collapsed) .rail-panels { display: none; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user