生成模型 :Claude Opus 4.6 (anthropic/claude-opus-4-6) Token 消耗 :输入 ~185k tokens,输出 ~6k tokens(本节)
上一节介绍了 Canvas 和 A2UI 的概念。本节将深入 Canvas Host 的源码实现,分三个部分:Canvas 静态文件服务器、A2UI 核心模块、以及 A2UI 打包流程。
17.2.1 Canvas 服务器(src/canvas-host/server.ts)
两层架构:Handler + Server
Canvas Host 的实现分为两层:
Copy startCanvasHost() ← 外层:创建 HTTP 服务器,绑定端口
└── createCanvasHostHandler() ← 内层:文件服务逻辑、Live Reload 这种分离允许 Canvas Handler 被嵌入到已有的 HTTP 服务器 中(例如 Gateway 的 HTTP 层),而不必总是启动独立的服务器。
Copy // src/canvas-host/server.ts — 类型定义
export type CanvasHostHandler = {
rootDir : string ; // Canvas 根目录
basePath : string ; // URL 基路径(默认 /__openclaw__/canvas)
handleHttpRequest : ( req , res ) => Promise < boolean >; // HTTP 请求处理
handleUpgrade : ( req , socket , head ) => boolean ; // WebSocket 升级处理
close : () => Promise < void >; // 清理资源
};
export type CanvasHostServer = {
port : number ; // 监听端口
rootDir : string ; // Canvas 根目录
close : () => Promise < void >;
}; Canvas 的内容目录默认位于 ~/.openclaw/canvas/:
服务器启动时会确保该目录存在,并在缺少 index.html 时写入一个默认的测试页面:
默认测试页面 defaultIndexHTML() 是一个内嵌的完整 HTML 字符串,包含四个按钮(Hello、Time、Photo、Dalek),用于验证 Canvas 和 A2UI 动作桥是否正常工作。
Canvas Host 的核心是一个静态文件服务器,但它必须防止路径遍历攻击 (Path Traversal)——恶意请求通过 ../../etc/passwd 这样的路径试图访问根目录以外的文件。
路径安全通过三重机制保障:
decodeURIComponent + path.posix.normalize 消除编码绕过
底层安全函数(来自 src/infra/fs-safe.ts),通过 realpath 验证最终路径确实在根目录内
衍生解释——路径遍历攻击(Path Traversal) :这是一种常见的 Web 安全漏洞。攻击者通过在 URL 中使用 ../(父目录引用)来尝试访问服务器上预期目录之外的文件。例如,请求 http://server/files/../../../etc/passwd 试图读取系统密码文件。防御方法包括:规范化路径、过滤 .. 段、验证最终路径是否在允许的目录内。OpenClaw 同时使用了这三种方法。
handleHttpRequest 是整个文件服务的入口:
关键设计点:
Cache-Control: no-store ——禁用缓存,确保 Agent 更新文件后 WebView 立即获取最新版本
HTML 自动注入 ——所有 HTML 响应都会被注入 Live Reload 脚本和跨平台动作桥
返回 boolean ——true 表示已处理,false 表示不归 Canvas 管,由其他处理器继续
Live Reload:文件变更自动刷新
Live Reload 使用 chokidar 监视文件变更,通过 WebSocket 通知客户端刷新:
衍生解释——chokidar :chokidar 是 Node.js 生态中最流行的文件监视库,是 fs.watch 的高级封装。它解决了原生 fs.watch 的诸多跨平台问题:macOS 上的 FSEvents 有时不报告文件名、Linux 上的 inotify 对递归监视支持有限、某些编辑器(如 Vim)会先删除再创建文件导致 watch 失效等。chokidar 统一了这些差异,提供可靠的跨平台文件监视。
Live Reload 的时序:
两个 75ms 延迟的含义不同:
awaitWriteFinish.stabilityThreshold: 75——等待文件写入完成(文件大小在 75ms 内不变才算完成)
setTimeout(broadcastReload, 75)——防抖:合并短时间内的多个文件变更为一次刷新
Canvas Host 在测试环境中自动禁用:
禁用时返回一个空壳 Handler,所有方法都是 no-op:
startCanvasHost() 在 Handler 之上创建一个独立的 HTTP 服务器:
请求优先级链:A2UI → Canvas → 404 。A2UI 路径(/__openclaw__/a2ui)优先于 Canvas 路径(/__openclaw__/canvas),因为 A2UI 是 Canvas 之上的功能层。
17.2.2 A2UI 核心(src/canvas-host/a2ui.ts 与 a2ui/)
A2UI 模块定义了三个关键路径:
所有路径都以 /__openclaw__/ 为前缀,这是一个不太可能与用户内容冲突的命名空间。
A2UI 渲染器的资源文件(index.html + a2ui.bundle.js)可能位于不同位置,取决于运行方式:
发现策略的优先级反映了不同的部署场景:
../../src/canvas-host/a2ui
cwd()/src/canvas-host/a2ui
cwd()/dist/canvas-host/a2ui
发现结果会被缓存(cachedA2uiRootReal),并通过 Promise 去重(resolvingA2uiRoot)避免并发时重复搜索。
handleA2uiHttpRequest 处理 /__openclaw__/a2ui 路径下的请求:
A2UI 的文件服务与 Canvas 共享相同的安全策略:拒绝 .. 段、拒绝符号链接、验证 realpath 在根目录内。
injectCanvasLiveReload() 是 Canvas 和 A2UI 共享的关键函数,它将跨平台动作桥和 Live Reload 客户端注入到每个 HTML 响应中:
注入的脚本做了两件事:
动作桥 :暴露 window.OpenClaw.sendUserAction() 和 window.openclawSendUserAction() 全局函数
Live Reload :建立到 /__openclaw__/ws 的 WebSocket 连接,收到 "reload" 消息时自动刷新页面
17.2.3 A2UI 打包(scripts/bundle-a2ui.sh)
A2UI 渲染器的实际 UI 实现由两部分组成:
vendor/a2ui/renderers/lit
apps/shared/OpenClawKit/Tools/CanvasA2UI
最终打包为一个 a2ui.bundle.js 文件,放置在 src/canvas-host/a2ui/ 目录下。
打包脚本使用 SHA-256 哈希校验 实现增量构建——只在源文件实际变更时才重新打包:
compute_hash 函数内嵌了一段 Node.js 脚本,递归遍历所有输入文件,对每个文件的相对路径和内容计算 SHA-256 哈希。哈希结果存储在 .bundle.hash 文件中。
在 Docker 构建中,vendor/ 和 apps/ 目录可能被 .dockerignore 排除。脚本对此有特殊处理:
这意味着 a2ui.bundle.js 被视为受版本控制的预构建产物 ——提交到仓库中,Docker 构建直接使用,不需要重新打包。
实际的构建分两步:
衍生解释——Rolldown :Rolldown 是一个用 Rust 编写的 JavaScript 打包工具(bundler),旨在成为 Rollup 的高性能替代品。它与 Rollup 共享配置格式(rolldown.config.mjs),但编译速度快得多。OpenClaw 选择 Rolldown 将 A2UI 的多个模块打包为一个 a2ui.bundle.js 文件,使其可以在 WebView 中通过单个 <script src="a2ui.bundle.js"> 加载。
衍生解释——Lit :Lit 是 Google 开发的轻量级 Web Components 库。它基于浏览器原生的 Custom Elements 和 Shadow DOM 标准,提供了响应式属性、模板字面量(html\...`)等便捷 API。OpenClaw 的 A2UI 渲染器使用 Lit 来实现 ` 自定义元素,将 JSONL 命令解析为实际的 DOM 元素。
Canvas Host 采用两层架构 :CanvasHostHandler(可嵌入)和 CanvasHostServer(独立运行),Handler 可以被嵌入 Gateway 的 HTTP 层
静态文件服务 从 ~/.openclaw/canvas/ 目录提供内容,通过三重路径安全机制(URL 规范化、.. 检测、openFileWithinRoot)防止路径遍历攻击
Live Reload 使用 chokidar 监视文件 + WebSocket 广播 + 75ms 防抖,实现文件变更时 WebView 自动刷新
所有 HTML 响应都会被自动注入 跨平台动作桥(iOS/Android JS Bridge)和 Live Reload 客户端
A2UI 根目录通过多候选路径搜索 定位,支持源码运行、dist 运行、打包为可执行文件等多种部署场景
A2UI 打包使用 SHA-256 增量构建 ——通过哈希校验跳过未变更的构建,Docker 环境使用预构建产物
构建工具链为 tsc (TypeScript 编译)+ Rolldown (Rust 打包器),产出单文件 a2ui.bundle.js