数据截至 (上游 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:4 锁 agentServer: 1.42.1,启动脚本用 uvx --from openhands-agent-server==1.42.1 拉它(scripts/dev-with-automation.mjs:913-914 的 buildAgentServerCommand)。所以仓库里凡涉及「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 + 前端 + ingress | scripts/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. 阅读地图(建议顺序)
01-top-level.md— 顶层全景。 本地栈四个进程怎么起、ingress 怎么分流、四种部署形态(npm CLI / Docker / Electron / 库构建)、前端内部怎么分层。先读这章。02-conversation-lifecycle.md— 会话生命周期。 工程含量最高的一章:start 请求里到底装了什 么(加密设置、技能、客户端工具、LookupSecret)、WebSocket 怎么接、消息怎么发、确认(confirmation)怎么走。03-sandbox.md— 后端抽象与运行时。 原「沙箱」章的替代:新架构把隔离交给部署形态,本章讲「一个 agent 后端」是怎么被抽象、登记、探活、代理的。04-extensibility.md— 可插拔点。 接新 agent(ACP)、存 agent 配方(Agent Profile)、agent 反向驱动 UI(客户端工具)、技能/插件/MCP、自动化清单。
5. 代码地图(导航索引)
| 主题 | 文件 | 关键符号 |
|---|---|---|
| 产品定位 | README.md | Agent Canvas(README.md:5) |
| 架构边界自述 | docs/architecture.md | 「responsible / not responsible」(docs/architecture.md:7-19) |
| 路由表 | src/routes.ts | RouteConfig(src/routes.ts:7-45) |
| 服务层约定 | src/api/README.md | 「service 是 UI 与后端 API 的抽象层」(src/api/README.md:5) |
| 后端抽象 | src/api/backend-registry/types.ts | Backend、BackendKind(src/api/backend-registry/types.ts:1-13) |
| 活动后端切换 | src/api/backend-registry/active-store.ts | getEffectiveLocalBackend(src/api/backend-registry/active-store.ts:140-144) |
| 客户端连接参数 | src/api/agent-server-client-options.ts | getAgentServerClientOptions(src/api/agent-server-client-options.ts:52-69) |
| 会话服务 | src/api/conversation-service/agent-server-conversation-service.api.ts | AgentServerConversationService.createConversation(src/api/conversation-service/agent-server-conversation-service.api.ts:403) |
| start 请求组装 | src/api/agent-server-adapter.ts | buildStartConversationRequest(src/api/agent-server-adapter.ts:1050) |
| 实时事件 | src/contexts/conversation-websocket-context.tsx | buildWebSocketUrl + useWebSocket(src/contexts/conversation-websocket-context.tsx:1069-1072) |
| 后端兼容门禁 | src/api/agent-server-compatibility.ts | MINIMUM_COMPATIBLE_AGENT_SERVER_VERSION(src/api/agent-server-compatibility.ts:16-17) |
| 本地栈启动器 | scripts/dev-with-automation.mjs | main(scripts/dev-with-automation.mjs:1383) |
| CLI 入口 | bin/agent-canvas.mjs | main({ staticMode: true, ... })(bin/agent-canvas.mjs:161-167) |
| 栈版本锁 | config/defaults.json | versions.agentServer(config/defaults.json:4) |
| 桌面壳 | electron/main.mjs | Electron main 进程(electron/main.mjs:1-8) |
| ACP provider 注册表 | src/constants/acp-providers.ts | ACP_PROVIDERS(src/constants/acp-providers.ts:151-167) |