主题
OpenClaw 源码解读(十八)原生应用架构
一、导读
OpenClaw 不只是一个命令行工具——它拥有完整的 三端原生应用:macOS 菜单栏应用(Swift/SwiftUI)、iOS 移动端(Swift/SwiftUI)、Android 移动端(Kotlin/Jetpack Compose)。这三端应用共享同一个 Gateway WebSocket 协议,但各自有独特的平台能力和架构风格。
┌──────────────────────────────────────────────────────┐
│ Gateway (Node.js) │
│ WebSocket RPC │
└───────┬──────────────────┬──────────────────┬────────┘
│ │ │
┌────┴────┐ ┌────┴────┐ ┌────┴────┐
│ macOS │ │ iOS │ │ Android │
│ SwiftUI │ │ SwiftUI │ │ Compose │
│ MenuBar │ │ @Observable │ │ MVVM │
│ Actor │ │ 双连接 │ │ StateFlow │
└─────────┘ └─────────┘ └─────────┘代码位于 apps/macos/、apps/ios/、apps/android/,总计约 400+ 源文件。
原生应用架构/01-infographic-three-platform-overview-1775150787440.png)
二、macOS 应用 —— 菜单栏 + 进程管理
macOS 版是三端中功能最丰富的,因为它不仅是 Gateway 的客户端,还是 Gateway 的宿主——它负责启动、管理和维护本地 Gateway 进程。
2.1 入口 —— MenuBar.swift
swift
@main
struct OpenClawApp: App {
@NSApplicationDelegateAdaptor(AppDelegate.self) private var delegate
@State private var state: AppState
var body: some Scene {
MenuBarExtra { MenuContent(state: self.state, updater: ...) } label: {
CritterStatusLabel(...) // 动画菜单栏图标
}
.menuBarExtraStyle(.menu)
Settings {
SettingsRootView(state: self.state, ...)
}
}
}macOS 应用是一个 MenuBarExtra(纯菜单栏应用),没有 Dock 图标(可选开启)。入口只有两个 Scene:菜单栏下拉菜单 + 设置窗口。
鼠标交互精巧设计:
- 左键点击 → 弹出 WebChat 面板(浮动聊天窗口)
- 右键点击 → 弹出系统菜单(设置/状态)
- 悬停 → 显示 HoverHUD(快速状态)
2.2 AppState —— 状态中枢
AppState 使用 Swift Observation 框架(@Observable),管理 50+ 个持久化设置:
| 设置类别 | 典型属性 | 持久化 |
|---|---|---|
| 连接模式 | connectionMode(local/remote/unconfigured) | UserDefaults |
| 语音唤醒 | swabbleEnabled, swabbleTriggerWords | UserDefaults |
| 外观 | iconAnimationsEnabled, showDockIcon | UserDefaults |
| 安全 | execApprovalMode | ExecApprovalsStore |
| Canvas | canvasEnabled, canvasPanelVisible | UserDefaults |
| Talk | talkEnabled | UserDefaults |
每个属性的 didSet 都通过 ifNotPreview 保护——Preview 模式下不触发持久化,避免 Xcode 预览污染数据。
2.3 GatewayProcessManager —— 进程生命周期
macOS 独有的核心组件,管理本地 Gateway 进程:
swift
enum Status: Equatable {
case stopped
case starting
case running(details: String?)
case attachedExisting(details: String?) // 附着到已有进程
case failed(String)
}启动策略(4 步):
- 模式检查:Remote 模式跳过(不在本地启动 Gateway)
- 附着探测:尝试连接已运行的 Gateway(
attachExistingGatewayIfAvailable()) - Launchd 启动:通过
GatewayLaunchAgentManager启用 launchd 服务 - 健康探测:连接成功后执行
healthRPC 验证
自动恢复机制:当 request() 失败时,分三档延迟重试(150ms → 400ms → 900ms),并尝试 Tailscale 回退路径。
2.4 GatewayConnection —— Actor 模式
swift
actor GatewayConnection {
static let shared = GatewayConnection()
// ...
}使用 Swift Actor 隔离并发访问,保证线程安全。定义了 40+ 个 RPC Method(agent, status, chatSend, cronList...),通过类型安全的枚举避免字符串硬编码。
2.5 Canvas 生态
macOS 端拥有完整的 Canvas 画布系统:
CanvasManager— Canvas 窗口生命周期CanvasWindowController— 窗口控制器(位置/大小/导航)CanvasSchemeHandler— 自定义 URL Scheme 处理CanvasA2UIActionMessageHandler— A2UI 动作消息桥接CanvasFileWatcher— 文件变更监听
2.6 语音唤醒(VoiceWake)
macOS 端独有的语音唤醒系统:
VoiceWakeRuntime— 语音识别运行时VoiceWakeForwarder— 识别文本转发到 AgentVoiceWakeOverlay*— 浮动 UI 叠层- 支持多语言识别、自定义触发词、快捷键 Push-to-Talk
原生应用架构/02-infographic-macos-architecture-1775150788408.png)
三、iOS 应用 —— 双连接 + 能力路由
iOS 版是一个功能丰富的移动客户端,同时充当 Gateway 的"远程节点"。
3.1 NodeAppModel —— 双连接架构
iOS 最独特的架构设计是双 Gateway 连接:
swift
private let nodeGateway = GatewayNodeSession() // 节点连接(设备能力)
private let operatorGateway = GatewayNodeSession() // 操作连接(聊天/配置)为什么需要两个连接?
- Node 连接:注册设备能力(相机、位置、屏幕截图),处理 Gateway 的
node.invoke请求 - Operator 连接:处理聊天、配置、语音等交互操作
分离的好处是:Node 连接可以保持后台活跃(处理设备指令),而 Operator 连接只在前台使用。
3.2 能力路由器
iOS 端注册了 15+ 种设备能力:
| 能力 | 服务 | 说明 |
|---|---|---|
| Camera | CameraServicing | 拍照/录像/闪光灯 |
| Location | LocationServicing | GPS/显著位置变化 |
| Screen | ScreenController | 截屏/录屏 |
| Contacts | ContactsServicing | 通讯录读取 |
| Calendar | CalendarServicing | 日历事件 |
| Reminders | RemindersServicing | 提醒事项 |
| Photos | PhotosServicing | 相册访问 |
| Motion | MotionServicing | 运动传感器 |
| Notifications | NotificationCentering | 推送通知 |
| VoiceWake | VoiceWakeManager | 语音唤醒 |
| TalkMode | TalkModeManager | 对话模式 |
| WatchMessaging | WatchMessagingServicing | Apple Watch 消息 |
所有服务通过 Protocol 注入(依赖注入),支持测试替换。
3.3 后台生命周期
swift
var isBackgrounded: Bool = false
private var backgroundGraceTaskID: UIBackgroundTaskIdentifier = .invalid
private var backgroundReconnectSuppressed: Bool = false
private var backgroundReconnectLeaseUntil: Date?iOS 后台策略:
- 前台 → 后台:暂停语音唤醒、Talk 模式
- 后台宽限期:使用
UIApplication.beginBackgroundTask()保持短暂活跃 - 显著位置变化:通过
significantLocationChange在后台唤醒并执行任务 - 推送唤醒:通过 APNS 静默推送唤醒处理消息
- Watch 快速回复:Apple Watch 的回复排队,在连接恢复时发送
3.4 Deep Link 处理
swift
enum IOSDeepLinkAgentPolicy {
static let maxMessageChars = 20000
static let maxUnkeyedConfirmChars = 240
}支持 openclaw://agent?message=... 格式的 Deep Link,用于 Shortcuts/快捷指令集成。安全限制:超过 240 字符的未授权消息需要用户确认。
3.5 Gateway 信任机制
首次连接新 Gateway 时触发信任审批:
swift
var pendingGatewayTrust: GatewayNodeSession.TrustPromptEvent?
var gatewayPairingPaused: Bool = false // 暂停重连循环
var gatewayPairingRequestId: String?信任流程:Gateway TLS 指纹 → 用户确认 → 持久化信任 → 建立连接。
四、Android 应用 —— MVVM + StateFlow
Android 版使用 Kotlin + Jetpack Compose + MVVM 架构。
4.1 三层架构
MainActivity (Compose UI)
↓
MainViewModel (状态暴露)
↓
NodeRuntime (业务核心)
├── GatewaySession (WebSocket)
├── CanvasController
├── CameraCaptureManager
├── LocationCaptureManager
├── MicCaptureManager
├── 12+ Handler (能力分发)
└── InvokeDispatcher (命令路由)4.2 MainViewModel —— 状态透传
kotlin
class MainViewModel(app: Application) : AndroidViewModel(app) {
private val runtime: NodeRuntime = (app as NodeApp).runtime
val isConnected: StateFlow<Boolean> = runtime.isConnected
val chatMessages = runtime.chatMessages
val cameraEnabled: StateFlow<Boolean> = runtime.cameraEnabled
// ... 60+ StateFlow 属性
}ViewModel 是一个纯粹的状态透传层——所有 StateFlow 直接引用 NodeRuntime 的同名属性。方法也是一比一代理。这种设计让 ViewModel 极其轻薄(~200 行),所有逻辑集中在 NodeRuntime。
4.3 NodeRuntime —— 业务核心
NodeRuntime 是 Android 端的业务中枢(~600+ 行),管理:
| 组件 | 类型 | 职责 |
|---|---|---|
GatewaySession × 2 | operatorSession / nodeSession | 双连接(同 iOS) |
GatewayDiscovery | 服务发现 | 局域网 Gateway 发现 |
InvokeDispatcher | 命令路由 | 12+ Handler 的路由分发 |
ConnectionManager | 连接管理 | 参数构造/能力声明 |
ChatController | 聊天控制 | 消息/流式/会话管理 |
4.4 GatewaySession —— OkHttp WebSocket
kotlin
class GatewaySession(
private val scope: CoroutineScope,
private val identityStore: DeviceIdentityStore,
private val deviceAuthStore: DeviceAuthTokenStore,
private val onConnected: (serverName, remoteAddress, mainSessionKey) -> Unit,
private val onDisconnected: (message: String) -> Unit,
private val onEvent: (event, payloadJson) -> Unit,
private val onInvoke: (suspend (InvokeRequest) -> InvokeResult)?,
)基于 OkHttp WebSocket,协议帧格式与 Web UI 完全一致(req/res/event)。
关键差异:
- Android 端使用协程(
CoroutineScope+Dispatchers.IO),而非回调 - Mutex 写锁:
writeLock = Mutex()保证 WebSocket 写操作串行化 - Pending 请求:
ConcurrentHashMap<String, CompletableDeferred<RpcResponse>>,线程安全 - 连接超时:12s(高于默认值,适应低端设备)
4.5 InvokeDispatcher —— 能力处理
Android 端通过 12 个 Handler 实现设备能力:
| Handler | 职责 |
|---|---|
CameraHandler | 拍照/录像/HUD 控制 |
LocationHandler | GPS 定位 |
DeviceHandler | 设备信息(电量/存储/网络) |
NotificationsHandler | 通知权限/列表 |
SystemHandler | 系统操作(音量/亮度) |
PhotosHandler | 相册读取 |
ContactsHandler | 通讯录 |
CalendarHandler | 日历 |
MotionHandler | 加速度/计步器 |
ScreenHandler | 截屏/录屏 |
SmsHandler | 短信发送 |
A2UIHandler | Canvas A2UI 动作 |
DebugHandler | 调试信息 |
AppUpdateHandler | 应用更新 |
4.6 TLS 指纹信任
kotlin
private val onTlsFingerprint: ((stableId: String, fingerprint: String) -> Unit)?Android 端支持 TLS 证书指纹信任——首次连接时计算并存储证书指纹,后续连接验证指纹匹配。
五、三端共同架构模式
5.1 双连接模式
三端都使用 Operator + Node 双连接:
| 连接 | 角色 | 用途 |
|---|---|---|
| Operator | role: "operator" | 聊天、配置、语音 |
| Node | role: "node" | 设备能力、Canvas、node.invoke |
Operator 连接在前台交互使用,Node 连接可在后台保持。
5.2 Gateway 发现
三端都支持局域网自动发现 Gateway:
- macOS:通过
GatewayDiscoveryHelpers - iOS/Android:通过
GatewayDiscovery(mDNS/广播探测)
5.3 设备身份
三端都使用设备级密钥对进行认证:
- macOS:Keychain 存储
- iOS:Keychain 存储(
DeviceIdentityStore) - Android:加密 SharedPreferences(
DeviceIdentityStore)
5.4 Canvas A2UI
三端都支持 Canvas A2UI 渲染:
- macOS:
CanvasWindowController(独立窗口 + WKWebView) - iOS:
ScreenController(WebView 嵌入) - Android:
CanvasController(WebView 嵌入)
六、平台差异对比
| 维度 | macOS | iOS | Android |
|---|---|---|---|
| 语言 | Swift | Swift | Kotlin |
| UI 框架 | SwiftUI + AppKit | SwiftUI + UIKit | Jetpack Compose |
| 状态管理 | @Observable | @Observable | StateFlow |
| 并发模型 | Swift Actor | Swift Structured Concurrency | Kotlin Coroutines |
| Gateway 角色 | 宿主(启动+管理进程) | 纯客户端 | 纯客户端 |
| 后台策略 | 常驻(菜单栏应用) | 显著位置+推送唤醒 | 前台服务 |
| WebSocket | URLSession | URLSession | OkHttp |
| 密钥存储 | Keychain | Keychain | 加密 SharedPreferences |
| Canvas | 独立窗口 | 嵌入 WebView | 嵌入 WebView |
| 语音唤醒 | ✅(Speech Framework) | ✅(Speech Framework) | ❌(计划中) |
| Talk 模式 | ✅ | ✅ | ✅ |
| 特有能力 | 进程管理/Launchd | Apple Watch/Shortcuts | SMS 发送 |
七、设计模式总结
| 模式 | 应用位置 | 效果 |
|---|---|---|
| Actor Isolation | GatewayConnection (macOS) | 线程安全的共享连接 |
| MVVM | MainViewModel (Android) | UI/业务分离 |
| Protocol DI | NodeAppModel (iOS) | 15+ 服务可测试替换 |
| Dual Connection | 三端 | 前台交互与后台能力分离 |
| Command Dispatch | InvokeDispatcher (Android) | 12 Handler 路由 |
| State Hoisting | AppState (macOS) | 50+ 设置集中管理 |
| Auto Recovery | GatewayConnection.request() | 三档重试 + 路径回退 |
| Trust-on-First-Use | TLS 指纹 | 首次信任后验证 |
八、推荐阅读顺序
macOS:
apps/macos/Sources/OpenClaw/MenuBar.swift— 应用入口apps/macos/Sources/OpenClaw/AppState.swift— 状态中枢apps/macos/Sources/OpenClaw/GatewayProcessManager.swift— 进程管理apps/macos/Sources/OpenClaw/GatewayConnection.swift— Actor 连接apps/macos/Sources/OpenClaw/ControlChannel.swift— 控制频道apps/macos/Sources/OpenClaw/CanvasWindowController.swift— Canvas 窗口
iOS:
apps/ios/Sources/OpenClawApp.swift— 应用入口apps/ios/Sources/Model/NodeAppModel.swift— 双连接核心apps/ios/Sources/Gateway/GatewayNodeSession.swift— WebSocket 会话
Android:
apps/android/.../MainActivity.kt— Compose 入口apps/android/.../MainViewModel.kt— 状态透传apps/android/.../NodeRuntime.kt— 业务核心apps/android/.../gateway/GatewaySession.kt— OkHttp WebSocket
九、思考题
macOS 版同时是 Gateway 宿主和客户端——这种"自连接"模式有什么风险? 如果 Gateway 崩溃,App 如何检测和恢复?
双连接(Operator + Node)是必要的吗? 一个连接用不同的 scope 能否达到同样效果?分开的真正原因是什么?
Android 的 MainViewModel 是纯透传层——这算过度设计吗? 直接在 Compose 中引用 NodeRuntime 的 StateFlow 有什么问题?
iOS 后台策略使用"显著位置变化"唤醒——这对不需要位置功能的用户是否合理? 有没有更好的后台保活方案?
三端使用不同的密钥存储方案(Keychain vs 加密 SharedPreferences)——安全等级一致吗? 哪个更容易被攻击?