|
| 1 | +# Sandbox 不可用时的对话降级设计 |
| 2 | + |
| 3 | +## 1. 背景 |
| 4 | + |
| 5 | +App Chat 的 Sandbox 可能因应用配置、团队套餐或系统配置变为不可用。当前不同入口的处理不一致:部分路径会跳过 Sandbox,部分路径会抛出 Agent 错误并中断对话,Sandbox 文件入口也无法向用户稳定说明不可用原因。 |
| 6 | + |
| 7 | +本需求统一 Sandbox 不可用时的运行行为和用户提示,使 Sandbox 从对话的强依赖降级为可选能力。 |
| 8 | + |
| 9 | +## 2. 需求范围 |
| 10 | + |
| 11 | +### 2.1 Sandbox 不可用原因 |
| 12 | + |
| 13 | +需要区分以下三种原因: |
| 14 | + |
| 15 | +1. `appDisabled`:应用开发者关闭 Sandbox。 |
| 16 | +2. `teamPlanUnavailable`:应用所属团队套餐不再提供 Sandbox 权限,包括套餐过期后降级到无 Sandbox 权限的套餐。 |
| 17 | +3. `systemDisabled`:系统未配置或已下架 Sandbox 功能。 |
| 18 | + |
| 19 | +### 2.2 对话运行行为 |
| 20 | + |
| 21 | +普通 App Chat 运行时,只要命中任意一种不可用原因: |
| 22 | + |
| 23 | +- 不向模型注入 Sandbox system tools。 |
| 24 | +- 不注入 Sandbox system prompt。 |
| 25 | +- 不创建、恢复、连接或调用 Sandbox runtime。 |
| 26 | +- 不执行 Sandbox entrypoint。 |
| 27 | +- 不再把 Sandbox 权限错误作为 Agent/ToolCall 节点错误写入对话。 |
| 28 | +- 其他模型调用、普通工具、知识库和工作流节点继续正常运行。 |
| 29 | +- 后端记录结构化日志,并能区分三种不可用原因。 |
| 30 | + |
| 31 | +### 2.3 前端行为 |
| 32 | + |
| 33 | +- 页面加载和正常对话过程中不主动弹出 Sandbox 不可用提示。 |
| 34 | +- 用户点击右上角虚拟机入口时才进行提示。 |
| 35 | +- 三种不可用原因使用统一 Toast 文案。 |
| 36 | +- Sandbox 可用时维持现有行为,正常打开 Sandbox 文件编辑器。 |
| 37 | + |
| 38 | +## 3. 当前实现差异 |
| 39 | + |
| 40 | +### 3.1 应用开发者关闭 |
| 41 | + |
| 42 | +Agent/ToolCall 节点的 `useAgentSandbox` 为 `false` 时,普通 Sandbox tool 和 runtime 已基本不会启用。但 Agent 节点配置了 Skill 时,会因为 Skill 的 runtime 依赖强制启用 Sandbox,不能完全满足“应用关闭后不调用 Sandbox”。 |
| 43 | + |
| 44 | +### 3.2 团队套餐无权限 |
| 45 | + |
| 46 | +- Agent 路径在 `prepareAgentSandboxRuntime` 中校验套餐,失败后由 Agent dispatch catch 转成节点错误。 |
| 47 | +- ToolCall 路径在 dispatch 开始阶段校验套餐并直接抛错。 |
| 48 | +- 两条路径都会阻断本轮正常对话。 |
| 49 | + |
| 50 | +### 3.3 系统关闭 |
| 51 | + |
| 52 | +- 普通 Sandbox 开关会因 `show_agent_sandbox=false` 被忽略。 |
| 53 | +- Agent 配置了 Skill 时会显式抛出 Sandbox 权限错误,阻断本轮对话。 |
| 54 | +- `checkExist` 直接返回 `exists=false`,前端无法区分系统关闭和实例不存在。 |
| 55 | + |
| 56 | +## 4. 已确认验收标准 |
| 57 | + |
| 58 | +1. 三种 Sandbox 不可用状态均不导致对话报错或中断。 |
| 59 | +2. 三种状态均不向模型暴露 Sandbox 工具,也不发生 Sandbox runtime 调用。 |
| 60 | +3. 后端日志能通过稳定的原因字段区分三种状态。 |
| 61 | +4. 前端只在用户点击虚拟机入口时显示 Toast。 |
| 62 | +5. Toast 文案对三种状态保持一致。 |
| 63 | +6. Sandbox 可用时不改变现有对话和文件编辑器行为。 |
| 64 | + |
| 65 | +## 5. 已确认产品决策 |
| 66 | + |
| 67 | +1. 简体中文 Toast 文案:`虚拟机功能已被关闭,暂时无法使用`。 |
| 68 | +2. Sandbox 不可用时,同时跳过依赖 Sandbox 的所有 Skill 和 Sandbox system tools。 |
| 69 | +3. 虚拟机入口保持原有显示逻辑,不因为本需求新增常驻入口: |
| 70 | + - 后端确认 Sandbox 实例存在;或 |
| 71 | + - 当前已加载对话历史中出现过 Sandbox 调用。 |
| 72 | +4. 静默降级覆盖普通 App Chat,包括 Workflow ToolCall 和 Agent v2/agent loop。 |
| 73 | +5. Skill 编辑和 Skill 调试继续保留现有强依赖与阻断行为。 |
| 74 | +6. 英文文案按简体中文语义翻译为:`The virtual machine feature has been disabled and is temporarily unavailable.` |
| 75 | +7. 繁体中文文案按简体中文语义翻译为:`虛擬機功能已被關閉,暫時無法使用`。 |
| 76 | + |
| 77 | +## 6. 开发设计 |
| 78 | + |
| 79 | +### 6.1 统一状态模型 |
| 80 | + |
| 81 | +在 Global Sandbox 类型中增加稳定的不可用原因枚举: |
| 82 | + |
| 83 | +```typescript |
| 84 | +type SandboxUnavailableReason = |
| 85 | + | 'appDisabled' |
| 86 | + | 'teamPlanUnavailable' |
| 87 | + | 'systemDisabled'; |
| 88 | +``` |
| 89 | + |
| 90 | +`undefined` 表示 Sandbox 可用。不可用原因只描述产品配置和权限状态,不混入 provider 故障、初始化失败、文件错误等运行时异常。 |
| 91 | + |
| 92 | +后端普通 App Chat 的判定顺序为: |
| 93 | + |
| 94 | +1. 系统未启用 Sandbox:`systemDisabled`。 |
| 95 | +2. 当前 Agent/ToolCall 节点未开启 Sandbox:`appDisabled`。 |
| 96 | +3. 应用所属团队无 Sandbox 套餐权限:`teamPlanUnavailable`。 |
| 97 | +4. 以上均未命中:Sandbox 可用。 |
| 98 | + |
| 99 | +这样可以在系统下架时避免无意义的套餐查询,在应用关闭时也不会为每次对话额外查询套餐。 |
| 100 | + |
| 101 | +### 6.2 后端统一降级入口 |
| 102 | + |
| 103 | +在 Sandbox application 层提供普通 App Chat 专用的可用性判定,调用方传入: |
| 104 | + |
| 105 | +- 节点是否开启 Sandbox; |
| 106 | +- 应用所属 `teamId`。 |
| 107 | + |
| 108 | +返回不可用原因后,Agent 和 ToolCall 只决定是否组装 Sandbox 能力,不通过异常表达三种关闭状态。可用性判定函数在返回关闭状态时直接记录固定事件名和唯一的 `reason` 字段: |
| 109 | + |
| 110 | +```text |
| 111 | +reason |
| 112 | +``` |
| 113 | + |
| 114 | +三种关闭状态属于可控降级,使用 `info` 级别;套餐查询本身出现异常时统一降级为 `teamPlanUnavailable`,保证对话可继续。调用方不再负责日志,也不暴露额外日志 helper。 |
| 115 | + |
| 116 | +Sandbox 文件 API 不能只依赖前端状态查询。服务端增加统一的 Session 访问编排层: |
| 117 | + |
| 118 | +- `authSandboxSession` 只负责 Chat/App/Skill 的资源访问鉴权,不再混入套餐校验开关。 |
| 119 | +- 普通 App Session 在资源鉴权后统一解析 `appDisabled/teamPlanUnavailable/systemDisabled`;不可用时拒绝 Ticket、上传、下载和预览请求,且不能创建、恢复或连接 Sandbox。 |
| 120 | +- Skill Edit Session 使用明确的强可用性断言,系统关闭或团队无权限时继续抛出结构化 Sandbox 权限错误。 |
| 121 | +- Agent runtime 的底层准备函数只负责实例和路径准备,不再暴露 `checkTeamPermission` 布尔绕过参数;App 静默降级和 Skill Edit 强校验都在进入 runtime 前完成。 |
| 122 | + |
| 123 | +文件/runtime API 与对话运行共用同一个拒绝日志,只输出稳定的 `reason` 字段,不透传 API 操作或资源身份等额外上下文。 |
| 124 | + |
| 125 | +### 6.3 Agent v2 / agent loop |
| 126 | + |
| 127 | +`dispatchRunAgent` 对普通 App Chat 使用统一判定结果: |
| 128 | + |
| 129 | +- 可用:维持现有 Sandbox runtime、entrypoint、Skill 注入和初始化流程。 |
| 130 | +- 不可用: |
| 131 | + - `effectiveUseAgentSandbox=false`; |
| 132 | + - 不调用 `ensureAppSandboxRuntimeReady`; |
| 133 | + - 不发送 Sandbox 初始化 SSE; |
| 134 | + - `ensureAgentSandboxRuntime` 直接返回空 `skillInfos`,不创建或连接 Sandbox; |
| 135 | + - 不传 `sandboxClient`,因此 agent loop 不注入 Sandbox system tools; |
| 136 | + - 不拼接 `SANDBOX_SYSTEM_PROMPT`; |
| 137 | + - 忽略本轮 `selectedSkills`、`skillIds` 和 Sandbox entrypoint; |
| 138 | + - 普通工具、数据集工具、plan/ask 和模型调用保持不变。 |
| 139 | + |
| 140 | +Skill Edit 通过 `sourceType=skillEdit` 保持原路径,不走静默降级。 |
| 141 | + |
| 142 | +### 6.4 Workflow ToolCall |
| 143 | + |
| 144 | +`dispatchRunTools` 对普通 App Chat使用同一判定: |
| 145 | + |
| 146 | +- 移除当前套餐失败时直接抛出 `agentSandboxPermissionDenied` 的行为。 |
| 147 | +- 不可用时把运行参数中的 `useAgentSandbox` 归一化为 `false`。 |
| 148 | +- 不准备 Sandbox runtime,不注入输入文件,不执行 entrypoint。 |
| 149 | +- ToolCall 的 agent loop 不接收 `sandboxClient`,因此不注入 Sandbox system tools。 |
| 150 | +- `useToolCatalog` 不拼接 Sandbox system prompt。 |
| 151 | +- 工作流配置的其他工具节点照常进入工具目录和执行。 |
| 152 | + |
| 153 | +### 6.5 `checkExist` 状态扩展 |
| 154 | + |
| 155 | +扩展 `/core/ai/sandbox/checkExist` 响应: |
| 156 | + |
| 157 | +```typescript |
| 158 | +{ |
| 159 | + exists: boolean; |
| 160 | + unavailableReason?: SandboxUnavailableReason; |
| 161 | +} |
| 162 | +``` |
| 163 | + |
| 164 | +接口继续使用 `parseApiInput` 校验请求,并使用响应 Schema 校验返回值。为返回套餐不可用原因,`checkExist` 只做 Chat/App 读取鉴权,不在鉴权 helper 内提前抛 Sandbox 套餐错误,再显式执行状态判定。 |
| 165 | + |
| 166 | +为保持原有入口显示逻辑: |
| 167 | + |
| 168 | +- 三种关闭状态与可用状态都从本地实例表返回真实 `exists`,关闭原因只通过 `unavailableReason` 表达。 |
| 169 | +- 本地实例查询不会创建、恢复或连接远端 Sandbox。 |
| 170 | +- Skill Edit:沿用现有强权限校验,不返回普通 App Chat 的静默降级原因。 |
| 171 | + |
| 172 | +应用是否开启 Sandbox 根据实际会话类型计算: |
| 173 | + |
| 174 | +- 线上、分享等普通 App Chat 使用当前已发布版本工作流。 |
| 175 | +- App Chat Test 运行的是请求携带的当前编辑态节点,不能读取已发布版本代替。测试会话在每轮运行后保存本轮 Sandbox 开关状态,状态查询和文件 API 使用该服务端会话状态;旧测试会话再回退到 App 草稿节点。 |
| 176 | +- 任意 Agent/ToolCall 节点的 `useAgentSandbox` 为 `true`,即视为该会话开启 Sandbox。 |
| 177 | + |
| 178 | +### 6.6 前端点击拦截 |
| 179 | + |
| 180 | +`useSandboxStatus` 在原有 `sandboxExists` 状态外保存当前 target/chat 对应的 `unavailableReason`: |
| 181 | + |
| 182 | +- 按钮是否渲染仍只取决于原有 `sandboxExists` 公式。 |
| 183 | +- 页面加载、状态请求完成、对话生成时均不弹 Toast。 |
| 184 | +- 桌面端 `SandboxEntryIcon` 点击时: |
| 185 | + - 每次点击都重新查询服务端状态,不使用页面加载时的缓存状态作为最终结论;请求失败时维持原有打开行为,由文件 API 做服务端兜底。 |
| 186 | + - 有 `unavailableReason`:显示统一 warning Toast,不打开编辑器; |
| 187 | + - 无不可用原因:正常打开编辑器。 |
| 188 | +- 移动端 ToolMenu 使用同一个点击 guard,避免绕过桌面按钮组件直接打开编辑器。 |
| 189 | +- AI 回复气泡底部的虚拟机入口也使用同一个点击 guard,不允许直接打开编辑器。 |
| 190 | +- 如果入口已由历史记录显示、但状态请求尚未完成,点击时主动等待或补发一次状态请求,再决定提示或打开,避免竞态下误调用 `getTicket`。 |
| 191 | + |
| 192 | +### 6.7 国际化 |
| 193 | + |
| 194 | +在 `chat.json` 增加统一 key,例如 `sandbox_unavailable_toast`: |
| 195 | + |
| 196 | +- `zh-CN`: `虚拟机功能已被关闭,暂时无法使用` |
| 197 | +- `zh-Hant`: `虛擬機功能已被關閉,暫時無法使用` |
| 198 | +- `en`: `The virtual machine feature has been disabled and is temporarily unavailable.` |
| 199 | + |
| 200 | +### 6.8 测试策略 |
| 201 | + |
| 202 | +后端局部测试: |
| 203 | + |
| 204 | +- 可用性判定覆盖系统关闭、应用关闭、套餐无权限和正常可用。 |
| 205 | +- Agent v2 在三种关闭状态下继续完成模型调用,不创建 Sandbox、不注入 Sandbox tools/prompt、不注入 Skill。 |
| 206 | +- Workflow ToolCall 在三种关闭状态下继续运行普通工具,不创建 Sandbox、不注入 Sandbox tools/prompt。 |
| 207 | +- Skill Edit 在系统/套餐不可用时仍保持原有阻断行为。 |
| 208 | +- `checkExist` 覆盖三种原因、实例存在性和读取鉴权。 |
| 209 | + |
| 210 | +最后按仓库要求执行全量测试。 |
| 211 | + |
| 212 | +## 7. TODO |
| 213 | + |
| 214 | +- [x] 增加 Sandbox 不可用原因枚举、统一可用性判定和结构化日志。 |
| 215 | +- [x] 改造 Agent v2:普通 App Chat 不可用时跳过 runtime、prompt、tools、Skills 和 entrypoint。 |
| 216 | +- [x] 改造 Workflow ToolCall:普通 App Chat 不可用时继续运行其他工具。 |
| 217 | +- [x] 扩展 `checkExist` 返回不可用原因,并保持虚拟机入口原有显示逻辑。 |
| 218 | +- [x] 前端桌面端和移动端统一增加点击 guard 与 Toast,不在其他时机提示。 |
| 219 | +- [x] 增加简体中文、繁体中文和英文文案。 |
| 220 | +- [x] 更新三类关闭状态、Skill Edit 强依赖和状态接口的局部测试。 |
| 221 | +- [ ] 清理被替代的异常分支、导入和旧测试,运行全量测试。 |
| 222 | +- [x] 增加 Sandbox Session 服务端访问兜底,覆盖 Ticket、上传、下载和预览。 |
| 223 | +- [x] 移除 `authSandboxSession` 和 Agent runtime 的 `checkTeamPermission` 布尔绕过参数。 |
| 224 | +- [x] 区分线上发布版本与 App Chat Test 编辑态的 Sandbox 开关来源。 |
| 225 | +- [x] 所有虚拟机入口统一点击守卫,并在每次点击时刷新状态。 |
| 226 | +- [x] 补充服务端访问和编辑调试版本回归测试。 |
0 commit comments