跳到主要内容

顶层全景:控制中心、沙箱、agent-server

这章给你「大盘」:三个角色、它们怎么连、一条会话怎么从你点「开始」一路流到 agent 真正动手。

1. 三个角色

OpenHands 控制中心运转时有三层,分清楚它们是读懂全仓库的关键:

角色是谁住在哪职责
控制中心(control plane)本仓库的 openhands/app_server你的机器 / 服务器编排:开沙箱、克隆 repo、拼会话请求、转发事件、存元数据
沙箱(sandbox)一个 Docker 容器(或远程 / 进程)隔离环境给 agent 一个有权限但被关起来的执行环境
agent-server外部包 openhands-agent-server跑在沙箱内真正的 agent 循环:想—调工具—观察、改文件、跑命令

一句话:控制中心负责「安排」,agent-server 负责「干活」,沙箱是干活的笼子。 控制中心和 agent-server 之间走 HTTP——这就是为什么仓库里到处是 httpx_client.post(f'{agent_server_url}/api/conversations', ...) 这样的调用。

2. 一张顶层图

怎么读这张图:从上到下是一次「开始会话」的控制流;虚线是事后异步的事件 / 自动化回流。

你 / 浏览器 / Slack / GitHub webhook
│ POST /api/v1/conversations

┌───────────────────────────────────────────────┐
│ 控制中心 app_server (FastAPI) │
│ │
│ v1_router ──► AppConversationService │
│ │ │
│ ① 要一个沙箱 │ ② 拼会话请求 │
│ ▼ │
│ SandboxService ───────────┐ │
└───────────────────────┬───────────────────┼─────┘
│ 启动/等待 │ HTTP
▼ ▼
┌─────────────────────────────────────────┐
│ 沙箱 (Docker 容器) │
│ │
│ 暴露端口: │
│ AGENT_SERVER ──► 真正的 agent 循环 │
│ VSCODE ──► 浏览器里的编辑器 │
│ │
│ 门票:session_api_key │
└─────────────────────────────────────────┘

┊ ③ agent 产生事件,webhook 回流

EventCallbackProcessor(改标题 / 触发自动化…)

部件一句话职责:

部件干什么在哪个文件
v1_router把所有 REST 路由挂在 /api/v1openhands/app_server/v1_router.py
AppConversationService会话的 CRUD + 启动编排app_conversation/app_conversation_service.py
LiveStatusAppConversationService启动主线的具体实现(本仓库最核心)app_conversation/live_status_app_conversation_service.py
SandboxService沙箱生命周期(开 / 等 / 停 / 删)sandbox/sandbox_service.py
AsyncRemoteWorkspace(SDK)对沙箱里 agent-server 的远程文件 / 命令客户端外部包 openhands.sdk.workspace.remote
EventCallbackProcessor事件触发的自动化钩子event_callback/event_callback_models.py

3. 控制中心怎么装起来

它是个标准 FastAPI 应用。app.py 做三件事:挂 MCP 子应用、挂 v1 路由、加中间件:

# openhands/app_server/app.py(节选)
app = FastAPI(title='OpenHands', ...)
app.include_router(v1_router.router) # 所有 /api/v1/... 业务路由
app.include_router(health_router) # 健康检查
app.add_middleware(LocalhostCORSMiddleware)
app.add_middleware(RateLimitMiddleware, rate_limiter=InMemoryRateLimiter(...))

所有业务路由集中在一个文件里挂载,一眼能看全控制中心对外暴露的能力面:

# openhands/app_server/v1_router.py(节选)
router = APIRouter(prefix='/api/v1')
router.include_router(event_router.router) # 事件存取 / 流
router.include_router(app_conversation_router.router) # 会话
router.include_router(sandbox_router.router) # 沙箱
router.include_router(settings_router) # 设置
router.include_router(secrets_router) # 密钥
router.include_router(skills_router.router) # 技能
router.include_router(webhook_router.router) # webhook(自动化入口)
router.include_router(git_router) # git 集成

参见 openhands/app_server/v1_router.py:21-37(router)。这张表基本就是 OpenHands 控制中心的「功能目录」。

4. 主线走一遍(高层,不进代码)

一次「让 agent 改我的 repo」端到端是这样:

  1. 请求进来 → 命中 AppConversationRouter,转交 AppConversationService.start_app_conversation(...)
  2. 要沙箱 → 服务向 SandboxService 要一个运行中的沙箱(没有就开一个 Docker 容器),并等它真的 ready(轮询状态 + 打 agent-server 的 /alive)。见 sandbox/sandbox_service.py:93(wait_for_sandbox_running)。
  3. 拿到 agent-server 地址 → 从沙箱暴露的 URL 里挑出名字叫 AGENT_SERVER 的那个。见 live_status_app_conversation_service.py:1026(_get_agent_server_url)。
  4. 准备工作区 → 用 AsyncRemoteWorkspace 这个远程客户端,在沙箱里克隆你选的 repo、跑 .openhands/setup.sh、装 pre-commit hook、加载技能。(细节见 02-conversation-lifecycle.md)
  5. 拼会话请求 → 把 LLM 配置、初始消息、技能、安全策略组装成一个 StartConversationRequest(SDK 的类型)。
  6. POST 给 agent-serverhttpx_client.post(f'{agent_server_url}/api/conversations', ...)。从这一刻起,agent 循环在沙箱里跑起来了,控制中心退居「转发 + 记账」。见 live_status_app_conversation_service.py:460
  7. 事件回流 → agent 产生的事件通过 webhook / 事件流回到控制中心,可触发 EventCallbackProcessor(比如自动改会话标题)。

记住这条主线,后面每一章都是在放大其中一步:第 2 章放大 4–6 步,第 3 章放大第 2 步,第 4 章放大第 7 步与「换后端」的可插拔机制。

5. 这层架构「妙」在哪

  • 控制面 / 数据面分离。 控制中心从不直接执行 agent 的危险动作;所有「跑命令、改文件」都发生在沙箱内的 agent-server。控制中心只发 HTTP 指令。安全边界天然清晰(详见 03-sandbox.md)。
  • agent 可替换。 因为耦合点只是「向 agent-server 发会话请求」这条 HTTP 协议,所以同一套控制中心能驱动 OpenHands 自家 agent,也能驱动任何讲 ACP 的第三方 agent(Claude Code / Codex…)。代码里能看到 agent_kind == 'acp' 的分支(live_status_app_conversation_service.py:489)。
  • 一切皆服务 + 注入。 沙箱、事件、会话信息……每个子系统都是一个抽象基类(SandboxServiceEventService…)+ 多个实现(Docker / 远程 / 文件系统 / SQL),靠 Injector 在配置里选实现。这就是「换后端不改业务代码」的来源(详见 04-extensibility.md)。