diff --git a/frontend-web/.env.development b/frontend-web/.env.development index 41decf09..6228c720 100644 --- a/frontend-web/.env.development +++ b/frontend-web/.env.development @@ -16,7 +16,13 @@ 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_CONSUMER_URL=https://ch1.b.uat.4.cn/consumer 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..4492f3e5 100644 --- a/frontend-web/docs/multi-agent-backend-dev.md +++ b/frontend-web/docs/multi-agent-backend-dev.md @@ -12,8 +12,8 @@ - 三个圆桌 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(单选、点击即发送)。 @@ -282,33 +282,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 +333,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`) @@ -609,13 +624,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 +727,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 + 推荐流程跑不起来,跟客户业务无关 diff --git a/frontend-web/docs/multi-agent-frontend-dev.md b/frontend-web/docs/multi-agent-frontend-dev.md index 88da48a8..89ee2963 100644 --- a/frontend-web/docs/multi-agent-frontend-dev.md +++ b/frontend-web/docs/multi-agent-frontend-dev.md @@ -16,6 +16,7 @@ - **不使用 LangGraph SDK**:所有 API 都是自研的 `apiFetch` + `ReadableStream` SSE 解析(见 `api/intent.ts`、`api/recommend.ts`、`api/multi-agent.ts`)。 - **不写死模型名**:与 `/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__`,文档历史版本有误)。 @@ -341,7 +342,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)` diff --git a/frontend-web/src/assets/icon/down-logo.svg b/frontend-web/src/assets/icon/down-logo.svg new file mode 100644 index 00000000..19343061 --- /dev/null +++ b/frontend-web/src/assets/icon/down-logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend-web/src/assets/icon/nav-active.svg b/frontend-web/src/assets/icon/nav-active.svg new file mode 100644 index 00000000..776703e6 --- /dev/null +++ b/frontend-web/src/assets/icon/nav-active.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend-web/src/assets/icon/nav-line-logo.svg b/frontend-web/src/assets/icon/nav-line-logo.svg new file mode 100644 index 00000000..0f31a8e2 --- /dev/null +++ b/frontend-web/src/assets/icon/nav-line-logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend-web/src/components/page-sdebar.css b/frontend-web/src/components/page-sdebar.css index 61890fb3..de2edd84 100644 --- a/frontend-web/src/components/page-sdebar.css +++ b/frontend-web/src/components/page-sdebar.css @@ -1,24 +1,185 @@ -.sidebar-contents{ - width:98px; - height:986px; - background:rgba(3, 38, 80, 0.8); - border:1px solid; - border-color:rgba(43, 143, 254, 0.6); - border-radius:4px; - backdrop-filter:blur(2px); +.sidebar-contents { + width: 98px; + min-height: 986px; + background: rgba(3, 38, 80, 0.8); + border: 1px solid; + border-color: rgba(43, 143, 254, 0.6); + border-radius: 4px; + backdrop-filter: blur(2px); + display: flex; + flex-direction: column; + align-items: center; + padding: 12px 10px; + position: relative; } -.item-nav{ - width:78px; - height:70px; - background:rgba(0, 12, 28, 0.15); - border:1px solid; - border-color:rgba(85, 166, 254, 0.2); - line-height:8px; - font-family:Noto Sans S Chinese; - font-weight:500; - color:rgba(255, 255, 255, 0.8); - font-size:20px; - padding-bottom:18px; - text-align:center; +.item-nav { + width: 100%; + height: 70px; + background: #ffffff1a; + border-color: #fefafa4d; + margin-top: 13px; + font-family: Noto Sans S Chinese; + font-weight: 550; + color: #fff; + font-size: 20px; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + cursor: pointer; + background: #000c1c26; + border: 1px solid; + border-color: #55a6fe33; + box-shadow: 0 0 20px #00b9ff26 inset; + position: relative; +} + +.item-nav:hover { + background: rgba(85, 166, 254, 0.12); + color: #ffffff; +} + +.item-nav__label { + display: block; + width: 3em; + margin: 0 auto; + padding-left: 6px; + box-sizing: border-box; + letter-spacing: 6px; + word-break: break-all; +} + +.item-nav-content { + width: 155px; + min-height: 970px; + background: rgba(3, 38, 80, 0.8); + border: 1px solid; + border-color: rgba(43, 143, 254, 0.6); + border-radius: 4px; + backdrop-filter: blur(2px); + position: absolute; + left: 97px; + top: 0; + z-index: 1000; + padding: 14px 13px 18px 16px; + overflow-y: auto; +} + +.item-nav-active { + color: #ffffff; + background: linear-gradient(134.25deg, + rgba(0, 98, 217, 0.59) 2.28%, + rgba(0, 98, 217, 0.3) 99.59%) !important; + background-size: cover; + border: 1px solid; + border-color: rgba(255, 255, 255, 0.2); + position: relative; +} + +.item-nav-active::before { + content: ''; + position: absolute; + right: -10px; + top: 50%; + transform: translateY(-50%); + width: 0; + height: 0; + border-top: 8px solid transparent; + border-bottom: 8px solid transparent; + border-left: 10px solid rgba(0, 98, 217, 0.45); + pointer-events: none; +} +.item-nav-active::after { + content: ''; + position: absolute; + left: 0; + top: 24px; + width: 77px; + height: 45px; + background: url('../assets/icon/nav-line-logo.svg') no-repeat; + background-size: cover; +} + +.sidebar-flyout-list { + display: flex; + flex-direction: column; + gap: 2px; + position: relative; +} + +.sidebar-flyout-list--level-1, +.sidebar-flyout-list--level-2, +.sidebar-flyout-list--level-3 { + margin-left: 19px; + padding-left: 14px; +} + +.sidebar-flyout-node { + position: relative; +} + +.sidebar-flyout-node--has-children.sidebar-flyout-node--expanded>.sidebar-flyout-list::before { + content: ''; + position: absolute; + left: 0; + top: 0; + width: 1px; + height: 100%; + background: rgba(43, 143, 254, 0.22); +} + +.sidebar-flyout-item { + width: 100%; + min-height: 34px; + display: flex; + align-items: center; + gap: 9px; + border: 0; + background: transparent; + padding: 3px 0; + font-family: Noto Sans S Chinese; + font-size: 18px; + line-height: 28px; + font-weight: 500; + letter-spacing: 0; + color: rgba(255, 255, 255, 0.8); + text-align: left; + cursor: pointer; + transition: color 0.2s ease, text-shadow 0.2s ease; +} + +.sidebar-flyout-item:hover, +.sidebar-flyout-item--active { + color: #48ddff; + text-shadow: 0 0 8px rgba(72, 221, 255, 0.25); +} + +.sidebar-flyout-item--leaf { + justify-content: center; + padding-left: 0; +} + +.sidebar-flyout-list--level-1>.sidebar-flyout-node>.sidebar-flyout-item--leaf, +.sidebar-flyout-list--level-2>.sidebar-flyout-node>.sidebar-flyout-item--leaf, +.sidebar-flyout-list--level-3>.sidebar-flyout-node>.sidebar-flyout-item--leaf { + padding-left: 0; +} + +.sidebar-flyout-arrow { + width: 9px; + height: 9px; + flex: 0 0 9px; + background: url('../assets/icon/down-logo.svg') center / contain no-repeat; + transform: rotate(-90deg); + transition: transform 0.2s ease; +} + +.sidebar-flyout-node--expanded>.sidebar-flyout-item>.sidebar-flyout-arrow { + transform: rotate(0deg); +} + +.sidebar-flyout-label { + flex: 1; + white-space: nowrap; } \ No newline at end of file diff --git a/frontend-web/src/components/page-sidebar-dark.tsx b/frontend-web/src/components/page-sidebar-dark.tsx index 251e1d82..aa18608f 100644 --- a/frontend-web/src/components/page-sidebar-dark.tsx +++ b/frontend-web/src/components/page-sidebar-dark.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import type { ElementType } from "react"; import { useNavigate, useLocation } from "react-router-dom"; import { @@ -74,6 +74,7 @@ const menuItems: MenuItem[] = [ icon: HelpCircle, children: [ { id: "general-qa", label: "通用问答", icon: CircleHelp, path: "/page/workspace/chats/new" }, + { id: "expert-qa", label: "专家问答", icon: Brain, path: "/page/strategy/qa/expert-qa" }, { id: "qa-zzjc", label: "zzjc支持", icon: Bot }, { id: "qa-zzxd", label: "zzXD应用", icon: Layout }, { id: "qa-research", label: "课题研究", icon: FileText, path: "/page/canvas/ai-writing" }, @@ -132,9 +133,24 @@ const menuItems: MenuItem[] = [ label: "COH中任务", icon: Folder, children: [ - { id: "coh-task-3qfx", label: "3QFX", icon: ListTodo }, - { id: "coh-task-fx", label: "任务FX", icon: ListTodo }, - { id: "coh-task-xdfx", label: "XDFX", icon: ListTodo }, + { + id: "coh-task-3qfx", + label: "3QFX", + icon: ListTodo, + path: "/page/strategy/task-management/plan-task-sentiment", + }, + { + id: "coh-task-fx", + label: "任务FX", + icon: ListTodo, + path: "/page/strategy/task-management/plan-task/plan-task-fx", + }, + { + id: "coh-task-xdfx", + label: "XDFX", + icon: ListTodo, + path: "/page/strategy/task-management/plan-task/plan-task-action", + }, ], }, { @@ -222,9 +238,24 @@ const menuItems: MenuItem[] = [ label: "资源管理", icon: Database, children: [ - { id: "resource-network-xwt", label: "网络xwt资源管理", icon: Network }, - { id: "resource-knowledge-base", label: "知识库资源管理", icon: Library }, - { id: "resource-zz", label: "ZZ资源管理", icon: FileBox }, + { + id: "resource-network-xwt", + label: "网络xwt资源管理", + icon: Network, + path: "/page/strategy/resource-management/resource-network-actor", + }, + { + id: "resource-knowledge-base", + label: "知识库资源管理", + icon: Library, + path: "/page/strategy/resource-management/resource-knowledge-base", + }, + { + id: "resource-evaluation-index", + label: "作战资源管理", + icon: FileBox, + path: "/page/strategy/resource-management/resource-evaluation-index", + }, ], }, ]; @@ -254,7 +285,10 @@ export function PageSidebarDark({ const { theme } = useTheme(); const isDark = theme === "dark"; const isAdmin = getStoredAuth()?.system_role === "admin"; - const visibleMenuItems = filterMenuItemsByRole(menuItems, !!isAdmin); + const visibleMenuItems = useMemo( + () => filterMenuItemsByRole(menuItems, !!isAdmin), + [isAdmin], + ); const { t } = useI18n(); const { data: unreadCount = 0 } = useUnreadCount(); const [expandedIds, setExpandedIds] = useState>(defaultExpandedIds); @@ -275,6 +309,26 @@ export function PageSidebarDark({ setSettingsOpen(true); }; + const hasActiveChild = (item: MenuItem): boolean => { + if (item.path && pathname === item.path) { + return true; + } + + return item.children?.some(hasActiveChild) ?? false; + }; + + const initialActiveTopItem = visibleMenuItems.find(hasActiveChild) ?? visibleMenuItems[0]; + const [activeTopId, setActiveTopId] = useState(initialActiveTopItem?.id ?? "qa-center"); + + useEffect(() => { + const matchedTopItem = visibleMenuItems.find(hasActiveChild); + if (matchedTopItem) { + setActiveTopId(matchedTopItem.id); + } + }, [pathname, visibleMenuItems]); + + const activeTopItem = visibleMenuItems.find((item) => item.id === activeTopId) ?? initialActiveTopItem; + const toggleExpanded = (id: string) => { setExpandedIds((prev) => { const next = new Set(prev); @@ -287,6 +341,51 @@ export function PageSidebarDark({ }); }; + const handleTopItemClick = (item: MenuItem) => { + setActiveTopId(item.id); + + if (!item.children?.length && item.path) { + navigate(item.path); + } + }; + + const handleMenuItemClick = (item: MenuItem) => { + if (item.children?.length) { + toggleExpanded(item.id); + return; + } + + if (item.path) { + navigate(item.path); + } + }; + + const renderFlyoutMenu = (items: MenuItem[], level = 0) => ( +
+ {items.map((item) => { + const hasChildren = !!item.children?.length; + const isExpanded = expandedIds.has(item.id); + const isSelected = item.path ? pathname === item.path : hasActiveChild(item); + + return ( +
+ + {hasChildren && isExpanded ? renderFlyoutMenu(item.children ?? [], level + 1) : null} +
+ ); + })} +
+ ); return ( <>
-
- 问答管理 +
+ {activeTopItem?.children?.length ? renderFlyoutMenu(activeTopItem.children) : null}
+ {visibleMenuItems.map((item) => ( + + ))}
diff --git a/frontend-web/src/components/page-sidebar.tsx b/frontend-web/src/components/page-sidebar.tsx index 810e9e74..11dec345 100644 --- a/frontend-web/src/components/page-sidebar.tsx +++ b/frontend-web/src/components/page-sidebar.tsx @@ -73,6 +73,7 @@ const menuItems: MenuItem[] = [ icon: HelpCircle, children: [ { id: "general-qa", label: "通用问答", icon: CircleHelp, path: "/page/workspace/chats/new" }, + { id: "expert-qa", label: "专家问答", icon: Brain, path: "/page/strategy/qa/expert-qa" }, // 「圆桌规划」入口暂时隐藏 —— 功能仍在 /page/roundtable/planning,只是不在侧边栏出现。 // 需要重新开放时取消注释即可。 // { @@ -130,8 +131,18 @@ const menuItems: MenuItem[] = [ label: "XK中任务", icon: Folder, children: [ - { id: "xk-task-jc", label: "任务JC", icon: ListTodo }, - { id: "xk-task-dispatch", label: "任务调度", icon: ListTodo }, + { + id: "xk-task-jc", + label: "任务JC", + icon: ListTodo, + path: "/page/strategy/task-management/xk-task-jc", + }, + { + id: "xk-task-dispatch", + label: "任务调度", + icon: ListTodo, + path: "/page/strategy/task-management/xk-task-dispatch", + }, ], }, { @@ -139,9 +150,24 @@ const menuItems: MenuItem[] = [ label: "COH中任务", icon: Folder, children: [ - { id: "coh-task-3qfx", label: "3QFX", icon: ListTodo }, - { id: "coh-task-fx", label: "任务FX", icon: ListTodo }, - { id: "coh-task-xdfx", label: "XDFX", icon: ListTodo }, + { + id: "coh-task-3qfx", + label: "3QFX", + icon: ListTodo, + path: "/page/strategy/task-management/plan-task-sentiment", + }, + { + id: "coh-task-fx", + label: "任务FX", + icon: ListTodo, + path: "/page/strategy/task-management/plan-task/plan-task-fx", + }, + { + id: "coh-task-xdfx", + label: "XDFX", + icon: ListTodo, + path: "/page/strategy/task-management/plan-task/plan-task-action", + }, ], }, { @@ -149,9 +175,24 @@ const menuItems: MenuItem[] = [ label: "PG中任务", icon: Folder, children: [ - { id: "pg-task-pg", label: "任务PG", icon: ListTodo }, - { id: "pg-task-xdpg", label: "XDPG", icon: ListTodo }, - { id: "pg-task-info-pg", label: "信息PG", icon: ListTodo }, + { + id: "pg-task-pg", + label: "任务PG", + icon: ListTodo, + path: "/page/strategy/task-management/effect-task-pg", + }, + { + id: "pg-task-xdpg", + label: "XDPG", + icon: ListTodo, + path: "/page/strategy/task-management/effect-action-pg", + }, + { + id: "pg-task-info-pg", + label: "信息PG", + icon: ListTodo, + path: "/page/strategy/task-management/effect-info-pg", + }, ], }, { @@ -159,9 +200,24 @@ const menuItems: MenuItem[] = [ label: "TY中任务", icon: Folder, children: [ - { id: "ty-task-xdty", label: "XDTY任务", icon: ListTodo }, - { id: "ty-task-plan", label: "方案TY任务", icon: ListTodo }, - { id: "ty-task-xxty", label: "XXTY任务", icon: ListTodo }, + { + id: "ty-task-xdty", + label: "XDTY任务", + icon: ListTodo, + path: "/page/strategy/task-management/mnty-task-mechanism", + }, + { + id: "ty-task-plan", + label: "方案TY任务", + icon: ListTodo, + path: "/page/strategy/task-management/mnty-task-plan", + }, + { + id: "ty-task-xxty", + label: "XXTY任务", + icon: ListTodo, + path: "/page/strategy/task-management/mnty-task-zsty", + }, ], }, ], @@ -236,9 +292,24 @@ const menuItems: MenuItem[] = [ label: "资源管理", icon: Database, children: [ - { id: "resource-network-xwt", label: "网络xwt资源管理", icon: Network }, - { id: "resource-knowledge-base", label: "知识库资源管理", icon: Library }, - { id: "resource-zz", label: "ZZ资源管理", icon: FileBox }, + { + id: "resource-network-xwt", + label: "网络xwt资源管理", + icon: Network, + path: "/page/strategy/resource-management/resource-network-actor", + }, + { + id: "resource-knowledge-base", + label: "知识库资源管理", + icon: Library, + path: "/page/strategy/resource-management/resource-knowledge-base", + }, + { + id: "resource-evaluation-index", + label: "作战资源管理", + icon: FileBox, + path: "/page/strategy/resource-management/resource-evaluation-index", + }, ], }, ]; diff --git a/frontend-web/src/components/ui/card.tsx b/frontend-web/src/components/ui/card.tsx index eb693c16..8a613603 100644 --- a/frontend-web/src/components/ui/card.tsx +++ b/frontend-web/src/components/ui/card.tsx @@ -7,7 +7,7 @@ function Card({ className, ...props }: React.ComponentProps<"div">) {
) { return (
); @@ -65,7 +65,7 @@ function CardContent({ className, ...props }: React.ComponentProps<"div">) { return (
); diff --git a/frontend-web/src/components/ui/input-group.tsx b/frontend-web/src/components/ui/input-group.tsx index bb535dbb..ec7272b4 100644 --- a/frontend-web/src/components/ui/input-group.tsx +++ b/frontend-web/src/components/ui/input-group.tsx @@ -14,7 +14,7 @@ function InputGroup({ className, ...props }: React.ComponentProps<"div">) { data-slot="input-group" role="group" className={cn( - "group/input-group border-input/50 dark:bg-background/80 relative flex w-full items-center rounded-md border bg-white/80 shadow-xs transition-[color,box-shadow] outline-none", + "group/input-group relative flex w-full items-center rounded-md border border-input/50 bg-white/80 shadow-xs transition-[color,box-shadow] outline-none dark:border-[#1B477E] dark:bg-[#022248]", "h-9 min-w-0 has-[>textarea]:h-auto", // Variants based on alignment. diff --git a/frontend-web/src/components/ui/input.tsx b/frontend-web/src/components/ui/input.tsx index 481e6788..6537bb0f 100644 --- a/frontend-web/src/components/ui/input.tsx +++ b/frontend-web/src/components/ui/input.tsx @@ -8,9 +8,12 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) { type={type} data-slot="input" className={cn( - "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", - "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]", - "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", + "h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base outline-none transition-[color,box-shadow] md:text-sm", + "border-input placeholder:text-muted-foreground", + "dark:border-[#1B477E] dark:bg-[#022248] dark:text-[#AACEFF] dark:placeholder:text-[#566B7F] rounded-[4px] text-[16px]", + "file:text-foreground selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50", + "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] dark:focus-visible:border-[#3b82f6] dark:focus-visible:ring-[#3b82f6]/30", + "aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40", className, )} {...props} diff --git a/frontend-web/src/components/ui/tabs.tsx b/frontend-web/src/components/ui/tabs.tsx index 41a53cc7..9bbb70e2 100644 --- a/frontend-web/src/components/ui/tabs.tsx +++ b/frontend-web/src/components/ui/tabs.tsx @@ -30,7 +30,8 @@ const tabsListVariants = cva( { variants: { variant: { - default: "bg-muted", + default: + "bg-muted dark:group-data-[orientation=horizontal]/tabs:h-8! dark:items-stretch dark:gap-0 dark:overflow-hidden dark:rounded-[2px] dark:border dark:border-[#55A6FE] dark:bg-[#022248] dark:p-0", line: "gap-1 bg-transparent", }, }, @@ -40,60 +41,65 @@ const tabsListVariants = cva( }, ); -const TabsList = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef & - VariantProps ->(({ className, variant = "default", ...props }, ref) => { - return ( - - ); -}); - -TabsList.displayName = TabsPrimitive.List.displayName; +const TabsList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, variant = "default", ...props }, ref) => { + return ( + + ); +}); -const TabsTrigger = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => { - return ( - - ); -}); - -TabsTrigger.displayName = TabsPrimitive.Trigger.displayName; +TabsList.displayName = TabsPrimitive.List.displayName; -const TabsContent = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => { - return ( - - ); -}); - -TabsContent.displayName = TabsPrimitive.Content.displayName; +const TabsTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + return ( + + ); +}); + +TabsTrigger.displayName = TabsPrimitive.Trigger.displayName; + +const TabsContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + return ( + + ); +}); + +TabsContent.displayName = TabsPrimitive.Content.displayName; export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants }; diff --git a/frontend-web/src/components/ui/textarea.tsx b/frontend-web/src/components/ui/textarea.tsx index d55b5a4b..5bdc6ef7 100644 --- a/frontend-web/src/components/ui/textarea.tsx +++ b/frontend-web/src/components/ui/textarea.tsx @@ -7,7 +7,9 @@ function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {