跳到主要内容

数据截至 (上游 commit 1916c9046c4e)

顶层全景:本地栈、ingress 与前端分层

这章给你「大盘」:一条 agent-canvas 命令到底拉起了哪几个进程、浏览器的一个动作怎么穿过各层到达 Agent Server、以及这套代码有哪几种部署形态。读之前请先看过 index.md 的 pivot 说明:agent 循环不在这个仓库,本章所有「后端」都是外部 pip 包。

1. 本地栈:一条命令拉起四个进程

agent-canvas CLI(bin/agent-canvas.mjs)是 npm 包暴露的唯一命令(package.json:16-18)。它做的事很薄:检查一下构建产物存在,然后把活全交给启动器脚本(bin/agent-canvas.mjs:151-167 导入 scripts/dev-with-automation.mjsmain 并以 static 模式调用)。

main 拉起的进程组(scripts/dev-with-automation.mjs:1544-1596):

浏览器
│ http://localhost:8000

┌──────────────────────┐
│ ingress 代理 :8000 │ 唯一对外入口
└──────────────────────┘
│ 按路径前缀分流(见 §2)
┌─────┼─────────────────┐
▼ ▼ ▼
静态前端 Agent Server Automation 后端
:3001 (uvx) :18000 (uvx) :18001
(build/ 真正跑 agent 定时/webhook
的 SPA) 循环的 pip 包 触发器的 pip 包

三个要点:

  • 两个后端都是 uvx 拉起的 pip 包,版本锁在 config/defaults.json:4-7(agent-server 1.42.1、automation 1.8.0)。agent-server 的启动命令由 buildAgentServerCommand 拼出(scripts/dev-with-automation.mjs:913-914),automation 同理(scripts/dev-with-automation.mjs:963-982startAutomationBackend)。
  • 端口默认值也锁在同一文件:config/defaults.json:21-27(ingress 8000、agent-server 18000、automation 18001)。
  • 启动有先后:main 先起 agent-server 并轮询它的 /server_info 直到就绪(scripts/dev-with-automation.mjs:1553-1561waitForService),再把会话密钥写进它的 secrets 存储,最后才起 automation、前端、ingress。

一把钥匙开两把锁。 启动器生成一个 session API key,同时喂给两个后端:agent-server 通过环境变量 OH_SESSION_API_KEYS_0 拿到(scripts/dev-with-automation.mjs:936),automation 拿到同一把(scripts/dev-with-automation.mjs:1041AUTOMATION_LOCAL_API_KEY)。两边都认 X-Session-API-Key 请求头。这把钥匙还会被 seedAutomationSecret 写进 agent-server 的 secrets 存储(scripts/dev-with-automation.mjs:1215-1229),让会话里的 agent 自己也能调 automation API。

2. ingress:一个端口,按路径前缀分流

浏览器只跟一个源(origin)打交道。ingress 是个极简反向代理,按最长路径前缀匹配转发(scripts/ingress.mjs:18-22 的路由说明)。分流表由启动器生成(scripts/dev-with-automation.mjs:729-781):

