17.2 Canvas Host 实现

生成模型: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 的实现分为两层:

startCanvasHost()         ← 外层:创建 HTTP 服务器,绑定端口
  └── createCanvasHostHandler()  ← 内层:文件服务逻辑、Live Reload

这种分离允许 Canvas Handler 被嵌入到已有的 HTTP 服务器中(例如 Gateway 的 HTTP 层),而不必总是启动独立的服务器。

// 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 根目录准备

Canvas 的内容目录默认位于 ~/.openclaw/canvas/

服务器启动时会确保该目录存在,并在缺少 index.html 时写入一个默认的测试页面:

默认测试页面 defaultIndexHTML() 是一个内嵌的完整 HTML 字符串,包含四个按钮(Hello、Time、Photo、Dalek),用于验证 Canvas 和 A2UI 动作桥是否正常工作。

文件服务与路径安全

Canvas Host 的核心是一个静态文件服务器,但它必须防止路径遍历攻击(Path Traversal)——恶意请求通过 ../../etc/passwd 这样的路径试图访问根目录以外的文件。

路径安全通过三重机制保障:

层级
机制
说明

第一层

URL 规范化

decodeURIComponent + path.posix.normalize 消除编码绕过

第二层

.. 段检测

显式拒绝包含 .. 的路径段

第三层

openFileWithinRoot

底层安全函数(来自 src/infra/fs-safe.ts),通过 realpath 验证最终路径确实在根目录内

衍生解释——路径遍历攻击(Path Traversal):这是一种常见的 Web 安全漏洞。攻击者通过在 URL 中使用 ../(父目录引用)来尝试访问服务器上预期目录之外的文件。例如,请求 http://server/files/../../../etc/passwd 试图读取系统密码文件。防御方法包括:规范化路径、过滤 .. 段、验证最终路径是否在允许的目录内。OpenClaw 同时使用了这三种方法。

HTTP 请求处理

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.tsa2ui/

路径常量

A2UI 模块定义了三个关键路径:

所有路径都以 /__openclaw__/ 为前缀,这是一个不太可能与用户内容冲突的命名空间。

A2UI 根目录发现

A2UI 渲染器的资源文件(index.html + a2ui.bundle.js)可能位于不同位置,取决于运行方式:

发现策略的优先级反映了不同的部署场景:

优先级
候选路径
场景

1

process.execPath

打包为单文件可执行程序

2

当前模块同级 a2ui/

源码直接运行或 dist 已复制资源

3

../../src/canvas-host/a2ui

dist 运行但未复制资源,回退到源码

4

cwd()/src/canvas-host/a2ui

从仓库根目录运行

5

cwd()/dist/canvas-host/a2ui

从仓库根目录运行 dist

发现结果会被缓存(cachedA2uiRootReal),并通过 Promise 去重(resolvingA2uiRoot)避免并发时重复搜索。

A2UI HTTP 请求处理

handleA2uiHttpRequest 处理 /__openclaw__/a2ui 路径下的请求:

A2UI 的文件服务与 Canvas 共享相同的安全策略:拒绝 .. 段、拒绝符号链接、验证 realpath 在根目录内。

Live Reload 注入

injectCanvasLiveReload() 是 Canvas 和 A2UI 共享的关键函数,它将跨平台动作桥和 Live Reload 客户端注入到每个 HTML 响应中:

注入的脚本做了两件事:

  1. 动作桥:暴露 window.OpenClaw.sendUserAction()window.openclawSendUserAction() 全局函数

  2. Live Reload:建立到 /__openclaw__/ws 的 WebSocket 连接,收到 "reload" 消息时自动刷新页面


17.2.3 A2UI 打包(scripts/bundle-a2ui.sh

A2UI 渲染器的实际 UI 实现由两部分组成:

组件
路径
技术栈

Lit 渲染器

vendor/a2ui/renderers/lit

Lit Web Components

应用逻辑

apps/shared/OpenClawKit/Tools/CanvasA2UI

TypeScript + Rolldown

最终打包为一个 a2ui.bundle.js 文件,放置在 src/canvas-host/a2ui/ 目录下。

打包流程

增量构建:哈希校验

打包脚本使用 SHA-256 哈希校验 实现增量构建——只在源文件实际变更时才重新打包:

compute_hash 函数内嵌了一段 Node.js 脚本,递归遍历所有输入文件,对每个文件的相对路径和内容计算 SHA-256 哈希。哈希结果存储在 .bundle.hash 文件中。

Docker 环境处理

在 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 元素。

打包产物在架构中的位置


本节小结

  1. Canvas Host 采用两层架构CanvasHostHandler(可嵌入)和 CanvasHostServer(独立运行),Handler 可以被嵌入 Gateway 的 HTTP 层

  2. 静态文件服务~/.openclaw/canvas/ 目录提供内容,通过三重路径安全机制(URL 规范化、.. 检测、openFileWithinRoot)防止路径遍历攻击

  3. Live Reload 使用 chokidar 监视文件 + WebSocket 广播 + 75ms 防抖,实现文件变更时 WebView 自动刷新

  4. 所有 HTML 响应都会被自动注入跨平台动作桥(iOS/Android JS Bridge)和 Live Reload 客户端

  5. A2UI 根目录通过多候选路径搜索定位,支持源码运行、dist 运行、打包为可执行文件等多种部署场景

  6. A2UI 打包使用 SHA-256 增量构建——通过哈希校验跳过未变更的构建,Docker 环境使用预构建产物

  7. 构建工具链为 tsc(TypeScript 编译)+ Rolldown(Rust 打包器),产出单文件 a2ui.bundle.js

Last updated