原生客户端

一句话概括

三个原生客户端应用(macOS、iOS、Android)充当轻量 Node 客户端,通过 WebSocket 向 Gateway 暴露设备能力(相机、麦克风、屏幕、位置、通讯录等),同时提供本地聊天 UI 和语音交互。

职责

  • 通过 WebSocket 连接 Gateway,使用 Ed25519 设备认证和 TLS 证书锁定
  • 向 Gateway 广播设备能力(相机、屏幕、位置、语音、通讯录、日历等)
  • 执行 Gateway 调用的命令(camera.snaplocation.getscreen.recordcanvas.navigate 等)
  • 提供本地聊天界面,支持流式响应和 Markdown 渲染
  • 支持语音交互:唤醒词检测和按键说话
  • 通过内嵌 WebView 显示 A2UI Canvas,带 JavaScript 桥接

架构图

关键源码文件

共享(Apple 平台)

文件角色
apps/shared/OpenClawKit/共享 Swift 包:协议、会话管理、设备身份、命令定义、A2UI
apps/shared/OpenClawChatUI/共享聊天 UI:ChatViewModel、ChatView、ChatTransport 协议、Markdown 渲染
apps/shared/OpenClawProtocol/协议模型:GatewayModels、AnyCodable JSON 处理

macOS

文件角色
apps/macos/Sources/AppState.swift全局状态@Observable 单例,管理连接、设置、能力
apps/macos/Sources/CanvasWindowController.swiftCanvas 窗口:WKWebView 管理、A2UI 桥接、文件监听
apps/macos/Sources/VoiceWakeRuntime.swift语音唤醒:可配置触发词("Claude"、"Computer"、"Jarvis"),SwabbleKit 引擎
apps/macos/Sources/ExecApprovalsGatewayPrompter.swift执行审批:工具执行的操作员审批提示
apps/macos/Package.swift依赖:OpenClawKit、Sparkle 2.8、MenuBarExtraAccess、swift-subprocess

iOS

文件角色
apps/ios/Sources/OpenClawApp.swift入口:SwiftUI @main,AppDelegate 处理推送通知 + 后台任务
apps/ios/Sources/GatewayConnectionController.swift连接:Bonjour/mDNS 发现、TLS 锁定、WebSocket 生命周期
apps/ios/Sources/IOSGatewayChatTransport.swift聊天传输chat.sendchat.history、事件订阅
apps/ios/Sources/CanvasController.swiftCanvas:WKWebView + JavaScript 桥接(A2UI)
apps/ios/Sources/VoiceWakeManager.swift语音唤醒:Speech 框架,唤醒词检测

Android

文件角色
apps/android/.../MainActivity.kt入口:单个 ComponentActivity + Jetpack Compose
apps/android/.../NodeApp.ktApplication:初始化 NodeRuntime
apps/android/.../NodeRuntime.kt核心运行时:GatewaySession、ChatController、语音、相机、屏幕、位置处理器
apps/android/.../GatewaySession.ktWebSocket:OkHttp WebSocket,协议 v3,自动重连(350ms * 1.7^n,最大 8s)
apps/android/.../InvokeDispatcher.kt命令路由:将 Gateway 调用路由到平台处理器,含权限/前台检查
apps/android/.../DeviceIdentityStore.kt设备身份:Ed25519 密钥对存储在加密 SharedPreferences 中

WebSocket 协议(三个平台共享)

协议版本

所有应用使用协议 v3(min=3,max=3)。

连接握手

1. 客户端打开 WebSocket 到 ws://host:port 或 wss://host:port
2. 服务端发送 { type: "event", event: "connect.challenge", payload: { nonce } }
3. 客户端构造签名载荷:
   "v2|deviceId|clientId|clientMode|role|scopes|signedAtMs|token|nonce"
4. 客户端使用 Ed25519 私钥签名
5. 客户端发送 "connect" RPC,包含:
   {
     proto: { min: 3, max: 3 },
     client: { id, displayName, version, platform, mode },
     caps: ["canvas", "camera", "screen", "voiceWake", "location", ...],
     commands: [...支持的命令列表...],
     permissions: { camera, microphone, location, ... },
     role: "node",
     auth: { device: { deviceId, publicKey, signedAtMs, signature, token } }
   }
6. 服务端响应:
   {
     ok: true,
     server: { host },
     auth: { deviceToken },
     canvasHostUrl,
     snapshot: { sessionDefaults: { mainSessionKey } }
   }

Node 调用流程

Gateway → 客户端:
  { type: "event", event: "node.invoke.request",
    payload: { invokeId, command, params } }

客户端执行命令(如 camera.snap、location.get)

客户端 → Gateway:
  { type: "req", method: "node.invoke.result",
    params: { invokeId, result: {...} } }

  { type: "req", method: "node.invoke.result",
    params: { invokeId, error: { code, message } } }

各平台设备能力

