Compare commits

...

No commits in common. "9602875391cb17138d3c30909c46b43e9941a795" and "c908e8484490aac6cf017af9cd56a323d5f5f007" have entirely different histories.

27 changed files with 227 additions and 6533 deletions

13
.gitignore vendored
View File

@ -40,8 +40,10 @@ skills/
# 本地实验残留与历史文档归档(不进仓库) # 本地实验残留与历史文档归档(不进仓库)
_experiments/ _experiments/
# Ignore docs # Ignore docsdoc/ 与 docs/ 均为本地文档,不纳入版本控制)
doc/ doc/
docs/
workflow_research/
# 部署级配置(含密钥引用 / 机器特定,本地保留,仓库仅留 .example 种子) # 部署级配置(含密钥引用 / 机器特定,本地保留,仓库仅留 .example 种子)
config/host_workspaces.json config/host_workspaces.json
@ -73,15 +75,6 @@ static/old_version_backup/
static/debug-theme.html static/debug-theme.html
# 个人开发笔记与研究资料 # 个人开发笔记与研究资料
docs/papers/
docs/code_block_demo.html
docs/sidebar_overlay_demo.html
docs/sidebar_workspace_demo.html
docs/cli_slash_commands_spec.md
docs/cli_ui_display_spec.md
docs/conversation_level_terminal_design.md
docs/mcp-tool-config-guide.md
docs/multi_agent_mode/05_data_model.md
research/ research/
# 含私人信息 / 个人工作流约定(脱敏前不进仓库) # 含私人信息 / 个人工作流约定(脱敏前不进仓库)

View File

@ -13,7 +13,7 @@
- **构词**`Astr-`(希腊语 *astron*,星星/天体)+ `-ion`(粒子/状态后缀,如 photon、electron - **构词**`Astr-`(希腊语 *astron*,星星/天体)+ `-ion`(粒子/状态后缀,如 photon、electron
- **意象**:星之子、星际粒子、来自宇宙尘埃的微小发光体。 - **意象**:星之子、星际粒子、来自宇宙尘埃的微小发光体。
- **寓意**Agent 像星际间传递信号的粒子,把人类意图传递到工具、文件、终端与子智能体之间;同时把零散任务像星尘聚合成恒星一样,聚合成可执行结果。 - **寓意**Agent 像星际间传递信号的粒子,把人类意图传递到工具、文件、终端与子智能体之间;同时把零散任务像星尘聚合成恒星一样,聚合成可执行结果。
- **状态**:暂定名,后续若变更需同步更新 `AGENTS.md`、`claude.md` 与项目记忆 `astrion_naming` - **状态**:暂定名,后续若变更需同步更新 `AGENTS.md`(及本地 `claude.md`,未随仓库发布)
- **中文参考**:阿斯特里恩;简称可考虑 **星子** - **中文参考**:阿斯特里恩;简称可考虑 **星子**
## 1) 当前项目结构(按代码现状) ## 1) 当前项目结构(按代码现状)
@ -35,7 +35,7 @@
- `static/src/components/chat/monitor/MonitorDirector.ts` - `static/src/components/chat/monitor/MonitorDirector.ts`
- `static/src/stores/monitor.ts` - `static/src/stores/monitor.ts`
- `static/src/components/chat/monitor/*` - `static/src/components/chat/monitor/*`
- **CLI 相关文档** - **CLI 相关文档**(本地文档,未随仓库发布)
- `docs/cli_ui_display_spec.md`: CLI 显示层设计说明 - `docs/cli_ui_display_spec.md`: CLI 显示层设计说明
- `docs/cli_slash_commands_spec.md`: CLI `/` 指令设计说明 - `docs/cli_slash_commands_spec.md`: CLI `/` 指令设计说明
- **其他子项目/资源** - **其他子项目/资源**
@ -110,11 +110,6 @@
- 兼容启动方式:`python web_server.py` - 兼容启动方式:`python web_server.py`
- 统一入口:`python main.py`(当前实现会默认进入 Web 启动流程) - 统一入口:`python main.py`(当前实现会默认进入 Web 启动流程)
> **运行环境边界(重要)**:当前仓库(程序本体)跑在 **8091** 端口;**8092** 端口运行的是另一个本项目的代码副本所启动的智能体程序(即你自己的程序)。任何时候都禁止操作或修改 8092 端口的程序及其数据。两套环境的位置如下:
>
> - **macOS 开发环境**程序本体8091的数据存储在 `/Users/jojo/.astrion/astrion`你的程序8092的数据存储在 `/Users/jojo/.astrion/astrion-clone`
> - **Windows 环境**程序本体8091的数据存储在 `C:\Users\KOJO JOTARO\.astrion\astrion`你的程序8092代码位于 `E:\astrion-clone`,数据存储在 `C:\Users\KOJO JOTARO\.astrion\astrion-clone`
### Frontend根目录 ### Frontend根目录
- 安装依赖:`npm install` - 安装依赖:`npm install`
- 构建:`npm run build --silent 2>&1 | tail -n 5`(默认只看最后 5 行,减少 Vite 构建资源列表等无关输出) - 构建:`npm run build --silent 2>&1 | tail -n 5`(默认只看最后 5 行,减少 Vite 构建资源列表等无关输出)
@ -199,7 +194,7 @@
7. **带文字容器固定高度**:所有含内部文字的选项 / 按钮 / 标签 / 容器固定高度,禁止因文字多少被撑大撑高;过长文字用省略号或内部滚动处理。 7. **带文字容器固定高度**:所有含内部文字的选项 / 按钮 / 标签 / 容器固定高度,禁止因文字多少被撑大撑高;过长文字用省略号或内部滚动处理。
8. **窗口设最大尺寸 + 内部滚动**:所有弹窗 / 面板 / 列表设置 `max-height` / `max-width`,超出由内部容器滚动,不顶大整个窗口;滚动条要么隐藏,要么做样式适配,不暴露原生粗滚动条。 8. **窗口设最大尺寸 + 内部滚动**:所有弹窗 / 面板 / 列表设置 `max-height` / `max-width`,超出由内部容器滚动,不顶大整个窗口;滚动条要么隐藏,要么做样式适配,不暴露原生粗滚动条。
9. **实体面板禁止半透明**:实体 UI 容器(对话区 / 侧栏 / 下拉/二级菜单 / 抽屉 / 对话框本体 / 状态条 / tooltip / 输入栏)背景必须不透明,且不加 `backdrop-filter` 磨砂。唯一例外:遮罩层 scrim`--overlay-scrim``position:fixed;inset:0`)和刻意玻璃质感装饰保留半透明。 9. **实体面板禁止半透明**:实体 UI 容器(对话区 / 侧栏 / 下拉/二级菜单 / 抽屉 / 对话框本体 / 状态条 / tooltip / 输入栏)背景必须不透明,且不加 `backdrop-filter` 磨砂。唯一例外:遮罩层 scrim`--overlay-scrim``position:fixed;inset:0`)和刻意玻璃质感装饰保留半透明。
10. **主题变体必须与基础定义同文件(禁止跨文件补丁覆盖)**:组件的 dark/light/classic 主题变体写在该组件自己的样式文件里(文件内统一的 `body[data-theme='…'] { … }` 块),禁止把某组件的主题样式写到别的文件去覆盖。新增主题样式前先确认该组件基础定义所在文件;发现存量跨文件覆盖应顺手回迁并删除原覆盖。`!important` 只准用于遮盖第三方库样式,禁止用于压过项目自有定义(出现这种需求说明应该合并定义,而不是加 `!important`)。审计脚本:`python3 cache/style_audit.py` + `python3 cache/theme_diff.py` 10. **主题变体必须与基础定义同文件(禁止跨文件补丁覆盖)**:组件的 dark/light/classic 主题变体写在该组件自己的样式文件里(文件内统一的 `body[data-theme='…'] { … }` 块),禁止把某组件的主题样式写到别的文件去覆盖。新增主题样式前先确认该组件基础定义所在文件;发现存量跨文件覆盖应顺手回迁并删除原覆盖。`!important` 只准用于遮盖第三方库样式,禁止用于压过项目自有定义(出现这种需求说明应该合并定义,而不是加 `!important`)。审计脚本:`python3 cache/style_audit.py` + `python3 cache/theme_diff.py`(本地脚本,未随仓库发布)
11. **大面积色块禁止纯黑/近黑**:任何窗口的 hover、头部、背景等大面积色块禁止使用纯黑`#000`)或肉眼近黑(如 `#0a0a0a`/`#0f0f0f` 一档的颜色深色模式同样如此。深色下的层次靠中性灰阶拉开hover 用 `--hover-bg`(深色为白色微量 tint提亮而非压黑弹窗/面板头部优先用分隔线区分而非深色衬底(参考 `VersioningDialog.vue` 头部)。纯黑/近黑只允许用于小范围强调渲染(如行内代码衬底等小面积场景)。 11. **大面积色块禁止纯黑/近黑**:任何窗口的 hover、头部、背景等大面积色块禁止使用纯黑`#000`)或肉眼近黑(如 `#0a0a0a`/`#0f0f0f` 一档的颜色深色模式同样如此。深色下的层次靠中性灰阶拉开hover 用 `--hover-bg`(深色为白色微量 tint提亮而非压黑弹窗/面板头部优先用分隔线区分而非深色衬底(参考 `VersioningDialog.vue` 头部)。纯黑/近黑只允许用于小范围强调渲染(如行内代码衬底等小面积场景)。
## 5.6) 工具结果显示规范(强制) ## 5.6) 工具结果显示规范(强制)
@ -290,14 +285,14 @@ AI 执行以下流程时,每一步都要向用户说明在做什么:
## 8) Android App 发布联动要求(重要) ## 8) Android App 发布联动要求(重要)
当修改前端并需要发布 Android WebView App 时,必须同步执行以下步骤(依据 `doc/android_app_release_and_update.md` 当修改前端并需要发布 Android WebView App 时,必须同步执行以下步骤(依据 `doc/android_app_release_and_update.md`,本地文档未随仓库发布
1. 同时更新版本信息 1. 同时更新版本信息
- `android-webview-app/app/build.gradle.kts`:递增 `versionCode`,更新 `versionName` - `android-webview-app/app/build.gradle.kts`:递增 `versionCode`,更新 `versionName`
2. 同时更新更新说明 2. 同时更新更新说明
- `android-webview-app/APP_CHANGELOG.md`:在顶部新增当前版本说明 - `android-webview-app/APP_CHANGELOG.md`:在顶部新增当前版本说明
3. 完成修改后提醒用户运行上传脚本 3. 完成修改后提醒用户运行上传脚本
- 在发布流程中运行:`bash /Users/jojo/Desktop/agents/正在修复中/agents/upload_android_apk.sh` - 在发布流程中运行:`bash ./upload_android_apk.sh`(本地私有脚本,未随仓库发布)
> 禁止只改前端代码而不更新版本号/更新说明,否则会导致客户端更新提示与分发信息不一致。 > 禁止只改前端代码而不更新版本号/更新说明,否则会导致客户端更新提示与分发信息不一致。
@ -313,7 +308,7 @@ AI 执行以下流程时,每一步都要向用户说明在做什么:
## 10) 宿主机权限模式与沙箱执行机制2026-05 更新) ## 10) 宿主机权限模式与沙箱执行机制2026-05 更新)
完整说明文档:`docs/host_sandbox_and_permission_model.md` 完整说明文档:`docs/host_sandbox_and_permission_model.md`(本地文档,未随仓库发布)
为避免误解,当前系统有两套独立但叠加的控制: 为避免误解,当前系统有两套独立但叠加的控制:
@ -393,7 +388,7 @@ AI 执行以下流程时,每一步都要向用户说明在做什么:
## 11) 多智能体对话类型multi-agent conversation type ## 11) 多智能体对话类型multi-agent conversation type
> **2026-08 重构**:已废除「多智能体模式」全局状态,多智能体是**对话的不可变属性**`metadata.multi_agent_mode = true`创建时确定、不可变。URL 统一为 `/<conv_id>`(旧 `/multiagent/*` 由前端 bootstrap 重定向到裸路径)。数据目录 `~/.astrion/astrion/host/mutiagents/`(保留原拼写)。完整设计与踩坑记录见项目记忆 `multi_agent_mode_design`重构方案见 `docs/conversation_type_unification_plan.md`,本节只列 Agent 改代码时必须知道的硬约束。 > **2026-08 重构**:已废除「多智能体模式」全局状态,多智能体是**对话的不可变属性**`metadata.multi_agent_mode = true`创建时确定、不可变。URL 统一为 `/<conv_id>`(旧 `/multiagent/*` 由前端 bootstrap 重定向到裸路径)。数据目录 `~/.astrion/astrion/host/mutiagents/`(保留原拼写)。重构方案见 `docs/conversation_type_unification_plan.md`(本地设计文档,未随仓库发布),本节只列 Agent 改代码时必须知道的硬约束。
### 11.0 前端对话类型模型(重构后) ### 11.0 前端对话类型模型(重构后)
@ -429,7 +424,7 @@ AI 执行以下流程时,每一步都要向用户说明在做什么:
- 输出端(`SubAgentTask._forward_output_to_master`):子智能体每次 assistant 文本输出都封为标准格式消息并 `push_master_message` 到会话的 `MultiAgentState.pending_master_messages`,不再做事。 - 输出端(`SubAgentTask._forward_output_to_master`):子智能体每次 assistant 文本输出都封为标准格式消息并 `push_master_message` 到会话的 `MultiAgentState.pending_master_messages`,不再做事。
- 接收端(`MultiAgentState` + dispatch根据主智能体当前状态选择插入方式 - 接收端(`MultiAgentState` + dispatch根据主智能体当前状态选择插入方式
- **情况1主运行中**:在 `execute_tool_calls` 末尾 `process_multi_agent_master_messages(inline=True, after_tool_call_id=...)` 插入到下一轮模型 messages 列表里(详见 `server/chat_flow_tool_loop.py:1326`)。 - **情况1主运行中**:在 `execute_tool_calls` 末尾 `process_multi_agent_master_messages(inline=True, after_tool_call_id=...)` 插入到下一轮模型 messages 列表里(详见 `server/chat_flow_tool_loop.py:1326`)。
- **情况2主空闲**`poll_multi_agent_notifications` spawn 出后台 poll主对话空闲且 pool 有消息就 drain`_dispatch_multi_agent_idle_messages` 创建 `task_type="notice"` 的后续 task触发新一轮工作。完整顺序见项目记忆 multi_agent_mode_design 中的「情况2完整链路」图。 - **情况2主空闲**`poll_multi_agent_notifications` spawn 出后台 poll主对话空闲且 pool 有消息就 drain`_dispatch_multi_agent_idle_messages` 创建 `task_type="notice"` 的后续 task触发新一轮工作。
- **情况3主最后一轮无工具调用**:主循环 `if not tool_calls:` 分支调 `process_multi_agent_master_messages(inline=False)``continue` 继续迭代Team Leader 在同 task 续跑处理子智能体输出。实现上与情况2同路径即情况3通过后续 task 完成情况2 - **情况3主最后一轮无工具调用**:主循环 `if not tool_calls:` 分支调 `process_multi_agent_master_messages(inline=False)``continue` 继续迭代Team Leader 在同 task 续跑处理子智能体输出。实现上与情况2同路径即情况3通过后续 task 完成情况2
**情况2 硬约束(调试踩过坑)** **情况2 硬约束(调试踩过坑)**
@ -517,13 +512,13 @@ AI 执行以下流程时,每一步都要向用户说明在做什么:
--- ---
注:本节按「现有架构 + 多智能体分支」方案描述,与项目记忆 `multi_agent_mode_design` 同步;如两者冲突以代码为准并同步修订本节。 注:本节按「现有架构 + 多智能体分支」方案描述;如与代码冲突,以代码为准并同步修订本节。
--- ---
## 12) 对话级主任务门闸与单写者不变量2026-08-12 ## 12) 对话级主任务门闸与单写者不变量2026-08-12
> 事故背景一个对话并发运行了两个主聊天任务socketio 用户任务 + 完成通知轮询器派发的通知任务),交叉写入共享 `conversation_history`,产生 `assistant→assistant→tool→tool` 乱序段,最终 API 400 `tool_call_id is not found`、通知永久丢失。完整根因与修复细节见项目记忆 `main_task_gate_and_parallel_universe` > 事故背景一个对话并发运行了两个主聊天任务socketio 用户任务 + 完成通知轮询器派发的通知任务),交叉写入共享 `conversation_history`,产生 `assistant→assistant→tool→tool` 乱序段,最终 API 400 `tool_call_id is not found`、通知永久丢失。本节机制即为修复该事故引入
### 12.1 单写者不变量(核心约束) ### 12.1 单写者不变量(核心约束)

View File

@ -46,7 +46,7 @@
```bash ```bash
# 1. 克隆 # 1. 克隆
git clone <仓库地址> git clone https://github.com/JOJO6618/astrion.git
cd astrion cd astrion
# 2. 初始化:创建 venv、安装依赖、运行交互式配置向导 # 2. 初始化:创建 venv、安装依赖、运行交互式配置向导
@ -164,8 +164,8 @@ docker build -f docker/terminal.Dockerfile -t my-agent-shell:latest .
| `server.port` / `server.host` | `WEB_SERVER_PORT` / `WEB_SERVER_HOST` | Web 监听 | | `server.port` / `server.host` | `WEB_SERVER_PORT` / `WEB_SERVER_HOST` | Web 监听 |
| `admin.username` | `AGENT_ADMIN_USERNAME` | 管理员用户名 | | `admin.username` | `AGENT_ADMIN_USERNAME` | 管理员用户名 |
| `admin.password_hash` | `AGENT_ADMIN_PASSWORD_HASH` | 管理员密码哈希(见「管理员账户」) | | `admin.password_hash` | `AGENT_ADMIN_PASSWORD_HASH` | 管理员密码哈希(见「管理员账户」) |
| `admin.secondary_password_hash` | `ADMIN_SECONDARY_PASSWORD_HASH` | 可选:敏感操作二级密码 | | `admin.secondary_password_hash` | `ADMIN_SECONDARY_PASSWORD_HASH` | 敏感操作二级密码**web 模式实际必需**:未配置时管理接口校验恒失败,管理面板功能不可用) |
| `secrets.web_secret_key` | `WEB_SECRET_KEY` | Session 签名密钥(随机 hex | | `secrets.web_secret_key` | `WEB_SECRET_KEY` | Session 签名密钥(随机 hex**web 模式强烈建议配置**:未配置时使用临时密钥,重启后所有登录会话失效 |
| `secrets.api_token_secret` | `API_TOKEN_SECRET` | API Token 加密密钥(随机 hex | | `secrets.api_token_secret` | `API_TOKEN_SECRET` | API Token 加密密钥(随机 hex |
| `models.default_response_max_tokens` | `AGENT_DEFAULT_RESPONSE_MAX_TOKENS` | 单轮响应 token 全局上限 | | `models.default_response_max_tokens` | `AGENT_DEFAULT_RESPONSE_MAX_TOKENS` | 单轮响应 token 全局上限 |
| `search.tavily_api_key(_2)` | `AGENT_TAVILY_API_KEY(_2)` | Tavily 搜索 Key可选 | | `search.tavily_api_key(_2)` | `AGENT_TAVILY_API_KEY(_2)` | Tavily 搜索 Key可选 |
@ -214,9 +214,15 @@ docker build -f docker/terminal.Dockerfile -t my-agent-shell:latest .
python3 -c "from werkzeug.security import generate_password_hash; print(generate_password_hash('你的密码'))" python3 -c "from werkzeug.security import generate_password_hash; print(generate_password_hash('你的密码'))"
``` ```
管理员登录邮箱自动生成,规则为 `<用户名>@local`(例如用户名为 `admin`,则登录邮箱为 `admin@local`)。
### 普通用户web 模式) ### 普通用户web 模式)
**邀请码注册制**`/register` 注册必须提供有效邀请码;邀请码由管理员在用户管理中生成(支持次数限制)。 **邀请码注册制**`/register` 注册必须提供有效邀请码;邀请码由管理员在管理面板(`/admin/monitor`)的「邀请码」标签页生成(支持次数限制)。
> ⚠️ **安全提示**
> - 注册邮箱仅用于登录与查重,**系统不发送验证邮件**——邀请码是注册的唯一实质门槛,请妥善保管。
> - 新用户的默认权限模式为「无限制」(`unrestricted`,可执行任意终端命令)。多用户或包含不可信用户的场景下,建议用户在个人空间将默认权限模式调整为 `approval` / `readonly`
## 项目结构 ## 项目结构

View File

@ -3,5 +3,4 @@ name: mcp-tool-config
description: 指导模型在宿主机模式下如何给自己配置新的mcp工具 description: 指导模型在宿主机模式下如何给自己配置新的mcp工具
--- ---
Just Read This File 阅读本目录下的 [mcp-tool-config-guide.md](mcp-tool-config-guide.md),获取完整的 MCP 工具配置指南。
skills/mcp-tool-config/mcp-tool-config-guide.md

View File

@ -1,7 +1,6 @@
# 本项目新增 MCP 工具配置指南 # 本项目新增 MCP 工具配置指南
> 最后更新2026-04-26 > 最后更新2026-08-22
> 适用目录:`/Users/jojo/Desktop/agents/正在修复中/agents`
本文面向后续维护者,说明如何在当前项目里新增并接入一个 MCP 工具MCP Server 本文面向后续维护者,说明如何在当前项目里新增并接入一个 MCP 工具MCP Server
@ -44,7 +43,7 @@
仓库已有可直接参考的测试服务: 仓库已有可直接参考的测试服务:
- `/Users/jojo/Desktop/agents/正在修复中/agents/scripts/mcp_calculator_server.py` - `scripts/mcp_calculator_server.py`
这个示例使用 stdio支持 这个示例使用 stdio支持
@ -146,11 +145,11 @@
- `MCP_PROTOCOL_VERSION=2025-06-18` - `MCP_PROTOCOL_VERSION=2025-06-18`
- `MCP_DEFAULT_TIMEOUT_SECONDS=25` - `MCP_DEFAULT_TIMEOUT_SECONDS=25`
如果你只是个AI那就去改这个文件) AI 维护者请直接改这个文件)
默认配置文件是运行态文件: 默认配置文件是运行态文件`DATA_DIR` 解析见 `config/paths.py`
- `/Users/jojo/Desktop/agents/正在修复中/agents/data/mcp_servers.json` - `~/.astrion/astrion/<mode>/data/mcp_servers.json`
--- ---
@ -191,12 +190,12 @@
## 10. 关键代码位置(便于二次开发) ## 10. 关键代码位置(便于二次开发)
- MCP 配置:`/Users/jojo/Desktop/agents/正在修复中/agents/config/mcp.py` - MCP 配置:`config/mcp.py`
- 服务注册表:`/Users/jojo/Desktop/agents/正在修复中/agents/modules/mcp_server_registry.py` - 服务注册表:`modules/mcp_server_registry.py`
- MCP 客户端桥接:`/Users/jojo/Desktop/agents/正在修复中/agents/modules/mcp_client_manager.py` - MCP 客户端桥接:`modules/mcp_client_manager/`(子包)
- 管理 API`/Users/jojo/Desktop/agents/正在修复中/agents/server/admin.py` - 管理 API`server/admin.py`
- 工具定义/执行: - 工具定义/执行:
- `/Users/jojo/Desktop/agents/正在修复中/agents/core/main_terminal_parts/tools_definition.py` - `core/main_terminal_parts/tools_definition/`(子包,`tools_definition.py` 为兼容入口)
- `/Users/jojo/Desktop/agents/正在修复中/agents/core/main_terminal_parts/tools_execution.py` - `core/main_terminal_parts/tools_execution.py`
- 管理端页面:`/Users/jojo/Desktop/agents/正在修复中/agents/static/src/admin/PolicyApp.vue` - 管理端页面:`static/src/admin/PolicyApp.vue`
- 测试示例服务:`/Users/jojo/Desktop/agents/正在修复中/agents/scripts/mcp_calculator_server.py` - 测试示例服务:`scripts/mcp_calculator_server.py`

View File

@ -1,202 +0,0 @@
# AI Agent 系统对话区域功能分析文档
本目录包含对 AI Agent 系统对话显示区域的详细功能分析,用于识别历史包袱和需要改造为纯轮询模式的部分。
## 📋 分析文档列表
### ✅ 已完成的分析
1. **[停止按钮和运行状态机制](./analysis-stop-button.md)** (22KB)
- 停止按钮的显示/隐藏逻辑
- 运行状态的设置和清除
- WebSocket vs REST API 的停止机制
- 已知问题:状态同步、竞态条件、页面刷新恢复
2. **[对话记录加载机制](./analysis-conversation-loading.md)** (21KB)
- 切换对话时的加载流程
- 历史消息的数据结构
- 防止重复加载的机制
- 初始化阶段的特殊处理
3. **[运行中对话恢复机制](./analysis-task-recovery.md)** (20KB)
- 页面刷新后的任务恢复
- `_rebuildingFromScratch` 标志的作用
- 历史事件的重放机制
- 轮询的恢复逻辑
4. **思考块机制** (分析已完成)
- 思考块的创建和内容追加
- 展开/折叠逻辑(流式输出 vs 历史加载)
- 自动折叠机制1秒延迟
- 无打字机效果,直接追加内容
5. **工具块机制** (分析已完成)
- 工具块的创建和状态变化preparing → running → completed/failed
- tool_intent 的打字机效果0.5-1秒
- 工具块的堆叠和融合机制
- toolActionIndex 注册机制
- append_payload 和 modify_payload 处理
6. **模型输出内容显示** (分析已完成)
- 文本流式显示text_start/chunk/end
- Markdown 渲染marked.js + Prism.js + KaTeX
- 代码块的语法高亮和复制功能
- 双事件监听系统可能导致复制bug
- 图片和视频的显示
### ⏸️ 未完成的分析
7. **子智能体机制** (分析中断)
- waitingForSubAgent 状态
- sub_agent_waiting 事件
- WebSocket 在子智能体场景下的特殊作用
## 🎯 重点关注的问题区域
### 1. 停止按钮Bug最多
**主要问题:**
- 前端在后端确认前就清理状态,导致同步问题
- 多个布尔标志taskInProgress, streamingMessage, stopRequested难以维护
- 存在竞态条件
- 页面刷新后状态恢复复杂
**涉及文件:**
- `static/src/components/input/InputComposer.vue` - 停止按钮UI
- `static/src/app/methods/message.ts` - 停止逻辑
- `static/src/stores/task.ts` - 任务状态管理
- `server/tasks.py` - 后端停止处理
### 2. 思考块和工具块
**展开/折叠机制:**
- 流式输出时思考块自动展开1秒后折叠
- 历史加载时:所有块默认折叠
- 用户手动点击:状态存储在 `expandedBlocks` Set不持久化
**打字机效果:**
- 思考块:无打字机效果,直接追加
- 工具块 intent0.5-1秒打字机效果
- 历史加载时:跳过打字机效果
**堆叠机制:**
- 连续的思考/工具块会被分组
- 默认显示最后6个其余折叠
- 使用 ResizeObserver 跟踪高度变化
### 3. 对话恢复机制
**`_rebuildingFromScratch` 标志:**
- 用于区分"从头重建"和"增量恢复"
- 影响思考块展开、工具意图动画、生成标签显示
- 页面刷新后,如果历史不完整,从 offset=0 重放所有事件
**轮询恢复:**
- 150ms 间隔轮询
- 通过 `lastEventIndex` 避免重复处理
- 支持从任意 offset 恢复
### 4. 代码块复制功能
**可能的Bug原因**
- 双事件监听系统(全局 + Vue方法
- Clipboard API 权限问题
- HTML 实体编码/解码问题
- 流式输出时按钮添加时机
**实现位置:**
- `static/index.html` - 全局事件监听
- `static/src/app/methods/ui.ts` - Vue 复制方法
- `static/src/app/bootstrap.ts` - Markdown 渲染初始化
## 🔄 WebSocket vs 轮询模式
### 当前状态
系统已设置 `usePollingMode: true`,大部分聊天事件通过 REST API 轮询处理。
**WebSocket 仍在使用的场景:**
1. 子智能体通知(`sub_agent_waiting`
2. 配额更新(`quota_update`
3. 系统消息(`system_message`
4. 错误处理(`error`
**事件过滤机制:**
```typescript
if (ctx.usePollingMode && !ctx.waitingForSubAgent) {
// 跳过 WebSocket 事件处理
return;
}
```
### 需要改造的部分
**完全移除 WebSocket 依赖:**
1. 删除 `useLegacySocket.ts` 中的聊天事件处理(~1200行
2. 删除流式文本处理代码(`streamingState`~500行
3. 统一停止逻辑到 REST API
4. 子智能体通知改为 SSEServer-Sent Events
**后端改造:**
1. `server/socket_handlers.py` - 简化为仅处理连接管理
2. `server/tasks.py` - 添加任务清理、重试、优先级
3. `server/chat_flow.py` - 移除 WebSocket emit
## 📊 代码统计
### 前端关键文件
| 文件 | 行数 | 功能 | 需要改造 |
|------|------|------|---------|
| `useLegacySocket.ts` | 1706 | WebSocket 事件处理 | ✅ 删除聊天事件 |
| `taskPolling.ts` | 989 | 轮询事件处理 | ⚠️ 修复bug |
| `task.ts` (store) | 296 | 任务状态管理 | ⚠️ 添加重试 |
| `ChatArea.vue` | ~800 | 对话显示 | ⚠️ 修复UI bug |
| `InputComposer.vue` | ~200 | 输入框和停止按钮 | ⚠️ 简化状态 |
### 后端关键文件
| 文件 | 行数 | 功能 | 需要改造 |
|------|------|------|---------|
| `socket_handlers.py` | 353 | WebSocket 处理 | ✅ 移除聊天事件 |
| `tasks.py` | 380 | 任务管理 | ⚠️ 增强功能 |
| `chat_flow.py` | 241 | 聊天流程 | ⚠️ 移除 WebSocket |
## 🚀 重构优先级
### 优先级1必须做
1. ✅ 修复停止按钮的状态同步问题
2. ✅ 修复代码块复制功能
3. ✅ 修复重复推送内容的问题
4. ✅ 修复任务无法结束的问题
### 优先级2建议做
1. 删除 `useLegacySocket.ts` 中的聊天事件处理
2. 统一停止逻辑到 REST API
3. 添加轮询重试机制
4. 任务清理机制
### 优先级3可选
1. WebSocket 改为 SSE
2. 任务存储改为 Redis
3. 改进错误提示
## 📝 使用说明
1. **阅读分析文档**:按照上述列表顺序阅读各个分析文档
2. **对比实际网页**:在浏览器中打开系统,对比文档描述和实际行为
3. **标记问题点**记录不一致的地方、bug、历史包袱
4. **制定重构计划**:根据优先级制定具体的重构步骤
## 🔗 相关资源
- [项目说明](../CLAUDE.md)
- [Git提交历史](../.git) - 查看架构演变
- [前端源码](../static/src/)
- [后端源码](../server/)
---
**生成时间**: 2026-04-04
**分析工具**: Claude Code + 7个并行子智能体
**总分析时间**: ~15分钟

View File

@ -1,790 +0,0 @@
# AI Agent 系统对话记录加载和显示机制分析
## 概述
本文档详细分析 AI Agent 系统中对话记录的加载和显示机制,包括切换对话时的完整流程、数据结构、防重复加载机制、加载动画、初始化处理等核心功能。
---
## 1. 切换对话时的加载流程
### 1.1 用户触发对话切换
用户可以通过以下方式切换对话:
- 点击侧边栏对话列表中的对话项
- 通过浏览器前进/后退按钮popstate 事件)
- 直接访问对话 URL`/conv_20240101_120000_123`
### 1.2 前端发送的请求
当用户点击对话列表项时,触发 `loadConversation` 方法:
**文件位置**: `/static/src/app/methods/conversation.ts` (164-276行)
```typescript
async loadConversation(conversationId, options = {}) {
const force = Boolean(options.force);
// 1. 检查是否已是当前对话
if (!force && conversationId === this.currentConversationId) {
return; // 跳过加载
}
// 2. 检查是否有运行中的任务,提示用户确认
if (hasActiveTask) {
const confirmed = await this.confirmAction({...});
if (!confirmed) return;
}
// 3. 调用加载API
const response = await fetch(`/api/conversations/${conversationId}/load`, {
method: 'PUT'
});
// 4. 更新当前对话信息
this.currentConversationId = conversationId;
this.currentConversationTitle = result.title;
// 5. 重置UI状态
this.resetAllStates(`loadConversation:${conversationId}`);
// 6. 立即加载历史和统计
await this.fetchAndDisplayHistory();
this.fetchConversationTokenStatistics();
}
```
### 1.3 后端返回的数据结构
**API端点**: `PUT /api/conversations/<conversation_id>/load`
**文件位置**: `/server/conversation.py` (232-271行)
```python
@conversation_bp.route('/api/conversations/<conversation_id>/load', methods=['PUT'])
def load_conversation(conversation_id, terminal, workspace, username):
# 1. 终止当前对话的运行中子智能体
_terminate_running_sub_agents(terminal, reason="用户切换对话")
# 2. 调用终端的加载方法
result = terminal.load_conversation(conversation_id)
# 3. 广播对话切换事件
socketio.emit('conversation_changed', {
'conversation_id': conversation_id,
'title': result.get("title", "未知对话"),
'messages_count': result.get("messages_count", 0)
}, room=f"user_{username}")
# 4. 清理和重置相关UI状态
socketio.emit('conversation_loaded', {
'conversation_id': conversation_id,
'clear_ui': True
}, room=f"user_{username}")
return jsonify(result)
```
**返回数据结构**:
```json
{
"success": true,
"title": "对话标题",
"messages_count": 15,
"conversation_id": "conv_20240101_120000_123"
}
```
### 1.4 前端渲染历史消息
加载对话后,前端立即调用 `fetchAndDisplayHistory` 获取完整消息历史:
**文件位置**: `/static/src/app/methods/history.ts` (8-123行)
```typescript
async fetchAndDisplayHistory(options = {}) {
const targetConversationId = this.currentConversationId;
// 1. 防止重复加载
if (this.historyLoading && this.historyLoadingFor === targetConversationId) {
return; // 同一对话正在加载
}
const alreadyHydrated =
!force &&
this.lastHistoryLoadedConversationId === targetConversationId &&
this.messages.length > 0;
if (alreadyHydrated) {
return; // 历史已加载,跳过
}
// 2. 设置加载状态
this.historyLoading = true;
this.historyLoadingFor = targetConversationId;
// 3. 获取消息历史
const messagesResponse = await fetch(
`/api/conversations/${targetConversationId}/messages`
);
const messagesData = await messagesResponse.json();
// 4. 检查对话是否已切换
if (this.currentConversationId !== targetConversationId) {
return; // 丢弃过期的历史加载结果
}
// 5. 渲染历史消息
this.messages = [];
this.renderHistoryMessages(messages);
// 6. 滚动到底部
this.$nextTick(() => {
this.scrollToBottom();
});
}
```
---
## 2. 历史消息的数据结构
### 2.1 消息格式
**API端点**: `GET /api/conversations/<conversation_id>/messages`
**文件位置**: `/server/conversation.py` (353-391行)
**返回数据结构**:
```json
{
"success": true,
"data": {
"conversation_id": "conv_20240101_120000_123",
"messages": [
{
"role": "user",
"content": "用户消息内容",
"images": ["path/to/image.png"],
"videos": ["path/to/video.mp4"],
"metadata": {
"images": [...],
"videos": [...]
}
},
{
"role": "assistant",
"content": "AI回复内容",
"reasoning_content": "思考过程(可选)",
"tool_calls": [
{
"id": "call_abc123",
"function": {
"name": "read_file",
"arguments": "{\"path\": \"/workspace/file.txt\"}"
}
}
],
"metadata": {
"model_key": "示例模型"
}
},
{
"role": "tool",
"name": "read_file",
"tool_call_id": "call_abc123",
"content": "{\"output\": \"文件内容\", \"success\": true}",
"metadata": {
"tool_payload": {...}
}
}
],
"total_count": 3
}
}
```
### 2.2 思考块的存储
思考内容存储在 `assistant` 消息的 `reasoning_content` 字段中:
```json
{
"role": "assistant",
"reasoning_content": "让我分析一下这个问题...",
"content": "根据分析,我建议..."
}
```
前端渲染时,将思考内容转换为独立的 action
```typescript
if (reasoningText) {
currentAssistantMessage.actions.push({
id: `history-thinking-${Date.now()}-${Math.random()}`,
type: 'thinking',
content: reasoningText,
streaming: false,
collapsed: true, // 默认折叠
timestamp: Date.now()
});
}
```
### 2.3 工具块的存储
工具调用分为两部分:
1. **工具调用请求** (assistant 消息的 `tool_calls` 字段)
2. **工具执行结果** (独立的 tool 消息)
前端渲染时,将工具调用和结果关联:
**文件位置**: `/static/src/app/methods/history.ts` (218-309行)
```typescript
// 处理工具调用
if (message.tool_calls && Array.isArray(message.tool_calls)) {
message.tool_calls.forEach((toolCall) => {
const action = {
id: `history-tool-${toolCall.id}`,
type: 'tool',
tool: {
id: toolCall.id,
name: toolCall.function.name,
arguments: JSON.parse(toolCall.function.arguments),
status: 'preparing',
result: null
}
};
currentAssistantMessage.actions.push(action);
});
}
// 处理工具结果
if (message.role === 'tool') {
// 查找对应的工具action
const toolAction = currentAssistantMessage.actions.find(action =>
action.type === 'tool' &&
action.tool.id === message.tool_call_id
);
if (toolAction) {
toolAction.tool.status = 'completed';
toolAction.tool.result = JSON.parse(message.content);
}
}
```
### 2.4 图片和视频的存储
图片和视频路径存储在用户消息的 `images``videos` 字段中:
```json
{
"role": "user",
"content": "请看这张图片",
"images": ["/user_upload/images/photo.png"],
"videos": ["/user_upload/videos/demo.mp4"],
"metadata": {
"images": [...],
"videos": [...]
}
}
```
前端渲染时,从 `images``metadata.images` 中提取:
```typescript
const images = message.images || (message.metadata && message.metadata.images) || [];
const videos = message.videos || (message.metadata && message.metadata.videos) || [];
this.messages.push({
role: 'user',
content: message.content || '',
images,
videos
});
```
---
## 3. 防止重复加载的机制
### 3.1 lastHistoryLoadedConversationId 的作用
**文件位置**: `/static/src/app/state.ts` (24-25行)
```typescript
// 记录上一次成功加载历史的对话ID防止初始化阶段重复加载导致动画播放两次
lastHistoryLoadedConversationId: null,
```
**作用**:
- 记录最后一次成功加载历史的对话 ID
- 防止同一对话的历史被重复加载
- 避免初始化阶段动画播放两次
### 3.2 如何判断是否需要重新加载
**文件位置**: `/static/src/app/methods/history.ts` (24-35行)
```typescript
const alreadyHydrated =
!force &&
this.lastHistoryLoadedConversationId === targetConversationId &&
Array.isArray(this.messages) &&
this.messages.length > 0;
if (alreadyHydrated) {
debugLog('历史已加载,跳过重复请求');
return;
}
```
**判断条件**:
1. 非强制刷新 (`!force`)
2. 目标对话 ID 与上次加载的 ID 相同
3. 消息数组存在且不为空
### 3.3 缓存机制
系统使用多层缓存机制:
1. **前端消息缓存**: `this.messages` 数组
2. **历史加载标记**: `lastHistoryLoadedConversationId`
3. **加载状态跟踪**: `historyLoading``historyLoadingFor`
**并发加载保护**:
```typescript
// 若同一对话正在加载,直接复用
if (this.historyLoading && this.historyLoadingFor === targetConversationId) {
debugLog('同一对话历史正在加载,跳过重复请求');
return;
}
// 使用序列号防止过期响应
const loadSeq = ++this.historyLoadSeq;
this.historyLoading = true;
this.historyLoadingFor = targetConversationId;
// 响应返回时检查序列号
if (loadSeq !== this.historyLoadSeq ||
this.currentConversationId !== targetConversationId) {
debugLog('检测到对话已切换,丢弃过期的历史加载结果');
return;
}
```
---
## 4. 加载动画和状态提示
### 4.1 historyLoading 状态
**文件位置**: `/static/src/app/state.ts` (46-48行)
```typescript
historyLoading: false, // 是否正在加载历史
historyLoadingFor: null, // 正在加载哪个对话的历史
historyLoadSeq: 0, // 加载序列号
```
**状态管理**:
```typescript
// 开始加载
const loadSeq = ++this.historyLoadSeq;
this.historyLoading = true;
this.historyLoadingFor = targetConversationId;
// 加载完成
finally {
// 仅在本次加载仍是最新请求时清除 loading 状态
if (loadSeq === this.historyLoadSeq) {
this.historyLoading = false;
this.historyLoadingFor = null;
}
}
```
### 4.2 加载动画的显示
前端可以根据 `historyLoading` 状态显示加载动画:
```vue
<div v-if="historyLoading" class="loading-indicator">
<div class="spinner"></div>
<p>加载对话历史...</p>
</div>
```
### 4.3 加载失败的处理
**文件位置**: `/static/src/app/methods/history.ts` (107-114行)
```typescript
catch (error) {
console.error('获取历史对话失败:', error);
debugLog('尝试不显示错误弹窗,仅在控制台记录');
// 不显示alert避免打断用户体验
this.messages = [];
}
```
**失败处理策略**:
- 仅在控制台记录错误
- 不显示弹窗,避免打断用户体验
- 清空消息数组,显示空白状态
---
## 5. 初始化阶段的特殊处理
### 5.1 防止动画播放两次
**问题**: 初始化时可能触发多次历史加载,导致动画重复播放
**解决方案**: 使用 `lastHistoryLoadedConversationId` 标记
**文件位置**: `/static/src/app/methods/history.ts` (336行)
```typescript
this.lastHistoryLoadedConversationId = this.currentConversationId || null;
```
**流程**:
1. 首次加载历史后,记录对话 ID
2. 后续加载前检查是否已加载
3. 如果已加载且消息不为空,跳过加载
### 5.2 initialRouteResolved 的作用
**文件位置**: `/static/src/app/state.ts` (7行)
```typescript
initialRouteResolved: false,
```
**作用**:
- 标记初始路由是否已解析完成
- 防止路由解析期间触发不必要的对话加载
- 避免与 `bootstrapRoute` 冲突
**使用场景**:
1. **对话列表自动加载**:
```typescript
// 只有在初始化完成后,才自动加载第一个对话
if (this.initialRouteResolved) {
const latestConversation = this.conversations[0];
if (latestConversation && latestConversation.id) {
await this.loadConversation(latestConversation.id);
}
}
```
2. **Socket 事件处理**:
```typescript
// 初始化期间不修改 currentConversationId避免与 bootstrapRoute 冲突
if (ctx.initialRouteResolved && data && data.conversation_id) {
ctx.currentConversationId = data.conversation_id;
}
```
### 5.3 路由解析的流程
**文件位置**: `/static/src/app/methods/ui.ts` (967-1014行)
```typescript
async bootstrapRoute() {
// 1. 抑制标题动画
this.suppressTitleTyping = true;
this.titleReady = false;
// 2. 解析路径
const path = window.location.pathname.replace(/^\/+/, '');
// 3. 处理新对话
if (!path || path === 'new') {
this.currentConversationId = null;
this.currentConversationTitle = '新对话';
this.initialRouteResolved = true;
return;
}
// 4. 加载指定对话
const convId = path.startsWith('conv_') ? path : `conv_${path}`;
const resp = await fetch(`/api/conversations/${convId}/load`, {
method: 'PUT'
});
const result = await resp.json();
if (result.success) {
this.currentConversationId = convId;
this.currentConversationTitle = result.title || '';
history.replaceState({ conversationId: convId }, '', `/${convId}`);
} else {
// 加载失败,回退到新对话
history.replaceState({}, '', '/new');
this.currentConversationId = null;
this.currentConversationTitle = '新对话';
}
// 5. 标记路由解析完成
this.initialRouteResolved = true;
}
```
**调用时机**:
**文件位置**: `/static/src/app/lifecycle.ts` (32-33行)
```typescript
async mounted() {
// 并行启动路由解析与初始化数据
const routePromise = this.bootstrapRoute();
await routePromise;
// 立即加载初始数据
this.loadInitialData();
}
```
---
## 6. 对话切换的状态管理
### 6.1 当前对话ID的存储
**状态存储位置**:
1. **前端状态**: `this.currentConversationId`
2. **浏览器历史**: `history.pushState({ conversationId }, '', '/conv_id')`
3. **后端会话**: 服务器端 `terminal.context_manager.current_conversation_id`
**同步机制**:
```typescript
// 前端切换对话
this.currentConversationId = conversationId;
history.pushState({ conversationId }, '', `/${conversationId}`);
// 后端加载对话
terminal.load_conversation(conversation_id)
```
### 6.2 切换时的状态清理
**文件位置**: `/static/src/app/methods/conversation.ts` (6-79行)
```typescript
resetAllStates(reason = 'unspecified', options = {}) {
// 1. 检查是否正在等待子智能体
if (this.waitingForSubAgent) {
return; // 跳过状态重置
}
// 2. 重置消息和流状态
this.streamingMessage = false;
this.currentMessageIndex = -1;
this.stopRequested = false;
this.taskInProgress = false;
this.dropToolEvents = false;
// 3. 清理工具状态
this.toolResetTracking();
// 4. 将所有未完成的工具标记为已完成
this.messages.forEach(msg => {
if (msg.role === 'assistant' && msg.actions) {
msg.actions.forEach(action => {
if (action.type === 'tool' &&
(action.tool.status === 'preparing' ||
action.tool.status === 'running')) {
action.tool.status = 'completed';
}
});
}
});
// 5. 清理Markdown缓存
if (this.markdownCache) {
this.markdownCache.clear();
}
// 6. 重置输入状态
this.inputClearMessage();
this.inputClearSelectedImages();
this.imageEntries = [];
this.imageLoading = false;
// 7. 重置已加载对话标记
this.lastHistoryLoadedConversationId = null;
// 8. 强制更新视图
this.$forceUpdate();
}
```
### 6.3 任务状态的处理
**切换对话前检查运行中的任务**:
**文件位置**: `/static/src/app/methods/conversation.ts` (182-214行)
```typescript
// 有任务或后台子智能体运行时,提示用户确认切换
const hasActiveTask = taskStore.hasActiveTask ||
this.taskInProgress ||
this.waitingForSubAgent;
if (hasActiveTask) {
const confirmed = await this.confirmAction({
title: '切换对话',
message: this.waitingForSubAgent
? '后台子智能体正在运行,切换对话后将不会自动接收完成提示。确定要切换吗?'
: '当前有任务正在执行,切换对话后任务会停止。确定要切换吗?',
confirmText: '切换',
cancelText: '取消'
});
if (!confirmed) {
return; // 用户取消切换
}
// 停止任务
if (taskStore.hasActiveTask) {
await taskStore.cancelTask();
taskStore.clearTask();
}
// 重置任务状态
this.streamingMessage = false;
this.taskInProgress = false;
this.stopRequested = false;
this.waitingForSubAgent = false;
}
```
---
## 7. 完整流程图
```
用户点击对话列表
检查是否已是当前对话
↓ (否)
检查是否有运行中的任务
↓ (无任务或用户确认)
调用 loadConversation(conversationId)
发送 PUT /api/conversations/{id}/load
后端终止运行中的子智能体
后端加载对话数据
后端广播 conversation_changed 事件
前端更新 currentConversationId
前端更新 currentConversationTitle
前端调用 resetAllStates()
├─ 重置消息和流状态
├─ 清理工具状态
├─ 清理Markdown缓存
├─ 重置输入状态
└─ 重置 lastHistoryLoadedConversationId
前端调用 fetchAndDisplayHistory()
├─ 检查是否已加载 (lastHistoryLoadedConversationId)
├─ 设置加载状态 (historyLoading = true)
├─ 发送 GET /api/conversations/{id}/messages
├─ 检查对话是否已切换 (防止过期响应)
├─ 清空当前消息 (this.messages = [])
└─ 调用 renderHistoryMessages(messages)
├─ 遍历历史消息
├─ 处理用户消息 (images, videos)
├─ 处理AI消息 (reasoning_content, tool_calls)
├─ 处理工具结果 (tool role)
├─ 关联工具调用和结果
└─ 更新 lastHistoryLoadedConversationId
前端滚动到底部
前端获取Token统计
完成
```
---
## 8. 关键文件索引
### 前端文件
| 文件路径 | 主要功能 |
|---------|---------|
| `/static/src/app/methods/conversation.ts` | 对话管理核心功能 |
| `/static/src/app/methods/history.ts` | 历史消息加载和渲染 |
| `/static/src/app/methods/ui.ts` | UI状态管理和路由解析 |
| `/static/src/app/state.ts` | 应用状态定义 |
| `/static/src/app/lifecycle.ts` | 生命周期钩子 |
| `/static/src/stores/conversation.ts` | 对话状态存储 |
### 后端文件
| 文件路径 | 主要功能 |
|---------|---------|
| `/server/conversation.py` | 对话API端点 |
| `/utils/conversation_manager.py` | 对话持久化管理 |
---
## 9. 最佳实践和注意事项
### 9.1 防止重复加载
1. 使用 `lastHistoryLoadedConversationId` 标记已加载的对话
2. 使用 `historyLoadSeq` 序列号防止过期响应
3. 检查 `historyLoading` 状态避免并发加载
### 9.2 用户体验优化
1. 切换对话前检查运行中的任务,提示用户确认
2. 加载失败时不显示弹窗,仅在控制台记录
3. 使用标题打字效果提升视觉体验
4. 自动滚动到底部,确保用户看到最新消息
### 9.3 性能优化
1. 并行加载路由和初始数据
2. 使用 `$nextTick` 确保DOM更新后再滚动
3. 清理Markdown缓存避免内存泄漏
4. 使用序列号机制丢弃过期响应
### 9.4 状态一致性
1. 前端和后端同步 `currentConversationId`
2. 浏览器历史状态与应用状态保持一致
3. 切换对话时完整重置UI状态
4. 使用 `initialRouteResolved` 标记防止冲突
---
## 10. 总结
AI Agent 系统的对话加载机制设计精巧,通过多层缓存、状态标记、序列号机制等手段,确保了对话切换的流畅性和数据一致性。核心特点包括:
1. **防重复加载**: 使用 `lastHistoryLoadedConversationId``historyLoadSeq` 防止重复加载
2. **并发保护**: 使用序列号机制丢弃过期响应
3. **用户体验**: 任务确认、加载动画、自动滚动等细节优化
4. **状态管理**: 完整的状态重置和同步机制
5. **初始化处理**: 使用 `initialRouteResolved` 防止路由冲突
这套机制为多对话管理提供了坚实的基础,值得在类似系统中借鉴。

View File

@ -1,741 +0,0 @@
# AI Agent 系统停止按钮和运行状态机制分析
## 概述
本文档详细分析 AI Agent 系统中停止按钮和运行状态机制的实现,这是系统中 bug 最多的部分。系统采用 REST API + 轮询模式(`usePollingMode: true`),已禁用 WebSocket 实时推送。
## 1. 停止按钮的显示/隐藏逻辑
### 1.1 组件定义
**文件位置**: `/static/src/components/input/InputComposer.vue:48-61`
```vue
<button
type="button"
class="stadium-btn send-btn"
@click="$emit('send-or-stop')"
:disabled="
!isConnected ||
(inputLocked && !streamingMessage) ||
(mediaUploading && !streamingMessage) ||
((!(inputMessage || '').trim() && (!selectedImages?.length && !selectedVideos?.length)) && !streamingMessage)
"
>
<span v-if="streamingMessage" class="stop-icon"></span>
<span v-else class="send-icon"></span>
</button>
```
### 1.2 显示条件
停止按钮的显示由 `streamingMessage` 状态控制:
- **显示停止按钮**: `streamingMessage === true`
- **显示发送按钮**: `streamingMessage === false`
### 1.3 禁用条件
按钮被禁用的情况:
1. 未连接服务器 (`!isConnected`)
2. 输入被锁定且不在流式输出中 (`inputLocked && !streamingMessage`)
3. 媒体上传中且不在流式输出中 (`mediaUploading && !streamingMessage`)
4. 没有输入内容且不在流式输出中
### 1.4 关键状态变量
**文件位置**: `/static/src/app/state.ts:23`
```typescript
taskInProgress: false, // 任务是否在进行中(用于保持输入区的"停止"状态)
```
**文件位置**: `/static/src/stores/chat.ts:78`
```typescript
streamingMessage: false, // 是否正在流式输出消息
```
**文件位置**: `/static/src/app/computed.ts:160-166`
```typescript
streamingUi() {
return this.streamingMessage || this.hasPendingToolActions();
},
composerBusy() {
const monitorLock = this.monitorIsLocked && this.chatDisplayMode === 'monitor';
return this.streamingUi || this.taskInProgress || monitorLock || this.stopRequested;
}
```
## 2. 点击停止按钮的完整流程
### 2.1 前端事件处理
**文件位置**: `/static/src/app/methods/message.ts:104-110`
```typescript
handleSendOrStop() {
if (this.composerBusy) {
this.stopTask();
} else {
this.sendMessage();
}
},
```
### 2.2 停止任务方法
**文件位置**: `/static/src/app/methods/message.ts:254-282`
```typescript
async stopTask() {
const canStop = this.composerBusy && !this.stopRequested;
if (!canStop) {
return;
}
const shouldDropToolEvents = this.streamingUi;
this.stopRequested = true;
this.dropToolEvents = shouldDropToolEvents;
// 使用 REST API 取消任务
try {
const { useTaskStore } = await import('../../stores/task');
const taskStore = useTaskStore();
if (taskStore.currentTaskId) {
await taskStore.cancelTask();
debugLog('[Message] 任务已取消');
}
} catch (error) {
console.error('[Message] 取消任务失败:', error);
}
// 立即清理前端状态,避免出现"不可输入也不可停止"的卡死状态
this.clearPendingTools('user_stop');
this.streamingMessage = false;
this.taskInProgress = false;
this.forceUnlockMonitor('user_stop');
},
```
### 2.3 REST API 取消请求
**文件位置**: `/static/src/stores/task.ts:185-214`
```typescript
async cancelTask() {
if (!this.currentTaskId) {
return;
}
try {
debugLog('[Task] 取消任务:', this.currentTaskId);
const response = await fetch(`/api/tasks/${this.currentTaskId}/cancel`, {
method: 'POST',
});
if (!response.ok) {
throw new Error('取消任务失败');
}
const result = await response.json();
if (!result.success) {
throw new Error(result.error || '取消任务失败');
}
this.taskStatus = 'canceled';
this.stopPolling();
debugLog('[Task] 任务已取消');
} catch (error) {
console.error('[Task] 取消任务失败:', error);
throw error;
}
},
```
### 2.4 后端接收停止请求
**文件位置**: `/server/tasks.py:372-379`
```python
@tasks_bp.route("/api/tasks/<task_id>/cancel", methods=["POST"])
@api_login_required
def cancel_task_api(task_id: str):
username = get_current_username()
ok = task_manager.cancel_task(username, task_id)
if not ok:
return jsonify({"success": False, "error": "任务不存在"}), 404
return jsonify({"success": True})
```
### 2.5 设置停止标志
**文件位置**: `/server/tasks.py:144-164`
```python
def cancel_task(self, username: str, task_id: str) -> bool:
rec = self.get_task(username, task_id)
if not rec:
return False
rec.stop_requested = True
# 标记停止标志chat_flow 会检测 stop_flags
entry = stop_flags.get(task_id)
if not isinstance(entry, dict):
entry = {'stop': False, 'task': None, 'terminal': None}
stop_flags[task_id] = entry
entry['stop'] = True
# 注释掉直接取消任务,改为通过停止标志让任务内部处理
# try:
# if entry.get('task') and hasattr(entry['task'], "cancel"):
# entry['task'].cancel()
# except Exception:
# pass
with self._lock:
rec.status = "cancel_requested"
rec.updated_at = time.time()
return True
```
### 2.6 聊天流程检测停止标志
系统在多个关键位置检查停止标志:
**位置1**: `/server/chat_flow_tool_loop.py:23-30` - 工具调用前检查
```python
# 检查停止标志
client_stop_info = get_stop_flag(client_sid, username)
if client_stop_info:
stop_requested = client_stop_info.get('stop', False) if isinstance(client_stop_info, dict) else client_stop_info
if stop_requested:
debug_log("在工具调用过程中检测到停止状态")
# ... 取消工具调用
```
**位置2**: `/server/chat_flow_tool_loop.py:229-236` - 工具执行过程中检查
```python
check_count += 1
client_stop_info = get_stop_flag(client_sid, username)
if client_stop_info:
stop_requested = client_stop_info.get('stop', False) if isinstance(client_stop_info, dict) else client_stop_info
if stop_requested:
debug_log(f"[停止检测] 工具执行过程中检测到停止请求(检查次数:{check_count}),立即取消工具")
tool_task.cancel()
tool_cancelled = True
```
**位置3**: `/server/chat_flow_stream_loop.py:46-53` - 流式输出过程中检查
```python
# 检查停止标志
client_stop_info = get_stop_flag(client_sid, username)
if client_stop_info:
stop_requested = client_stop_info.get('stop', False) if isinstance(client_stop_info, dict) else client_stop_info
if stop_requested:
debug_log(f"检测到停止请求,中断流处理")
cancel_pending_tools(tool_calls_list=tool_calls, sender=sender, messages=messages)
sender('task_stopped', {...})
```
**位置4**: `/server/chat_flow_task_support.py:147-154` - 命令执行过程中检查
```python
while time.time() < deadline:
client_stop_info = get_stop_flag(client_sid, username)
if client_stop_info:
stop_requested = client_stop_info.get('stop', False) if isinstance(client_stop_info, dict) else client_stop_info
if stop_requested:
sender('task_stopped', {
'message': '命令执行被用户取消',
'reason': 'user_stop'
})
```
## 3. 运行状态的设置和清除
### 3.1 taskInProgress 状态管理
**设置为 true 的时机**:
1. **发送消息时** (`/static/src/app/methods/message.ts:200`)
```typescript
this.taskInProgress = true;
```
2. **AI消息开始时** (`/static/src/app/methods/taskPolling.ts:128,138`)
```typescript
this.taskInProgress = true;
```
3. **收到用户消息事件时** (`/static/src/app/methods/taskPolling.ts:566`)
```typescript
this.taskInProgress = true;
```
4. **恢复任务状态时** (`/static/src/app/methods/taskPolling.ts:739,882`)
```typescript
this.taskInProgress = true;
```
**设置为 false 的时机**:
1. **任务完成时** (`/static/src/app/methods/taskPolling.ts:538`)
```typescript
this.taskInProgress = false;
```
2. **任务错误时** (`/static/src/app/methods/taskPolling.ts:588`)
```typescript
this.taskInProgress = false;
```
3. **停止任务时** (`/static/src/app/methods/message.ts:280`)
```typescript
this.taskInProgress = false;
```
4. **创建任务失败时** (`/static/src/app/methods/message.ts:218`)
```typescript
this.taskInProgress = false;
```
### 3.2 streamingMessage 状态管理
**设置为 true 的时机**:
1. **AI消息开始时** (`/static/src/app/methods/taskPolling.ts:130,141`)
```typescript
this.streamingMessage = true;
```
2. **恢复任务状态时** (`/static/src/app/methods/taskPolling.ts:738,881`)
```typescript
this.streamingMessage = true;
```
**设置为 false 的时机**:
1. **任务完成时** (`/static/src/app/methods/taskPolling.ts:532`)
```typescript
this.streamingMessage = false;
```
2. **任务错误时** (`/static/src/app/methods/taskPolling.ts:587`)
```typescript
this.streamingMessage = false;
```
3. **停止任务时** (`/static/src/app/methods/message.ts:279`)
```typescript
this.streamingMessage = false;
```
4. **收到用户消息事件时** (`/static/src/app/methods/taskPolling.ts:567`)
```typescript
this.streamingMessage = false;
```
### 3.3 stopRequested 状态管理
**设置为 true 的时机**:
1. **停止任务时** (`/static/src/app/methods/message.ts:261`)
```typescript
this.stopRequested = true;
```
**设置为 false 的时机**:
1. **AI消息开始时** (`/static/src/app/methods/taskPolling.ts:129,140`)
```typescript
this.stopRequested = false;
```
2. **任务完成时** (`/static/src/app/methods/taskPolling.ts:533`)
```typescript
this.stopRequested = false;
```
3. **任务错误时** (`/static/src/app/methods/taskPolling.ts:589`)
```typescript
this.stopRequested = false;
```
## 4. 状态转换图
```
[用户点击发送]
[taskInProgress = true]
[streamingMessage = false]
[创建任务 POST /api/tasks]
[开始轮询 GET /api/tasks/{id}?from={offset}]
[收到 ai_message_start 事件]
[streamingMessage = true]
[显示停止按钮]
┌─────────────────────────────────┐
│ 用户点击停止按钮 │
│ ↓ │
│ [stopRequested = true] │
│ ↓ │
│ [POST /api/tasks/{id}/cancel] │
│ ↓ │
│ [stop_flags[task_id]['stop'] = True] │
│ ↓ │
│ [立即清理前端状态] │
│ [streamingMessage = false] │
│ [taskInProgress = false] │
│ [stopRequested = false] │
│ ↓ │
│ [显示发送按钮] │
└─────────────────────────────────┘
[收到 task_complete 事件]
[streamingMessage = false]
[taskInProgress = false]
[stopRequested = false]
[停止轮询]
[显示发送按钮]
```
## 5. 事件流程图
### 5.1 正常完成流程
```
前端 后端 任务线程
│ │ │
├─ sendMessage() │ │
├─ taskInProgress = true │ │
├─ POST /api/tasks ─────────>│ │
│ ├─ create_chat_task() │
│ ├─ thread.start() ──────────>│
│ │ ├─ run_chat_task_sync()
<──── task_id ──────────────┤ │
├─ startPolling() │ │
│ │ │
├─ GET /api/tasks/{id} ─────>│ │
<──── events ────────────────┤ │
├─ handleTaskEvent() │ │
├─ streamingMessage = true │ │
│ │ │
│ (轮询继续...) │ │
│ │ │
│ │ ├─ 任务完成
│ │<──── task_complete ────────┤
├─ GET /api/tasks/{id} ─────>│ │
<──── task_complete ─────────┤ │
├─ handleTaskComplete() │ │
├─ streamingMessage = false │ │
├─ taskInProgress = false │ │
├─ stopPolling() │ │
```
### 5.2 用户停止流程
```
前端 后端 任务线程
│ │ │
├─ (任务运行中) │ │
├─ streamingMessage = true │ │
│ │ │
├─ stopTask() │ │
├─ stopRequested = true │ │
├─ POST /api/tasks/{id}/cancel ─>│ │
│ ├─ cancel_task() │
│ ├─ stop_flags[task_id]['stop'] = True
<──── success ──────────────┤ │
├─ streamingMessage = false │ │
├─ taskInProgress = false │ │
├─ stopRequested = false │ │
│ │ │
│ │ ├─ get_stop_flag()
│ │ ├─ 检测到停止标志
│ │ ├─ 取消工具调用
│ │ ├─ sender('task_stopped')
│ │<──── task_stopped ─────────┤
│ ├─ status = 'canceled' │
│ ├─ stop_flags.pop(task_id) │
```
## 6. 停止后的UI状态变化
### 6.1 输入框状态
**文件位置**: `/static/src/components/input/InputComposer.vue:41`
```vue
:disabled="!isConnected || streamingMessage || inputLocked"
```
- **停止前**: `streamingMessage = true` → 输入框禁用
- **停止后**: `streamingMessage = false` → 输入框启用
### 6.2 停止按钮变为发送按钮
**文件位置**: `/static/src/components/input/InputComposer.vue:59-60`
```vue
<span v-if="streamingMessage" class="stop-icon"></span>
<span v-else class="send-icon"></span>
```
- **停止前**: 显示停止图标
- **停止后**: 显示发送图标
### 6.3 工具块状态
停止时会调用 `clearPendingTools('user_stop')`,清理所有待处理的工具调用。
## 7. 已知问题和Bug
### 7.1 停止按钮无法点击
**问题描述**: 在某些情况下,停止按钮显示但无法点击。
**可能原因**:
1. `composerBusy` 计算错误,导致 `canStop` 条件不满足
2. `stopRequested` 已经为 true阻止重复停止
3. 按钮被其他元素遮挡CSS z-index 问题)
**相关代码**: `/static/src/app/methods/message.ts:255-258`
```typescript
const canStop = this.composerBusy && !this.stopRequested;
if (!canStop) {
return;
}
```
### 7.2 停止后任务仍在运行
**问题描述**: 点击停止按钮后,前端状态已清除,但后端任务仍在执行。
**可能原因**:
1. 后端停止标志检查不够频繁
2. 某些长时间运行的工具没有检查停止标志
3. 停止标志设置和检查之间存在竞态条件
**相关代码**:
- `/server/chat_flow_tool_loop.py:229-236` - 工具执行过程中每 0.1 秒检查一次
- `/server/chat_flow_task_support.py:147-154` - 命令执行过程中检查
### 7.3 状态不同步
**问题描述**: 前端和后端的任务状态不一致。
**可能原因**:
1. 轮询间隔过长150ms导致状态更新延迟
2. 事件处理顺序错误
3. 多个状态变量(`taskInProgress`, `streamingMessage`, `stopRequested`)更新不原子
**相关代码**: `/static/src/stores/task.ts:154-157`
```typescript
// 设置定时轮询150ms 间隔,接近流式输出效果)
this.pollingInterval = window.setInterval(() => {
this.pollTaskEvents(handler);
}, 150);
```
### 7.4 页面刷新后状态丢失
**问题描述**: 页面刷新后,运行中的任务状态无法正确恢复。
**解决方案**: 已实现 `restoreTaskState()` 方法,在页面加载时查找运行中的任务并恢复状态。
**相关代码**: `/static/src/app/methods/taskPolling.ts:637-921`
```typescript
async restoreTaskState() {
// 查找运行中的任务
const runningTask = await taskStore.loadRunningTask(this.currentConversationId);
if (!runningTask) {
return;
}
// 恢复状态
this.streamingMessage = true;
this.taskInProgress = true;
// 启动轮询
taskStore.startPolling((event: any) => {
this.handleTaskEvent(event);
});
}
```
### 7.5 立即清理导致的问题
**问题描述**: 停止任务时立即清理前端状态,可能导致后端仍在发送事件,前端无法正确处理。
**相关代码**: `/static/src/app/methods/message.ts:278-281`
```typescript
// 立即清理前端状态,避免出现"不可输入也不可停止"的卡死状态
this.clearPendingTools('user_stop');
this.streamingMessage = false;
this.taskInProgress = false;
this.forceUnlockMonitor('user_stop');
```
**潜在风险**:
- 后端可能仍在发送 `tool_start`, `text_chunk` 等事件
- 前端已清理状态,这些事件会被忽略或导致错误
- 可能出现 UI 不一致的情况
### 7.6 dropToolEvents 机制
**问题描述**: `dropToolEvents` 标志用于在停止后忽略工具事件,但可能导致状态不一致。
**相关代码**:
- `/static/src/app/methods/message.ts:262` - 设置标志
- `/static/src/app/methods/taskPolling.ts:294-296` - 检查标志
```typescript
if (this.dropToolEvents) {
return;
}
```
**潜在问题**:
- 如果后端发送了 `task_complete` 事件,但前端已经 `dropToolEvents`,可能导致状态不清理
- `dropToolEvents` 何时清除不明确
### 7.7 composerBusy 计算复杂
**问题描述**: `composerBusy` 依赖多个状态变量,计算逻辑复杂,容易出错。
**相关代码**: `/static/src/app/computed.ts:163-166`
```typescript
composerBusy() {
const monitorLock = this.monitorIsLocked && this.chatDisplayMode === 'monitor';
return this.streamingUi || this.taskInProgress || monitorLock || this.stopRequested;
}
```
**问题**:
- `streamingUi` 又依赖 `streamingMessage``hasPendingToolActions()`
- 多层依赖导致状态更新顺序敏感
- 难以调试和维护
## 8. 改进建议
### 8.1 统一状态管理
建议使用单一的状态机模式,而不是多个独立的布尔变量:
```typescript
enum TaskState {
IDLE = 'idle',
CREATING = 'creating',
RUNNING = 'running',
STREAMING = 'streaming',
STOPPING = 'stopping',
STOPPED = 'stopped',
COMPLETED = 'completed',
FAILED = 'failed'
}
state: {
taskState: TaskState.IDLE
}
```
### 8.2 增加停止确认机制
在前端立即清理状态后,等待后端确认停止成功:
```typescript
async stopTask() {
this.stopRequested = true;
// 发送停止请求
await taskStore.cancelTask();
// 等待后端确认(轮询直到收到 task_stopped 或 task_complete
await this.waitForStopConfirmation();
// 清理前端状态
this.streamingMessage = false;
this.taskInProgress = false;
this.stopRequested = false;
}
```
### 8.3 增加停止超时机制
如果后端在一定时间内没有响应停止请求,强制清理前端状态:
```typescript
const STOP_TIMEOUT = 5000; // 5秒
setTimeout(() => {
if (this.stopRequested) {
// 强制清理
this.streamingMessage = false;
this.taskInProgress = false;
this.stopRequested = false;
}
}, STOP_TIMEOUT);
```
### 8.4 改进停止标志检查频率
在长时间运行的操作中,增加停止标志检查频率:
```python
# 当前: 每 0.1 秒检查一次
await asyncio.sleep(0.1)
# 建议: 每 0.05 秒检查一次
await asyncio.sleep(0.05)
```
### 8.5 添加停止日志
在所有停止相关的代码路径中添加详细日志,便于调试:
```typescript
debugLog('[Stop] 用户点击停止按钮', {
composerBusy: this.composerBusy,
stopRequested: this.stopRequested,
taskInProgress: this.taskInProgress,
streamingMessage: this.streamingMessage,
currentTaskId: taskStore.currentTaskId
});
```
### 8.6 前端状态恢复优化
页面刷新后的状态恢复逻辑复杂,建议:
1. 简化恢复逻辑,只恢复必要的状态
2. 添加恢复失败的降级处理
3. 提供手动刷新按钮,让用户可以重新加载状态
## 9. 总结
停止按钮和运行状态机制是系统中最复杂的部分之一涉及前端多个状态变量、REST API 轮询、后端停止标志检查等多个环节。主要问题包括:
1. **状态管理复杂**: 多个独立的布尔变量(`taskInProgress`, `streamingMessage`, `stopRequested`)难以维护
2. **停止响应延迟**: 后端停止标志检查不够频繁,导致停止响应慢
3. **状态同步问题**: 前端立即清理状态,但后端可能仍在运行,导致状态不一致
4. **页面刷新恢复**: 状态恢复逻辑复杂,容易出错
建议采用状态机模式统一管理任务状态,增加停止确认机制,改进停止标志检查频率,并添加详细的调试日志。

View File

@ -1,692 +0,0 @@
# AI Agent 系统任务恢复与推送机制分析
## 概述
本文档详细分析 AI Agent 系统中**正在运行中的对话恢复机制和推送机制**,重点关注页面刷新后如何恢复任务状态、重放历史事件以及维护 UI 一致性。
---
## 1. 页面刷新后的任务恢复流程
### 1.1 恢复触发时机
任务恢复在页面挂载时立即触发:
**文件**: `/static/src/app/lifecycle.ts`
```typescript
export async function mounted() {
// ... 其他初始化代码
// 注册全局事件处理器(用于任务轮询)
(window as any).__taskEventHandler = (event: any) => {
if (typeof this.handleTaskEvent === 'function') {
this.handleTaskEvent(event);
}
};
// 立即尝试恢复运行中的任务(不延迟)
if (typeof this.restoreTaskState === 'function') {
this.restoreTaskState();
}
}
```
**关键点**:
- 在 `mounted()` 生命周期钩子中立即调用 `restoreTaskState()`
- 不延迟执行,确保用户刷新后能快速看到任务恢复
- 先注册全局事件处理器 `__taskEventHandler`,用于后续轮询事件分发
### 1.2 查找运行中的任务
**文件**: `/static/src/stores/task.ts` - `loadRunningTask()`
```typescript
async loadRunningTask(conversationId: string | null = null) {
try {
debugLog('[Task] 查找运行中的任务');
// 1. 获取所有任务列表
const response = await fetch('/api/tasks');
const result = await response.json();
// 2. 查找运行中的任务可选按对话ID过滤
const runningTask = result.data.find((task: any) =>
task.status === 'running' &&
(!conversationId || task.conversation_id === conversationId)
);
if (runningTask) {
// 3. 更新任务状态
this.currentTaskId = runningTask.task_id;
this.taskStatus = runningTask.status;
this.taskCreatedAt = runningTask.created_at;
this.taskUpdatedAt = runningTask.updated_at;
// 4. 获取任务详情,计算已处理的事件数量
const detailResponse = await fetch(`/api/tasks/${runningTask.task_id}`);
if (detailResponse.ok) {
const detailResult = await detailResponse.json();
if (detailResult.success && detailResult.data.events) {
// 设置为当前事件数量,只获取新事件
this.lastEventIndex = detailResult.data.next_offset || detailResult.data.events.length;
debugLog('[Task] 设置起始偏移量:', this.lastEventIndex);
}
}
return runningTask;
}
return null;
} catch (error) {
console.error('[Task] 加载运行中任务失败:', error);
return null;
}
}
```
**关键逻辑**:
1. 调用 `/api/tasks` 获取用户的所有任务
2. 筛选 `status === 'running'` 且匹配当前对话ID的任务
3. 获取任务详情,计算 `lastEventIndex`(已处理的事件数量)
4. 返回运行中的任务信息
---
## 2. `_rebuildingFromScratch` 标志机制
### 2.1 标志的作用
`_rebuildingFromScratch` 是一个关键的布尔标志,用于区分两种恢复模式:
- **从头重建模式** (`true`): 历史消息不完整或缺失,需要从头重放所有事件来重建 assistant 响应
- **增量恢复模式** (`false`): 历史消息完整,只需恢复状态并继续轮询新事件
### 2.2 何时设置为 true
**文件**: `/static/src/app/methods/taskPolling.ts` - `restoreTaskState()`
```typescript
// 检查是否需要从头重建
const historyActionsCount = lastMessage?.actions?.length || 0;
const eventCount = allEvents.length;
const historyIncomplete = eventCount > historyActionsCount + 5; // 允许5个事件的误差
const needsRebuild = !isAssistantMessage ||
(isAssistantMessage && (!lastMessage.actions || lastMessage.actions.length === 0)) ||
historyIncomplete;
if (needsRebuild) {
debugLog('[TaskPolling] 需要从头重建 assistant 响应');
// 清空不完整的 assistant 消息
if (isAssistantMessage) {
this.messages.pop();
}
// 重置偏移量为 0从头获取所有事件
taskStore.lastEventIndex = 0;
// 标记正在从头重建
this._rebuildingFromScratch = true;
this._rebuildingEventCount = allEvents.length;
// 启动轮询
taskStore.startPolling((event: any) => {
this.handleTaskEvent(event);
});
// 延迟清除重建标记2秒后
setTimeout(() => {
this._rebuildingFromScratch = false;
this._rebuildingEventCount = 0;
}, 2000);
}
```
**触发条件**:
1. **最后一条消息不是 assistant 消息**: 说明历史加载有问题
2. **最后一条是空的 assistant 消息**: `actions` 数组为空或不存在
3. **历史不完整**: 事件数量远大于历史中的 actions 数量允许5个事件的误差
### 2.3 对事件处理的影响
`_rebuildingFromScratch` 标志会影响多个事件处理函数的行为:
#### 2.3.1 思考块处理
```typescript
handleThinkingStart(data: any, eventIdx: number) {
// ... 创建思考块
// 只在非历史恢复时展开思考块
if (!this._rebuildingFromScratch) {
this.chatExpandBlock(blockId);
}
}
handleThinkingEnd(data: any) {
// ... 完成思考块
// 只在非历史恢复时延迟折叠思考块
if (!this._rebuildingFromScratch) {
setTimeout(() => {
this.chatCollapseBlock(blockId);
}, 1000);
} else {
// 历史恢复时立即折叠,不需要动画
this.chatCollapseBlock(blockId);
}
}
```
**差异**:
- **从头重建**: 不展开思考块,立即折叠,跳过动画
- **正常流式**: 展开思考块延迟1秒后折叠有动画效果
#### 2.3.2 工具意图打字机效果
```typescript
handleToolIntent(data: any) {
const isHistoryRestore = this._rebuildingFromScratch;
if (isHistoryRestore) {
// 历史恢复,直接显示完整 intent
action.tool.intent_rendered = newIntent;
} else {
// 新工具块,逐字符显示(打字机效果)
action.tool.intent_rendered = '';
action.tool._intentTyping = true;
// 逐字符显示动画
const typeNextChar = () => {
if (charIndex < newIntent.length && action.tool._intentTyping) {
action.tool.intent_rendered += newIntent[charIndex];
charIndex++;
this.$forceUpdate();
action.tool._intentTimer = setTimeout(typeNextChar, charInterval);
}
};
action.tool._intentTimer = setTimeout(typeNextChar, 50);
}
}
```
**差异**:
- **从头重建**: 直接显示完整 intent无打字机效果
- **正常流式**: 逐字符显示,有打字机动画
#### 2.3.3 AI 消息开始处理
```typescript
handleAiMessageStart(data: any, eventIdx: number) {
// 如果是从头重建,标记消息为静默恢复
if (this._rebuildingFromScratch) {
const newMessage = this.messages[this.messages.length - 1];
if (newMessage && newMessage.role === 'assistant') {
newMessage.awaitingFirstContent = false;
newMessage.generatingLabel = '';
}
}
}
```
**差异**:
- **从头重建**: 清除"生成中"标签,静默恢复
- **正常流式**: 显示"生成中"动画
---
## 3. 历史事件的重放机制
### 3.1 事件获取
**后端**: `/server/tasks.py` - `get_task_api()`
```python
@tasks_bp.route("/api/tasks/<task_id>", methods=["GET"])
@api_login_required
def get_task_api(task_id: str):
username = get_current_username()
rec = task_manager.get_task(username, task_id)
if not rec:
return jsonify({"success": False, "error": "任务不存在"}), 404
# 获取偏移量参数
try:
offset = int(request.args.get("from", 0))
except Exception:
offset = 0
# 过滤事件(只返回 idx >= offset 的事件)
events = [e for e in rec.events if e["idx"] >= offset]
next_offset = events[-1]["idx"] + 1 if events else offset
return jsonify({
"success": True,
"data": {
"task_id": rec.task_id,
"status": rec.status,
"events": events,
"next_offset": next_offset,
}
})
```
**关键点**:
- 支持 `from` 参数,只返回 `idx >= from` 的事件
- 返回 `next_offset`,用于下次轮询
- 事件存储在 `deque(maxlen=1000)`最多保留1000个事件
### 3.2 事件重放顺序
**文件**: `/static/src/stores/task.ts` - `pollTaskEvents()`
```typescript
async pollTaskEvents(eventHandler: (event: any) => void) {
const response = await fetch(
`/api/tasks/${this.currentTaskId}?from=${this.lastEventIndex}`
);
const result = await response.json();
const data = result.data;
// 处理新事件(按顺序)
if (data.events && data.events.length > 0) {
for (const event of data.events) {
try {
eventHandler(event);
} catch (err) {
console.error('[Task] 处理事件失败:', err, event);
}
}
// 更新偏移量
this.lastEventIndex = data.next_offset;
}
}
```
**重放顺序**:
1. 按事件的 `idx` 顺序(从小到大)
2. 同步处理,确保顺序正确
3. 每个事件处理完后才处理下一个
### 3.3 重放时的特殊处理
**文件**: `/static/src/app/methods/taskPolling.ts` - `handleTaskEvent()`
```typescript
handleTaskEvent(event: any) {
// 1. 检查事件的 conversation_id 是否匹配
if (eventData.conversation_id && this.currentConversationId) {
if (eventData.conversation_id !== this.currentConversationId) {
debugLog(`忽略不匹配的事件`);
return;
}
}
// 2. 根据事件类型调用对应的处理方法
switch (eventType) {
case 'ai_message_start':
this.handleAiMessageStart(eventData, eventIdx);
break;
case 'thinking_start':
this.handleThinkingStart(eventData, eventIdx);
break;
// ... 其他事件类型
}
// 注意:不要在这里清除 _rebuildingFromScratch 标记
// 该标记应该在恢复完所有历史事件后才清除
}
```
**特殊处理**:
- **对话ID过滤**: 忽略不匹配当前对话的事件
- **跳过动画**: 通过 `_rebuildingFromScratch` 标志跳过动画效果
- **静默恢复**: 不显示"生成中"标签,不展开思考块
---
## 4. 恢复时的 UI 状态管理
### 4.1 思考块状态恢复
**文件**: `/static/src/app/methods/taskPolling.ts` - `restoreTaskState()`
```typescript
// 恢复思考块状态
if (lastMessage.actions) {
const thinkingActions = lastMessage.actions.filter(a => a.type === 'thinking');
if (inThinking && thinkingActions.length > 0) {
// 正在思考中,检查最后一个思考块是否正在流式输出
const lastThinking = thinkingActions[thinkingActions.length - 1];
// 只有当思考块正在流式输出时才设置锁定状态(但不展开)
if (lastThinking.streaming && lastThinking.blockId) {
this.$nextTick(() => {
this.chatSetThinkingLock(lastThinking.blockId, true);
});
}
}
// 确保所有思考块都是折叠状态
for (const thinking of thinkingActions) {
if (thinking.blockId) {
thinking.collapsed = true;
}
}
}
```
**状态恢复**:
- **折叠状态**: 所有思考块默认折叠
- **锁定状态**: 如果正在流式输出,设置锁定(防止用户手动展开)
- **不展开**: 即使正在流式输出,也不自动展开(避免干扰用户)
### 4.2 工具块状态恢复
```typescript
// 注册历史中的工具块到 toolActionIndex
const toolActions = lastMessage.actions.filter(a => a.type === 'tool');
for (const toolAction of toolActions) {
if (toolAction.tool && toolAction.tool.id) {
// 注册到 toolActionIndex
this.toolRegisterAction(toolAction, toolAction.tool.id);
// 如果有 executionId也注册
if (toolAction.tool.executionId) {
this.toolRegisterAction(toolAction, toolAction.tool.executionId);
}
// 追踪工具调用
if (toolAction.tool.name) {
this.toolTrackAction(toolAction.tool.name, toolAction);
}
}
}
```
**关键点**:
- **注册工具块**: 将历史中的工具块注册到 `toolActionIndex`
- **支持更新**: 后续的 `update_action` 事件可以找到对应的块进行状态更新
- **双重注册**: 同时注册 `id``executionId`,确保能找到
### 4.3 停止按钮状态
```typescript
// 标记状态为进行中
this.streamingMessage = true;
this.taskInProgress = true;
```
**状态设置**:
- `streamingMessage = true`: 显示停止按钮
- `taskInProgress = true`: 禁用输入框
- `stopRequested = false`: 重置停止请求标志
### 4.4 输入框状态
输入框状态由 `taskInProgress` 控制:
```typescript
// 在模板中
<input :disabled="taskInProgress" />
```
**状态**:
- `taskInProgress = true`: 输入框禁用
- `taskInProgress = false`: 输入框启用
---
## 5. 轮询的恢复机制
### 5.1 轮询启动
**文件**: `/static/src/stores/task.ts` - `startPolling()`
```typescript
startPolling(eventHandler?: (event: any) => void) {
if (this.isPolling) {
return;
}
this.isPolling = true;
// 获取事件处理器
const handler = eventHandler || ((window as any).__taskEventHandler);
// 立即执行一次
this.pollTaskEvents(handler);
// 设置定时轮询150ms 间隔)
this.pollingInterval = window.setInterval(() => {
this.pollTaskEvents(handler);
}, 150);
}
```
**关键点**:
- **立即执行**: 启动轮询后立即执行一次,不等待第一个间隔
- **150ms 间隔**: 接近流式输出效果
- **全局处理器**: 使用 `window.__taskEventHandler` 作为默认处理器
### 5.2 从哪个 offset 开始轮询
**两种模式**:
#### 5.2.1 从头重建模式
```typescript
// 重置偏移量为 0从头获取所有事件
taskStore.lastEventIndex = 0;
```
- **offset = 0**: 从第一个事件开始
- **目的**: 重建完整的 assistant 响应
#### 5.2.2 增量恢复模式
```typescript
// 获取任务详情,计算已处理的事件数量
const detailResponse = await fetch(`/api/tasks/${runningTask.task_id}`);
const detailResult = await detailResponse.json();
if (detailResult.success && detailResult.data.events) {
// 设置为当前事件数量,只获取新事件
this.lastEventIndex = detailResult.data.next_offset || detailResult.data.events.length;
}
```
- **offset = next_offset**: 从历史事件之后开始
- **目的**: 只获取新事件,避免重复处理
### 5.3 避免重复处理事件
**机制**:
1. **事件索引**: 每个事件有唯一的 `idx`
2. **偏移量过滤**: 后端只返回 `idx >= offset` 的事件
3. **偏移量更新**: 处理完事件后更新 `lastEventIndex = next_offset`
```typescript
// 处理新事件
if (data.events && data.events.length > 0) {
for (const event of data.events) {
eventHandler(event);
}
// 更新偏移量,下次轮询从这里开始
this.lastEventIndex = data.next_offset;
}
```
**保证**:
- 每个事件只处理一次
- 不会遗漏事件
- 不会重复处理
---
## 6. WebSocket vs 轮询的恢复差异
### 6.1 WebSocket 模式(已废弃)
**特点**:
- **实时推送**: 服务器主动推送事件
- **连接断开**: 刷新页面后 WebSocket 连接断开
- **无法恢复**: 断开期间的事件丢失
**问题**:
- 页面刷新后无法恢复任务状态
- 需要复杂的重连和事件补偿机制
### 6.2 轮询模式(当前实现)
**特点**:
- **主动拉取**: 客户端主动轮询事件
- **无连接状态**: 不依赖持久连接
- **完整恢复**: 可以从任意 offset 开始拉取事件
**优势**:
- 页面刷新后可以完整恢复任务状态
- 实现简单,无需处理重连逻辑
- 支持从头重建和增量恢复两种模式
### 6.3 恢复流程对比
| 特性 | WebSocket 模式 | 轮询模式 |
|------|---------------|---------|
| 连接状态 | 有状态(需要重连) | 无状态 |
| 事件恢复 | 困难(需要补偿机制) | 简单(从 offset 拉取) |
| 实时性 | 高(服务器推送) | 中150ms 轮询) |
| 可靠性 | 低(连接断开丢失事件) | 高(事件持久化) |
| 刷新恢复 | 不支持 | 完全支持 |
---
## 7. 完整恢复流程图
```
页面刷新
mounted() 生命周期
注册全局事件处理器 __taskEventHandler
调用 restoreTaskState()
检查是否已有任务在进行 ──→ 是 ──→ 跳过恢复
↓ 否
调用 taskStore.loadRunningTask()
查询 /api/tasks 获取任务列表
筛选 status === 'running' 的任务 ──→ 无 ──→ 恢复子智能体等待状态
↓ 有
获取任务详情 /api/tasks/{task_id}
计算 lastEventIndex已处理的事件数量
等待历史加载完成
检查历史完整性
├─→ 历史不完整 ──→ 从头重建模式
│ ↓
│ 删除不完整的 assistant 消息
│ ↓
│ 设置 _rebuildingFromScratch = true
│ ↓
│ 设置 lastEventIndex = 0
│ ↓
│ 启动轮询(从头重放所有事件)
│ ↓
│ 2秒后清除 _rebuildingFromScratch 标志
└─→ 历史完整 ──→ 增量恢复模式
恢复思考块状态(折叠、锁定)
恢复工具块状态(注册到 toolActionIndex
恢复文本块状态streaming 标志)
设置 streamingMessage = true
设置 taskInProgress = true
启动轮询(只获取新事件)
显示"任务恢复"提示
```
---
## 8. 关键代码文件索引
### 8.1 前端文件
| 文件路径 | 功能 |
|---------|------|
| `/static/src/app/lifecycle.ts` | 页面生命周期,触发恢复 |
| `/static/src/stores/task.ts` | 任务状态管理,轮询控制 |
| `/static/src/app/methods/taskPolling.ts` | 事件处理,状态恢复 |
| `/static/src/app/methods/history.ts` | 历史消息加载和渲染 |
| `/static/src/app/methods/conversation.ts` | 对话管理 |
### 8.2 后端文件
| 文件路径 | 功能 |
|---------|------|
| `/server/tasks.py` | 任务 API事件存储和查询 |
| `/server/chat_flow.py` | 聊天流程,事件生成 |
---
## 9. 最佳实践和注意事项
### 9.1 事件设计
1. **每个事件必须有唯一的 idx**: 用于偏移量过滤
2. **事件必须包含 conversation_id**: 用于过滤不匹配的事件
3. **事件必须按顺序生成**: 确保 idx 递增
4. **事件必须持久化**: 存储在 `deque(maxlen=1000)`
### 9.2 状态恢复
1. **先加载历史,再恢复任务**: 确保有完整的消息列表
2. **检查历史完整性**: 对比事件数量和 actions 数量
3. **注册工具块**: 确保后续的 `update_action` 事件能找到对应的块
4. **设置正确的偏移量**: 避免重复处理或遗漏事件
### 9.3 UI 体验
1. **跳过动画**: 从头重建时跳过所有动画效果
2. **静默恢复**: 不显示"生成中"标签
3. **折叠思考块**: 默认折叠,避免干扰用户
4. **显示提示**: 恢复成功后显示"任务恢复"提示
### 9.4 错误处理
1. **任务不存在**: 恢复子智能体等待状态
2. **历史加载失败**: 延迟重试最多5次
3. **事件处理失败**: 捕获异常,继续处理下一个事件
4. **对话切换**: 忽略不匹配的事件
---
## 10. 总结
AI Agent 系统的任务恢复机制通过以下关键技术实现:
1. **事件持久化**: 所有事件存储在后端,支持从任意 offset 查询
2. **轮询模式**: 客户端主动拉取事件,无需维护连接状态
3. **双模式恢复**: 支持从头重建和增量恢复两种模式
4. **智能判断**: 根据历史完整性自动选择恢复模式
5. **UI 优化**: 跳过动画、静默恢复,提升用户体验
6. **状态同步**: 恢复思考块、工具块、文本块的完整状态
这套机制确保了用户在页面刷新后能够无缝恢复任务,不会丢失任何进度,同时保持了良好的用户体验。

View File

@ -1,522 +0,0 @@
# 配置系统改造计划
> 版本v2.0 | 日期2026-06-13 | 基于 OpenCode 架构分析 + 当前代码现状
---
## 〇、最终目录结构(已确认)
### 目录树
```
~/.astrion/astrion/ ← 数据根目录data_root
├── settings.json ← 唯一配置文件
├── config/ ← 部署级 JSON两个模式共享
│ ├── custom_models.json
│ ├── host_workspaces.json
│ ├── host_sandbox_policy.json
│ ├── forbidden_commands.json
│ └── ...
├── host/ ← host 模式(单用户)
│ ├── data/ ← 对话/记忆/用户数据库
│ └── logs/
└── web/ ← web 模式(多用户)
├── users/ ← 每个用户独立工作区
│ ├── alice/
│ │ └── data/ ← alice 的对话/记忆
│ └── bob/
│ └── data/
└── logs/ ← web 服务级日志
```
> **注意**host 模式没有 `users/`(单用户直接走 `data/`web 模式根下没有 `data/`(数据分散在各 `users/<name>/` 下)。
### 元配置 vs 普通配置
**元配置**(启动前必须知道,不在 `settings.json` 里):
| 元配置项 | 设定方式 | 默认值 |
|---------|---------|--------|
| `data_root` | CLI `--data-root` / 环境变量 `ASTRION_DATA_ROOT` | `~/.astrion/astrion` |
**普通配置**(启动后加载,在 `settings.json` 里):
| 分类 | 示例 |
|------|------|
| 运行模式 | `terminal.sandbox_mode`host/web |
| 服务 | `server.port`、`server.host` |
| 模型 | `models.default_model_key` |
| 终端 | `terminal.execution_mode_default`、沙箱参数 |
| 管理员 | `admin.username`、`admin.password_hash` |
| Flags | `flags.api_dump_enabled` 等 |
### 启动流程
```
1. 解析 CLI args / 环境变量 → 确定 data_root默认 ~/.astrion/astrion
2. 在 {data_root}/ 下创建目录结构(若不存在)
3. 加载 {data_root}/settings.json
4. 根据 settings.json 中的 sandbox_mode 确定数据子目录host/ 或 web/
5. 初始化子目录下的 data/ users/ logs/
6. 加载 {data_root}/config/ 下的部署级 JSON
7. 启动 Web 服务
```
关键约束:
- `data_root` 是元配置,**不在** `settings.json` 里(否则鸡生蛋)
- `config/` 在根目录,两个模式**共享**custom_models 不应该切换模式就丢失)
- 首次启动时若 `settings.json` 不存在,使用硬编码默认值 + 自动弹出初始化向导
与 OpenCode 对照:
```
OpenCode: ~/.config/opencode/config.json + ~/.local/share/opencode/
我们: {data_root}/settings.json + {data_root}/{mode}/
其中 data_root 默认 ~/.astrion/astrion
```
---
## 一、现状诊断
### 1.1 架构问题全景图
```
当前启动流程:
./start.sh
→ _bootstrap.sh创建 venv、pip install、npm ci & build
→ 若 .env 不存在 → python -m scripts.setup交互式向导写出 .env
→ exec python -m server.app
→ import config触达 _load_dotenv(),锁定一切 ↓)
┌──────────────────────────────────────────────────────────┐
│ import config 瞬间,以下全部固化为模块级常量: │
│ │
│ config/paths.py RUNTIME_ROOT, DATA_DIR, LOGS_DIR, │
│ USER_SPACE_DIR, DEFAULT_PROJECT_PATH │
│ config/server.py WEB_SERVER_PORT, WEB_SERVER_HOST │
│ config/terminal.py TERMINAL_SANDBOX_MODE, ... │
│ config/auth.py ADMIN_USERNAME, ADMIN_PASSWORD_HASH │
│ config/model_profiles.py 模型列表 │
└──────────────────────────────────────────────────────────┘
当前目录结构问题web 模式数据也落在 host/ 下,分流名存实亡):
~/.astrion/
├── host/ ← 实际数据全在这里(因为 .env 写死 host
│ ├── .env ← ❌ 配置居然放在数据目录里
│ ├── config/
│ ├── data/
│ ├── users/
│ └── logs/
└── web/ ← 几乎空,从未被真正使用
```
### 1.2 .env 被 6 处独立读取
| # | 文件 | 读取方式 | 问题 |
|---|------|---------|------|
| 1 | `config/__init__.py:_load_dotenv()` | 主加载器,读入 `os.environ` | 唯一的"正规"入口,但逻辑是 .env 不覆盖已有环境变量 |
| 2 | `config/auth.py:_dotenv_cache()` | 直接读 `.env` 文件,有独立缓存 | **优先级反转**:优先 .env 再回退 os.environ |
| 3 | `config/model_profiles.py:_env_optional()` | 先 os.environ回退读 `.env` | 与主加载器逻辑重复 |
| 4 | `utils/api_client.py:_resolve_env_value()` | 同上 | 同上 |
| 5 | `server/chat_flow_helpers.py:_env_optional()` | 同上 | 同上 |
| 6 | `modules/balance_client.py:_read_dotenv()` | 直接读 `.env`,有独立缓存 | 第 3 份 .env 解析实现 |
**核心问题**没有统一的配置来源6 个模块各有一套 .env 解析逻辑,优先级不一致。
### 1.3 历史遗留代码
| 文件 | 问题 |
|------|------|
| `web_server.py` | 已标记 deprecated但仍存在且是 `main.py` 的实际调用路径 |
| `main.py` | CLI 模式代码是死代码(`setup_run_mode()` 硬编码 `self.web_mode = True` |
| `server/app.py` | 完全转发 `app_legacy.py`"逐步替换"从未实现 |
| `server/app_legacy.py` | 1409 行巨型文件,包含了从路由到工具执行的几乎所有逻辑 |
### 1.4 初始化脚本
```
setup.sh → _bootstrap.sh共享引导→ scripts/setup.py交互向导
start.sh → _bootstrap.sh共享引导→ python -m server.app
```
- `scripts/setup.py` 刻意不 `import config`(怕过早锁定路径),说明架构本身有问题
- 每次启动都走 `ensure_python_env` / `ensure_node_env`
---
## 二、OpenCode 架构参考
### 2.1 核心设计原则
| 原则 | OpenCode 做法 |
|------|-------------|
| **配置存储** | JSON(C) 文件 + 环境变量,**无 .env 文件** |
| **加载时机** | 运行时 Effect Service惰性按需加载 |
| **路径标准** | XDG 标准(`~/.config/opencode`、`~/.local/share/opencode` |
| **配置层级** | 9 层优先级叠加全局→项目→远程→托管→MDM |
| **变量引用** | `{env:VAR}` / `{file:path}` 语法 |
| **运行时修改** | `Config.update()` / `Config.updateGlobal()` 写回 JSON |
| **依赖管理** | Effect-TS Layer 依赖注入,服务间松耦合 |
### 2.2 可借鉴的关键模式
**模式 1运行时 Service 而非模块常量**
```python
# ❌ 当前做法
from config import DATA_DIR # import 时固化
# ✅ 目标做法
from config.manager import config_manager
data_dir = config_manager.get("DATA_DIR") # 运行时获取,支持热更新
```
**模式 2分层配置优先级**
```
优先级(高→低):
运行时 Setconfig_manager.set()
→ 环境变量覆盖
→ 项目级配置(<workspace>/.astrion/config.json
→ 全局配置(~/.astrion/astrion/settings.json
→ 默认值
```
**模式 3统一 ConfigManager 单例**
```
ConfigManager
├── load() 从 JSON 文件加载
├── get(key) 获取配置值
├── set(key, value) 运行时修改 + 持久化
├── reload() 重新加载(配置热更新)
├── subscribe() 变更监听
└── _resolve_path() 路径解析
```
**模式 4Flag 系统**
```python
# 所有 feature flag 集中定义
class Flags:
DISABLE_AUTOCOMPACT = _flag("AGENT_DISABLE_AUTOCOMPACT")
ENABLE_API_DUMP = _flag("AGENT_API_DUMP_ENABLED")
# ...
```
---
## 三、改造目标
### 3.1 最终状态
```
用户启动
→ python -m server.app [--data-root ~/.astrion/astrion]
→ 解析 data_root元配置CLI > 环境变量 > 默认值)
→ ConfigManager.initialize(data_root)
→ 加载 {data_root}/settings.json
→ 解析 sandbox_mode → 确定数据子目录 {data_root}/{mode}/
→ 加载 {data_root}/config/ 下的部署级 JSON
→ Web 服务启动
→ 管理后台 UI 可实时修改 settings.json 中的配置
→ 配置变更自动通知相关组件
```
### 3.2 核心原则
1. **启动后再配置**:除最低限度启动参数(端口等)外,所有配置可在运行时修改
2. **单一配置源**:所有模块统一通过 `ConfigManager` 获取配置
3. **JSON 为主、.env 退役**:配置文件统一为 `{data_root}/settings.json`。.env 仅作为开发环境快捷方式(可选),不再参与正式配置流程
4. **元配置与普通配置分离**`data_root` 等必须在加载配置前确定的参数走 CLI/环境变量,其余走 `settings.json`
4. **向后兼容**:分阶段改造,每阶段可独立上线
5. **最小改动**:先统一读取入口,再逐步改造消费端
---
## 四、分阶段改造计划
### 阶段 1统一配置源预计改动文件数~8
**目标**:消除 6 处独立 .env 读取,所有配置从单一入口获取。
#### 1.1 新建 `config/manager.py`
```python
"""
ConfigManager — 统一配置管理器(单例)
职责:
- 从 JSON 配置文件加载配置
- .env 仅作为"种子"ConfigManager 加载后统一接管
- 所有模块通过 ConfigManager 获取配置,不再直接读 .env 或 os.environ
"""
class ConfigManager:
_instance = None
def __init__(self):
self._data: dict = {}
self._seeds: dict = {} # 从 .env 一次性读入的种子值
self._callbacks: list = []
# ── 初始化 ──
def initialize(self, mode: str | None = None):
"""启动时调用一次。加载 .env 种子 → 合并 JSON 配置 → 应用默认值。"""
self._load_env_seeds()
self._load_json_config(mode)
self._apply_defaults()
# ── 读取 ──
def get(self, key: str, default=None):
"""获取配置值。优先级:运行时 Set > 环境变量 > JSON 配置 > 默认值。"""
def get_path(self, key: str) -> str:
"""获取路径类配置(自动展开 ~、转绝对路径)。"""
# ── 写入 ──
def set(self, key: str, value):
"""运行时修改配置(持久化到 JSON + 通知订阅者)。"""
# ── 热更新 ──
def reload(self):
"""重新加载 JSON 配置(保留运行时 Set 的覆盖)。"""
def on_change(self, callback):
"""注册配置变更回调。"""
```
#### 1.2 统一改造点
| 当前位置 | 改造方式 |
|---------|---------|
| `config/__init__.py:_load_dotenv()` | 替换为 `config_manager.initialize()` |
| `config/auth.py:_dotenv_cache()` | 改为 `config_manager.get("AGENT_ADMIN_USERNAME")` |
| `config/model_profiles.py:_env_optional()` | 改为 `config_manager.get(...)`,移除 .env 回退 |
| `utils/api_client.py:_resolve_env_value()` | 同上 |
| `server/chat_flow_helpers.py:_env_optional()` | 同上 |
| `modules/balance_client.py:_read_dotenv()` | 同上 |
| `config/terminal.py` | 常量改为从 `config_manager` 获取 |
| `config/server.py` | 同上(端口等可保留环境变量覆盖,但统一入口) |
#### 1.3 兼容性策略
```python
# config/__init__.py 保持向后兼容
from config.manager import config_manager
# 旧的 from config import DATA_DIR 仍然可用(通过模块级 __getattr__ 代理)
def __getattr__(name):
return config_manager.get(name)
```
#### 1.4 配置文件格式
```jsonc
// ~/.astrion/astrion/settings.json唯一配置不分 host/web
{
"terminal": {
"sandbox_mode": "host", // "host" | "web"
"execution_mode_default": "sandbox",
"direct_ttl_seconds": 600
},
"server": {
"port": 8091,
"host": "127.0.0.1"
},
"paths": {
"project_path": null // null = ./project相对于仓库根目录
},
"models": {
"default_model_key": null
},
"flags": {
"api_dump_enabled": false
}
}
```
路径解析规则:
- `data_root` 未设置时,默认 `~/.astrion/astrion`
- `data_root` 设置后host/web 子目录在其下创建
- 模式子目录:`{data_root}/{sandbox_mode}/`,如 `~/.astrion/astrion/host/data/`
#### 1.5 验证标准
- [ ] 删除项目根目录 `.env` 后,程序仍能正常启动(使用默认值)
- [ ] 所有现有功能不受影响
- [ ] `grep -rn 'parents\[1\].*\.env'` 结果中只剩 `config/manager.py``scripts/setup.py`
---
### 阶段 2运行时配置修改预计改动文件数~10
**目标**:管理后台 UI 可修改配置,无需重启。
#### 2.1 ConfigManager 增强
```python
class ConfigManager:
def set(self, key: str, value):
"""运行时修改配置"""
self._data[key] = value
self._persist() # 写回 JSON
self._notify(key, value) # 通知订阅者
def subscribe(self, key: str, callback):
"""订阅特定配置项的变更"""
def get_all(self) -> dict:
"""获取完整配置(供管理后台展示)"""
```
#### 2.2 前端管理后台新增"系统设置"页面
```
/ 管理后台
├── 用户管理
├── 模型配置 (已有)
├── 系统设置 (新增)
│ ├── 运行模式host/web
│ ├── 数据目录
│ ├── Web 服务(端口/地址)
│ ├── 终端沙箱配置
│ └── 管理员账户
└── ...
```
#### 2.3 不可运行时修改的配置
以下配置修改后需要手动重启才能生效,管理后台应有明确提示:
| 配置项 | 原因 |
|--------|------|
| `server.port` | Flask/SocketIO 已在监听端口 |
| `server.host` | 同上 |
| `terminal.sandbox_mode` | 影响终端管理器初始化 |
| `data_root` | 元配置,启动后数据目录已锁定 |
#### 2.4 验证标准
- [ ] 在管理后台修改模型配置后,新对话立即生效(无需重启)
- [ ] 修改数据目录路径后,新数据写入新位置
- [ ] 修改 Web 端口时,前端提示"需要重启"
---
### 阶段 3分层配置 + 项目级覆盖(预计改动文件数:~6
**目标**:支持全局配置 + 项目级配置 + 环境变量覆盖。
#### 3.1 配置优先级
```
优先级(高→低):
1. 运行时 Setconfig_manager.set(),内存中)
2. 环境变量覆盖os.environ
3. 项目级配置(<workspace>/.astrion/config.json
4. 全局配置(~/.astrion/astrion/<mode>/config/settings.json
5. 默认值(硬编码)
```
#### 3.2 变量引用语法
```jsonc
// 配置文件中支持变量引用
{
"model": {
"base_url": "{env:AGENT_API_BASE_URL}",
"api_key": "{env:AGENT_API_KEY}"
},
"prompt": {
"custom": "{file:~/my_prompt.txt}"
}
}
```
- `{env:VAR}` — 引用环境变量
- `{file:path}` — 引用文件内容(`~/` 展开为用户主目录)
#### 3.3 项目级配置
```python
# 当用户选择了工作区后,自动加载项目级配置
config_manager.load_project_config(workspace_path)
# → 读取 <workspace_path>/.astrion/config.json
# → 浅层覆盖全局配置
```
#### 3.4 验证标准
- [ ] 不同工作区可以有不同配置(如不同默认模型)
- [ ] `{env:VAR}` 语法正常工作
- [ ] 配置文件缺失时回退到上一级,不报错
---
### 阶段 4清理历史遗留预计改动文件数~5
**目标**:移除屎山代码,精简入口。
#### 4.1 清理清单
| 文件 | 操作 |
|------|------|
| `web_server.py` | 删除(已是兼容入口,无实际逻辑) |
| `main.py` | 删除 CLI 模式死代码(`setup_project_path`、`setup_run_mode` 等交互式部分),精简为纯 Web 启动 |
| `server/app.py` | 合并 `app_legacy.py` 的核心逻辑,"转发"改为"所有权" |
| `server/app_legacy.py` | 拆分为多个模块chat/、tasks/、conversation/ 已有蓝图,继续拆分) |
#### 4.2 标准化启动
```bash
# 唯一的启动方式
python -m server.app [--port 8091] [--host 127.0.0.1]
# 或保留 start.sh 作为便利脚本
./start.sh
```
移除 `setup.sh``start.sh` 的双脚本依赖,合并为一个入口。
#### 4.3 验证标准
- [ ] `python -m server.app` 正常工作
- [ ] 旧的 `python web_server.py` 报错提示使用新入口
- [ ] `python main.py` 仍可工作但输出 deprecation warning
---
## 五、文件变更清单总览
| 阶段 | 新增文件 | 修改文件 | 删除文件 |
|------|---------|---------|---------|
| 1 | `config/manager.py` | `config/__init__.py`, `config/auth.py`, `config/model_profiles.py`, `config/paths.py`, `config/terminal.py`, `config/server.py`, `utils/api_client.py`, `server/chat_flow_helpers.py`, `modules/balance_client.py` | — |
| 2 | `static/src/components/admin/SystemSettings.vue`(前端) | `config/manager.py`(增强), `server/` 新增 `/api/admin/settings` 路由 | — |
| 3 | — | `config/manager.py`(增强分层加载、变量解析) | — |
| 4 | — | `main.py`(精简), `server/app.py`(接管) | `web_server.py` |
---
## 六、风险控制
1. **每阶段独立可上线**:阶段 1 完成后即可合并,不影响阶段 2-4 的进度
2. **向后兼容**:阶段 1 保留 `from config import DATA_DIR` 语法,通过 `__getattr__` 代理
3. **回滚安全**:配置文件备份机制(`settings.json.bak_<timestamp>`
4. **渐进式清理**:先统一入口,再逐个模块迁移,不搞大爆炸重构
---
## 七、OpenCode vs 我们的对照速查表
| 概念 | OpenCode | 我们的目标 |
|------|---------|-----------|
| 全局配置路径 | `~/.config/opencode/config.json` | `{data_root}/settings.json`(默认 `~/.astrion/astrion/settings.json` |
| 项目配置 | `<cwd>/opencode.json(c)` 向上查找 | `<workspace>/.astrion/config.json` |
| 配置 Service | `Config.Service` (Effect-TS) | `ConfigManager` (Python 单例) |
| 变量引用 | `{env:VAR}` / `{file:path}` | 同 |
| 运行时修改 | `Config.update()` / `Config.updateGlobal()` | `config_manager.set()` |
| Flag 系统 | `Flag.OPENCODE_*` 集中定义 | `config/flags.py` 集中定义 |
| 分层加载 | 9 层 mergeDeep | 4 层优先级 |
| 路径标准 | XDG (`xdg-basedir`) | `{data_root}/` + `{mode}/`data_root 由 CLI/环境变量指定 |
| 初始化入口 | CLI handler → Runtime → Bootstrap → Config.get() | `server.app``config_manager.initialize()` → 服务启动 |

View File

@ -1,204 +0,0 @@
# 对话加载统一化计划防回写A+ 统一加载协议B
> 2026-07-20 制定。状态:实施中。
> 前置事实报告:`sub_agent_results/frontend_load_flow/frontend_load_flow.md`(前端链路代码事实)。
## 1. 背景与动机
### 1.1 P0旧内存回写数据丢失当日三次实锤
8092 实例 `conversation_save_debug.log` 证据:
| 时间 | 对话 | 缩减 | 调用栈末端 |
|------|------|------|-----------|
| 19:51:10 | conv_20260720_193008_709 | 542→506 | `load_conversation_by_id``save_current_conversation` |
| 19:29:27 | conv_20260720_155350_423 | 506→101 | 同上 |
| 15:53:50 | conv_20260720_151851_314 | 104→77 | `start_new_conversation``save_current_conversation` |
| 15:53:22 | conv_20260720_151851_314 | 103→77 | `WebTerminal.__del__``save_current_conversation` |
根因:工作区级 terminal`@with_terminal` 注入)与对话级 terminal`get_user_resources(conversation_id)`)是两个对象、两份 `conversation_history` 内存。任务在对话级实例上推进历史,工作区级实例停留在旧状态;其"切换/新建对话前保存当前对话"和析构保存把旧历史写回磁盘,覆盖新消息。
已有防御(`8628954f` 非空守卫、debug trace 的 `wipe_suspect`)只检测"空列表覆盖",不检测"旧非空覆盖新非空"。
### 1.2 P1运行中刷新的两段式体验
- 第一段:`GET /{id}/messages` 返回**文件态**历史(文件只在每条消息完成时 auto_save运行中的流式 chunk 不在其中),表现为"加载到最近一条完整消息处"。
- 第二段:`restoreTaskState()` 与历史加载并行启动但互相等待:等 `messages` 非空500ms×8 死等,最长 4s→ 串行 `GET /api/tasks` + `GET /api/tasks/{id}``lastEventIndex=0` 全量事件重放。断裂感 = 重试粒度 + 串行请求 + 全量重放。
### 1.3 P3入口与职责混乱
- "进入对话"需 4 个接口PUT load、GET messages、GET tasks、GET running-status+ 事件轮询,无单一入口。
- 防重复加载靠三道手工保险丝(`skipConversationHistoryReload`、`lastHistoryLoadedConversationId`、事件 `task_id:idx` Set
- safe_nav / 全量 load 双轨制靠"工作区活跃任务"的**内存态**判定任务刚结束或进程重启后走错分支19:51 回写即因此进入全量 load
## 2. 方案 A防回写止血
### A1. `save_conversation` 防回退守卫(`utils/conversation_manager/crud_mixin.py`
- 新增参数 `allow_shrink: bool = False`
- 在 `existing_data = self.load_conversation(...)` 之后、覆写 `messages` 之前:
- `new_len < old_len``not allow_shrink` → 打印 🚨 警告并 `return False`,不写盘。
- 合法缩减路径显式豁免:**检查点恢复**`server/conversation.py` `_restore_checkpoint_to_conversation`)传 `allow_shrink=True`
> ⚠️ **语义已演进(方向 C 后)**:缩减写回不再拒绝,而是按 `message_id` 合并矫正(见 §7本小节描述为 A 落地时的历史设计,当前行为以代码为准。
- 已确认不需要豁免的路径压缩in-place 打 `deep_compacted` 标记消息数不减、设置保存原样传递内存历史、新建对话后同步模型old_len=0
### A2. `load_conversation_by_id` 同对话跳过切换前保存(`utils/context_manager/conversation_mixin.py`
- 现行为:加载任何对话前无条件 `save_current_conversation()`
- 改为:加载目标 == 当前对话(`conv_` 前缀归一化后比较)时跳过保存——接下来会以磁盘为准重载,保存不仅无意义,还是回写源头。
- 目标 != 当前对话时保留保存(内存领先时有效),落后时由 A1 守卫拦截。
### A3. debug trace 增强(同文件,临时调试代码)
- `_debug_conversation_save_trace` 增加 `shrink_suspect``new_len < old_len`字段弥补只检测 `new==0` 的盲区
- 该调试代码在确认问题根治后应整体移除(已记入项目记忆)。
## 3. 方案 B统一加载协议
### B1. 后端聚合接口 `GET /api/conversations/<id>/bootstrap`
新蓝图 `server/conversation_bootstrap.py`(注册于 `app_legacy.py`,不动 `server/conversation.py` 现有路由)。**只读语义**:不写对话文件、不调用 `terminal.load_conversation`、不切换工作区级 terminal 当前上下文,天然规避 safe_nav 双轨问题;但为聚合后台运行态会按需获取/创建对话级 terminal与既有 running-status 端点行为一致,属内存态操作)。
响应契约:
```jsonc
{
"success": true,
"data": {
"conversation_id": "conv_xxx",
"meta": {
"title": "...", "run_mode": "thinking", "thinking_mode": true,
"model_key": "...", "multi_agent_mode": false,
"permission_mode": "...", "execution_mode": "...", "network_permission": "...",
"messages_count": 542
},
"messages": [ /* 文件态消息全量,同 GET /{id}/messages */ ],
"running": {
// 复用 running-status 聚合逻辑(主 task + 传统子智能体 + 后台命令 + 多智能体)
"is_main_running": true, "main_task_id": "...", "main_task_type": "chat",
"has_running_sub_agents": false, "has_running_background_commands": false,
"has_running_multi_agent": false, "is_truly_active": true
},
"task_replay": { // 仅 is_main_running 时存在
"task_id": "...",
"event_count": 137,
"needs_rebuild": true, // 后端判定(见下)
"replay_from": 0 // needs_rebuild ? 0 : event_count从新事件续播
}
}
}
```
`needs_rebuild` 后端判定(移植自前端 `compression.ts:218-263` 判据,任一成立即 true
1. 文件态末尾消息不是 assistant
2. 文件态末尾是空 assistant无 content 且无 actions
3. 任务事件流显示处于流式中段(末尾区间存在未配对的 `thinking_chunk`/`text_chunk`,或 `ai_message_start` 之后无对应完成事件);
4. 文件态末尾 assistant 存在 status=working / streaming 的进行中 action。
实现要点:
- 任务事件来源:`task_manager.list_tasks(username)` 找该对话活跃 task复用 running-status 的查找逻辑),直接遍历 `rec.events`deque元素含 `idx`)。
- `get_user_resources(username, workspace_id, conversation_id)` 惰性创建对话级 terminal 用于读取 sub_agent_manager / background_command_manager 状态——A 修复后此路径安全。
- `@with_terminal` 注入的工作区级 terminal 仅用于取 `context_manager` 读文件,**绝不调用** `load_conversation`
### B2. 前端统一编排 `enterConversation(convId)`
新增 `static/src/app/methods/conversation/bootstrap.ts`,单一入口,取代"PUT load + GET messages + GET tasks + 死等重试"串行链:
```
enterConversation(convId, { source: 'refresh' | 'sidebar' })
state: idle → loading → ready → replaying → live
1. loadingsidebar 来源先 resetAllStates()(保留现有视觉语义)
2. GET bootstrap一次请求
3. 渲染 meta标题/模式/模型)+ renderHistoryMessages(messages)(复用现有渲染,改为接受数据而非自行 fetch
4. running.is_truly_active
- 主任务 → clearProcessedEvents() + lastEventIndex = task_replay.replay_from
+ startPolling()needs_rebuild 时先清空末尾 assistant 的 actions逻辑保留
+ toast「任务恢复」保留
- 仅后台活跃 → 交由现有对账 probe 兜底(不变)
5. ready/live
```
调用点改造:
- `bootstrapRoute()`route.ts解析 URL 后改调 `enterConversation`**移除 PUT load 调用**。
- `loadConversation()`conversation/load.ts改调 `enterConversation`,移除 PUT load 与显式 `fetchAndDisplayHistory`
- `fetchAndDisplayHistory`history.ts拆分为 `renderHistoryMessages(messages)`(纯渲染,保留)与数据获取(由 bootstrap 承担);保留 `lastHistoryLoadedConversationId` 防重。
- `restoreTaskState()`compression.ts删除"等 messages 非空"死等段与 `GET /api/tasks` 查找段(由 bootstrap 的 task_replay 取代);保留事件重放执行与恢复 toast。对账 `probe.ts` 不变(兜底)。
- `loadInitialData`socket.ts移除手动 `fetchAndDisplayHistory()` 调用(由 enterConversation 覆盖);`GET /api/conversations/current` 改为读取 bootstrap meta 的本地状态。
### B3. PUT load 的去留
- 后端路由**保留**API 兼容),前端进入对话不再调用。
- 工作区级 terminal 的"当前对话"不再被前端导航改变——这反而消除了 A 类的另一个回写触发面;其 `_ensure_conversation`(最近对话兜底)行为不变。
- `socketio.emit``conversation_changed`/`conversation_loaded` 仍由 PUT load 发出;前端为纯 REST 轮询,不依赖这两个事件(已验证 `initSocket` 空实现)。
## 4. 非目标(本轮不做)
- **P2**reasoning_content 历史路径渲染两遍):用户确认从未出现,不修。
- **方向 C**(内存历史仅作缓存、写入即落盘的单一事实来源):动 chat task 主循环A+B 验证稳定后再评估(本文档 §7
- 事件重放幂等性改造(保持现有从 replay_from 重放的语义)。
- Android/桌面端协议变更(同一前端 build随 Web 生效)。
## 5. 测试计划
1. **静态**`python3 -m py_compile` 改动文件;`npm run build`(含 stylelint/tsc
2. **冒烟**`python -m unittest test.test_server_refactor_smoke`。
3. **守卫单元脚本**`_experiments/verify_save_guard.py``ASTRION_DATA_ROOT=/tmp/...` 隔离):
- old=5/new=3 无豁免 → 拒绝且文件不变;`allow_shrink=True` → 通过;
- new≥old → 通过;`load_conversation_by_id` 同对话不触发 `save_current_conversation`mock 计数),异对话触发。
4. **隔离实例集成测试**:独立端口(如 8123+ `ASTRION_DATA_ROOT=/tmp/astrion_boot_test` 启动实例(不碰 8091/8092 及其数据):
- 建对话→发消息→`GET bootstrap` 断言 meta/messages/running 结构;
- 任务运行中调 `bootstrap` 断言 `task_replay.needs_rebuild/replay_from`
- 模拟回写场景(直接向 manager 塞旧历史后 save断言守卫拒绝。
5. **人工回归清单**(用户):刷新对话 / 侧边栏切换 / 运行中刷新(应无两段式断裂)/ 压缩后刷新 / 多智能体对话刷新。
## 6. 风险与回滚
| 风险 | 缓解 |
|------|------|
| 守卫误拦合法缩减 | 全量调用点已盘点(仅检查点恢复豁免);拦截仅拒绝写入并告警,不损坏现有文件 |
| 前端去 PUT load 后某隐式依赖工作区级 current 的功能异常 | PUT load 后端保留;`loadInitialData` 其余调用不变;人工回归清单覆盖 |
| needs_rebuild 误判导致内容重复或缺失 | 判据移植自前端现逻辑,语义不变;误判方向与现状一致(现状同样可能误判) |
| 回滚 | A、B 各为独立 commit可分别 revert前端保留 `restoreTaskState` 原路径代码注释标记,便于快速还原 |
## 7. 第三步(方向 C合并写C-merge
> 2026-07-20 调研定稿,用户拍板"先 merge 后评估 full"。A+B 已上线并人工回归通过。
### 7.1 调研结论(决定方案形态)
- **"写入即落盘"现状已达成**`add_conversation`唯一消息追加入口append 后立即同步全量落盘,无防抖。回退根源不是落盘时机,而是"多实例都能全量覆写"。
- 消息追加仅 1 入口(`message_mixin.add_conversation`);整体替换仅 4 处(加载/新建/浅压缩 in-place/清空)。
- 每条消息有稳定 `message_id``msg_<uuid4>`,存量抽查 100% 覆盖)→ 合并键可靠。
- 合法"覆写语义"仅两处:检查点恢复(已豁免);浅压缩是 in-place 打标不缩减、手动压缩是新建对话——均不需要缩减豁免。
### 7.2 方案merge-on-save合并写代替覆写
`crud_mixin.save_conversation``existing_data["messages"] = messages` 全量覆写改为按 `message_id` 合并:
1. **快路径**正常追加99%):内存 id 序列前缀包含磁盘 id 序列 → 直接用内存版(与现状一致)。
2. **慢路径**(旧实例写回/分叉):以磁盘为基,同 id 消息取内存版(浅压缩打标等修改生效),内存独有 id 消息按原序追加在后 → 任何实例任何时机写回都不丢消息,回退从根免疫。
3. **豁免路径**`allow_shrink=True`,仅检查点恢复):保持覆写语义。
4. A1 守卫演化为对 merged 的断言(恒不缩减,触发即 bug**缩减写回不再拒绝而是矫正**(磁盘不动 + 旧实例独有消息追加救回),优于 A 的纯拒绝。
5. 慢路径实质矫正发生时打印 `🔀 [ConvSaveMerge]` 醒目日志(观察期信号;无 id 消息防御性跳过追加并注释)。
### 7.3 场景演算
| 场景 | 磁盘 | 内存 | merge 结果 |
|------|------|------|-----------|
| 正常追加 | m1-5 | m1-5+m6 | m1-6同现状 |
| 旧实例回写 | m1-10 | m1-5 | m1-10等于没写 ✓ |
| 分叉 | m1-10 | m1-5',x1,x2 | m1-5',m6-10,x1,x2 全保留 ✓ |
| 浅压缩打标+旧实例 | m1-10 | m1'-5'(打标) | m1'-5',m6-10打标生效且不丢新消息 ✓ |
### 7.4 已知折衷
- 同 id 消息取内存版:若旧实例持有同 id 旧内容(如打标前版本)且触发慢路径,会盖回旧内容——消息不被修改是常态,概率极低,危害从"丢消息"降级为"单条字段旧"。
- metadata/todo 保持现状(内存覆写),不在本次范围。
- C-full内存纯缓存+单写点)留待 merge 观察期后评估§7.1 已证明边际收益有限)。
### 7.5 测试与回滚
- `verify_save_guard.py` 扩展:缩减矫正/分叉保留/快路径等价/打标生效/豁免覆写 5 类场景。
- 回滚:单 commit revert 即回到 A 守卫语义。

View File

@ -1,214 +0,0 @@
# 对话类型统一重构方案:消除「多智能体模式」全局状态
> 状态方案已确认v2含用户设计修正实施中
> 撰写日期2026-08-09
> 关联记忆:`multi_agent_mode_design`(本方案实施后将取代其前端部分)
---
## 1. 背景与问题
当前「多智能体模式」是一个**全局标签页状态**,由三样东西共同维持:
- 路由前缀:`/multiagent/<id>` vs `/<id>`
- 全局布尔:`multiAgentMode`app 级状态37 处引用、18 个文件)
- 列表过滤:所有对话列表请求携带 `multi_agent_mode=0|1`,两种模式的对话互相不可见
这套设计在双模式并行使用时暴露了一串耦合 bug2026-08 实录):
1. 多智能体标签页工作区1+ 传统标签页工作区2互拽视图——socket 用户级广播无条件跟随。
2. 多智能体模式下,侧边栏「按项目分组」的项目旁新建按钮把对话建成了传统模式——每个新建入口都要手动"继承模式",漏一个就是一个 bug。
3. 修复过程中发现同类耦合点越修越多(共享 session 工作区、创建落点、URL 前缀拼接……)。
**根因**:模式分裂本身制造了双标签页使用需求(两类对话只能在各自页面看到),而全局模式状态与共享 session、用户级广播交织出 N 个耦合点,修补式修复无法穷尽。
**产品决断**(用户拍板):多智能体是**对话的属性**,不是应用的模式。单标签页、单当前工作区是设计如此(后端对话级隔离已保证同/跨工作区多对话并行);两类对话进入同一个页面后,双标签页场景自然消失。
## 2. 目标 / 非目标
### 目标
- 删除全局「多智能体模式」状态;对话类型(`normal` | `multi_agent`**创建时确定、之后不可变**,权威存储于对话 `metadata.multi_agent_mode`(现有字段,零迁移)。
- 侧边栏两类对话**左右切换显示**(单类型视图 + 滑动动画),平铺列表与按项目分组两种视图套用同一规则。
- 输入栏底行(发送按钮所在行)`+` 按钮**左侧**新增「智能体/多智能体」类型选择按钮。
- URL 统一为 `/<id>``/multiagent/<id>`、`/multiagent/new` 做兼容重定向。
- 打开对话时从 metadata 恢复类型(现有 `enterConversation` 机制),渲染/terminal/上下文走对应路径。
### 非目标(明确不做)
- **不改多智能体运行时**Team Leader、子智能体管理、消息池派发、多智能体消息渲染`isMultiAgentMessage` 按消息级 metadata 判断)全部保留。
- **不做类型转换**:普通对话不能"升级"为多智能体。
- **不改分开存储**:多智能体对话仍由独立 conversation manager 存储。
- **不做多标签页/多设备视图跟随**:各视图独立是设计前提。
- **不改后端**:列表继续用现有 `multi_agent_mode=0|1` 服务端过滤(此时它是用户显式控制的视图过滤器,语义正确),端点全部保留。
## 3. 已确认的设计决策
| # | 决策点 | 结论 |
|---|--------|------|
| 1 | 类型选择器位置 | 输入栏**最下面固定行**(发送按钮所在行),`+` 按钮**左侧**的独立按钮;**不在 + 菜单内部** |
| 2 | 侧边栏呈现 | **左右切换**(一次只显示一个类型),控件位置:平铺模式(单工作区)放在「搜索对话」下面;分组模式(多工作区)放在**工作区标题行、新建对话等按钮的左边** |
| 3 | 切换动画 | 点击后对话记录显示区域**整体向左滑出**,多智能体列表**从右侧滑入**;切回时动画反向 |
| 4 | URL 方案 | 统一 `/<id>` 为主路由;`/multiagent/<id>` 重定向到 `/<id>` |
| 5 | 类型可变性 | 创建时确定,**不可变**;打开对话从 metadata 恢复 |
| 6 | 工作区模型 | 单当前工作区维持设计session 权威 |
**实施细节默认**(用户可否决):
- 侧边栏类型过滤器:`sidebarConversationType``'normal' | 'multi_agent'`单一全局状态、localStorage 持久化、默认 `'normal'`,同时驱动平铺/分组两种视图的列表请求(`maParam` 由它生成)与切换动画方向。
- 侧边栏新建按钮(含项目分组旁按钮):创建**当前过滤器显示的类型**(看着多智能体列表点新建,就建多智能体对话,立即出现在当前视图中)。
- /new 页首条消息创建:按输入栏选择器的 `newConversationType`
- 空过滤器下(某工作区无该类型对话):列表区域显示该类型的空态。
## 4. 技术地基已验证2026-08-09 代码核对)
| 验证点 | 结论 | 位置 |
|--------|------|------|
| 聊天执行的模式判定 | ✅ 已是**对话 metadata 驱动**:加载对话时 `self.multi_agent_mode = bool(meta.get("multi_agent_mode", False))`chat_flow 全部读 terminal 实例标志 | `core/web_terminal.py:438-443`、`server/chat_flow_task_main.py` 多处 |
| 列表服务端过滤 | ✅ `multi_agent_mode=0|1` 过滤已支持且分页正确(每个 manager 各自分页);侧边栏切换器继续用它,**后端零改动** | `utils/context_manager/conversation_mixin.py:388`、`server/conversation.py:524` |
| 按 ID 定位对话归属 | ✅ 按文件存在性跨两个存储目录解析,类型无关 | `conversation_mixin.py:86` |
| 对话元数据前端恢复 | ✅ `enterConversation` 统一加载协议已读 `meta.multi_agent_mode`(重构后写入对话类型) | `bootstrap.ts:66` |
| 前端模式状态规模 | 37 处引用、18 个文件,绝大多数是 `isMultiAgent ? A : B` 分支 | 见 §6 改动地图 |
| 数据迁移 | ✅ 零迁移;旧对话缺字段时后端有 `/api/multiagent/rebuild-index` 补全机制(当前在 `/multiagent/new` bootstrap 触发,重构后改为 `/new` 首次进入或应用启动时触发一次) | `route.ts bootstrapRoute` |
## 5. 总体设计
### 5.1 状态模型:从「全局模式」到「对话类型 + 两个局部选择状态」
**删除**
- `app/state.ts``multiAgentMode` 全局布尔
- `watchers.ts` 的 multiAgentMode → conversation store 同步
- `stores/conversation.ts``multiAgentMode` patch
**新增**(三个语义单一的状态,替代原来一个语义混杂的全局模式):
| 状态 | 语义 | 生命周期 |
|------|------|----------|
| `currentConversationType: 'normal' \| 'multi_agent' \| null` | 已打开对话的类型 | `enterConversation``meta.multi_agent_mode` 落地;空对话态为 `null` |
| `newConversationType: 'agent' \| 'multi_agent'` | 输入栏选择器的待创建类型 | localStorage 持久化,默认 `'agent'` |
| `sidebarConversationType: 'normal' \| 'multi_agent'` | 侧边栏过滤器当前显示的类型 | localStorage 持久化,默认 `'normal'` |
**派生规则**:打开对话后所有「是否多智能体」判断读 `currentConversationType`/new 空态的创建判断读 `newConversationType`;侧边栏列表/新建按钮读 `sidebarConversationType`。三者互不复用。
### 5.2 路由统一
- 主路由:`/<id>``stripConversationPrefix` 的 `conv_` 前缀美化保留,与类型无关)。
- `/multiagent/new``replaceState``/new`
- `/multiagent/<id>` → 正常 `enterConversation` 加载(类型从 meta 恢复),成功后 `replaceState``/<id>`
- `isExplicitNewConversationRoute()` 收缩为只判 `new`
- `enterConversation``urlPrefix` 参数删除。
### 5.3 输入栏类型选择器(新 UI
- **位置**:输入栏最下面固定行(发送按钮所在行),`+` 按钮**左侧**,独立于 + 菜单。
- **形态**:参考 `permission-switcher`(批准方式菜单)——按钮显示当前值 + caret点击展开下拉
- 「智能体」——单智能体对话(默认)
- 「多智能体」——Team Leader + 子智能体协作
- **空对话(/new**:可选,选中写 `newConversationType` + localStorage。
- **已有对话**:显示该对话类型,**禁用态**(类型不可变)。
- QuickMenu 内现有 `toggle-agent-mode` 入口(`@toggle-agent-mode` 事件链)移除。
- 样式遵守 AGENTS.md §5.5:无 glow、语义 token、固定高度、菜单不透明、内部滚动。
### 5.4 侧边栏左右切换
**控件**(分段切换,两项:普通对话 / 多智能体对话,或反向顺序):
- 平铺模式(单工作区):放在「搜索对话」输入框下面。
- 分组模式(多工作区):放在工作区标题行,新建对话等按钮的**左边**(所有工作区分组共享同一个过滤器状态,控件在每个分组标题行重复出现或仅当前展开组——实施时按 DOM 结构定,优先每行都有,保证所见即所得)。
**动画**:点击切换后,对话记录显示区域**整体向左滑出**,目标类型列表**从右侧滑入**;切回时反向。实现:两个列表 pane 包一层 `overflow:hidden` 轨道,`transform: translateX` + transition普通→多智能体方向向左反向向右Vue `<Transition mode="out-in">` 自定义类或双 pane 轨道均可,以不引起高度跳动为准。
**列表请求**:继续携带 `maParam`,但来源从「全局模式」改为「侧边栏过滤器状态」;切换时重置 offset 重新加载。分组视图 `loadWorkspaceConversations` 同理。
### 5.5 创建链路
| 入口 | 类型来源 | 端点 |
|------|----------|------|
| /new 首条消息send.ts | `newConversationType` | `'multi_agent'``/api/multiagent/conversations``preserve_mode:true`),否则 `/api/conversations` |
| 侧边栏「新建对话」blank 行为) | `sidebarConversationType` | 同上 |
| 项目分组旁新建按钮 | `sidebarConversationType` | 同上 + 该项目 `workspace_id` |
创建成功后统一 `/<id>` 跳转。
### 5.6 后端改动
**零改动**。列表过滤、创建端点、类型恢复全部复用现有能力。(原计划的合并列表分页修正取消——不合并,改为显式过滤。)
## 6. 改动地图
### 6.1 前端18 个文件,多数为删除分支)
| 文件 | 改动要点 |
|------|----------|
| `app/state.ts` | 删 `multiAgentMode`;加 `currentConversationType`、`newConversationType`、`sidebarConversationType` |
| `app/watchers.ts` | 删 multiAgentMode watch 及路由模式推导 |
| `app/methods/ui/route.ts` | bootstrapRoute 删 multiagent 模式分支,改为重定向;`isExplicitNewConversationRoute` 收缩rebuild-index 触发时机迁移 |
| `app/methods/ui/mode.ts` | `toggle-agent-mode` 相关方法清理 |
| `app/methods/conversation/bootstrap.ts` | meta → `currentConversationType`;删 `urlPrefix` 与模式前缀拼接 |
| `app/methods/conversation/load.ts` | `maParam` 改由 `sidebarConversationType` 生成 |
| `app/methods/conversation/action.ts` | 创建按 `sidebarConversationType` 选调端点;删 URL 模式前缀 |
| `app/methods/message/send.ts` | 首条消息创建按 `newConversationType`URL 统一 `/<id>` |
| `app/methods/search.ts` | 搜索过滤来源改为 `sidebarConversationType`(或全类型,实施时定) |
| `app/methods/taskPolling/sync.ts` | 删 URL 前缀拼接 |
| `app/methods/ui/hostWorkspace.ts` | 工作区切换后统一跳 `/new` |
| `stores/conversation.ts` | 分组加载 `maParam` 改来源;删 `multiAgentMode` patch |
| `stores/subAgent.ts` | 核对模式依赖(验证点 V3 |
| `composables/useLegacySocket.ts` | `conversation_resolved` 等处 URL 前缀删除 |
| `components/input/InputComposer.vue` | 底行 `+` 左侧新增类型选择器;删 `multi-agent-mode` prop 透传 |
| `components/input/QuickMenu.vue` | 移除 `toggle-agent-mode` 入口 |
| `components/sidebar/ConversationSidebar.vue` | 切换控件(两处位置)+ 双 pane 滑动动画 + 按类型渲染 |
| `App.vue` | 删模式相关 props/事件透传 |
### 6.2 文档与记忆
- `AGENTS.md` §11 多智能体模式章节重写为「对话类型」模型
- 项目记忆 `multi_agent_mode_design` 标注被本方案取代(前端部分);新增 `conversation_type_model` 记忆
## 7. 历史修复的取舍2026-08-09 评估)
| 批次 | 结论 | 理由 |
|------|------|------|
| `be061bec` stop_flags 任务级隔离 | **保留** | 真 bug任意并行对话互相停止与模式架构无关新世界同样需要 |
| `165c771c` 网络权限实例级快照 + 快捷窗口残留 | **保留** | 真 bug进程级 env 污染后台指令网络),与模式架构无关 |
| 未提交的广播过滤批6 文件socket 防拽视图/工作区本地化/显式 workspace_id/新建模式继承) | **已回退** | 错误路线:给双标签页耦合打补丁;新模式下该场景被设计消除,且新建继承等改动将被重构重写 |
回退后基线 = `165c771c`(即"第一次说这个问题"时的状态 + 两个独立真修复)。
## 8. 风险与缓解
| 风险 | 缓解 |
|------|------|
| `enterConversation``urlPrefix` 后调用方未对齐 | 全仓搜索 `urlPrefix` 逐一核对;手测刷新恢复 |
| 切换动画引起列表高度跳动/滚动位置丢失 | 双 pane 轨道 + 固定容器高度;切换时保留各自滚动位置 |
| 分组视图每行都放控件导致拥挤 | 实施时评审 DOM 结构,必要时仅当前展开组显示 |
| `/multiagent/*` 旧链接App 历史、书签)失效 | 重定向保留Android App 下次发版同步 |
| 暗藏的的模式依赖subAgent store、监控动画等 | 实施收尾时全仓 grep `multiAgentMode`/`multi_agent` 清零 |
## 9. 实施时验证点
- **V3**`stores/subAgent.ts` 的 `multiAgentMode` 具体用途
- **V4**QuickMenu 内 `toggle-agent-mode` 的现有处理链App.vue → mode.ts
- **V5**`App.vue` 中 `multiAgentMode` 的 props 透传链
- **V6**:分组模式工作区标题行的按钮组构成(确定切换控件插入点)
## 10. 实施阶段与验证计划
- **阶段 A状态与路由**:状态层三态替换 + 路由统一/重定向 → 构建通过
- **阶段 B输入栏选择器**:底行 `+` 左侧按钮 + 创建链路 → 构建通过
- **阶段 C侧边栏切换**:控件 + 滑动动画 + 列表过滤来源切换 → 构建通过
- **阶段 D清理**:残留 grep 清零 + 文档/记忆更新 + 冒烟
**手测清单**(完成后请用户实测):
1. /new 页输入栏左侧选「多智能体」发首条消息 → 创建多智能体对话URL 为 `/<id>`
2. 侧边栏切换控件:普通 ↔ 多智能体,列表滑动动画方向正确
3. 平铺/分组两种视图切换控件位置符合 §3-#2
4. 打开多智能体对话 → 选择器显示「多智能体」禁用态Team Leader 路径正常
5. 刷新页面 → 对话类型正确恢复;旧 `/multiagent/<id>` 链接重定向正常
6. 切工作区 → 列表随过滤器类型正确加载;分组旁新建按钮创建的类型 = 当前过滤器类型
## 11. 提交策略
1. 回退错误路线未提交改动(已完成,无痕)
2. **本方案文档 commit**`docs:` 单独一个)
3. 重构实施:阶段 A-C 一个 `refactor` commit或按需拆分阶段 D 文档更新并入
4. 工作区存在用户自己的未提交改动(`tools_execution.py`、`modules/multi_agent/tools.py`、`toolRenderers.ts`、`PersonalizationDrawer.vue`、`personalization.ts`、`agent_context.py`),任何 commit 不得混入

View File

@ -1,135 +0,0 @@
# 对话区快捷窗口栈 · 设计定稿2026-07-21
> 状态demo 已验收,正式接入 `static/src` 中。
> Demo 位置:`demo/todo_float_window/`(目录名为历史遗留,内含全部 4 个窗口;浏览器直接打开 `index.html`)。
> 本文档是唯一设计依据;压缩对话后凭本文档 + demo 继续。
## 0.1 正式接入变更2026-07-21 用户补充,覆盖 demo 的悬浮定位)
1. **命名**:该功能正式名称为「**快捷窗口**」quick dock / quick window
2. **布局:占位而非悬浮**:快捷窗口**不是** `position: fixed` 悬浮,而是在**对话区域右侧空白区域占位显示**的一列,常驻时会**把对话区域向左挤压**(与 git/终端 right-panels 同为 `chat-container` 的 flex 兄弟节点,位于 `</main>` 之后、right-panels 之前)。有内容时才显示该列。
3. **设置项**个人空间PersonalizationDrawer新增「**隐藏快捷窗口**」开关,**默认关闭**(即默认显示)。字段 `hide_quick_dock: boolean` 存 personalization form。2026-07-28 核查该字段前端未消费,仅后端存在。)
3.1 **设置项**2026-07-28「外观与显示」新增「**快捷窗口自动展开**」开关,**默认是**(有内容时自动展开);切换为否后只能通过右侧悬浮按钮手动展开。字段 `quick_dock_auto_expand: boolean`(默认 true存 personalization form。行为语义QuickDock.vue `watch(hasContent, ..., {immediate:true})` 按模式校正 `userCollapsed = !autoExpand`——手动模式下内容集合变化(出现/清空)后保持收起等手动点击,用户手动展开后只要内容不完全清空就保持展开;`watch(autoExpand)` 让设置切换立即生效(切否收起 / 切是有内容即展开)。
4. 详情面板(子智能体/后台指令)保持 fixed 悬浮从快捷窗口列左侧滑出文件预览侧边栏在快捷窗口列右侧git/终端面板左侧)占位/滑出。
5. 移动端isMobileViewport第一版先隐藏。
## 0. Demo 文件地图
| 文件 | 内容 |
|------|------|
| `index.html` | 页面结构:模拟对话区 + 悬浮栈4 窗口)+ 详情面板 + 预览侧边栏 + 菜单 + toast |
| `style.css` | 变量(配色/尺寸/缓动)+ 窗口基础 + 待办样式与全部 keyframes |
| `app.js` | 待办逻辑(创建/完成/替换/滚动划线) |
| `runner.css` / `runner.js` | 子智能体 + 后台指令窗口、左侧详情面板、⋯ 菜单(强制关闭) |
| `file.css` / `file.js` | 文件记录窗口、右侧预览侧边栏、⋯ 菜单(下载/打开/复制路径、toast |
## 1. 总体布局
- 位置:对话区域右侧**占位列**(非悬浮):`chat-container` 的 flex 兄弟节点,宽 294px270 + 左右 padding 12`flex: none`,整列上下排布。对话区被自然向左挤压。
- 窗口顺序(从上到下,固定):**待办事项 → 子智能体 → 后台指令 → 文件**。
- 窗口上下排布,右对齐,`gap: 12px`。
- **整列可滚动**:列高 100%`overflow-y: auto`,滚动条隐藏。
- 防阴影被 overflow 裁剪:栈 `padding: 24px; margin: -24px`
- 栈滚动时关闭所有打开的 ⋯ 菜单(菜单 fixed 定位不随栈滚动)。
- 各窗口初始隐藏有内容时淡入出现fade + translateY(-6px)0.3s)。
## 2. 窗口通用规格
- **宽度固定 270px**= 最大宽度 300px × 90%,变量 `--window-max-w` 联动)。
- **列表区高度固定 5 行**(行高 36px → 180px不足留空超出内部滚动滚动条隐藏。
- 窗口 `flex: none`(防止被栈 flex 压缩变矮,已踩坑)。
- 圆角 12px背景不透明实体面板禁半透明中性阴影无发光1px 细边框。
- 标题栏 38pxSVG 图标accent 色)+ 标题12.5px 次要色)+ 右侧计数11.5px 三级色)。
- **列表无上下 padding**:首行 hover 紧贴标题栏分隔线,末行紧贴窗口底边。
- 行:固定 36px 高,直接铺在窗口上(无嵌套卡片/背景/边框),超长文字省略号。
- 正式接入时配色全部换成三主题语义 tokendemo 为经典主题暖色写死)。
## 3. 各窗口设计
### 3.1 待办事项
- 行结构:`[状态点] 任务文字`**无 hover、不可点击**。
- 计数:`done/total`。
- 动画:
- **创建**:逐行从左向右(位移 14px + 淡入0.34sstagger 60ms
- **完成**横线从左往右划过文字260ms`strike-in` scaleX 0→1origin left→ 文字延迟 120ms 变灰 + 状态点变色弹跳(`dot-pop`)。静态完成态(整表渲染时)不播划线,直接呈现。
- **整表替换**(收到新 todo旧行从上到下逐行从右向左移出0.26sstagger 45ms**带高度收拢**让下方行平滑上移)→ 新行逐行进入。
- **区域外条目**:要完成的行未完全显示时,先 `scrollIntoView({behavior:'smooth', block:'nearest'})` 滚出,滚动停稳 90ms 后再播划线800ms 兜底)。
### 3.2 子智能体 / 3.3 后台指令(两者同构,仅详情内容不同)
- 行结构:`[●状态点] 名称 [⋯]`**有 hover整行可点击**;后台指令名称用等宽字体。
- 状态点:运行中 = accent 色呼吸(`status-pulse` 1.6s);完成/终止 = 灰,名称同步变灰。
- 计数:`running/total`。
- 新条目:进入动画同待办;**区域外先滚动露出再播进入动画**(先隐身插入,滚动到位后播)。
- **无删除**(条目永久保留)。
- ⋯ 菜单:仅「强制关闭」(警示红色,运行完/已终止置灰);菜单与按钮**右对齐**向下展开两菜单本菜单与文件菜单互斥Esc / 点空白关闭。
- **点击行 → 左侧展开详情面板**480×460`right: calc(20px + 270px + 12px)`,滑入 0.24s
- 子智能体:工具调用 + 文本输出流,**扁平行式无嵌套卡片**;工具行:运行中 spinner旋转→ 完成变 ✓ + 右侧落定结果;文本行次要色。
- 后台指令:等宽终端行,首行 `$ 命令` 加粗。
- 新进度出现动画:淡入 + translateY(5px) 上浮0.25s)。
- **自动滚动**:距底 <80px 时新内容平滑滚到底用户上翻则不打断
- 再点同一行 / ✕ / Esc 关闭;点其他行切换(内容区快速淡入)。
- ⚠️ **实际 API 颗粒度提醒**:子智能体进度 API 没有细化的"运行中流式状态"**每个步骤完成后才推送一次进度**。demo 里的流式效果仅为演示,正式接入时的显示颗粒度届时再定。
### 3.4 文件
- 行结构:`[⋯] 文件名`**无状态点**;有 hover整行可点击。
- 计数:文件总数。
- 显示逻辑(正式版):**文件被编辑/创建过 + 文件仍存在**才显示;同一文件按 path 去重只出现一次。
- ⋯ 菜单(与按钮**左对齐**向下展开):
1. **下载**
2. **在文件管理器中打开**——复用 git 状态侧边栏已有逻辑,用默认应用打开;**docker 模式下无此选项**
3. **复制路径**——工作区内相对路径
- **点击行 → 右侧滑出预览侧边栏**(非左侧详情面板):
- 440px 宽、全高top/bottom 20px`translateX(100%+24px)` ↔ `0`0.3s。
- 打开时**悬浮窗栈与详情面板同步左移让位**(栈 `right: 20+440+12`,均带 0.3s transition
- 头部:文件图标 + 文件名(等宽加粗)+ 目录(灰小字)+ 关闭按钮。
- 内容:等宽 12px + 行号44px 灰),行 hover 有底色,滚动条隐藏。
- 再点同一文件 / ✕ / Esc 关闭;点其他文件直接切换。
- 仅支持特定种类 UTF-8 文本文件(正式版按扩展名白名单)。
## 4. 后端配合点(正式接入)
1. **文件记录**:每次 `write_file` / `edit_file` 成功后,把文件**相对路径**(含时间戳、操作类型)写入**对话文件(对话 JSON**;同一 path 更新而非追加重复项。
2. **前端读取**:前端从对话数据中读取文件记录列表渲染文件窗口。
3. **文件内容**:预览用**现有文件内容 API**(用户确认已有,无需新做)。
4. 待办 / 子智能体 / 后台指令:接各自现有数据流与事件推送。
## 5. 已踩过的实现坑(接入时避开)
1. `display: flex` 会覆盖 `hidden` 属性的 `display: none` → 必须补 `.panel[hidden] { display: none; }`
2. flex 容器会压缩子元素(窗口变矮)→ 窗口加 `flex: none`
3. `animationend` 会冒泡feed 行 → 详情面板),用 `{once:true}` 监听会被提前消费 → 时序控制改用 `setTimeout`
4. `overflow` 裁剪窗口阴影 → 滚动容器 `padding + 负 margin` 扩大滚动盒。
5. 菜单 fixed 定位不随栈滚动 → 栈滚动时主动关闭菜单。
6. 两个 ⋯ 菜单 + Esc 多层浮层菜单互斥Esc 按 菜单 → 详情面板 → 预览栏 顺序逐个关(用 `e.preventDefault()` + `e.defaultPrevented` 串联)。
7. **占位列展开/收起过渡中 `padding` 必须保持上下值一致**2026-07-28 踩坑):`.quick-dock--empty` 若 `padding: 0`,展开时 `padding-top 0→44px` 参与过渡会让内容垂直下移 44px叠加 `translateX(24px)→0` 形成「从右上角斜向出现」观感;收起态写 `padding: 44px 0 20px`(仅水平 padding 归零)后动画才是纯横向从右到左。
8. 展开/收起悬浮按钮(`.qd-toggle`App.vue 零宽锚点)**无内容时也常显**2026-07-28 用户要求):`v-if="!isMobileViewport"` 不含 `qdHasContent`;配套 `watch(hasContent)` 任一方向变化都重置 `userCollapsed`(否则「无内容点收起 → 新内容到达」路径 dock 不自动展开)。按钮线条色与左侧对话记录侧边栏导航图标一致用 `var(--claude-text)`**恒定不变**——无 hover 变色展开态也不变色is-active 样式已移除)。
## 6. 正式接入任务清单2026-07-21 已全部完成)
- [x] 后端:编辑文件路径记录写入对话 JSONwrite/edit 钩子)+ 对话数据带出该列表
- [x] 前端:`static/src` 快捷窗口占位列容器(非悬浮,挤压对话区)
- [x] 前端4 个窗口组件 + 详情面板 + 文件预览侧边栏(配色走语义 token 三主题)
- [x] 前端:接入待办 / 子智能体 / 后台指令 / 文件记录的真实数据与事件推送
- [x] 前端:文件预览内容接现有文件内容 API扩展名白名单
- [x] 动画参数按 demo 定稿迁移(位移 14px、stagger 60/45ms、划线 260ms 等)
### 接入实现位置2026-07-21
**后端**
- `core/main_terminal_parts/tools_execution.py``_record_edited_file` / `_remove_edited_file` / `_rename_edited_file` + `_mutate_edited_files`(写入对话 `metadata.edited_files`,同 path 去重;广播 `edited_files_updated`
- `server/conversation.py``GET /api/conversations/<id>/messages` 返回 `edited_files`(过滤不存在文件)
- `server/app_legacy.py``terminal_broadcast` 白名单加 `edited_files_updated`
- `modules/personalization_manager.py``hide_quick_dock` 字段(默认 False
**前端**
- `static/src/components/chat/quickdock/``QuickDock.vue`(占位列容器+菜单+Esc 分层+轮询)、`TodoWindow.vue`、`RunnerWindow.vue`(子智能体/后台指令通用)、`RunnerDetailPanel.vue`、`FileWindow.vue`、`FilePreviewPanel.vue`、`quickdock.css`(全部 keyframes 与语义 token 配色)
- `static/src/stores/quickDock.ts`editedFiles / detail / previewPath / menu 状态
- `static/src/composables/useLegacySocket.ts`:监听 `edited_files_updated`(按 conversation_id 过滤)
- `static/src/app/methods/history.ts`messages 加载后回填 editedFiles
- `static/src/App.vue``</main>` 后挂载 `QuickDock` + `FilePreviewPanel`(移动端与 hide_quick_dock 时不渲染)
- `static/src/components/personalization/PersonalizationDrawer.vue`:「隐藏快捷窗口」设置行(默认隐藏工作区之后)
**验证状态**py_compile ✓ / 6 项后端冒烟 ✓ / tsc+stylelint+vite build ✓ / 新文件 eslint 0 问题,存量文件 lint 与基线持平。8091 服务重启后的实际运行验证待手动进行。

View File

@ -1,198 +0,0 @@
# 宿主机沙箱与权限系统说明2026-05
本文档用于完整说明当前项目在 **宿主机模式(`TERMINAL_SANDBOX_MODE=host`** 下的安全执行机制,包括:
- OS 级沙箱执行
- 执行环境Execution Mode
- 权限模式Permission Mode
- 路径授权Path Authorization
- `run_command` / `terminal` / `sub_agent` 的实际行为
---
## 1. 设计目标
在保留宿主机开发效率的前提下,降低误操作与越权风险,核心目标是:
1. 默认命令执行必须经过系统沙箱(而非裸 host
2. 权限控制尽量由执行层强制(减少“猜指令”误判)
3. 对高风险写操作引入用户审批与单次授权机制
4. 保持可运维:可配置、可回退、可解释
---
## 2. 两层控制模型
当前系统分为两层控制,且叠加生效:
### 2.1 权限模式(产品层)
- `readonly`
- `approval`
- `auto_approval`
- `unrestricted`
作用:决定工具是否允许调用、是否先只读执行、是否需要用户审批。
### 2.2 执行环境(系统层)
- `sandbox`(默认)
- `direct`
作用:决定进程最终在 OS 沙箱中执行,还是直接在宿主机执行。
> 推荐默认:`permission_mode=approval` + `execution_mode=sandbox`
---
## 3. OS 级沙箱方案
宿主机模式下,默认执行环境为 `sandbox`,各系统实现如下:
- **macOS**`sandbox-exec`
- **Linux**`bubblewrap (bwrap) + seccomp`
- **Windows**`WSL2`
若宿主机沙箱不可用,关键执行路径会拒绝执行,不允许静默回退到裸 host。
---
## 4. 路径授权模型
路径授权分两类:
1. **可读可写路径writable_paths**
2. **仅可读路径readable_extra_paths**
语义:
- 可读集合 = 可读可写 + 仅可读
- 可写集合 = 可读可写
前端“路径授权”弹窗支持这两类路径的维护(新增/编辑/删除),并由后端策略模块统一读取。
---
## 5. 各权限模式的实际行为
## 5.1 readonly
- `run_command`:允许调用,但在**只读沙箱**执行
- 若命令触发写入:由系统返回权限拒绝(例如 `Operation not permitted`
- `write_file` / `edit_file` / 其他写入类工具:直接拒绝
## 5.2 approval
- `run_command` 采用“两段式”:
1) 先在只读沙箱执行
2) 若出现权限拒绝,再发起前端审批
3) 审批通过后,仅该次命令以可写沙箱重试
- 工具结果返回“最终执行结果”(即重试后结果),不会把中间权限拒绝作为最终结果返回给模型
- 审批拒绝或超时:返回 rejected不执行写入重试
## 5.3 auto_approval
- `write_file` / `edit_file`
- 目标路径在当前工作区内:直接执行
- 目标路径不在当前工作区内:进入审批流程
- `run_command` 采用“两段式”:
1) 先在只读沙箱执行
2) 若出现权限拒绝,触发自动审批(审批智能体)
3) 自动审批通过后,仅该次命令可写重试
- 自动审批拒绝:返回“工具调用被拒绝 + 原因”,主智能体继续下一步(不强制中断整轮任务)
- 用户可随时人工接管审批结果(同意/拒绝/切换无限制)
## 5.4 unrestricted
- 权限模式层不拦截工具
- 是否沙箱执行取决于执行环境:
- `sandbox`:仍在 OS 沙箱中执行
- `direct`:宿主机直接执行
---
## 6. 执行环境sandbox/direct细节
## 6.1 sandbox默认
- 命令在 OS 沙箱执行
- 配合路径授权限制写边界(与读边界策略)
- 可结合权限模式实现只读优先与单次升级写权限
## 6.2 direct高权限
- 命令直接在宿主机执行
- 仅建议临时用于必须操作
- 切换后一直生效无自动回退机制2026-07 已移除原 TTL 自动回退)
---
## 7. 关键工具与执行路径
### 7.1 run_command
- 支持权限模式联动readonly/approval/unrestricted
- 在 host + sandbox 下可走只读沙箱或可写沙箱
- 在 approval 模式可触发“权限拒绝 -> 审批 -> 单次可写重试”
- 在 auto_approval 模式可触发“权限拒绝 -> 自动审批 -> 单次可写重试”
### 7.2 terminal 系列
- 终端会话从启动时就在沙箱里host+sandbox
- 同一会话中的后续输入继承该会话沙箱上下文
- 若路径授权更新,需要新开终端会话才能让该会话使用新策略
### 7.3 子智能体sub_agent
- 宿主机下已接入执行环境策略:
- `sandbox`:子智能体进程经宿主机沙箱启动
- `direct`:直接宿主机启动
- Docker 模式维持容器执行
### 7.4 自动审批approval agent
- 自动审批由独立审批智能体执行(与主智能体分离)
- 配置个人空间「审核智能体」页统一设置personalization.json 的 `review_agents.auto_approval`),模型复用子智能体模型库;旧的 `config/auto_approval.json` 已彻底废除
- 审批智能体可调用能力:
- `run_command`(用于只读核查)
- 审批决策工具approved / rejected + reason
- 调试开关(代码变量):
- `modules/approval_agent.py``DEBUG_SAVE_APPROVAL_AGENT_TRANSCRIPT`
- 开启后记录到 `logs/approval_agent/`
---
## 8. 配置与运行建议
## 8.1 推荐默认策略
1. `TERMINAL_SANDBOX_MODE=host`
2. 执行环境默认 `sandbox`
3. 权限模式默认 `approval`
4. 路径授权默认最小化(工作区 + 临时目录)
## 8.2 何时使用 direct
仅在以下场景短时开启:
- 明确需要系统级高权限访问
- 沙箱下无法完成且替代方案不可行
执行完成后应尽快回到 `sandbox`
---
## 9. 已知权衡
1. `run_command` 若严格收紧读路径,可能导致大量系统命令可用性下降
2. 权限拒绝识别基于错误特征,需持续优化误判/漏判边界
3. 不同 OS 的沙箱能力不完全对齐Windows 侧为 WSL2 路径)
4. Docker 模式下目前不存在与宿主机同等的 OS 只读沙箱机制;`sandbox_write_access=False` 不会提供同等级只读隔离(需后续补充容器侧只读策略)
---
## 10. 文案与产品约束
- 面向用户的提示词仅描述“能力边界”和“可操作建议”
- 不暴露内部实现细节(如内部判定逻辑、实现细节名称)
- 审批流程文案要强调“单次授权、最小权限、可回退”

View File

@ -1,227 +0,0 @@
# 多智能体模式总体架构
> 文档状态:设计草案
> 适用范围宿主机模式host mode不覆盖 docker/web 模式
> 隔离原则:与现有对话系统完全隔离,复用底层能力但上层独立
---
## 1. 设计目标
在 Astrion 中引入一个实验性的「多智能体模式」:
- 主智能体固定扮演 **Team Leader**,负责任务拆解、调度、协调、回答子智能体提问。
- 支持预置角色与自定义角色,每个角色可以有多个实例(`RoleName_1`、`RoleName_2`)。
- 子智能体之间、子智能体与主智能体之间可以双向通信。
- 所有通信遵循统一的消息格式与路由规则。
- 与现有单智能体模式完全隔离,不影响现有代码与用户体验。
---
## 2. 核心设计原则
### 2.1 完全隔离
- 数据目录隔离:`~/.astrion/astrion/host/mutiagents/`
- 页面隔离:新页面 `/multiagent/new`
- 入口隔离:登录页单独按钮「多智能体模式 beta」
- 代码隔离:所有新增代码放在 `modules/multi_agent/`、`docs/multi_agent_mode/`、`static/src/views/MultiAgentView.vue` 等独立位置
- 现有文件如需改造,优先复制一份再改,不直接修改
### 2.2 能复用则复用
- 模型调用:复用 `DeepSeekClient`
- 工具执行链路:复用 `WebTerminal.handle_tool_call` 的底层执行能力
- 文件/终端/搜索等工具:复用现有工具实现
- 对话持久化格式:复用 `ConversationManager` 的存储格式
- 沙箱/权限机制:复用现有 `evaluate_tool_permission` 与宿主机沙箱
### 2.3 子智能体即完整对话
每个子智能体是一个独立的、完整的对话上下文:
- 有自己的 `messages` 列表
- 有自己的系统提示词
- 有自己的工具列表
- 自然输出 assistant 消息
- 支持被主智能体/其他子智能体在运行中插入消息引导
- 任务结束 = 本轮自然停止输出,但上下文保留
---
## 3. 角色与实例
### 3.1 角色Role
角色由 Markdown + YAML Frontmatter 定义,存储在:
```
~/.astrion/astrion/host/mutiagents/agents/<role_id>.md
```
示例 `ui-operator.md`
```markdown
---
id: ui-operator
name: UI Operator
description: 负责前端设计、UI 还原、配色方案
model: qwen3-max
thinking_mode: fast
---
你是团队的前端设计专家。你擅长:
- 根据需求设计 UI 界面
- 制定配色方案
- 输出设计文档和前端代码
工作风格:
- 先分析需求,再给出设计方案
- 输出简洁明确的设计说明
```
### 3.2 实例Agent
- `role_id`:角色类型,如 `ui-operator`
- `agent_id`:这个角色的第几个实例,从 1 开始
- 显示名:`{Role Name}_{agent_id}`,如 `UI Operator_1`、`Full-Stack Engineer_1`
- 同一个 `role_id` 可以有多个实例UI Operator_1、UI Operator_2
- 不同 `role_id` 的实例序号独立Full-Stack Engineer_1 与 UI Operator_1 可以同时存在
- 主智能体固定显示名为 `Team Leader`,没有 agent_id
---
## 4. 总体架构图
```
┌─────────────────────────────────────────────────────────────┐
│ 用户Web 前端) │
│ /multiagent/new 页面 │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ MultiAgentTerminal │
│ (基于 MainTerminal 复制改造) │
│ - 工具列表:主工具 + 多智能体专用工具 │
│ - 子智能体管理器MultiAgentSubAgentManager │
│ - 对话存储:指向 mutiagents/conversations/ │
└─────────────────────────────────────────────────────────────┘
┌─────────────────┼─────────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Agent 1 │ │ Agent 2 │ │ Agent 3 │
│UIOperator│ │ Full- │ │ Code │
│ 1 │ │ Stack │ │ Reviewer │
│ │ │Engineer1│ │ 1 │
└──────────┘ └──────────┘ └──────────┘
│ │ │
└─────────────────┴─────────────────┘
┌───────────────────────────────┐
│ MessageRouter │
│ 根据接收方状态决定: │
│ - inline 插入工具结果后 │
│ - 空闲时作为 user 消息触发新任务│
└───────────────────────────────┘
```
---
## 5. 主智能体职责
主智能体 = Team Leader其系统提示词中明确
- 除非任务极其简单或明确不需要,否则主动拆解任务并调用子智能体。
- 为每个子任务选择最合适的角色role_id
- 子智能体可以向你提问,你必须及时通过 `answer_sub_agent_question` 回答。
- 子智能体之间可以互相沟通,你负责监督整体进度。
- 如果子智能体的中间输出需要干预,使用 `send_message_to_sub_agent` 引导。
---
## 6. 子智能体职责
子智能体系统提示词由两部分组成:
1. **基础 prompt**:通用团队规则
2. **自定义 prompt**:角色专属设定
基础 prompt 中明确:
- 你是智能体集群团队的一员。
- 不要频繁输出内容,不重要内容会污染主智能体上下文。
- 只汇报关键步骤。
- 任务完成后给出详细结论。
- 需要主智能体决策时,使用 `ask_master`
- 需要与其他子智能体沟通时,使用 `ask_other_agent` / `answer_other_agent`
- 如果向其他子智能体提问,必须同时直接向主智能体输出汇报。
---
## 7. 与现有系统的边界
| 现有系统 | 多智能体模式处理方式 |
|---------|---------------------|
| 现有对话页面 | 不改动,新增 `/multiagent/new` |
| 现有 `MainTerminal` | 不改动,复制为 `MultiAgentTerminal` |
| 现有 `SubAgentManager` | 不改动,继承创建 `MultiAgentSubAgentManager` |
| 现有 `SubAgentTask` | 不改动,继承创建 `MultiAgentSubAgentTask` |
| 现有 `ConversationManager` | 不改动,多智能体模式用自己的存储目录 |
| 现有后台通知池机制 | 参考其设计,多智能体模式实现自己的消息路由 |
| 现有工具执行链路 | 复用 `execute_tool_for_sub_agent` |
| 现有沙箱/权限 | 复用,不额外限制子智能体工具参数 |
---
## 8. 入口与数据目录
### 8.1 入口
登录页面增加按钮:「多智能体模式 beta」。
点击后进入 `/multiagent/new` 页面,加载 `MultiAgentTerminal`
### 8.2 数据目录
```
~/.astrion/astrion/host/mutiagents/
├── agents/ # 角色定义
├── conversations/ # 多智能体会话
│ └── <conv_id>/
│ ├── metadata.json
│ ├── messages.json # Team Leader 对话
│ └── agents/
│ └── <agent_id>/
│ ├── metadata.json
│ └── messages.json # 子智能体完整对话记录
└── state.json # 全局状态
```
子智能体对话记录的保存精度、时机、格式与主智能体对话记录一致。
---
## 9. 实现顺序(非严格 phase
1. 创建独立目录与数据存储结构
2. 实现角色配置加载与预置角色
3. 实现 `MultiAgentSubAgentTask` / `MultiAgentSubAgentManager`
4. 实现 `MultiAgentTerminal` 与多智能体工具定义
5. 实现消息路由inline / idle / 通知池)
6. 实现前端 `/multiagent/new` 页面
7. 登录页加入口按钮
8. 联调测试
---
## 10. 相关文档
- `02_message_protocol.md`:统一消息格式
- `03_tool_definitions.md`:工具定义
- `04_message_routing.md`:消息路由机制
- `05_data_model.md`:数据模型
- `06_implementation_plan.md`:实现细节
- `07_existing_code_analysis.md`:现有代码分析

View File

@ -1,249 +0,0 @@
# 多智能体模式消息协议
> 所有插入对话的 user 消息必须遵循统一格式。
> 不使用 `[系统通知|xxx]` 前缀,改用自然语言前缀。
---
## 1. 消息格式模板
所有通信 user 消息都使用以下结构:
```
来自 {显示名} 的{消息类型}
id: {消息id}
<{显示名}>
<{标签}>
{内容}
</{标签}>
</{显示名}>
```
### 字段说明
| 字段 | 说明 | 示例 |
|------|------|------|
| 显示名 | `{Role Name}_{agent_id}``Team Leader` | `UI Operator_1`、`Full-Stack Engineer_1` |
| 消息类型 | 任务发布 / 任务进度输出 / 任务完成汇报 / 提问 / 消息 / 回答 | 见下表 |
| 消息id | 本次消息的全局唯一标识,用于 answer 时引用 | `ask_fse_001`、`msg_uio_003` |
| 标签 | Task / Output / Ask / Message / Answer | 与消息类型对应 |
| 内容 | 消息正文 | 任意文本 |
### 消息类型与标签对应表
| 消息类型 | 标签 | 发送方 | 接收方 |
|---------|------|--------|--------|
| 任务发布 | `<Task>` | Team Leader | 子智能体 |
| 任务进度输出 | `<Output>` | 子智能体 | Team Leader |
| 任务完成汇报 | `<Output>` | 子智能体 | Team Leader |
| 提问 | `<Ask>` | 子智能体 | Team Leader / 其他子智能体 |
| 消息 | `<Message>` | Team Leader / 子智能体 | 子智能体 |
| 回答 | `<Answer>` | 子智能体 | 子智能体(返回到 ask 工具结果) |
---
## 2. 主智能体 → 子智能体
### 2.1 任务发布
当主智能体通过 `create_sub_agent``send_message_to_sub_agent` 向子智能体发布任务时使用。
```
来自 Team Leader 的任务发布
id: task_001
<Team Leader>
<Task>
请为项目设计前端配色方案,输出 design.md 到 sub_agent_results/design/。
</Task>
</Team Leader>
```
### 2.2 后续消息 / 运行中引导
当主智能体在子智能体运行期间通过 `send_message_to_sub_agent` 插入引导消息时使用。
```
来自 Team Leader 的消息
id: msg_tl_002
<Team Leader>
<Message>
先不要创建 API先确认一下现有的 auth 模块是否可复用。
</Message>
</Team Leader>
```
### 2.3 主智能体问子智能体ask_sub_agent
当主智能体通过 `ask_sub_agent` 向子智能体提问时使用。
```
来自 Team Leader 的提问
id: ask_tl_001
<Team Leader>
<Ask>
你预计还需要多久完成?
</Ask>
</Team Leader>
```
子智能体的回答返回到 `ask_sub_agent` 工具结果中,不插入主智能体对话。
---
## 3. 子智能体 → 主智能体
### 3.1 任务进度输出
子智能体在自然输出中汇报进度时,由后端捕获并插入主智能体对话。
```
来自 UI Operator_1 的任务进度输出
id: out_uio_001
<UI Operator_1>
<Output>
我现在开始分析现有设计风格和用户需求...
</Output>
</UI Operator_1>
```
### 3.2 任务完成汇报
子智能体本轮自然结束输出时,最后一条输出作为完成汇报插入主智能体对话。
```
来自 UI Operator_1 的任务完成汇报
id: out_uio_002
<UI Operator_1>
<Output>
我完成了前端配色的设计,生成了 design.md主色调为蓝色系。
</Output>
</UI Operator_1>
```
**注意**:子智能体不调用 `report_progress``finish_task` 工具。自然的 assistant 输出即表示进度或完成。
### 3.3 子智能体向主智能体提问
子智能体通过 `ask_master` 工具向主智能体提问。
```
来自 Full-Stack Engineer_1 的提问
id: ask_fse_001
<Full-Stack Engineer_1>
<Ask>
我该怎么处理版本冲突问题?项目里当前用的是什么分支策略?
</Ask>
</Full-Stack Engineer_1>
```
主智能体通过 `answer_sub_agent_question` 工具回答,回答内容返回到 `ask_master` 工具结果中。
---
## 4. 子智能体 → 子智能体
### 4.1 A 向 B 提问
子智能体 A 通过 `ask_other_agent` 向子智能体 B 提问。
```
来自 UI Operator_1 的提问
id: ask_uio_001
<UI Operator_1>
<Ask>
API 接口已经确定了吗?我需要接口字段来设计表单。
</Ask>
</UI Operator_1>
```
这条消息插入到 B 的 user 消息中。
### 4.2 B 回答 A
子智能体 B 通过 `answer_other_agent` 回答 A。
```
来自 Full-Stack Engineer_1 的回答
id: ans_fse_001
<Full-Stack Engineer_1>
<Answer question_id="ask_uio_001">
已经确定,见 api.md。用户注册接口为 POST /api/users字段为...
</Answer>
</Full-Stack Engineer_1>
```
回答返回到 A 的 `ask_other_agent` 工具结果中。
### 4.3 A 向 B 发送普通消息
子智能体 A 通过 `ask_other_agent` 问 B但内容不是需要回答的问题而是通知。
```
来自 UI Operator_1 的消息
id: msg_uio_003
<UI Operator_1>
<Message>
前端页面已经 ready可以开始对接了。
</Message>
</UI Operator_1>
```
B 可以直接通过自然输出回复,回复会返回到 A 的 `ask_other_agent` 工具结果中。
---
## 5. ID 生成规则
| 消息类型 | ID 前缀 | 示例 |
|---------|---------|------|
| 任务发布 | `task_` | `task_001` |
| 任务进度输出 | `out_` | `out_uio_001` |
| 任务完成汇报 | `out_` | `out_uio_002` |
| 子智能体问主智能体 | `ask_` | `ask_fse_001` |
| 主智能体问子智能体 | `ask_` | `ask_tl_001` |
| 子智能体间提问 | `ask_` | `ask_uio_001` |
| 子智能体间回答 | `ans_` | `ans_fse_001` |
| 普通消息 | `msg_` | `msg_tl_002` |
ID 需要保证在多智能体会话内全局唯一。
---
## 6. 前端渲染
前端消息气泡只显示三部分:
1. 角色:如 `UI Operator_1`
2. 目的/动作:如 `任务进度输出`、`提问`、`消息`
3. 内容XML 标签内的文本
不显示 XML 标签本身。
示例渲染:
```
┌────────────────────────────────────┐
│ UI Operator_1 任务进度输出 │
│ │
│ 我现在开始分析现有设计风格... │
└────────────────────────────────────┘
```
---
## 7. 关键约束
1. 所有插入对话的 user 消息都必须包含完整的 XML 包裹,便于后端解析和前端渲染。
2. `id` 放在 XML 外面,不放在 `<>` 属性中。
3. 子智能体之间的通信不强制同步到主智能体,但子智能体必须在提示词中被要求:向其他子智能体提问时,必须同时直接向主智能体输出汇报。
4. 回答类消息(`answer_sub_agent_question`、`answer_other_agent`)不插入 user 消息,只返回到对应 ask 工具的结果中。

View File

@ -1,470 +0,0 @@
# 多智能体模式工具定义
> 主智能体工具基于现有主智能体工具,移除旧版子智能体工具,新增多智能体专用工具。
> 子智能体工具保留现有 8 个基础工具,新增多智能体通信工具。
> 所有工具定义集中在 `modules/multi_agent/tools/` 目录下,不修改现有 `core/main_terminal_parts/tools_definition/`
---
## 1. 主智能体Team Leader工具
主智能体 = Team Leader其工具列表 = 现有主智能体工具(去掉旧版子智能体工具) + 多智能体专用工具。
### 1.1 从现有工具中移除
以下旧版子智能体工具不在多智能体模式中使用:
- `create_sub_agent`(旧版)
- `close_sub_agent`
- `terminate_sub_agent`(旧版)
- `get_sub_agent_status`(旧版)
### 1.2 新增/替换的多智能体工具
#### `create_sub_agent`
创建并启动一个子智能体实例。
```json
{
"type": "function",
"function": {
"name": "create_sub_agent",
"description": "创建一个子智能体实例并启动它。一个角色可以有多个实例,如 UI Operator_1、UI Operator_2。",
"parameters": {
"type": "object",
"properties": {
"agent_id": {
"type": "integer",
"description": "这个角色的第几个实例,从 1 开始。同一 role_id 下每个编号只能用一次。"
},
"role_id": {
"type": "string",
"description": "角色 ID如 ui-operator、full-stack-engineer。"
},
"task": {
"type": "string",
"description": "任务描述,会作为给子智能体的首条任务发布消息。"
},
"run_in_background": {
"type": "boolean",
"description": "是否后台运行。多智能体模式下通常直接运行false因为需要观察输出。"
},
"timeout_seconds": {
"type": "integer",
"description": "超时时间,默认 600 秒。"
},
"thinking_mode": {
"type": "string",
"enum": ["fast", "thinking"],
"description": "思考模式,不指定则使用角色配置。"
},
"model_key": {
"type": "string",
"description": "模型 key不指定则使用角色配置。"
}
},
"required": ["agent_id", "role_id", "task"]
}
}
}
```
#### `terminate_sub_agent`
强制终止指定子智能体实例。
```json
{
"type": "function",
"function": {
"name": "terminate_sub_agent",
"description": "强制终止指定子智能体实例。终止后无法恢复,但已生成的文件保留。",
"parameters": {
"type": "object",
"properties": {
"agent_id": {
"type": "integer",
"description": "要终止的子智能体实例编号。"
}
},
"required": ["agent_id"]
}
}
}
```
#### `send_message_to_sub_agent`
向子智能体发送消息或运行中引导。不等待回答。
```json
{
"type": "function",
"function": {
"name": "send_message_to_sub_agent",
"description": "向指定子智能体发送消息。如果子智能体正在运行,消息会插入到当前输出流中作为引导;如果子智能体空闲,消息会触发新一轮运行。用于运行中纠正、补充上下文、追加指令。",
"parameters": {
"type": "object",
"properties": {
"agent_id": {
"type": "integer",
"description": "目标子智能体实例编号。"
},
"message": {
"type": "string",
"description": "要发送的消息内容。"
}
},
"required": ["agent_id", "message"]
}
}
}
```
#### `ask_sub_agent`
向子智能体提问并等待回答。
```json
{
"type": "function",
"function": {
"name": "ask_sub_agent",
"description": "向指定子智能体提问并等待其回答。适用于需要子智能体给出明确答复的场景。",
"parameters": {
"type": "object",
"properties": {
"agent_id": {
"type": "integer",
"description": "目标子智能体实例编号。"
},
"question": {
"type": "string",
"description": "问题内容。"
},
"question_id": {
"type": "string",
"description": "问题唯一 ID子智能体回答时会引用。"
}
},
"required": ["agent_id", "question", "question_id"]
}
}
}
```
#### `answer_sub_agent_question`
回答子智能体向主智能体提出的问题。
```json
{
"type": "function",
"function": {
"name": "answer_sub_agent_question",
"description": "回答子智能体提出的问题。回答会返回到子智能体 ask_master 工具的结果中。",
"parameters": {
"type": "object",
"properties": {
"agent_id": {
"type": "integer",
"description": "提问的子智能体实例编号。"
},
"question_id": {
"type": "string",
"description": "问题 ID与 ask_master 时一致。"
},
"answer": {
"type": "string",
"description": "回答内容。"
}
},
"required": ["agent_id", "question_id", "answer"]
}
}
}
```
#### `list_active_sub_agents`
查询当前多智能体会话中所有活跃/可通信的子智能体。
```json
{
"type": "function",
"function": {
"name": "list_active_sub_agents",
"description": "查询当前多智能体会话中所有活跃或可通信的子智能体列表,包括运行中和空闲的实例。",
"parameters": {
"type": "object",
"properties": {}
}
}
}
```
返回示例:
```json
{
"success": true,
"agents": [
{
"agent_id": 1,
"role_id": "ui-operator",
"display_name": "UI Operator_1",
"status": "running",
"summary": "设计前端配色方案"
},
{
"agent_id": 2,
"role_id": "full-stack-engineer",
"display_name": "Full-Stack Engineer_1",
"status": "idle",
"summary": "等待 API 接口确认"
}
]
}
```
#### `get_sub_agent_status`
查询指定子智能体的详细状态。
```json
{
"type": "function",
"function": {
"name": "get_sub_agent_status",
"description": "查询指定子智能体实例的详细状态、统计和最近输出。",
"parameters": {
"type": "object",
"properties": {
"agent_id": {
"type": "integer",
"description": "子智能体实例编号。"
}
},
"required": ["agent_id"]
}
}
}
```
#### `create_custom_agent`
创建并保存自定义角色。
```json
{
"type": "function",
"function": {
"name": "create_custom_agent",
"description": "创建一个自定义角色并保存到本地,后续可通过 role_id 调用。",
"parameters": {
"type": "object",
"properties": {
"role_id": {
"type": "string",
"description": "角色 ID唯一标识。"
},
"name": {
"type": "string",
"description": "角色显示名。"
},
"description": {
"type": "string",
"description": "角色简短描述。"
},
"model": {
"type": "string",
"description": "默认使用的模型。"
},
"thinking_mode": {
"type": "string",
"enum": ["fast", "thinking"],
"description": "默认思考模式。"
},
"prompt": {
"type": "string",
"description": "角色专属 prompt。"
}
},
"required": ["role_id", "name", "description", "prompt"]
}
}
}
```
#### `list_agents`
列出所有可用角色。
```json
{
"type": "function",
"function": {
"name": "list_agents",
"description": "列出所有可用的预置和自定义角色。",
"parameters": {
"type": "object",
"properties": {}
}
}
}
```
---
## 2. 子智能体工具
子智能体保留现有 8 个基础工具:
- `read_file`
- `write_file`
- `edit_file`
- `run_command`
- `web_search`
- `extract_webpage`
- `search_workspace`
- `read_mediafile`
新增以下多智能体通信工具:
### 2.1 `ask_master`
向主智能体Team Leader提问阻塞等待回答。
```json
{
"type": "function",
"function": {
"name": "ask_master",
"description": "向主智能体Team Leader提问。主智能体回答后会将结果返回到此工具调用中。",
"parameters": {
"type": "object",
"properties": {
"question": {
"type": "string",
"description": "问题内容。"
},
"question_id": {
"type": "string",
"description": "问题唯一 ID。"
}
},
"required": ["question", "question_id"]
}
}
}
```
### 2.2 `ask_other_agent`
向其他子智能体提问或发送消息,阻塞等待回复。
```json
{
"type": "function",
"function": {
"name": "ask_other_agent",
"description": "向其他子智能体提问或发送消息。对方回复后会将结果返回到此工具调用中。注意:向其他子智能体提问时,必须同时直接向主智能体输出汇报。",
"parameters": {
"type": "object",
"properties": {
"target_agent_id": {
"type": "integer",
"description": "目标子智能体实例编号。"
},
"content": {
"type": "string",
"description": "提问或消息内容。"
},
"message_id": {
"type": "string",
"description": "消息唯一 ID。"
}
},
"required": ["target_agent_id", "content", "message_id"]
}
}
}
```
### 2.3 `answer_other_agent`
回答其他子智能体的问题。
```json
{
"type": "function",
"function": {
"name": "answer_other_agent",
"description": "回答其他子智能体的问题。回答会返回到对方 ask_other_agent 工具的结果中。",
"parameters": {
"type": "object",
"properties": {
"source_agent_id": {
"type": "integer",
"description": "提问方的子智能体实例编号。"
},
"message_id": {
"type": "string",
"description": "对方提问时的 message_id。"
},
"answer": {
"type": "string",
"description": "回答内容。"
}
},
"required": ["source_agent_id", "message_id", "answer"]
}
}
}
```
### 2.4 `list_active_sub_agents`
查询当前活跃子智能体。
```json
{
"type": "function",
"function": {
"name": "list_active_sub_agents",
"description": "查询当前多智能体会话中所有活跃或可通信的子智能体。",
"parameters": {
"type": "object",
"properties": {}
}
}
}
```
---
## 3. 工具与消息映射
| 动作 | 工具 | 发送方 | 接收方形式 |
|------|------|--------|-----------|
| 创建子智能体 | `create_sub_agent` | Team Leader | 启动实例,首条消息以任务发布形式插入 |
| 终止子智能体 | `terminate_sub_agent` | Team Leader | 强制停止实例 |
| 向子智能体发消息/引导 | `send_message_to_sub_agent` | Team Leader | 插入子智能体 user 消息inline 或触发新任务) |
| Team Leader 问子智能体 | `ask_sub_agent` | Team Leader | 插入子智能体 user 消息,等待回复 |
| 子智能体问 Team Leader | `ask_master` | 子智能体 | 插入 Team Leader user 消息,等待回答 |
| Team Leader 回答子智能体 | `answer_sub_agent_question` | Team Leader | 返回到 `ask_master` 工具结果 |
| 子智能体 A 问 B | `ask_other_agent` | A | 插入 B 的 user 消息inline 或触发新任务) |
| 子智能体 B 回答 A | `answer_other_agent` | B | 返回到 A 的 `ask_other_agent` 工具结果 |
| 查询活跃子智能体 | `list_active_sub_agents` | 任意 | 返回列表 |
| 子智能体输出/汇报 | 自然 assistant 输出 | 子智能体 | 捕获后插入 Team Leader user 消息 |
---
## 4. 工具定义文件位置
- 主智能体工具:`modules/multi_agent/tools/master_tools.py`
- 子智能体工具:`modules/multi_agent/tools/agent_tools.py`
- 工具处理:`modules/multi_agent/tools/tool_handlers.py`
不修改现有 `core/main_terminal_parts/tools_definition.py``modules/sub_agent/toolkit.py`

View File

@ -1,222 +0,0 @@
# 多智能体模式消息路由机制
> 消息路由由**接收方**决定:根据接收方当前状态,选择 inline 插入工具结果后,或作为 user 消息触发新一轮任务。
> 参考现有后台任务通知池机制实现,保证消息不丢失、不错乱、可批量处理。
---
## 1. 核心思想
### 1.1 接收方决定插入方式
当一条消息需要发送给某个子智能体(或主智能体)时,路由层只关心接收方当前处于什么状态:
| 接收方状态 | 插入方式 | 效果 |
|-----------|---------|------|
| 正在运行中,且当前在某次工具调用中阻塞等待 | 把消息作为该工具调用的结果返回 | 子智能体在当前轮次继续执行,立即看到消息 |
| 正在运行中,但不在阻塞等待状态 | 把消息 inline 插入到当前对话上下文末尾 | 子智能体下一轮输出时自然看到 |
| 空闲状态(本轮已自然结束) | 把消息作为普通 user 消息插入 | 触发子智能体新一轮运行 |
### 1.2 参考现有后台通知池
现有后台任务通知机制:
- **运行期间inline**`server/chat_flow_tool_loop.py` 中 `execute_tool_calls` 末尾调用 `process_sub_agent_updates(..., inline=True)`,把已完成的子智能体任务一次性插入当前 `messages`,不触发新任务。
- **停止输出后polling**`server/chat_flow_task_main.py` 中 `handle_task_with_sender` 结尾启动 `run_completion_poll`,统一轮询子智能体 + 后台命令完成通知,批量插入 user 消息,只触发一个后续任务。
多智能体模式的消息路由借鉴此机制:
- 每个子智能体有自己的「待处理消息队列」。
- 子智能体每次进入可接收消息的状态时,从队列中取出所有消息,按顺序处理。
- 避免逐条消息触发多次「工作 → 停止 → 再工作」循环。
---
## 2. 消息路由状态机
### 2.1 子智能体状态
```
┌─────────────┐
│ idle │ 空闲
└──────┬──────┘
│ create_sub_agent / send_message_to_sub_agent
┌─────────────┐
│ running │ 运行中
│ (normal) │ 正常输出
└──────┬──────┘
│ 调用 ask_master / ask_other_agent
┌─────────────┐
│ waiting │ 阻塞等待回答
│ (asking) │
└──────┬──────┘
│ 收到 answer / 自然结束
┌─────────────┐
│ completed │ 本轮结束(上下文保留)
└─────────────┘
```
### 2.2 路由决策
```python
def route_message(target_agent_id, message):
agent = get_agent(target_agent_id)
if agent.state == "waiting" and agent.pending_tool_call:
# 目标正在阻塞等待回答:直接返回到工具结果
agent.resolve_pending_tool_call(message)
return "resolved"
if agent.state == "running":
# 目标正在运行inline 插入到当前对话上下文末尾
agent.inject_inline_message(message)
return "injected_inline"
if agent.state in ("idle", "completed"):
# 目标空闲:作为 user 消息插入,触发新一轮运行
agent.inject_user_message(message)
return "triggered_new_turn"
```
---
## 3. 关键场景分析
### 3.1 子智能体正在输出,主智能体要引导
场景:
- UI Operator_1 正在运行,输出了「接下来我会创建新 API...」
- Team Leader 要立即干预:「先不要创建 API先确认现有 auth 模块是否可复用。」
处理:
- UI Operator_1 状态为 `running`(正常输出,不在阻塞等待)
- `send_message_to_sub_agent` 的消息 inline 插入到 UI Operator_1 的 `messages` 末尾
- UI Operator_1 下一轮模型调用时会看到这条 user 消息
### 3.2 子智能体正在等待回答
场景:
- Full-Stack Engineer_1 调用了 `ask_master`,等待 Team Leader 回答
- Team Leader 调用 `answer_sub_agent_question`
处理:
- Full-Stack Engineer_1 状态为 `waiting`
- 回答直接返回到 `ask_master` 工具结果中
- Full-Stack Engineer_1 继续执行
### 3.3 子智能体已经完成,主智能体追加任务
场景:
- Full-Stack Engineer_1 已经自然结束输出,状态为 `completed`
- Team Leader 要追加新任务
处理:
- `send_message_to_sub_agent` 的消息作为普通 user 消息插入
- 触发 Full-Stack Engineer_1 新一轮运行
### 3.4 边界情况:子智能体正在进行最后一轮输出
场景:
- UI Operator_1 正在输出最后一段话,后面没有工具调用了
- Team Leader 此时调用 `send_message_to_sub_agent`
处理:
- 如果消息到达时 UI Operator_1 还在运行:尝试 inline 插入
- 但由于这是最后一轮后面没有工具调用了inline 的消息不会被模型看到
- 因此需要在 UI Operator_1 本轮任务结束后,把这条消息作为触发新一轮任务的 user 消息发送
实现要点:
- 路由层维护每个子智能体的「待处理消息队列」
- 子智能体任务自然结束时,检查队列
- 如果有待处理消息,立即作为 user 消息触发新一轮运行
### 3.5 子智能体 A 问 BB 正在运行
场景:
- UI Operator_1 调用 `ask_other_agent(target=2)` 问 Full-Stack Engineer_1
- Full-Stack Engineer_1 正在运行中
处理:
- 如果 Full-Stack Engineer_1 处于 `running` 状态inline 插入 user 消息
- Full-Stack Engineer_1 下一轮输出时看到问题
- 如果 Full-Stack Engineer_1 调用 `answer_other_agent`:回答返回到 UI Operator_1 的 `ask_other_agent` 工具结果
---
## 4. 待处理消息队列
每个子智能体维护一个待处理消息队列 `pending_messages`
```python
@dataclass
class PendingMessage:
id: str
source_display_name: str
source_agent_id: Optional[int]
target_agent_id: int
message_type: str # task / output / ask / message / answer
content: str
question_id: Optional[str] # 用于 answer 匹配
created_at: float
```
### 4.1 队列消费时机
1. 子智能体每次模型调用前,先检查队列,把待处理消息合并到 `messages`
2. 子智能体从 `waiting` 状态恢复时,优先消费回答类消息
3. 子智能体自然结束时,如果有剩余待处理消息,立即触发新一轮运行
### 4.2 批量消费
参考现有通知池,每次消费时尽可能一次性取出所有可消费消息:
```python
def consume_pending_messages(agent):
messages = agent.pending_messages.drain_all()
for msg in messages:
formatted = format_message(msg)
agent.messages.append({"role": "user", "content": formatted})
return len(messages)
```
---
## 5. 与现有通知池的对比
| 维度 | 现有后台通知池 | 多智能体消息路由 |
|------|--------------|----------------|
| 触发源 | 子智能体/后台命令完成 | 子智能体间/主智能体向子智能体发消息 |
| 接收方 | 主智能体对话 | 子智能体对话或主智能体对话 |
| 插入方式 | inline / 触发新任务 | inline / 返回到工具结果 / 触发新任务 |
| 批量处理 | `_collect_pending_completion_notices` 一次性取多条 | 每个子智能体维护自己的待处理队列 |
| 持久化 | 直接插入对话历史 | 先进入队列,再按状态消费并持久化 |
---
## 6. 消息路由实现位置
- 核心路由逻辑:`modules/multi_agent/message_router.py`
- 待处理队列:`MultiAgentSubAgentTask.pending_messages`
- 状态管理:`MultiAgentSubAgentTask.state`
- 工具调用等待:`MultiAgentSubAgentTask.pending_tool_calls`
---
## 7. 防丢失机制
1. 每条消息都有唯一 `id`
2. 消息进入队列时立即持久化到子智能体 metadata
3. 消费完成后从队列移除并持久化
4. 子智能体恢复时从 metadata 加载未消费消息
5. 回答类消息通过 `question_id` / `message_id` 精确匹配
---
## 8. 关键代码参考
- `server/chat_flow_task_support.py``inject_runtime_user_message`、`process_sub_agent_updates`
- `server/chat_flow_task_main.py``_collect_pending_completion_notices`、`poll_completion_notifications`、`_dispatch_completion_user_notice`
- `server/chat_flow_tool_loop.py``execute_tool_calls` 末尾的 `process_sub_agent_updates(..., inline=True)`

View File

@ -1,235 +0,0 @@
# 多智能体模式实现计划
> 实现顺序不作为严格 phase而是按依赖关系自然推进。
> 核心原则:完全隔离、复制改造、底层复用。
---
## 1. 目录与文件规划
### 1.1 后端代码
```
modules/multi_agent/
├── __init__.py
├── terminal.py # MultiAgentTerminal
├── manager.py # 多智能体会话与全局状态管理
├── agent_store.py # 角色配置加载与保存
├── conversation_store.py # 多智能体会话存储
├── message_router.py # 消息路由与待处理队列
├── sub_agent_task.py # MultiAgentSubAgentTask
├── sub_agent_manager.py # MultiAgentSubAgentManager
├── prompts.py # prompt 模板
└── tools/
├── __init__.py
├── master_tools.py # Team Leader 工具定义
├── agent_tools.py # 子智能体工具定义
└── tool_handlers.py # 工具处理函数
```
### 1.2 前端代码
```
static/src/views/MultiAgentView.vue
static/src/components/multi-agent/
├── ChatPanel.vue
├── AgentList.vue
├── MessageBubble.vue
└── CreateAgentDialog.vue
```
### 1.3 设计文档
```
docs/multi_agent_mode/
├── 01_overview.md
├── 02_message_protocol.md
├── 03_tool_definitions.md
├── 04_message_routing.md
├── 05_data_model.md
├── 06_implementation_plan.md
└── 07_existing_code_analysis.md
```
### 1.4 数据目录
```
~/.astrion/astrion/host/mutiagents/
├── agents/
├── conversations/
└── state.json
```
---
## 2. 实现步骤
### Step 1基础设施
1. 创建 `modules/multi_agent/`
2. 创建 `docs/multi_agent_mode/` 目录(已完成)
3. 创建 `~/.astrion/astrion/host/mutiagents/` 数据目录
4. 预置 4 个角色文件到 `agents/`
### Step 2角色配置
1. 实现 `agent_store.py`
- 加载角色 Frontmatter
- 保存自定义角色
- 列出所有角色
- 构建完整 prompt基础 prompt + 自定义 prompt
2. 预置角色:
- `ui-operator`
- `full-stack-engineer`
- `code-reviewer`
- `researcher`
### Step 3子智能体改造
1. 实现 `MultiAgentSubAgentTask`(继承 `SubAgentTask`
- 覆盖 `_run_loop`,使用多智能体工具列表
- 覆盖系统提示词生成
- 捕获 assistant 输出并转发到主智能体
- 维护 `pending_messages` 队列
- 维护 `pending_tool_calls` 等待列表
- 支持 inline 消息注入
- 支持自然结束(不依赖 finish_task
2. 实现 `MultiAgentSubAgentManager`(继承 `SubAgentManager`
- 创建 `MultiAgentSubAgentTask` 实例
- 管理所有子智能体状态
- 提供查询活跃子智能体接口
- 提供终止实例接口
### Step 4消息路由
1. 实现 `message_router.py`
- `route_message(target_agent_id, message)`
- 根据目标状态选择 inline / 工具结果返回 / 触发新任务
- 处理边界情况(最后一轮输出时收到消息)
- 批量消费待处理消息
2. 实现消息格式化函数
- `format_task_message(display_name, content)`
- `format_output_message(display_name, content)`
- `format_ask_message(display_name, content, message_id)`
- `format_message_message(display_name, content, message_id)`
- `format_answer_message(display_name, content, message_id)`
### Step 5主智能体 Terminal
1. 实现 `MultiAgentTerminal`(基于 `MainTerminal` 复制)
- 移除旧版子智能体工具
- 添加多智能体专用工具
- 使用 `MultiAgentSubAgentManager`
- 使用 `MultiAgentConversationStore`
- 实现 `answer_sub_agent_question` 等工具处理
2. 实现工具处理函数
- `create_sub_agent`
- `terminate_sub_agent`
- `send_message_to_sub_agent`
- `ask_sub_agent`
- `answer_sub_agent_question`
- `list_active_sub_agents`
- `get_sub_agent_status`
- `create_custom_agent`
- `list_agents`
### Step 6会话存储
1. 实现 `MultiAgentConversationStore`
- 复制 `ConversationManager` 的 CRUD 逻辑
- 数据目录指向 `~/.astrion/astrion/host/mutiagents/conversations/`
- 保存主智能体对话
- 保存每个子智能体对话
### Step 7前端页面
1. 创建 `MultiAgentView.vue`
- 类似现有 ChatView但专门用于多智能体模式
- 支持消息气泡按角色/类型渲染
- 支持显示活跃子智能体列表
- 支持创建子智能体
2. 创建组件
- `MessageBubble.vue`:渲染统一消息格式
- `AgentList.vue`:显示子智能体状态
- `CreateAgentDialog.vue`:创建自定义角色
3. 添加路由 `/multiagent/new`
### Step 8入口
1. 登录页添加「多智能体模式 beta」按钮
2. 点击后跳转到 `/multiagent/new`
3. 初始化 `MultiAgentTerminal`
### Step 9联调测试
1. 测试创建子智能体
2. 测试子智能体自然输出转发
3. 测试 `send_message_to_sub_agent` 运行中引导
4. 测试 `ask_master` / `answer_sub_agent_question`
5. 测试 `ask_other_agent` / `answer_other_agent`
6. 测试子智能体对话持久化
7. 测试角色创建与保存
---
## 3. 代码隔离清单
| 现有文件 | 处理方式 |
|---------|---------|
| `core/main_terminal.py` | 复制为 `modules/multi_agent/terminal.py` |
| `core/main_terminal_parts/tools_definition/agent_tools.py` | 复制改造为 `modules/multi_agent/tools/master_tools.py` |
| `core/main_terminal_parts/tools_execution.py` | 复制需要的部分到 `modules/multi_agent/terminal.py` |
| `modules/sub_agent/manager.py` | 继承创建 `modules/multi_agent/sub_agent_manager.py` |
| `modules/sub_agent/task.py` | 继承创建 `modules/multi_agent/sub_agent_task.py` |
| `modules/sub_agent/toolkit.py` | 复制改造为 `modules/multi_agent/tools/agent_tools.py` |
| `modules/sub_agent/prompts.py` | 复制改造为 `modules/multi_agent/prompts.py` |
| `utils/conversation_manager/*.py` | 复制改造为 `modules/multi_agent/conversation_store.py` |
| `server/chat_flow.py` | 参考实现,为 `/multiagent` 创建新的 API 入口 |
| `server/chat_flow_task_support.py` | 参考 `inject_runtime_user_message` 实现多智能体消息注入 |
| `static/src/views/ChatView.vue` | 复制改造为 `static/src/views/MultiAgentView.vue` |
---
## 4. 复用点清单
| 能力 | 复用方式 |
|------|---------|
| 模型调用 | `DeepSeekClient` |
| 工具执行底层 | `WebTerminal.handle_tool_call` 的执行链路 |
| 文件/终端/搜索等工具 | 现有工具函数 |
| 沙箱/权限 | `evaluate_tool_permission` |
| 对话存储格式 | `ConversationManager` 的 JSON 结构 |
| 原子写入 | `_atomic_write_json` 模式 |
| 前端组件 | 复制 ChatView 改造 |
---
## 5. 风险与应对
| 风险 | 应对 |
|------|------|
| 子智能体消息丢失 | 待处理队列持久化 + 唯一 id + 消费确认 |
| 子智能体状态错乱 | 状态机清晰 + reconcile 机制 |
| 主智能体上下文爆炸 | 子智能体只汇报关键步骤,详细内容放文件 |
| 多实例并发冲突 | 每个实例独立上下文,独立存储 |
| 与现有代码耦合 | 严格隔离,复制改造 |
---
## 6. 验收标准
1. 登录页有「多智能体模式 beta」按钮
2. 点击后进入 `/multiagent/new`
3. 可以创建至少 2 个不同角色的子智能体
4. 子智能体输出自动显示在主对话中
5. Team Leader 可以运行中引导子智能体
6. 子智能体可以向 Team Leader 提问并收到回答
7. 子智能体可以向其他子智能体提问并收到回答
8. 子智能体对话完整保存,刷新后可恢复
9. 现有单智能体模式不受影响

View File

@ -1,277 +0,0 @@
# 多智能体模式:现有代码分析
> 本文档梳理实现多智能体模式需要理解的现有代码,为复制改造提供依据。
---
## 1. 子智能体实现
### 1.1 核心文件
```
modules/sub_agent/
├── __init__.py
├── manager.py # SubAgentManager
├── task.py # SubAgentTask
├── toolkit.py # 工具定义
├── prompts.py # 提示词
├── state.py # 状态管理
├── stats.py # 统计
├── creation.py # 创建参数
└── tools.py # 本地工具实现
```
### 1.2 SubAgentManagermanager.py
- 在主进程内以独立事件循环运行子智能体协程
- `create_sub_agent()` 创建并启动任务
- `wait_for_completion()` 阻塞等待完成
- `terminate_sub_agent()` 强制终止
- `poll_updates()` 检查已完成任务
- `execute_tool_for_sub_agent()` 代理工具执行,复用主进程链路
- `tasks` 字典保存所有任务状态
多智能体模式可继承点:
- 创建 `MultiAgentSubAgentManager`,重写 `create_sub_agent` 以创建 `MultiAgentSubAgentTask`
- 扩展任务记录字段:`role_id`、`display_name`、`pending_messages`、`pending_tool_calls`
- 扩展查询接口:`list_active_sub_agents()`、`get_sub_agent_status()`
### 1.3 SubAgentTasktask.py
- `_run_loop()`LLM 主循环,最多 50 轮
- `_call_model()`:流式调用模型
- `_execute_tool()`:通过 manager 执行工具
- `_finalize_task()`:任务结束时保存 output.json、stats.json、conversation.json
- `FINISH_TOOL`:必须调用 finish_task 才结束
多智能体模式改造点:
- 移除 `FINISH_TOOL` 依赖,自然结束输出即任务完成
- 在 `_run_loop` 中每轮模型调用前消费 `pending_messages`
- 捕获 assistant 输出并转发到主智能体对话
- 新增 `ask_master`、`ask_other_agent`、`answer_other_agent` 工具处理
- 维护 `pending_tool_calls`,支持阻塞等待回答
### 1.4 工具定义toolkit.py
现有 8 个工具:
- `read_file`
- `write_file`
- `edit_file`
- `run_command`
- `web_search`
- `extract_webpage`
- `search_workspace`
- `read_mediafile`
多智能体模式可复用这些工具定义,新增通信工具。
### 1.5 提示词prompts.py
- `build_user_message()`:给子智能体的任务消息
- `build_system_prompt()`:子智能体系统提示词
多智能体模式需要:
- 新的 `build_system_prompt()`,包含团队规则
- 新的 `build_user_message()`,使用统一消息格式
- 角色 prompt 拼接函数
---
## 2. 后台任务通知池机制
### 2.1 核心文件
- `server/chat_flow_task_support.py`
- `server/chat_flow_task_main.py`
- `server/chat_flow_tool_loop.py`
### 2.2 inject_runtime_user_message
位置:`server/chat_flow_task_support.py`
功能:运行期/空闲期统一向对话插入一条 user 消息。
关键参数:
- `web_terminal`:终端实例
- `messages`当前运行中的消息列表inline 时用)
- `text`:消息文本
- `source`:消息来源,如 `sub_agent`
- `inline`True 表示运行期插入,不触发新任务
- `persist`:是否持久化到对话历史
多智能体模式参考点:
- 子智能体输出转发到主智能体时,使用类似机制
- 但消息格式改为统一 XML 格式,不使用 `[系统通知|sub_agent]` 前缀
### 2.3 process_sub_agent_updates
位置:`server/chat_flow_task_support.py`
功能:轮询子智能体完成状态,把结果插入当前对话上下文。
两种模式:
- `inline=True`:运行期间,插入 `messages` 不触发新任务
- `inline=False`:空闲期间,触发后续任务
多智能体模式参考点:
- 子智能体自然输出捕获后,可以 inline 插入主智能体 `messages`
- 子智能体空闲时收到的消息,作为 user 消息触发新任务
### 2.4 _collect_pending_completion_notices
位置:`server/chat_flow_task_main.py`
功能:从子智能体和后台命令两路统一取出所有待通知项,按时间排序,一次性处理。
关键设计:
- 取出时即就地标记 `notified`,防止重复消费
- 返回多条通知时,前 N-1 条作为前置通知,最后一条触发新任务
多智能体模式参考点:
- 每个子智能体维护自己的待处理消息队列
- 消费时批量取出,避免多次触发新任务
- 使用唯一 id 标记已消费
### 2.5 poll_completion_notifications
位置:`server/chat_flow_task_main.py`
功能:统一轮询器,单工作区只 spawn 一个,避免并发冲突。
多智能体模式参考点:
- 多智能体模式也需要一个类似的通知/消息处理循环
- 但消息源更多Team Leader → Agent、Agent → Agent、Agent → Team Leader
---
## 3. 主智能体工具调用链路
### 3.1 工具定义
位置:`core/main_terminal_parts/tools_definition/agent_tools.py`
现有主智能体子智能体工具:
- `create_sub_agent`
- `close_sub_agent`
- `terminate_sub_agent`
- `get_sub_agent_status`
多智能体模式需要移除这些,新增自己的工具定义。
### 3.2 工具执行
位置:`core/main_terminal_parts/tools_execution.py` 中 `handle_tool_call`
- 参数预检查
- 权限检查
- 根据 tool_name 分发到具体处理函数
- 返回 JSON 字符串
多智能体模式参考点:
- `MultiAgentTerminal.handle_tool_call` 中增加多智能体工具的分支
- 子智能体工具处理函数放在 `modules/multi_agent/tools/tool_handlers.py`
### 3.3 WebTerminal 广播
位置:`core/web_terminal.py`
- `handle_tool_call()` 覆盖父类,广播 `tool_execution_start` / `tool_status` / `tool_execution_complete`
- `broadcast()` 发送 WebSocket 事件
多智能体模式参考点:
- `MultiAgentTerminal` 可继承 `WebTerminal``MainTerminal`
- 保留广播能力
---
## 4. 对话上下文管理
### 4.1 ContextManager
位置:`utils/context_manager/`
- `ConversationMixin.start_new_conversation()`:创建新对话
- `ConversationMixin.load_conversation_by_id()`:加载对话
- `ConversationMixin.save_current_conversation()`:保存当前对话
多智能体模式参考点:
- 创建 `MultiAgentContextManager` 或直接用 `MultiAgentConversationStore`
- 保存格式与现有 `messages.json` 一致
### 4.2 ConversationManager
位置:`utils/conversation_manager/`
- `crud_mixin.py``create_conversation`、`save_conversation`、`load_conversation`
- `path_mixin.py`:文件路径管理
- `index_mixin.py`:索引管理
多智能体模式参考点:
- 复制 `ConversationManager` 的实现到 `MultiAgentConversationStore`
- 修改数据目录为 `~/.astrion/astrion/host/mutiagents/conversations/`
---
## 5. Skill 归档机制
### 5.1 核心文件
`modules/skills_manager.py`
### 5.2 关键函数
- `validate_skill_directory()`:验证 skill 目录
- `archive_skill_directory()`:移动 skill 到归档目录
- `_parse_frontmatter()`:解析 YAML Frontmatter
- `_scan_skills_catalog()`:扫描 skill 目录
多智能体模式参考点:
- 角色配置使用类似的 Frontmatter 格式
- 角色归档目录:`~/.astrion/astrion/host/mutiagents/agents/`
- 可以复制 `_parse_frontmatter`、`_scan_skills_catalog` 的实现到 `agent_store.py`
---
## 6. 前端消息渲染
### 6.1 现有机制
- `server/chat_flow_task_support.py``inject_runtime_user_message` 发送 `user_message` 事件
- 前端 `messaging.ts` 处理 `user_message` 事件
- 根据 `message_source`、`visibility`、`starts_work` 等 metadata 渲染
### 6.2 多智能体模式改造点
- 多智能体消息也发送 `user_message` 事件
- 前端根据消息内容中的 XML 解析出角色、类型、内容
- 渲染为特殊气泡,不显示 XML 标签
---
## 7. 关键接口总结
| 能力 | 现有实现位置 | 多智能体模式实现位置 |
|------|------------|-------------------|
| 子智能体创建/管理 | `modules/sub_agent/manager.py` | `modules/multi_agent/sub_agent_manager.py` |
| 子智能体运行循环 | `modules/sub_agent/task.py` | `modules/multi_agent/sub_agent_task.py` |
| 子智能体工具 | `modules/sub_agent/toolkit.py` | `modules/multi_agent/tools/agent_tools.py` |
| 子智能体提示词 | `modules/sub_agent/prompts.py` | `modules/multi_agent/prompts.py` |
| 主智能体工具定义 | `core/main_terminal_parts/tools_definition/agent_tools.py` | `modules/multi_agent/tools/master_tools.py` |
| 主智能体工具执行 | `core/main_terminal_parts/tools_execution.py` | `modules/multi_agent/terminal.py` |
| 运行时消息注入 | `server/chat_flow_task_support.py` | `modules/multi_agent/message_router.py` |
| 通知池轮询 | `server/chat_flow_task_main.py` | `modules/multi_agent/message_router.py` |
| 对话存储 | `utils/conversation_manager/` | `modules/multi_agent/conversation_store.py` |
| 角色归档 | `modules/skills_manager.py` | `modules/multi_agent/agent_store.py` |
| 前端页面 | `static/src/views/ChatView.vue` | `static/src/views/MultiAgentView.vue` |
---
## 8. 实现注意事项
1. **不要修改现有文件**:所有改造在 `modules/multi_agent/` 中完成,通过继承或复制实现。
2. **状态同步**:子智能体状态变更时需要同步更新 `metadata.json`
3. **消息不丢失**:待处理消息队列需要持久化。
4. **上下文隔离**:每个子智能体独立 messages不要互相污染。
5. **模型工具列表**:每轮模型调用前需要重新构造工具列表,确保多智能体通信工具可用。
6. **自然结束检测**:子智能体某轮没有工具调用且 assistant 输出为空时,认为本轮结束。
7. **阻塞问答超时**`ask_master` / `ask_other_agent` 需要设置合理超时,避免永久阻塞。

View File

@ -1,260 +0,0 @@
# 工作流Workflow功能设计与实施方案
> 状态:**设计定稿2026-08-21**。工作流库页与可视化编辑器已实现;本文档是运行时实施的最终依据。
> 调研依据:`workflow_research/` 三份报告(外部产品 / 范式要素 / 代码挂载点)。
> 历史说明:本文档前身是 2026-08-18 的讨论稿;当时「审核作为阶段属性」「兜底提醒轮询」「整体结束审核」等设想已在定稿中推翻,以本文档为准。
## 1. 功能定义与核心理念
用户把「一套既定流程:工作方式 → 验证方式 → 结束方式」存为一个**工作流**workflow之后在任何普通对话中激活它让主智能体复用现有循环与执行引擎按流程推进阶段间可由审核智能体软审核。
三条核心理念(定稿):
1. **结构在边上,自主在节点内**——画布拓扑(节点 + 路由是结构约束阶段内主智能体完全自主自由调工具、跑脚本。外部调研LangGraph / Dify / CrewAI / n8n一致验证的范式。
2. **工作流是智能体的辅助流程,不是宿主**——任何工作流异常/终态都不得掐断智能体工作。所有退出/失败/超限统一为**柔性通知**:摘牌(改状态)+ 一条 user 消息,处置权交回模型与用户。
3. **对话级持续状态**——工作流状态不随单次任务结束、模型停止输出、用户按停止按钮而结束。模型停下后用户可自由穿插讨论,说「继续」即接着推进。
## 2. 已拍板决策(定稿汇总)
| 决策点 | 结论 |
|---|---|
| 运行方式 | 不做独立模式/特殊 URL/新对话类型;在普通对话中激活 |
| 激活入口 | slash 菜单人工激活(仅智能体空闲可用)+ AI 工具 `activate_workflow` 自主激活 |
| 同时激活数 | **一个对话同时只有一个工作流处于激活**;重复激活同工作流幂等返回进度,激活另一个被拒绝 |
| 审核建模 | **独立 review 节点**(菱形,一等公民),非阶段属性;要结束审核就在 end 前显式放 review 节点 |
| 结束节点语义 | end 即终点,汇报到 end 即 completed**无隐藏的整体结束审核** |
| 分支节点语义 | 单出线 = 并线器(自动穿过,无需决策);多出线 = AI 决策点(等 `choose_workflow_branch` |
| 审核异常 | 视为驳回(计入 reject_counts工具返回中含「请告知用户」 |
| maxRejects 撞限 | 工作流 failed经工具返回告知模型已在场不另发 user 消息 |
| max_stage_rounds 撞限 | **不停工作流、不停智能体**;注入 user 消息让模型立刻停下、告知用户、询问是否继续 |
| deactivate用户/模型/撞限) | 一律柔性通知退出,见 §4.7 |
| 兜底提醒轮询 | **取消**。模型停止输出即停止,工作流挂起不断,期间可与用户讨论 |
| 停止任务联动 | **不做**(不仿 stop_goal_user_cancel |
| 版本快照 | 激活时把 WORKFLOW.md 复制进状态目录;运行期一切读取只读快照,库文件被改不影响运行中实例 |
| 前后端通信 | 轮询(复用 task 轮询 session_data 快照链路)+ 统一通知池轮询器user 消息) |
| 状态隔离 | 对话级状态目录,多对话互不惊扰 |
| 硬校验 | 不做步骤级硬校验;保存时做结构校验(对齐前后端 validate |
## 3. 数据模型与存储(已实现)
### 3.1 节点模型(对齐 `static/src/components/workflow/workflowModel.ts`
五种节点,串行边界(无并行语义):
| kind | 形态 | 语义 |
|---|---|---|
| `start` | 胶囊 | 入口,右 1 出,恰好 1 个 |
| `end` | 胶囊 | 终点,左 n 入,至少 1 个 |
| `stage` | 矩形 | AI 执行阶段:`goal` + `instructions` + `next`(单值,显式连接) |
| `review` | 菱形 | 审核把关:`prompt`(审核关注点)+ `next`(通过路由)+ `rejectTo`(驳回路由)+ `maxRejects`(连续驳回上限,超限 failed |
| `branch` | 虚线矩形 | 分线/并线器:`next[]` 路由数组,每条带 `condition`自然语言AI 决策依据;多出线必填) |
所有路径显式化(`next` 为 null 校验不通过);`position` 仅画布坐标,与执行解耦。
### 3.2 工作流库(已实现)
- WORKFLOW.md = YAML frontmatter上述结构+ markdown 正文(工作方式/验证方式/结束方式)
- 双源合并:源码树 `workflows/`(内置种子)+ 运行态用户库host`~/.astrion/astrion/host/workflows/`
- 模块:`modules/workflow_manager.py`CRUD + 校验 + camelCase↔snake_case
- REST`server/workflow_page.py``/workflows`、`/workflow/<name>` 页面 + `/api/workflows` CRUD
### 3.3 运行时状态目录(待实现)
```
{workspace.data_dir}/workflow_states/<conversation_id>/
├── state.json # 运行状态
└── WORKFLOW.md # 激活时刻的原样快照
```
`state.json` schema
```json
{
"workflow_name": "code-review-pipeline",
"status": "active | completed | stopped | failed",
"exit_reason": "user | model | max_rejects | round_limit_ack …",
"current_node_id": "review-target-stage",
"stage_rounds": 3,
"round_limit_notified": false,
"stage_start_msg_index": 142,
"reject_counts": {"review-gate": 1},
"history": [
{"node_id": "explore", "kind": "stage", "summary": "…", "rounds": 5, "completed_at": 169…},
{"node_id": "review-gate", "kind": "review", "decision": "reject", "message": "…", "at": 169…}
],
"pending_notices": [
{"type": "deactivated_by_user", "message": "完整文本", "created_at": 169…}
],
"started_at": 169…
}
```
要点:
- **`current_node_id` 只停 stage / branch / end**——review 是**瞬态节点**汇报时同步审完直接走到下一站review 只作为 history 记录。状态机与前端进度展示因此大幅简化。
- **`stage_start_msg_index`(消息游标)**:进入阶段时记录 conversation_history 长度,审核 payload 截取增量作为阶段工作痕迹§4.5)。
- **`pending_notices`**柔性通知池§4.7),落盘持久,取出即清除。
## 4. 运行时机制(待实现)
### 4.1 上下文注入:目录常存,详情跟走
| 信息 | 载体 | 时机 |
|---|---|---|
| 全景目录(工作流名/描述/正文三段/全部节点一句话清单/当前位置) | system 段(**不冻结**,每次 build_messages 现生成) | 激活期间恒在,压缩免疫 |
| 当前节点详情stage 的 goal + instructions 全文) | 工具返回tool 消息进历史) | 激活时、每次推进时 |
| 迷失自查 | `get_workflow_status` 工具 | 模型主动调 |
注意:主循环单次任务内 messages 只构建一次,阶段推进后同一任务后续迭代的 system 段会滞后。实施时二选一:推进时在工具 handler 内同步刷新 messages 中的工作流段;或 system 段注明「以最新工具返回/状态查询为准」。首选前者。
### 4.2 激活(两路入口,共享 `build_activation_text()`
**AI 自主激活**`activate_workflow(name)` 工具 → 加载定义 → 复制快照 → 初始化 state → 工具返回全景 + 入口阶段详情。重复激活规则:同工作流幂等返回当前进度;不同工作流拒绝(提示先退出)。
**slash 菜单激活**REST
```
POST /api/workflow/activate {conversation_id, name}
→ 检查该对话无运行中主任务(门闸/terminal 状态),忙则 409「智能体运行中无法激活」
→ 复制 WORKFLOW.md 快照 + 初始化 state
→ 构造完整 user 消息(含全景 + 入口详情 + 「请开始执行」),走正常消息链路:
持久化 + 广播 + metadata 齐全(正常 user 消息参数一个不少,
另打 auto_message_type: "workflow_activate" 标记供前端识别样式)
→ 创建主任务过门闸 → 模型收到消息直接开干(无需再调 activate_workflow
```
### 4.3 阶段推进矩阵(核心)
`report_workflow_stage(summary)` 仅在当前节点为 stage 时可调。引擎读当前 stage 的 `next`,按目标类型分派:
| 下一节点 | 引擎动作 | 工具返回 |
|---|---|---|
| `stage` | 直接推进 | 「已记录完成。下一阶段「X」goal + instructions」 |
| `review` | **同步 await 审核智能体**§4.5 | 通过 → 「审核通过。下一阶段「X」…」驳回 → reject_counts+1回到 rejectTo「审核未通过意见…。你已回到阶段「Y」整改」 |
| `branch`(多出线) | 不推进,停在 branch 等选择 | 「前方分支点:→ A条件→ B条件。请调 choose_workflow_branch」 |
| `branch`(单出线) | 自动穿过(并线器无需决策) | 同 stage |
| `end` | status=completed | 「工作流已完成。请输出总结后结束」(模型随后自然停止) |
`choose_workflow_branch(target_node_id)`:仅当前停在 branch 时可调;校验 target 在候选路由集;推进后返回新阶段详情。
技术可行性已验证:`handle_tool_call` 为 asynctools_execution.py L960`submit_plan` 已证明工具可长阻塞等待外部事件——审核在工具内同步 await 成立。
### 4.4 审核智能体
`modules/workflow_review_agent.py`fork `goal_review_agent.py`),内部工具 `report_workflow_review(decision: pass|reject, message)`(仿 `report_goal_status`:唯一结论出口 + 强制工具调用重试)。配置统一走个人空间「审核智能体」页(`modules/review_agent_config.py::resolve_review_agent_config`,模型复用子智能体模型库);`review_mode: active` 时注入只读 run_command 取证。
**payload 构建**(核心:让审核看证据而非听汇报):
```
【工作流】{name}{description}
【本次审核把关】{review 节点名}
审核关注点:{review.prompt}
【被审核阶段】{stage 名}
阶段目标:{stage.goal}
阶段要求:{stage.instructions}
【阶段执行痕迹】(消息游标 stage_start_msg_index 截取的对话增量,截断控长)
1. run_command: git diff --stat → …
2. read_file: server/chat.py L120-180 → …
【主智能体阶段汇报】{summary}
【历史审核意见】(若是重审,带上前几次驳回意见)
```
### 4.5 审核结果与边界
- **pass** → 推进到 review.next若为 branch 多出线 → 停在 branch 等选择,返回分支菜单)。
- **reject** → reject_counts[node]+1 → 回到 review.rejectTostage 或 branch工具返回带审核意见。
- **审核异常/超时** → 视为驳回(计入计数),返回中含「审核服务可能异常,**请告知用户**」。
- **撞 maxRejects** → status=failedexit_reason=max_rejects工具返回「连续驳回超限工作流已终止请告知用户」。模型在场同轮闭环不另发 user 消息。
### 4.6 max_stage_rounds 撞限(柔性询问)
- `stage_rounds` 跨任务累计:主循环**每轮迭代** +1挂点在主循环层不在工具循环内——遵守注入时序铁律进入新阶段清零。
- 撞限 → 注入 inline user 消息(`inject_runtime_user_message(inline=True)`,下轮模型即看到):
「工作流阶段「X」已进行 N 轮,达到上限。请立刻停下当前工作,告知用户已超过 N 轮,并询问是否还要继续。」
- **工作流不停**status 保持 active原地挂起等用户发话。
- 防重复:撞限后 `round_limit_notified=true`
- 清零时机:下一条真实用户消息到达、新任务开始时清零(视作用户已知情交互);之后若再跑 X 轮会再次询问——每超 X 轮问一次。
### 4.7 柔性退出与通知池
**模型自主 deactivate / completed**:工具返回同轮闭环,不需要 user 消息;前端 UI 走进度事件。
**用户 slash deactivate**REST随时可用不做空闲检查——摘牌不打断正在跑的任务
- 智能体**运行中** → 事件入 `pending_notices`;在 `execute_tool_calls` 末尾统一消费点注入 inline 通知(与 `process_sub_agent_updates` 同位置;遵守「循环内收集 → 循环后注入」铁律),模型下轮看到。
- 智能体**空闲** → REST 直接派发:预占主任务门闸 → 完整 user 消息(持久化+广播+metadata 齐全)→ 创建主任务。与 slash activate 共用派发函数。
- **兜底**:运行中模型在通知被消费前就停了 → 任务结尾轮询器 spawn 条件兜住(见下)。
**通知池 = 统一完成通知轮询器的第三路**`poll_completion_notifications`2026-06 实施、2026-08 门闸化),不新造轮子。改动点(均在 `server/chat_flow_task_main.py`
1. `_collect_pending_completion_notices`:增加 workflow 一路(与子智能体、后台命令按时间混排)。
2. `needs_completion_poll`L2478 附近):扩展条件 `or _has_pending_workflow_notices(...)`——否则无后台任务时工作流通知产生后轮询器不 spawn。
3. 轮询器主体零改动(门闸预占、批量预写+末条触发、新任务续轮询全复用)。
通知消息 metadata 新增 `message_source: "workflow"`(未识别时回落 "user")。
**摘牌后的工具行为**stopped/failed 后模型再调 `report_workflow_stage` / `choose_workflow_branch` → 返回 `success:false` + 「工作流已退出(原因),如需重新开始请重新激活」——返回错误但不炸任务。
### 4.8 与 goal 模式叠加
不互斥。工作流活跃时主循环尾部 no-tool-calls 分支不做任何工作流拦截兜底提醒已取消goal 审核照常。
## 5. 工具清单
### 主智能体5 个)
| 工具 | 参数 | 行为/返回 |
|---|---|---|
| `activate_workflow` | `name` | 加载+快照+初始化;返回全景目录+入口阶段详情。同工作流幂等;不同工作流拒绝 |
| `report_workflow_stage` | `summary` | 阶段汇报核心状态机,返回按 §4.3 矩阵分派 |
| `choose_workflow_branch` | `target_node_id` | 分支选择;校验候选集;返回新阶段详情 |
| `get_workflow_status` | — | 返回当前节点/已走路径/各阶段轮数/审核历史(迷失自查+用户问进度) |
| `deactivate_workflow` | `reason` | 柔性退出:摘牌+工具返回确认(模型自主退出无需 user 消息) |
### 审核智能体内部1 个)
| 工具 | 参数 | 说明 |
|---|---|---|
| `report_workflow_review` | `decision: pass\|reject`, `message` | 唯一结论出口,仿 report_goal_status |
### REST3 个)
- `POST /api/workflow/activate`仅智能体空闲409 检查);快照+初始化+派发完整 user 消息任务
- `POST /api/workflow/deactivate`:随时可用;忙入通知池、闲直发任务
- `GET /api/workflow/status?conversation_id=`:轮询/刷新恢复
## 6. 通信与前端
- **轮询事件**sender → session_data 快照 → REST 轮询,对齐 goal 链路):`workflow_progress`(推进/驳回)/ `workflow_review_progress`(审核进行中)/ `workflow_completed` / `workflow_stopped` / `workflow_failed`。快照自带 conversation_id。
- **slash 菜单**:新增 workflows 模式,数据用现成 `GET /api/workflows`
- **进度展示**:仿 goal 进度组件——当前阶段名 · 已走路径 · 审核状态 · 轮数。
- **刷新恢复**`GET /api/workflow/status`。
## 7. 已实现部分清单(编辑器期)
- `modules/workflow_manager.py`、`server/workflow_page.py`
- `static/src/components/workflow/``workflowModel.ts`(模型/校验/dagre 自动排版)、`WorkflowLibraryView.vue`、`WorkflowEditorView.vue`、四种节点组件
- `workflows/` 四个内置示例code-review-pipeline / bug-fix-triage / feature-development / research-report
## 8. 实施清单(文件级,按依赖顺序)
1. `modules/workflow_state_manager.py`(新增):状态目录读写、快照复制、计数、通知池、消息游标。
2. `modules/workflow_review_agent.py`新增fork goal_review_agent+ `prompts/workflow_review_agent.txt`(配置后改为统一审核智能体设置,见 `modules/review_agent_config.py`)。
3. `server/workflow_flow.py`(新增):编排——`build_activation_text`、system 段构建、推进矩阵、审核调用、通知文本构造、状态查询。
4. 工具注册:`core/main_terminal_parts/tools_definition/` 新增 5 个定义;`tools_execution.py` 注册 handler。
5. system 段注入:`core/main_terminal_parts/context/messages.py` 追加工作流段(不冻结)+ 推进时刷新机制。
6. 主循环挂点(`server/chat_flow_task_main.py`stage_rounds 计数与撞限注入;`needs_completion_poll` 扩展;`_collect_pending_completion_notices` 加 workflow 路;工具循环末尾消费工作流通知。
7. REST`server/tasks/workflows.py`新增activate / deactivate / status。
8. 前端slash 菜单 workflows 模式、进度组件、轮询事件消费(`taskPolling/lifecycle.ts`)、刷新恢复。
9. 验证:`python3 -m py_compile` 改动文件 + `python -m pytest test/test_server_refactor_smoke.py -q`
## 9. 硬约束(实施必须遵守)
- **单写者/门闸**:所有工作流逻辑发生在主任务内部(工具执行 + 主循环层不新增主任务入口REST 派发走门闸预占AGENTS.md §12
- **注入时序铁律**:工具循环内禁止直接 `inject_runtime_user_message`——一律「循环内收集 → 循环后注入」(记忆 `runtime_injected_message_convention`)。
- **运行期注入标记**:注入的 user 消息必须走 `inject_runtime_user_message`(自动带 `runtime_injected` 等标记),否则刷新恢复重建会重复显示。
- **不掐断智能体**:任何工作流路径不得 raise/return 导致主任务异常终止;柔性优先。

View File

@ -0,0 +1,11 @@
# 公开发布排除清单GitHub public 分支)
# 用途:发布脚本同步 main → public 时,按本清单剔除文件。
# 语义:清单内文件在私人仓库 main 正常跟踪、正常工作;仅不进入 GitHub 公开仓库。
# 格式rsync --exclude-from 兼容模式(每行一条,# 开头为注释)。
# 维护规则:新增敏感/内部文件时追加一行;净化后可公开发布的条目应移除。
# ---- 内部调研笔记(已完成使命,公开后易过时误导)----
workflow_research/
# ---- docs/ 内部文档(分析/方案过程稿AGENTS.md 中对 docs/ 的引用需随 AGENTS.md 处理一并调整)----
docs/

179
scripts/release_public.sh Executable file
View File

@ -0,0 +1,179 @@
#!/usr/bin/env bash
# =============================================================================
# 公开发布脚本main → GitHub 公开仓库
#
# 机制:
# 1. 本地 public 分支为 orphan 分支(与 main 无共同历史),防止 main 历史中
# 曾出现过的敏感文件(如旧版 AGENTS.md随历史泄漏到公开仓库。
# 2. 内容来源是 `git archive main`(仅含被跟踪文件),未跟踪文件
# .env / node_modules / .astrion/ 等)天然不会混入。
# 3. 按 scripts/public_release_excludes.txt 剔除「被跟踪但不公开」的条目。
# 4. 发布前执行泄漏扫描(硬闸),命中即中止。
#
# 用法:
# bash scripts/release_public.sh # 交互式(摘要后逐确认 commit / push
# bash scripts/release_public.sh --dry-run # 只导出+扫描+展示摘要,不提交不推送
# bash scripts/release_public.sh --yes # 跳过确认(仅限已获得明确授权时)
# =============================================================================
set -euo pipefail
# ---- 配置 -------------------------------------------------------------------
PUBLIC_BRANCH="public" # 本地公开分支orphan
GITHUB_REMOTE="github" # GitHub remote 名
GITHUB_TARGET_BRANCH="main" # 推送到 GitHub 的目标分支
EXCLUDES_FILE="scripts/public_release_excludes.txt"
# 泄漏扫描模式(上下文特征,非裸词;避免误报公开信息如 GitHub 用户名)
LEAK_PATTERNS=(
'/Users/jojo'
'KOJO JOTARO'
'C:\\Users\\KOJO'
'cyjai\.com'
'git\.cyjai'
'正在修复中'
'astrion-clone'
'E:\\astrion'
)
# ---- 参数 -------------------------------------------------------------------
DRY_RUN=false
ASSUME_YES=false
for arg in "$@"; do
case "$arg" in
--dry-run) DRY_RUN=true ;;
--yes) ASSUME_YES=true ;;
-h|--help)
grep '^#' "$0" | sed 's/^# \{0,1\}//' | head -20
exit 0 ;;
*) echo "未知参数: $arg"; exit 2 ;;
esac
done
confirm() { # $1=提示语
if $ASSUME_YES; then return 0; fi
local reply
read -r -p "$1 [y/N] " reply
[[ "$reply" == "y" || "$reply" == "Y" ]]
}
# ---- 前置检查 ---------------------------------------------------------------
cd "$(dirname "$0")/.."
echo "==> 仓库根: $(pwd)"
if ! git remote get-url "$GITHUB_REMOTE" >/dev/null 2>&1; then
echo "!! 未找到 remote '$GITHUB_REMOTE'。请先执行:"
echo " git remote add $GITHUB_REMOTE <GitHub 仓库地址>"
exit 1
fi
CURRENT_BRANCH=$(git symbolic-ref --short HEAD)
if [[ "$CURRENT_BRANCH" != "main" ]]; then
echo "!! 当前分支是 $CURRENT_BRANCH,请先切换到 main。"
exit 1
fi
MAIN_SHA=$(git rev-parse --short main)
echo "==> 发布来源: main @ ${MAIN_SHA}(未提交到 main 的改动不会包含)"
# 工作区脏检测:未提交改动不会进入发布内容,明确警告防止「改了忘提交」
if [[ -n "$(git status --porcelain)" ]]; then
echo "!! 警告:工作区存在未提交改动,以下内容【不会】出现在本次发布中:"
git status --short | head -20
if ! $DRY_RUN; then
confirm "仍要基于 main 最新提交继续吗?" || { echo "已取消。请先提交或暂存改动。"; exit 1; }
fi
fi
# ---- 准备 public orphan 分支 ------------------------------------------------
if ! git show-ref --verify --quiet "refs/heads/$PUBLIC_BRANCH"; then
echo "==> 首次发布:创建 orphan 分支 '$PUBLIC_BRANCH'(空初始提交,与 main 无历史关联)"
EMPTY_TREE=$(git mktree </dev/null)
INIT_COMMIT=$(git commit-tree "$EMPTY_TREE" -m "chore: initialize public release branch")
git update-ref "refs/heads/$PUBLIC_BRANCH" "$INIT_COMMIT"
fi
# ---- 准备临时 worktree不碰当前工作区 -------------------------------------
WT=$(mktemp -d /tmp/astrion_public_release.XXXXXX)
cleanup() {
git worktree remove --force "$WT" >/dev/null 2>&1 || true
git worktree prune >/dev/null 2>&1 || true
}
trap cleanup EXIT
git worktree add --force "$WT" "$PUBLIC_BRANCH" >/dev/null
echo "==> 临时 worktree: $WT"
# 清空 worktree 内容(保留 .git保证被删除的文件能反映为删除
find "$WT" -mindepth 1 -maxdepth 1 ! -name .git -exec rm -rf {} +
# ---- 导出 main 内容 ----------------------------------------------------------
git archive main | tar -x -C "$WT"
echo "==> 已从 main 导出被跟踪文件"
# ---- 按排除清单剔除 ----------------------------------------------------------
if [[ -f "$EXCLUDES_FILE" ]]; then
while IFS= read -r pattern; do
[[ -z "$pattern" || "$pattern" =~ ^[[:space:]]*# ]] && continue
target="$WT/${pattern%/}"
if [[ -e "$target" ]]; then
rm -rf "$target"
echo "==> 已剔除: $pattern"
fi
done < "$EXCLUDES_FILE"
else
echo "!! 警告:排除清单 $EXCLUDES_FILE 不存在"
fi
# ---- 泄漏扫描(硬闸) --------------------------------------------------------
echo "==> 执行泄漏扫描..."
LEAK_HIT=false
for pattern in "${LEAK_PATTERNS[@]}"; do
hits=$(grep -rInE --exclude-dir=.git --exclude=.git --exclude=release_public.sh -- "$pattern" "$WT" 2>/dev/null || true)
if [[ -n "$hits" ]]; then
LEAK_HIT=true
echo "!! 命中模式 [$pattern]:"
echo "$hits" | sed "s|$WT/||" | head -10
fi
done
if $LEAK_HIT; then
echo "!! 泄漏扫描未通过,已中止。请净化后重试,或确认误报后调整扫描模式。"
exit 1
fi
echo "==> 泄漏扫描通过"
# ---- 摘要 --------------------------------------------------------------------
cd "$WT"
git add -A
echo
echo "==================== 变更摘要(相对上一次公开提交) ===================="
git status --short | head -60
CHANGED=$(git status --short | wc -l | tr -d ' ')
TOTAL=$(find . -type f ! -path './.git/*' | wc -l | tr -d ' ')
echo "--------------------------------------------------------------------"
echo "变更条目: $CHANGED 公开文件总数: $TOTAL"
echo "======================================================================"
echo
if $DRY_RUN; then
echo "==> dry-run 结束(未提交、未推送)。临时 worktree 已自动清理。"
exit 0
fi
if [[ "$CHANGED" == "0" ]]; then
echo "==> 与上一次公开提交相比无变化。"
confirm "仍然创建一个空发布提交并推送吗?" || { echo "已取消。"; exit 0; }
fi
# ---- 提交 --------------------------------------------------------------------
DATE=$(date +%Y-%m-%d)
echo "==> 提交信息: release: sync from main @$MAIN_SHA ($DATE)"
confirm "确认在 '$PUBLIC_BRANCH' 分支上创建发布提交?" || { echo "已取消。"; exit 0; }
git commit -m "release: sync from main @$MAIN_SHA ($DATE)"
# ---- 推送 --------------------------------------------------------------------
echo "==> 推送目标: $GITHUB_REMOTE $PUBLIC_BRANCH:$GITHUB_TARGET_BRANCH"
confirm "确认推送到 GitHub公开仓库" || { echo "已取消推送(本地提交已保留)。 "; exit 0; }
git push "$GITHUB_REMOTE" "$PUBLIC_BRANCH:$GITHUB_TARGET_BRANCH"
echo "==> 完成。公开仓库已更新到 main @$MAIN_SHA 的快照。"

View File

@ -1,94 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Theme Debug</title>
<script>
window.addEventListener('DOMContentLoaded', () => {
console.log('=== Theme Debug Info ===');
console.log('document.documentElement.getAttribute("data-theme"):', document.documentElement.getAttribute('data-theme'));
console.log('document.body.getAttribute("data-theme"):', document.body.getAttribute('data-theme'));
console.log('document.documentElement.dataset.theme:', document.documentElement.dataset.theme);
console.log('document.body.dataset.theme:', document.body.dataset.theme);
// 检查顶部栏元素
const topbarTitle = document.querySelector('.mobile-topbar-title');
if (topbarTitle) {
const styles = window.getComputedStyle(topbarTitle);
console.log('=== .mobile-topbar-title ===');
console.log('color:', styles.color);
console.log('text-shadow:', styles.textShadow);
}
// 检查模型选择器
const topbarSelector = document.querySelector('.mobile-topbar-selector');
if (topbarSelector) {
const styles = window.getComputedStyle(topbarSelector);
console.log('=== .mobile-topbar-selector ===');
console.log('color:', styles.color);
}
// 检查快捷菜单
const quickMenu = document.querySelector('.quick-menu');
if (quickMenu) {
const styles = window.getComputedStyle(quickMenu);
console.log('=== .quick-menu ===');
console.log('background:', styles.background);
console.log('background-color:', styles.backgroundColor);
}
// 检查菜单项
const menuEntry = document.querySelector('.menu-entry');
if (menuEntry) {
const styles = window.getComputedStyle(menuEntry);
console.log('=== .menu-entry ===');
console.log('color:', styles.color);
}
console.log('=== End Debug Info ===');
});
</script>
</head>
<body>
<p>请在浏览器中打开主应用,然后查看控制台输出。</p>
<p>或者将以下代码复制到浏览器控制台运行:</p>
<pre>
console.log('=== Theme Debug Info ===');
console.log('document.documentElement.getAttribute("data-theme"):', document.documentElement.getAttribute('data-theme'));
console.log('document.body.getAttribute("data-theme"):', document.body.getAttribute('data-theme'));
console.log('document.documentElement.dataset.theme:', document.documentElement.dataset.theme);
console.log('document.body.dataset.theme:', document.body.dataset.theme);
const topbarTitle = document.querySelector('.mobile-topbar-title');
if (topbarTitle) {
const styles = window.getComputedStyle(topbarTitle);
console.log('=== .mobile-topbar-title ===');
console.log('color:', styles.color);
console.log('text-shadow:', styles.textShadow);
}
const topbarSelector = document.querySelector('.mobile-topbar-selector');
if (topbarSelector) {
const styles = window.getComputedStyle(topbarSelector);
console.log('=== .mobile-topbar-selector ===');
console.log('color:', styles.color);
}
const quickMenu = document.querySelector('.quick-menu');
if (quickMenu) {
const styles = window.getComputedStyle(quickMenu);
console.log('=== .quick-menu ===');
console.log('background:', styles.background);
console.log('background-color:', styles.backgroundColor);
}
const menuEntry = document.querySelector('.menu-entry');
if (menuEntry) {
const styles = window.getComputedStyle(menuEntry);
console.log('=== .menu-entry ===');
console.log('color:', styles.color);
}
console.log('=== End Debug Info ===');
</pre>
</body>
</html>

View File

@ -1,218 +0,0 @@
# 代码库「工作流Workflow功能」挂载点调研报告
> 调研目标为即将新增的「工作流Workflow功能」定位代码库内的接入挂载点。
> 前提理解:工作流 = 用户把「一套既定流程:工作方式→验证方式→结束方式」存为一个工作流文件夹(类似现有 skill 文件夹机制,可附带验证过的脚本/代码文件);之后在对话中复用以现有智能体循环+执行引擎运行它;可能有独立 URL`/workflow/new`)或作为普通对话的一种运行方式;阶段/整体结束时由审核智能体审核(复用两套现有审核:工具审核 + 目标模式 goal_review
>
> **调研约束**:仅只读操作,未修改任何项目文件。所有结论均基于实际读到的代码,标注文件路径与行号;不确定处明确写「未确认」。
> 所有代码路径均以项目根 `/Users/jojo/Desktop/agents/正在修复中/agents` 为准(相对路径书写)。
---
## 1. Skill 系统全链路(工作流文件夹需仿照它)
### 1.1 目录结构与 SKILL.md frontmatter
- **源码树 skill 库**`agentskills/`(每个 skill 一个子目录,内含 `SKILL.md`,可含 `references/`、`scripts/` 等附带文件)。示例:`agentskills/terminal-guide/SKILL.md`、`agentskills/skill-creator/SKILL.md`。
- **SKILL.md frontmatter 格式**YAML 简单解析):
```
---
name: terminal-guide
description: 持久化终端使用指南。……
---
```
后端校验逻辑见 `modules/skills_manager.py::validate_skill_directory`(第 146 行起),**只强制要求存在 `name:``description:`**`re.search(r"(?m)^\s*name\s*:", content)` 与 description 同理。frontmatter 全文解析见 `_parse_frontmatter`L174
- **目录名校验**`_is_valid_skill_id`L202`SKILL_ID_PATTERN`
**工作流接入判断**工作流文件夹可直接复用这套「SKILL.md + 附带脚本」结构。建议新增一个 `WORKFLOW.md`(或复用 frontmatter 扩展字段),并在目录中携带验证过的 `scripts/` 目录。校验时除 SKILL.md 的 name/description 外,可扩展校验「工作方式→验证方式→结束方式」三段定义。
### 1.2 skill 创建/归档工具后端实现
- **工具定义**`core/main_terminal_parts/tools_definition/file_tools.py` L139 定义 `create_skill`(描述「验证并归档一个已创建好的 skill 文件夹」)。
- **handler**`core/main_terminal_parts/tools_execution.py`
- `_resolve_create_skill_source_dir`L525解析 source_dir相对/绝对/宿主机任意路径)。
- `_get_create_skill_target_root`L539归档目标根目录 —— 优先 `infer_private_skills_dir(self.data_dir)`,否则 `data_dir` 父级下 `agentskills`
- `_handle_create_skill_tool`L545调用 `archive_skill_directory(source, target_root)` 归档;成功后同步工作区 skills`sync_workspace_skills`)。
- **归档函数**`modules/skills_manager.py::archive_skill_directory`L182——先 `validate_skill_directory` 校验,再 `shutil.move` 移动到目标库;目标存在则拒绝(不覆盖)。
- **私有 skill 目录推断**`modules/skills_manager.py::infer_private_skills_dir`L97——host 模式统一 `~/.astrion/astrion/<mode>/agentskills`web/docker 按用户路径推断。
**工作流接入判断**:可仿 create_skill 新增 `create_workflow` 工具或复用同一归档通道handler 在 `tools_execution.py``handle_tool_call` 分发链L960elif tool_name 分支约 L1130注册归档逻辑复用 `archive_skill_directory` 的校验+移动+拒绝覆盖机制。
### 1.3 skill 如何注入提示词
- 注入点:`core/main_terminal_parts/context/messages.py` L305-320`build_messages` 内按 system prompt 顺序追加 skills 列表:
```python
skills_catalog = get_skills_catalog(private_dir=infer_private_skills_dir(self.data_dir))
enabled_skills = merge_enabled_skills(...)
def _build_skills_system_prompt():
template = self.load_prompt("skills_system")
return build_skills_prompt(template, build_skills_list(...))
skills_prompt = self._get_or_init_frozen_prompt("frozen_skills_prompt", _build_skills_system_prompt)
if skills_prompt: messages.append({"role":"system","content":skills_prompt})
```
- **模板**`prompts/skills_system.txt`(占位 `{skills_list}`,提示用 `read_skill` 读取)。
- **enabled/sync**`merge_enabled_skills`skills_manager L373、`sync_workspace_skills`L409——把启用 skill 复制到工作区 `.astrion/skills/`
- **skill_hints 意图识别**`config/skill_hints.json`关键词→hint用于在用户消息中检测相关 skill 并注入提示(读 `_inject_intent` 相关)。
**工作流接入判断**:工作流列表可仿 skills_prompt 在 `messages.py` `build_messages` 增加一段 system prompt 注入当前工作流上下文(当前已激活工作流的流程说明),复用 `_get_or_init_frozen_prompt` 冻结机制与 `load_prompt` 模板机制。
### 1.4 skill 列表/读取 API
- **列表 API**`server/tasks/skills.py` `GET /api/skills`L126`@tasks_bp.route`)→ `_list_workspace_skills`L83读取工作区 `.astrion/skills/*/SKILL.md`,解析 name/description/path。
- **读取**:工具 `read_skill`file_tools.py L132→ handler `_handle_read_skill_tool`tools_execution.py 中)。
**工作流接入判断**:仿 `server/tasks/skills.py` 新增 `GET /api/workflows`(读工作流库目录),用于前端列表展示与「作为对话新类型创建」时选择工作流。
---
## 2. 两套审核智能体架构(工作流阶段审核复用)
### 2.1 工具审核 approval_agent
- **实现**`modules/approval_agent.py`(由配置 `CONFIG_PATH = Path(resolve_deploy_config("auto_approval.json"))` 指定L16 → `config/auto_approval.json[.example]`)。
- **调用方式**`modules/auto_approval_service.py::run_auto_approval`L84构造 `ApprovalAgent(web_terminal=web_terminal)``await agent.review(payload_text=..., progress_cb=..., cancel_check=...)`。`build_auto_approval_payload`L15组装用户输入+最近工具操作+触发风险标记。
- **输入**payload 纯文本(审批场景上下文 + 当前工具调用函数名/参数)。
- **输出**`{"decision": "approved"|"rejected", "reason": str, "source": "approval_agent"}`。内部通过工具 `approve_decision`L168返回异常/配置缺失时兜底 `{"decision":"rejected", ...}`
- **执行模式**readonly 或注入只读 run_command 取证(与 goal_review 同构)。
**工作流接入判断**:工作流的「工具/操作级」安全审核可直接复用 `ApprovalAgent`,通过 `auto_approval_service` 同构入口调用;无需新造。若需按工作流阶段应用不同审核,可在 `run_auto_approval` 之外包装一个「阶段审核」封装函数。
### 2.2 目标审核 goal_review
- **实现**`modules/goal_review_agent.py`(配置 `CONFIG_PATH = resolve_deploy_config("goal_review.json")`L20。`GoalReviewAgent(web_terminal=...)``async review(payload_text, review_mode, progress_cb, cancel_check)`。
- **输出**`{"status":"done"|"continue", "message": str, "source":"goal_review_agent"}`;兜底保守返回 continue。review_mode 由 `modules/goal_state_manager.py` 常量 `REVIEW_MODE_READONLY="readonly"` / `REVIEW_MODE_ACTIVE="active"`L34-35决定是否注入只读 run_command。
- **目标模式如何触发审核**`server/goal_flow.py`
- `maybe_start_goal`L56用户显式开启目标模式时启动读 personalization 的 review_mode/end_conditions
- `inject_goal_prompt`L88注入目标模式提示词。
- `handle_goal_after_turn`L147**在主循环「主模型一轮无 tool_calls」时拦截**——先空转保护/边界检查,再调 `GoalReviewAgent.review`
- `status=="continue"``append_review` 记录审核历史 + `inject_runtime_user_message(CONTINUE_PREFIX + message)` **回写续命 user 消息到 messages**,调用方 `continue` 回主循环开下一轮;
- `status=="done"``mark_done`,任务结束。
- **状态落盘**`modules/goal_state_manager.py`,工作区 `{data_dir}/goal_state.json`(含 goal、review_mode、turns、review_history 等)。
- **触发入口**(后端):创建任务时传 `goal_mode``server/tasks/models.py` `create_chat_task` 存 session_data → `run_chat_task_sync` 时置 `terminal._goal_mode_requested=True`models.py L961/L998-999`chat_flow_task_main.py L1863` 检测并 `maybe_start_goal`
**工作流接入判断(关键复用点)**:工作流的「阶段结束/整体结束审核」几乎可 1:1 复用 goal_review 的回写机制——在 `chat_flow_task_main.py` 主循环的 no-tool-call 结束判定处L2226 附近 `if not tool_calls:`),当前已有 `if goal_is_active(workspace): handle_goal_after_turn(...)` 分支;工作流运行时可仿照新增「工作流阶段/整体审核」调用 `GoalReviewAgent`(或新 `WorkflowReviewAgent`),并把 continue/done 语义映射为「进入下一阶段/结束工作流」。建议抽出一个与 `goal_flow.py` 同构的 `workflow_flow.py` 编排模块,减少对主循环侵入。
---
## 3. 智能体循环与执行引擎(工作流运行时复用)
### 3.1 主循环入口链路(骨架级)
- **唯一入口(门闸)**`server/chat_flow.py::process_message_task`L130——先 `acquire_adopted_main_task_gate` 拿对话级门闸§12 单写者不变量),然后 `loop.create_task(handle_task_with_sender(...))`。**任何新增的主任务入口必须走此门闸**AGENTS.md §12.4)。
- **实际任务执行**`server/chat_flow_task_main.py::handle_task_with_sender`L1411
- L1855 `messages = web_terminal.build_messages(context, message)``tools = web_terminal.define_tools()`。
- L1863 目标模式启动/续注入。
- L1962 `while max_iterations is None or iteration < max_iterations:` 主循环迭代:
- 每轮 `run_streaming_attempts(messages=messages, tools=tools, ...)`L2082导入自 `server/chat_flow_stream_loop.py`)调模型,返回 full_response / tool_calls。
- **有 tool_calls** → L2325 `execute_tool_calls(...)` 执行工具(结果 append 到 messages/对话历史),`continue` 下一轮。
- **无 tool_calls**L2226 `if not tool_calls:`)→ 目标审核分支L2230→ 多智能体注入分支L2314→ 否则 `break` 结束。
- **工具执行循环**`server/chat_flow_tool_loop.py::execute_tool_calls`L409`_execute_tool_calls_impl`L426守护包装 try/finally 复位 `_tool_loop_active`
- 白名单校验allowed_tool_names→ 权限评估 `web_terminal.evaluate_tool_permission` → 逐个 `tool_call` 解析/执行,结果 `web_terminal.context_manager.add_conversation("tool", ...)` + `messages.append({"role":"tool", ...})`
- 工具真实执行在 `core/main_terminal_parts/tools_execution.py::handle_tool_call`L960elif tool_name = handler 分发大表)。
**工作流接入判断**:工作流运行**不需要**新增主循环,直接复用 `process_message_task` 门闸 + `handle_task_with_sender` 迭代即可(把工作流定义作为额外的 system 上下文 + 结束阶段审核挂钩即可)。若想「阶段结束审核后进入下一阶段并继续迭代」,参照 goal_review 的 continue → `inject_runtime_user_message` + `continue` 模式,完全在现有消息/迭代机制内实现。
### 3.2 运行模式 work_mode 三档plan/ask/execute——新增一种运行方式的最相近参照
- **核心实现**`core/main_terminal_parts/tools_policy.py``WORK_MODES = {"plan","ask","execute"}`, `WORK_MODE_DEFAULT="plan"`L88`set_work_mode` / `switch_work_mode`(处理 plan⇄只读权限+沙箱联动)/ `get_work_mode`;权限模式 `PERMISSION_MODES` 与 plan 锁。
- **怎么注入提示词规则**`core/main_terminal_parts/context/mode.py`
- `_build_work_mode_rules(mode)`L192三档规则文本唯一来源plan/ask/execute 各自的 detailed_rules
- `_build_work_mode_message`L220`load_prompt("work_mode")` 模板 + `replace work_mode/work_mode_label/mode_description/detailed_rules`
- 属于 `_RUNTIME_MODE_KINDS`L133含 permission_mode/execution_mode/network_permission/work_mode切换走 drift 通知(`_RUNTIME_MODE_SOURCE` 第 花 种 "work_mode_change")。
- **冻结注入**`messages.py::build_messages` L203-208 `_get_or_init_frozen_mode_prompt("frozen_work_mode_prompt", self._build_work_mode_message)`
- **怎么影响工具可用性**work_mode 主要通过**提示词**约束(三档规则正文),而非硬性工具过滤(行为差异在规则里,如 ask 禁用 ask_user 工具由提示词说明plan 的只读是权限层联动)。工具的硬开关由 `tool_category_states`/`disabled_tools`(同一文件 tools_policy.py控制。
**工作流接入判断**:若把「工作流运行」作为与 work_mode 平行的「一种运行方式」,可仿此模式:新增一类提示词规则(如 `workflow.txt` 模板 + `_build_workflow_rules`+ 在 `messages.py` 追加一段冻结注入;运行会话期间注入工作流当前阶段上下文。但注意 work_mode 是「与用户交互节奏」正交档位,与工作流(一套流程)语义不同——更倾向工作流作为「消息级上下文+审核挂钩」,而非新增第四档 work_mode。建议工作流保持并在 `_RUNTIME_MODE_KINDS` 之外独立管理(不要混入 work_mode 切档,避免交互节奏冲突)。
### 3.3 提示词模板机制prompts/ 组织 + mode.py 构建)
- **目录**`prompts/*.txt`(如 `main_system.txt`、`work_mode.txt`、`skills_system.txt`、`goal_review_agent.txt`、`execution_mode/`、`multi_agent/`、`sub_agent/`)。
- **加载**`core/main_terminal_parts/context/prompt.py::load_prompt(name)`L157——`Path(PROMPTS_DIR)/f"{name}.txt"`,存在则读,否则返回兜底文案。
- **构建规则**`context/mode.py` 负责把「运行期状态」渲染进 promptpermission/execution/work_mode/network`context/messages.py` 在 `build_messages`L109按固定 order 追加各 system prompt主 prompt → 权限 → 执行环境 → 运行模式 → 最近对话 → 个性化 → 工作区 → AGENTS.md → skills → 记忆 → 自定义 → 禁用提示。
- **冻结机制**`_get_or_init_frozen_prompt` / `_get_or_init_frozen_mode_prompt` 对可变但需稳定的 system 段做一次性构建缓存。
**工作流接入判断**:新增 `prompts/workflow.txt` 模板(或 `prompts/workflow/` 子目录),在 `messages.py::build_messages` 适当的 order 位(建议主 prompt 后、skills 前/后)插入工作流上下文 system 段,并用 `load_prompt("workflow")` 加载。完全复用现有模板/冻结机制。
---
## 4. 前端入口与路由(/workflow/new 怎么挂)
### 4.1 路由定义位置与现有 /multiagent 兼容重定向
- **前端未用 vue-router**,而是手动处理 `window.location.pathname`(无 createRouter/createWebHistory全库无 vue-router
- **路由解析/重定向核心**`static/src/app/methods/ui/route.ts::bootstrapRoute`L26 起):
```
let path = window.location.pathname.replace(/^\/+/, '');
if (path === 'multiagent/new' || path === 'multiagent') {
history.replaceState({}, '', '/new'); path = 'new';
} else if (path.startsWith('multiagent/')) {
const bareId = ...; history.replaceState({}, '', `/${bareId}`); path = bareId;
}
```
即旧 `/multiagent/*` → 重定向到裸 `/conv_xxx``/multiagent/new` → `/new`(对话类型是 metadata 属性,不再是路由概念)。
- 若裸新建路由:清空当前对话、恢复 draft → 结束。
- 否则按 `conv_<id>``this.enterConversation(convId, ...)``app/methods/conversation/bootstrap.ts`)加载并恢复。
- `isExplicitNewConversationRoute()`:判断是否显式新建路由。
**工作流接入判断**`/workflow/new` 可在 `bootstrapRoute` 增加一个分支:`if (path === 'workflow/new' || path === 'workflow')` → 进入「新建工作流对话」模式(清空 + 标记 `newConversationType='workflow'``history.replaceState` 保持 URL 为 `/workflow/new`。后端需相应 `with_terminal`/路由能接受该路径(未确认:当前 route 无条件把 path 当 conv id需额外分支避开
### 4.2 对话类型机制newConversationType / currentConversationType
- **状态定义**`static/src/app/state.ts`
- `currentConversationType`L57当前已打开对话类型 `'normal'|'multi_agent'|null`,由 `enterConversation` 从 metadata 恢复(`bootstrap.ts` L66
- `newConversationType`L59空对话待创建类型 `'agent'|'multi_agent'`localStorage 持久化key 见 state.ts 上方 `NEW_CONVERSATION_TYPE_STORAGE_KEY`),由 input composer 的 `agent-type-switcher` 修改。
- **创建对话按类型选 API**
- 首条消息创建:`static/src/app/methods/message/send.ts` L269`const isMultiAgent = this.newConversationType === 'multi_agent'; const createUrl = isMultiAgent ? '/api/multiagent/conversations' : '/api/conversations';`
- 侧边栏新建:`static/src/app/methods/conversation/action.ts` L134/L276`useConversationStore().sidebarConversationType` 选同两个 URL。
- **输入栏类型选择器**`components/input/InputComposer.vue` L328 `agent-type-switcher`(渲染 `newConversationType` 选项:智能体/多智能体)。
**工作流接入判断(关键)**:若工作流作为新对话类型:需在 `newConversationType`/`currentConversationType` 类型联合中增加 `'workflow'``send.ts L269` 与 `action.ts L134/276` 增加「工作流创建」的 URL 分支(如 `/api/workflow/conversations` 或复用 `/api/conversations`+`workflow_id` 参数);`InputComposer.vue` 的 agent-type-switcher 增加工作流选项。注意 types 是字符串字面量联合L780 `newConversationType?: 'agent'|'multi_agent'`),需同步扩展 TS 类型与所有 switch 点。
### 4.3 输入栏运行模式切换器 work-mode-switcher
- **实现位置**`static/src/components/input/InputComposer.vue` L354`<div class="agent-type-switcher work-mode-switcher" ...>`,渲染 `workModeOptions``@click` 开 `workModeMenuOpen` 菜单。
- **切换逻辑**`static/src/app/methods/ui/workMode.ts``fetchWorkMode`GET /api/work-mode带 conversation_id、`changeWorkMode`POST /api/work-mode409 空闲才可切)、`toggleWorkModeMenu`/`closeWorkModeMenu`。plan 联动更新前端 permissionMode/executionMode 显示。
- **后端 API**`server/chat/permission.py` `GET/POST /api/work-mode`L402/L418
**工作流接入判断**:若工作流作为「普通对话的一种运行方式」(而非新 URL可在 work-mode-switcher 同区域加一个「工作流选择器」(选当前激活的工作流 / 另存为新工作流),方法放 `static/src/app/methods/ui/workflow.ts`(同构 workMode.ts后端提供 `GET/POST /api/workflow` 子资源。
---
## 5. 数据存储约定(工作流文件夹放哪)
- **运行态数据根**`config/paths.py``RUNTIME_ROOT = ~/.astrion/astrion`L83-84`ASTRION_DATA_ROOT` 覆盖);`_MODE = _runtime_mode()` 分流 host/web`TERMINAL_SANDBOX_MODE` 决定)。各目录见 §1.5`DATA_DIR/LOGS_DIR/USER_SPACE_DIR/API_USER_SPACE_DIR`L102-106
- **CUSTOM_SKILLS_DIR**`str(Path(RUNTIME_ROOT)/_MODE/"agentskills")`L109——私有 skill/工作流的运行态位置。host 模式 `IS_HOST_MODE`L112
- **源码树 seed**`AGENT_SKILLS_DIR`(默认 `./agentskills`L163、`PROMPTS_DIR``./prompts`L162、`AGENT_SKILLS_DIR` 默认源码树。
- **resolve_deploy_config 回退链**L135`<data_root>/config/<filename>`(部署者真实配置)→ 源码树 `config/<filename>`seed→ 源码树 `config/<filename>.example`(示例兜底)。`deploy_config_path`L126用于可写配置不回退
- **工作区级 .astrion**`WORKSPACE_SKILLS_DIRNAME=".astrion/skills"`L170、`WORKSPACE_MEMORY_DIRNAME=".astrion/memory"`L171、`WORKSPACE_REVIEW_DIRNAME=".astrion/review"`L172。**注意:这是工作区项目内(源码树或其他工作目录)的 `.astrion`,与运行态 `~/.astrion/astrion/<mode>` 不同。**
**工作流接入判断(关键决策点)**
- **源码树 vs 运行态**skill 采用「源码树 seed`agentskills/`)” + “运行态私有(`~/.astrion/astrion/<mode>/agentskills/`CUSTOM_SKILLS_DIR双源」通过 `get_skills_catalog(base_dir=AGENT_SKILLS_DIR, private_dir=CUSTOM_SKILLS_DIR)` 合并skills_manager.py L282启用后同步到工作区 `.astrion/skills/``sync_workspace_skills`)。
- 现有机制**同时支持**两种:工作流库若仿 skill可在源码树 `workflows/`seed+ 运行态 `~/.astrion/astrion/<mode>/workflows`CUSTOM_SKILLS_DIR 同级)建库,再按启用/激活同步到工作区 `.astrion/workflows/`。**判断**:用户创建的工作流属「运行态/用户私有」,应落运行态(含脚本的可执行文件),源码树只放内置种子示例;这与 AGENTS.md §1.5「运行态数据不落源码树」一致。
- 部署级配置(如 `auto_approval`、`goal_review`)走 `resolve_deploy_config`;工作流若需独立审核配置可仿 `config/workflow_review.json[.example]` 加入该回退链。
---
## 总结:新增工作流功能的最小改动路径建议(文件级)
基于上述调研,判断实现「工作流」的最小改动路径如下(按依赖顺序,每个文件给出改动性质):
1. **`config/paths.py`**(改动):新增 `CUSTOM_WORKFLOWS_DIR`(运行态,仿 `CUSTOM_SKILLS_DIR` L109、`WORKFLOWS_DIR`(源码树 seed仿 `AGENT_SKILLS_DIR` L163、`WORKSPACE_WORKFLOWS_DIRNAME`(仿 L170 `.astrion/workflows`)三个路径常量。
2. **`modules/workflows_manager.py`**(新增):仿 `modules/skills_manager.py` 提供 `validate_workflow_directory` / `archive_workflow_directory` / `get_workflows_catalog` / `sync_workspace_workflows`,校验「工作方式→验证方式→结束方式」三段 + 附带 scripts。
3. **`core/main_terminal_parts/tools_definition/file_tools.py` + `tools_execution.py`**(改动):新增 `create_workflow` 工具定义(仿 create_skill L139与 handler `_handle_create_workflow_tool`(仿 L545/530`handle_tool_call` 分发链L960 的 elif 分支)注册;归档复用 `modules/workflows_manager`
4. **`prompts/workflow.txt`**(新增)+ **`core/main_terminal_parts/context/messages.py`**(改动):新增工作流上下文模板;在 `build_messages`L109skills 段 L305 附近)追加「当前激活工作流」的 system 上下文注入(复用 `load_prompt` + `_get_or_init_frozen_prompt` 冻结)。
5. **`server/workflow_flow.py`**(新增)+ **`server/chat_flow_task_main.py`**(改动):仿 `server/goal_flow.py` 编排「阶段/整体审核」。接入点:主循环 `if not tool_calls:` 结束判定L2226仿 goal 分支L2230增加工作流审核调用——复用 `modules/goal_review_agent.py`(或其派生 `WorkflowReviewAgent`)返回 done/continuecontinue 注入续命 user 消息回写 messages复用 `chat_flow_task_support.inject_runtime_user_message`)。**注意**:任何新增主任务入口必须遵守 §12 门闸(`process_message_task`)。
6. **`server/tasks/workflows.py`**(新增,可并入 skills.py仿 `server/tasks/skills.py``GET /api/workflows`(列表/选择)+ 创建工作流对话的后端端点(如 `/api/workflow/conversations` 或复用 `/api/conversations`+`workflow_id`)。
7. **`static/src/app/methods/ui/route.ts`**(改动):`bootstrapRoute`L26增加 `/workflow/new`(及 `/workflow`)分支,进入「新建工作流对话」模式并 `history.replaceState`;避免被当作 conv id 解析。
8. **前端对话类型链路**(改动):`static/src/app/state.ts`L57/59 类型联合加 `'workflow'`)、`static/src/app/methods/message/send.ts`L269 createUrl 分支)、`static/src/app/methods/conversation/action.ts`L134/276 分支)、`static/src/components/input/InputComposer.vue`L328 agent-type-switcher / L354 work-mode-switcher 区域加「工作流选择器」)。若新增「选择/另存工作流」UI仿 `static/src/app/methods/ui/workMode.ts``static/src/app/methods/ui/workflow.ts`
> 附注 / 未确认项:
> - `/workflow/new` 后端的 `with_terminal` 路由是否接受该路径、前端是否需新 socket/status 兼容——**未确认**,需在实现时验证。
> - 工作流「阶段结束审核后进入下一阶段」与 goal mode「目标完成后结束」的边界语义略有差异需在 `workflow_flow.py` 内明确「阶段衔接」与「整体结束」的状态机,避免与 `chat_flow_task_main.py` 现有 break/continue 逻辑冲突。
> - work_mode 三档plan/ask/execute是「交互节奏」正交档位工作流不应作为第四档 work_mode而应作为独立的「消息级上下文 + 审核挂钩」,避免与现有 `_RUNTIME_MODE_KINDS` drift 通知机制冲突。

View File

@ -1,258 +0,0 @@
# 外部智能体工作流Agentic Workflow产品设计调研报告
> 调研范围:主流产品的「智能体工作流」设计,即工作流中的节点/步骤由 LLM 智能体自主执行(有工具调用、有推理循环),区别于传统确定性 BPM/ETL 工作流。
> 信息来源标注:**[官方文档]** = 官方文档/官方博客;**[官方博客]** = 官方博客;**[第三方]** = 社区/媒体/分析文章(标注具体来源)。
> 本报告的撰写基于对上述公开资料的归纳,反映的是各产品公开设计,非代码级验证。
---
## 0. 核心立论:工作流与智能体是"控制光谱"的两端
调研所有产品后发现一条贯穿主线:**所有"智能体工作流"产品本质上都在解决同一个问题——在"确定性流程控制"与"LLM 自主决策"之间找一个平衡点。** Anthropic 官方博客《Building Effective Agents》给出了最清晰的框架化表述**agentic systems 被分为两类——workflows工作流控制流由预设代码定义与 agents智能体控制流由模型根据环境反馈自主决定**。[官方博客]
这条光谱从"完全结构约束"到"完全模型自治"大致为:
**确定性画布 (n8n传统节点) → 结构+局部自主 (Dify 工作流/Coze/Flowise) → 自主协作 (CrewAI Crew) → 图式可控循环 (LangGraph) → 纯自主 (OpenAI Agent/Anthropic Agent)**
没有任何一款产品走极端,全部是"混合体"。下面的调研即为各产品如何具体落在这条光谱之上。
---
## 1. LangGraphLangChain
### A. 文件/数据格式
LangGraph 不依赖外部 DSL 文件表达图,而是用 **Python 代码 + 类型注解**直接构建。图的三个核心单元是 [官方文档]
- **State**:共享数据结构,用 `TypedDict` 或 Pydantic `BaseModel` 定义 schema是节点和边的输入输出契约。
- **Nodes**:普通 Python 函数,接收 state做计算或副作用返回更新后的部分state。`State -> Partial`。
- **Edges**:决定下一个执行哪个节点的路由函数,既有固定边也有条件边。
```python
class OverallState(TypedDict):
foo: str
messages: Annotated[list, add_messages] # reducer 决定如何合并
builder = StateGraph(OverallState, input_schema=InputState, output_schema=OutputState)
builder.add_node("node_a", node_a)
builder.add_edge("node_a", "node_b")
builder.add_conditional_edges("node_a", routing_fn, {True: "node_b", False: "node_c"})
graph = builder.compile(checkpointer=...)
```
图的**状态 schema 本质上是 JSON-serializable** 的类型契约(`TypedDict`/Pydantic每个 key 可带 **reducer 函数**(如 `operator.add`、`add_messages`)声明多个节点更新该 key 时如何合并。执行模型借鉴 Google Pregel 的 **super-step 消息传递**:每轮 super-step 里可并行的节点同批执行,节点间通过"channel 消息"激活。 [官方文档]
### B. 节点类型体系
LangGraph **没有预置的语义化节点类型**(没有"Dify 的 LLM 节点/代码节点"之分)。所有节点都是普通函数——"**nodes do the work, edges tell what to do next**" [官方文档]。LLM 调用、工具调用、条件判断、代码执行都是节点函数的内嵌逻辑。语言层面的抽象只有:
- 普通边 / 条件边 / 条件入口(`add_conditional_edges`
- `Send` 对象:实现 **map-reduce**,从一条条件边动态扇出多个并行子节点(数量运行时才知道)
- `Command(update=..., goto=...)`:节点内同时"更新状态 + 换路由"的统一原语
- 支持多 input/output/private schema 分离,节点可写私有 state channel
循环通过"条件边指回上游节点"表达;并行通过"一个节点多条出边即并行扇出"表达。
### C. 自主性与结构性约束的平衡
LangGraph 体现了最清晰的"**约束留白**"哲学:**边edge是开发者唯一强制约束的地方**(结构、路由、参数 schema**节点内部完全交给代码/LLM 自主执行**。一个节点函数里可以是单次 LLM 调用,也可以是完整的多轮 ReAct 循环。开发者可以"一个 agent 就是整个图(自循环)",也可以"把 agent 作为图中一个节点,用边把它的自主范围圈住"。LangGraph 官方把这种本地化自主称为 **"workflows + agents"** 的经典模式。 [官方文档]
### D. 验证与审核机制
- **human-in-the-loop**:核心机制是 `interrupt()` 函数,可在节点内任意代码位置动态暂停图执行,将任意 JSON 可序列化值抛给外部,等待 `Command(resume=...)` 注入继续。 [官方文档]
- 通过 checkpointer如 PostgresSaver/MemorySaver+ `thread_id` 持久化图状态暂停后可安全恢复time travel
- 没有内建的 "evaluator 节点" 语义,但 evaluator-optimizer 可以作为普通节点组合实现。
- 提供 **recursion limit**super-step 上限,默认 1000+ `RecursionLimit` 主动软降级机制,防止无限循环。
### E. 可视化与运行时
LangGraph **没有拖拽画布作为主要原语**,但编译后的图(`.compile()`)可用 LangGraph Studio 可视化。**编译是必要步骤**(必须有 `.compile()` 才能运行),编译做结构校验(孤儿节点等)+ 附加运行时参数checkpointer、断点。思维模型是"**代码即图定义**",运行时按编译后的图执行。 [官方文档]
### F. 代码/脚本复用
节点的本质就是 Python 函数,因此复用现有代码是**最直接的**——把任意函数 `add_node` 上去即可(封装为 `RunnableLambda`)。工具也用 Python 装饰器/函数定义。没有脚本沙箱(信任你的 Python 进程)。
---
## 2. Dify Workflow / Chatflow / Agent
### A. 文件/数据格式
Dify 对智能体工作流进行产品分级,并提供明确的三套模式 [官方文档 + 第三方]
- **Agent 模式**:纯自主,写 prompt、挂工具由模型自行决定工具调用顺序无画布路径
- **Workflow 模式**:可视化画布,**从头到尾运行一次**,无对话记忆,适合批处理/流水线。
- **Chatflow 模式**:同一套节点系统 + 对话层(每条消息触发流程,支持记忆、流式输出)。
工作流的存储格式是 **YAML/JSON DSL**`.dify.yml` 或 `.dify.json`),顶层含 `app`(元信息)、`workflow`features + graphgraph 由 `nodes[]` + `edges[]` 组成。DSL 保留节点的 **画布坐标 position**,可由代码直接生成和编辑(社区甚至有"用自然语言生成 Dify DSL"的工具)。 [第三方,基于 dify-workflow-skills]
节点内变量引用用 `{{#node_id.field_name#}}`DSL 内部用数组 `value_selector: ['node_id', 'field_name']` 表达同一条引用。变量类型严格化string/number/object/array/file/secret 等),有 `env`(环境变量)和 `sys` 系统变量节点。
### B. 节点类型体系
Dify 的工作流节点**语义化、预置、丰富**,是按"业务功能"而非"执行能力"分类的:
- **LLM & AI**`llm`(确定性单次调用)、`agent`(自主多轮推理 + 工具调用)、`parameter-extractor`、`question-classifier`
- **逻辑控制**`if-else`、`iteration`、`loop`(条件分支、循环、迭代)
- **数据处理**`code`python3/javascript、`template-transform`、`variable-aggregator`、`variable-assigner`、`list-operator`
- **外部集成**`http-request`、`tool`、`knowledge-retrieval`
- **输入输出**`start`、`end`、`answer`
- **人工审批**`human-input`(暂停等用户输入)
关键设计点:**`agent` 节点是"自主性"在结构化画布内的显式封装**——它像 LLM 节点一样挂在流程某处,但内部是带工具调用 + 多轮推理循环的自主执行。节点有**执行分类**EXECUTABLE逻辑节点、BRANCH条件路由如 if-else 的 true/false handle、CONTAINER迭代/循环子图,有内部 start/end 节点、RESPONSE、ROOT入口
### C. 平衡
Dify 的哲学是"**AI 在工作流定义的边界内运行**"。官方对工作流的定位:"与其依赖单个模型自行解决所有问题不如设计一个流程来逐步编排…AI 仍然承担繁重的工作,但在你定义的边界内运行。" [官方文档] 结构性约束体现在:**边/位置固定、变量类型严格、条件分支显式**;自主性体现在 LLM/agent 节点的 prompt 内。官方与社区都推崇"**workflow 当骨架agent 做关节**"的混合架构。 [官方文档 + 第三方 53AI]
### D. 验证与审核
- **错误处理三级**`fail-branch`(错误走并行分支,提供 error_message/error_type、`default-value`(错误用默认值继续)、`abort`(中止)、`retry`(带重试次数与间隔)。 [第三方基于官方源码]
- `human-input` 节点作为**人工审批点**,执行到它即进入 PAUSED 状态等待输入。
- 工作流执行状态含 PARTIAL_SUCCEEDED错误被处理则部分成功等细粒度状态。
### E. 可视化与运行时
Dify 前端用 **React Flow** 做画布DSL 便是画布状态的序列化nodes/edges/position。运行时后端是 **Python GraphEngine + WorkerPool线程池并行+ VariablePool共享变量+ EdgeProcessor条件路由**,前置 Graph Validator 做运行前完整性校验。DSL 与 UI 一一映射(节点→画布块、边→连线、分支用 true/false 标签)。这属于典型的"**编辑态结构数据 = 运行时执行计划**",运行时直接解释执行该结构,画布自由布局的 x/y 仅作视觉呈现,执行顺序由 **edges 连接关系**决定而非坐标。 [第三方基于官方源码]
### F. 复用
`code` 节点内置 python3/javascript 沙箱;`tool` 节点对接外部 APIagent 节点可挂载工具与内部工作流。
---
## 3. Coze / 扣子
### A. 文件/数据格式
Coze 工作流以**可视化画布**为主,支持**导入导出的 JSON DSL**`{"nodes":[...],"edges":[...],"variables":[...]}`)。工作流与对话流是两类:工作流预置 input 参数,对话流额外预置 `USER_INPUT`、`CONVERSATION_NAME` 会话参数(可互相转换)。 [官方文档]
### B. 节点类型体系
Coze 按官方文档的节点家族分为:基础节点(开始/结束)、**大模型节点**、**插件节点**(调用工具 API、**工作流节点**(工作流间嵌套调用)、业务逻辑节点(条件判断、循环、代码)、数据库节点、知识库节点、图像处理、会话/消息节点等。 [官方文档]
三个最核心的自主/执行节点:
- **开始节点**定义触发条件与输入参数String/Number/Boolean/Object/Array/File 等类型),是工作流入口。
- **大模型节点**:核心 AI 节点。可配置系统/用户提示词,**"添加技能"(插件/工作流/知识库)后它变身为近自主智能体**——官方明确说"大模型节点运行时,会根据用户提示词自动调用插件…能力更接近一个独立运行的智能体"。 [官方文档]
- **代码节点**:支持 Python/JS通过 `params = args.params` 接收上游变量,`return {key:value}` 定义输出参数。
- **循环节点**:数组循环/指定次数/无限循环三种模式;条件判断节点决定流转。
### C. 输入输出参数绑定
Coze 的一个特色是**节点输入输出参数显式定义**:大模型/代码/插件节点在配置面板明确声明输出参数(名称+描述+类型),下游节点通过 `{{变量}}``{{变量名.子变量名}}`/`{{变量名[索引]}}` 引用。参数类型需严格匹配(注释:类型不一致会报错)。大模型节点的输出参数有名称与描述,帮助模型正确生成结构化返回。 [官方文档 + 第三方]
### D/F (合并)
- 验证:大模型节点支持**异常忽略**(失败继续,用默认输出);代码节点默认最长执行 5 分钟(有超时约束);插件是能力扩展的载体。没有突出的原生 guardrail/evaluator 层(主要靠 prompt 与试运行调试)。
- 复用:**插件节点**是复用既有 API/工具的一等公民,代码节点复用脚本(沙箱)。
### E. 可视化
Coze 是纯拖拽画布产品,画布布局即工作流拓扑,运行直接按连线顺序解释执行。循环体是一类**容器式画布**(内部节点不可拖出,需在循环体内添加)。
---
## 4. OpenAI AgentKit / Agent Builder2025 发布)
### A/B. 定位与节点
2025-10-06 DevDay 发布 AgentKit核心是 **Agent Builder**beta一个拖拽可视化画布"像 agents 界的 Canva",直接用拖拽节点 + 连线构建多步骤、多智能体工作流。 [官方博客 + 第三方 MarkTechPost/Superprompt/CodeConductor]
节点类型据第三方整理为:**Agent节点**(由 GPT 模型驱动)、**工具节点**actions、**逻辑节点**(条件、循环)、以及 **Guardrails流入/流出筛查)** 与人工审批点。 [第三方 digitalapplied] 支持节点顺序/并行连接Responses API 驱动执行。配上 Connector Registry连接器注册中心、ChatKit嵌入聊天 UI、Evals痕迹打分、数据集、自动 prompt 优化)、强化微调,构成"构建-部署-优化"全栈。
### C/D. 自主性与审核
Agent 节点封装了模型自主决策Guardrails 提供输入/输出的安全筛查;支持 **preview runs 与 inline eval**(画布内联评估),以及完整 versioning。这正是"结构性约束画布、connector、guardrail+ 节点内自主agent 节点)"的落地。 [官方博客 + 第三方]
### E. 发布与嵌入
Agent Builder 的产物可通过 **ChatKit**(可嵌入聊天 UI**Agents SDK** 发布嵌入应用。第三方普遍指出其强项是**原型快速搭建**,弱项是生产级(部署、回滚、可观测、成本、状态管理)仍需外部工程层补齐。 [第三方 CodeConductor]
### C 备注
OpenAI 官方对 Agent Builder 的核心叙事是"**把编排层的 plumbing 接管**",把工作流从 Zapier/n8n 式 API 连接升级为"推理+工具+评估"的 agent 平台。
---
## 5. Anthropic《Building effective agents》workflow vs agent 判定标准)
这是本次调研的**理论基石**。其核心概念workflow 与 agent 的分野)准确刻画了"智能体工作流"产品的本质。 [官方博客]
### 五大 workflow 模式
1. **Prompt Chaining提示链**:任务分解为固定有序步骤,每步处理上一步输出;可加程序化 gate 检查环节是否偏离。适用于可干净分解为固定子任务、且用延迟换精度的场景。
2. **Routing路由**:先分类输入,再导向专门子任务/子提示词。用于类别差异大、分类可靠的任务。
3. **Parallelization并行化**LLM 同时对子任务工作程序化聚合输出两种变体——sectioning分片与 voting多attempt投票
4. **Orchestrator-Workers编排者-工人)**:中央 LLM 动态分解任务、分派给 worker LLM、合成结果。与并行化的关键区别是**子任务不是预定义**,而是由编排者依据具体输入**运行时决定**。
5. **Evaluator-Optimizer评估者-优化者)**:一个 LLM 生成、另一个提供评估反馈,构成循环。适合有清晰评估标准且迭代优化有可度量价值的场景。
### workflow 与 agent 的判定标准(核心)
- **Workflow**:通过**预定义代码编排 LLM 与工具调用**,开发者掌控控制流(控制路径)。
- **Agent**:让**模型从环境反馈中选择下一次工具调用**,开发者掌控的是目标与 guardrail而非每条分支。
### 何时优先选哪类
- 需可预测、一致性 → workflow需大规模灵活性 + 模型驱动决策 → agent。
- Workflow **优于** agent 的判定:**任务步骤可预测、有清晰成功标准、可程序化 gate**。反之 open-ended、步骤数不可预测、能硬编码固定路径时才应考虑 agent。
- 关键警告agent 有更高成本与"复合错误compounding errors"风险,应在沙箱环境充分测试 + 配 guardrail没有评估度量就添加复杂度是错误。
---
## 6. CrewAICrew + Flow 双层设计)
### A/B. 双层抽象
CrewAI 是 Python 开源框架,用**代码 + YAML 配置**表达(`agents.yaml`/`tasks.yaml`@CrewBase 装饰器连接 YAML 与代码是五类原语Agent角色/目标/背景/工具、Tool、Task、Process、Crew。 [第三方 Mastra + GitHub 官方]
其最具借鉴意义的设计是 **Crew自主协作与 Flow确定性流程并存的双层架构** [第三方 Mastra / firecrawl / GitHub]
- **Crew = 自主层**代理以角色扮演、自主协作、委派delegation适应开放、路径不可预知的 agentic 任务。Process 有 sequential顺序传参与 hierarchicalmanager 动态分工)两种。
- **Flow = 控制层****确定性、事件驱动的编排**,用装饰器(`@start`/`@listen`/`@router`)构建事件状态机,提供细粒度状态管理与可预测执行路径,满足可审计、可复现需求。
### C/D/F
- 平衡哲学(官方 & 社区一致强调):"多数生产部署**把 Crew 包在 Flow 里**——Flow 在必须受控的部分施加强结构Crew 在需要灵活的地方自主"。[第三方 Mastra]
- 验证Task 支持 human-in-the-loop完成前要求人工审核、结构化输出JSON/Pydantic、任务级约束校验output validation reject工具用 `@tool` 装饰器复用 Python 函数。
---
## 7. 补充确定性画布中嵌入自主智能体n8n / Flowise
n8n 的 **AI Agent 根节点**是"如何在确定性画布里嵌入自主体"的标杆做法 [官方文档 n8n + 第三方]
- **AI Agent 节点是一个"根节点",把 LLM 工具循环封装成一个单节点停在画布上**。接线连到该节点的下游节点HTTP Request、数据库、Code、Airtable、MCP server 等)自动成为它的"工具"。
- **模型在这个节点内部跑循环**:读上下文 → 决定是否/调用哪个工具 → 用工具输出选择下一步或产出最终答案。第三方明确:"agentic 的部分就是循环。" [第三方 christopheralarcon]
- **约束留白**:画布拓扑、连线(哪些能力可用)是开发者强制约束;节点内部每轮如何选工具是模型自主。
- n8n 的数据哲学是 item-per-item 逐项处理,可加 Loop/Execute Workflow 做批处理内循环。
Flowise 则是**把 LangChain 的组件模型搬上画布**:每个 chain/agent/vector store/memory/tool 都是节点,一 flow ≈ 一 agent 或 chain多 agent 通过 flow 接线组合。 [第三方 Jahanzaib.ai]
---
## 综合问题归纳A-G 对照)
**A. 文件/数据格式**
| 产品 | 格式 | 特征 |
|---|---|---|
| LangGraph | Python + TypedDict/Pydantic | 图 = 代码;状态 schema 即 JSON 契约 + reducer |
| Dify | YAML/JSON DSL `.dify.yml` | app/workflow/nodes[]/edges[];变量 `{{#node.field#}}`;含画布坐标 |
| Coze | 可视化 + JSON DSL导入导出 | 节点/边/变量声明;输入输出参数显式 |
| CrewAI | YAML + Python | agents.yaml/tasks.yaml + @CrewBase 绑定 |
| OpenAI Agent Builder | 云端可视化 + SDKResponses API | 画布产物经 ChatKit/Agents SDK 发布 |
| n8n | JSON 工作流定义 | 节点数组 + 连线AI Agent 为根节点 |
**B. 节点类型体系** 分两类设计流派:
- **语义化预置节点Dify/Coze**:区分"deterministic 节点"llm、code、if-else、http与"autonomy 节点"agent、大模型节点带技能条件用 if-else/question-classifier循环用 iteration/loop并行靠 DAG 扇出。
- **统一函数/图LangGraph**:不预置语义节点,结构化约束全部落到"边/状态 schema"这一层,节点内部任意。
**C. 自主与约束的平衡**:共同最佳实践是"**结构在边上、自主在节点内**"。三层约束次序:① **结构约束**(画布拓扑/边/参数 schema强制开发者可控→ ② **本地自主**(节点内 LLM 多轮循环+工具)→ ③ **软性/安全约束**guardrail/evaluator/human-in-loop/recursion limit。Anthropic 指出管理自主性的关键不是剥夺模型决策,而是"控制目标与 guardrail 而非每条分支"。
**D. 验证与审核**
- Human-in-loopLangGraph `interrupt()`、Dify `human-input`、CrewAI task 级 human review。
- Evaluator/GuardrailOpenAI Guardrails + inline EvalsAnthropic evaluator-optimizer 是流程级模式Dify fail-branch/retry 是错误处理LangGraph recursion limit 兜底。
- 共同点:**审核是"插入流程的停顿点/旁路节点"**,而非破坏流程的硬校验。
**E. 可视化 vs 运行时**主流Dify/Coze/n8n/OpenAI是"**编辑态结构数据 = 运行时执行计划**",画布 x/y 坐标仅作视觉,执行顺序由 edges 决定前端React Flow序列化 DSL → 后端(解释执行引擎,如 Dify GraphEngine/WorkerPool逐节点运行。LangGraph 走"代码定义图 → 编译 → 运行时 super-step 消息执行"。没有"编译成独立二进制"的做法,都是**解释执行结构数据**。
**F. 复用代码/脚本**Dify code 节点沙箱、Coze 代码节点、CrewAI/n8n 的 `@tool` 函数、LangGraph 节点即函数、n8n 把普通节点当 agent 工具。
---
## G. 对"复用现有智能体循环引擎 + 自然语言+结构文件定义流程 + 软性阶段审核"项目的 5 个建议决策
> 针对该项目(复用已有智能体循环引擎,用自然语言+结构化文件定义流程,软性阶段审核而非硬性步骤校验),以下是最值得借鉴的设计:
**✅ 建议决策 1双层抽象——"引擎自主"与"流程结构"分离。**
借鉴 CrewAI Crew/Flow 与 Dify "workflow 骨架 + agent 关节",把"复用现有循环引擎"封装成一个"自主节点",让它在流程文件的某一步骤内自由执行(本地多轮循环),而流程文件只约束"节点顺序、路由、进来的参数 schema、出去的产物契约"。结构在边上、自主在节点内。
**✅ 建议决策 2用"结构化文件YAML/JSON定义流程 + 自然语言填充节点行为"双通道表达。**
借鉴 Dify DSL 与 CrewAI YAML文件承载拓扑/参数/路由(可版本化、可评审、可 diff节点内部行为prompt、工具约束、评估标准用自然语言声明。这样"用中文写流程"即为合法编辑,天然支持本项目叙事。
**✅ 建议决策 3把关卡设计为"软性阶段审核点/旁路节点",而非硬校验。**
借鉴 LangGraph `interrupt()` 与 Dify human-input审核是一个"插入流程的暂停节点",只暴露当前阶段产物与决策参数,等待外部确认继续,而非在每个步骤做格式硬校验。这正好契合"软性阶段审核"的定位——审核即一个节点类型,可开可关。
**✅ 建议决策 4把"评估/护栏"做成可插拔的非阻塞节点。**
借鉴 Anthropic evaluator-optimizer 与 OpenAI guardrail+eval评估者作为独立 LLM 节点在阶段旁路运行,产出"是否通过/反馈"通过与否由审核节点消费而不是硬性阻断流程软性。设置最大循环数recursion limit 思想),防评估死循环。
**✅ 建议决策 5画布布局与执行逻辑解耦。**
借鉴 Dify/React Flow存储结构文件中的 position 仅作可视化布局,执行顺序完全由 edges/节点顺序决定。这样无需布局也能运行(纯结构文件 + 自然语言即能定义流程),画布只是可选的可视化层。
**❌ 要避免的 3 个坑**
**⚠️ 坑 1为"结构感"而过度预置节点类型,导致每一种思维都要新造一类节点。**
Dify 节点爆炸是反面教材——每加一个能力就多一类型。应让节点类型"最小化且正交"(流程结构节点 + 一个通用自主节点 + 一个审核节点),把大部分逻辑留给节点内 LLM/引擎,而非堆节点类型。
**⚠️ 坑 2软性审核做成"无值守地自动放行",退化为硬校验或完全形同虚设。**
不做"硬步骤校验"≠ 不做任何结构完整性校验。应做**运行前结构性验证**(节点 ID 唯一、边引用存在、有终点、无非法循环——借鉴 Dify Graph Validator而把"内容质量"交给软性阶段审核。结构与内容两类校验分开,缺一不可。
**⚠️ 坑 3忽视状态持久化与恢复。**
智能体循环被暂停在审核点后,若没有 checkpoint/thread 机制(借鉴 LangGraph checkpointer就无法恢复、无法时间回溯、故障无法续跑。一切 HITL 都依赖"暂停-恢复"这层底座,必须在流程引擎里一等地位,而不是事后补丁。
---
> 来源标注汇总LangGraph 部分主要依据 [官方文档 docs.langchain.com]Dify 部分依据 [官方文档 docs.dify.ai] 与基于官方开源的 [第三方 dify-workflow-skills/EvoMap/53AI]Coze 依据 [官方文档 docs.coze.cn] 与 [第三方]OpenAI AgentKit 依据 [官方博客 openai.com DevDay] 与 [第三方 MarkTechPost/digitalapplied/CodeConductor/Superprompt]Anthropic 依据 [官方博客]。CrewAI 依据 [官方 GitHub] 与 [第三方 Mastra/firecrawl]n8n 依据 [官方文档 docs.n8n.io] 与 [第三方]Flowise 依据 [第三方 Jahanzaib.ai]。第三方内容多基于官方源码/转述,个别细节(如 Dify 具体节点内部字段)以第三方整理为主,未做运行级验证。

View File

@ -1,280 +0,0 @@
# 工作流/流程图编排范式元素体系与「审核」建模方式调研报告
> 调研目的为「AI 智能体工作流编辑器(类 ComfyUI 拖拽画布、节点为 LLM 执行阶段)」的数据模型设计做前期调研,重点回答:**「审核」应建模为独立节点、节点的属性、还是边的属性?**
>
> 调研范围BPMN 2.0(权威标准)、国内审批流产品(钉钉/飞书/企业微信等、AI 工作流产品Dify/Coze/n8n/LangGraph、状态机理论XState
> 本报告基于公开文档与第三方文章整理,**所有结论标注了来源**;官方文档与第三方文章分别注明。
---
## 目录
1. [各范式的元素体系清单(表格)](#1-各范式的元素体系清单表格)
2. [审核/审批建模方式的产品对比(表格)](#2-审核审批建模方式的产品对比表格)
3. [三种审核建模方式的利弊分析](#3-三种审核建模方式的利弊分析)
4. [对「AI 智能体工作流」场景的建模建议](#4-对ai-智能体工作流场景的建模建议)
5. [来源 URL 汇总](#5-来源-url-汇总)
---
## 1. 各范式的元素体系清单(表格)
### 1.1 BPMN 2.0 元素体系(权威标准,重点)
BPMN 2.0 是 OMG 制定的标准ISO/IEC 19510元素分五大类**流程对象Flow Objects、数据Data、连接对象Connecting Objects、泳道Swimlanes、制品Artifacts**。以下按官方符号参考整理。
| 类别 | 元素 | 细分类型 | 语义要点 |
| --- | --- | --- | --- |
| **事件 Events** | Start / Intermediate / End按位置分 | 如图标所示再按类型分 | 事件是流程中"发生的事",是圆;分为 **catching等待触发****throwing主动发出** 两类 |
| | 事件类型(与位置组合) | None空白、Message消息、Timer定时、Conditional条件、Link链接、Signal信号、Error错误、Escalation升级、Termination终止、Compensation补偿、Cancel取消、Multiple多选、Multiple Parallel并行多选 | 消息=发给特定接收方;信号=广播式通知;定时=时间触发;错误=异常处理(只能做边界/结束);补偿=撤销已完成的活动的副作用 |
| | 边界事件Attached/Boundary | 中断式(实线)与非中断式(虚线) | 挂在活动边界上;中断式:事件触发即取消当前活动走异常流;非中断式:克隆 token 并行继续 |
| **活动 Activities** | Task任务原子活动 | **Undefined未定义/ Manual人工无系统辅助/ User用户任务有系统辅助/ Receive接收/ Send发送/ Script脚本/ Service服务/ Business Rule业务规则** | User Task 即"审批/表单填写"等需要人与系统交互的任务Camunda 8 官方文档user task = 需要人工完成、由工作流引擎/软件辅助的工作Service Task = 自动化服务调用Script Task = 执行内联/外部脚本 |
| | Subprocess子流程复合活动 | 内嵌 Subprocessembedded全局 Subprocess 通过 Call Activity调用活动粗边框引用 | 子流程内部有独立的起止事件;父流程 token 等待子流程完成。另有 **Event Subprocess**(虚线框,由事件触发,可中断/非中断)与 **Ad-hoc Subprocess**(波浪线标记,内部活动任意顺序/跳过) |
| | 标记Markers | Loop循环、Multiple Instance多实例可并行/串行、Compensation补偿 | 附着在任务/子流程上,扩展其执行语义 |
| **网关 Gateways** | 排他网关 ExclusiveXOR | — | **恰好走一条路径**(数据条件互斥时用);经典"是/否、通过/拒绝"决策 |
| | 并行网关 ParallelAND | — | **所有路径同时走**,合并时等待所有分支到达(漏掉 AND 合并会导致活动重复执行) |
| | 包容网关 InclusiveOR | — | **一条或多条**条件为真则都走,合并时等待所有激活分支 |
| | 基于事件网关 Event-based | — | 不按数据路由,而是**等待先发生的那个事件**(如收到消息 vs 计时器超时) |
| | 复杂网关 Complex | — | 复杂的组合条件规则ProcessMind 指南) |
| **流向/连接对象** | Sequence Flow顺序流 | — | 连接流程对象,表达执行顺序;**可在每条出边上写条件表达式**(如 Camunda/Flowable`#{approved}`、ProcessMaker`@@DocumentationReview == 'yes'`、`@#amount >= 1000`),条件为 true 的边被选中 |
| | Message Flow消息流 | — | 跨参与方(泳道/池)之间的消息传递 |
| | Association关联 | — | 把注释/数据对象关联到元素 |
| **泳道** | Pool、Lane泳道 | — | Pool=一个参与方/编排边界Lane=责任划分(角色/部门/人员) |
| **数据** | Data Object / Data Store / Input / Output | — | 建模数据的输入输出与持久化状态 |
| **制品** | Group分组、Annotation注释 | — | 辅助说明 |
> 关键认知:**网关不是任务**——它不做任何工作,只是根据已有数据/事件决定走哪条路Camunda 官方最佳实践:把决策问题放在网关前,各答案分别建模为一条出边)。
### 1.2 国内审批流产品的节点模型(钉钉/飞书/企业微信等)
| 产品 | 节点类型清单 | 审批是否独立节点 | 多人审批规则 | 特色操作 |
| --- | --- | --- | --- | --- |
| **钉钉宜搭**(官方帮助中心) | 发起人节点、**审批人节点**、抄送人节点、执行人(办理人)节点、条件分支节点、结束节点;节点上可配置"条件模式"(不同条件路由到不同审批人) | 是,审批人是独立一等节点 | 会签(全部同意)/ 或签(一人同意)/ 依次审批;或签时第一个提交结果者决定结果 | 审批按钮:**同意、拒绝、保存、转交、加签、退回、收回**;超时处理;审批人为空处理 |
| **飞书审批**(官方帮助中心) | 发起节点、**审批人节点**、抄送人节点、办理人节点、条件分支节点、结束节点 | 是,审批人是独立一等节点 | 会签 / 或签 / 依次审批 | 审批类型:人工审批 / **自动通过 / 自动拒绝**;审批人类型 11 种(上级、部门负责人、角色、用户组、指定成员、提交人自选、提交人本人、节点审批人、连续多级上级、表单内联系人、表单内部门);操作权限:允许转交、允许加/减签(前/后/并加签)、允许回退;审批人为空、审批人与发起人相同时的特殊处理;抄送:发起时/中间/结束时,支持"仅同意时抄送" |
| **企业微信审批**(官方开发者文档) | 节点类型 `node_type`1=审批人、2=抄送人、3=办理人;多人办理方式 `apv_rel`1=会签、2=或签、3=依次审批 | 是,节点类型字典中审批人是独立类型 | 会签 / 或签 / 依次审批 | 每个节点有状态 `sp_status`1 审批中、2 同意、3 驳回、4 转审、11 退回给指定审批人、12 加签、13 同意并加签、14 办理、15 转交——**「同意/驳回/退回」是节点级状态** |
| **明道云**(官方帮助) | 发起审批节点、审批节点、抄送节点、条件分支等 | 是 | 会签 / 或签 / 按通过比例 | 审批按钮:同意、拒绝、退回;转审;加签(通过后加签 / 审批前加签);退回范围:可退回到所有节点 / 仅上个节点 / 指定节点 / 发起节点;**退回流程不结束,驳回(拒绝)流程结束** |
| **简道云**(官方帮助) | 发起节点、审批节点、抄送节点、条件分支、结束节点 | 是 | — | 节点操作:保存草稿、流程回退、流程否决、流程暂存、流程加签、流程转交、批量处理;**回退**:可回退到「上一节点」或「指定范围内节点(多选)」;回退节点重新提交时可选:按流程顺序审批 / 直达当前节点 / 由回退人决定;**节点回退 ≠ 流程撤回** |
| **氚云**(官方帮助) | 发起(经办)节点、**审批节点**、抄送节点、办理(经办)节点、子流程、汇合点、连接线 | 是 | 会签 / 或签(可设同意比例、驳回人数) | **明确区分「退回」与「驳回」**:驳回=按节点流转规则(会签/或签)返回到前面节点或终止流程,审批人不可自由调节点;退回=由单个审批人**指定任意前序节点**退回 |
### 1.3 AI 工作流产品的节点类型体系
| 产品 | 节点/块类型(代表性) | 条件分支形态 | 人工审批HITL有无与建模方式 | 执行模型 |
| --- | --- | --- | --- | --- |
| **Dify**(官方产品页 + 第三方源码解析) | 开始(用户输入/定时触发/Webhook、结束、LLM、知识检索、问题分类器、条件分支、迭代、工具、代码执行、HTTP 请求、变量聚合器、模板转换、参数提取、直接回复、**人工介入** | **一等公民节点**IF_ELSE 节点,条件组 cases 判定后返回 `edge_source_handle` 决定走哪条出边) | **有,「人工介入」是独立节点**(官方:在敏感数据/权限/合规操作前暂停,由专人**审批、修改、评论、转交或超时处理**后再继续运行) | 图引擎:节点+边edge节点可多输出 |
| **Coze扣子**(官方知识库 + 第三方教程) | 开始、结束、大模型LLM、插件、代码、知识库、条件判断、循环数组/次数/无限、变量、数据库、信息、意图识别、子工作流、HTTP | **一等公民节点**条件判断节点IF-ELSEtrue/false 两出口) | **无专门审批节点**;人工介入通常靠「结束节点返回 + 对话式确认」或外部系统实现 | 可视化节点编排,节点输入输出引用(`{{node.output}}`)连接 |
| **n8n**(官方节点 + 第三方深度分析) | Trigger、AI Agent、LLM、IF / Switch / Merge、Wait等待、Form表单、Code / Function、HTTP Request、Sub-workflow、**Human in the LoopHuman Review**、Chat | **一等公民节点**IF/Switch 节点负责分支Merge 汇总) | **有,两层 HITL**:① workflow 级——Wait 节点暂停到指定时间/Webhook/表单到达;② **AI 工具调用级**——AI Agent 执行特定工具前要求人工批准(更接近 LangGraph interrupts。审批模式Human Review 节点approve-only 或 approve-and-disapprove+ 之后的 IF 节点按 true/false 分流 | 节点流(可并行、子工作流嵌套) |
| **LangGraph**(官方文档 + 第三方指南) | Node节点任意函数、StateGraph/edges、conditional edges条件边/路由器、checkpoint检查点持久化、**interrupt中断HITL**、Command动态续跑 | **条件分支是「边的一等属性」**`add_conditional_edges` 路由器,写代码定义,不是画布节点) | **有HITL 是第一公民**:在节点内调用 `interrupt()` 暂停并持久化状态,外部(人)输入后通过 `Command` 恢复;典型模式="get_approval 节点 + 条件边 router"(同意→继续,拒绝→取消/改稿) | 有向图(节点+边)+ 共享 State + checkpoint 持久化 |
> 小结:
> - **可视化工作流产品Dify/Coze/n8n条件分支是一等公民节点**;只有 LangGraph代码框架把条件分支放在边的层面conditional edges
> - **人工审批节点**Dify 有「人工介入」节点、n8n 有「Human Review」节点均为一等公民节点Coze 无专门审批节点LangGraph 用**节点内 interrupt暂停原语+ 条件边**实现,本质上是"审批作为节点内事件 + 条件边路由"。
> - 这些产品在这一点上高度一致:**"人工介入"需要有一个明确的载体(节点或节点内暂停点),因为要挂接审批人、表单、意见、暂停/恢复等多类状态**。
### 1.4 状态机视角XState / 状态机理论)
| 概念 | 定义 | 与"审核"的对应关系 |
| --- | --- | --- |
| **状态 State** | 系统在某一时刻所处的确定配置;状态机任一时刻**只能处于一个状态** | "待审核 / 审核中pending_approval"是独立状态 |
| **事件 Event** | 触发状态转换的信号(如 `APPROVE`、`REJECT`、`REVISE` | 审批人的"同意/拒绝/退回"动作 = 事件 |
| **转换 Transition** | 状态+事件 → 确定的下一个状态(**确定性**state+event 永远指向同一目标) | 审核通过 → 进入下一执行阶段;审核拒绝 → 进入终止/修改状态 |
| **守卫 Guard** | 转换前的条件(`cond` 谓词,为 true 才允许该转换发生) | "审批人是否具有权限""失败重试次数上限"等条件 |
| **动作 Action** | 转换发生时执行的副作用 | 发送通知、写入审批记录 |
| **扩展状态 Context / 层级状态 / 并行状态** | 上下文变量、嵌套状态、并行区域 | 审批意见/审批人/超时计时都放 context"执行中"可嵌套"等待审批"子状态 |
**「审核」在状态机里通常的表达**:将审核建模为**一个独立状态(等待审批/审核中)+ 事件驱动的转换**——`APPROVE → next`、`REJECT → cancelled/rework`、`REVISE → 回到某前置阶段`,必要时用 guard 控制转换合法性。它与工作流图是**同构**的:工作流的"节点+边"可无损映射为状态机的"状态+转换",节点间的条件边就是带 guard 的转换。
---
## 2. 审核/审批建模方式的产品对比(表格)
| 产品/范式 | 审核建模方式 | 通过路由 | 拒绝路由 | 回退/驳回语义 |
| --- | --- | --- | --- | --- |
| **BPMN 2.0Camunda/Flowable** | **User Task活动+ Exclusive Gateway网关** 的经典组合:`Submit for approval` → XOR 网关 → 出边各写条件 | XOR 网关的 "approved == true" 出边 → 下一活动ProcessMind/Gliffy/ProcessMaker 官方示例) | XOR 网关 "approved == false" 出边 → 结束事件或修正路径Gliffy 明确approval 后加 XOR 网关分 approved/rejected 两条互斥路径) | **原生 BPMN 无回退原语**走向先前节点需建模为显式序列流loop 回边);动态"退回任意节点"需引擎级处理Camunda 官方论坛:用 Process Instance Modification 的 cancelActivityInstance + startTransition 实现回退Flowable 官方论坛:同样要靠引擎 API/迁移实现,官方称"process instance migration API" |
| **钉钉宜搭**(官方) | **审批人节点 = 独立一等节点**;审批操作 = 节点内按钮(同意/拒绝/转交/加签/退回/收回) | 审批人点「同意」→ 节点流转到下一节点(条件模式可路由到不同审批人) | 点「拒绝」→ 节点状态为拒绝,流程按规则终止(或签时第一人拒绝即终止) | 「退回」按钮可退回指定前序节点;「转交」转给他人;加签分前/后加签;支撑驳回终止与回退重审 |
| **飞书审批**(官方) | **审批人节点 = 独立一等节点**;节点类型含"自动通过/自动拒绝" | 同意 → 下一节点/结束;可配"仅同意时抄送" | 拒绝 → 对应节点拒绝,流程按设计流转(通常是终止) | 操作权限勾选"允许回退"后,审批人可**回退到 1 个或多个前序节点**(所勾选节点均重新审批,重审完回到当前节点继续);支持转交、前/后/并加签 |
| **企业微信审批**(官方 API | **审批人节点 = 独立节点类型**node_type=1审批结果是**节点级状态**(同意/驳回/退回等 10+ 种 sp_status | 节点状态=同意 → 流转下一节点(或结束) | 节点状态=驳回 → 申请单状态=已驳回 | 状态机里显式建模11=退回给指定审批人、15=转交、12=加签、13=同意并加签——**回退是节点状态而非图边** |
| **明道云**(官方) | **审批节点 = 独立节点**;审批操作:同意/拒绝/退回/转审/加签 | 同意 → 下一节点 | 拒绝(否决)→ 整个审批流程结束,需重新发起新流程 | 退回 → 流程**不结束**,可退到:所有节点/仅上个节点/指定节点/发起节点;退回意见必填;被退回节点重新提交后按流程顺序或直达当前节点 |
| **简道云**(官方) | **审批节点 = 独立节点**;节点操作含"流程否决/流程回退" | 同意 → 按流程顺序流转 | 流程否决 → 流程终止 | 回退:可退「上一节点」或「指定范围内多个节点」;重提设置:按流程顺序审批 / 直达当前节点 / 由回退人确定;发起/结束节点不可配置回退 |
| **氚云**(官方) | **审批节点 = 独立节点**;功能按钮:同意、不同意、暂存、转交、退回 | 同意 → 下一节点 | 驳回=按**流转规则**(会签/或签、驳回人数)返回前面节点或终止,不可自由选节点 | 退回=由单个审批人**指定任意前序节点**(不受流转规则限制) |
| **Dify**(官方) | **「人工介入」= 独立节点**;节点负责收集审批意见、修改、评论、转交或超时处理 | 人工放行 → 该节点完成,沿出边继续下一个节点 | 节点内被驳回/修改后,由**条件分支节点**按结果路由(或按输出变量走不同出边) | 超时处理内建(官方列出的能力);"转交"内建;暂未见"退回任意节点"的图级原语,通常靠条件边回跳实现 |
| **n8n**(官方+社区) | **Human Review 节点(独立)**;配置 approve-only 或 approve-and-disapprove、超时 | 审批值 true → 后续 IF 节点 true 分支继续 | 审批值 false → IF 节点 false 分支do nothing / 通知 / 改稿) | Wait 节点暂停 + resume URL 恢复;无图级"回退任意节点",回退逻辑靠条件边回跳子工作流 |
| **LangGraph**(官方) | **节点内 `interrupt()` 暂停(半独立节点语义)+ 条件边路由**官方模式get_approval 节点 + router 条件边 | 条件边读取 resume 载荷approved → 继续执行 | rejected/revised → 条件边路由到取消或修改分支 | checkpoint 持久化使"任意点恢复/编辑状态"成为第一公民(可在中断点修改应用状态再续跑),最接近"任意节点回退" |
| **XState状态机** | **独立状态**(如 review/pending+ **事件驱动转换**APPROVE/REJECT/REVISE+ guard | `APPROVE` 事件 → 转换到下一阶段状态 | `REJECT` 事件 → 转换到终止/返工状态 | 回退 = 转换到任意前置状态只要该转换被显式定义guard 控制哪些回退合法 |
**提炼共识**
1. **审批几乎总是被建模为"独立节点/独立状态"**(唯一例外是把审核当普通活动属性的场景,未见主流产品这么做)。
2. **通过/拒绝的路由表达**分两派:① 节点多个出口/节点状态值 + 下游条件分支国内审批产品、n8n IF、LangGraph 条件边);② 显式排他网关 + 带条件出边BPMN 正统,与 ① 等价)。
3. **回退(驳回退回)是超图语义**:主流做法是把"退回目标"作为审批节点的**内建操作/节点属性**(可退回发起人/上一节点/任意指定前序节点/多个节点),由引擎在执行期计算目标节点,而不是在画布上画一条固定的回边——因为退回目标是**运行期动态选择**的。
4. **驳回reject与退回rollback是两回事**:驳回=终止或按规则回退、流程可能结束;退回=流程不结束、退到指定节点重审后继续(明道云、氚云、简道云、飞书均如此区分)。
---
## 3. 三种审核建模方式的利弊分析
核心问题:**「审核」应建模为独立节点、节点的属性、还是边的属性?**
### 方案 A独立节点审核把关节点 / Review Node / User Task / Human Review
| 维度 | 评价 |
| --- | --- |
| 可视化清晰度 | **★★★ 最高**。审核点在画布上一眼可见,阶段间"人机交接"的位置明确;审核人、意见、按钮(同意/拒绝/退回)都直观挂在一个节点上 |
| 数据模型简洁性 | **★★☆ 中等**。需要引入一个节点子类型(或节点 kind=review节点 schema 需含审批人配置、审批表单、超时、多出口定义;但**审核自身的复杂度被封装在节点内部**主图模型保持统一node + edge |
| 表达力 | **★★★ 最高**。通过→正常出边;拒绝→终止/改稿出边;退回→节点内建操作(退回目标可运行期计算);可附加抄送、会签/或签、超时自动通过等复杂规则而不污染图结构 |
| 执行引擎复杂度 | **★★☆ 中等**。引擎需要支持"暂停-等待-恢复"HITL 暂停原语)与多出口路由;但由于暂停语义集中在一个节点类型里,实现可控 |
| 业界采用 | **主流**:钉钉/飞书/企业微信/明道云/简道云/氚云审批节点、Dify人工介入节点、n8nHuman Review 节点、BPMNUser Task + 网关、LangGraphinterrupt 节点内暂停 + 条件边) |
### 方案 B节点的属性审核配置挂在某个"阶段/任务"节点上)
| 维度 | 评价 |
| --- | --- |
| 可视化清晰度 | **★☆ 最低**。审核藏在属性面板里,画布上看不出"这个阶段有人工把关";阶段多时难以区分哪些要审、哪些不要 |
| 数据模型简洁性 | **★★★ 最高**。不新增节点类型,阶段节点上多一个 `needsApproval` 配置即可 |
| 表达力 | **★★☆ 中**。单点审核勉强可行;但"审核通过后走分支A、拒绝走分支B"这类路由仍需要额外条件节点/条件边;多个审核结果(通过/拒绝/退回不同目标)表达复杂;"阶段间审核"变成"阶段内嵌审核",颗粒度不符合"阶段间把关"的诉求 |
| 执行引擎复杂度 | **★★☆ 中**。暂停语义仍在(任何节点都可能要暂停),但引擎要为"几乎所有节点"支持 HITL 挂起,暂停点分散,审计/追踪困难 |
| 业界采用 | **极少作为主方案**;仅在轻量场景(如一条消息内审)出现;主流产品都选择独立节点以换取可视化与表达能力 |
### 方案 C边的属性通过/拒绝 = 出边上的条件)
| 维度 | 评价 |
| --- | --- |
| 可视化清晰度 | **★★☆ 中**。路由本身画得清楚(一条边标 approved、一条标 rejected等价于 BPMN 网关的出边条件),但"审核"这个**活动本身**(谁审、暂停、意见、超时)没有载体,审核过程不可见 |
| 数据模型简洁性 | **★★☆ 中**。边需要挂条件表达式;"审核"仍需要一个执行体(否则谁产生 approved 变量?),通常退化为"某个节点输出审核结果变量 + 条件边路由"(即 LangGraph 模式) |
| 表达力 | **★★☆ 中**。对"审核结果路由"表达很好(条件边天然支持);但对"审核的交互语义"(审批人指派、表单、意见、退回动态目标、抄送)几乎无法承载 |
| 执行引擎复杂度 | **★★★ 最低(仅就路由而言)**。无需暂停原语的话就是普通条件跳转;但**只要有真实人工介入,就必须在某处暂停**——那时其实又建了一个"伪节点"LangGraph 的 interrupt 正是如此:暂停点在节点内,路由在边上) |
| 业界采用 | **作为路由机制广泛使用**BPMN 序列流条件、LangGraph conditional edges、Dify 的 edge_source_handle但**从不单独承载"审核"语义**——都是"审核节点/事件 + 条件边"组合 |
### 综合对比表
| 评判维度 | 方案A 独立节点 | 方案B 节点属性 | 方案C 边的属性 |
| --- | --- | --- | --- |
| 可视化清晰度 | ★★★ | ★☆ | ★★(仅路由) |
| 数据模型简洁性 | ★★☆ | ★★★ | ★★☆ |
| 表达力(审核结果路由) | ★★★ | ★★ | ★★★(路由部分) |
| 表达力(审核交互:人/表单/意见/超时/抄送) | ★★★ | ★★ | ★☆ |
| 回退/驳回动态目标表达 | ★★★(节点内建操作) | ★☆ | ★☆ |
| 执行引擎复杂度 | ★★☆(暂停点集中、可控) | ★★☆(暂停点分散) | ★★★(路由简单,但真实暂停仍需节点载体) |
| 业界主流 | ✅ 主流 | ❌ 罕见 | 部分仅作为路由层与A组合使用 |
**结论(对核心问题的回答)**
> **「审核」应当建模为独立节点(一等公民),并采用"节点多出口/节点输出决策变量 + 条件边路由"的组合式建模**——即方案 A 为主体、C 作为路由机制配合(等价于 BPMN 的 User Task + Exclusive Gateway 经典模式)。
>
> 理由:
> 1. **审核同时是"活动"与"决策点"**:它是需要暂停、需要人做事、需要挂表单/意见/指派的活动,同时它的结果决定后续路由。独立节点能同时承载这两个属性;边只能承载路由、属性只能承载配置,二者都承担不了"暂停等人"的交互语义。
> 2. **业界共识**:从最严谨的 BPMNUser Task + XOR到国内审批产品审批人节点到 AI 工作流产品Dify 人工介入、n8n Human Review、LangGraph interrupt+条件边),全部采用"审核有独立载体(节点/状态)+ 结果驱动路由"的同一模式。这是被检验过的工程共识。
> 3. **回退/驳回是超图语义**,必须放在节点内建操作或引擎能力里(退回到发起人/上一节点/任意前序节点是运行期动态选择),无法用静态边表达。
---
## 4. 对「AI 智能体工作流」场景的建模建议
场景画像:类似 ComfyUI 的拖拽画布,节点是 **LLM 执行阶段**Agent 自主执行),需要在**阶段之间插入人工审核把关**(阶段产物需人工确认后才能进入下一阶段)。以下为综合上述调研的建模建议。
### 4.1 总体建议:审核把关节点作为一等公民节点
```
[开始] → [阶段A(LLM/Agent)] → [审核把关节点] ──通过──→ [阶段B(LLM/Agent)] → [结束]
├──拒绝/终止──→ [结束(失败)]
└──退回──→ [指定前序阶段重跑](节点内建操作,运行期计算目标)
```
- **审核把关节点Review Gate Node**:独立节点类型,语义 = "暂停执行,等待人工审核,输出审核结论"。字段建议:`approvers`(审批人/角色)、`review_form`(审核表单:展示的阶段产物 + 意见输入)、`decision_outputs`通过的决策变量approved/rejected/revised、`timeout_policy`(超时:提醒/自动通过/自动拒绝)、`escalation`(转交)、`notify`(抄送,可关"仅通过时通知")、`allow_rollback`(退回目标范围:发起阶段/上一阶段/任意前序阶段/多节点)。
- **通过/拒绝/退回的出口表达**:审核节点定义 **2~3 个命名出端口**pass / reject / revise或输出 `decision` 变量 + 下游条件边(推荐两者都支持:默认出端口直连,复杂分支交给条件节点/条件边,对齐 Dify 的 `edge_source_handle` 与 LangGraph 的条件边)。
- **回退语义**:放在审核节点的内建操作("退回"按钮 + 可选目标列表),由引擎执行"取消当前分支 token → 在目标阶段创建重跑 token",借鉴 Camunda Process Instance ModificationcancelActivityInstance + startTransition与飞书/简道云"可退到多个指定前序节点"的做法。退回重跑后的再提交语义参考简道云两种模式:**按流程顺序重走 / 直达当前审核节点**。
- **与条件分支节点的关系**条件分支节点保留为通用路由节点AI 自主判断的路由用 if/else 或 LLM 判断);审核把关节点**不是条件分支的替代品**——它是"暂停+人工",条件分支是"即时+规则/模型"。两者分工明确。
### 4.2 数据模型要点(供后续设计参考)
| 实体 | 建议 | 依据 |
| --- | --- | --- |
| 图模型 | `Graph = { nodes[], edges[ {source, sourceHandle, target, condition?} ] }`边可带条件表达式BPMN sequence flow 条件、Dify edge_source_handle | BPMN / Dify / LangGraph |
| 节点基类 | `node = { id, type, name, inputs, outputs }`type 枚举含 `llm_stage / agent_stage / review_gate / condition / code / tool / start / end` | Dify / Coze / n8n 节点体系 |
| 审核节点 | 作为 `review_gate` 独立子类型;**务必建模"暂停-恢复"状态**pending_review → approved/rejected/rolled_back与状态机视角一致 | XState独立状态+事件转换)、企业微信节点状态字典 |
| 条件分支 | 推荐作为**一等公民节点**(对齐 Dify/Coze/n8n 的用户习惯);同时**允许边带条件**(对齐 LangGraph 与 BPMN作为高级能力 | Dify/Coze/n8n/LangGraph 对比 |
| 执行引擎 | 需要三类原语:① 节点执行LLM/工具/代码);② **暂停/等待人工interrupt/resume参考 LangGraph checkpoint + interrupt**;③ 分支路由(条件边/多出口)。回退 = 引擎 API参考 Camunda Process Instance Modification | LangGraph / Camunda |
| 运行状态 | 每次执行为 DAG 的一次遍历:记录每个节点的输入/输出/决策句柄;审核节点额外记录审批人、意见、时间戳(审计) | Dify WorkflowNodeExecutionModel |
### 4.3 为什么这个建议适合"AI 自主执行 + 阶段间审核"场景
- **AI 阶段是"黑箱自动化"**,阶段间审核是把控点,必须**显式、可见**——独立节点让"人在回路的位置"一目了然,符合 Dify 官方"在敏感操作前暂停、关键判断交给人"的实践。
- **暂停必须有一等公民支持**AI 阶段时长不定(一次 LLM 调用秒级,但一个 agent 阶段可能分钟级),真实人工审核更可能数小时——需要 checkpoint 持久化 + 恢复机制LangGraph 已验证该模式interrupt + Command + checkpoint
- **审核结果驱动路由**:通过→下一阶段、拒绝→终止或改稿、退回→指定前序阶段重跑——用"多出口 + 条件边"表达,与 LangGraph 的 get_approval + router、n8n 的 Human Review + IF 完全同构,参考实现成本最低。
- **避免过度设计**:不建议"节点的属性"方案(审核不可见、暂停点分散);不建议纯"边的属性"方案(没有载体承载审批交互与回退语义)。**独立节点 + 条件边路由**是三方案中可视化、表达力与实现复杂度平衡最优的。
### 4.4 建议的最小节点清单(供数据模型落地)
| 节点类型 | 用途 | 对应参考 |
| --- | --- | --- |
| Start / End | 输入与输出 | Dify、Coze、BPMN 事件 |
| LLM / Agent 阶段节点(可含工具调用) | AI 自主执行阶段(可 loop/多实例) | Dify LLM/Agent、Coze 大模型、n8n AI Agent、LangGraph node |
| **审核把关节点Review Gate** | 阶段间人工审核多出口pass/reject/revise内建退回/转交/超时 | BPMN User Task、钉钉/飞书/企微审批节点、Dify 人工介入、n8n Human Review、LangGraph interrupt |
| 条件分支节点if/else | AI 或规则驱动的路由 | Dify/Coze/n8n |
| 代码/工具/HTTP 节点 | 通用能力 | Dify、Coze、n8n、BPMN Service/Script Task |
| 变量聚合/模板(可选) | 数据整理 | Dify、Coze |
---
## 5. 来源 URL 汇总
### 官方文档(权威来源)
- Camunda — BPMN 2.0 Symbols Reference官方符号大全事件/活动/网关/流向语义https://camunda.com/bpmn/reference
- Camunda 8 Docs — User Tasks用户任务语义、监听、Job worker 实现https://docs.camunda.io/docs/components/modeler/bpmn/user-tasks
- Camunda — BPMN TasksTask 类型参考https://camunda.com/bpmn/reference/#activities (活动章节)
- Camunda Blog — Events: Basic Conceptscatching/throwing、边界事件、非中断事件https://camunda.com/bpmn/reference/#events
- Camunda Forum — "Go back to previous user task"(官方论坛:回退需 Process Instance Modification 的 cancelActivityInstance + startTransitionhttps://forum.camunda.io/t/go-back-to-previous-user-task/29485
- Flowable Forum — "How to set the completed task to active to achieve the rollback"官方论坛Flowable 无原生回退,靠引擎 API/迁移https://forum.flowable.org/t/how-to-set-the-completed-task-to-active-to-achieve-the-rollback-in-flowable/7335
- 钉钉宜搭帮助中心 — 审批人节点(节点类型、审批按钮、会签/或签/依次、条件模式https://docs.aliwork.com/docs/yida_support/_2/trbqg6/rq8i94
- 飞书审批帮助中心 — 管理员设计审批流程(节点类型、审批人 11 类、会签/或签/依次https://www.feishu.cn/hc/zh-CN/articles/360036163653-管理员设计审批流程
- 飞书审批帮助中心 — 管理员设置转交、加减签、回退审批(回退/加签语义https://www.feishu.cn/hc/zh-CN/articles/360049067381-管理员设置转交、加减签、回退审批
- 飞书审批帮助中心 — 管理员设置抄送人抄送节点、仅同意时抄送https://www.feishu.cn/hc/zh-CN/articles/360041749473-管理员设置抄送人
- 企业微信开发者中心 — 获取审批申请详情node_type / sp_status 节点状态字典:同意/驳回/退回给指定审批人/加签/转交https://developer.work.weixin.qq.com/document/path/92634
- 企业微信开发者中心 — 审批申请状态变化回调通知https://developer.work.weixin.qq.com/document/path/96508
- 明道云帮助中心 — 发起审批流程-审批节点(同意/拒绝/退回、转审、加签、退回范围https://help.mingdao.com/workflow/node-approve
- 简道云帮助中心 — 流程回退(上一节点/指定范围内节点、重提设置、节点回退 vs 流程撤回https://hc.jiandaoyun.com/doc/12524
- 氚云帮助中心 — 流程节点和连接线(审批/经办/抄送/子流程/汇合点退回与驳回区别加签https://help.h3yun.com/contents/818/2482.html
- Dify 官方产品页 — Workflow Studio节点列表含「人工介入」审批、修改、评论、转交、超时处理https://dify.ai/zh/workflows
- LangGraph 官方文档 — Interrupts暂停/持久化/Command 恢复/条件边校验模式https://docs.langchain.com/oss/python/langgraph/interrupts
- LangChain Blog — "Making it easier to build human-in-the-loop agents with interrupt"https://www.langchain.com/blog/making-it-easier-to-build-human-in-the-loop-agents-with-interrupt
- StatelyXState 官方) — Events and transitions状态/事件/转换确定性https://stately.ai/docs/transitions
### 第三方文章/教程(补充说明,标记为第三方)
- Dokuflex — BPMN 2.0 guide四大元素族、网关语义https://www.dokuflex.com/en/resources/bpmn-guide.html (第三方)
- Lucid — BPMN Tutorial and Templates五大类元素https://lucid.co/diagram/bpmn/tutorial (第三方)
- ProcessMaker Wiki — GatewaysXOR 网关条件表达式示例 `@@DocumentationReview`https://wiki.processmaker.com/3.1/Gateways (第三方)
- ProcessMind — BPMN Gateway Types & Usage Guide审批示例approved 继续 / rejected 结束https://processmind.com/resources/docs/bpmn-building-blocks/gateways (第三方)
- Gliffy — How to Read and Use BPMN Gateways提交审批 + XOR 网关 approved/rejected 两互斥路径https://www.gliffy.com/blog/bpmn-gateways (第三方)
- BA Copilot — Gateway (BPMN)XOR 用于 yes/no、approved/declined 分支https://ba-copilot.com/glossary/gateway-bpmn (第三方)
- eduMAX — Exclusive vs Parallel vs Inclusive Gateways路径数对比表https://www.edumax.pro/blog/exclusive-vs-parallel-vs-inclusive-gateway-in-bpmn-whats-the-difference (第三方)
- MediumSebastian Lesser— BPMN Gateways Explainedhttps://medium.com/@sebastian.lesser/bpmn-gateways-explained-2fbea236b0fa (第三方)
- ProcessCamp — BPMN Elements Reference元素分类计数https://processcamp.io/bpmn-elements (第三方)
- Beyond Engineering — BPMN 2.0 Elementshttp://www.beyondengineering.io/bpmn-elements (第三方)
- 火山引擎开发者社区 — 从零开始学 Dify 工作流实现机制(节点类型/IF_ELSE 实现/edge_source_handlehttps://developer.volcengine.com/articles/7538284296277229609 (第三方)
- Dify 官网博客转述 — Dify Workflow 重磅上线核心节点LLM/工具/意图分类器/知识检索/代码/If-Elsehttps://www.53ai.com/news/dify/1866.html (第三方转载)
- 阿里云开发者社区 — 使用 Dify 创建 AI 应用并详解节点IF/ELIF/ELSE 条件:包含/开始是/为空等https://developer.aliyun.com/article/1589591 (第三方)
- 博客园 — Coze 智能体之工作流节点(开始/结束/大模型/插件/代码/条件分支/循环 节点表https://www.cnblogs.com/fuminer/p/19377669 (第三方)
- 掘金 — Coze 工作流与触发器(大模型节点配置、节点输入输出引用、工作流/对话流差异https://juejin.cn/post/7517107495392575514 (第三方)
- SkillHub腾讯云— Coze 节点清单LLM/代码/知识库/插件/条件/循环/变量/HTTPhttps://skillhub.cloud.tencent.com/skills/coze (第三方)
- REBUILD 帮助文档 — 审批流程(发起人/条件分支/审批人/抄送人节点、加签/转审/会签或签/限时/自由审批https://getrebuild.com/docs/admin/approval (第三方)
- FlyFlow 更新日志(节点新增:投票节点/办理节点等佐证国内审批流节点体系https://www.flyflow.cc/upgrade (第三方)
- ZenML Blog — LangGraph vs n8nn8n 两层 HITLWait 节点 + AI 工具调用级批准LangGraph interruptshttps://www.zenml.io/blog/langgraph-vs-n8n (第三方)
- Peliqan — LangGraph vs n8nHITL 对比、checkpointinghttps://peliqan.io/blog/langgraph-vs-n8n (第三方)
- LOW/CODE — n8n vs LangGraphn8n 节点/400+ 集成/HITL 暂停审批https://www.lowcode.agency/blog/n8n-vs-langgraph (第三方)
- TopsInfoSolutions — n8n vs LangGraphLangGraph checkpoint、HITL 暂停审批恢复https://www.topsinfosolutions.com/blog/n8n-vs-langgraph (第三方)
- jimmysong.io — Open Source AI Agent Platform ComparisonDify/Coze/n8n/LangGraph 平台对比https://jimmysong.io/blog/open-source-ai-agent-workflow-comparison (第三方)
- CustomJS — n8n Human-in-the-LoopWait 节点/webhook 恢复、审批表单局限https://www.customjs.space/blog/n8n-human-in-the-loop (第三方)
- GrowwStacks — Human in the Loop in n8n GuideHuman Review 节点approve-only / approve-and-disapprove、超时https://growwstacks.com/blog/human-in-the-loop-n8n-guide (第三方)
- nocodecreative.io — n8n Wait Node v2Wait 节点 + 子工作流 HITLhttps://blog.nocodecreative.io/n8n-v2-wait-node-hitl-sub-workflows (第三方)
- DEV Community — LangGraph Interrupts and Commandsget_approval 节点 + router 条件边示例https://dev.to/jamesbmour/interrupts-and-commands-in-langgraph-building-human-in-the-loop-workflows-4ngl (第三方)
- Future AGI — What is LangGraphStateGraph/节点/边/条件边/checkpoint/interrupthttps://futureagi.com/blog/what-is-langgraph-2026 (第三方)
- sdust.dev — XState 101状态/转换/事件/守卫https://sdust.dev/posts/2023-04-12_xstate-101-quick-introduction-to-finite-state-machine (第三方)
- egghead — State Transitions through Events状态机确定性转换https://egghead.io/lessons/javascript-handle-state-transitions-through-events-in-a-finite-state-machine-with-xstate (第三方)
- DEV Community — State machine advent: Guardguard 守卫语义https://dev.to/codingdive/state-machine-advent-guard-state-transitions-guard-actions-14-24-oc3 (第三方)
---
### 附:本报告的确定性说明
- **百分百确认**BPMN 元素分类与网关语义、各产品官方文档中列出的节点类型与审批按钮/状态字典、Dify/n8n/LangGraph 官方 HITL 能力描述——均直接出自官方文档原文。
- **很大概率**:对"业界主流选择是独立节点+条件路由"的归纳——基于上述 12+ 个产品的官方/权威资料的一致性得出;"审批应建模为独立节点"为综合分析结论,属合理推断而非任何官方标准的规定。
- **可能**"回退目标作为节点内建操作、由引擎在执行期计算"的工程建议——综合 Camunda/Flowable 官方论坛做法与国内审批产品的一致设计推断。
调研日期2026-08-20未修改项目任何代码文件。