跳到主要内容

数据截至 (上游 commit 1916c9046c4e)

OpenHands (Agent Canvas) — 架构与原理

30 秒导读: 这个仓库是 Agent Canvas——一个自托管的「编码 agent 控制台」。它本身不实现 agent 的「想—调工具—观察」循环:真正干活的 Agent Server 是一个独立的 pip 包(openhands-agent-server,源码在另一个仓库 software-agent-sdk),由本仓库的启动器用 uvx 拉起来。本仓库做的是控制台该做的一切:React 前端、多后端切换、会话编排、实时事件渲染、技能/插件/MCP 配置、自动化管理。npm 包名 @openhands/agent-canvas(package.json:2),一条命令 agent-canvas 起全套本地栈。

0. 先交代一次大 pivot(读旧资料前必看)

这个仓库已经转型。旧的「OpenHands 编码 agent 成品」架构(Python 后端 openhands/app_server、沙箱服务、FastAPI 控制中心)在本仓库里已经不存在了——克隆根下没有 openhands/ Python 包,只有 TypeScript 源码。

pivot 前(旧)pivot 后(现在,本仓库)
仓库角色编码 agent 成品(含 agent 循环)Agent Canvas:编码 agent 控制台
agent 循环在哪本仓库 Python 代码外部 pip 包 openhands-agent-server(config/defaults.json:4 锁版本 1.42.1)
主要语言Python(FastAPI)TypeScript / React 19(package.json:49-50)
产物形态服务端应用npm 包 + Docker 镜像 + Electron 桌面壳(electron/main.mjs:1-8)

证据就在门面文件里:README.md:5 的标题是 Agent Canvas,自我定位是「The self-hosted developer control center for coding agents and automations」(README.md:7),能「Run OpenHands, Claude Code, Codex, Gemini, or any ACP-compatible agent across local, remote, and cloud backends」(README.md:10)。

这套文档全部按 pivot 后的现实重写。 旧架构内容已删除;凡涉及「agent 怎么思考」的问题,都不在本仓库范围。

1. 这是什么(零基础也能懂)

一句话定义: Agent Canvas 是一个控制台(console)——你在一个网页界面里发起、监视、管理编码 agent 的会话和自动化,agent 的实际执行发生在一个或多个后端(Agent Server)上,控制台通过 API 驱动它们。

解决谁的什么问题。 假设你想让 AI 帮你改代码,你可能会同时用好几个 agent(OpenHands、Claude Code、Codex……),还想让它们跑在不同地方(笔记本、家里一台 Mac Mini、公司服务器、云)。每个 agent、每台机器一套界面太累了。Agent Canvas 给你一个界面,后面挂多个「agent 后端」,随时切换(README.md:35 的原话:本地默认可跑,也能连 Docker 容器、VM、公司基础设施里的后端)。

它能做什么:

  • 起会话:选 agent、选工作区,开始一段编码对话,实时看事件流(思考、工具调用、文件diff、终端输出)。
  • 切后端:同一个前端连多台 Agent Server,本地/远程/云一键切换(README.md:60-61)。
  • 换 agent:内置 OpenHands agent,也能驱动任何讲 ACP(Agent Client Protocol,agent 与客户端之间的 JSON-RPC 标准) 的 agent(docs/ACP_AGENTS.md:9-16)。
  • 自动化:把 agent 挂到定时任务或 webhook 上(Slack、GitHub、Linear 等),由配套的 Automation 后端执行(README.md:135)。

用起来什么样(README.md:71-73 的 Quickstart):

npm install -g @openhands/agent-canvas
agent-canvas # 起全套本地栈:前端 + agent-server + automation + 统一入口

一句话直觉 / 类比。 把它想成编码 agent 的「塔台调度系统」:塔台的屏幕(React 前端)自己不飞,它通过无线电(REST + WebSocket)指挥各条跑道上的飞机(Agent Server 上的 agent),还能排班(自动化)。换飞机型号(换 agent)不用换塔台。

2. 一个关键事实:agent 循环不在这个仓库里

读这份源码最容易踩的坑,先说清楚。

docs/architecture.md:3 写得很直白:Agent Canvas 是一个 React/TypeScript 前端,直接对话 OpenHands Agent Server。同一份文件把边界划得更清楚(docs/architecture.md:14-19)——它不负责:

  • 直接执行 agent 动作;
  • 提供沙箱 / 工作区隔离层;
  • 在配置的后端之外托管 LLM 凭据;
  • 在没有 automation 后端时跑定时/事件触发的自动化。

那本仓库的代码到底是什么?docs/architecture.md:35-43 列了前端模块;概括成三句话:

  • UI 层:React Router 7 路由 + 组件(src/routes.ts:7-45 的路由表)。
  • 服务层:src/api/ 下的 service 模块(16 个 service 目录 + 7 个独立 service 文件,另有共享适配器/配置),是「UI 与后端 API 之间的抽象层」(src/api/README.md:5),绝大多数调用经由固定版本的 npm 包 @openhands/typescript-client(package.json:27)发给 Agent Server。
  • 启动器:bin/agent-canvas.mjs + scripts/ 下一组 Node 脚本,负责用 uvx 把外部 Agent Server / Automation 后端拉起来,并用一个 ingress 代理把三方流量统一到一个端口。