能力iOSAndroidmacOS
canvasWKWebViewAndroid WebViewWKWebView
cameraAVFoundationCameraX-
screenReplayKitMediaProjection-
locationCoreLocationFusedLocation-
voiceWakeSpeech 框架Android SpeechRecognizerSwabbleKit
deviceUIDevice 信息Build 信息-
contactsContacts 框架--
calendarEventKit--
remindersEventKit--
photosPhotos 框架--
watchWatchConnectivity--
sms-SmsManager-

各平台 Node 命令

命令iOSAndroidmacOS
canvas.present / canvas.hide
canvas.navigate / canvas.evalJS
canvas.a2ui.push / a2ui.reset
canvas.snapshot
camera.snap / camera.clip-
screen.record-
location.get-
system.notify--
chat.push--
talk.pttStart / pttStop
contacts.search / contacts.add--
calendar.events / calendar.add--
reminders.list / reminders.add--
photos.latest--
watch.status / watch.notify--
motion.activity / motion.pedometer--
device.status / device.info-
sms.send--

平台差异

维度iOSAndroidmacOS
语言Swift 6KotlinSwift 6
UI 框架SwiftUIJetpack Compose(Material 3)SwiftUI + AppKit
最低版本iOS 18+API 31(Android 12)macOS 15+
应用类型全屏应用全屏应用菜单栏应用
WebSocketURLSessionWebSocketTask(via OpenClawKit)OkHttp 5.3URLSessionWebSocketTask(via OpenClawKit)
加密CryptoKit(Ed25519)BouncyCastle(Ed25519)CryptoKit(Ed25519)
后台运行BGAppRefreshTask + 静默推送前台 Service常驻运行
发现Bonjour/mDNSmDNS + dnsjava(Tailscale)Bonjour + 广域 DNS-SD
自动更新App Store / TestFlightAPK 自动更新Sparkle 2.8
架构模式MVVM + ObservationMVVM + StateFlowMVVM + Observation
WebViewWKWebViewAndroid WebViewWKWebView

TLS 与安全

证书锁定:
  - 服务端 TLS 证书的 SHA256 指纹
  - 首次信任(用户需一次性确认指纹)
  - 按 Gateway 存储在设备认证存储中

本地回环例外:
  - localhost/127.0.0.1 连接不要求 TLS

Tailscale 域名:
  - .ts.net 域名强制 TLS
  - 支持广域 DNS-SD 发现

设备身份:
  - 首次启动时生成 Ed25519 密钥对
  - 私钥存储于:
    - iOS:Keychain
    - Android:EncryptedSharedPreferences(BouncyCastle)
    - macOS:Keychain
  - 连接握手时发送公钥
  - 服务端返回设备 token,存储用于后续连接

A2UI Canvas 集成

三个平台均通过内嵌 WebView 实现 A2UI Canvas:

Gateway → Node:  "canvas.a2ui.push" { messages: [...] }
Node → WebView:  JavaScript 注入
WebView → Node:  window.openclawCanvasA2UIAction.postMessage(json)
Node → Gateway:  "node.invoke.result" { ... }

Canvas 生命周期:
  - canvas.present → 显示 WebView
  - canvas.navigate → 加载 URL
  - canvas.a2ui.push → 注入 A2UI 消息
  - canvas.snapshot → 截屏(base64)
  - canvas.hide → 隐藏 WebView

与其他模块的关系

  • 依赖

    • gateway/ — WebSocket 服务器处理所有通信,提供 canvas 宿主,管理设备认证
    • apps/shared/OpenClawKit/ — iOS + macOS 共享 Swift 包(协议、会话、身份、命令)
    • apps/shared/OpenClawChatUI/ — iOS + macOS 共享聊天 UI 组件
  • 被依赖

    • 无 — 原生应用是叶节点客户端

我的认知盲区

  • SwabbleKit 唤醒词引擎的精确内部实现 — 第三方依赖,非 OpenClaw 源码
  • macOS 上 PeekabooBridgeHostCoordinator 如何处理 UI 自动化 — 似乎是一个重要功能
  • Android 的 APK 自动更新机制(AppUpdateHandler)— 如何发现和应用更新?
  • Watch 应用功能 — 引用了 WatchConnectivity 但源码树中未找到实际的 watchOS 应用
  • Android 是否支持 iOS 的完整命令集(通讯录、日历、提醒、照片),还是这些仅限 iOS
  • 屏幕录制结果如何编码和传输 — WebSocket 帧的大小限制
  • 各平台语音唤醒的准确性和电池影响

变更频率

  • iOS:高 — 功能最丰富的平台,定期添加新命令和能力
  • Android:中 — 跟随 iOS 功能,有一些平台特定的补充(SMS、前台 Service)
  • macOS:中 — 菜单栏应用,功能集聚焦,Peekaboo 集成在演进
  • 共享(OpenClawKit):中 — 协议变更首先传播到这里,然后到平台代码
  • 协议(v3):低 — 线协议稳定;能力添加不需要协议变更