Files
clawdbot/apps/macos/Sources/Clawdbot/NodeMode/MacNodeGatewaySession.swift
2026-01-19 10:08:29 +00:00

151 lines
5.5 KiB
Swift

import ClawdbotKit
import ClawdbotProtocol
import Foundation
import OSLog
private struct NodeInvokeRequestPayload: Codable, Sendable {
var id: String
var nodeId: String
var command: String
var paramsJSON: String?
var timeoutMs: Int?
var idempotencyKey: String?
}
actor MacNodeGatewaySession {
private let logger = Logger(subsystem: "com.clawdbot", category: "node.gateway")
private let decoder = JSONDecoder()
private let encoder = JSONEncoder()
private var channel: GatewayChannelActor?
private var activeURL: URL?
private var activeToken: String?
private var activePassword: String?
private var connectOptions: GatewayConnectOptions?
private var onConnected: (@Sendable () async -> Void)?
private var onDisconnected: (@Sendable (String) async -> Void)?
private var onInvoke: (@Sendable (BridgeInvokeRequest) async -> BridgeInvokeResponse)?
func connect(
url: URL,
token: String?,
password: String?,
connectOptions: GatewayConnectOptions,
sessionBox: WebSocketSessionBox?,
onConnected: @escaping @Sendable () async -> Void,
onDisconnected: @escaping @Sendable (String) async -> Void,
onInvoke: @escaping @Sendable (BridgeInvokeRequest) async -> BridgeInvokeResponse
) async throws {
let shouldReconnect = self.activeURL != url ||
self.activeToken != token ||
self.activePassword != password ||
self.channel == nil
self.connectOptions = connectOptions
self.onConnected = onConnected
self.onDisconnected = onDisconnected
self.onInvoke = onInvoke
if shouldReconnect {
if let existing = self.channel {
await existing.shutdown()
}
let channel = GatewayChannelActor(
url: url,
token: token,
password: password,
session: sessionBox,
pushHandler: { [weak self] push in
await self?.handlePush(push)
},
connectOptions: connectOptions,
disconnectHandler: { [weak self] reason in
await self?.onDisconnected?(reason)
})
self.channel = channel
self.activeURL = url
self.activeToken = token
self.activePassword = password
}
guard let channel = self.channel else {
throw NSError(domain: "Gateway", code: 0, userInfo: [
NSLocalizedDescriptionKey: "gateway channel unavailable",
])
}
do {
try await channel.connect()
await onConnected()
} catch {
await onDisconnected(error.localizedDescription)
throw error
}
}
func disconnect() async {
await self.channel?.shutdown()
self.channel = nil
self.activeURL = nil
self.activeToken = nil
self.activePassword = nil
}
func sendEvent(event: String, payloadJSON: String?) async {
guard let channel = self.channel else { return }
let params: [String: ClawdbotProtocol.AnyCodable] = [
"event": ClawdbotProtocol.AnyCodable(event),
"payloadJSON": ClawdbotProtocol.AnyCodable(payloadJSON ?? NSNull()),
]
do {
_ = try await channel.request(method: "node.event", params: params, timeoutMs: 8000)
} catch {
self.logger.error("node event failed: \(error.localizedDescription, privacy: .public)")
}
}
private func handlePush(_ push: GatewayPush) async {
switch push {
case let .event(evt):
await self.handleEvent(evt)
default:
break
}
}
private func handleEvent(_ evt: EventFrame) async {
guard evt.event == "node.invoke.request" else { return }
guard let payload = evt.payload else { return }
do {
let data = try self.encoder.encode(payload)
let request = try self.decoder.decode(NodeInvokeRequestPayload.self, from: data)
guard let onInvoke else { return }
let req = BridgeInvokeRequest(id: request.id, command: request.command, paramsJSON: request.paramsJSON)
let response = await onInvoke(req)
await self.sendInvokeResult(request: request, response: response)
} catch {
self.logger.error("node invoke decode failed: \(error.localizedDescription, privacy: .public)")
}
}
private func sendInvokeResult(request: NodeInvokeRequestPayload, response: BridgeInvokeResponse) async {
guard let channel = self.channel else { return }
var params: [String: ClawdbotProtocol.AnyCodable] = [
"id": ClawdbotProtocol.AnyCodable(request.id),
"nodeId": ClawdbotProtocol.AnyCodable(request.nodeId),
"ok": ClawdbotProtocol.AnyCodable(response.ok),
"payloadJSON": ClawdbotProtocol.AnyCodable(response.payloadJSON ?? NSNull()),
]
if let error = response.error {
params["error"] = ClawdbotProtocol.AnyCodable([
"code": ClawdbotProtocol.AnyCodable(error.code.rawValue),
"message": ClawdbotProtocol.AnyCodable(error.message),
])
}
do {
_ = try await channel.request(method: "node.invoke.result", params: params, timeoutMs: 15000)
} catch {
self.logger.error("node invoke result failed: \(error.localizedDescription, privacy: .public)")
}
}
}