OpenHands Agent Canvas 开发指南:本地 Dev Stack 架构、Agent Server 版本选择与嵌入式定制(基于 docs/DEVELOPMENT.md)
本文以 docs/DEVELOPMENT.md 为主体,面向需要参与 agent-canvas 本身开发的贡献者与集成方,完整梳理其本地开发工作流:无 Docker 的全栈开发启动方式、agent-server 版本选择优先级、各种替代开发模式(静态构建、前后端分离、最小模式、Mock 模式)、构建与变异测试流程,以及将 UI 嵌入宿主应用时的 CSS 隔离与主题定制策略。读完后,你可以独立搭起完整本地开发栈、理解各服务的端口与隔离机制,并能基于源码定位每个启动参数的实际落点。
仓库定位与仓库边界
docs/DEVELOPMENT.md 开宗明义:本文档面向 agent-canvas 自身(@openhands/agent-canvas,见 package.json)的贡献者。该仓库包含 Agent Canvas 前端 与 本地开发栈编排(scripts/ 下的启动脚本),而后端能力分属若干兄弟仓库。文档给出了清晰的归属划分:
OpenHands/software-agent-sdk拥有 Python SDK、Agent Server、agent/tool 行为、conversations、workspaces、events 与 server API;OpenHands/typescript-client拥有面向浏览器、兼容该 Agent Server API 的类型化客户端。新 API 调用方法应加在该仓库,而不是在 Canvas 里重新实现;OpenHands/extensions拥有可复用的 skills、plugins、automations 与 integrations;OpenHands/automation拥有 automation 定义、调度、webhooks、运行历史与派发;agent-server/SDK 侧负责执行被派发的 conversation。
跨仓库功能的标准协作顺序是:先在 SDK 中实现后端契约,再通过 typescript-client 暴露,最后在 Canvas 中消费;automation 生命周期变更需在 automation 仓库协调。package.json 的依赖列表印证了这一关系:前端以 npm 依赖形式消费 @openhands/typescript-client(1.39.0)与 @openhands/extensions(0.19.0)。文档同时要求每个 PR 遵循仓库 贡献者说明 与 自定义 code-review 指南。
推荐本地工作流:npm run dev 全栈开发
文档推荐的核心工作流是 npm run dev,它一次性拉起完整本地栈,无需 Docker:
- 通过
uvx临时安装并运行agent-server(后端); - 通过
uvx运行 automation 后端; - Vite 开发服务器(带热更新);
- 一个 ingress 代理统一入口。
npm run dev 实际执行的是 node --env-file-if-exists=.env scripts/dev-with-automation.mjs(见 package.json 的 scripts 定义)。从 dev-with-automation.mjs 顶部的架构注释看,整条链路是:
┌──────────────────────────────────────────────────────────────────────────┐
│ http://localhost:8000 (Ingress Proxy) │
│ /api/automation/* → Automation Backend │
│ /api/*, /sockets → Agent Server │
│ /* → Vite Dev Server │
└──────────────────────────────────────────────────────────────────────────┘
│ │ │
▼ ▼ ▼
┌─────────────┐ ┌───────────────┐ ┌──────────────────┐
│ Vite │ │ Agent Server │ │ Automation │
│ :3001 │ │ (uvx) :18000 │ │ Backend (uvx) │
│ │ │ │ │ :18001 │
└─────────────┘ └───────────────┘ └──────────────────┘
各端口的默认值集中定义在 config/defaults.json —— 该文件是版本钉扎、端口与路径的唯一事实来源,被 scripts/dev-safe.mjs、scripts/dev-with-automation.mjs 与 Docker 入口共同读取:agent-server 端口 18000、automation 端口 18001、ingress 代理端口 8000。该文件同时钉扎了默认版本(agent-server 1.44.0、automation 1.9.0)与兼容性下限(minimumAgentServer: 1.28.0)。
启动成功后有两个入口(来自 dev-with-automation.mjs 的帮助文本):
- 主 UI:
http://localhost:<PORT>/ - automation API 文档:
http://localhost:<PORT>/api/automation/docs
ingress 路由表与部分栈模式
路由前缀在 dev-with-automation.mjs 中硬编码:/api/automation 前缀指向 automation 后端,/api、/sockets、/server_info、/health 等前缀指向 agent-server,其余请求回落到 Vite。此外,捆绑编辑器(openvscode-server)不单独发布端口,而是通过 /vscode 前缀路由(vscodeBasePath 同样来自 config/defaults.json)。
文档指出,发布的 agent-canvas 二进制还支持部分栈模式,以便把前端与后端进程分开运行:
agent-canvas --frontend-only
agent-canvas --backend-only
两种模式都会启动 ingress 代理,代理只把流量路由到该模式实际启动的服务。从源码可以印证其“宁可 503 也不误回 SPA”的设计:getRejectPrefixes()(dev-with-automation.mjs)在 frontend-only 模式下会把所有无后端的 API 前缀加入 --reject-prefix,使其返回 503,而不是被 SPA fallback 成 index.html;--frontend-only 与 --backend-only 互斥,--public 模式还强制要求显式设置 LOCAL_BACKEND_API_KEY。
开发栈的隔离机制
文档对 npm run dev 的隔离机制有详细说明,值得逐条展开:
- 开发栈使用
uvx在127.0.0.1:18000上运行一个临时agent-server安装,并让前端指向它; - 通过设置相互独立的
OH_CONVERSATIONS_PATH、OH_BASH_EVENTS_DIR与OH_VSCODE_PORT来隔离 conversation 持久化,使其不与其它本地或云后端 OpenHands 会话冲突(文档以.openhands-dev/作为隔离位置表述;从 dev-safe.mjs 的buildConfigFromPorts实现看,这些路径统一从一个隔离 state dir 派生,默认~/.openhands/agent-canvas,可用OH_CANVAS_SAFE_STATE_DIR覆盖,其下含dev_conversations、workspaces、bash_events等子目录); - tmux socket 位于
~/.openhands/agent-canvas/tmux(通过TMUX_TMPDIR环境变量传递)。源码注释解释了为何默认不用系统临时目录:macOS 上$TMPDIR会被系统定期清理,删除存活的 tmux socket 会导致后续 new-window 全部失败; - 若
$HOME位于不支持 Unix domain socket 的文件系统(某些 devcontainer、NFS/CIFS home),应把标准环境变量TMUX_TMPDIR设为本地路径(如/tmp),开发栈会直接使用它(dev-safe.mjs 中tmuxTmpDir: env.TMUX_TMPDIR || path.join(stateDir, "tmux"))。
会话密钥也有对应的持久化约定:LOCAL_BACKEND_API_KEY 未设置时,启动器会自动生成一个 256-bit 十六进制 key 并写入 ~/.openhands/agent-canvas/api-key.txt(dev-safe.mjs),保证跨重启稳定,使前端烘焙的 VITE_SESSION_API_KEY 与 localStorage 中的 backend 注册条目保持同步;该 key 同时作为 OH_SESSION_API_KEYS_0 注入 agent-server,并会被种子进 automation 侧(见 dev-with-automation.mjs)。
前置依赖
启动器会做两项前置检查(dev-with-automation.mjs 的 checkPrerequisites):uvx 与 npm 必须在 PATH 中,前端依赖(cross-env、react-router 等 bin)必须已安装。缺 uvx 时会打印 uv 安装指引并建议改用 npm run dev:frontend 或 npm run dev:mock;缺前端依赖时提示在仓库根目录执行 npm ci。另外 package.json 声明了运行环境前提:node >= 22.12.0(engines 与 volta 配置一致)。
启动器环境变量(文档第一张表)
文档给出的核心启动器变量如下:
| 变量 | 说明 | 默认值 |
|---|---|---|
PORT |
Ingress 端口 | 8000 |
OH_AUTOMATION_GIT_REF |
automation 后端的 Git ref | main |
OH_AGENT_SERVER_GIT_REF |
agent-server 的 Git ref | main |
静态前端构建:npm run dev:static
对于慢网络、远程访问或隧道场景,文档推荐使用静态前端构建:
npm run dev:static
它对应 scripts/dev-static.mjs,即先构建生产前端、再交由静态服务器托管(dev-with-automation.mjs 同样暴露 --static / --static-dir / --skip-build 等开关,可直接复用已有的 build/ 产物)。静态服务器在运行时向 index.html 注入会话 key(window.__AGENT_CANVAS_SESSION_API_KEY__)与鉴权标记,这也是发布二进制在预构建 bundle 中 VITE_SESSION_API_KEY 为空时仍能完成 onboarding 的路径 —— 见 agent-server-config.ts 中 getBakedSessionApiKey() 的两个来源说明。
最小模式(无 Automation)
若不想启动 automation 服务,文档给出:
npm run dev:minimal
该模式只运行 agent-server + Vite(无 automation 后端、无 ingress),访问地址为 http://localhost:3001/。其实现是 scripts/dev-safe.mjs:先以 buildSafeDevConfigAsync() 做端口预检(assertPortsFree 会在端口被占时立即失败并提示可能已有实例在运行),轮询 GET /server_info 确认后端就绪后,再拉起 npm run dev:frontend。注意此模式与完整模式的一个差异:VS Code 旁路端口默认是 backend port + 1(dev-safe.mjs),而完整栈中编辑器端口派生为 backend port + 1000(dev-with-automation.mjs);两种模式都支持 OH_CANVAS_SAFE_VSCODE_PORT 覆盖。
Agent Server 版本选择
文档说明默认使用 PyPI 上最新发布的版本,并给出(按优先级从高到低)三种覆盖方式:
# 针对本地 software-agent-sdk 检出运行
OH_AGENT_SERVER_LOCAL_PATH=/abs/path/to/software-agent-sdk npm run dev
# 使用 git 分支或提交(优先于 version)
OH_AGENT_SERVER_GIT_REF=main npm run dev
OH_AGENT_SERVER_GIT_REF=abc1234 npm run dev
# 使用指定 PyPI 版本
OH_AGENT_SERVER_VERSION=1.18.0 npm run dev
这段逻辑完整实现在 dev-safe.mjs 的 buildAgentServerCommand() 中,源码补充了几个文档未展开的关键细节:
- 本地路径要求:
OH_AGENT_SERVER_LOCAL_PATH必须是绝对路径,且指向包含openhands-agent-server、openhands-sdk、openhands-tools、openhands-workspace四个 workspace 包的software-agent-sdk检出(validateLocalAgentServerPath逐项校验,缺失即启动失败)。agent-server 本体每次启动都用uvx --reinstall从本地源码重建,其余三个包以 editable 方式安装,源码修改无需重新安装即可生效; - git ref 为何要
--reinstall:分支上的版本号字符串可能与 PyPI 当前发布相同,不加--reinstall时 uv 会静默复用缓存的 PyPI wheel,导致你指定的 ref 根本没被使用; - 版本钉扎的一致性:无论走哪条路径,
openhands-sdk、openhands-tools、openhands-workspace都与 agent-server 取同一版本/ref,保证跨包 API 同步; - 所有启动方式最终都会追加
--import-modules canvas_ui_tool(dev-safe.mjs),在创建 conversation 前注册 tools/canvas_ui_tool.py。
automation 侧有对称的一套变量(OH_AUTOMATION_LOCAL_PATH / OH_AUTOMATION_GIT_REF / OH_AUTOMATION_VERSION,见 dev-with-automation.mjs 的 buildAutomationCommand()),且 --automation-git-ref 命令行参数会显式压过 OH_AUTOMATION_LOCAL_PATH,避免 shell 中残留的环境变量让你误以为自己复现的是指定 ref。
其它有用覆盖项
文档列出的补充变量,均可在源码中找到对应解析点(dev-safe.mjs、dev-with-automation.mjs):
OH_CANVAS_SAFE_BACKEND_PORT— 隔离服务器端口(默认18000);OH_CANVAS_SAFE_VSCODE_PORT— VS Code 旁路端口(默认backend port + 1,见上文最小模式说明);OH_CANVAS_SAFE_STATE_DIR— 隔离服务器状态的基础目录;VITE_WORKING_DIR— 新建 conversation 使用的仓库根目录(默认当前检出)。
替代开发工作流
多本地后端(共享持久化)
要一边 npm run dev、一边再挂一个独立 agent-server 并共享其会话历史与加密 secrets,用文档提供的辅助脚本:
npm run dev:extra-backend
它由 scripts/dev-extra-backend.mjs 实现:在 :18002 上启动一个额外服务器,复用捆绑实例的 state dir,从而看到同一份会话与密钥数据。
前端对接已有后端
仅在你确实自行启动了 agent-server、或希望前端指向别的后端时使用:
npm run dev:frontend
该工作流默认期望后端位于 127.0.0.1:8000。若设置了 LOCAL_BACKEND_API_KEY,它会被用作 agent-server 的 API key(内部映射到 OH_SESSION_API_KEYS_0);未设置时启动器自动生成并持久化一个 key。dev:frontend 在 package.json 中定义为 make-i18n && cross-env VITE_MOCK_API=false react-router dev,即标准 Vite/React Router 开发服务器,API 转发目标由 VITE_BACKEND_HOST 决定(dev-safe.mjs 的注释区分了 VITE_BACKEND_HOST 只供开发代理使用、VITE_BACKEND_BASE_URL 则刻意留空让前端回落到同源 origin,从而在 SSH 隧道/ngrok 等场景下保持可移植)。
Mock 模式
想在没有真实后端的情况下运行前端:
npm run dev:mock
即 cross-env VITE_MOCK_API=true react-router dev,通过 MSW 拦截 API(mock worker 位于 public/mockServiceWorker.js)。
构建与测试
文档给出的三条基础命令:
npm run test
npm run build
npm run start
对应关系(package.json):test 会先执行 make-i18n 再跑 vitest run;build 走 build:app(make-i18n && react-router build);start 用 sirv-cli build/ --single 提供构建产物。
针对隔离开发启动器的定向验证,文档推荐:
npm run test -- __tests__/api/agent-server-config.test.ts __tests__/scripts/dev-safe.test.ts
两个测试文件分别覆盖前端侧的 agent-server 配置解析(对应 src/api/agent-server-config.ts)与启动器核心的端口分配、API key 持久化、命令构建等纯函数。ingress 路由相关行为另有 tests/scripts/ingress.test.ts 等脚本测试。
变异测试(Mutation Testing)
文档介绍了 Stryker 对 src/ 下第一方 TypeScript 源码做变异验证,确认 Vitest 套件能捕获被刻意引入的缺陷:
# 全量变异运行(对整个前端而言开销大)
npm run test:mutation
# 复用上一次运行的结果
npm run test:mutation:incremental
# 只变异相对本地 main 分支变更的生产文件
npm run test:mutation:diff
# 与其它 base ref 比较,例如最新的远程 main
npm run test:mutation:diff -- origin/main
HTML 报告写入 reports/mutation.html。文档特别强调:变异分数目前仅作报告用途,应先建立稳定基线,再考虑加入失败阈值。
stryker.config.mjs 展示了文档所称的“默认排除”具体规则:mutate 模式为 src/**/*.{ts,tsx},并排除测试文件(*.test.* / *.spec.*)、__tests__/、.d.ts、*.types.ts、.gen/.generated.ts、i18n/declaration.ts 以及 src/{fixtures,mocks,dev}/**;vitest runner 开启 related: true,只执行与被变异文件相关的测试,显著降低成本。文档同时说明:Stryker 不覆盖仓库中的少量 Python 代码面,那部分需要 Python 测试框架与 Python 专用变异工具。
CSS 隔离与宿主应用定制
文档的这一节讲的是把 Agent Canvas UI 嵌入宿主应用时的样式边界。独立应用与导出的 provider/root 包装器现在把所有捆绑 CSS 限定在一个带 data-agent-server-ui 属性专用 shell 元素之下 —— Tailwind 工具类、HeroUI 组件样式、xterm 样式与本地 CSS 只作用于 OpenHands UI 子树内,不会泄漏到宿主应用。
源码层面的证据是 src/styles/agent-server-ui-style-scope.ts:
- 作用域常量
AGENT_SERVER_UI_SCOPE_ATTRIBUTE = "data-agent-server-ui",选择器[data-agent-server-ui]; transformAgentServerUISelector()在构建期改写 CSS 选择器::root/body/html这类全局选择器直接改写为作用域前缀,:host也替换为前缀,从而保证没有任何选择器逃逸出子树;- 主题令牌以 CSS 自定义属性(
--oh-*)形式挂在作用域根上,默认值集中在AGENT_SERVER_UI_DEFAULT_CSS_VARIABLES(--oh-color-base、--oh-accent、--oh-surface、--oh-border等 60 余项);其中--oh-color-primary、--oh-accent、--oh-warning三个品牌变量被单独列为“可被颜色主题在运行时覆盖”的变量,刻意不做内联,避免被element.style压住。
嵌入策略
- 宿主应用使用
AgentServerUIProviders(src/components/providers/agent-server-ui-providers.tsx),默认渲染一个带作用域的样式根; - 需要直接控制包装层时使用
AgentServerUIRoot(src/components/providers/agent-server-ui-root.tsx); - 独立应用(standalone app)因为路由布局已经渲染了带作用域的根,反而选择退出 provider 包装。
定制策略
主题与表面令牌通过作用域根上的 CSS 自定义属性暴露,可以两种途径覆盖:provider/root 的 styleOverrides prop,或宿主 CSS 直接选择 [data-agent-server-ui]。文档示例:
<AgentServerUIProviders
styleOverrides={{
"--oh-color-base": "#101820",
"--oh-color-content-2": "#f5f7ff",
"--oh-accent": "#8b5cf6",
}}
>
<App />
</AgentServerUIProviders>
styleOverrides 的类型 AgentServerUIStyleOverrides(agent-server-ui-style-scope.ts)对键名做了约束:只接受 AGENT_SERVER_UI_DEFAULT_CSS_VARIABLES 的键或三个主题化品牌变量,避免拼错变量名静默失效。
还有一个文档明确点出的坑:若希望内层主题化容器拥有 Tailwind 布局工具类,应传 contentClassName 而不是 className —— 因为外层作用域元素才是所有生成选择器的锚点,把工具类挂在锚点之外会导致样式失效。
项目 .env 环境变量
文档最后给出基于 .env.sample 在项目中创建 .env 的变量表,完整继承如下:
| 变量 | 说明 | 默认值 |
|---|---|---|
VITE_BACKEND_BASE_URL |
浏览器直接请求使用的 agent server 完整 base URL | 当前浏览器 origin |
VITE_BACKEND_HOST |
Vite 开发代理使用的后端 host | 127.0.0.1:8000 |
VITE_SESSION_API_KEY |
(内部)由启动器注入的会话 API key —— 用户请改设 LOCAL_BACKEND_API_KEY |
- |
VITE_WORKING_DIR |
新建 conversation 时发送的工作区路径 | workspace/project |
VITE_ENABLE_BROWSER_TOOLS |
设为 false 可从新 conversation 载荷中省略 BrowserToolSet |
true |
VITE_BASE_PATH |
在子路径(如 /canvas)下构建/托管 SPA |
/ |
VITE_MOCK_API |
开关 MSW API 模拟 | false |
VITE_USE_TLS |
Vite 代理目标使用 HTTPS/WSS | false |
VITE_FRONTEND_PORT |
前端应用运行端口 | 3001 |
VITE_INSECURE_SKIP_VERIFY |
代理后端请求时跳过 TLS 证书校验 | false |
其中 VITE_WORKING_DIR 的默认值 workspace/project 与前端常量 DEFAULT_WORKING_DIR(agent-server-config.ts)一致;VITE_MOCK_API 正是上文 dev:mock / dev:frontend 脚本切换的开关;VITE_USE_TLS 在 VITE_BACKEND_BASE_URL 为 https:// 而用户未显式指定时会被启动器自动推导为 true(dev-with-automation.mjs)。
小结:按场景选工作流
把文档中的工作流汇总成一张选型表:
| 场景 | 命令 | 组成 | 入口 |
|---|---|---|---|
| 完整本地开发(推荐) | npm run dev |
agent-server + automation + Vite + ingress | http://localhost:8000 |
| 慢网络 / 隧道 | npm run dev:static |
静态前端 + 后端 + ingress | ingress 端口 |
| 前后端分离 | agent-canvas --frontend-only / --backend-only |
单侧进程 + ingress | ingress 端口 |
| 仅 agent-server 联调 | npm run dev:minimal |
agent-server + Vite(无 automation/ingress) | http://localhost:3001 |
| 已有后端 | npm run dev:frontend |
仅前端,默认指向 127.0.0.1:8000 |
http://localhost:3001 |
| 纯前端 / 演示 | npm run dev:mock |
前端 + MSW | http://localhost:3001 |
| 共享持久化的第二后端 | npm run dev:extra-backend |
额外 agent-server 于 :18002 |
- |
配合 agent-server 版本三变量(OH_AGENT_SERVER_LOCAL_PATH > OH_AGENT_SERVER_GIT_REF > OH_AGENT_SERVER_VERSION > 默认 PyPI 钉扎版本)与 PORT / OH_CANVAS_SAFE_* 覆盖项,开发者可以在不改动代码的前提下覆盖绝大多数本地联调与集成调试场景;而所有默认值最终都收敛在 config/defaults.json 这一份配置里,便于审计与升级。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00