Agent Server 本身以 pip 包形式、按版本锁 被启动:config/defaults.json:4agentServer: 1.42.1,启动脚本用 uvx --from openhands-agent-server==1.42.1 拉它(scripts/dev-with-automation.mjs:913-914buildAgentServerCommand)。所以仓库里凡涉及「agent 怎么想、工具怎么执行」的行为,实现都在那个 pip 包里,不在这份源码里

3. 顶层全景(一屏版)

怎么读这张图:左到右是「你 → 控制台 → 外部后端」;实线是浏览器发出的流量,启动器进程只负责把右边的服务拉起来。

浏览器(React 前端,Agent Canvas 本体)
│ REST /api/* · WebSocket /sockets · /api/automation/*

ingress 代理(端口 8000,唯一入口)
│ 按路径前缀分流
├──────────────► Agent Server(uvx 起的 pip 包,:18000)
│ 会话 CRUD · 事件流 · 工具执行 · agent 循环在这
└──────────────► Automation 后端(uvx 起的 pip 包,:18001)
定时/webhook 触发 agent 跑任务
部件干什么在哪
React 前端全部 UI:会话、终端、浏览器、文件、设置、自动化src/routes.ts:7-45
服务层把 UI 动作翻译成后端 API 调用src/api/README.md:5
后端注册表登记/切换多个 Agent Server(local/cloud)src/api/backend-registry/types.ts:4-13
ingress 代理一个端口后面挂多个服务,按路径分流scripts/ingress.mjs:18-22
启动器拉起 agent-server + automation + 前端 + ingressscripts/dev-with-automation.mjs:1383 main
Agent Server(外部)真正跑 agent 的 REST 服务config/defaults.json:4 锁版本
Automation 后端(外部)定时/事件触发 agent 运行config/defaults.json:6 锁版本 1.8.0

主线走一遍(建会话,高层版): 你在首页敲一句话 → 前端服务层把「设置 + 技能 + 密钥引用 + 工作区」拼成一个 start 请求 → POST 给当前后端的 Agent Server → Agent Server 同步建好会话并开始跑 → 前端先用 REST 拉历史事件,再开 WebSocket 订实时事件流 → 事件进 Zustand store,渲染成聊天气泡、终端、diff。每一步的细节在第 02 章。

4. 阅读地图(建议顺序)

  1. 01-top-level.md — 顶层全景。 本地栈四个进程怎么起、ingress 怎么分流、四种部署形态(npm CLI / Docker / Electron / 库构建)、前端内部怎么分层。先读这章。
  2. 02-conversation-lifecycle.md — 会话生命周期。 工程含量最高的一章:start 请求里到底装了什么(加密设置、技能、客户端工具、LookupSecret)、WebSocket 怎么接、消息怎么发、确认(confirmation)怎么走。
  3. 03-sandbox.md — 后端抽象与运行时。 原「沙箱」章的替代:新架构把隔离交给部署形态,本章讲「一个 agent 后端」是怎么被抽象、登记、探活、代理的。
  4. 04-extensibility.md — 可插拔点。 接新 agent(ACP)、存 agent 配方(Agent Profile)、agent 反向驱动 UI(客户端工具)、技能/插件/MCP、自动化清单。

5. 代码地图(导航索引)

主题文件关键符号
产品定位README.mdAgent Canvas(README.md:5)
架构边界自述docs/architecture.md「responsible / not responsible」(docs/architecture.md:7-19)
路由表src/routes.tsRouteConfig(src/routes.ts:7-45)
服务层约定src/api/README.md「service 是 UI 与后端 API 的抽象层」(src/api/README.md:5)
后端抽象src/api/backend-registry/types.tsBackendBackendKind(src/api/backend-registry/types.ts:1-13)
活动后端切换src/api/backend-registry/active-store.tsgetEffectiveLocalBackend(src/api/backend-registry/active-store.ts:140-144)
客户端连接参数src/api/agent-server-client-options.tsgetAgentServerClientOptions(src/api/agent-server-client-options.ts:52-69)
会话服务src/api/conversation-service/agent-server-conversation-service.api.tsAgentServerConversationService.createConversation(src/api/conversation-service/agent-server-conversation-service.api.ts:403)
start 请求组装src/api/agent-server-adapter.tsbuildStartConversationRequest(src/api/agent-server-adapter.ts:1050)
实时事件src/contexts/conversation-websocket-context.tsxbuildWebSocketUrl + useWebSocket(src/contexts/conversation-websocket-context.tsx:1069-1072)
后端兼容门禁src/api/agent-server-compatibility.tsMINIMUM_COMPATIBLE_AGENT_SERVER_VERSION(src/api/agent-server-compatibility.ts:16-17)
本地栈启动器scripts/dev-with-automation.mjsmain(scripts/dev-with-automation.mjs:1383)
CLI 入口bin/agent-canvas.mjsmain({ staticMode: true, ... })(bin/agent-canvas.mjs:161-167)
栈版本锁config/defaults.jsonversions.agentServer(config/defaults.json:4)
桌面壳electron/main.mjsElectron main 进程(electron/main.mjs:1-8)
ACP provider 注册表src/constants/acp-providers.tsACP_PROVIDERS(src/constants/acp-providers.ts:151-167)