|
| 1 | +# OpenSandbox Adapter 生命周期与 SDK 适配重构 |
| 2 | + |
| 3 | +状态:已实现 |
| 4 | + |
| 5 | +最后核对:2026-07-23 |
| 6 | + |
| 7 | +## 1. 背景与目标 |
| 8 | + |
| 9 | +FastGPT 会在 Sandbox 闲置 5 分钟后调用 provider adapter 的 `stop()`。当前 |
| 10 | +`OpenSandboxAdapter.stop()` 删除远端 sandbox,导致下次使用时只能重新创建,无法保留 |
| 11 | +容器状态。本次调整要求 OpenSandbox 的 `stop()` 使用 pause,并通过 resume 复用原实例。 |
| 12 | + |
| 13 | +同时完整核对 `OpenSandboxAdapter` 与当前 OpenSandbox JavaScript SDK 的实现,处理生命周期 |
| 14 | +状态、HTTP agent 释放、readiness、命令输出和测试职责中已经存在的问题。 |
| 15 | + |
| 16 | +## 2. 范围 |
| 17 | + |
| 18 | +- `OpenSandboxAdapter` 的 create/connect/ensure/start/stop/delete/getInfo/close 生命周期。 |
| 19 | +- OpenSandbox SDK 的 ConnectionConfig、Sandbox、SandboxManager、readiness 和命令执行约定。 |
| 20 | +- 基于 FastGPT `sandboxId` 的远端实例复用,以及同一 adapter 内的 SDK client 复用。 |
| 21 | +- OpenSandbox adapter 单元测试、上传恢复 helper 测试和 OpenSandbox 集成测试。 |
| 22 | +- OpenSandbox SDK 依赖与锁文件版本。 |
| 23 | + |
| 24 | +不调整 FastGPT application 层的 5 分钟 cron、Mongo 生命周期状态机和其他 sandbox provider。 |
| 25 | + |
| 26 | +## 3. 当前实现审查 |
| 27 | + |
| 28 | +### 3.1 已有 reuse |
| 29 | + |
| 30 | +当前已经存在远端资源复用,但不是 SDK 内置的 pool: |
| 31 | + |
| 32 | +1. FastGPT 把稳定的业务 `sandboxId` 作为 adapter `sessionId`。 |
| 33 | +2. 创建 OpenSandbox 时写入 `metadata.sessionId`。 |
| 34 | +3. `ensureRunning()` 通过 `SandboxManager.listSandboxInfos()` 按 metadata 查找远端实例。 |
| 35 | +4. 找到运行中实例时 connect,找到暂停实例时 resume,不存在时才 create。 |
| 36 | + |
| 37 | +因此本次不新增另一套资源池。需要保留并强化 metadata 寻址,使 pause 后仍能找到同一个远端 |
| 38 | +sandbox。 |
| 39 | + |
| 40 | +### 3.2 本地 SDK client 未复用 |
| 41 | + |
| 42 | +`ensureRunning()` 在远端状态为 Running 时始终调用 `Sandbox.connect()`,即使 adapter 已经绑定 |
| 43 | +同一个 `Sandbox`。SDK 的每个静态 create/connect 和 SandboxManager 都可能持有独立的 undici |
| 44 | +agent;旧 `Sandbox` 没有 close 就被覆盖,会泄漏 client 资源。 |
| 45 | + |
| 46 | +目标行为: |
| 47 | + |
| 48 | +- 已绑定同一个 Running sandbox 时直接复用,不重新 connect。 |
| 49 | +- 静态 create/connect 替换旧实例时释放旧实例。 |
| 50 | +- instance resume 返回的新对象复用原 connection transport,不重复关闭共享 transport。 |
| 51 | +- `close()` 幂等释放并解除绑定。 |
| 52 | + |
| 53 | +### 3.3 SandboxManager 生命周期过碎 |
| 54 | + |
| 55 | +删除等待会每秒创建并关闭一个 SandboxManager。每个 manager 都拥有独立 transport,这会产生 |
| 56 | +不必要的 agent churn。 |
| 57 | + |
| 58 | +目标行为是在一次顶层 lifecycle 操作内只创建一个 manager,查找、pause/kill 和轮询共用它, |
| 59 | +并在 `finally` 中关闭。manager 不提升为 adapter 长生命周期字段,因为资源 cron 构造临时 adapter |
| 60 | +后只调用 stop/delete,不保证额外调用 close。 |
| 61 | + |
| 62 | +### 3.4 生命周期状态映射不完整 |
| 63 | + |
| 64 | +当前映射缺少 OpenSandbox 的 `Pending`、`Pausing`、`Resuming`、`Terminated` 和 `Failed`。 |
| 65 | +尤其是 pause API 返回 202 后会经历 `Running -> Pausing -> Paused`;如果 stop 在 202 后立即返回, |
| 66 | +FastGPT 会提前把本地记录标成 stopped,紧接着 resume 也可能因为仍在 Pausing 而失败。 |
| 67 | + |
| 68 | +目标映射: |
| 69 | + |
| 70 | +| OpenSandbox | Adapter | |
| 71 | +| --- | --- | |
| 72 | +| Pending / Creating | Creating | |
| 73 | +| Running | Running | |
| 74 | +| Resuming / Starting | Starting | |
| 75 | +| Pausing / Stopping | Stopping | |
| 76 | +| Paused / Stopped | Stopped | |
| 77 | +| Deleting | Deleting | |
| 78 | +| Deleted / Terminated | UnExist | |
| 79 | +| Error / Failed / unknown | Error | |
| 80 | + |
| 81 | +`stop()` 必须等到 Paused 或资源已不存在才返回;遇到 Pausing 时只等待,不重复发 pause。恢复时 |
| 82 | +遇到 Pausing 先等 Paused,再发 resume。 |
| 83 | + |
| 84 | +### 3.5 未遵循当前 SDK 约定的实现 |
| 85 | + |
| 86 | +- create 和 start 在 SDK 内置 readiness 之后又调用 adapter `waitUntilReady()`,重复轮询,并让 |
| 87 | + `skipHealthCheck` 失效。 |
| 88 | +- adapter 已有 ping 失败时用命令兜底的需求,但没有通过 SDK 的 custom `healthCheck` 接口接入。 |
| 89 | +- bound stop/delete 仍通过额外的 SandboxManager 操作,没有使用 `sandbox.pause()` / `kill()`; |
| 90 | + 当前 SDK 会在这两个 instance 方法中同步失效 endpoint cache。 |
| 91 | +- `getInfo()` 为读取生命周期信息强制 connect execd,导致 Paused sandbox 被错误返回为不存在。 |
| 92 | +- 命令 exit code 忽略 SDK 已提供的 `execution.exitCode`。 |
| 93 | +- SDK 0.1.10 支持 handler `skipAccumulation`;adapter 已自行使用有界 buffer 时应禁用 SDK 的重复 |
| 94 | + 日志累积,避免长输出双份占用内存。 |
| 95 | +- OpenSandbox metadata 实际要求 `Record<string, string>`,当前 provider 类型和透传中仍有 |
| 96 | + `any` / `unknown` 未归一化。 |
| 97 | + |
| 98 | +## 4. 开发设计 |
| 99 | + |
| 100 | +### 4.1 Manager 作用域 |
| 101 | + |
| 102 | +增加内部 `withSandboxManager()`:每次顶层管理操作创建一个 manager,通过 callback 复用, |
| 103 | +最后无条件 close。查找、按 id 获取状态、pause、kill 和状态轮询都接收同一个 manager。 |
| 104 | + |
| 105 | +### 4.2 远端状态等待 |
| 106 | + |
| 107 | +增加统一状态轮询 helper,输入 sandbox id、期望 adapter 状态和 info getter: |
| 108 | + |
| 109 | +- 404 视为 `UnExist`。 |
| 110 | +- 到达期望状态后返回最新 info。 |
| 111 | +- Error 立即抛出 ConnectionError。 |
| 112 | +- 超时抛出 SandboxStateError,并保留当前状态和期望状态。 |
| 113 | + |
| 114 | +stop 使用 `Stopped | UnExist` 作为完成条件。ensure/start 对 Pausing 使用 `Stopped`,对 |
| 115 | +Creating/Resuming 使用 `Running`。 |
| 116 | + |
| 117 | +### 4.3 生命周期操作 |
| 118 | + |
| 119 | +- `create()`:使用 `Sandbox.create()`,metadata 归一化为字符串,接入 custom health check;不再 |
| 120 | + 额外调用 adapter readiness。 |
| 121 | +- `connect()`:同 id 已绑定时直接返回;新连接成功后替换绑定并关闭旧的独立 Sandbox client。 |
| 122 | +- `ensureRunning()`:优先读取已绑定 sandbox 的 info;未绑定时按 `metadata.sessionId` 查找。 |
| 123 | + Running 复用、Paused resume、Pausing 等待后 resume、Creating/Resuming 等待后 connect、Deleting |
| 124 | + 按 `allowCreate` 决定等待重建或报错。 |
| 125 | +- `start()`:复用 ensure 语义但禁止创建;bound sandbox 使用 instance resume。 |
| 126 | +- `stop()`:bound sandbox 使用 `sandbox.pause()`,unbound sandbox 使用 manager pause;等待 Paused |
| 127 | + 后返回,不清除绑定。 |
| 128 | +- `delete()`:bound target 使用 `sandbox.kill()`,unbound/显式其他 id 使用 manager kill;删除后 |
| 129 | + close 并清除匹配绑定。 |
| 130 | +- `getInfo()`:bound 时用 `sandbox.getInfo()`;unbound 时直接用 manager 的 lifecycle info,不为 |
| 131 | + Paused sandbox 建立 execd 连接。 |
| 132 | +- `close()`:未绑定时也成功,释放后清除本地引用,不改变远端状态。 |
| 133 | + |
| 134 | +### 4.4 Readiness 与命令输出 |
| 135 | + |
| 136 | +把 `/ping` + `true` 命令兜底封装为接收明确 Sandbox 参数的 health check,传给 SDK 的 |
| 137 | +create/connect/static resume。instance resume 先跳过默认检查,再调用 SDK `waitUntilReady()` 并传入 |
| 138 | +同一个 custom health check,保证只有一套轮询。 |
| 139 | + |
| 140 | +命令结果优先使用 `execution.exitCode`,旧服务没有返回时才解析 error value/traceback。execute 和 |
| 141 | +executeStream handler 开启 `skipAccumulation`,adapter 继续用 `BoundedOutputBuffer` 控制输出上限。 |
| 142 | + |
| 143 | +### 4.5 类型与测试收敛 |
| 144 | + |
| 145 | +- touched provider config 使用 `type`,移除 `any`。 |
| 146 | +- `OpenSandboxConfigType` 从共享 create spec 派生,只收窄 image、metadata、volumes。 |
| 147 | +- Adapter 测试使用统一 SDK sandbox/manager factory,移除访问真实保留端口的伪单测。 |
| 148 | +- 错误类自身行为只留在 errors 测试,不在 adapter 测试重复验证。 |
| 149 | +- uploadRecovery 的纯逻辑留在专属测试;adapter 文件只验证一次上传失败恢复的 wiring,不重复每个 |
| 150 | + helper 分支。 |
| 151 | +- 增加 pause 完成等待、Pausing 后 resume、bound Running client 复用、paused getInfo、幂等 close、 |
| 152 | + transport 替换释放和 SDK exitCode 测试。 |
| 153 | +- 集成测试恢复 stop/start contract,并修正 `timeout` 为 `timeoutSeconds`。 |
| 154 | + |
| 155 | +## 5. 验收标准 |
| 156 | + |
| 157 | +1. 5 分钟闲置 stop 对 OpenSandbox 发 pause,不发 delete。 |
| 158 | +2. stop 仅在 Paused/不存在后完成,本地 stopped 不领先于远端状态。 |
| 159 | +3. 后续 ensure/start 恢复同一个 OpenSandbox id,工作区状态保留。 |
| 160 | +4. 同一 adapter 重复 ensure Running 不创建新的 SDK Sandbox client。 |
| 161 | +5. 所有临时 SandboxManager 和被替换/断开的独立 Sandbox client 都会释放 transport。 |
| 162 | +6. Paused sandbox 的 getInfo 可正常返回,不依赖 execd connect。 |
| 163 | +7. OpenSandbox 单元测试、adapter build 和格式检查通过;按用户要求不运行仓库全量测试。 |
| 164 | + |
| 165 | +## 6. TODO |
| 166 | + |
| 167 | +- [x] 完整审查 OpenSandboxAdapter、调用方、现有测试和当前 OpenSandbox SDK。 |
| 168 | +- [x] 明确已有 metadata 远端复用和缺失的本地 client 复用。 |
| 169 | +- [x] 重构状态映射、manager 作用域和 lifecycle 轮询。 |
| 170 | +- [x] 改造 create/connect/ensure/start/stop/delete/getInfo/close。 |
| 171 | +- [x] 接入 SDK custom health check、exitCode 和 skipAccumulation。 |
| 172 | +- [x] 收敛 provider 类型和 upload recovery 类型。 |
| 173 | +- [x] 清理并补充 OpenSandbox adapter 单元测试和集成测试配置。 |
| 174 | +- [x] 更新 OpenSandbox SDK 依赖及 lockfile(已确认只支持 0.1.10,不保留 0.1.6 兼容)。 |
| 175 | +- [x] 只运行 sandbox-adapter 定向测试、build、格式与 diff 检查。 |
0 commit comments