151 lines
5.5 KiB
Swift
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)")
|
|
}
|
|
}
|
|
}
|