路径前缀转发到
/api/automationAutomation 后端 :18001
/api/sockets/server_info/healthAgent Server :18000
其余(/*)前端(Vite dev server 或静态构建)

这个设计的直接收益:前端代码里不需要写死后端地址。生产模式下前端的 base URL 就是 window.location.origin(src/api/agent-server-config.ts:181-190getAgentServerBaseUrl),一切相对路径都由 ingress 送到正确的地方。

密钥怎么进前端? 静态服务器在返回 index.html 时把 session key 注入一个 window 全局变量;前端启动时从两处之一读它:构建期烧进去的 VITE_SESSION_API_KEY,或运行期注入的 window.__AGENT_CANVAS_SESSION_API_KEY__(src/api/agent-server-config.ts:119-132getBakedSessionApiKey)。--public 模式则不注入,用户首次打开页面时手动粘贴密钥(bin/agent-canvas.mjs:71-74 的帮助文本)。

3. 前端内部:四层,每层只做一件事

前端是 React 19 + React Router 7 应用(package.json:49-55)。从 UI 到网络,一次调用穿过四层:

React 组件(只渲染)
│ 调用

TanStack Query hook(缓存/去重/加载态)
│ 调用

src/api 服务层(拼参数、选后端、双路由)
│ 用

@openhands/typescript-client(typed HTTP 客户端)
│ HTTP / WebSocket

Agent Server(外部 pip 包)

两条硬约定,都写在 src/api/README.md 里:

  • 服务层是 UI 与后端 API 之间唯一的抽象层(src/api/README.md:5),每个 service 是「一个 plain object + async 方法」,命名固定为 feature-service/feature-service.api.ts(src/api/README.md:91-98)。
  • 组件不许直接调 service,必须包一层 TanStack Query hook——换来缓存、去重、加载态(src/api/README.md:63-76)。

服务层与后端之间还有一个固定版本的中介:@openhands/typescript-client(package.json:27 锁 1.38.1),它是 Agent Server REST API 的官方 typed 客户端(ConversationClientFileClientBashClient 等)。每个 service 调它之前先问一句「当前该连哪个后端、用什么密钥」——这就是 getAgentServerClientOptions(src/api/agent-server-client-options.ts:52-69):它从后端注册表取当前活动 local 后端的 host 与 apiKey,拼成连接参数;没有可用后端就抛 NoBackendAvailableError(src/api/agent-server-client-options.ts:22-27)。后端注册表本身很大,是第 03 章的主题。

4. 一条请求的完整流向(以「列出会话」为例)

把 §3 走一遍真实代码。你在首页打开会话列表:

  1. 组件挂载,TanStack Query hook 触发 AgentServerConversationService.searchConversations(src/api/conversation-service/agent-server-conversation-service.api.ts:747-764)。
  2. service 先看当前后端种类:cloud 走云代理,local 走 typed client(src/api/conversation-service/agent-server-conversation-service.api.ts:751-752 的分支)。
  3. local 分支 new 一个 ConversationClient,连接参数来自 getAgentServerClientOptions(src/api/conversation-service/agent-server-conversation-service.api.ts:755-756)。
  4. typescript-client 发 HTTP GET /api/conversations/search,带 X-Session-API-Key 头。
  5. ingress 按前缀 /api 转给 agent-server :18000。
  6. 响应回来后,service 用 toConversationPage 把 wire 格式适配成 UI 模型(src/api/agent-server-adapter.ts:404-412),hook 缓存结果,组件渲染。

「看后端种类再决定走哪条路」这个双路由(local typed client / cloud 代理)模式在几乎每个 service 里都会出现,是读 src/api/ 代码时最先要认识的模式,细节见第 03 章。

5. 四种部署形态

同一份前端代码,以四种方式交付(docs/architecture.md:58-66 也列了打包形态):

形态入口适合谁
npm CLIbin/agent-canvas.mjs:161-167(static 模式起全套栈)大多数人,npm install -g 即用
Docker 全一体镜像docker/Dockerfile:5-16(agent-server + automation + 前端 + ingress 一容器)想要文件系统隔离的笔记本用户
Electron 桌面壳electron/main.mjs:1-8(内置全套栈,uvx 都打包进去)不想碰命令行的用户
库构建package.json:206-248exports(把 conversation/files/terminal 等组件当库用)把 Canvas 嵌进自己应用的宿主

开发模式下还有几种变体(docs/architecture.md:47-56):npm run dev 起全栈(Vite dev server 代替静态前端)、dev:minimal 不起 automation、dev:mock 用 MSW mock 后端。

安全提示值得单独说一句。 本地形态( npm / 源码)意味着 agent-server 直接跑在你的机器上,agent 拥有你整个文件系统的权限——README 在「Without a Sandbox」选项上挂了显眼的 WARNING(README.md:63-66)。想要隔离就用 Docker 形态,或把后端放到远程机器/云上。这就是 pivot 后「沙箱」的落点:隔离是部署决策,不是本仓库的运行时机制(第 03 章展开)。

6. 本章小结 + 下一步

  • 本地栈 = 前端 + ingress + 两个 uvx 拉起的 pip 包后端;ingress 按路径前缀分流。
  • 一把 session API key 贯穿启动器、两个后端、前端注入。
  • 前端四层:组件 → TanStack Query hook → src/api 服务层 → typescript-client。
  • 接下来:第 02 章顺着「建一个会话」把服务层最厚的一段走完;第 03 章讲后端注册表与运行时抽象。