# 圆桌 Step 2 沙箱预览 · 开发进度 > 最后更新:2026-05-27 > 关联文档:`multi-agent-frontend-dev.md`(总览)、`multi-agent-backend-dev.md`(后端)、`multi-agent-api.md`(接口) > 对照参考:主聊天 `/page/workspace/chats/:threadId`(`ChatBox` + `ArtifactsProvider` + `message-group.tsx`) --- ## 1. 目标 在 Step 2「多智能体圆桌研讨」中,当子智能体调用 `write_file` / `str_replace` / `present_files` 时,**行为与主聊天一致**: | 能力 | 主聊天 | Step 2 目标 | |------|--------|-------------| | `write_file` 流式期间自动打开右侧沙箱 | ✅ | ✅ 虚拟 URL | | 步骤卡里点击文件路径打开沙箱 | ✅ | ✅ | | 消息下方文件卡片(`present_files`)点击打开 | ✅ | ✅ `PresentFilesRow` | | 沙箱打开时收起 workspace 左侧栏 | ✅(`ArtifactsProvider.select`) | ✅ Step 2 左栏联动 | | 60/40 对话 \| 产物预览分栏 | ✅ `ChatBox` | ✅ `Step2SandboxLayout` | | 流式 MD 从 tool_call.args.content 预览 | ✅ 虚拟 URL + stub messages | ✅ `seatStubMessages`(双通道:`onPhaseChange` + `onUpstreamFrame`) | | 落盘后 HTTP 拉取真实文件 | ✅ artifacts API | ✅ `present_files` 卡片走 HTTP | | 草稿 reload 后历史文件可预览 | ✅ | ✅ `seatStubMessages` 一并入草稿 | --- ## 2. 已完成 ### 2.1 Step 2 布局调整 | 项 | 状态 | 说明 | |----|------|------| | 推理模型下拉 | ✅ | 从右侧栏迁至 Step 2 顶栏「多智能体圆桌会商中心」右侧 | | 意图理解 | ✅ | 移至左侧栏顶部,支持展开/收起;收起时仅显示「目标」 | | 任务概览 | ✅ | 放在「人工参与」下方 | | 右侧栏 Step 2/3 隐藏 | ✅ | `RightSidebar` 仅在 `currentStep === 1` 渲染 | | 左侧栏整体收起 | ✅ | 宽度 56px;展开按钮在顶部;研讨中角色 ring + ping 动画 | | 沙箱打开 → 左栏收起 | ✅ | `Step2SandboxSidebarSync` 监听 `useArtifacts().open` | **主要文件**:`RoundtablePlanningPage.tsx`(`step2SidebarExpanded`、`LeftSidebar`、`Step2SandboxSidebarSync`) ### 2.2 沙箱基础设施(Phase 1) | 项 | 状态 | 说明 | |----|------|------| | 路由包 `ChatRuntime` | ✅ | `WorkspaceRoutes.tsx` → `roundtable/planning` 外包 `ArtifactsProvider` | | 60/40 分栏壳 | ✅ | `Step2SandboxLayout.tsx`,对齐 `chat-box.tsx` | | `ArtifactFileDetail` / `ArtifactFileList` | ✅ | 嵌在右栏 ResizablePanel | | `ThreadContext` stub | ✅ | 最小 `BaseStream` stub,供 `useArtifactContent` 工作 | ### 2.3 沙箱打开逻辑(Phase 2) | 项 | 状态 | 说明 | |----|------|------| | `write_file` 自动打开 | ✅ | `handleArtifactToolPhase` 在 `tool_calling` + 完整 `args.path` 时触发 | | 步骤卡点击打开 | ✅ | `MessageStepsCard` → `openArtifactFromStep` | | 虚拟 URL 机制 | ✅ | `write-file:?message_id=...&tool_call_id=...`(对齐主聊天) | | Stub messages 注入 | ✅ | `seatStubMessages` + `upsertSeatToolCall` → `loadArtifactContentFromToolCall` | | 重置清理 | ✅ | `resetStep2State` 清空 artifacts / stub / deselect | | `present_files` 步骤点击 | ✅ | 走真实路径 `openArtifactPreview`(HTTP API) | ### 2.4 上游帧兜底注入(Phase 3) | 项 | 状态 | 说明 | |----|------|------| | `onPhaseChange` 主通道 | ✅ | `tool_calling` 阶段每帧把完整 `args` 喂进 stub;首次出现 `path` 即触发自动打开 | | `onUpstreamFrame` 兜底通道 | ✅ | `handleUpstreamFrame` 扫描 messages-tuple 帧的 `tool_calls[]`,对 `write_file`/`str_replace` 持续 upsert `args.content`;覆盖 phase 已切走但 tool_calls 又回包的边界 | | 双通道幂等 | ✅ | `upsertSeatToolCall` 按 `(toolName, path)` 去重,两个通道并发写入安全 | ### 2.5 `present_files` 文件卡(Phase 4) | 项 | 状态 | 说明 | |----|------|------| | 气泡下方文件卡行 | ✅ | `Step2Message` → `collectPresentedFiles` → `PresentFilesRow` | | 卡片样式 | ✅ | 文件名 + 扩展名徽章(FILE_EXT_LABELS),最小宽度 30/最大 50(hover 边框亮色) | | 点击 → HTTP 预览 | ✅ | `openArtifactPreview(agentThreadId, path)`,与步骤卡 `present_files` 走同一通道 | | 席位 thread 缺失保护 | ✅ | 没有 `agentThreadId` 时 disabled + tooltip | | 同气泡多次 present_files | ✅ | `filepaths` 顺序去重,全部聚合到一行卡片 | ### 2.6 草稿持久化(Phase 5) | 项 | 状态 | 说明 | |----|------|------| | `seatStubMessages` 进 `Step2Snapshot` | ✅ | `useStep2Orchestration.ts` Step2Snapshot 新增字段 | | `seatStubMessages` 进 `DraftStep2Snapshot` | ✅ | `drafts.ts` 加 optional 字段,老草稿 reload 时回退到 `{}` | | `hydrateFromDraft` 还原 stub | ✅ | 同步写 `seatStubMessagesRef` + state,加载草稿后点击历史 write_file 步骤可直接预览 | | `getStep2Snapshot` 透出 stub | ✅ | `RoundtablePlanningPage.tsx` 给 `useDraftPersistence` 的 getter 加上该字段 | **主要文件**: ``` frontend-web/src/pages/WorkspaceRoutes.tsx frontend-web/src/roundtable-planning/hooks/useStep2Orchestration.ts frontend-web/src/roundtable-planning/components/Step2SandboxLayout.tsx frontend-web/src/roundtable-planning/components/Step2Panel.tsx frontend-web/src/roundtable-planning/components/MessageStepsCard.tsx frontend-web/src/roundtable-planning/pages/RoundtablePlanningPage.tsx frontend-web/src/roundtable-planning/utils/drafts.ts ``` ### 2.7 与主聊天对齐的核心数据流 ``` special seat SSE (streamMultiAgent) ├── onPhaseChange("tool_calling", "write_file", args) │ ↓ │ upsertSeatToolCall(seatThreadId, "write_file", args) │ → seatStubMessages[seatThreadId] = [{ type:"ai", tool_calls:[{ id, name, args }] }] │ ↓ │ buildWriteFileVirtualUrl(path, messageId, toolCallId) │ → "write-file:/mnt/user-data/outputs/foo.md?message_id=...&tool_call_id=..." │ ↓ │ openSeatVirtualArtifact(seatThreadId, virtualUrl, auto=true) │ → selectArtifact(virtualUrl) + setArtifactsOpen(true) │ └── onUpstreamFrame(frame) ← 兜底通道 ↓ handleUpstreamFrame —— 持续 upsert tool_calls 里完整的 args(不打开沙箱) Step2SandboxLayout ThreadContext.thread.messages = seatStubMessages[activeThreadId] ArtifactFileDetail(filepath=virtualUrl, threadId=seatThreadId) ↓ useArtifactContent → loadArtifactContentFromToolCall → 从 thread.messages 读 tool_call.args.content(不发 HTTP) ``` **为何不用真实 path 直接预览**:流式期间文件可能尚未落盘,直接调 `/api/threads/:id/artifacts/...` 会返回 `404 {"detail":"Not Found"}`。主聊天同样用虚拟 URL 绕过此问题。 --- ## 3. 未完成 / 待验证 ### 3.1 功能缺口(按优先级) | 优先级 | 项 | 状态 | 说明 | |--------|----|------|------| | P0 | 端到端回归 | ⚠️ 待验证 | 虚拟 URL + 双通道 + 文件卡 + 草稿持久化全链路尚未在浏览器里跑一遍 | | P2 | `str_replace` 流式编辑预览 | ❌ | 主聊天 `applyPendingTextEdit` 依赖完整 message 链;stub 未模拟 tool result / 多轮 str_replace 累加 | | P2 | 沙箱 `artifacts[]` 列表与虚拟 URL 混排 | ❌ | 虚拟 URL 不写入 `setArtifacts([])`;多文件切换、文件名下拉体验未完全对齐主聊天 | | P3 | Step 3 与沙箱衔接 | ❌ | Step 3 仍用 `HighFidelityReport` + `listSessionArtifacts`;与 Step 2 沙箱选中态无联动 | | P3 | 文档同步 | ❌ | `multi-agent-frontend-dev.md` 仍描述旧布局(右侧栏模型选择器等),需更新 § 布局 + 沙箱章节 | ### 3.2 已知风险 / 边界 1. **打开时机**:仅在 SSE 进入 `tool_calling` 且 `tool_calls[0].args` 为完整对象时触发自动打开(`multi-agent.ts` 刻意忽略 `tool_call_chunks` 的拼接 args)。若模型只发 chunks、迟迟不发完整 tool_calls,自动打开会延迟到 args 完整那一帧——与主聊天一致。 2. **多 write_file 同气泡**:`upsertSeatToolCall` 按 `(toolName, path)` 幂等;同 path 多次 write 会更新 args,不会新建多条 stub tool_call。 3. **leader 席位**:当前仅 special 流绑定 `agentThreadId` 并接 `onUpstreamFrame`;总控若直接 write_file,需单独接 thread id 与帧回调。 4. **`present_files` 文件大小**:流式期间 backend 尚未给出真实大小;卡片只显示扩展名徽章,不显示 size。落盘后用户点开走 HTTP API 自然能看到完整内容。 5. **调试日志**:控制台 `[roundtable-sandbox]` 前缀;生产环境可考虑加 `window.__ROUNDTABLE_SANDBOX_DEBUG__` 门控。 --- ## 4. 分阶段计划(原始规划 vs 现状) | 阶段 | 内容 | 状态 | |------|------|------| | Phase 1 | `ChatRuntime` + 分栏壳 + 侧栏联动 | ✅ 完成 | | Phase 2 | `write_file` 自动/点击打开 + 虚拟 URL 预览 | ✅ 完成 | | Phase 3 | `onUpstreamFrame` 注入完整 LangGraph messages | ✅ 完成(双通道兜底) | | Phase 4 | `present_files` 卡片 + Step 3 文件区衔接 | ⚠️ 卡片完成;Step 3 衔接遗留 | | Phase 5 | 草稿 stub 持久化 + 文档更新 | ✅ stub 完成;本文档同步更新 | --- ## 5. 测试清单 在 `/page/workspace/roundtable/planning` Step 2 下逐项勾选: - [ ] 子智能体 `write_file` 进入 `tool_calling` 后,右侧沙箱 **自动** 打开(约 100ms 延迟,对齐主聊天) - [ ] 沙箱标题为文件名(如 `sample-paper.md`),**不是** `{"detail":"Not Found"}` - [ ] 流式期间 MD 内容随 `args.content` 更新(双通道:phase 切换帧 + 兜底帧) - [ ] 点击 ChainOfThought 步骤「写入文件」/ 路径 chip → 手动打开同一文件 - [ ] `present_files` 后气泡下方出现文件卡(带扩展名徽章),点击 → HTTP 预览 - [ ] 沙箱打开时左侧「意图理解」栏 **自动收起** - [ ] 关闭沙箱(X)后对话区恢复全宽 - [ ] 「新建任务」后 artifacts / 沙箱 / `seatStubMessages` 全部清空 - [ ] 「保存草稿」→「加载草稿」后,点击历史 `write_file` 步骤可立即预览(stub 已还原) - [ ] 与主聊天 `/page/workspace/chats/:threadId` 对比:同一 write_file 行为一致 **调试**:DevTools Console 过滤 `[roundtable-sandbox]` 或 `[stream:special:...]`。 --- ## 6. 关键代码索引 | 用途 | 路径 | |------|------| | 主聊天 write_file 虚拟 URL + 自动打开 | `src/components/workspace/messages/message-group.tsx` | | 主聊天 `RichFileCard`(present_files) | `src/components/workspace/messages/message-list-item.tsx` | | 主聊天 60/40 分栏 | `src/components/workspace/chats/chat-box.tsx` | | 虚拟 URL 内容加载 | `src/core/artifacts/loader.ts` → `loadArtifactContentFromToolCall` | | Artifacts 上下文 | `src/components/workspace/artifacts/context.tsx` | | Step 2 编排 + 沙箱状态 + 草稿快照 | `src/roundtable-planning/hooks/useStep2Orchestration.ts` | | Step 2 沙箱布局 | `src/roundtable-planning/components/Step2SandboxLayout.tsx` | | Step 2 文件卡 + 步骤渲染 | `src/roundtable-planning/components/Step2Panel.tsx` | | SSE phase 推断 + `onUpstreamFrame` | `src/roundtable-planning/api/multi-agent.ts` → `setPhase` | | 主页面侧栏 + 联动 | `src/roundtable-planning/pages/RoundtablePlanningPage.tsx` | | 草稿持久化 | `src/roundtable-planning/utils/drafts.ts`、`hooks/useDraftPersistence.ts` | --- ## 7. 下一步建议(给接续开发者) 1. **先跑通 P0 测试清单**,确认 Phase 3/4/5 改造后无回归;重点观察双通道并发写入 stub 时 args 是否被旧帧覆盖(理论上 LangGraph 完整 tool_calls 是单调累积的,应该没问题)。 2. **Phase 4 收尾**:把 Step 3 的 `listSessionArtifacts` / `HighFidelityReport` 接进沙箱选中态——Step 3 进入时复用 Step 2 的 `ArtifactsProvider`,让用户在 Step 3 也能预览 Step 2 留下的产物。 3. **Phase 2 边界**:`str_replace` 多轮编辑的 stub 模拟(追加 tool result 消息让 `applyPendingTextEdit` 能 replay edit 序列),优先级低。 4. **更新** `multi-agent-frontend-dev.md`:Step 2 布局、沙箱架构、`seatStubMessages` 数据流,避免与本文档重复——可在总览文档加一节「见 `roundtable-step2-sandbox-progress.md`」。