主题
OpenClaw 源码解读(二十二)Canvas 画布系统
1. 引言
Canvas 画布系统是 OpenClaw 提供的可视化渲染面板——它让 AI Agent 不再局限于纯文本输出,而是拥有了一块可以展示任意 Web 内容的"屏幕"。无论是在 macOS 菜单栏弹出一个浮动面板、在 iOS 全屏显示 A2UI 界面、还是在 Android 的 WebView 中呈现交互式 Canvas,背后都是同一套架构在驱动。
本文将从 Node.js 层的 Canvas Host HTTP 服务器讲起,延伸到 A2UI 前端框架、跨平台 JS Bridge、Agent 工具接口,最后深入各原生平台的实现细节。
2. 整体架构总览
Canvas 系统的分层结构如下:
┌─────────────────────────────────────────────────────┐
│ Agent Tool Layer (canvas-tool.ts) │
│ 7 actions: present/hide/navigate/eval/snapshot/ │
│ a2ui_push/a2ui_reset │
├─────────────────────────────────────────────────────┤
│ CLI Layer (register.canvas.ts) │
│ openclaw nodes canvas <subcommand> │
├─────────────────────────────────────────────────────┤
│ Gateway → Node IPC (node.invoke) │
├───────────────┬───────────────┬─────────────────────┤
│ macOS │ iOS │ Android │
│ WKWebView │ WKWebView │ WebView │
│ NSPanel │ ScreenTab │ Compose │
│ CanvasScheme │ NodeAppModel │ CanvasController │
├───────────────┴───────────────┴─────────────────────┤
│ Canvas Host Server (server.ts) │
│ HTTP 静态文件服务 + WebSocket Live Reload │
├─────────────────────────────────────────────────────┤
│ A2UI Frontend (a2ui/index.html + a2ui.bundle.js) │
│ 跨平台 JS Bridge + 动态 UI 渲染 │
├─────────────────────────────────────────────────────┤
│ File Resolver (file-resolver.ts) │
│ 安全路径解析 + 遍历防护 │
└─────────────────────────────────────────────────────┘核心数据流是 Agent → Gateway → Node Invoke → Canvas 操控,同时 A2UI action 事件可以反向从 Canvas WebView 通过 JS Bridge 回传到 Agent。
Canvas 画布系统/01-infographic-canvas-architecture-1775150670846.png)
3. Canvas Host HTTP 服务器
3.1 核心类型定义
src/canvas-host/server.ts 定义了两层抽象:
typescript
type CanvasHostHandler = {
rootDir: string;
basePath: string;
handleHttpRequest: (req, res) => Promise<boolean>;
handleUpgrade: (req, socket, head) => boolean;
close: () => Promise<void>;
};
type CanvasHostServer = {
port: number;
rootDir: string;
close: () => Promise<void>;
};Handler 是请求处理逻辑的封装,可以独立使用(嵌入到其他 HTTP 服务器中);Server 则是包含 Handler + HTTP 监听的完整服务。这种分层设计让 Canvas Host 既能独立运行,也能作为 Gateway 的子路由挂载。
3.2 服务启动流程
createCanvasHostHandler 的初始化分为四个阶段:
阶段一:环境检测
typescript
function isDisabledByEnv() {
if (isTruthyEnvValue(process.env.OPENCLAW_SKIP_CANVAS_HOST)) return true;
if (process.env.NODE_ENV === "test") return true;
if (process.env.VITEST) return true;
return false;
}在测试环境下 Canvas Host 默认关闭,可通过 allowInTests 显式开启。
阶段二:根目录准备
默认根目录为 ~/.openclaw/state/canvas。如果该目录下没有 index.html,会自动写入一个内置的默认测试页面,包含 Hello/Time/Photo/Dalek 四个交互按钮。
阶段三:文件监听器
使用 chokidar 监听根目录的文件变更:
typescript
const watcher = chokidar.watch(rootReal, {
ignoreInitial: true,
awaitWriteFinish: {
stabilityThreshold: 75, // 测试模式 12ms
pollInterval: 10, // 测试模式 5ms
},
ignored: [/(^|[\\/])\./, /(^|[\\/])node_modules/],
});文件变更时触发 防抖广播(75ms 防抖间隔),通过 WebSocket 向所有连接的客户端发送 "reload" 指令。
阶段四:WebSocket Server
typescript
const wss = new WebSocketServer({ noServer: true });使用 noServer 模式,由 HTTP upgrade 事件手动分发。WebSocket 路径为 /__openclaw__/ws。
Canvas 画布系统/02-infographic-server-startup-1775150671619.png)
3.3 HTTP 请求处理
请求处理链遵循责任链模式:
- 检查是否为 WebSocket upgrade 请求 → 返回 426
- 剥离 basePath 前缀(默认
/__openclaw__/canvas) - 仅允许 GET/HEAD 方法
- 通过
resolveFileWithinRoot安全解析文件 - HTML 文件自动注入 live-reload 脚本
- 非 HTML 文件直接返回,
Cache-Control: no-store
Canvas 画布系统/03-infographic-http-request-chain-1775150672520.png)
4. 安全文件解析器
src/canvas-host/file-resolver.ts 虽然只有 50 行,却是整个静态文件服务的安全基石。
4.1 三层安全防护
第一层:URL 规范化
typescript
function normalizeUrlPath(rawPath: string): string {
const decoded = decodeURIComponent(rawPath || "/");
return path.posix.normalize(decoded);
}第二层:路径遍历拦截
typescript
if (rel.split("/").some((p) => p === "..")) {
return null;
}即使经过 normalize 处理后仍然包含 ..,直接拒绝。
第三层:符号链接检测
typescript
const st = await fs.lstat(candidate);
if (st.isSymbolicLink()) return null;底层还依赖 openFileWithinRoot(来自 src/infra/fs-safe.ts),该函数使用 realpath 验证最终路径确实在根目录内,构成了第四道防线。
Canvas 画布系统/04-infographic-file-security-1775150673375.png)
4.2 目录请求处理
当请求路径是目录时(以 / 结尾或 lstat 判定为目录),自动尝试 index.html 回退——这是标准静态文件服务器的行为。
5. A2UI 模块
A2UI(App-to-UI)是 Canvas 系统中最精巧的部分——它实现了从 Agent 到 UI 的声明式界面推送。
5.1 路径常量
typescript
const A2UI_PATH = "/__openclaw__/a2ui";
const CANVAS_HOST_PATH = "/__openclaw__/canvas";
const CANVAS_WS_PATH = "/__openclaw__/ws";三个路径分别服务于:A2UI 前端资源、用户自定义 Canvas 文件、WebSocket live-reload 通道。
5.2 A2UI 资源发现
resolveA2uiRoot 在多达 10+ 个候选路径中搜索 A2UI 资源目录,覆盖了所有运行场景:
| 场景 | 候选路径 |
|---|---|
| 源码运行(bun) | <当前文件目录>/a2ui |
| dist 打包运行 | <当前文件目录>/canvas-host/a2ui |
| launchd 守护进程 | <当前文件目录>/../canvas-host/a2ui |
| 入口脚本回退 | <process.argv[1]目录>/a2ui |
| 开发回退 | src/canvas-host/a2ui、dist/canvas-host/a2ui |
验证条件是 index.html 和 a2ui.bundle.js 必须同时存在。结果会被缓存,null 结果有 10 秒重试间隔。
Canvas 画布系统/05-infographic-a2ui-discovery-1775150674123.png)
5.3 Live-Reload 注入
injectCanvasLiveReload 是一个关键函数,向 HTML 注入两段脚本:
跨平台 Action Bridge:
javascript
// iOS 桥接
window.webkit?.messageHandlers?.openclawCanvasA2UIAction?.postMessage(raw);
// Android 桥接
window.openclawCanvasA2UIAction?.postMessage(raw);桥接脚本同时在 globalThis 上注册四个 API:
OpenClaw.postMessage/openclawPostMessage— 底层消息发送OpenClaw.sendUserAction/openclawSendUserAction— 高级 action 发送(自动分配 UUID)
WebSocket Live-Reload 客户端:
javascript
const ws = new WebSocket(proto + "://" + location.host + "/__openclaw__/ws" + capQuery);
ws.onmessage = (ev) => {
if (String(ev.data) === "reload") location.reload();
};注入位置在 </body> 标签之前;如果找不到 </body>,则追加到文件末尾。
Canvas 画布系统/06-infographic-live-reload-bridge-1775150675134.png)
6. A2UI 前端页面
src/canvas-host/a2ui/index.html 是 A2UI 的宿主页面,设计精致:
6.1 视觉设计
页面使用暗色主题,背景由三层径向渐变组成:
css
background:
radial-gradient(... rgba(42, 113, 255, 0.18) ...), /* 蓝 */
radial-gradient(... rgba(255, 0, 138, 0.14) ...), /* 粉 */
radial-gradient(... rgba(0, 209, 255, 0.1) ...), /* 青 */
#000;两个伪元素分别实现网格漂移动画和光晕漂移动画,给等待画面带来生命感。Android 平台有更高的饱和度(因为 Android WebView 的色彩表现不同)。
6.2 DOM 结构
html
<canvas id="openclaw-canvas"></canvas> <!-- 全屏 2D Canvas -->
<div id="openclaw-status">...</div> <!-- 调试状态卡片 -->
<openclaw-a2ui-host></openclaw-a2ui-host> <!-- A2UI 自定义元素 -->
<script src="a2ui.bundle.js"></script> <!-- A2UI 运行时 -->四层 z-index 叠加:Canvas 元素 (z:1) → 背景动画 → 状态卡片 (z:3) → A2UI Host (z:4)。
6.3 全局 API
页面通过 window.__openclaw 暴露画布操控接口:
javascript
window.__openclaw = {
canvas, // HTMLCanvasElement
ctx, // CanvasRenderingContext2D
setDebugStatusEnabled, // (enabled: boolean) => void
setStatus, // (title, subtitle) => void
};Canvas 会自动处理 DPR(devicePixelRatio)缩放:
javascript
const dpr = window.devicePixelRatio || 1;
canvas.width = Math.floor(window.innerWidth * dpr);
canvas.height = Math.floor(window.innerHeight * dpr);
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);7. A2UI JSONL 协议
7.1 消息格式
A2UI 使用 JSONL(JSON Lines)格式传输 UI 描述,每行一个 JSON 对象,必须包含以下 action key 之一:
| Action Key | 版本 | 用途 |
|---|---|---|
beginRendering | v0.8 | 开始渲染指定 surface |
surfaceUpdate | v0.8 | 更新 surface 的组件树 |
dataModelUpdate | v0.8 | 更新数据模型 |
deleteSurface | v0.8 | 删除 surface |
createSurface | v0.9 | 创建 surface(仅 v0.9) |
Canvas 画布系统/07-infographic-jsonl-protocol-1775150676007.png)
7.2 快速文本生成
buildA2UITextJsonl 展示了最简单的 A2UI 使用场景——显示一段文本:
typescript
function buildA2UITextJsonl(text: string) {
const payloads = [
{
surfaceUpdate: {
surfaceId: "main",
components: [
{ id: "root", component: { Column: { children: { explicitList: ["text"] } } } },
{ id: "text", component: { Text: { text: { literalString: text }, usageHint: "body" } } },
],
},
},
{ beginRendering: { surfaceId: "main", root: "root" } },
];
return payloads.map(JSON.stringify).join("\n");
}两行 JSONL 就构成了一个完整的 UI 描述:先定义组件树,再触发渲染。
7.3 版本验证
validateA2UIJsonl 确保:
- 每行是有效 JSON 对象
- 每行恰好包含一个 action key
- 不允许 v0.8 和 v0.9 消息混合
- 当前 CLI 层显式拒绝 v0.9(
createSurface)
8. Agent Canvas Tool
src/agents/tools/canvas-tool.ts 将 Canvas 操控暴露给 AI Agent。
8.1 扁平化 Schema 设计
工具采用扁平化参数结构(所有 action 的参数合并为一个对象),而非嵌套联合类型,这是因为 Agent 工具 schema 有反对 Union 类型的约束:
typescript
const CanvasToolSchema = Type.Object({
action: stringEnum(CANVAS_ACTIONS),
target: Type.Optional(Type.String()), // present
url: Type.Optional(Type.String()), // navigate
javaScript: Type.Optional(Type.String()), // eval
outputFormat: optionalStringEnum([...]), // snapshot
jsonl: Type.Optional(Type.String()), // a2ui_push
jsonlPath: Type.Optional(Type.String()), // a2ui_push(文件路径)
x/y/width/height: Type.Optional(...), // present placement
});8.2 七个 Action
| Action | 功能 | 关键参数 |
|---|---|---|
present | 显示 Canvas | target/url, x/y/width/height |
hide | 隐藏 Canvas | — |
navigate | 导航到 URL | url(兼容 target) |
eval | 执行 JavaScript | javaScript |
snapshot | 截图 | outputFormat, maxWidth, quality |
a2ui_push | 推送 A2UI JSONL | jsonl 或 jsonlPath |
a2ui_reset | 重置 A2UI | — |
Canvas 画布系统/08-infographic-canvas-tool-actions-1775150676900.png)
8.3 JSONL 路径安全
readJsonlFromPath 对文件路径执行双重安全检查:
typescript
const roots = getDefaultMediaLocalRoots();
if (!isInboundPathAllowed({ filePath: resolved, roots })) {
throw new Error("jsonlPath outside allowed roots");
}
const canonical = await fs.realpath(resolved);
if (!isInboundPathAllowed({ filePath: canonical, roots })) {
throw new Error("jsonlPath outside allowed roots");
}先检查字面路径,再 realpath 后再次检查——防止符号链接绕过。
8.4 截图流水线
截图 action 的处理流程:
- 发送
canvas.snapshotinvoke 到目标 Node - Node 原生层截取 WebView 内容
- 返回 base64 编码的图片数据
- 写入临时文件
- 经过图片大小限制处理
- 返回
imageResult(可直接嵌入 Agent 对话)
9. CLI Canvas 命令
src/cli/nodes-cli/register.canvas.ts 注册了完整的 CLI 子命令树:
openclaw nodes canvas
├── snapshot --node <id> [--format png|jpg] [--max-width <px>]
├── present --node <id> [--target <url>] [--x/y/width/height]
├── hide --node <id>
├── navigate --node <id> <url>
├── eval --node <id> [<js>] [--js <code>]
└── a2ui
├── push --node <id> --jsonl <path> | --text <text>
└── reset --node <id>所有命令都通过 invokeCanvas 封装,最终调用 Gateway 的 node.invoke RPC。a2ui push 命令有两种快捷输入方式:
--jsonl <path>— 从文件读取完整 JSONL--text <text>— 自动生成包含该文本的 A2UI JSONL
Canvas 画布系统/09-infographic-cli-commands-1775150677631.png)
10. macOS 原生实现
macOS 的 Canvas 实现是三个平台中最复杂的,包含 12 个源文件。
Canvas 画布系统/10-infographic-macos-canvas-1775150678400.png)
10.1 CanvasManager 单例
swift
@MainActor
final class CanvasManager {
static let shared = CanvasManager()
private var panelController: CanvasWindowController?
private var panelSessionKey: String?
}CanvasManager 是 Canvas 子系统的入口,通过 @MainActor 确保所有操作在主线程执行。它维护当前活跃的面板控制器,并通过 startGatewayObserver() 监听 Gateway 推送,自动导航到 A2UI 页面。
10.2 CanvasWindowController
这是 Canvas 窗口的核心控制器,继承 NSWindowController 并实现 WKNavigationDelegate 和 NSWindowDelegate:
swift
@MainActor
final class CanvasWindowController: NSWindowController {
let sessionKey: String
let webView: WKWebView
private let schemeHandler: CanvasSchemeHandler
private let watcher: CanvasFileWatcher
}它管理着四个关键组件:
自定义 URL Scheme Handler:
通过 openclaw-canvas:// 协议,将 URL 请求映射到本地文件系统。这样 WKWebView 可以安全地加载本地 Canvas 内容,无需启动 HTTP 服务器。
文件监听器:
基于 FSEvents 的 CanvasFileWatcher(仅 12 行),当本地文件变更时自动刷新 WebView。
A2UI Action 桥接:
注入 JS 脚本,通过 WKScriptMessageHandler 将 DOM 事件回传到原生层。
窗口位置管理:
支持两种展示模式——标准窗口和浮动面板(NSPanel)。面板位置会通过 UserDefaults 持久化,并在屏幕边界内约束。
10.3 CanvasSchemeHandler
处理 openclaw-canvas:// 协议请求的完整流程:
openclaw-canvas://<session>/<path>
↓
路径遍历检测 → 文件查找 → MIME 推断 → 返回响应当没有 index.html 时,会显示内置的 scaffold 页面或 welcome 页面。支持的 MIME 类型覆盖了常见的 Web 资产格式。
10.4 导航策略
CanvasWindowController+Navigation.swift 定义了 WKNavigationDelegate 策略:
openclaw://deep link → 交给DeepLinkHandler- canvas scheme / https / http → 在面板内导航
- 其他 URL → 通过
NSWorkspace打开外部应用
10.5 UI 容器
CanvasChromeContainerView 实现了精致的悬停 UI:
- 鼠标进入时淡入关闭按钮和拖拽手柄
- 鼠标离开时淡出
- 右下角提供窗口大小调整手柄
- 背景使用毛玻璃效果
11. iOS 原生实现
iOS 的 Canvas 以全屏 WebView 为核心,由 RootCanvas 视图管理。
11.1 A2UI 自动导航
NodeAppModel+Canvas.swift 中的 resolveA2UIHostURL 构造 A2UI URL:
swift
func resolveA2UIHostURL() async -> String? {
guard let raw = await self.gatewaySession.currentCanvasHostUrl() else { return nil }
guard let base = URL(string: trimmed) else { return nil }
if let host = base.host, LoopbackHost.isLoopback(host) {
return nil // 拒绝 loopback 地址
}
return base.appendingPathComponent("__openclaw__/a2ui/").absoluteString + "?platform=ios"
}关键细节:loopback 地址会被过滤掉(因为 iOS 设备无法访问 Gateway 的本地地址)。
11.2 连接状态联动
swift
func showA2UIOnConnectIfNeeded() async {
guard let a2uiUrl = await resolveA2UIHostURL() else {
self.screen.showDefaultCanvas() // 回退到默认画面
return
}
if await Self.probeTCP(url: url, timeoutSeconds: 2.5) {
self.screen.navigate(to: a2uiUrl)
} else {
self.screen.showDefaultCanvas() // TCP 探测失败也回退
}
}在导航到 A2UI URL 之前,先进行 TCP 端口探测(2.5 秒超时)。这是因为 WKWebView 加载失败会留下一个持久的错误遮罩,所以宁可不尝试也不要显示错误页面。
11.3 RootCanvas 视图
RootCanvas.swift 是一个功能丰富的 SwiftUI 视图,集成了:
- Canvas WebView(ScreenTab)
- 聊天/设置/快速设置 Sheet
- 语音唤醒 Toast
- 相机闪光动画
- 连接状态药丸指示器
- Talk Mode 语音球
- 引导向导
12. Android 原生实现
12.1 CanvasController
CanvasController.kt 是 Android Canvas 的核心控制器:
kotlin
class CanvasController {
@Volatile private var webView: WebView? = null
@Volatile private var url: String? = null
fun attach(webView: WebView) { ... }
fun detach(webView: WebView) { ... }
fun navigate(url: String) { ... }
suspend fun eval(javaScript: String): String = ...
suspend fun snapshotBase64(format, quality, maxWidth): String = ...
}核心设计特点:
线程安全的 WebView 操作:
kotlin
private inline fun withWebViewOnMain(crossinline block: (WebView) -> Unit) {
val wv = webView ?: return
if (Looper.myLooper() == Looper.getMainLooper()) {
block(wv)
} else {
wv.post { block(wv) }
}
}WebView 操作必须在主线程执行,withWebViewOnMain 自动处理线程调度。
截图实现:
Android 端使用 WebView.draw(Canvas) 而非 PixelCopy API,因为后者对 WebView 的支持不可靠:
kotlin
private suspend fun WebView.captureBitmap(): Bitmap =
suspendCancellableCoroutine { cont ->
val bitmap = createBitmap(width, height, Bitmap.Config.ARGB_8888)
draw(Canvas(bitmap))
cont.resume(bitmap)
}12.2 CanvasScreen Composable
CanvasScreen.kt 使用 Jetpack Compose 的 AndroidView 包装 WebView:
kotlin
@Composable
fun CanvasScreen(viewModel: MainViewModel, modifier: Modifier) {
AndroidView(factory = {
WebView(context).apply {
settings.javaScriptEnabled = true
settings.domStorageEnabled = true
// 禁用暗色模式强制(Canvas 自己管理主题)
WebSettingsCompat.setAlgorithmicDarkeningAllowed(settings, false)
// 注入 A2UI Action Bridge
addJavascriptInterface(bridge, "openclawCanvasA2UIAction")
viewModel.canvas.attach(this)
}
})
}12.3 A2UIHandler
A2UIHandler.kt 处理 A2UI 相关的所有逻辑:
URL 构建:
kotlin
fun resolveA2uiHostUrl(): String? {
val raw = nodeUrl ?: operatorUrl
return "${raw.trimEnd('/')}/__openclaw__/a2ui/?platform=android"
}就绪检测:
kotlin
suspend fun ensureA2uiReady(a2uiUrl: String): Boolean {
canvas.navigate(a2uiUrl)
repeat(50) { // 最多等待 6 秒
val ready = canvas.eval(a2uiReadyCheckJS)
if (ready == "true") return true
delay(120)
}
return false
}通过轮询 openclawA2UI.applyMessages 是否可用来判断 A2UI 运行时是否就绪。
JSONL 解码与验证:
kotlin
fun decodeA2uiMessages(command: String, paramsJson: String?): String {
// 支持两种输入格式:
// 1. jsonl 字段(JSONL 文本,按行分割)
// 2. messages 数组(JSON Array)
// 两种格式都会经过 v0.8 验证
}12.4 A2UI Action 事件
OpenClawCanvasA2UIAction.kt 定义了 A2UI 事件的处理协议:
kotlin
fun formatAgentMessage(...): String {
return listOf(
"CANVAS_A2UI",
"action=${sanitizeTagValue(actionName)}",
"session=${sanitizeTagValue(sessionKey)}",
"surface=${sanitizeTagValue(surfaceId)}",
"component=${sanitizeTagValue(sourceComponentId)}",
"host=${sanitizeTagValue(host)}",
"instance=${sanitizeTagValue(instanceId)}$ctxSuffix",
"default=update_canvas",
).joinToString(separator = " ")
}A2UI action 事件被格式化为结构化的标签字符串,发送到 Gateway 作为 Agent 消息。sanitizeTagValue 确保标签值中只包含安全字符。
13. 跨平台 JS Bridge 对比
| 特性 | macOS | iOS | Android |
|---|---|---|---|
| WebView 引擎 | WKWebView | WKWebView | WebView (Chromium) |
| JS 注入方式 | WKScriptMessageHandler | 服务端 HTML 注入 | JavascriptInterface |
| 本地内容加载 | 自定义 URL Scheme | HTTP URL | file:///android_asset/ |
| 文件监听 | FSEvents | — | — |
| 截图方式 | WKWebView API | WKWebView API | Canvas.draw() |
| 暗色模式 | 系统自适应 | .preferredColorScheme(.dark) | 显式禁用 ForceDark |
三个平台都通过 openclawCanvasA2UIAction 通道名实现了统一的 A2UI 事件桥接。
Canvas 画布系统/11-infographic-cross-platform-bridge-1775150679287.png)
14. 配置体系
Canvas Host 的配置通过 CanvasHostConfig 类型管理:
typescript
type CanvasHostConfig = {
enabled?: boolean; // 是否启用
root?: string; // 服务目录(默认 ~/.openclaw/workspace/canvas)
port?: number; // HTTP 端口(默认 18793)
liveReload?: boolean; // 是否启用热更新(默认 true)
};配置层级中,Zod schema 验证了所有字段类型,schema.help.ts 提供了 CLI --help 的描述文本。
15. 设计亮点总结
| 设计点 | 实现策略 | 评价 |
|---|---|---|
| 文件安全 | 四层防护(normalize + .. 拦截 + symlink 检测 + realpath 验证) | 纵深防御 |
| Live Reload | chokidar + WebSocket + 防抖 | 开发体验优秀 |
| A2UI 资源发现 | 10+ 候选路径 + 缓存 + 重试 | 覆盖所有运行场景 |
| JS Bridge | 统一通道名 + 三平台适配 | 一套 Web 代码跨平台 |
| 工具 Schema | 扁平化参数 + 运行时验证 | 适配 Agent 工具约束 |
| 截图安全 | 路径双重校验(字面 + realpath) | 防止符号链接绕过 |
| JSONL 验证 | 版本检测 + action key 唯一性 | 前端后端双重校验 |
Canvas 画布系统/12-infographic-design-highlights-1775150680151.png)
16. 推荐阅读顺序
src/canvas-host/file-resolver.ts— 理解安全文件解析src/canvas-host/a2ui.ts— 理解 A2UI 资源路由和 live-reload 注入src/canvas-host/server.ts— 理解 Canvas Host 服务器完整流程src/canvas-host/a2ui/index.html— 理解前端视觉和 API 设计src/cli/nodes-cli/a2ui-jsonl.ts— 理解 A2UI JSONL 协议src/agents/tools/canvas-tool.ts— 理解 Agent 工具接口apps/macos/.../CanvasWindowController.swift— 理解 macOS 原生实现apps/android/.../CanvasController.kt— 理解 Android 原生实现apps/ios/.../NodeAppModel+Canvas.swift— 理解 iOS 原生实现
17. 思考题
- Canvas Host 的 chokidar 监听器在大型目录下可能产生性能问题,源码中如何处理这一场景?
- A2UI 为什么需要同时支持
jsonl字符串和messages数组两种输入格式? - iOS 端为什么在导航到 A2UI URL 前要做 TCP 探测?直接加载会有什么问题?
- macOS 使用自定义 URL Scheme 而 Android 使用 asset 文件,iOS 则通过 HTTP URL——三种方案各有什么优劣?
- A2UI v0.9 引入了
createSurface,为什么 CLI 层当前显式拒绝它?