Compare commits
No commits in common. "9602875391cb17138d3c30909c46b43e9941a795" and "c908e8484490aac6cf017af9cd56a323d5f5f007" have entirely different histories.
9602875391
...
c908e84844
13
.gitignore
vendored
13
.gitignore
vendored
@ -40,8 +40,10 @@ skills/
|
||||
# 本地实验残留与历史文档归档(不进仓库)
|
||||
_experiments/
|
||||
|
||||
# Ignore docs
|
||||
# Ignore docs(doc/ 与 docs/ 均为本地文档,不纳入版本控制)
|
||||
doc/
|
||||
docs/
|
||||
workflow_research/
|
||||
|
||||
# 部署级配置(含密钥引用 / 机器特定,本地保留,仓库仅留 .example 种子)
|
||||
config/host_workspaces.json
|
||||
@ -73,15 +75,6 @@ static/old_version_backup/
|
||||
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/
|
||||
|
||||
# 含私人信息 / 个人工作流约定(脱敏前不进仓库)
|
||||
|
||||
25
AGENTS.md
25
AGENTS.md
@ -13,7 +13,7 @@
|
||||
- **构词**:`Astr-`(希腊语 *astron*,星星/天体)+ `-ion`(粒子/状态后缀,如 photon、electron)。
|
||||
- **意象**:星之子、星际粒子、来自宇宙尘埃的微小发光体。
|
||||
- **寓意**:Agent 像星际间传递信号的粒子,把人类意图传递到工具、文件、终端与子智能体之间;同时把零散任务像星尘聚合成恒星一样,聚合成可执行结果。
|
||||
- **状态**:暂定名,后续若变更需同步更新 `AGENTS.md`、`claude.md` 与项目记忆 `astrion_naming`。
|
||||
- **状态**:暂定名,后续若变更需同步更新 `AGENTS.md`(及本地 `claude.md`,未随仓库发布)。
|
||||
- **中文参考**:阿斯特里恩;简称可考虑 **星子**。
|
||||
|
||||
## 1) 当前项目结构(按代码现状)
|
||||
@ -35,7 +35,7 @@
|
||||
- `static/src/components/chat/monitor/MonitorDirector.ts`
|
||||
- `static/src/stores/monitor.ts`
|
||||
- `static/src/components/chat/monitor/*`
|
||||
- **CLI 相关文档**
|
||||
- **CLI 相关文档**(本地文档,未随仓库发布)
|
||||
- `docs/cli_ui_display_spec.md`: CLI 显示层设计说明
|
||||
- `docs/cli_slash_commands_spec.md`: CLI `/` 指令设计说明
|
||||
- **其他子项目/资源**
|
||||
@ -110,11 +110,6 @@
|
||||
- 兼容启动方式:`python web_server.py`
|
||||
- 统一入口:`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(根目录)
|
||||
- 安装依赖:`npm install`
|
||||
- 构建:`npm run build --silent 2>&1 | tail -n 5`(默认只看最后 5 行,减少 Vite 构建资源列表等无关输出)
|
||||
@ -199,7 +194,7 @@
|
||||
7. **带文字容器固定高度**:所有含内部文字的选项 / 按钮 / 标签 / 容器固定高度,禁止因文字多少被撑大撑高;过长文字用省略号或内部滚动处理。
|
||||
8. **窗口设最大尺寸 + 内部滚动**:所有弹窗 / 面板 / 列表设置 `max-height` / `max-width`,超出由内部容器滚动,不顶大整个窗口;滚动条要么隐藏,要么做样式适配,不暴露原生粗滚动条。
|
||||
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` 头部)。纯黑/近黑只允许用于小范围强调渲染(如行内代码衬底等小面积场景)。
|
||||
|
||||
## 5.6) 工具结果显示规范(强制)
|
||||
@ -290,14 +285,14 @@ AI 执行以下流程时,每一步都要向用户说明在做什么:
|
||||
|
||||
## 8) Android App 发布联动要求(重要)
|
||||
|
||||
当修改前端并需要发布 Android WebView App 时,必须同步执行以下步骤(依据 `doc/android_app_release_and_update.md`):
|
||||
当修改前端并需要发布 Android WebView App 时,必须同步执行以下步骤(依据 `doc/android_app_release_and_update.md`,本地文档未随仓库发布):
|
||||
|
||||
1. 同时更新版本信息
|
||||
- `android-webview-app/app/build.gradle.kts`:递增 `versionCode`,更新 `versionName`
|
||||
2. 同时更新更新说明
|
||||
- `android-webview-app/APP_CHANGELOG.md`:在顶部新增当前版本说明
|
||||
3. 完成修改后提醒用户运行上传脚本
|
||||
- 在发布流程中运行:`bash /Users/jojo/Desktop/agents/正在修复中/agents/upload_android_apk.sh`
|
||||
- 在发布流程中运行:`bash ./upload_android_apk.sh`(本地私有脚本,未随仓库发布)
|
||||
|
||||
> 禁止只改前端代码而不更新版本号/更新说明,否则会导致客户端更新提示与分发信息不一致。
|
||||
|
||||
@ -313,7 +308,7 @@ AI 执行以下流程时,每一步都要向用户说明在做什么:
|
||||
|
||||
## 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)
|
||||
|
||||
> **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 前端对话类型模型(重构后)
|
||||
|
||||
@ -429,7 +424,7 @@ AI 执行以下流程时,每一步都要向用户说明在做什么:
|
||||
- 输出端(`SubAgentTask._forward_output_to_master`):子智能体每次 assistant 文本输出都封为标准格式消息并 `push_master_message` 到会话的 `MultiAgentState.pending_master_messages`,不再做事。
|
||||
- 接收端(`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`)。
|
||||
- **情况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)。
|
||||
|
||||
**情况2 硬约束(调试踩过坑)**:
|
||||
@ -517,13 +512,13 @@ AI 执行以下流程时,每一步都要向用户说明在做什么:
|
||||
|
||||
---
|
||||
|
||||
注:本节按「现有架构 + 多智能体分支」方案描述,与项目记忆 `multi_agent_mode_design` 同步;如两者冲突以代码为准并同步修订本节。
|
||||
注:本节按「现有架构 + 多智能体分支」方案描述;如与代码冲突,以代码为准并同步修订本节。
|
||||
|
||||
---
|
||||
|
||||
## 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 单写者不变量(核心约束)
|
||||
|
||||
|
||||
14
README.md
14
README.md
@ -46,7 +46,7 @@
|
||||
|
||||
```bash
|
||||
# 1. 克隆
|
||||
git clone <仓库地址>
|
||||
git clone https://github.com/JOJO6618/astrion.git
|
||||
cd astrion
|
||||
|
||||
# 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 监听 |
|
||||
| `admin.username` | `AGENT_ADMIN_USERNAME` | 管理员用户名 |
|
||||
| `admin.password_hash` | `AGENT_ADMIN_PASSWORD_HASH` | 管理员密码哈希(见「管理员账户」) |
|
||||
| `admin.secondary_password_hash` | `ADMIN_SECONDARY_PASSWORD_HASH` | 可选:敏感操作二级密码 |
|
||||
| `secrets.web_secret_key` | `WEB_SECRET_KEY` | Session 签名密钥(随机 hex) |
|
||||
| `admin.secondary_password_hash` | `ADMIN_SECONDARY_PASSWORD_HASH` | 敏感操作二级密码(**web 模式实际必需**:未配置时管理接口校验恒失败,管理面板功能不可用) |
|
||||
| `secrets.web_secret_key` | `WEB_SECRET_KEY` | Session 签名密钥(随机 hex;**web 模式强烈建议配置**:未配置时使用临时密钥,重启后所有登录会话失效) |
|
||||
| `secrets.api_token_secret` | `API_TOKEN_SECRET` | API Token 加密密钥(随机 hex) |
|
||||
| `models.default_response_max_tokens` | `AGENT_DEFAULT_RESPONSE_MAX_TOKENS` | 单轮响应 token 全局上限 |
|
||||
| `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('你的密码'))"
|
||||
```
|
||||
|
||||
管理员登录邮箱自动生成,规则为 `<用户名>@local`(例如用户名为 `admin`,则登录邮箱为 `admin@local`)。
|
||||
|
||||
### 普通用户(web 模式)
|
||||
|
||||
**邀请码注册制**:`/register` 注册必须提供有效邀请码;邀请码由管理员在用户管理中生成(支持次数限制)。
|
||||
**邀请码注册制**:`/register` 注册必须提供有效邀请码;邀请码由管理员在管理面板(`/admin/monitor`)的「邀请码」标签页生成(支持次数限制)。
|
||||
|
||||
> ⚠️ **安全提示**
|
||||
> - 注册邮箱仅用于登录与查重,**系统不发送验证邮件**——邀请码是注册的唯一实质门槛,请妥善保管。
|
||||
> - 新用户的默认权限模式为「无限制」(`unrestricted`,可执行任意终端命令)。多用户或包含不可信用户的场景下,建议用户在个人空间将默认权限模式调整为 `approval` / `readonly`。
|
||||
|
||||
## 项目结构
|
||||
|
||||
|
||||
@ -3,5 +3,4 @@ name: mcp-tool-config
|
||||
description: 指导模型在宿主机模式下,如何给自己配置新的mcp工具
|
||||
---
|
||||
|
||||
Just Read This File
|
||||
skills/mcp-tool-config/mcp-tool-config-guide.md
|
||||
阅读本目录下的 [mcp-tool-config-guide.md](mcp-tool-config-guide.md),获取完整的 MCP 工具配置指南。
|
||||
@ -1,7 +1,6 @@
|
||||
# 本项目新增 MCP 工具配置指南
|
||||
|
||||
> 最后更新:2026-04-26
|
||||
> 适用目录:`/Users/jojo/Desktop/agents/正在修复中/agents`
|
||||
> 最后更新:2026-08-22
|
||||
|
||||
本文面向后续维护者,说明如何在当前项目里新增并接入一个 MCP 工具(MCP Server)。
|
||||
|
||||
@ -44,7 +43,7 @@
|
||||
|
||||
仓库已有可直接参考的测试服务:
|
||||
|
||||
- `/Users/jojo/Desktop/agents/正在修复中/agents/scripts/mcp_calculator_server.py`
|
||||
- `scripts/mcp_calculator_server.py`
|
||||
|
||||
这个示例使用 stdio,支持:
|
||||
|
||||
@ -146,11 +145,11 @@
|
||||
- `MCP_PROTOCOL_VERSION=2025-06-18`
|
||||
- `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. 关键代码位置(便于二次开发)
|
||||
|
||||
- MCP 配置:`/Users/jojo/Desktop/agents/正在修复中/agents/config/mcp.py`
|
||||
- 服务注册表:`/Users/jojo/Desktop/agents/正在修复中/agents/modules/mcp_server_registry.py`
|
||||
- MCP 客户端桥接:`/Users/jojo/Desktop/agents/正在修复中/agents/modules/mcp_client_manager.py`
|
||||
- 管理 API:`/Users/jojo/Desktop/agents/正在修复中/agents/server/admin.py`
|
||||
- MCP 配置:`config/mcp.py`
|
||||
- 服务注册表:`modules/mcp_server_registry.py`
|
||||
- MCP 客户端桥接:`modules/mcp_client_manager/`(子包)
|
||||
- 管理 API:`server/admin.py`
|
||||
- 工具定义/执行:
|
||||
- `/Users/jojo/Desktop/agents/正在修复中/agents/core/main_terminal_parts/tools_definition.py`
|
||||
- `/Users/jojo/Desktop/agents/正在修复中/agents/core/main_terminal_parts/tools_execution.py`
|
||||
- 管理端页面:`/Users/jojo/Desktop/agents/正在修复中/agents/static/src/admin/PolicyApp.vue`
|
||||
- 测试示例服务:`/Users/jojo/Desktop/agents/正在修复中/agents/scripts/mcp_calculator_server.py`
|
||||
- `core/main_terminal_parts/tools_definition/`(子包,`tools_definition.py` 为兼容入口)
|
||||
- `core/main_terminal_parts/tools_execution.py`
|
||||
- 管理端页面:`static/src/admin/PolicyApp.vue`
|
||||
- 测试示例服务:`scripts/mcp_calculator_server.py`
|
||||
|
||||
202
docs/README.md
202
docs/README.md
@ -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(不持久化)
|
||||
|
||||
**打字机效果:**
|
||||
- 思考块:无打字机效果,直接追加
|
||||
- 工具块 intent:0.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. 子智能体通知改为 SSE(Server-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分钟
|
||||
@ -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` 防止路由冲突
|
||||
|
||||
这套机制为多对话管理提供了坚实的基础,值得在类似系统中借鉴。
|
||||
@ -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. **页面刷新恢复**: 状态恢复逻辑复杂,容易出错
|
||||
|
||||
建议采用状态机模式统一管理任务状态,增加停止确认机制,改进停止标志检查频率,并添加详细的调试日志。
|
||||
@ -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. **状态同步**: 恢复思考块、工具块、文本块的完整状态
|
||||
|
||||
这套机制确保了用户在页面刷新后能够无缝恢复任务,不会丢失任何进度,同时保持了良好的用户体验。
|
||||
@ -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:分层配置优先级**
|
||||
|
||||
```
|
||||
优先级(高→低):
|
||||
运行时 Set(config_manager.set())
|
||||
→ 环境变量覆盖
|
||||
→ 项目级配置(<workspace>/.astrion/config.json)
|
||||
→ 全局配置(~/.astrion/astrion/settings.json)
|
||||
→ 默认值
|
||||
```
|
||||
|
||||
**模式 3:统一 ConfigManager 单例**
|
||||
|
||||
```
|
||||
ConfigManager
|
||||
├── load() 从 JSON 文件加载
|
||||
├── get(key) 获取配置值
|
||||
├── set(key, value) 运行时修改 + 持久化
|
||||
├── reload() 重新加载(配置热更新)
|
||||
├── subscribe() 变更监听
|
||||
└── _resolve_path() 路径解析
|
||||
```
|
||||
|
||||
**模式 4:Flag 系统**
|
||||
|
||||
```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. 运行时 Set(config_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()` → 服务启动 |
|
||||
@ -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. loading:sidebar 来源先 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 守卫语义。
|
||||
@ -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`,两种模式的对话互相不可见
|
||||
|
||||
这套设计在双模式并行使用时暴露了一串耦合 bug(2026-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 不得混入
|
||||
@ -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 兄弟节点,宽 294px(270 + 左右 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 细边框。
|
||||
- 标题栏 38px:SVG 图标(accent 色)+ 标题(12.5px 次要色)+ 右侧计数(11.5px 三级色)。
|
||||
- **列表无上下 padding**:首行 hover 紧贴标题栏分隔线,末行紧贴窗口底边。
|
||||
- 行:固定 36px 高,直接铺在窗口上(无嵌套卡片/背景/边框),超长文字省略号。
|
||||
- 正式接入时配色全部换成三主题语义 token(demo 为经典主题暖色写死)。
|
||||
|
||||
## 3. 各窗口设计
|
||||
|
||||
### 3.1 待办事项
|
||||
|
||||
- 行结构:`[状态点] 任务文字`;**无 hover、不可点击**。
|
||||
- 计数:`done/total`。
|
||||
- 动画:
|
||||
- **创建**:逐行从左向右(位移 14px + 淡入,0.34s,stagger 60ms)。
|
||||
- **完成**:横线从左往右划过文字(260ms,`strike-in` scaleX 0→1,origin left)→ 文字延迟 120ms 变灰 + 状态点变色弹跳(`dot-pop`)。静态完成态(整表渲染时)不播划线,直接呈现。
|
||||
- **整表替换**(收到新 todo):旧行从上到下逐行从右向左移出(0.26s,stagger 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] 后端:编辑文件路径记录写入对话 JSON(write/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 服务重启后的实际运行验证待手动进行。
|
||||
@ -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. 文案与产品约束
|
||||
|
||||
- 面向用户的提示词仅描述“能力边界”和“可操作建议”
|
||||
- 不暴露内部实现细节(如内部判定逻辑、实现细节名称)
|
||||
- 审批流程文案要强调“单次授权、最小权限、可回退”
|
||||
@ -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`:现有代码分析
|
||||
@ -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 工具的结果中。
|
||||
@ -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`。
|
||||
@ -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 问 B,B 正在运行
|
||||
|
||||
场景:
|
||||
- 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)`
|
||||
@ -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. 现有单智能体模式不受影响
|
||||
@ -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 SubAgentManager(manager.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 SubAgentTask(task.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` 需要设置合理超时,避免永久阻塞。
|
||||
@ -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` 为 async(tools_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.rejectTo(stage 或 branch),工具返回带审核意见。
|
||||
- **审核异常/超时** → 视为驳回(计入计数),返回中含「审核服务可能异常,**请告知用户**」。
|
||||
- **撞 maxRejects** → status=failed(exit_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 |
|
||||
|
||||
### REST(3 个)
|
||||
|
||||
- `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 导致主任务异常终止;柔性优先。
|
||||
11
scripts/public_release_excludes.txt
Normal file
11
scripts/public_release_excludes.txt
Normal 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
179
scripts/release_public.sh
Executable 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 的快照。"
|
||||
@ -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>
|
||||
@ -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` 分发链(L960,elif 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`(L960,elif 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` 负责把「运行期状态」渲染进 prompt(permission/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-mode,409 空闲才可切)、`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`(L109,skills 段 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/continue;continue 注入续命 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 通知机制冲突。
|
||||
@ -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. LangGraph(LangChain)
|
||||
|
||||
### 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 + graph,graph 由 `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` 节点对接外部 API;agent 节点可挂载工具与内部工作流。
|
||||
|
||||
---
|
||||
|
||||
## 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 Builder(2025 发布)
|
||||
|
||||
### 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. CrewAI(Crew + 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(顺序传参)与 hierarchical(manager 动态分工)两种。
|
||||
- **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 | 云端可视化 + SDK(Responses 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-loop:LangGraph `interrupt()`、Dify `human-input`、CrewAI task 级 human review。
|
||||
- Evaluator/Guardrail:OpenAI Guardrails + inline Evals;Anthropic 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 具体节点内部字段)以第三方整理为主,未做运行级验证。
|
||||
@ -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(子流程,复合活动) | 内嵌 Subprocess(embedded)/全局 Subprocess 通过 Call Activity(调用活动,粗边框)引用 | 子流程内部有独立的起止事件;父流程 token 等待子流程完成。另有 **Event Subprocess**(虚线框,由事件触发,可中断/非中断)与 **Ad-hoc Subprocess**(波浪线标记,内部活动任意顺序/跳过) |
|
||||
| | 标记(Markers) | Loop(循环)、Multiple Instance(多实例,可并行/串行)、Compensation(补偿) | 附着在任务/子流程上,扩展其执行语义 |
|
||||
| **网关 Gateways** | 排他网关 Exclusive(XOR) | — | **恰好走一条路径**(数据条件互斥时用);经典"是/否、通过/拒绝"决策 |
|
||||
| | 并行网关 Parallel(AND) | — | **所有路径同时走**,合并时等待所有分支到达(漏掉 AND 合并会导致活动重复执行) |
|
||||
| | 包容网关 Inclusive(OR) | — | **一条或多条**条件为真则都走,合并时等待所有激活分支 |
|
||||
| | 基于事件网关 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-ELSE,true/false 两出口) | **无专门审批节点**;人工介入通常靠「结束节点返回 + 对话式确认」或外部系统实现 | 可视化节点编排,节点输入输出引用(`{{node.output}}`)连接 |
|
||||
| **n8n**(官方节点 + 第三方深度分析) | Trigger、AI Agent、LLM、IF / Switch / Merge、Wait(等待)、Form(表单)、Code / Function、HTTP Request、Sub-workflow、**Human in the Loop(Human 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.0(Camunda/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(人工介入节点)、n8n(Human Review 节点)、BPMN(User Task + 网关)、LangGraph(interrupt 节点内暂停 + 条件边) |
|
||||
|
||||
### 方案 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. **业界共识**:从最严谨的 BPMN(User 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 Modification(cancelActivityInstance + 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 Tasks(Task 类型参考):https://camunda.com/bpmn/reference/#activities (活动章节)
|
||||
- Camunda Blog — Events: Basic Concepts(catching/throwing、边界事件、非中断事件):https://camunda.com/bpmn/reference/#events
|
||||
- Camunda Forum — "Go back to previous user task"(官方论坛:回退需 Process Instance Modification 的 cancelActivityInstance + startTransition):https://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
|
||||
- Stately(XState 官方) — 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 — Gateways(XOR 网关条件表达式示例 `@@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 (第三方)
|
||||
- Medium(Sebastian Lesser)— BPMN Gateways Explained:https://medium.com/@sebastian.lesser/bpmn-gateways-explained-2fbea236b0fa (第三方)
|
||||
- ProcessCamp — BPMN Elements Reference(元素分类计数):https://processcamp.io/bpmn-elements (第三方)
|
||||
- Beyond Engineering — BPMN 2.0 Elements:http://www.beyondengineering.io/bpmn-elements (第三方)
|
||||
- 火山引擎开发者社区 — 从零开始学 Dify 工作流实现机制(节点类型/IF_ELSE 实现/edge_source_handle):https://developer.volcengine.com/articles/7538284296277229609 (第三方)
|
||||
- Dify 官网博客转述 — Dify Workflow 重磅上线(核心节点:LLM/工具/意图分类器/知识检索/代码/If-Else):https://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/代码/知识库/插件/条件/循环/变量/HTTP):https://skillhub.cloud.tencent.com/skills/coze (第三方)
|
||||
- REBUILD 帮助文档 — 审批流程(发起人/条件分支/审批人/抄送人节点、加签/转审/会签或签/限时/自由审批):https://getrebuild.com/docs/admin/approval (第三方)
|
||||
- FlyFlow 更新日志(节点新增:投票节点/办理节点等,佐证国内审批流节点体系):https://www.flyflow.cc/upgrade (第三方)
|
||||
- ZenML Blog — LangGraph vs n8n(n8n 两层 HITL:Wait 节点 + AI 工具调用级批准;LangGraph interrupts):https://www.zenml.io/blog/langgraph-vs-n8n (第三方)
|
||||
- Peliqan — LangGraph vs n8n(HITL 对比、checkpointing):https://peliqan.io/blog/langgraph-vs-n8n (第三方)
|
||||
- LOW/CODE — n8n vs LangGraph(n8n 节点/400+ 集成/HITL 暂停审批):https://www.lowcode.agency/blog/n8n-vs-langgraph (第三方)
|
||||
- TopsInfoSolutions — n8n vs LangGraph(LangGraph checkpoint、HITL 暂停审批恢复):https://www.topsinfosolutions.com/blog/n8n-vs-langgraph (第三方)
|
||||
- jimmysong.io — Open Source AI Agent Platform Comparison(Dify/Coze/n8n/LangGraph 平台对比):https://jimmysong.io/blog/open-source-ai-agent-workflow-comparison (第三方)
|
||||
- CustomJS — n8n Human-in-the-Loop(Wait 节点/webhook 恢复、审批表单局限):https://www.customjs.space/blog/n8n-human-in-the-loop (第三方)
|
||||
- GrowwStacks — Human in the Loop in n8n Guide(Human Review 节点:approve-only / approve-and-disapprove、超时):https://growwstacks.com/blog/human-in-the-loop-n8n-guide (第三方)
|
||||
- nocodecreative.io — n8n Wait Node v2(Wait 节点 + 子工作流 HITL):https://blog.nocodecreative.io/n8n-v2-wait-node-hitl-sub-workflows (第三方)
|
||||
- DEV Community — LangGraph Interrupts and Commands(get_approval 节点 + router 条件边示例):https://dev.to/jamesbmour/interrupts-and-commands-in-langgraph-building-human-in-the-loop-workflows-4ngl (第三方)
|
||||
- Future AGI — What is LangGraph(StateGraph/节点/边/条件边/checkpoint/interrupt):https://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: Guard(guard 守卫语义):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;未修改项目任何代码文件。)
|
||||
Loading…
Reference in New Issue
Block a user