OpenClaw源码解读之原生客户端

OpenClaw 提供 macOS、iOS 和 Android 三个原生客户端,它们共享同一套 Gateway WebSocket 协议与后端通信。macOS 和 iOS 共享一个 Swift 包 `OpenClawKit`,Android 用 Kotlin 独立实现。本文将从协议层、共享基础设施,到各平台的特有架构,逐层剖析原生客户端的实现。

一、统一协议层:所有客户端的通信基础
所有原生客户端与 Gateway 之间的通信都基于 WebSocket,帧格式为 JSON。协议定义了三种帧类型:
-
请求帧:`{“type”: “req”, “id”: “uuid”, “method”: “…”, “params”: {…}}`
-
响应帧:`{“type”: “res”, “id”: “uuid”, “ok”: true/false, “payload”: {…}}`
-
事件帧:`{“type”: “event”, “event”: “…”, “payload”: {…}, “seq”: 123}`
请求-响应通过 UUID 做 correlation。事件帧携带递增序列号 `seq`,客户端可以检测间隙判断是否丢失了事件。
连接建立后,客户端发送 `connect` 请求完成握手,携带客户端信息、认证凭据和能力声明:
// apps/shared/OpenClawKit/.../GatewayChannel.swift (sendConnect)var params: [String: ProtoAnyCodable] = ["minProtocol": ProtoAnyCodable(GATEWAY_PROTOCOL_VERSION),"maxProtocol": ProtoAnyCodable(GATEWAY_PROTOCOL_VERSION),"client": ProtoAnyCodable(client), // id, displayName, version, platform, mode"role": ProtoAnyCodable(role),"scopes": ProtoAnyCodable(scopes),"caps": ProtoAnyCodable(options.caps),"commands": ProtoAnyCodable(options.commands),"locale": ProtoAnyCodable(primaryLocale),"userAgent": ProtoAnyCodable(ProcessInfo.processInfo.operatingSystemVersionString),]
认证支持三种方式:设备身份签名(Ed25519)、共享 token 和密码。与 Web 控制台类似,首次连接时用设备密钥签名换取 `deviceToken`,后续复用。Gateway 返回的 `connect` 响应包含初始快照,一次性推送通道状态、Agent 列表等全量数据。
二、Swift 共享层:OpenClawKit
macOS 和 iOS 的网络层共享 `OpenClawKit` 包,核心是两个 Swift Actor。
GatewayChannelActor:底层 WebSocket 通道
`GatewayChannelActor` 管理原始的 WebSocket 连接,提供 `connect`、`request`、`send` 三个核心方法:
// apps/shared/OpenClawKit/.../GatewayChannel.swiftpublic actor GatewayChannelActor {private var task: WebSocketTaskBox?private var pending: [String: CheckedContinuation<GatewayFrame, Error>] = [:]private var connected = falseprivate var backoffMs: Double = 500private var lastSeq: Int?private var watchdogTask: Task<Void, Never>?public func connect() async throws {self.task = self.session.makeWebSocketTask(url: self.url)self.task?.resume()try await AsyncTimeout.withTimeout(seconds: self.connectTimeoutSeconds,operation: { try await self.sendConnect() })self.listen() // 开始接收消息self.connected = trueself.backoffMs = 500 // 重置退避}}
几个关键设计点:
-
使用 Swift Actor 保证并发安全:所有状态(`pending`、`connected`、`lastSeq`)都受 Actor 隔离保护,无需手动加锁
-
Watchdog 机制:一个后台 `watchdogLoop` 每 30 秒检查一次连接状态,如果断开则触发重连,防止指数退避停滞
-
Connect Waiter 队列:如果多个调用方同时请求连接,`connectWaiters` 数组收集所有等待的 continuation,连接成功后统一 resume,避免重复连接
-
请求超时:`connect` 有 6 秒超时,单个 RPC 默认 15 秒超时
`request` 方法通过 `CheckedContinuation` 实现 async/await 到回调的桥接——发送请求后将 continuation 存入 `pending` 字典(key 为请求 ID),收到响应后从字典取出并 resume:
public func request(method: String, params: [String: AnyCodable]?,timeoutMs: Double) async throws -> Data {let reqId = UUID().uuidStringlet frame = GatewayFrame(type: "req", id: reqId, method: method, params: params)try await self.sendFrame(frame)return try await withCheckedThrowingContinuation { cont inself.pending[reqId] = cont}}
GatewayNodeSession:节点会话层
`GatewayNodeSession` 在 `GatewayChannelActor` 之上封装了更高层的业务逻辑,是 iOS 端的主要网络接口:
// apps/shared/OpenClawKit/.../GatewayNodeSession.swiftpublic actor GatewayNodeSession {private var channel: GatewayChannelActor?public func connect(url:, token:, password:, connectOptions:, ...) async throws {let shouldReconnect = self.activeURL != url ||self.activeToken != token ||self.activeConnectOptionsKey != nextOptionsKeyif shouldReconnect {if let existing = self.channel { await existing.shutdown() }let channel = GatewayChannelActor(url: url, token: token, password: password,pushHandler: { [weak self] push in await self?.handlePush(push) },disconnectHandler: { [weak self] reason in await self?.handleChannelDisconnected(reason) })self.channel = channel}try await channel.connect()_ = await self.waitForSnapshot(timeoutMs: 500)await self.notifyConnectedIfNeeded()}}
它的职责包括:
-
连接参数变更检测:只有 URL、token 或 connectOptions 变化时才重建通道
-
快照等待:连接成功后等待 500ms 接收初始快照,确保 UI 有初始数据可展示
-
节点调用(Invoke):Gateway 可以向客户端”反向调用”——比如请求拍照、获取位置。`onInvoke` 回调处理这些请求,有超时保护:
static func invokeWithTimeout(request:, timeoutMs:, onInvoke:) async -> BridgeInvokeResponse {let latch = InvokeLatch()onInvokeTask = Task.detached {let result = await onInvoke(request)latch.resume(result)}timeoutTask = Task.detached {try? await Task.sleep(nanoseconds: UInt64(timeout) * 1_000_000)latch.resume(BridgeInvokeResponse(id: request.id, ok: false,error: OpenClawNodeError(code: .unavailable, message: "node invoke timed out")))}}
这是一个经典的”race”模式——业务处理和超时计时器竞争,先完成的赢。`InvokeLatch` 用 `NSLock` 保证只有一个 resume 生效。
-
服务器事件订阅:`subscribeServerEvents` 返回 `AsyncStream<EventFrame>`,使调用方可以用 `for await` 消费推送事件
三、macOS 应用:菜单栏与进程管理
macOS 端是一个菜单栏应用,通过 `MenuBarExtra` API 驻留在系统状态栏:
// apps/macos/Sources/OpenClaw/MenuBar.swift@mainstruct OpenClawApp: App {@State private var state: AppStateprivate let gatewayManager = GatewayProcessManager.sharedprivate let controlChannel = ControlChannel.sharedvar body: some Scene {MenuBarExtra {MenuContent(state: self.state, updater: self.delegate.updaterController)} label: {CritterStatusLabel(isPaused: self.state.isPaused,isSleeping: self.isGatewaySleeping,isWorking: self.state.isWorking,gatewayStatus: self.gatewayManager.status,animationsEnabled: self.state.iconAnimationsEnabled)}}}
状态栏图标 `CritterStatusLabel` 是动态的——根据 Gateway 状态(运行中/暂停/休眠/工作中)显示不同的动画。
Gateway 进程管理是 macOS 端的核心差异化功能。`GatewayProcessManager` 负责管理本地 Gateway 进程的生命周期:
// apps/macos/Sources/OpenClaw/GatewayProcessManager.swift@Observablefinal class GatewayProcessManager {enum Status: Equatable {case stoppedcase startingcase running(details: String?)case attachedExisting(details: String?)case failed(String)}func setActive(_ active: Bool) {if CommandResolver.connectionModeIsRemote() {self.stop() // 远程模式不启动本地 Gatewayreturn}if active {self.startIfNeeded()} else {self.stop()}}func startIfNeeded() {guard self.desiredActive else { return }switch self.status {case .starting, .running, .attachedExisting:return // 已经在运行或启动中,避免重复case .stopped, .failed:break}self.status = .startingTask {if await self.attachExistingGatewayIfAvailable() {return // 优先挂载已运行的 Gateway}await self.enableLaunchdGateway() // 否则通过 launchd 启动}}}
启动策略采用”先挂后启”——`attachExistingGatewayIfAvailable` 先尝试连接已运行的 Gateway 实例(可能是之前遗留的或通过 CLI 启动的),如果成功则直接使用,状态设为 `attachedExisting`。只有找不到已有实例时才通过 `enableLaunchdGateway` 调用 macOS 的 `launchd` 启动新的 Gateway 进程。
暂停/恢复通过 `onChange(of: self.state.isPaused)` 监听状态变化,暂停时调用 `gatewayManager.setActive(false)` 停止本地进程。远程模式下(`connectionMode == .remote`)不启动本地 Gateway,直接连接远端。
macOS 端还有几个平台特有功能模块:`WebChatManager` 管理聊天浮动面板、`CanvasManager` 管理 Canvas 窗口、`VoiceWakeManager` 管理语音唤醒、Sparkle 框架处理应用自动更新。
四、iOS 应用:节点模型与系统集成
iOS 端的入口极简——32 行代码:
// apps/ios/Sources/OpenClawApp.swift@mainstruct OpenClawApp: App {@State private var appModel: NodeAppModel@State private var gatewayController: GatewayConnectionControllerinit() {GatewaySettingsStore.bootstrapPersistence()let appModel = NodeAppModel()_appModel = State(initialValue: appModel)_gatewayController = State(initialValue: GatewayConnectionController(appModel: appModel))}var body: some Scene {WindowGroup {RootCanvas().environment(self.appModel).environment(self.appModel.voiceWake).environment(self.gatewayController).onOpenURL { url inTask { await self.appModel.handleDeepLink(url: url) }}.onChange(of: self.scenePhase) { _, newValue inself.appModel.setScenePhase(newValue)self.gatewayController.setScenePhase(newValue)}}}}
`NodeAppModel` 是 iOS 端的核心状态持有者,管理所有设备能力(相机、位置、日历、联系人、运动传感器)。`GatewayConnectionController` 封装了 `GatewayNodeSession`,处理连接/断开和 scene phase 变化(进入后台时断开、回到前台时重连)。
iOS 作为”节点”(node)连接到 Gateway,这意味着 Gateway 可以反向调用 iOS 设备的能力。例如,Agent 可以通过 `node.invoke.request` 要求 iOS 端拍照——`NodeRuntime` 收到调用后分发给 `CameraController`,拍照完成后将结果通过 `node.invoke.result` 返回给 Gateway。
聊天传输层通过 `IOSGatewayChatTransport` 实现 `OpenClawChatTransport` 协议,将聊天消息的发送和历史加载桥接到 Gateway RPC 调用。
五、Android 应用:Compose UI 与 OkHttp WebSocket
Android 端用 Kotlin + Jetpack Compose 构建,结构上类似 iOS 但有几个显著差异。
入口是 `MainActivity`,初始化时启动前台服务并设置设备能力:
// apps/android/.../MainActivity.ktclass MainActivity : ComponentActivity() {private val viewModel: MainViewModel by viewModels()override funonCreate(savedInstanceState: Bundle?) {NodeForegroundService.start(this)viewModel.camera.attachLifecycleOwner(this)viewModel.sms.attachPermissionRequester(permissionRequester)viewModel.screenRecorder.attachScreenCaptureRequester(screenCaptureRequester)setContent {OpenClawTheme {Surface { RootScreen(viewModel = viewModel) }}}}}
`NodeForegroundService` 是 Android 特有的设计——通过前台服务保持 WebSocket 连接不被系统杀死。Android 对后台进程限制严格,前台服务是唯一可靠的保活方式。
GatewaySession(Kotlin 版本)用 OkHttp 的 WebSocket 客户端实现连接,核心结构是一个 `Connection` 内部类:
// apps/android/.../gateway/GatewaySession.ktprivate inner class Connection(...) {private val connectDeferred = CompletableDeferred<Unit>()private val closedDeferred = CompletableDeferred<Unit>()private val client: OkHttpClient = buildClient()suspend funconnect() {val url = "$scheme://${endpoint.host}:${endpoint.port}"socket = client.newWebSocket(Request.Builder().url(url).build(), Listener())connectDeferred.await() // 等待握手完成}suspend funrequest(method: String, params: JsonElement?, timeoutMs: Long): RpcResponse {val id = UUID.randomUUID().toString()val deferred = CompletableDeferred<RpcResponse>()pending[id] = deferredsendJson(buildJsonObject {put("type", JsonPrimitive("req"))put("id", JsonPrimitive(id))put("method", JsonPrimitive(method))if (params != null) put("params", params)})return withTimeout(timeoutMs) { deferred.await() }}}
与 Swift 版本的 `CheckedContinuation` 对应,Kotlin 用 `CompletableDeferred` 实现异步等待。`writeLock` 是一个 `Mutex`,保证 WebSocket 的 `send` 不会并发。
Android 还有独特的 TLS 处理——`buildGatewayTlsConfig` 构建自定义的 `SSLSocketFactory` 和 `TrustManager`,支持自签名证书和 TLS 指纹固定(pinning),`onTlsFingerprint` 回调上报指纹用于信任验证。
`GatewayDiscovery` 通过 Android 的 NSD (Network Service Discovery) API 实现 mDNS 发现,服务类型为 `_openclaw-gw._tcp.`,与 macOS/iOS 的 Bonjour 发现兼容。
Android 端的 Canvas 通过 `CanvasController` 管理 WebView——`navigate`、`eval`(执行 JavaScript)、`snapshotBase64`(截图)等方法让 Agent 可以操作 WebView 内容。`NodeRuntime`(1270+ 行)是 Android 端最大的文件,聚合了所有设备能力的调度。
六、跨平台模式总结
三个平台虽然语言和框架不同,但遵循一致的架构模式:
-
连接管理:都实现了自动重连与指数退避(Swift 500ms 起步、Kotlin 类似),连接成功后重置退避。watchdog/runLoop 作为兜底确保连接最终恢复。
-
请求-响应匹配:都用 UUID correlation ID + 挂起字典(Swift `pending: [String: Continuation]`、Kotlin `pending: ConcurrentHashMap<String, CompletableDeferred>`)实现异步 RPC。
-
节点调用(双向通信):Gateway 不仅接收客户端请求,还可以反向调用客户端能力(拍照、录屏、GPS)。这种”节点”模式让移动设备成为 Agent 的感知延伸。
-
设备身份:所有平台都在本地持久化 Ed25519 密钥对(macOS/iOS 用 Keychain 或文件、Android 用 `DeviceIdentityStore`),首次连接签名握手换取 `deviceToken`。
-
生命周期适配:macOS 通过 launchd 管理 Gateway 进程;iOS 通过 `scenePhase` 管理前后台连接;Android 通过 `Foreground Service` 保活。各自用平台原生机制解决后台保持问题。
下面是讲解项目的基本信息:
-
项目地址:https://github.com/openclaw/openclaw
-
使用的项目分支是:main
-
commit版本是:f5160ca6becaeeb6a4dfd892fffd2130a696f766
讲解模块和顺序如下:
1. CLI 框架与进程模型
2. 配置系统
3. Gateway 核心
4. 通道与路由
5. Agent 引擎
6. 自动回复管线
7. 插件系统
8. 记忆系统
9. Web 控制台
10. 原生客户端(今日讲解)
11. 浏览器自动化
12. 运维与测试
夜雨聆风
