diff --git a/frontend-web/.env.development b/frontend-web/.env.development index 41decf09..0bb1cdba 100644 --- a/frontend-web/.env.development +++ b/frontend-web/.env.development @@ -16,9 +16,17 @@ VITE_AIPI_GENERATE_CHAT_URL=https://ch1.b.uat.4.cn/jialai_b1_chat VITE_AIPI_TASK_CHAT_URL=https://ch1.b.uat.4.cn/qq_b1_chat VITE_AIPI_KNOWLEDGE_URL=https://chlib.b.uat.4.cn/knowlibrary VITE_AIPI_B1COH_URL=https://ch1.b.uat.4.cn/web -VITE_AIPI_B2ACTOR_URL=htttps://ty.b.uat.4.cn +VITE_AIPI_B2ACTOR_URL=https://ty.b.uat.4.cn +VITE_AIPI_B2ACTOR_XWT_PATH=/fromB/toXWT?isFrame=1 +VITE_AIPI_KNOWLEDGE_IFRAME_API_URL=https://ch1.b.uat.4.cn/knowes/api/iframeKnowledgeUrl +VITE_AIPI_KNOWLEDGE_IFRAME_SKIN=red +VITE_AIPI_B2ACTOR_XWT_PATH=/fromB/toXWT?isFrame=1 +VITE_AIPI_KNOWLEDGE_IFRAME_API_URL=https://ch1.b.uat.4.cn/knowes/api/iframeKnowledgeUrl +VITE_AIPI_KNOWLEDGE_IFRAME_SKIN=red VITE_AIPI_MOPECHAT_URL=https://ch1.b.uat.4.cn/likun_b1_mope VITE_AIPI_BACKENP_URL=https://knowledgetoolserver.b.uat.4.cn +# 我的笔记前端 +VITE_AIPI_FRONTEND_URL= xxx VITE_AIPI_CONSUMER_URL=https://ch1.b.uat.4.cn/consumer VITE_AIPI_PGLINKTO_URL=https://pg.b.uat.4.cn VITE_AIPI_XKLINKTO_URL=https://kz.b.uat.4.cn diff --git a/frontend-web/.env.development.local.example b/frontend-web/.env.development.local.example index 471b2649..f01f2187 100644 --- a/frontend-web/.env.development.local.example +++ b/frontend-web/.env.development.local.example @@ -10,7 +10,10 @@ VITE_AIPI_GENERATE_CHAT_URL=https://ch1.b.uat.4.cn/jialai_b1_chat VITE_AIPI_TASK_CHAT_URL=https://ch1.b.uat.4.cn/qq_b1_chat VITE_AIPI_KNOWLEDGE_URL=https://chlib.b.uat.4.cn/knowlibrary VITE_AIPI_B1COH_URL=https://ch1.b.uat.4.cn/web -VITE_AIPI_B2ACTOR_URL=htttps://ty.b.uat.4.cn +VITE_AIPI_B2ACTOR_URL=https://ty.b.uat.4.cn +VITE_AIPI_B2ACTOR_XWT_PATH=/fromB/toXWT?isFrame=1 +VITE_AIPI_KNOWLEDGE_IFRAME_API_URL=https://ch1.b.uat.4.cn/knowes/api/iframeKnowledgeUrl +VITE_AIPI_KNOWLEDGE_IFRAME_SKIN=red VITE_AIPI_MOPECHAT_URL=https://ch1.b.uat.4.cn/likun_b1_mope VITE_AIPI_BACKENP_URL=https://knowledgetoolserver.b.uat.4.cn VITE_AIPI_CONSUMER_URL=https://ch1.b.uat.4.cn/consumer diff --git a/frontend-web/.env.example b/frontend-web/.env.example index 30e82ad6..adb14fb8 100644 --- a/frontend-web/.env.example +++ b/frontend-web/.env.example @@ -24,6 +24,11 @@ # --- 生产打包(见 .env.production)--- # 部署前修改 .env.production 中的地址,再执行 pnpm build +# VITE_AIPI_B2ACTOR_URL=https://ty.b.uat.4.cn +# VITE_AIPI_B2ACTOR_XWT_PATH=/fromB/toXWT?isFrame=1 +# VITE_AIPI_KNOWLEDGE_IFRAME_API_URL=https://ch1.b.uat.4.cn/knowes/api/iframeKnowledgeUrl +# VITE_AIPI_KNOWLEDGE_IFRAME_SKIN=red + # 可选 # VITE_CANVAS_LANGGRAPH_BASE_URL= # VITE_STATIC_WEBSITE_ONLY= diff --git a/frontend-web/.env.production b/frontend-web/.env.production index 0e94e082..0f10b55c 100644 --- a/frontend-web/.env.production +++ b/frontend-web/.env.production @@ -14,7 +14,10 @@ VITE_AIPI_GENERATE_CHAT_URL=https://ch1.b.uat.4.cn/jialai_b1_chat VITE_AIPI_TASK_CHAT_URL=https://ch1.b.uat.4.cn/qq_b1_chat VITE_AIPI_KNOWLEDGE_URL=https://chlib.b.uat.4.cn/knowlibrary VITE_AIPI_B1COH_URL=https://ch1.b.uat.4.cn/web -VITE_AIPI_B2ACTOR_URL=htttps://ty.b.uat.4.cn +VITE_AIPI_B2ACTOR_URL=https://ty.b.uat.4.cn +VITE_AIPI_B2ACTOR_XWT_PATH=/fromB/toXWT?isFrame=1 +VITE_AIPI_KNOWLEDGE_IFRAME_API_URL=https://ch1.b.uat.4.cn/knowes/api/iframeKnowledgeUrl +VITE_AIPI_KNOWLEDGE_IFRAME_SKIN=red VITE_AIPI_MOPECHAT_URL=https://ch1.b.uat.4.cn/likun_b1_mope VITE_AIPI_BACKENP_URL=https://knowledgetoolserver.b.uat.4.cn VITE_AIPI_CONSUMER_URL=https://ch1.b.uat.4.cn/consumer diff --git a/frontend-web/docs/multi-agent-backend-dev.md b/frontend-web/docs/multi-agent-backend-dev.md index e759d19d..3e5bf522 100644 --- a/frontend-web/docs/multi-agent-backend-dev.md +++ b/frontend-web/docs/multi-agent-backend-dev.md @@ -12,14 +12,15 @@ - 三个圆桌 Router(`intent / recommend / multi_agent`)**通过 httpx ASGITransport 进程内直调本机 ASGI app**,复用既有 `/api/threads`、`/api/agents`、`/api/threads/{id}/runs/stream` 路由——**不走 TCP / 不经过网络栈**。所有 client 都由 `multi_agent._make_loopback_client(request)` 工厂统一构造。 - 历史包袱:早期实现走 `httpx.AsyncClient(trust_env=False)` + `127.0.0.1:`,遇到开发机 `HTTP_PROXY` / `HTTPS_PROXY` 时仍可能把回环走代理(症状 `headers_in=42.160s`)。**改用 ASGITransport 后此问题不再存在**,相关 `trust_env=False` workaround 全部移除。 -- **席位 agent 持久注册**(磁盘 `.deer-flow/agents/` + `_sync_legacy_agents`),**协调智能体每次新建**(命名 `roundtable-coordinator-{timestamp36}`,DB 累积,需后续 GC)。 -- **`.deer-flow/` 整个目录被 git ignore**(仓库根 `.gitignore` 第 64 / 74 行),`git clone` 后内置 agent 目录在部署机上**不会存在**——任何环境都必须把 `roundtable-intent` + `roundtable-recommender` 两个**功能型 agent** 持久化进发布物,否则 `/api/intent/stream` 报 500。**互联网 demo 还需要 6 个示例研讨席位**(`roundtable-intelligence` 等),**内网部署用客户自有 agent 替代**这些席位,无需打包。详见 §11.7。 +- **席位 agent 持久注册**(磁盘 `.deer-flow/agents/` + `_sync_legacy_agents`),**协调智能体也是持久单例**(id 固定为 `roundtable-coordinator`,与 intent/recommender 同样走 `_roundtable_seed.ensure_roundtable_functional_agents()` 缺失自动落地)。2026-05 之前曾按 `roundtable-coordinator-{timestamp36}` 每会话新建并把动态席位列表写进 SOUL,导致 `.deer-flow/agents/` 累积大量壳目录;改造后 SOUL 是静态通用版本,「本次可调度席位列表」由 `multi_agent.init` 在 coordinator thread 创建后以 human 消息追加到对话历史,leader run 每轮都能看到。 +- **`.deer-flow/` 整个目录被 git ignore**(仓库根 `.gitignore` 第 64 / 74 行),`git clone` 后内置 agent 目录在部署机上**不会存在**。但功能型 agent 现在通过 `_roundtable_seed.ensure_roundtable_functional_agents()` **自动落地**(首次任何 intent/recommend/multi-agent 路由调用都会触发),因此**新部署不再需要手动准备** `roundtable-intent` / `roundtable-recommender` / `roundtable-coordinator` 三个目录——它们都是 seeder 自动写盘的。**互联网 demo 仍需要 6 个示例研讨席位**(`roundtable-intelligence` 等),**内网部署用客户自有 agent 替代**这些席位,无需打包。详见 §11.7。 - **`init` 是 SSE 而非一次性 JSON**:用 `asyncio.as_completed` 按真实完成顺序推送 `seat_ready`,前端因此能逐个点亮席位。 - **`init_done.thread_ids` 包含协调智能体自身**(`agent_threads[main_agent_name] = coord_thread`),前端 Step 3 列文件时需自行过滤。 - **澄清协议统一**:Step 1 与 Step 2 leader 都借 `ask_clarification` + 终态 `clarification` 帧驱动同一套选项卡片组件;`allow_multiple` 默认 false(单选、点击即发送)。 - **leader 共识 = `status: []` 空数组**;澄清 = `status: "clarification"`(字符串);派活 = `status: [[name, task], ...]`。这三者**互斥不混用**。 - **后端 picks clamp 2–6,前端用户可手动加到 8**。 - **不写死任何具体模型名**:`intent / recommend / multi_agent` 三个 router 的 `model_name = str(payload.get("model") or "")`,空串会被下游 `lead_agent._resolve_model_name()` 自动回退到 `config.yaml` 的 `models[0]`(与 `/api/ai-writing` 行为一致)。内网部署只需配 `config.yaml`,前后端均无需改硬编码。 +- **草稿/推荐历史持久化(2026-06)**:圆桌页的「历史草稿」从浏览器 `localStorage` 迁到**后端 MySQL**(`/api/roundtable-drafts`,按登录用户隔离);新增**会话级推荐历史**让推荐弹窗回显上次结果。两张表 `roundtable_drafts` / `roundtable_recommend_history`,启动 `create_all` 自动建表,详见 §7A。 --- @@ -73,7 +74,12 @@ offline-backend-20260512/backend/ │ ├── multi_agent.py # Step 2:init(SSE) + run/stream + 共享的 loopback 工具 │ ├── intent.py # Step 1:init + stream(import multi_agent 工具) │ ├── recommend.py # Step 1→2:推荐 stream(import multi_agent 工具) -│ └── artifacts.py # Step 3:列出/下载/编辑 thread 产出(通用,非圆桌专用) +│ ├── artifacts.py # Step 3:列出/下载/编辑 thread 产出(通用,非圆桌专用) +│ └── roundtable_drafts.py # 持久化:草稿 CRUD + 推荐历史(§7A,2026-06 新增) +├── packages/harness/deerflow/persistence/roundtable_drafts/ +│ ├── model.py # ORM:RoundtableDraftRow + RoundtableRecommendHistoryRow +│ ├── sql.py # RoundtableDraftRepository(CRUD + 历史) +│ └── __init__.py # make_roundtable_draft_store(sf) ├── packages/harness/deerflow/tools/builtins/ │ └── clarification_utils.py # resolve_allow_multiple + coerce_bool(Step1/Step2 共享) └── .deer-flow/agents/ @@ -110,8 +116,15 @@ offline-backend-20260512/backend/ | GET | `/api/threads/{thread_id}/artifacts` | 3 | 列出该 thread 的 outputs 文件 | | GET | `/api/threads/{thread_id}/artifacts/{path}` | 3 | 预览/下载单个产出(含 `.skill/` 内文件提取) | | PATCH | `/api/threads/{thread_id}/artifacts/{path}` | 3 | 编辑文本类产物(白名单后缀) | +| GET | `/api/roundtable-drafts` | 持久化 | 列出当前用户草稿(轻量元数据) | +| POST | `/api/roundtable-drafts` | 持久化 | 新建草稿 | +| GET | `/api/roundtable-drafts/{id}` | 持久化 | 取草稿全量(含 step1/step2 快照) | +| PUT | `/api/roundtable-drafts/{id}` | 持久化 | 局部更新(title / furthest_step / step1 / step2) | +| DELETE | `/api/roundtable-drafts/{id}` | 持久化 | 删除草稿 | +| GET | `/api/roundtable-drafts/{id}/recommendations` | 持久化 | 列出该草稿的推荐历史(倒序) | +| POST | `/api/roundtable-drafts/{id}/recommendations` | 持久化 | 追加一次推荐记录 | -字段级请求/响应见各 step 的 API 文档;下文只写**后端实现语义**。 +字段级请求/响应见各 step 的 API 文档;下文只写**后端实现语义**。圆桌草稿/推荐历史持久化见 §7A。 --- @@ -282,33 +295,50 @@ offline-backend-20260512/backend/ 请求体: { "agent_names": ["roundtable-intelligence", ...], // 用户选的席位 id - "main_agent_name": "roundtable-coordinator-xxx", // 当次总控 id,前端生成 - "model": "deepseek-chat" // 可选,写入 coordinator agent + "main_agent_name": "<可选,已废弃>", // 历史字段;2026-05 改造后后端忽略,统一用 COORDINATOR_AGENT_ID + "model": "deepseek-chat" // 可选;不传由 lead_agent 回退到 config.yaml models[0] } ``` +**协调智能体改造说明(2026-05)**:以前 `main_agent_name` 由前端用时间戳生成 +(`roundtable-coordinator-${Date.now().toString(36)}`),后端 `_create_coordinator_direct` +把动态拼接的席位列表 SOUL 写盘并入 DB,导致 `.deer-flow/agents/` 越用越多。改造后: + +- **id 固定**:`COORDINATOR_AGENT_ID = "roundtable-coordinator"`(前后端常量必须保持一致); +- **缺失自动落地**:路由入口调 `ensure_roundtable_functional_agents()`,与 intent/recommender 共用同一套 seeder; +- **席位列表运行时注入**:init 创建好 coordinator thread 之后,立刻通过 `_append_thread_message` 把「【圆桌系统初始化】本次可调度席位:…」作为 human 消息追加到 thread 历史最前面,leader run 每轮都能看到; +- **`main_agent_name` 入参保留为向后兼容**:旧 client 仍可传,后端只是日志里看到不会用它创建新 agent。 + **早期校验**(同步抛 HTTP 4xx,**不**进 SSE): - `agent_names` 必须非空 -- 每个 `agent_names` 元素 + `main_agent_name` 都需通过 `_AGENT_ID_PATTERN = ^[A-Za-z0-9_-]+$`(用 `_validate_agent_id` 提前 400;上游若收到不合规 id 会抛 422,提前拦更友好) +- 每个 `agent_names` 元素需通过 `_AGENT_ID_PATTERN = ^[A-Za-z0-9_-]+$`(用 `_validate_agent_id` 提前 400;上游若收到不合规 id 会抛 422,提前拦更友好) - 所有 id 都 `.lower()` 后比较 / 存储 +- `main_agent_name` 入参不再做校验(被忽略,统一用 `COORDINATOR_AGENT_ID` 常量) -**SSE 内部流程**: +**SSE 内部流程**(2026-05 单例改造后): ``` +ensure_roundtable_functional_agents() + // 路由入口:保证 .deer-flow/agents/roundtable-coordinator/ 存在 + // 缺失时由 _roundtable_seed.COORDINATOR_AGENT_SOUL 落地静态 SOUL + asyncio.as_completed(各席位 _prepare_sub_agent) 每完成一个 → data: {"event":"seat_ready","agent_name","thread_id","display_name","description"} (按真实完成顺序,先完成先发,前端因此能逐个点亮) 异常时:取消所有未完成的 task,避免泄露到 AsyncClient close 之后 -协调智能体: - POST /api/threads → coord_thread - POST /api/agents - skills=["agent_orchestration"] - soul=动态拼接(本次 init 的席位列表 + 派活规则) - id=main_agent_name(前端生成的 roundtable-coordinator-) +协调智能体(单例): + main_agent_name = COORDINATOR_AGENT_ID # 固定常量,不再每次新建 agent + coord_thread = _create_thread_direct(...) # 仍然每次新 thread,会话独立 agent_threads[main_agent_name] = coord_thread // ⚠️ thread_ids 含 coordinator 自身 + + # 通过 HTTP loopback append 一条 human 消息,把本次席位清单写入对话历史: + # "【圆桌系统初始化】本次研讨可调度的席位清单如下..." + # leader run 每轮都能从对话历史里读到,等效于旧版动态 SOUL 的作用。 + _append_thread_message(seed_client, ..., coord_thread, seat_introduction) + → data: {"event":"coordinator_ready","main_agent_name","thread_id"} → data: {"event":"init_done","main_agent_name","thread_ids":{...}} @@ -316,28 +346,26 @@ asyncio.as_completed(各席位 _prepare_sub_agent) 失败 → data: {"event":"error","detail":"..."} ``` -**协调智能体创建幂等**:若 `_create_agent` 返回 (False, "exists ...")(同名 agent 已存在)则继续往下走,不报错。注意这里的「已存在」会复用历史 agent 的 SOUL(与本次动态拼接的不同),通常只在前端用同一个时间戳跑过两遍时触发。 +**协调智能体单例 SOUL**(磁盘上唯一一份,见 `_roundtable_seed.COORDINATOR_AGENT_SOUL`): -**协调智能体 SOUL**(动态拼接,与席位列表一起写入): +通用协调规则,**不含**席位列表。模型派活时从对话历史最前面的「【圆桌系统初始化】…」消息读取本次可用 agent_name。仍然强调英文 id(避免把中文显示名拼进 `agent_name`),靠后端 `_strip_display_suffix` 兜底(详见 §6.4)。 + +**席位清单注入消息形状**(init 阶段写入 coordinator thread): ``` -#核心定位: -用途:根据用户的任务目标,调用其他智能体干活,只负责协调汇总, -当你觉得已有信息支持回答用户问题时候,停止调度,并输出最后的结果 - -#调度方法: -通过使用 agent_orchestration 技能调用其他智能体干活,使用技能时传入 -agent_name 和需要做的活。 -⚠️ agent_name 必须严格使用下方列出的英文 id(例如 roundtable-intelligence), - 不要把括号里的中文显示名拼进 agent_name 里。 - -#可调度智能体: -- agent_name: roundtable-intelligence(情报收集): -- agent_name: roundtable-environment(环境评估): +【圆桌系统初始化】本次研讨可调度的席位清单如下,请严格按 agent_name 通过 agent_orchestration 技能派活,不要派给不在清单内的 agent: +- agent_name: roundtable-intelligence(情报收集): +- agent_name: roundtable-environment(环境评估): ... ``` -中文显示名通过 `GET /api/agents/{id}` 取 `name`、描述取 `description`。**强调英文 id** 是因为部分模型会把整段 `roundtable-x(中文)` 当 `agent_name` 塞回来,靠后端 `_strip_display_suffix` 兜底(详见 §6.4)。 +中文显示名通过 `_get_agents_batch_direct` 一次 DB 查询取 `name`、描述取 `description`。 + +**单例 agent 的几个语义保障**: + +1. **agent 落地幂等**:`_ensure_one` 检查目录是否存在,存在就 no-op,不覆盖运维手改的 SOUL; +2. **席位列表 append 失败不阻断**:`_append_thread_message` 包在 try/except 里,失败只记 warning,leader run 仍能跑(只是看不到席位清单,可能派活到错的 id,由 `_validate_agent_id` 在 run/stream 里报错); +3. **`_create_coordinator_direct` 已删除**:旧函数已移除(2026-05 改造),避免后续维护者误调;如果你正在 review 旧 PR 看到这个函数,那是 pre-rebase 版本。 ### 6.2 `/run/stream` — `agent_type = "leader"`(`_leader_run`) @@ -574,6 +602,53 @@ Step 3 **没有** 单独的 `multi_agent` 路由;使用既有 artifacts 能力 --- +## 7A. 圆桌草稿与推荐历史持久化(`roundtable_drafts.py`,2026-06) + +圆桌规划页的「历史记录」此前只存浏览器 `localStorage`(前端 `utils/drafts.ts`),换设备/清缓存即丢、大对话还会撞 5MB 配额。2026-06 迁到**后端 MySQL**,按登录用户隔离、跨设备保留;并新增**会话级推荐历史**,让推荐弹窗能回显上次推荐结果。 + +### 7A.1 数据模型(`deerflow.persistence.roundtable_drafts`) + +仿 `user_prompts`(最简 CRUD)+ `ai_writing_sessions`(大 JSON 用 `PortableLongText` 存)两个现有模式。两张表: + +- **`roundtable_drafts`**(会话主表,一条 = 一次完整圆桌会话) + - `id`(PK, str64) / `user_id`(idx) / `title`(512) / `furthest_step`(int, 1|2|3) + - `step1` / `step2`:前端 `DraftStep{1,2}Snapshot` 的 JSON,列类型 **`PortableLongText`**(MySQL `LONGTEXT`),避免 Step 2 长对话 + `seatStubMessages` 撞 `TEXT` 64KB 上限 + - `created_at` / `updated_at`:**`BeijingDateTime`**(项目规范,存北京时间且 tz-aware) +- **`roundtable_recommend_history`**(推荐历史,按 `draft_id` 归属) + - `id`(PK) / `user_id`(idx) / `draft_id`(idx) / `objective`(512) / `status`(32: done|fallback|asking|error) / `model`(128) + - `rationale` / `picks`(JSON) / `candidates`(JSON 候选池快照):均 `PortableLongText` + - `created_at`:`BeijingDateTime` + +JSON 列在 repo(`sql.py`)里用 `json.dumps/loads` 手动序列化(与 `ai_writing_sessions.transcript` 同法)。两个 Row 已登记进 `persistence/models/__init__.py`,**启动时 `Base.metadata.create_all` 自动建表**——新部署无需手动建表(需 mysql 账号有建库/表权限)。 + +### 7A.2 路由(`app/gateway/routers/roundtable_drafts.py`,前缀 `/api/roundtable-drafts`) + +| 方法 | 路径 | 说明 | +|------|------|------| +| GET | `` | `list_drafts` → 轻量元数据列表(不含 step 大 blob),按 `updated_at` 倒序 | +| POST | `` | `create_draft`,可带客户端 `id`(稳定 id 跨 create+update) | +| GET | `/{id}` | `get_draft` 全量;非属主 → 404 | +| PUT | `/{id}` | `update_draft`,`model_dump(exclude_unset=True)` → 只更新客户端实际发的字段 | +| DELETE | `/{id}` | 204;非属主 → 404 | +| GET | `/{id}/recommendations` | 推荐历史倒序 | +| POST | `/{id}/recommendations` | 追加一次推荐(先校验草稿属主,404 拦截越权) | + +鉴权/属主:用 `_current_user_id(request)`(登录态取 `request.state.user.id`,无登录回退 `get_effective_user_id()` 的 `"default"`),与 `user_prompts` 一致;所有 repo 读写按 `user_id` 过滤,跨用户访问一律 404。store 在 `deps.py` 接为 `app.state.roundtable_draft_store`(`make_roundtable_draft_store(sf)`),`app.py` `include_router`。 + +### 7A.3 与前端的契约要点 + +- **PUT 不带 `title` 时不改标题**:前端自动保存只发 `furthest_step`/`step1`/`step2`,不发 `title`,因此**不会覆盖用户手动重命名**;标题只在 create(派生)与显式重命名(只发 title)时改。 +- **推荐时机**:前端在打开推荐弹窗前先 `ensureDraft` 落地草稿拿到 `draft_id`,推荐完成后 POST 一条历史;重开弹窗 GET 历史回显最近一次,可「重新分析」再 POST。 +- **字段命名**:后端 snake_case(`furthest_step`/`created_at`),前端 `api/drafts.ts` 在边界映射成 camelCase。 + +### 7A.4 设计取舍 + +- **整存 JSON 而非拆表**:step1/step2 形状宽且常变(前端 hook 里是 loose 类型),整存 JSON 避免每次改前端快照都要迁库;代价是后端不校验嵌套字段(与 `ai_writing_sessions.transcript` 同取舍)。 +- **推荐历史绑定草稿而非全局**:语义是「这个会话上次推荐了什么」,故按 `draft_id` 归属;这也要求推荐前必有 `draft_id`(前端 `ensureDraft` 保证)。 +- **本地 dev 走 sqlite**:`config.yaml` `database.backend` 本地默认 sqlite,走同一套 SQLAlchemy 代码,`PortableLongText/JSON` 各自降级;生产切 mysql 用同一份代码。 + +--- + ## 8. 内置席位 SOUL 设计要点 每个 `roundtable-*`(除 intent / recommender)遵循统一结构:角色定位、核心职责、输出风格(emoji + 简短结论)、边界(只产出本席位、等总控派活)。 @@ -609,13 +684,20 @@ httpx.AsyncClient( **已废止的 `trust_env=False` workaround**:旧版本走 `127.0.0.1:` 真实 HTTP,需要 `trust_env=False` 阻止 httpx 读 `HTTP_PROXY` 把回环也走代理(症状日志 `headers_in=42.160s`)。改 ASGITransport 后这个问题在物理上已不存在——根本没走网络栈。 -### 9.2 协调智能体每次新建 +### 9.2 协调智能体单例(2026-05 改造) -`main_agent_name` 形如 `roundtable-coordinator-{timestamp36}`,会话隔离;长期会累积 agent 目录(实际仓库已有数十个 `roundtable-coordinator-mp*`),需后续 GC 策略: +历史实现 `main_agent_name = roundtable-coordinator-{timestamp36}`,会话隔离,但长期会累积 agent 目录(仓库里曾积累几十个 `roundtable-coordinator-mp*` 壳目录)。2026-05 改造选用上方决策 §9.2 选项 C 的本质:**把 coordinator 实例化为「内置 + 静态 SOUL + 运行时注入席位列表」**,固定 id `roundtable-coordinator`,与 `roundtable-intent` / `roundtable-recommender` 同样走 `ensure_roundtable_functional_agents()` 缺失自动落地。 -- 选项 A:定时任务清理 N 天前无活跃 thread 的 coordinator agent -- 选项 B:每次创建前 LRU 清理(注意:跨服务实例难协调) -- 选项 C:把 coordinator 实例化为「内置 + 动态 SOUL」复用同一个 id +实现要点: + +- **磁盘 SOUL 是通用版本**:不含本次席位列表,只规定协调规则、工具用法、终止条件; +- **席位列表运行时注入**:init 创建 coordinator thread 后立刻 `_append_thread_message` 写入一条 human 消息("【圆桌系统初始化】…"),位置在对话历史最前面,leader run 每轮自然读到; +- **`_create_coordinator_direct` 已删除**:不再有"每次创建"路径。 + +**遗留 `roundtable-coordinator-*` 目录的处理**:仓库可能仍有几十个历史时间戳壳目录。改造后**不会**再生成新的,但旧目录目前不自动清理(避免破坏可能仍存在的旧草稿引用)。运维可通过: + +- 在 admin agents 管理页逐个 DELETE; +- 或写一次性脚本扫描 `.deer-flow/agents/roundtable-coordinator-*` 并删除目录 + DB 记录(注意不要删除 `roundtable-coordinator` 本体)。 ### 9.3 澄清协议统一 @@ -705,15 +787,22 @@ offline-backend-20260512/backend/.deer-flow/* | Agent | 角色 | 是否必须打包发布 | |-------|------|------------------| -| `roundtable-intent` | **功能型**:Step 1 任务理解,所有部署都用它 | ✅ **必须**(任何环境) | -| `roundtable-recommender` | **功能型**:Step 1→2 之间从候选池里挑推荐,所有部署都用它 | ✅ **必须**(任何环境) | +| `roundtable-intent` | **功能型**:Step 1 任务理解,所有部署都用它 | ✅ seeder 自动落地(无需手工打包) | +| `roundtable-recommender` | **功能型**:Step 1→2 之间从候选池里挑推荐,所有部署都用它 | ✅ seeder 自动落地(无需手工打包) | +| `roundtable-coordinator` | **功能型单例**:Step 2 总控协调,2026-05 起为单例(id 固定) | ✅ seeder 自动落地(无需手工打包) | | `roundtable-intelligence` | 示例研讨席位(情报收集) | ⚠️ **互联网 demo 用**,内网不需要 | | `roundtable-environment` | 示例研讨席位(环境评估) | ⚠️ 同上 | | `roundtable-solution-design` | 示例研讨席位(方案设计) | ⚠️ 同上 | | `roundtable-risk-review` | 示例研讨席位(风险审查) | ⚠️ 同上 | | `roundtable-execution-plan` | 示例研讨席位(执行规划) | ⚠️ 同上 | | `roundtable-summary` | 示例研讨席位(总结陈述) | ⚠️ 同上 | -| `roundtable-coordinator-*` | 协调智能体壳,每次会话动态新建 | ❌ **不打包**(运行时生成) | +| `roundtable-coordinator-*`(带后缀) | 2026-05 之前每次会话动态新建的临时壳 | ❌ **遗留**(仓库可能仍有几十个旧目录,新版本不再生成;运维按需清理) | + +**关键变化(2026-05)**:三个功能型 agent(intent / recommender / coordinator)现在都由 +``_roundtable_seed.ensure_roundtable_functional_agents()`` **自动落地**——任何 intent / +recommend / multi-agent 路由的入口都会触发,首次请求时缺什么补什么。所以**新部署的 +机器 `git clone` 后既不需要手工拷目录、也不需要走下文的 `.gitignore` 白名单方案**, +SOUL 改动跟着 Python 代码走 git review 即可。 **关键区分**: - `roundtable-intent` / `roundtable-recommender` 是**圆桌功能本身的一部分**——没有它们,整个 Step 1 + 推荐流程跑不起来,跟客户业务无关 @@ -1166,6 +1255,9 @@ client = httpx.AsyncClient(transport=transport, base_url=...) | 推荐路由 | `offline-backend-20260512/backend/app/gateway/routers/recommend.py` | | Step 2 路由 | `offline-backend-20260512/backend/app/gateway/routers/multi_agent.py` | | Step 3 列出/下载/编辑 | `offline-backend-20260512/backend/app/gateway/routers/artifacts.py` | +| 草稿/推荐历史路由(§7A) | `offline-backend-20260512/backend/app/gateway/routers/roundtable_drafts.py` | +| 草稿/推荐历史持久化 | `offline-backend-20260512/backend/packages/harness/deerflow/persistence/roundtable_drafts/` | +| store 接线 | `offline-backend-20260512/backend/app/gateway/deps.py`(`app.state.roundtable_draft_store`) | | 路由注册 | `offline-backend-20260512/backend/app/gateway/app.py` | | 澄清单选/多选 | `offline-backend-20260512/backend/packages/harness/deerflow/tools/builtins/clarification_utils.py` | | 任务理解 Agent | `offline-backend-20260512/backend/.deer-flow/agents/roundtable-intent/` | diff --git a/frontend-web/docs/multi-agent-frontend-dev.md b/frontend-web/docs/multi-agent-frontend-dev.md index 88da48a8..a8d8e82c 100644 --- a/frontend-web/docs/multi-agent-frontend-dev.md +++ b/frontend-web/docs/multi-agent-frontend-dev.md @@ -14,8 +14,10 @@ - **步骤折叠卡与主聊天对齐**:思考过程 / 工具调用展示走 `MessageStepsCard`,**always-show-last-tool + 折叠 "查看其他 N 个步骤"** 的视觉与主聊天 `chats/new` 的 `MessageGroup` 一致。工具图标 / 文案映射在 `lib/step-display.tsx`,新增工具时与 `src/core/i18n/locales/zh-CN.ts` 的 `toolCalls` 字段对齐。 - **`` 标签自动提取为「思考过程」**:`lib/reasoning.ts::splitInlineReasoning(text)` 把 `...`(含未闭合的流式中段)抽离成 `reasoning`,正文走 markdown;由 `MessageBubble.tsx::ReasoningBlock` 渲染一个折叠卡。Step 1 / Step 2 气泡和推荐弹窗的 rationale 都走这一套,与主聊天 `Reasoning` 元素的视觉对齐。 - **不使用 LangGraph SDK**:所有 API 都是自研的 `apiFetch` + `ReadableStream` SSE 解析(见 `api/intent.ts`、`api/recommend.ts`、`api/multi-agent.ts`)。 +- **草稿/历史持久化在后端(2026-06)**:草稿不再存浏览器 `localStorage`,改走后端 MySQL(`/api/roundtable-drafts`,按登录用户隔离、跨设备保留)。`api/drafts.ts` 封装 CRUD,`useDraftPersistence` 异步对接、对外接口不变。**推荐弹窗带「会话级推荐历史」**:每个草稿一串历史,重开弹窗先显示上次推荐结果,可「重新分析」重跑(详见 §5.7 / §6.2)。后端实现见 backend doc 的「圆桌草稿持久化」一节。 - **不写死模型名**:与 `/page/canvas/ai-writing` 同样的策略 —— 全局选中模型由 `selectedModel` state 维护,首次 `useModels()` 完成后初始化为 `availableModels[0].name`,顶栏「模型」下拉可切换。所有 API 调用统一传 `model: selectedModel || undefined`;后端缺省由 `lead_agent._resolve_model_name()` 回退到 `config.yaml` 的 `models[0]`。**内网部署只需在 `config.yaml` 配自家模型即可,前后端均无需改动**。 - **席位区间硬约束**:推荐弹窗要求 **2 ≤ 选中数 ≤ 8**(`MIN_SELECTED` / `MAX_SELECTED`)。 +- **协调智能体单例(2026-05)**:`COORDINATOR_AGENT_ID = "roundtable-coordinator"` 是固定常量,不再每会话生成新 id。后端 seeder 自动落地,「本次席位列表」改由后端通过 thread 历史 human 消息注入,前端无感。`newCoordinatorName()` 仅作为兼容性壳保留。 - **总控编排循环上限**:`MAX_CYCLES = 8`,超出后强制停止并 toast。 - **席位派活策略**:**顺序**(不是并行)。旧实现并行触发多路 SSE 会让浏览器卡顿,现已改为顺序。 - **调试开关**:`window.__MULTI_AGENT_INIT_TIMING__` 与 `window.__MULTI_AGENT_DEBUG__`(**不是** `__MULTI_AGENT_STREAM_DEBUG__`,文档历史版本有误)。 @@ -53,7 +55,8 @@ frontend-web/src/roundtable-planning/ │ └── RoundtablePlanningPage.tsx # 主页面:组合 hook + 顶栏/侧栏 + 三步路由(≈1 150 行) ├── api/ │ ├── intent.ts # Step 1:initIntent、streamIntent -│ ├── recommend.ts # 推荐:streamRecommend、filterRecommendCandidates、parseRecommendStreamContent +│ ├── recommend.ts # 推荐:streamRecommend、filterRecommendCandidates、parseRecommendStreamContent;**+ 推荐历史 listRecommendHistory / saveRecommendHistory** +│ ├── drafts.ts # **草稿后端 CRUD**:listDrafts / getDraft / createDraft / updateDraft / deleteDraft / deriveDraftName(替代旧 localStorage utils/drafts.ts) │ └── multi-agent.ts # Step 2/3:initMultiAgent、streamMultiAgent、listSessionArtifacts、buildArtifactDownloadUrl ├── components/ │ ├── Avatars.tsx # 所有 SVG 头像 + getAvatar 选择器 @@ -72,14 +75,13 @@ frontend-web/src/roundtable-planning/ │ ├── useAutoScroll.ts # 滚动跟随 + 用户上滑暂停跟随 │ ├── useStep1Intent.ts # Step 1 状态机:init、send、stream、abort、INTENT_READY 同步 │ ├── useStep2Orchestration.ts # Step 2 状态机:initMultiAgent + 编排循环 + 对话流管理 + Persona -│ └── useDraftPersistence.ts # 草稿:自动保存(前进切换)/手动保存/加载/重命名/删除 +│ └── useDraftPersistence.ts # 草稿(后端版):异步走 api/drafts;自动保存(前进切换)/手动保存/加载/重命名/删除/ensureDraft ├── lib/ │ ├── clarification.ts # resolveAllowMultiple(单选/多选解析),与主聊天 ClarificationCard 对齐 │ ├── reasoning.ts # splitInlineReasoning:从模型流式文本里提取 `...` 思考过程 │ ├── roundtable-constants.ts # 类型(RoleEntry / SelectedAgent) + 常量 + 纯工具函数(buildRoleTemplates / splitIntentReadyBlock 等) │ └── step-display.tsx # Step 2 ChainOfThought 折叠卡图标 + 文案 + 子节点渲染 -├── utils/ -│ └── drafts.ts # localStorage 草稿存档(list / upsert / get / delete / rename / deriveName) +├── utils/ # (旧 drafts.ts 已删除 —— 草稿改走后端 api/drafts.ts) └── styles/ └── roundtable-planning.css # 页面主题变量(浅红品牌色 + 深色覆盖) ``` @@ -182,6 +184,7 @@ hooks/useDraftPersistence → utils/drafts + step1/step2 的 Snapshot 类型 - `signal`:必填(页面通过 `intentAborterRef` 控制暂停 / 切步取消) - `onTextDelta(text, messageId)`:每个 AI 文本增量 - `onPhaseChange(phase, toolName?)`:`connecting → connected → streaming|tool_calling` +- **`onClarification(clarification, index)`(2026-06)**:每解析到一条 `ask_clarification` 工具结果帧就回调一次,让前端**实时**逐个渲染澄清卡。任务理解智能体**一轮可能问多个问题**,这是「实时接收显示协助过程」的关键入口。 **`StreamIntentResult`**: @@ -190,14 +193,16 @@ hooks/useDraftPersistence → utils/drafts + step1/step2 的 Snapshot 类型 | `status` | `"asking" \| "clarification" \| "done" \| "error"` | 终态分支 | | `content` | `string` | 完整 AI 文本(含 `[INTENT_READY]` 块) | | `summary` | `IntentSummary \| null` | 仅 `done` 有;前端 fallback 也会从 `content` 里用 `splitIntentReadyBlock` 解 | -| `clarification` | `ClarificationPayload \| null` | 仅 `clarification` 有 | +| `clarification` | `ClarificationPayload \| null` | **第一个**澄清(向后兼容字段) | +| `clarifications` | `ClarificationPayload[]` | **本轮全部澄清问题**(2026-06)。来源是流式 `type:"tool"` 帧而非终态帧——**终态帧只带第 1 个**,多问题必须靠它。每条带 `id`(= `tool_call_id`)供前端按题维护选择。 | **SSE 解析要点**: - `event: metadata` → phase `connected` - `event: messages` + `extractAiTextDelta` → `onTextDelta` +- `event: messages` + `type:"tool"` 且 `name==="ask_clarification"` → `extractClarificationToolMessage` 解出完整澄清(options 已是 `{id,label}`),按 `tool_call_id` 去重后 `onClarification` 实时回调并收进 `result.clarifications` - tool call 帧(`tool_calls` / `tool_call_chunks` 非空)→ phase `tool_calling`(含 `ask_clarification` 工具名) -- 终态帧识别:`isFinalIntentFrame`(必须有 `status` + `content`) +- 终态帧识别:`isFinalIntentFrame`(必须有 `status` + `content`);`status==="clarification"` 且**未**收到任何流式 tool 澄清帧时,才用终态帧字段兜底合成单条澄清 **Abort 行为**: @@ -341,7 +346,12 @@ React 18 StrictMode 开发环境下,effect 会 mount → cleanup → mount 两 | `agentConfigs` | `Map` Persona 覆盖;下一轮该席位的派活会用此 model | | `isUpperPanelCollapsed` | 是否折叠圆桌可视化 + 调控台,给对话区让出垂直空间 | -**协调智能体命名**:`roundtable-coordinator-${Date.now().toString(36)}`,每次 Step 2 会话新建。后端把这类壳 agent 留在 DB 中,因此 `filterRecommendCandidates` 默认过滤掉。 +**协调智能体命名(2026-05 改造)**:现在是**固定单例 id** `roundtable-coordinator`(见 `lib/roundtable-constants.ts::COORDINATOR_AGENT_ID`)。`newCoordinatorName()` 函数为兼容性保留,内部直接返回这个常量字符串。后端 `ensure_roundtable_functional_agents()` 缺失自动落地,不再每次新建。`filterRecommendCandidates` 在 `EXCLUDED_AGENT_IDS` 集合里同时排除: + +- `roundtable-coordinator`(新版单例 id,永远过滤); +- `roundtable-coordinator-*` 前缀(历史残留,管理员清理之前先在 UI 隐藏)。 + +> 历史草稿兼容性:用户加载 2026-05 前保存的草稿时,存储的 `coordinatorName` 仍是 `roundtable-coordinator-` 形式。只要那个旧 agent 目录还在磁盘上(即运维没手动删过),加载后继续在 Step 2 干活就能正常工作;如果旧 agent 被清理掉了,需要让用户「重新起草」从头跑一遍 Step 1 → Step 2。 ### 5.4 编排循环 `runOrchestration(seedMessage, ids, coord)` @@ -424,19 +434,29 @@ isOrchestrating = false - **禁用态**:`availableModels.length === 0` 或 `modelsLoading` → `disabled`,placeholder 显示「加载中…」或「未配置模型」 - **可改换为 Radix Select**:当前用原生 `