数据截至 (上游 commit 1916c9046c4e)
可插拔机制:ACP、客户端工具、技能与自动化
Agent Canvas 的价值很大程度在「什么都能接」:换 agent、存配方、让 agent 反向操作 UI、给 agent 加技能、挂自动化。本章按「插头」逐个讲,每个都落到真实代码。前提不变:agent 的执行在外部 Agent Server,这里讲的都是前端侧的可插拔点。
1. ACP:把第三方 agent 插进来
它是什么。 ACP(Agent Client Protocol)是「客户端 ↔ agent」之间 JSON-RPC over stdio 的标准。Agent Server 把第三方 agent 的 CLI 当子进程拉起、逐轮转发;外部 agent 自己管 LLM 与工具,Canvas 只发消息、渲染结果(docs/ACP_AGENTS.md:9-16)。
provider 从哪来。 支持哪些 ACP agent,数据源不在本仓库——在 SDK 的注册表里,经 @openhands/typescript-client 镜像进来;Canvas 只加 UI 元数据(图标、文案 key),合成 ACP_PROVIDERS(src/constants/acp-providers.ts:151-167)。每条 provider 的形状见 ACPProviderConfig(src/constants/acp-providers.ts:76):注册表 key(也会作为会话 tag acpserver)、显示名、默认启动命令、可选模型列表。所以「新增一个 ACP agent」发生在上游 SDK,不在 Canvas。
onboarding 怎么知道用户已登录? AcpService.getAuthStatus 不新增任何后端端点,而是借 agent-server 的 bash 端点,在后端所在机器上跑各家 CLI 的状态命令再分类输出(src/api/acp-service/acp-service.api.ts:99-108):
| provider | 探测命令 | 分类逻辑 |
|---|---|---|
claude-code | claude auth status --json | 解析 JSON 的 loggedIn 字段 |
codex | codex login status | 匹配 "not logged in" / "logged in" 字样 |
gemini-cli | 检查 ~/.gemini/oauth_creds.json 是否存在 | 精确匹配 present / absent |
探测表本身在 ACP_AUTH_PROBES(src/api/acp-service/acp-service.api.ts:70-84)。分类不出的(命令不存在、输出不认识)一律报 unknown,让 onboarding 回落到「填 API key」而不是瞎猜——模型 token 一个都不花。
启动时怎么传给后端。 第 02 章见过的 buildConfiguredAcpAgentSettings(src/api/agent-server-adapter.ts:817-881)把 agent_kind: "acp"、acp_command(缺省时取 provider 默认命令)、acp_model、mcp_config 装进 agent_settings;会话跑起来后还能用 switchAcpModel 在不丢上下文的前提下换模型(src/api/conversation-service/agent-server-conversation-service.api.ts:925-944)。
2. Agent Profile:可复用的 agent 配方
它是什么。 一个 Agent Profile = 一份命名的 agent 配置(哪个 agent、哪个 LLM profile、禁用哪些技能等),存在后端,可以被任何会话按 id 引用。
服务层。 AgentProfilesService 提供 list/get/save/delete/rename/activate 一套 CRUD(src/api/agent-profiles-service/agent-profiles-service.api.ts:63-128),照例 local 走 AgentProfilesClient、cloud 走云代理。后端会懒种子一个名为 default 的基线 profile(常量 WELL_KNOWN_DEFAULT_AGENT_PROFILE_NAME,src/api/agent-profiles-service/agent-profiles-service.api.ts:50),镜像你的全局设置。
启动时怎么 用。 二选一,第 02 章已述:start 请求带 agent_profile_id 时由服务端解析配方,前端不再内联 agent_settings(src/api/agent-server-adapter.ts:1107-1109)。「激活」只是记一个指针(activateProfile),不写死任何设置——所以切 profile 是轻量的。
3. client_tools:让 agent 反向驱动 UI
这是最「控制台味」的一个机制:start 请求里,前端可以注册由前端自己实现的工具给 agent 调用。buildStartConversationRequest 对 OpenHands agent 固定带上两个(src/api/agent-server-adapter.ts:1116-1119):
① canvas_ui_control——agent 遥控右侧面板。 工具名常量见 src/constants/canvas-ui.ts:2(旧名 canvas_ui 保留为兼容常量);它的工具说明写得很直白:用户看不见你写的文件/终端输出,除非你调我把面板切过去(src/api/canvas-ui-client-tool.ts:20-53 的 CANVAS_UI_DESCRIPTION)。三个命令:navigate_to_file、open_tab、show_preview(schema 见 src/api/canvas-ui-client-tool.ts:60 的 CANVAS_UI_CLIENT_TOOL)。执行不在服务端——事件流回前端后,handleCanvasUIAction 直接操作 Zustand store 切 tab、选文件(src/services/canvas-ui.ts:32-60)。等于 agent 有了一只「指向屏幕的手」。
② launch_child_conversation——agent 开子会话。 让 agent 把一个范围清晰的任务委托给新会话并行去跑:本地子会话继承 当前工作区(可选 worktree 隔离),云端子会话在 OpenHands Cloud 开独立沙箱(src/api/launch-child-conversation-client-tool.ts:14-50 的工具说明)。子会话通过 start 请求的 parent_conversation_id 字段与父会话挂钩(src/api/agent-server-adapter.ts:1165-1167)。
一个使用约束值得知道:agent-server 会按工具名缓存 schema,同名工具换 schema 会被拒(ClientToolSchemaConflictError)——改这两个工具的 schema 后要重启 dev 栈(src/api/agent-server-adapter.ts:1111-1115 注释)。
4. 技能、插件、MCP:给 agent 加餐
三样都是「让 agent 更会干活」的外挂,但来源与通道各不同。
技能(skills)。 公共技能不打网络请求——它们来自 npm 包 @openhands/extensions 的 SKILLS_CATALOG,构建时就烧进前端 bundle(src/api/skills-service.ts:12-34 的 PUBLIC_SKILLS)。建会话时,buildBundledSkills 把目录条目转成 SDK 的 Skill JSON 形状(带 keyword 触发器),与用户在设置里配的技能合并后放进 agent_context.skills,并显式关掉服务端的公共技能克隆(load_public_skills: false)——前端是公共技能的唯一来源(src/api/agent-server-adapter.ts:722-747、src/api/agent-server-adapter.ts:769-787)。用户级/项目级技能仍由 agent-server 本地加载(src/api/skills-service.ts:36-63 的 getSkills)。
插件(plugins)。 插件是带技能/文件的包,分两类:市场目录里 的可安装项(MarketplacePlugin,src/api/plugins-service.ts:21-29)与本地目录里自动加载的「ambient」插件(LocalPlugin)。选中的插件以 {source, ref, repo_path} 坐标写进 start 请求(src/api/agent-server-adapter.ts:981-989),由 agent-server 在会话启动时拉取装载。
MCP。 MCP server 配置(stdio/sse/http 三种,src/api/mcp-service/mcp-service.api.ts:23-40 的 toMcpServer)存在设置里;建会话时作为 mcp_config 随 agent_settings 下发——对 ACP agent 就是直接转给子进程(src/api/agent-server-adapter.ts:857-860)。配置里的秘密以密文(round-trip)形式携带,不落明文。
5. 自动化清单:定时/webhook 任务的契约
自动化本身跑在外部 Automation 后端(第 01 章),前端负责管理界面与契约适配。
服务层双路由。 AutomationService 的每个方法都按后端 kind 分叉:local 走一个 axios 实例,其拦截器在每次请求时从后端注册表取当前 local 后端的 host 与 key(而不是模块加载时冻结),base path 恒为 /api/automation(src/api/automation-service/automation-service.api.ts:64-111);cloud 走 callCloudProxy。
端点不写死,走清单。 具体路径由 src/manifests/ 下的清单解析:getAutomationEndpoint / getAutomationIdEndpoint 按操作名查端点(src/manifests/automation-interface.ts:133-138), 这样宿主(云端)可以重映射自动化 API 的形状而前端不用改。
创建是一条「先建后改」流水线。 导入式创建 createAutomation 先用占位触发器 POST 建记录(保证新建的是惰性、不误触发),再 PATCH 上真实触发器与 enabled: false;第二步失败会回头 DELETE 清理,避免留下半成品(src/api/automation-service/automation-service.api.ts:320-388)。表单式创建则先问能力(getCapabilities,src/api/automation-service/automation-service.api.ts:539-555)、再校验草稿(validateDraft)、最后一发成单(createAutomationDraft,src/api/automation-service/automation-service.api.ts:590-613)。后端健在与否由 checkHealth 探(src/api/automation-service/automation-service.api.ts:762-787),UI 据此显示「automation 不可用」。
6. 番外:把整个 Canvas 当库插进你的应用
最后一层「可插拔」是对宿主的:package.json:206-248 的 exports 把 conversation、files、terminal、browser、settings、sidebar、i18n 等组件树暴露为库入口(npm run build:lib 构建),宿主应用可以把 Canvas 的会话界面整块嵌进自己的产品。
7. 本章小结
| 插头 | 机制 | 关键符号 |
|---|---|---|
| 接第三方 agent | ACP provider 注册表(上游 SDK 镜像)+ 登录探测 | ACP_PROVIDERS、AcpService.getAuthStatus |
| 存 agent 配方 | Agent Profile(后端存储,按 id 引用) | AgentProfilesService |
| agent 驱动 UI | client_tools(canvas_ui / launch_child) | CANVAS_UI_CLIENT_TOOL、handleCanvasUIAction |
| 技能 | 构建时烧入的 SKILLS_CATALOG + agent_context.skills | buildBundledSkills |
| 插件 | 市场/本地插件,坐标进 start 请求 | MarketplacePlugin |
| MCP | mcp_config 随 agent_settings 下发 | toMcpServer |
| 自动化 | 清单驱动的端点 + 先建后改流水线 | AutomationService.createAutomation |
| 嵌进宿主 | 库构建 exports | package.json:206-248 |