乐于分享
好东西不私藏

OpenClaw源码解读之原生客户端

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: [StringCheckedContinuation<GatewayFrameError>] = [:]    private var connected = false    private var backoffMs: Double = 500    private var lastSeq: Int?    private var watchdogTask: Task<VoidNever>?    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 = true        self.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(methodStringparams: [StringAnyCodable]?,                    timeoutMsDoubleasync throws -> Data {    let reqId = UUID().uuidString    let frame = GatewayFrame(type: "req", id: reqId, method: method, params: params)    try await self.sendFrame(frame)    return try await withCheckedThrowingContinuation { cont in        self.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 != nextOptionsKey        if 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: AppState    private let gatewayManager = GatewayProcessManager.shared    private let controlChannel = ControlChannel.shared    var body: some Scene {        MenuBarExtra {            MenuContent(stateself.state, updaterself.delegate.updaterController)        } label: {            CritterStatusLabel(                isPausedself.state.isPaused,                isSleepingself.isGatewaySleeping,                isWorkingself.state.isWorking,                gatewayStatusself.gatewayManager.status,                animationsEnabledself.state.iconAnimationsEnabled)        }    }}

状态栏图标 `CritterStatusLabel` 是动态的——根据 Gateway 状态(运行中/暂停/休眠/工作中)显示不同的动画。

Gateway 进程管理是 macOS 端的核心差异化功能。`GatewayProcessManager` 负责管理本地 Gateway 进程的生命周期:

// apps/macos/Sources/OpenClaw/GatewayProcessManager.swift@Observablefinal class GatewayProcessManager {    enum StatusEquatable {        case stopped        case starting        case running(details: String?)        case attachedExisting(details: String?)        case failed(String)    }    func setActive(_ activeBool) {        if CommandResolver.connectionModeIsRemote() {            self.stop()     // 远程模式不启动本地 Gateway            return        }        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 = .starting        Task {            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 OpenClawAppApp {    @State private var appModel: NodeAppModel    @State private var gatewayController: GatewayConnectionController    init() {        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 in                    Task { await self.appModel.handleDeepLink(url: url) }                }                .onChange(of: self.scenePhase) { _, newValue in                    self.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] = deferred        sendJson(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. 运维与测试

本站文章均为手工撰写未经允许谢绝转载:夜雨聆风 » OpenClaw源码解读之原生客户端

评论 抢沙发

7 + 8 =
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
×
订阅图标按钮