feat(workflow): 工作流库管理工具与编写技能,修复运行时 UI 多处问题
- 新增 list_workflows / save_workflow 工具:归档式创建与覆盖, 归档时完整结构校验、覆盖前备份可恢复、name 与目录名一致性检查 - 新增内置技能 workflow-authoring:WORKFLOW.md 编写指南 + init/validate 脚本 + 节点详解与三个完整示例,skill_hints 挂载 - deactivate_workflow 挪工具循环特判层并广播摘牌快照, 修复模型自主退出后 slash 菜单与快捷卡片残留 - 工作流卡片消失改为退出动画:store exiting 状态保留快照播完动画 再清空,QuickDock hasContent 动画期间保持展开 - 运行期 workflow 通知 visibility 修正为 compact(原误走用户消息渲染) - 激活/退出通知文案中性化,不向模型透露内部操作路径 - 修复空闲对话刷新后输入栏 token 圆环无数值(bootstrapRoute 补拉取) - 修复 slash 激活工作流后输入栏 "/" 残留(跳转新对话前落盘草稿) - 侧边栏工作流导航图标放大至 20px
This commit is contained in:
parent
4d9b709a9e
commit
8c1dd407df
106
agentskills/workflow-authoring/SKILL.md
Normal file
106
agentskills/workflow-authoring/SKILL.md
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
---
|
||||||
|
name: workflow-authoring
|
||||||
|
description: 编写、创建或修改工作流(workflow)定义文件 WORKFLOW.md 时使用。工作流把一套既定流程(阶段、审核、分支、结束方式)存为可复用文档,激活后智能体按流程逐步推进。当用户要求「新建一个工作流」「写一个 xx 流程」「调整工作流结构(增删阶段/审核/分支)」,或需要排查工作流格式与校验问题时使用。附带初始化与校验脚本。
|
||||||
|
---
|
||||||
|
|
||||||
|
# 工作流(Workflow)文档编写指南
|
||||||
|
|
||||||
|
工作流 = 一份 `WORKFLOW.md`:YAML frontmatter 描述节点拓扑,markdown 正文写流程约定。核心理念:**结构在边上,自主在阶段内**——拓扑只约束阶段间流转,每个执行阶段内智能体自由工作,阶段完成后汇报推进。
|
||||||
|
|
||||||
|
## 标准创建流程
|
||||||
|
|
||||||
|
1. **初始化模板**:
|
||||||
|
```bash
|
||||||
|
python3 scripts/init_workflow.py <name> [--path <目录>] [--template linear|review|branch]
|
||||||
|
```
|
||||||
|
生成 `<目录>/<name>/WORKFLOW.md`(已存在不覆盖)。三种模板:linear 线性 / review 带审核 / branch 带分支。
|
||||||
|
2. **填写内容**:替换模板中所有 `[方括号]` 占位,按需增删节点。
|
||||||
|
3. **保存前自检**:
|
||||||
|
```bash
|
||||||
|
python3 scripts/validate_workflow.py <name>
|
||||||
|
```
|
||||||
|
有 error 级问题必须全部修掉,否则保存会被拒绝。
|
||||||
|
4. 校验通过后交由保存流程(工作流名即 `<name>`,合法字符:小写字母/数字/连字符,首字符为字母或数字)。
|
||||||
|
|
||||||
|
## WORKFLOW.md 格式骨架
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
name: my-pipeline
|
||||||
|
description: 一句话说明这个流程干什么
|
||||||
|
review_mode: active
|
||||||
|
max_stage_rounds: 20
|
||||||
|
end_conditions: 什么算完成
|
||||||
|
nodes:
|
||||||
|
- id: start-1
|
||||||
|
kind: start
|
||||||
|
name: 开始
|
||||||
|
next: explore
|
||||||
|
- id: explore
|
||||||
|
kind: stage
|
||||||
|
name: 代码探索
|
||||||
|
goal: 理解改动范围,产出影响面清单
|
||||||
|
instructions: 先读 git diff 总览,再按模块深入;禁止通读整文件。
|
||||||
|
next: verify-gate
|
||||||
|
- id: verify-gate
|
||||||
|
kind: review
|
||||||
|
name: 验证审核
|
||||||
|
prompt: 验证证据是否真实可复现
|
||||||
|
next: end-1
|
||||||
|
reject_to: explore
|
||||||
|
max_rejects: 3
|
||||||
|
- id: end-1
|
||||||
|
kind: end
|
||||||
|
name: 结束
|
||||||
|
---
|
||||||
|
|
||||||
|
## 工作方式
|
||||||
|
先理解再动手,禁止跳过验证。
|
||||||
|
|
||||||
|
## 验证方式
|
||||||
|
构建通过 + 相关测试通过。
|
||||||
|
|
||||||
|
## 结束方式
|
||||||
|
更新文档并汇报。
|
||||||
|
```
|
||||||
|
|
||||||
|
`---` 之后的正文会在激活时原文呈现给执行者——写各阶段通用的整体约定,别写节点级细节。
|
||||||
|
|
||||||
|
## frontmatter 字段
|
||||||
|
|
||||||
|
| 字段 | 必填 | 默认 | 说明 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `name` | ✓ | — | 小写字母/数字/连字符 |
|
||||||
|
| `description` | 推荐 | `''` | 一句话用途,出现在流程选择菜单 |
|
||||||
|
| `review_mode` | | `active` | `active`:审核时可查证证据;`readonly`:审核只看汇报与记录 |
|
||||||
|
| `max_stage_rounds` | | `20` | 单阶段轮数上限;撞限不停流程,只会让执行者停下来向用户确认是否继续 |
|
||||||
|
| `end_conditions` | | `''` | 完成标准的声明性描述(展示用) |
|
||||||
|
| `nodes` | ✓ | — | 节点数组,五种节点见 references/nodes.md |
|
||||||
|
|
||||||
|
## 节点速查
|
||||||
|
|
||||||
|
| kind | 语义 | 关键字段 |
|
||||||
|
|---|---|---|
|
||||||
|
| `start` | 入口,恰好 1 个 | `next` |
|
||||||
|
| `end` | 终点,至少 1 个 | — |
|
||||||
|
| `stage` | 执行阶段(阶段内完全自主) | `goal` + `instructions` + `next` |
|
||||||
|
| `review` | 审核把关(汇报时同步审完立即走,不停留) | `prompt` + `next` + `reject_to` + `max_rejects` |
|
||||||
|
| `branch` | 单出线=并线器自动穿过;多出线=AI 决策点(`condition` 必填为决策依据) | `next[]`(target/condition) |
|
||||||
|
|
||||||
|
**字段详解与语义**:见 [references/nodes.md](references/nodes.md)
|
||||||
|
**完整示例**(线性 / 带审核 / 分支+并线):见 [references/examples.md](references/examples.md)
|
||||||
|
|
||||||
|
## 校验规则(违反即保存失败,validate 脚本同款)
|
||||||
|
|
||||||
|
1. `name` 非空且合法;节点 id 全局唯一
|
||||||
|
2. 恰好 1 个 start;至少 1 个 end
|
||||||
|
3. start/stage/review 的 `next`、review 的 `reject_to`、branch 每条出线的 `target` 都必须显式连接、指向存在的节点、且不指向 start
|
||||||
|
4. `max_rejects ≥ 1`;多出线 branch 的每条 `condition` 必填
|
||||||
|
|
||||||
|
## 写作建议
|
||||||
|
|
||||||
|
- 阶段 3~7 个为宜;`goal` 一句话、`instructions` 三五条关键约束(这两者在每次推进时会全文呈现给执行者)
|
||||||
|
- 需要把关就显式放 review 节点——终点前不会自动审核
|
||||||
|
- `reject_to` 指回真正能整改问题的阶段,不要指回紧邻前一站走过场
|
||||||
|
- 分支 `condition` 写给 AI 的决策依据:互斥、可判断、覆盖常见情形
|
||||||
|
- 修改已激活的工作流文档不影响正在运行的实例——运行中的实例按激活时的版本执行,需退出后重新激活才用新版本
|
||||||
195
agentskills/workflow-authoring/references/examples.md
Normal file
195
agentskills/workflow-authoring/references/examples.md
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
# 完整示例
|
||||||
|
|
||||||
|
三个典型结构的完整 WORKFLOW.md,可直接复制改造。目录:线性(无审核)→ 单审核 → 分支+并线+审核。
|
||||||
|
|
||||||
|
## 1. 线性流程(调研报告)
|
||||||
|
|
||||||
|
最小可用结构:start → 阶段链 → end。适合步骤固定、无需审核的收集整理类流程。
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
name: research-report
|
||||||
|
description: 调研流程:收集资料、交叉验证、输出结构化报告
|
||||||
|
review_mode: active
|
||||||
|
max_stage_rounds: 25
|
||||||
|
end_conditions: 报告落盘且来源标注完整
|
||||||
|
nodes:
|
||||||
|
- id: start-1
|
||||||
|
kind: start
|
||||||
|
name: 开始
|
||||||
|
next: collect
|
||||||
|
- id: collect
|
||||||
|
kind: stage
|
||||||
|
name: 资料收集
|
||||||
|
goal: 围绕主题收集足够的一手资料
|
||||||
|
instructions: 优先官方来源;每条资料记录出处;不下结论。
|
||||||
|
next: verify
|
||||||
|
- id: verify
|
||||||
|
kind: stage
|
||||||
|
name: 交叉验证
|
||||||
|
goal: 剔除孤证与不可靠信息
|
||||||
|
instructions: 关键事实至少两个独立来源;存疑内容明确标注。
|
||||||
|
next: report
|
||||||
|
- id: report
|
||||||
|
kind: stage
|
||||||
|
name: 输出报告
|
||||||
|
goal: 生成结构化调研报告
|
||||||
|
instructions: 结论先行,证据随后;每个结论标注来源。
|
||||||
|
next: end-1
|
||||||
|
- id: end-1
|
||||||
|
kind: end
|
||||||
|
name: 结束
|
||||||
|
---
|
||||||
|
|
||||||
|
## 工作方式
|
||||||
|
先广度收集,再深度验证,最后成文。
|
||||||
|
|
||||||
|
## 验证方式
|
||||||
|
关键事实双来源;报告结论均可回溯到证据。
|
||||||
|
|
||||||
|
## 结束方式
|
||||||
|
报告落盘并汇报要点。
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. 单审核流程(代码评审)
|
||||||
|
|
||||||
|
阶段链中嵌入 review 节点形成把关;驳回回到能整改的前序阶段。
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
name: code-review-pipeline
|
||||||
|
description: 代码评审标准流程:探索改动、逐项评审、输出结构化报告
|
||||||
|
review_mode: active
|
||||||
|
max_stage_rounds: 20
|
||||||
|
end_conditions: 评审报告落盘且最终审核通过
|
||||||
|
nodes:
|
||||||
|
- id: start-1
|
||||||
|
kind: start
|
||||||
|
name: 开始
|
||||||
|
next: explore
|
||||||
|
- id: explore
|
||||||
|
kind: stage
|
||||||
|
name: 代码探索
|
||||||
|
goal: 理解改动范围与相关模块,产出影响面清单
|
||||||
|
instructions: 先读 git diff 总览,再按模块逐个深入;禁止通读整文件。
|
||||||
|
next: review
|
||||||
|
- id: review
|
||||||
|
kind: stage
|
||||||
|
name: 逐项评审
|
||||||
|
goal: 按 checklist 评审每个改动文件
|
||||||
|
instructions: 关注边界条件、错误处理、安全问题;每条意见标注文件与行号。
|
||||||
|
next: review-gate
|
||||||
|
- id: review-gate
|
||||||
|
kind: review
|
||||||
|
name: 评审审核
|
||||||
|
prompt: 检查是否遗漏边界条件和安全问题
|
||||||
|
next: report
|
||||||
|
reject_to: explore
|
||||||
|
max_rejects: 3
|
||||||
|
- id: report
|
||||||
|
kind: stage
|
||||||
|
name: 输出报告
|
||||||
|
goal: 生成结构化评审报告并落盘
|
||||||
|
instructions: 按「严重/建议/可选」三档组织;给出明确结论。
|
||||||
|
next: report-gate
|
||||||
|
- id: report-gate
|
||||||
|
kind: review
|
||||||
|
name: 报告审核
|
||||||
|
prompt: 报告结论是否与评审意见一致
|
||||||
|
next: end-1
|
||||||
|
reject_to: review
|
||||||
|
max_rejects: 3
|
||||||
|
- id: end-1
|
||||||
|
kind: end
|
||||||
|
name: 结束
|
||||||
|
---
|
||||||
|
|
||||||
|
## 工作方式
|
||||||
|
按阶段推进,每个阶段完成后汇报。
|
||||||
|
|
||||||
|
## 验证方式
|
||||||
|
每条评审意见有文件与行号;审核关注边界与安全。
|
||||||
|
|
||||||
|
## 结束方式
|
||||||
|
报告落盘并给出明确结论。
|
||||||
|
```
|
||||||
|
|
||||||
|
注意两个审核节点的 `reject_to` 选择:评审不充分 → 回到「代码探索」重新理解;报告问题 → 回到「逐项评审」,而不是机械地都回前一站。
|
||||||
|
|
||||||
|
## 3. 分支 + 并线 + 审核(功能开发)
|
||||||
|
|
||||||
|
多出线决策点(AI 按 condition 选路)+ 单出线并线器(汇合后统一审核)。
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
name: feature-development
|
||||||
|
description: 功能开发流程:需求理解、按策略分支实现、汇总验证、文档收尾
|
||||||
|
review_mode: active
|
||||||
|
max_stage_rounds: 30
|
||||||
|
end_conditions: 验证通过且文档更新完成
|
||||||
|
nodes:
|
||||||
|
- id: start-1
|
||||||
|
kind: start
|
||||||
|
name: 开始
|
||||||
|
next: understand
|
||||||
|
- id: understand
|
||||||
|
kind: stage
|
||||||
|
name: 需求理解
|
||||||
|
goal: 明确需求边界与涉及模块
|
||||||
|
instructions: 产出改动清单与影响面;不确定处先问用户。
|
||||||
|
next: impl-branch
|
||||||
|
- id: impl-branch
|
||||||
|
kind: branch
|
||||||
|
name: 实现策略
|
||||||
|
next:
|
||||||
|
- target: prototype
|
||||||
|
condition: 方案不确定或风险高,先低成本验证
|
||||||
|
- target: full-impl
|
||||||
|
condition: 需求明确、改动范围清晰,直接正式实现
|
||||||
|
- id: prototype
|
||||||
|
kind: stage
|
||||||
|
name: 快速原型
|
||||||
|
goal: 最小成本验证方案可行性
|
||||||
|
instructions: 只写关键路径,不做边界打磨。
|
||||||
|
next: merge
|
||||||
|
- id: full-impl
|
||||||
|
kind: stage
|
||||||
|
name: 完整实现
|
||||||
|
goal: 完成正式代码修改
|
||||||
|
instructions: 小步快跑,优先根治不打补丁。
|
||||||
|
next: merge
|
||||||
|
- id: merge
|
||||||
|
kind: branch
|
||||||
|
name: 汇总
|
||||||
|
next:
|
||||||
|
- target: verify-gate
|
||||||
|
condition: ''
|
||||||
|
- id: verify-gate
|
||||||
|
kind: review
|
||||||
|
name: 验证审核
|
||||||
|
prompt: 验证证据是否真实可复现:构建与测试输出必须实际运行过
|
||||||
|
next: document
|
||||||
|
reject_to: impl-branch
|
||||||
|
max_rejects: 3
|
||||||
|
- id: document
|
||||||
|
kind: stage
|
||||||
|
name: 文档收尾
|
||||||
|
goal: 更新相关文档
|
||||||
|
instructions: 同步项目说明与相关文档。
|
||||||
|
next: end-1
|
||||||
|
- id: end-1
|
||||||
|
kind: end
|
||||||
|
name: 结束
|
||||||
|
---
|
||||||
|
|
||||||
|
## 工作方式
|
||||||
|
先理解再动手,禁止跳过验证。
|
||||||
|
|
||||||
|
## 验证方式
|
||||||
|
构建通过 + 相关测试通过。
|
||||||
|
|
||||||
|
## 结束方式
|
||||||
|
更新文档并汇报。
|
||||||
|
```
|
||||||
|
|
||||||
|
设计要点:分支的两条 `condition` 互斥可判断;并线器 `merge` 让两条实现路径汇合后共用同一套审核与收尾;审核驳回回到「实现策略」决策点,允许换条路重新实现。
|
||||||
98
agentskills/workflow-authoring/references/nodes.md
Normal file
98
agentskills/workflow-authoring/references/nodes.md
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
# 节点详解(五种 kind)
|
||||||
|
|
||||||
|
工作流拓扑为**串行**(无并行语义)。所有路由必须显式连接——`next` 留空会导致保存校验失败;任何路由不能指向 start 节点。
|
||||||
|
|
||||||
|
## start(入口,恰好 1 个)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- id: start-1
|
||||||
|
kind: start
|
||||||
|
name: 开始
|
||||||
|
next: explore # 必填:第一个节点的 id
|
||||||
|
```
|
||||||
|
|
||||||
|
## end(终点,至少 1 个)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- id: end-1
|
||||||
|
kind: end
|
||||||
|
name: 结束
|
||||||
|
```
|
||||||
|
|
||||||
|
无 `next`。流程推进到 end 即完成,执行者收到「输出总结后结束」的指令。可以有多个 end(不同路径收束到各自终点)。
|
||||||
|
|
||||||
|
## stage(执行阶段)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- id: explore
|
||||||
|
kind: stage
|
||||||
|
name: 代码探索
|
||||||
|
goal: 理解改动范围,产出影响面清单
|
||||||
|
instructions: 先读 git diff 总览,再按模块深入;禁止通读整文件。
|
||||||
|
next: verify-gate # 必填:可指向 stage/review/branch/end
|
||||||
|
```
|
||||||
|
|
||||||
|
- 阶段内执行者**完全自主**:自由调用工具、跑命令、读写文件;拓扑不约束阶段内行为
|
||||||
|
- `goal` 一句话目标;`instructions` 具体要求——两者在激活与每次推进时**全文呈现**给执行者,写关键约束,控制长度
|
||||||
|
- 审核节点把关时,`goal`/`instructions` 是「这个阶段应该做到什么」的标尺,写可检查的要求
|
||||||
|
- `max_stage_rounds` 限制单阶段轮数;撞限不中断流程,执行者会停下来向用户确认是否继续
|
||||||
|
|
||||||
|
## review(审核节点)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- id: verify-gate
|
||||||
|
kind: review
|
||||||
|
name: 验证审核
|
||||||
|
prompt: 验证证据是否真实可复现:构建与测试输出必须实际运行过
|
||||||
|
next: document # 必填:通过后的去向
|
||||||
|
reject_to: explore # 必填:驳回后回到哪里整改
|
||||||
|
max_rejects: 3 # 连续驳回上限,默认 3,必须 ≥ 1
|
||||||
|
```
|
||||||
|
|
||||||
|
- **瞬态节点**:阶段汇报到达时同步完成审核,通过/驳回立即走到下一站,流程不会在 review 上停留
|
||||||
|
- `prompt` 是写给审核者的**关注点**(重点查什么),不是流程描述;审核者能看到阶段执行痕迹与执行者的汇报
|
||||||
|
- 连续驳回达到 `max_rejects`,整个工作流以 failed 终止
|
||||||
|
- 审核服务异常按驳回处理(计入驳回计数)
|
||||||
|
- 想要「结束前总审核」:在 end 前显式放一个 review 节点,终点没有隐藏审核
|
||||||
|
|
||||||
|
## branch(分支 / 并线器)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# 多出线 = AI 决策点
|
||||||
|
- id: strategy
|
||||||
|
kind: branch
|
||||||
|
name: 策略选择
|
||||||
|
next:
|
||||||
|
- target: prototype
|
||||||
|
condition: 方案不确定或风险高,先低成本验证
|
||||||
|
- target: full-impl
|
||||||
|
condition: 需求明确、改动范围清晰,直接正式实现
|
||||||
|
|
||||||
|
# 单出线 = 并线器(自动穿过)
|
||||||
|
- id: merge
|
||||||
|
kind: branch
|
||||||
|
name: 汇总
|
||||||
|
next:
|
||||||
|
- target: verify-gate
|
||||||
|
condition: ''
|
||||||
|
```
|
||||||
|
|
||||||
|
- **多出线**:流程推进到此停下,执行者根据各出线的 `condition` 自主选择一条路径。`condition` 必填,是写给 AI 的决策依据——条件间互斥、可判断、覆盖常见情形
|
||||||
|
- **单出线**:并线器,自动穿过不做决策,`condition` 留空;典型用途是多条路径汇合后统一进 review 或 end
|
||||||
|
- 分支可以指向分支(连续决策),也可以回指形成循环(配合 review 驳回实现整改闭环)
|
||||||
|
|
||||||
|
## 通用字段说明
|
||||||
|
|
||||||
|
| 字段 | 说明 |
|
||||||
|
|---|---|
|
||||||
|
| `id` | 节点唯一标识(路由引用用它),建议 kebab-case |
|
||||||
|
| `name` | 显示名(进度展示用),可中文 |
|
||||||
|
| `kind` | 五种之一,缺省会按 stage 解析但应始终显式写 |
|
||||||
|
| `position` | 画布坐标,仅可视化编辑器使用;手写可省略,编辑器打开时会自动排版 |
|
||||||
|
|
||||||
|
## 语义速记(影响拓扑设计)
|
||||||
|
|
||||||
|
- 流程推进是「汇报驱动」:执行者完成当前阶段后主动汇报,引擎按当前阶段的 `next` 走向下一站
|
||||||
|
- review 驳回形成「整改循环」:`reject_to` 指回能真正整改的阶段(不一定是紧邻前一站)
|
||||||
|
- 已激活的运行实例按激活时的文档版本执行;修改文档后需重新激活才生效
|
||||||
|
- 工作流是执行者的辅助流程而非宿主:运行期间用户可自由穿插讨论,也可随时退出
|
||||||
172
agentskills/workflow-authoring/scripts/init_workflow.py
Normal file
172
agentskills/workflow-authoring/scripts/init_workflow.py
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Workflow Initializer - 生成新工作流的 WORKFLOW.md 模板
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
init_workflow.py <name> [--path <目录>] [--template linear|review|branch]
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
init_workflow.py my-pipeline
|
||||||
|
init_workflow.py code-check --path ./drafts --template review
|
||||||
|
init_workflow.py feature-flow --template branch
|
||||||
|
|
||||||
|
模板说明:
|
||||||
|
linear 线性流程:start → stage → stage → end
|
||||||
|
review 带审核:在实现阶段后加审核节点,驳回回到整改阶段
|
||||||
|
branch 带分支:策略分支(双出线决策点)+ 并线器 + 审核
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
NAME_RE = re.compile(r"^[a-z0-9][a-z0-9-]{0,63}$")
|
||||||
|
|
||||||
|
HEADER = """---
|
||||||
|
name: {name}
|
||||||
|
description: [一句话说明这个流程干什么]
|
||||||
|
review_mode: active
|
||||||
|
max_stage_rounds: 20
|
||||||
|
end_conditions: [什么算完成:验证标准与收尾动作]
|
||||||
|
nodes:
|
||||||
|
"""
|
||||||
|
|
||||||
|
FOOTER = """---
|
||||||
|
|
||||||
|
## 工作方式
|
||||||
|
[整体工作原则:各阶段通用的约束]
|
||||||
|
|
||||||
|
## 验证方式
|
||||||
|
[如何验证成果:要跑什么命令/检查什么证据]
|
||||||
|
|
||||||
|
## 结束方式
|
||||||
|
[收尾动作:文档、汇报等]
|
||||||
|
"""
|
||||||
|
|
||||||
|
TEMPLATES = {
|
||||||
|
"linear": """ - id: start-1
|
||||||
|
kind: start
|
||||||
|
name: 开始
|
||||||
|
next: step-1
|
||||||
|
- id: step-1
|
||||||
|
kind: stage
|
||||||
|
name: 阶段一
|
||||||
|
goal: [阶段目标,一句话]
|
||||||
|
instructions: [阶段要求与关键约束]
|
||||||
|
next: step-2
|
||||||
|
- id: step-2
|
||||||
|
kind: stage
|
||||||
|
name: 阶段二
|
||||||
|
goal: [阶段目标,一句话]
|
||||||
|
instructions: [阶段要求与关键约束]
|
||||||
|
next: end-1
|
||||||
|
- id: end-1
|
||||||
|
kind: end
|
||||||
|
name: 结束
|
||||||
|
""",
|
||||||
|
"review": """ - id: start-1
|
||||||
|
kind: start
|
||||||
|
name: 开始
|
||||||
|
next: implement
|
||||||
|
- id: implement
|
||||||
|
kind: stage
|
||||||
|
name: 实现
|
||||||
|
goal: [阶段目标,一句话]
|
||||||
|
instructions: [阶段要求与关键约束]
|
||||||
|
next: verify-gate
|
||||||
|
- id: fix
|
||||||
|
kind: stage
|
||||||
|
name: 整改
|
||||||
|
goal: 按审核意见修复问题
|
||||||
|
instructions: 逐条处理审核意见,说明每条的处置方式。
|
||||||
|
next: verify-gate
|
||||||
|
- id: verify-gate
|
||||||
|
kind: review
|
||||||
|
name: 验证审核
|
||||||
|
prompt: [审核关注点:重点检查什么]
|
||||||
|
next: end-1
|
||||||
|
reject_to: fix
|
||||||
|
max_rejects: 3
|
||||||
|
- id: end-1
|
||||||
|
kind: end
|
||||||
|
name: 结束
|
||||||
|
""",
|
||||||
|
"branch": """ - id: start-1
|
||||||
|
kind: start
|
||||||
|
name: 开始
|
||||||
|
next: prepare
|
||||||
|
- id: prepare
|
||||||
|
kind: stage
|
||||||
|
name: 准备
|
||||||
|
goal: [阶段目标,一句话]
|
||||||
|
instructions: [阶段要求与关键约束]
|
||||||
|
next: strategy
|
||||||
|
- id: strategy
|
||||||
|
kind: branch
|
||||||
|
name: 策略选择
|
||||||
|
next:
|
||||||
|
- target: path-a
|
||||||
|
condition: [走 A 路径的条件,写给 AI 的决策依据]
|
||||||
|
- target: path-b
|
||||||
|
condition: [走 B 路径的条件,与 A 互斥]
|
||||||
|
- id: path-a
|
||||||
|
kind: stage
|
||||||
|
name: 路径 A
|
||||||
|
goal: [阶段目标]
|
||||||
|
instructions: [阶段要求]
|
||||||
|
next: merge
|
||||||
|
- id: path-b
|
||||||
|
kind: stage
|
||||||
|
name: 路径 B
|
||||||
|
goal: [阶段目标]
|
||||||
|
instructions: [阶段要求]
|
||||||
|
next: merge
|
||||||
|
- id: merge
|
||||||
|
kind: branch
|
||||||
|
name: 汇总
|
||||||
|
next:
|
||||||
|
- target: verify-gate
|
||||||
|
condition: ''
|
||||||
|
- id: verify-gate
|
||||||
|
kind: review
|
||||||
|
name: 验证审核
|
||||||
|
prompt: [审核关注点]
|
||||||
|
next: end-1
|
||||||
|
reject_to: strategy
|
||||||
|
max_rejects: 3
|
||||||
|
- id: end-1
|
||||||
|
kind: end
|
||||||
|
name: 结束
|
||||||
|
""",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser(description="生成新工作流的 WORKFLOW.md 模板")
|
||||||
|
parser.add_argument("name", help="工作流名(小写字母/数字/连字符)")
|
||||||
|
parser.add_argument("--path", default=".", help="目标父目录(默认当前目录)")
|
||||||
|
parser.add_argument("--template", default="linear", choices=sorted(TEMPLATES), help="模板类型")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
name = args.name.strip()
|
||||||
|
if not NAME_RE.match(name):
|
||||||
|
print(f"错误:工作流名不合法 {name!r}(仅限小写字母/数字/连字符,首字符为字母或数字)")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
target_dir = Path(args.path).expanduser().resolve() / name
|
||||||
|
target_file = target_dir / "WORKFLOW.md"
|
||||||
|
if target_file.exists():
|
||||||
|
print(f"错误:{target_file} 已存在(不会覆盖)")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
target_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
content = HEADER.format(name=name) + TEMPLATES[args.template] + FOOTER
|
||||||
|
target_file.write_text(content, encoding="utf-8")
|
||||||
|
print(f"已创建 {target_file}")
|
||||||
|
print("下一步:填写 [方括号] 占位内容,然后运行 validate_workflow.py 自检。")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
159
agentskills/workflow-authoring/scripts/validate_workflow.py
Normal file
159
agentskills/workflow-authoring/scripts/validate_workflow.py
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Workflow Validator - 校验 WORKFLOW.md 的格式与结构
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
validate_workflow.py <WORKFLOW.md 或其所在目录>
|
||||||
|
|
||||||
|
校验内容:
|
||||||
|
- YAML frontmatter 存在且可解析;name 为合法 slug
|
||||||
|
- review_mode / max_stage_rounds 取值合法
|
||||||
|
- 节点:id 唯一、恰好 1 个 start、至少 1 个 end
|
||||||
|
- 各 kind 字段齐全(stage/review/branch/start 的必填项)
|
||||||
|
- 路由显式连接、指向存在的节点、不指向 start
|
||||||
|
- max_rejects 为 ≥1 的整数
|
||||||
|
|
||||||
|
退出码:0 = 通过;1 = 有 error 级问题。
|
||||||
|
保存工作流时服务端会做同等校验,本脚本用于保存前自检。
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
try:
|
||||||
|
import yaml
|
||||||
|
except ImportError:
|
||||||
|
print("错误:需要 PyYAML(pip install pyyaml)")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
NAME_RE = re.compile(r"^[a-z0-9][a-z0-9-]{0,63}$")
|
||||||
|
NODE_KINDS = ("start", "end", "stage", "review", "branch")
|
||||||
|
REVIEW_MODES = ("active", "readonly")
|
||||||
|
|
||||||
|
|
||||||
|
def locate(path_str: str) -> Path:
|
||||||
|
path = Path(path_str).expanduser().resolve()
|
||||||
|
if path.is_dir():
|
||||||
|
path = path / "WORKFLOW.md"
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def validate(text: str) -> list:
|
||||||
|
errors = []
|
||||||
|
match = re.match(r"^---\s*\n(.*?)\n---\s*\n?(.*)$", text, re.DOTALL)
|
||||||
|
if not match:
|
||||||
|
return ["WORKFLOW.md 缺少 YAML frontmatter(文件必须以 --- 包裹的 YAML 开头)"]
|
||||||
|
try:
|
||||||
|
meta = yaml.safe_load(match.group(1))
|
||||||
|
except yaml.YAMLError as exc:
|
||||||
|
return [f"YAML frontmatter 解析失败:{exc}"]
|
||||||
|
if not isinstance(meta, dict):
|
||||||
|
return ["YAML frontmatter 必须是键值对结构"]
|
||||||
|
|
||||||
|
name = str(meta.get("name") or "").strip()
|
||||||
|
if not name:
|
||||||
|
errors.append("缺少 name")
|
||||||
|
elif not NAME_RE.match(name):
|
||||||
|
errors.append(f"name 不合法:{name!r}(仅限小写字母/数字/连字符,首字符为字母或数字)")
|
||||||
|
|
||||||
|
review_mode = meta.get("review_mode")
|
||||||
|
if review_mode is not None and review_mode not in REVIEW_MODES:
|
||||||
|
errors.append(f"review_mode 只能是 active 或 readonly(当前 {review_mode!r})")
|
||||||
|
|
||||||
|
rounds = meta.get("max_stage_rounds")
|
||||||
|
if rounds is not None and (not isinstance(rounds, int) or rounds < 1):
|
||||||
|
errors.append(f"max_stage_rounds 必须是 ≥1 的整数(当前 {rounds!r})")
|
||||||
|
|
||||||
|
nodes = meta.get("nodes") or []
|
||||||
|
if not nodes:
|
||||||
|
errors.append("至少需要一个开始节点和一个结束节点")
|
||||||
|
return errors
|
||||||
|
|
||||||
|
by_id = {}
|
||||||
|
for n in nodes:
|
||||||
|
if not isinstance(n, dict):
|
||||||
|
errors.append("nodes 数组中每个元素必须是键值对结构")
|
||||||
|
continue
|
||||||
|
nid = n.get("id")
|
||||||
|
if not nid:
|
||||||
|
errors.append(f"存在缺少 id 的节点(kind={n.get('kind')!r})")
|
||||||
|
continue
|
||||||
|
if nid in by_id:
|
||||||
|
errors.append(f"节点 id 重复:{nid}")
|
||||||
|
by_id[nid] = n
|
||||||
|
if n.get("kind") not in NODE_KINDS:
|
||||||
|
errors.append(f"节点 {nid} 的 kind 非法:{n.get('kind')!r}(可选 {NODE_KINDS})")
|
||||||
|
|
||||||
|
starts = [n for n in by_id.values() if n.get("kind") == "start"]
|
||||||
|
ends = [n for n in by_id.values() if n.get("kind") == "end"]
|
||||||
|
if len(starts) == 0:
|
||||||
|
errors.append("缺少开始节点")
|
||||||
|
elif len(starts) > 1:
|
||||||
|
errors.append(f"开始节点只能有一个(当前 {len(starts)} 个)")
|
||||||
|
if not ends:
|
||||||
|
errors.append("缺少结束节点")
|
||||||
|
|
||||||
|
def check_ref(owner, target, label):
|
||||||
|
if target is None or target == "":
|
||||||
|
errors.append(f"{label}未连接")
|
||||||
|
elif target not in by_id:
|
||||||
|
errors.append(f"{label}指向不存在的节点:{target}")
|
||||||
|
elif by_id[target].get("kind") == "start":
|
||||||
|
errors.append(f"{label}不能指向开始节点")
|
||||||
|
|
||||||
|
for n in by_id.values():
|
||||||
|
kind = n.get("kind")
|
||||||
|
label_name = n.get("name") or n.get("id")
|
||||||
|
if kind == "start":
|
||||||
|
check_ref(n, n.get("next"), "开始节点")
|
||||||
|
elif kind == "stage":
|
||||||
|
if not str(n.get("goal") or "").strip():
|
||||||
|
errors.append(f"阶段「{label_name}」缺少 goal")
|
||||||
|
if not str(n.get("instructions") or "").strip():
|
||||||
|
errors.append(f"阶段「{label_name}」缺少 instructions")
|
||||||
|
check_ref(n, n.get("next"), f"阶段「{label_name}」")
|
||||||
|
elif kind == "review":
|
||||||
|
if not str(n.get("prompt") or "").strip():
|
||||||
|
errors.append(f"审核「{label_name}」缺少 prompt(审核关注点)")
|
||||||
|
check_ref(n, n.get("next"), f"审核「{label_name}」的通过路由")
|
||||||
|
check_ref(n, n.get("reject_to"), f"审核「{label_name}」的驳回路由")
|
||||||
|
max_rejects = n.get("max_rejects", 3)
|
||||||
|
if not isinstance(max_rejects, int) or max_rejects < 1:
|
||||||
|
errors.append(f"审核「{label_name}」驳回上限 max_rejects 必须 ≥ 1")
|
||||||
|
elif kind == "branch":
|
||||||
|
routes = n.get("next") or []
|
||||||
|
if not routes:
|
||||||
|
errors.append(f"分支「{label_name}」没有任何出线")
|
||||||
|
for r in routes:
|
||||||
|
if not isinstance(r, dict):
|
||||||
|
errors.append(f"分支「{label_name}」的出线必须是 target/condition 结构")
|
||||||
|
continue
|
||||||
|
check_ref(n, r.get("target"), f"分支「{label_name}」的出线")
|
||||||
|
if len(routes) > 1:
|
||||||
|
for r in routes:
|
||||||
|
if isinstance(r, dict) and not str(r.get("condition") or "").strip():
|
||||||
|
errors.append(f"分支「{label_name}」多出线的 condition 必填(AI 决策依据):→ {r.get('target')}")
|
||||||
|
return errors
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
if len(sys.argv) != 2:
|
||||||
|
print(__doc__)
|
||||||
|
return 1
|
||||||
|
target = locate(sys.argv[1])
|
||||||
|
if not target.exists():
|
||||||
|
print(f"错误:文件不存在 {target}")
|
||||||
|
return 1
|
||||||
|
errors = validate(target.read_text(encoding="utf-8"))
|
||||||
|
if errors:
|
||||||
|
print(f"校验未通过({len(errors)} 个问题):")
|
||||||
|
for e in errors:
|
||||||
|
print(f" - {e}")
|
||||||
|
return 1
|
||||||
|
print(f"校验通过:{target}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@ -57,5 +57,13 @@
|
|||||||
"custom functionality", "package", "skill development"
|
"custom functionality", "package", "skill development"
|
||||||
],
|
],
|
||||||
"hint": "检测到用户想要创建新的 skill。建议先阅读 skill-creator skill。"
|
"hint": "检测到用户想要创建新的 skill。建议先阅读 skill-creator skill。"
|
||||||
|
},
|
||||||
|
"workflow-authoring": {
|
||||||
|
"keywords": [
|
||||||
|
"工作流", "workflow", "WORKFLOW.md", "流程编排", "审核节点", "分支节点",
|
||||||
|
"创建工作流", "新建工作流", "修改工作流", "工作流文档", "save_workflow",
|
||||||
|
"create workflow", "workflow pipeline", "review node", "branch node"
|
||||||
|
],
|
||||||
|
"hint": "检测到用户可能需要创建或修改工作流(WORKFLOW.md)。如果情况符合,必须先阅读 workflow-authoring skill。"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,13 +1,15 @@
|
|||||||
"""工作流(Workflow)工具定义。
|
"""工作流(Workflow)工具定义。
|
||||||
|
|
||||||
5 个主智能体工具(定稿 docs/workflow_feature_plan.md §5):
|
7 个主智能体工具:
|
||||||
- activate_workflow / report_workflow_stage / choose_workflow_branch
|
- 运行时(定稿 docs/workflow_feature_plan.md §5):activate_workflow /
|
||||||
- get_workflow_status / deactivate_workflow
|
report_workflow_stage / choose_workflow_branch / get_workflow_status / deactivate_workflow
|
||||||
|
- 库管理:list_workflows(列表 / name 形态读原文)/ save_workflow(归档式创建与覆盖)
|
||||||
|
|
||||||
handler 分布:
|
handler 分布:
|
||||||
- activate / get_status / deactivate 走 tools_execution.py 常规链(无需 sender)
|
- activate / get_status / list_workflows / save_workflow 走 tools_execution.py 常规链(无需 sender)
|
||||||
- report_workflow_stage / choose_workflow_branch 走 chat_flow_tool_loop.py 特判
|
- report_workflow_stage / choose_workflow_branch / deactivate_workflow 走
|
||||||
(需要 sender 发审核进度事件、conversation_id 与 workspace)
|
chat_flow_tool_loop.py 特判(需要 sender 发审核进度/摘牌广播、conversation_id
|
||||||
|
与 workspace;deactivate 摘牌后需广播 {active: False} 让前端实时摘卡片)
|
||||||
"""
|
"""
|
||||||
from typing import Any, Dict, List
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
@ -115,4 +117,52 @@ class ToolsDefinitionWorkflowToolsMixin:
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": "list_workflows",
|
||||||
|
"description": (
|
||||||
|
"列出全部可用工作流(内置示例 + 用户创建),返回名称、描述、来源与节点数。"
|
||||||
|
"当需要告诉用户有哪些工作流可激活、或激活前确认工作流名是否存在时调用。"
|
||||||
|
"传入 name 时改为返回该工作流的完整定义文档(WORKFLOW.md 原文),"
|
||||||
|
"用于修改前读取现状。"
|
||||||
|
),
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": self._inject_intent({
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "可选。指定工作流名时返回其完整定义文档,而非列表。",
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": "save_workflow",
|
||||||
|
"description": (
|
||||||
|
"把已创建的工作流目录校验并归档到工作流库,使其可被 activate_workflow 激活。"
|
||||||
|
"先用 write_file 创建 <name>/WORKFLOW.md(格式与编写规范阅读 "
|
||||||
|
"workflow-authoring 技能,内含初始化与自检脚本),再调用本工具归档。"
|
||||||
|
"目录名必须与 WORKFLOW.md 的 name 字段一致;校验不通过返回完整错误清单且不归档;"
|
||||||
|
"归档成功后源目录被移除。"
|
||||||
|
),
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": self._inject_intent({
|
||||||
|
"source_dir": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "包含 WORKFLOW.md 的目录路径(工作区内,如 drafts/my-flow)。",
|
||||||
|
},
|
||||||
|
"overwrite": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "目标已存在(含与内置同名)时是否覆盖。默认 false:已存在则报错并提示。",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
"required": ["source_dir"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@ -219,6 +219,7 @@ class MainTerminalToolsExecutionMixin:
|
|||||||
"save_webpage",
|
"save_webpage",
|
||||||
"terminal_session",
|
"terminal_session",
|
||||||
"create_skill",
|
"create_skill",
|
||||||
|
"save_workflow",
|
||||||
}
|
}
|
||||||
|
|
||||||
# 扩展的只读命令白名单(包含常用管道命令)
|
# 扩展的只读命令白名单(包含常用管道命令)
|
||||||
@ -612,16 +613,62 @@ class MainTerminalToolsExecutionMixin:
|
|||||||
"message": build_status_text(data_dir=self.data_dir, conversation_id=str(conversation_id)),
|
"message": build_status_text(data_dir=self.data_dir, conversation_id=str(conversation_id)),
|
||||||
}
|
}
|
||||||
|
|
||||||
def _handle_deactivate_workflow_tool(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
|
def _handle_list_workflows_tool(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
from server.workflow_flow import deactivate_workflow
|
from modules.workflow_manager import list_workflows, read_workflow_markdown
|
||||||
conversation_id = getattr(self.context_manager, "current_conversation_id", None)
|
|
||||||
if not conversation_id:
|
name = str(arguments.get("name") or "").strip()
|
||||||
return {"success": False, "error": "当前没有打开的对话。"}
|
if name:
|
||||||
return deactivate_workflow(
|
try:
|
||||||
data_dir=self.data_dir,
|
text = read_workflow_markdown(name, self.data_dir)
|
||||||
conversation_id=str(conversation_id),
|
except FileNotFoundError as exc:
|
||||||
reason=str(arguments.get("reason") or ""),
|
return {"success": False, "error": str(exc)}
|
||||||
|
except ValueError as exc:
|
||||||
|
return {"success": False, "error": str(exc)}
|
||||||
|
return {"success": True, "workflow_name": name, "message": text}
|
||||||
|
|
||||||
|
items = list_workflows(self.data_dir)
|
||||||
|
if not items:
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"count": 0,
|
||||||
|
"workflows": [],
|
||||||
|
"message": "当前没有可用工作流。可阅读 workflow-authoring 技能后用 save_workflow 创建。",
|
||||||
|
}
|
||||||
|
lines = [f"可用工作流(共 {len(items)} 个):"]
|
||||||
|
for item in items:
|
||||||
|
src = "内置" if item.get("source") == "builtin" else "用户"
|
||||||
|
lines.append(
|
||||||
|
f"- {item.get('name')}:{item.get('description') or '(无描述)'}"
|
||||||
|
f"({src},{item.get('nodeCount')} 节点)"
|
||||||
)
|
)
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"count": len(items),
|
||||||
|
"workflows": items,
|
||||||
|
"message": "\n".join(lines),
|
||||||
|
}
|
||||||
|
|
||||||
|
def _handle_save_workflow_tool(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
from modules.workflow_manager import archive_workflow_directory
|
||||||
|
|
||||||
|
source = self._resolve_create_skill_source_dir(arguments.get("source_dir"))
|
||||||
|
if source is None:
|
||||||
|
return {"success": False, "error": "source_dir 不能为空"}
|
||||||
|
result = archive_workflow_directory(
|
||||||
|
source,
|
||||||
|
self.data_dir,
|
||||||
|
overwrite=bool(arguments.get("overwrite")),
|
||||||
|
)
|
||||||
|
if result.get("success"):
|
||||||
|
note = "(覆盖已有版本)" if result.get("overwritten") else ""
|
||||||
|
if result.get("shadows_builtin"):
|
||||||
|
note = "(用户副本,遮蔽同名内置工作流)"
|
||||||
|
result["summary"] = (
|
||||||
|
f"已归档工作流:{result.get('workflow_name')}"
|
||||||
|
f"({result.get('node_count')} 节点){note}。"
|
||||||
|
"可使用 activate_workflow 激活。"
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
def _handle_update_project_memory(self, name: str, description: str, content: str) -> Dict[str, Any]:
|
def _handle_update_project_memory(self, name: str, description: str, content: str) -> Dict[str, Any]:
|
||||||
"""处理 update_project_memory:写入 .astrion/memory/{name}.md"""
|
"""处理 update_project_memory:写入 .astrion/memory/{name}.md"""
|
||||||
@ -1178,8 +1225,10 @@ class MainTerminalToolsExecutionMixin:
|
|||||||
result = self._handle_activate_workflow_tool(arguments)
|
result = self._handle_activate_workflow_tool(arguments)
|
||||||
elif tool_name == "get_workflow_status":
|
elif tool_name == "get_workflow_status":
|
||||||
result = self._handle_get_workflow_status_tool(arguments)
|
result = self._handle_get_workflow_status_tool(arguments)
|
||||||
elif tool_name == "deactivate_workflow":
|
elif tool_name == "list_workflows":
|
||||||
result = self._handle_deactivate_workflow_tool(arguments)
|
result = self._handle_list_workflows_tool(arguments)
|
||||||
|
elif tool_name == "save_workflow":
|
||||||
|
result = self._handle_save_workflow_tool(arguments)
|
||||||
elif tool_name in {"vlm_analyze", "ocr_image"}:
|
elif tool_name in {"vlm_analyze", "ocr_image"}:
|
||||||
path = arguments.get("path")
|
path = arguments.get("path")
|
||||||
prompt = arguments.get("prompt")
|
prompt = arguments.get("prompt")
|
||||||
|
|||||||
@ -326,3 +326,123 @@ def delete_workflow(name: str, data_dir: str | Path | None) -> None:
|
|||||||
raise ValueError("内置示例不可删除(可复制为用户工作流后删除副本)")
|
raise ValueError("内置示例不可删除(可复制为用户工作流后删除副本)")
|
||||||
raise FileNotFoundError(f"工作流不存在:{name}")
|
raise FileNotFoundError(f"工作流不存在:{name}")
|
||||||
shutil.rmtree(target_dir)
|
shutil.rmtree(target_dir)
|
||||||
|
|
||||||
|
|
||||||
|
def read_workflow_markdown(name: str, data_dir: str | Path | None) -> str:
|
||||||
|
"""读取工作流 WORKFLOW.md 原文(用户库优先,其次内置)。供 list_workflows 工具 name 形态。"""
|
||||||
|
_safe_name(name)
|
||||||
|
user_root = infer_user_workflows_dir(data_dir)
|
||||||
|
candidates: List[Path] = []
|
||||||
|
if user_root:
|
||||||
|
candidates.append(_workflow_file(user_root, name))
|
||||||
|
candidates.append(_workflow_file(BUILTIN_WORKFLOWS_DIR, name))
|
||||||
|
for wf_file in candidates:
|
||||||
|
if wf_file.exists():
|
||||||
|
return wf_file.read_text(encoding="utf-8")
|
||||||
|
raise FileNotFoundError(f"工作流不存在:{name}")
|
||||||
|
|
||||||
|
|
||||||
|
def archive_workflow_directory(
|
||||||
|
source_dir: str | Path,
|
||||||
|
data_dir: str | Path | None,
|
||||||
|
*,
|
||||||
|
overwrite: bool = False,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""把含有 WORKFLOW.md 的目录校验并归档到用户工作流库(对齐 archive_skill_directory)。
|
||||||
|
|
||||||
|
规则(与 save_workflow 工具设计定稿一致):
|
||||||
|
- 目录名必须与 frontmatter 的 name 字段一致
|
||||||
|
- overwrite=false 时:用户库已存在同名 → 报错;与内置同名 → 报错(提示将遮蔽内置)
|
||||||
|
- 覆盖前先备份旧目录,移动失败自动恢复
|
||||||
|
- 成功后源目录随 move 移除
|
||||||
|
"""
|
||||||
|
source = Path(source_dir).expanduser().resolve()
|
||||||
|
if not source.exists() or not source.is_dir():
|
||||||
|
return {"success": False, "error": "source_dir 不是目录"}
|
||||||
|
wf_file = source / WORKFLOW_FILENAME
|
||||||
|
if not wf_file.exists() or not wf_file.is_file():
|
||||||
|
return {"success": False, "error": f"目录中缺少 {WORKFLOW_FILENAME}"}
|
||||||
|
|
||||||
|
try:
|
||||||
|
wf = workflow_from_markdown(wf_file.read_text(encoding="utf-8"), "user")
|
||||||
|
except Exception as exc:
|
||||||
|
return {"success": False, "error": f"WORKFLOW.md 解析失败:{exc}"}
|
||||||
|
|
||||||
|
raw_name = str(wf.get("name") or "").strip()
|
||||||
|
try:
|
||||||
|
name = _safe_name(raw_name)
|
||||||
|
except ValueError as exc:
|
||||||
|
return {"success": False, "error": str(exc)}
|
||||||
|
if source.name != name:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": f"目录名({source.name})必须与 WORKFLOW.md 的 name 字段({name})一致",
|
||||||
|
}
|
||||||
|
|
||||||
|
errors = validate_structure(wf)
|
||||||
|
if errors:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": "结构校验未通过:" + ";".join(errors),
|
||||||
|
"validation_errors": errors,
|
||||||
|
"workflow_name": name,
|
||||||
|
}
|
||||||
|
|
||||||
|
root = infer_user_workflows_dir(data_dir)
|
||||||
|
if not root:
|
||||||
|
return {"success": False, "error": "无法确定用户工作流库目录"}
|
||||||
|
root = root.resolve()
|
||||||
|
target = (root / name).resolve()
|
||||||
|
if not str(target).startswith(str(root)):
|
||||||
|
return {"success": False, "error": "非法路径"}
|
||||||
|
|
||||||
|
existed_user = target.exists()
|
||||||
|
existed_builtin = _workflow_file(BUILTIN_WORKFLOWS_DIR, name).exists()
|
||||||
|
if not overwrite:
|
||||||
|
if existed_user:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": f"工作流「{name}」已存在。确认覆盖请设 overwrite=true。",
|
||||||
|
"already_exists": True,
|
||||||
|
"workflow_name": name,
|
||||||
|
}
|
||||||
|
if existed_builtin:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": (
|
||||||
|
f"与内置工作流「{name}」同名。归档后将创建用户副本遮蔽内置版本,"
|
||||||
|
"确认请设 overwrite=true。"
|
||||||
|
),
|
||||||
|
"builtin_conflict": True,
|
||||||
|
"workflow_name": name,
|
||||||
|
}
|
||||||
|
|
||||||
|
backup: Optional[Path] = None
|
||||||
|
if existed_user:
|
||||||
|
backup = root / f".{name}.backup-{int(datetime.now().timestamp())}"
|
||||||
|
try:
|
||||||
|
target.rename(backup)
|
||||||
|
except Exception as exc:
|
||||||
|
return {"success": False, "error": f"覆盖前备份旧版本失败:{exc}"}
|
||||||
|
try:
|
||||||
|
shutil.move(str(source), str(target))
|
||||||
|
except Exception as exc:
|
||||||
|
if backup is not None and backup.exists() and not target.exists():
|
||||||
|
try:
|
||||||
|
backup.rename(target)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return {"success": False, "error": f"归档移动失败:{exc}", "workflow_name": name}
|
||||||
|
if backup is not None and backup.exists():
|
||||||
|
try:
|
||||||
|
shutil.rmtree(backup)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"workflow_name": name,
|
||||||
|
"node_count": len(wf.get("nodes") or []),
|
||||||
|
"overwritten": existed_user,
|
||||||
|
"shadows_builtin": (not existed_user) and existed_builtin,
|
||||||
|
}
|
||||||
|
|||||||
@ -48,6 +48,11 @@ def _runtime_message_ui_defaults(src: str, *, inline: bool = False) -> Dict[str,
|
|||||||
# 确保运行期直接渲染与刷新后从历史加载行为相同。
|
# 确保运行期直接渲染与刷新后从历史加载行为相同。
|
||||||
if normalized in {"sub_agent", "background_command", "compression", "compression_handoff"}:
|
if normalized in {"sub_agent", "background_command", "compression", "compression_handoff"}:
|
||||||
return {"visibility": "compact", "starts_work": False}
|
return {"visibility": "compact", "starts_work": False}
|
||||||
|
# 工作流运行期通知(退出/柔性通知 inline 注入):延续当前工作段的紧凑通知,
|
||||||
|
# 与其他系统通知一致走 compact 渲染;闲时任务入口派发走 task_main 的
|
||||||
|
# _user_message_ui_defaults(chat + starts_work=True,开启新一轮工作段)。
|
||||||
|
if normalized == "workflow":
|
||||||
|
return {"visibility": "compact", "starts_work": False}
|
||||||
# 目标审核续命:开启新一轮工作。
|
# 目标审核续命:开启新一轮工作。
|
||||||
if normalized == "goal_review":
|
if normalized == "goal_review":
|
||||||
return {"visibility": "compact", "starts_work": True}
|
return {"visibility": "compact", "starts_work": True}
|
||||||
|
|||||||
@ -278,11 +278,12 @@ async def _wait_for_plan_approval(*, approval_id: str, username: str, timeout_se
|
|||||||
|
|
||||||
|
|
||||||
async def _handle_workflow_tool(*, function_name: str, web_terminal, arguments, sender, workspace, messages, conversation_id: Optional[str]) -> str:
|
async def _handle_workflow_tool(*, function_name: str, web_terminal, arguments, sender, workspace, messages, conversation_id: Optional[str]) -> str:
|
||||||
"""report_workflow_stage / choose_workflow_branch 的工具循环特判 handler。
|
"""report_workflow_stage / choose_workflow_branch / deactivate_workflow 的工具循环特判 handler。
|
||||||
|
|
||||||
推进矩阵需要 sender(审核进度事件)、workspace 与 conversation_id,
|
推进矩阵需要 sender(审核进度事件)、workspace 与 conversation_id,
|
||||||
与 submit_plan 一样在工具循环层处理而非 tools_execution 常规链。
|
与 submit_plan 一样在工具循环层处理而非 tools_execution 常规链。
|
||||||
审核在工具内同步 await(handler 支持长阻塞,submit_plan 已验证该模式)。
|
审核在工具内同步 await(handler 支持长阻塞,submit_plan 已验证该模式)。
|
||||||
|
deactivate 摘牌后需要 sender 广播 {active: False} 快照(前端实时摘卡片)。
|
||||||
推进成功后同步刷新 messages 里的工作流 system 段(当前位置不滞后)。
|
推进成功后同步刷新 messages 里的工作流 system 段(当前位置不滞后)。
|
||||||
"""
|
"""
|
||||||
from server import workflow_flow
|
from server import workflow_flow
|
||||||
@ -297,7 +298,7 @@ async def _handle_workflow_tool(*, function_name: str, web_terminal, arguments,
|
|||||||
conversation_id=conversation_id,
|
conversation_id=conversation_id,
|
||||||
summary=str(args.get("summary") or ""),
|
summary=str(args.get("summary") or ""),
|
||||||
)
|
)
|
||||||
else:
|
elif function_name == "choose_workflow_branch":
|
||||||
result = await workflow_flow.handle_branch_choice(
|
result = await workflow_flow.handle_branch_choice(
|
||||||
web_terminal=web_terminal,
|
web_terminal=web_terminal,
|
||||||
data_dir=workspace.data_dir,
|
data_dir=workspace.data_dir,
|
||||||
@ -305,6 +306,13 @@ async def _handle_workflow_tool(*, function_name: str, web_terminal, arguments,
|
|||||||
conversation_id=conversation_id,
|
conversation_id=conversation_id,
|
||||||
target_node_id=str(args.get("target_node_id") or ""),
|
target_node_id=str(args.get("target_node_id") or ""),
|
||||||
)
|
)
|
||||||
|
else: # deactivate_workflow
|
||||||
|
result = workflow_flow.deactivate_workflow(
|
||||||
|
data_dir=workspace.data_dir,
|
||||||
|
conversation_id=conversation_id,
|
||||||
|
reason=str(args.get("reason") or ""),
|
||||||
|
sender=sender,
|
||||||
|
)
|
||||||
if isinstance(result, dict) and result.get("success"):
|
if isinstance(result, dict) and result.get("success"):
|
||||||
workflow_flow.refresh_workflow_system_segment(
|
workflow_flow.refresh_workflow_system_segment(
|
||||||
messages, data_dir=workspace.data_dir, conversation_id=conversation_id
|
messages, data_dir=workspace.data_dir, conversation_id=conversation_id
|
||||||
@ -960,7 +968,7 @@ async def _execute_tool_calls_impl(*, web_terminal, tool_calls, sender, messages
|
|||||||
conversation_id=conversation_id,
|
conversation_id=conversation_id,
|
||||||
tool_call_id=str(tool_call_id) if tool_call_id else None,
|
tool_call_id=str(tool_call_id) if tool_call_id else None,
|
||||||
)
|
)
|
||||||
elif function_name in ("report_workflow_stage", "choose_workflow_branch"):
|
elif function_name in ("report_workflow_stage", "choose_workflow_branch", "deactivate_workflow"):
|
||||||
tool_result = await _handle_workflow_tool(
|
tool_result = await _handle_workflow_tool(
|
||||||
function_name=function_name,
|
function_name=function_name,
|
||||||
web_terminal=web_terminal,
|
web_terminal=web_terminal,
|
||||||
|
|||||||
@ -647,13 +647,19 @@ async def run_stage_review(
|
|||||||
# ---------------------------------------------------------------- 退出 / 通知文本
|
# ---------------------------------------------------------------- 退出 / 通知文本
|
||||||
|
|
||||||
|
|
||||||
def deactivate_workflow(*, data_dir, conversation_id: str, reason: str) -> Dict[str, Any]:
|
def deactivate_workflow(*, data_dir, conversation_id: str, reason: str, sender=None) -> Dict[str, Any]:
|
||||||
"""模型自主退出(deactivate_workflow 工具):摘牌,工具返回闭环,不发 user 通知。"""
|
"""模型自主退出(deactivate_workflow 工具):摘牌,工具返回闭环,不发 user 通知。"""
|
||||||
|
if not conversation_id:
|
||||||
|
return {"success": False, "error": "当前没有打开的对话。"}
|
||||||
wsm = get_active_manager(data_dir, conversation_id)
|
wsm = get_active_manager(data_dir, conversation_id)
|
||||||
if wsm is None:
|
if wsm is None:
|
||||||
return {"success": False, "error": "当前对话没有激活的工作流。"}
|
return {"success": False, "error": "当前对话没有激活的工作流。"}
|
||||||
name = str(wsm.state.get("workflow_name") or "")
|
name = str(wsm.state.get("workflow_name") or "")
|
||||||
wsm.deactivate(status=STATUS_STOPPED, reason=REASON_MODEL)
|
wsm.deactivate(status=STATUS_STOPPED, reason=REASON_MODEL)
|
||||||
|
# 摘牌后广播 {active: False} 快照(此时 progress_snapshot 只剩 active=False):
|
||||||
|
# 前端实时摘除快捷窗口卡片与 slash 菜单「进行中」状态,
|
||||||
|
# 否则只能等下次刷新/切换对话静态校正才消失。
|
||||||
|
emit_workflow_progress(wsm=wsm, sender=sender, conversation_id=conversation_id)
|
||||||
note = str(reason or "").strip()
|
note = str(reason or "").strip()
|
||||||
return {
|
return {
|
||||||
"success": True,
|
"success": True,
|
||||||
@ -671,7 +677,7 @@ def deactivate_workflow_by_user(*, data_dir, conversation_id: str) -> Dict[str,
|
|||||||
wsm.push_notice(
|
wsm.push_notice(
|
||||||
notice_type="deactivated_by_user",
|
notice_type="deactivated_by_user",
|
||||||
message=(
|
message=(
|
||||||
f"用户已通过快捷操作退出工作流「{name}」。工作流已摘牌,无需继续按流程推进;"
|
f"用户已退出工作流「{name}」。工作流已摘牌,无需继续按流程推进;"
|
||||||
"你可以继续自由工作。若用户之后要求恢复,可重新激活。"
|
"你可以继续自由工作。若用户之后要求恢复,可重新激活。"
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@ -152,7 +152,7 @@ def api_activate_workflow(terminal, workspace, username):
|
|||||||
|
|
||||||
activation_text = str(result.get("text") or "")
|
activation_text = str(result.get("text") or "")
|
||||||
prompt = (
|
prompt = (
|
||||||
"用户已通过快捷菜单激活工作流,请立即开始按流程执行。"
|
"工作流已激活,请立即开始按流程执行。"
|
||||||
"完成当前步骤后调用 report_workflow_stage(summary) 汇报。\n\n"
|
"完成当前步骤后调用 report_workflow_stage(summary) 汇报。\n\n"
|
||||||
f"{activation_text}"
|
f"{activation_text}"
|
||||||
)
|
)
|
||||||
|
|||||||
@ -87,6 +87,15 @@ export const modeMethods = {
|
|||||||
if (!normalized || normalized === this.currentConversationId) {
|
if (!normalized || normalized === this.currentConversationId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// 跳转前把当前输入内容(slash 触发符已被 deleteSlashToken 删除)立即落盘。
|
||||||
|
// 草稿是全局单一份(/api/input-draft 不按对话隔离),而 enterConversation 会经
|
||||||
|
// currentConversationId watcher 触发 restoreComposerDraftState;若只等 1s debounce,
|
||||||
|
// 恢复拿到的仍是删除前的旧草稿,已删除的 "/" 会被重新写回输入框。
|
||||||
|
try {
|
||||||
|
await this.persistComposerDraftNow({ reason: 'workflow-activated', force: true });
|
||||||
|
} catch (_e) {
|
||||||
|
// 落盘失败不阻断进入对话
|
||||||
|
}
|
||||||
// 侧边栏列表先插入占位(对齐 send.ts 首条消息创建对话的行为),否则列表要待下次刷新才出现
|
// 侧边栏列表先插入占位(对齐 send.ts 首条消息创建对话的行为),否则列表要待下次刷新才出现
|
||||||
try {
|
try {
|
||||||
const newPlaceholder = {
|
const newPlaceholder = {
|
||||||
|
|||||||
@ -92,6 +92,11 @@ export const routeMethods = {
|
|||||||
this.titleReady = true;
|
this.titleReady = true;
|
||||||
this.suppressTitleTyping = false;
|
this.suppressTitleTyping = false;
|
||||||
this.startTitleTyping(this.currentConversationTitle, { animate: false });
|
this.startTitleTyping(this.currentConversationTitle, { animate: false });
|
||||||
|
// 刷新路径不经 loadConversation,需在此补拉 token 统计,
|
||||||
|
// 否则输入栏右下角上下文用量圆环保持 0(enterConversation 内
|
||||||
|
// skipConversationHistoryReload 会让 watcher 也跳过拉取)
|
||||||
|
this.fetchConversationTokenStatistics();
|
||||||
|
this.updateCurrentContextTokens();
|
||||||
} else {
|
} else {
|
||||||
history.replaceState({}, '', '/new');
|
history.replaceState({}, '', '/new');
|
||||||
this.currentConversationId = null;
|
this.currentConversationId = null;
|
||||||
|
|||||||
@ -94,6 +94,10 @@ export function renderEnhancedToolResult(
|
|||||||
return renderReadFile(result, args);
|
return renderReadFile(result, args);
|
||||||
} else if (name === 'create_skill') {
|
} else if (name === 'create_skill') {
|
||||||
return renderCreateSkill(result);
|
return renderCreateSkill(result);
|
||||||
|
} else if (name === 'list_workflows') {
|
||||||
|
return renderListWorkflows(result, args);
|
||||||
|
} else if (name === 'save_workflow') {
|
||||||
|
return renderSaveWorkflow(result, args);
|
||||||
} else if (name === 'vlm_analyze') {
|
} else if (name === 'vlm_analyze') {
|
||||||
return renderVlmAnalyze(result, args);
|
return renderVlmAnalyze(result, args);
|
||||||
} else if (name === 'ocr_image') {
|
} else if (name === 'ocr_image') {
|
||||||
@ -1164,6 +1168,82 @@ function renderCreateSkill(result: any): string {
|
|||||||
return html;
|
return html;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderListWorkflows(result: any, args: any): string {
|
||||||
|
if (!result?.success) {
|
||||||
|
const error = result?.error ?? '查询失败';
|
||||||
|
return `<div class="tool-result-error">⚠️ ${escapeHtml(String(error))}</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// name 形态:展示该工作流的完整定义文档
|
||||||
|
const detailName = String(args?.name || result?.workflow_name || '');
|
||||||
|
if (detailName) {
|
||||||
|
let html = '<div class="tool-result-meta">';
|
||||||
|
html += `<div><strong>工作流:</strong>${escapeHtml(detailName)}</div>`;
|
||||||
|
html += '</div>';
|
||||||
|
const doc = String(result?.message || '');
|
||||||
|
if (doc) {
|
||||||
|
html += '<div class="tool-result-content scrollable">';
|
||||||
|
html += `<pre>${escapeHtml(doc)}</pre>`;
|
||||||
|
html += '</div>';
|
||||||
|
}
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 列表形态
|
||||||
|
const items = Array.isArray(result?.workflows) ? result.workflows : [];
|
||||||
|
if (items.length === 0) {
|
||||||
|
return '<div class="tool-result-empty">当前没有可用工作流。</div>';
|
||||||
|
}
|
||||||
|
let html = '<div class="tool-result-meta">';
|
||||||
|
html += `<div><strong>工作流数量:</strong>${items.length}</div>`;
|
||||||
|
html += '</div>';
|
||||||
|
html += '<div class="search-result-list">';
|
||||||
|
items.forEach((item: any) => {
|
||||||
|
const name = escapeHtml(String(item?.name || '未命名'));
|
||||||
|
const desc = escapeHtml(String(item?.description || '(无描述)'));
|
||||||
|
const source = item?.source === 'builtin' ? '内置' : '用户';
|
||||||
|
const nodeCount = Number(item?.nodeCount || 0);
|
||||||
|
html += '<div class="search-result-item">';
|
||||||
|
html += `<div class="search-result-title">${name}</div>`;
|
||||||
|
html += `<div>${desc}</div>`;
|
||||||
|
html += `<div><strong>来源:</strong>${source} | <strong>节点:</strong>${nodeCount}</div>`;
|
||||||
|
html += '</div>';
|
||||||
|
});
|
||||||
|
html += '</div>';
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSaveWorkflow(result: any, args: any): string {
|
||||||
|
const status = formatToolStatusLabel(result, '✓ 已归档');
|
||||||
|
let html = '<div class="tool-result-meta">';
|
||||||
|
html += `<div><strong>状态:</strong>${status}</div>`;
|
||||||
|
const sourceDir = String(args?.source_dir || '');
|
||||||
|
if (sourceDir) {
|
||||||
|
html += `<div><strong>源目录:</strong>${escapeHtml(sourceDir)}</div>`;
|
||||||
|
}
|
||||||
|
if (result?.workflow_name) {
|
||||||
|
html += `<div><strong>工作流:</strong>${escapeHtml(String(result.workflow_name))}</div>`;
|
||||||
|
}
|
||||||
|
if (result?.success && typeof result?.node_count === 'number') {
|
||||||
|
html += `<div><strong>节点数:</strong>${result.node_count}</div>`;
|
||||||
|
}
|
||||||
|
if (result?.success && result?.overwritten) {
|
||||||
|
html += '<div><strong>模式:</strong>覆盖已有版本</div>';
|
||||||
|
} else if (result?.success && result?.shadows_builtin) {
|
||||||
|
html += '<div><strong>模式:</strong>用户副本(遮蔽同名内置)</div>';
|
||||||
|
}
|
||||||
|
html += '</div>';
|
||||||
|
if (!result?.success && result?.error) {
|
||||||
|
html += `<div class="tool-result-error">${escapeHtml(String(result.error))}</div>`;
|
||||||
|
}
|
||||||
|
if (result?.success && result?.summary) {
|
||||||
|
html += '<div class="tool-result-content">';
|
||||||
|
html += escapeHtml(String(result.summary));
|
||||||
|
html += '</div>';
|
||||||
|
}
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
function formatPersonalizationFieldLabel(field: string): string {
|
function formatPersonalizationFieldLabel(field: string): string {
|
||||||
const labelMap: Record<string, string> = {
|
const labelMap: Record<string, string> = {
|
||||||
self_identify: 'AI 自称',
|
self_identify: 'AI 自称',
|
||||||
|
|||||||
@ -401,6 +401,31 @@ watch(
|
|||||||
{ flush: 'sync', immediate: true }
|
{ flush: 'sync', immediate: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 退出动画(deactivate 广播 / slash 退出):store 收到 live 的 active=false 时
|
||||||
|
// 置 exiting 并保留快照,此处播整窗退出动画后调 finishExit 真正清空。
|
||||||
|
// 动画期间 QuickDock 的 hasContent 因 exiting 保持展开,容器不会提前收起吞动画。
|
||||||
|
watch(
|
||||||
|
() => workflowStore.exiting,
|
||||||
|
async (exiting) => {
|
||||||
|
if (!exiting) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const myGen = ++gen;
|
||||||
|
await playWindowExit(myGen);
|
||||||
|
if (myGen !== gen) {
|
||||||
|
// 被新快照/对话切换打断:复位退出态,交给 snapshot watch 处理
|
||||||
|
windowExiting.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
windowExiting.value = false;
|
||||||
|
rows.value = [];
|
||||||
|
footnote.value = null;
|
||||||
|
visible.value = false;
|
||||||
|
workflowStore.finishExit();
|
||||||
|
},
|
||||||
|
{ flush: 'sync' }
|
||||||
|
);
|
||||||
|
|
||||||
// 脚注进入动画标志
|
// 脚注进入动画标志
|
||||||
watch(
|
watch(
|
||||||
() => snapshot.value.footnote,
|
() => snapshot.value.footnote,
|
||||||
|
|||||||
@ -178,8 +178,14 @@ export const useQuickDockStore = defineStore('quickDock', {
|
|||||||
bgStore.commands.length > 0 ||
|
bgStore.commands.length > 0 ||
|
||||||
state.editedFiles.length > 0;
|
state.editedFiles.length > 0;
|
||||||
// 乐观掩码生效期间(初始加载/切换对话的内容未到齐窗口)以假定状态为准;
|
// 乐观掩码生效期间(初始加载/切换对话的内容未到齐窗口)以假定状态为准;
|
||||||
// 掩码关闭后纯真实状态
|
// 掩码关闭后纯真实状态。
|
||||||
return workflowStore.isActive || (state.assumedActive ? state.assumedContent : restReal);
|
// workflowStore.exiting:退出动画播放期间保持容器展开,否则容器 300ms
|
||||||
|
// 收起会吞掉窗口自身的 240ms 退出动画(动画播完 finishExit 后才真正清空)
|
||||||
|
return (
|
||||||
|
workflowStore.isActive ||
|
||||||
|
workflowStore.exiting ||
|
||||||
|
(state.assumedActive ? state.assumedContent : restReal)
|
||||||
|
);
|
||||||
},
|
},
|
||||||
/** 实际处于展开态:有内容且未被用户手动收起 */
|
/** 实际处于展开态:有内容且未被用户手动收起 */
|
||||||
expanded(state): boolean {
|
expanded(state): boolean {
|
||||||
|
|||||||
@ -44,6 +44,13 @@ interface WorkflowState {
|
|||||||
snapshot: WorkflowSnapshot;
|
snapshot: WorkflowSnapshot;
|
||||||
/** true = 来自任务期实时事件(播动画);false = 来自加载/恢复(静态呈现) */
|
/** true = 来自任务期实时事件(播动画);false = 来自加载/恢复(静态呈现) */
|
||||||
live: boolean;
|
live: boolean;
|
||||||
|
/**
|
||||||
|
* 退出动画进行中:收到 live 的 active=false(deactivate 广播 / slash 退出)时
|
||||||
|
* 不立即清空,保留快照供窗口播退出动画,播完由窗口调 finishExit 真正清空。
|
||||||
|
* QuickDock 的 hasContent 读取此标志,动画期间保持容器展开(否则容器 300ms
|
||||||
|
* 收起会吞掉窗口自身的 240ms 退出动画)。
|
||||||
|
*/
|
||||||
|
exiting: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const emptySnapshot = (): WorkflowSnapshot => ({
|
const emptySnapshot = (): WorkflowSnapshot => ({
|
||||||
@ -60,7 +67,8 @@ const emptySnapshot = (): WorkflowSnapshot => ({
|
|||||||
export const useWorkflowStore = defineStore('workflow', {
|
export const useWorkflowStore = defineStore('workflow', {
|
||||||
state: (): WorkflowState => ({
|
state: (): WorkflowState => ({
|
||||||
snapshot: emptySnapshot(),
|
snapshot: emptySnapshot(),
|
||||||
live: false
|
live: false,
|
||||||
|
exiting: false
|
||||||
}),
|
}),
|
||||||
getters: {
|
getters: {
|
||||||
isActive(state): boolean {
|
isActive(state): boolean {
|
||||||
@ -72,15 +80,40 @@ export const useWorkflowStore = defineStore('workflow', {
|
|||||||
setWorkflow(snapshot: Partial<WorkflowSnapshot> | null | undefined, live = false) {
|
setWorkflow(snapshot: Partial<WorkflowSnapshot> | null | undefined, live = false) {
|
||||||
this.live = live;
|
this.live = live;
|
||||||
if (!snapshot || snapshot.active !== true) {
|
if (!snapshot || snapshot.active !== true) {
|
||||||
|
// 退出动画进行中再收到停用事件(如 slash 退出后摘牌广播又到):忽略,
|
||||||
|
// 等动画播完由 finishExit 一次性清空。
|
||||||
|
// 例外:静态校正(切换对话/刷新恢复 live=false)优先,直接清空并打断动画
|
||||||
|
if (this.exiting) {
|
||||||
|
if (!live) {
|
||||||
|
this.exiting = false;
|
||||||
|
this.snapshot = emptySnapshot();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// live 事件驱动的消失:标记 exiting 保留快照,窗口播退出动画;
|
||||||
|
// 静态校正(切换对话/刷新恢复 live=false):瞬间清空不播动画
|
||||||
|
if (live && this.snapshot.active) {
|
||||||
|
this.exiting = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.snapshot = emptySnapshot();
|
this.snapshot = emptySnapshot();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// 新活跃快照到达:中止可能进行中的退出流程(快速重新激活场景)
|
||||||
|
this.exiting = false;
|
||||||
this.snapshot = { ...emptySnapshot(), ...snapshot, active: true };
|
this.snapshot = { ...emptySnapshot(), ...snapshot, active: true };
|
||||||
},
|
},
|
||||||
|
/** 窗口退出动画播完后由 WorkflowWindow 调用:真正清空状态 */
|
||||||
|
finishExit() {
|
||||||
|
this.exiting = false;
|
||||||
|
this.live = false;
|
||||||
|
this.snapshot = emptySnapshot();
|
||||||
|
},
|
||||||
/** 对话切换 / 离开对话时清空 */
|
/** 对话切换 / 离开对话时清空 */
|
||||||
reset() {
|
reset() {
|
||||||
this.snapshot = emptySnapshot();
|
this.snapshot = emptySnapshot();
|
||||||
this.live = false;
|
this.live = false;
|
||||||
|
this.exiting = false;
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* 对话切换时的状态回填:拉取目标对话的工作流状态覆盖本地(live=false 静态校正)。
|
* 对话切换时的状态回填:拉取目标对话的工作流状态覆盖本地(live=false 静态校正)。
|
||||||
|
|||||||
@ -180,8 +180,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.workflow-icon svg {
|
.workflow-icon svg {
|
||||||
width: 18px;
|
width: 20px;
|
||||||
height: 18px;
|
height: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.monitor-mode-btn.blocked {
|
.monitor-mode-btn.blocked {
|
||||||
|
|||||||
@ -10,7 +10,10 @@ const COMPACT_FALLBACK_SOURCES = new Set([
|
|||||||
'compression',
|
'compression',
|
||||||
'compression_handoff',
|
'compression_handoff',
|
||||||
'sub_agent',
|
'sub_agent',
|
||||||
'background_command'
|
'background_command',
|
||||||
|
// 工作流运行期通知(inline 注入):紧凑通知;闲时任务入口派发的激活/退出
|
||||||
|
// 消息带显式 visibility:'chat',不受本兜底影响
|
||||||
|
'workflow'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
function normalizeSource(value: any): string {
|
function normalizeSource(value: any): string {
|
||||||
|
|||||||
@ -52,6 +52,8 @@ from utils.tool_result_formatter.web_media import (
|
|||||||
_format_trigger_easter_egg,
|
_format_trigger_easter_egg,
|
||||||
_format_create_skill,
|
_format_create_skill,
|
||||||
_format_manage_personalization,
|
_format_manage_personalization,
|
||||||
|
_format_list_workflows,
|
||||||
|
_format_save_workflow,
|
||||||
)
|
)
|
||||||
from utils.tool_result_formatter.common import _format_failure
|
from utils.tool_result_formatter.common import _format_failure
|
||||||
|
|
||||||
@ -133,6 +135,8 @@ TOOL_FORMATTERS = {
|
|||||||
"todo_update_task": _format_todo_update_task,
|
"todo_update_task": _format_todo_update_task,
|
||||||
"update_memory": _format_update_memory,
|
"update_memory": _format_update_memory,
|
||||||
"create_skill": _format_create_skill,
|
"create_skill": _format_create_skill,
|
||||||
|
"list_workflows": _format_list_workflows,
|
||||||
|
"save_workflow": _format_save_workflow,
|
||||||
"manage_personalization": _format_manage_personalization,
|
"manage_personalization": _format_manage_personalization,
|
||||||
"create_sub_agent": _format_create_sub_agent,
|
"create_sub_agent": _format_create_sub_agent,
|
||||||
"close_sub_agent": _format_close_sub_agent,
|
"close_sub_agent": _format_close_sub_agent,
|
||||||
|
|||||||
@ -13,6 +13,17 @@ def _format_create_skill(result_data: Dict[str, Any]) -> str:
|
|||||||
]
|
]
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
def _format_list_workflows(result_data: Dict[str, Any]) -> str:
|
||||||
|
if not result_data.get("success"):
|
||||||
|
return _format_failure("list_workflows", result_data)
|
||||||
|
# message 已是格式化清单(无参形态)或 WORKFLOW.md 原文(name 形态),完整透传
|
||||||
|
return str(result_data.get("message") or "")
|
||||||
|
|
||||||
|
def _format_save_workflow(result_data: Dict[str, Any]) -> str:
|
||||||
|
if not result_data.get("success"):
|
||||||
|
return _format_failure("save_workflow", result_data)
|
||||||
|
return str(result_data.get("summary") or f"已归档工作流:{result_data.get('workflow_name') or '未命名'}")
|
||||||
|
|
||||||
def _format_extract_webpage(result_data: Dict[str, Any]) -> str:
|
def _format_extract_webpage(result_data: Dict[str, Any]) -> str:
|
||||||
if not result_data.get("success"):
|
if not result_data.get("success"):
|
||||||
return _format_failure("extract_webpage", result_data)
|
return _format_failure("extract_webpage", result_data)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user