Revert "iOS: wire node services and tests"

This reverts commit 7b0a0f3dac.
This commit is contained in:
Mariano Belinky
2026-02-02 17:27:56 +00:00
parent c83bdb73a4
commit 4ab814fd50
59 changed files with 182 additions and 6234 deletions

View File

@@ -1,93 +0,0 @@
import Foundation
public enum OpenClawCalendarCommand: String, Codable, Sendable {
case events = "calendar.events"
case add = "calendar.add"
}
public struct OpenClawCalendarEventsParams: Codable, Sendable, Equatable {
public var startISO: String?
public var endISO: String?
public var limit: Int?
public init(startISO: String? = nil, endISO: String? = nil, limit: Int? = nil) {
self.startISO = startISO
self.endISO = endISO
self.limit = limit
}
}
public struct OpenClawCalendarAddParams: Codable, Sendable, Equatable {
public var title: String
public var startISO: String
public var endISO: String
public var isAllDay: Bool?
public var location: String?
public var notes: String?
public var calendarId: String?
public var calendarTitle: String?
public init(
title: String,
startISO: String,
endISO: String,
isAllDay: Bool? = nil,
location: String? = nil,
notes: String? = nil,
calendarId: String? = nil,
calendarTitle: String? = nil)
{
self.title = title
self.startISO = startISO
self.endISO = endISO
self.isAllDay = isAllDay
self.location = location
self.notes = notes
self.calendarId = calendarId
self.calendarTitle = calendarTitle
}
}
public struct OpenClawCalendarEventPayload: Codable, Sendable, Equatable {
public var identifier: String
public var title: String
public var startISO: String
public var endISO: String
public var isAllDay: Bool
public var location: String?
public var calendarTitle: String?
public init(
identifier: String,
title: String,
startISO: String,
endISO: String,
isAllDay: Bool,
location: String? = nil,
calendarTitle: String? = nil)
{
self.identifier = identifier
self.title = title
self.startISO = startISO
self.endISO = endISO
self.isAllDay = isAllDay
self.location = location
self.calendarTitle = calendarTitle
}
}
public struct OpenClawCalendarEventsPayload: Codable, Sendable, Equatable {
public var events: [OpenClawCalendarEventPayload]
public init(events: [OpenClawCalendarEventPayload]) {
self.events = events
}
}
public struct OpenClawCalendarAddPayload: Codable, Sendable, Equatable {
public var event: OpenClawCalendarEventPayload
public init(event: OpenClawCalendarEventPayload) {
self.event = event
}
}

View File

@@ -6,10 +6,4 @@ public enum OpenClawCapability: String, Codable, Sendable {
case screen
case voiceWake
case location
case device
case photos
case contacts
case calendar
case reminders
case motion
}

View File

@@ -1,23 +0,0 @@
import Foundation
public enum OpenClawChatCommand: String, Codable, Sendable {
case push = "chat.push"
}
public struct OpenClawChatPushParams: Codable, Sendable, Equatable {
public var text: String
public var speak: Bool?
public init(text: String, speak: Bool? = nil) {
self.text = text
self.speak = speak
}
}
public struct OpenClawChatPushPayload: Codable, Sendable, Equatable {
public var messageId: String?
public init(messageId: String? = nil) {
self.messageId = messageId
}
}

View File

@@ -1,85 +0,0 @@
import Foundation
public enum OpenClawContactsCommand: String, Codable, Sendable {
case search = "contacts.search"
case add = "contacts.add"
}
public struct OpenClawContactsSearchParams: Codable, Sendable, Equatable {
public var query: String?
public var limit: Int?
public init(query: String? = nil, limit: Int? = nil) {
self.query = query
self.limit = limit
}
}
public struct OpenClawContactsAddParams: Codable, Sendable, Equatable {
public var givenName: String?
public var familyName: String?
public var organizationName: String?
public var displayName: String?
public var phoneNumbers: [String]?
public var emails: [String]?
public init(
givenName: String? = nil,
familyName: String? = nil,
organizationName: String? = nil,
displayName: String? = nil,
phoneNumbers: [String]? = nil,
emails: [String]? = nil)
{
self.givenName = givenName
self.familyName = familyName
self.organizationName = organizationName
self.displayName = displayName
self.phoneNumbers = phoneNumbers
self.emails = emails
}
}
public struct OpenClawContactPayload: Codable, Sendable, Equatable {
public var identifier: String
public var displayName: String
public var givenName: String
public var familyName: String
public var organizationName: String
public var phoneNumbers: [String]
public var emails: [String]
public init(
identifier: String,
displayName: String,
givenName: String,
familyName: String,
organizationName: String,
phoneNumbers: [String],
emails: [String])
{
self.identifier = identifier
self.displayName = displayName
self.givenName = givenName
self.familyName = familyName
self.organizationName = organizationName
self.phoneNumbers = phoneNumbers
self.emails = emails
}
}
public struct OpenClawContactsSearchPayload: Codable, Sendable, Equatable {
public var contacts: [OpenClawContactPayload]
public init(contacts: [OpenClawContactPayload]) {
self.contacts = contacts
}
}
public struct OpenClawContactsAddPayload: Codable, Sendable, Equatable {
public var contact: OpenClawContactPayload
public init(contact: OpenClawContactPayload) {
self.contact = contact
}
}

View File

@@ -1,134 +0,0 @@
import Foundation
public enum OpenClawDeviceCommand: String, Codable, Sendable {
case status = "device.status"
case info = "device.info"
}
public enum OpenClawBatteryState: String, Codable, Sendable {
case unknown
case unplugged
case charging
case full
}
public enum OpenClawThermalState: String, Codable, Sendable {
case nominal
case fair
case serious
case critical
}
public enum OpenClawNetworkPathStatus: String, Codable, Sendable {
case satisfied
case unsatisfied
case requiresConnection
}
public enum OpenClawNetworkInterfaceType: String, Codable, Sendable {
case wifi
case cellular
case wired
case other
}
public struct OpenClawBatteryStatusPayload: Codable, Sendable, Equatable {
public var level: Double?
public var state: OpenClawBatteryState
public var lowPowerModeEnabled: Bool
public init(level: Double?, state: OpenClawBatteryState, lowPowerModeEnabled: Bool) {
self.level = level
self.state = state
self.lowPowerModeEnabled = lowPowerModeEnabled
}
}
public struct OpenClawThermalStatusPayload: Codable, Sendable, Equatable {
public var state: OpenClawThermalState
public init(state: OpenClawThermalState) {
self.state = state
}
}
public struct OpenClawStorageStatusPayload: Codable, Sendable, Equatable {
public var totalBytes: Int64
public var freeBytes: Int64
public var usedBytes: Int64
public init(totalBytes: Int64, freeBytes: Int64, usedBytes: Int64) {
self.totalBytes = totalBytes
self.freeBytes = freeBytes
self.usedBytes = usedBytes
}
}
public struct OpenClawNetworkStatusPayload: Codable, Sendable, Equatable {
public var status: OpenClawNetworkPathStatus
public var isExpensive: Bool
public var isConstrained: Bool
public var interfaces: [OpenClawNetworkInterfaceType]
public init(
status: OpenClawNetworkPathStatus,
isExpensive: Bool,
isConstrained: Bool,
interfaces: [OpenClawNetworkInterfaceType])
{
self.status = status
self.isExpensive = isExpensive
self.isConstrained = isConstrained
self.interfaces = interfaces
}
}
public struct OpenClawDeviceStatusPayload: Codable, Sendable, Equatable {
public var battery: OpenClawBatteryStatusPayload
public var thermal: OpenClawThermalStatusPayload
public var storage: OpenClawStorageStatusPayload
public var network: OpenClawNetworkStatusPayload
public var uptimeSeconds: Double
public init(
battery: OpenClawBatteryStatusPayload,
thermal: OpenClawThermalStatusPayload,
storage: OpenClawStorageStatusPayload,
network: OpenClawNetworkStatusPayload,
uptimeSeconds: Double)
{
self.battery = battery
self.thermal = thermal
self.storage = storage
self.network = network
self.uptimeSeconds = uptimeSeconds
}
}
public struct OpenClawDeviceInfoPayload: Codable, Sendable, Equatable {
public var deviceName: String
public var modelIdentifier: String
public var systemName: String
public var systemVersion: String
public var appVersion: String
public var appBuild: String
public var locale: String
public init(
deviceName: String,
modelIdentifier: String,
systemName: String,
systemVersion: String,
appVersion: String,
appBuild: String,
locale: String)
{
self.deviceName = deviceName
self.modelIdentifier = modelIdentifier
self.systemName = systemName
self.systemVersion = systemVersion
self.appVersion = appVersion
self.appBuild = appBuild
self.locale = locale
}
}

View File

@@ -110,13 +110,7 @@ private enum ConnectChallengeError: Error {
public actor GatewayChannelActor {
private let logger = Logger(subsystem: "ai.openclaw", category: "gateway")
#if DEBUG
private var debugEventLogCount = 0
private var debugMessageLogCount = 0
private var debugListenLogCount = 0
#endif
private var task: WebSocketTaskBox?
private var listenTask: Task<Void, Never>?
private var pending: [String: CheckedContinuation<GatewayFrame, Error>] = [:]
private var connected = false
private var isConnecting = false
@@ -175,9 +169,6 @@ public actor GatewayChannelActor {
self.tickTask?.cancel()
self.tickTask = nil
self.listenTask?.cancel()
self.listenTask = nil
self.task?.cancel(with: .goingAway, reason: nil)
self.task = nil
@@ -230,8 +221,6 @@ public actor GatewayChannelActor {
self.isConnecting = true
defer { self.isConnecting = false }
self.listenTask?.cancel()
self.listenTask = nil
self.task?.cancel(with: .goingAway, reason: nil)
self.task = self.session.makeWebSocketTask(url: self.url)
self.task?.resume()
@@ -259,7 +248,6 @@ public actor GatewayChannelActor {
throw wrapped
}
self.listen()
self.logger.info("gateway ws listen registered")
self.connected = true
self.backoffMs = 500
self.lastSeq = nil
@@ -432,44 +420,24 @@ public actor GatewayChannelActor {
}
private func listen() {
#if DEBUG
if self.debugListenLogCount < 3 {
self.debugListenLogCount += 1
self.logger.info("gateway ws listen start")
}
#endif
self.listenTask?.cancel()
self.listenTask = Task { [weak self] in
self.task?.receive { [weak self] result in
guard let self else { return }
defer { Task { await self.clearListenTask() } }
while !Task.isCancelled {
guard let task = await self.currentTask() else { return }
do {
let msg = try await task.receive()
switch result {
case let .failure(err):
Task { await self.handleReceiveFailure(err) }
case let .success(msg):
Task {
await self.handle(msg)
} catch {
if Task.isCancelled { return }
await self.handleReceiveFailure(error)
return
await self.listen()
}
}
}
}
private func clearListenTask() {
self.listenTask = nil
}
private func currentTask() -> WebSocketTaskBox? {
self.task
}
private func handleReceiveFailure(_ err: Error) async {
let wrapped = self.wrap(err, context: "gateway receive")
self.logger.error("gateway ws receive failed \(wrapped.localizedDescription, privacy: .public)")
self.connected = false
self.listenTask?.cancel()
self.listenTask = nil
await self.disconnectHandler?("receive failed: \(wrapped.localizedDescription)")
await self.failPending(wrapped)
await self.scheduleReconnect()
@@ -481,13 +449,6 @@ public actor GatewayChannelActor {
case let .string(s): s.data(using: .utf8)
@unknown default: nil
}
#if DEBUG
if self.debugMessageLogCount < 8 {
self.debugMessageLogCount += 1
let size = data?.count ?? 0
self.logger.info("gateway ws message received size=\(size, privacy: .public)")
}
#endif
guard let data else { return }
guard let frame = try? self.decoder.decode(GatewayFrame.self, from: data) else {
self.logger.error("gateway decode failed")
@@ -501,13 +462,6 @@ public actor GatewayChannelActor {
}
case let .event(evt):
if evt.event == "connect.challenge" { return }
#if DEBUG
if self.debugEventLogCount < 12 {
self.debugEventLogCount += 1
self.logger.info(
"gateway event received event=\(evt.event, privacy: .public) payload=\(evt.payload != nil, privacy: .public)")
}
#endif
if let seq = evt.seq {
if let last = lastSeq, seq > last + 1 {
await self.pushHandler?(.seqGap(expected: last + 1, received: seq))

View File

@@ -11,7 +11,6 @@ private struct NodeInvokeRequestPayload: Codable, Sendable {
var idempotencyKey: String?
}
public actor GatewayNodeSession {
private let logger = Logger(subsystem: "ai.openclaw", category: "node.gateway")
private let decoder = JSONDecoder()
@@ -24,78 +23,34 @@ public actor GatewayNodeSession {
private var onConnected: (@Sendable () async -> Void)?
private var onDisconnected: (@Sendable (String) async -> Void)?
private var onInvoke: (@Sendable (BridgeInvokeRequest) async -> BridgeInvokeResponse)?
private var hasNotifiedConnected = false
private var snapshotReceived = false
private var snapshotWaiters: [CheckedContinuation<Bool, Never>] = []
static func invokeWithTimeout(
request: BridgeInvokeRequest,
timeoutMs: Int?,
onInvoke: @escaping @Sendable (BridgeInvokeRequest) async -> BridgeInvokeResponse
) async -> BridgeInvokeResponse {
let timeoutLogger = Logger(subsystem: "ai.openclaw", category: "node.gateway")
let timeout: Int = {
guard let timeoutMs else { return 0 }
return max(0, timeoutMs)
}()
let timeout = max(0, timeoutMs ?? 0)
guard timeout > 0 else {
return await onInvoke(request)
}
// Use an explicit latch so timeouts win even if onInvoke blocks (e.g., permission prompts).
final class InvokeLatch: @unchecked Sendable {
private let lock = NSLock()
private var continuation: CheckedContinuation<BridgeInvokeResponse, Never>?
private var resumed = false
func setContinuation(_ continuation: CheckedContinuation<BridgeInvokeResponse, Never>) {
self.lock.lock()
defer { self.lock.unlock() }
self.continuation = continuation
}
func resume(_ response: BridgeInvokeResponse) {
let cont: CheckedContinuation<BridgeInvokeResponse, Never>?
self.lock.lock()
if self.resumed {
self.lock.unlock()
return
}
self.resumed = true
cont = self.continuation
self.continuation = nil
self.lock.unlock()
cont?.resume(returning: response)
}
}
let latch = InvokeLatch()
var onInvokeTask: Task<Void, Never>?
var timeoutTask: Task<Void, Never>?
defer {
onInvokeTask?.cancel()
timeoutTask?.cancel()
}
let response = await withCheckedContinuation { (cont: CheckedContinuation<BridgeInvokeResponse, Never>) in
latch.setContinuation(cont)
onInvokeTask = Task.detached {
let result = await onInvoke(request)
latch.resume(result)
}
timeoutTask = Task.detached {
return await withTaskGroup(of: BridgeInvokeResponse.self) { group in
group.addTask { await onInvoke(request) }
group.addTask {
try? await Task.sleep(nanoseconds: UInt64(timeout) * 1_000_000)
timeoutLogger.info("node invoke timeout fired id=\(request.id, privacy: .public)")
latch.resume(BridgeInvokeResponse(
return BridgeInvokeResponse(
id: request.id,
ok: false,
error: OpenClawNodeError(
code: .unavailable,
message: "node invoke timed out")
))
)
}
let first = await group.next()!
group.cancelAll()
return first
}
timeoutLogger.info("node invoke race resolved id=\(request.id, privacy: .public) ok=\(response.ok, privacy: .public)")
return response
}
private var serverEventSubscribers: [UUID: AsyncStream<EventFrame>.Continuation] = [:]
private var canvasHostUrl: String?
@@ -123,7 +78,6 @@ public actor GatewayNodeSession {
self.onInvoke = onInvoke
if shouldReconnect {
self.resetConnectionState()
if let existing = self.channel {
await existing.shutdown()
}
@@ -153,10 +107,7 @@ public actor GatewayNodeSession {
do {
try await channel.connect()
let snapshotReady = await self.waitForSnapshot(timeoutMs: 500)
if snapshotReady {
await self.notifyConnectedIfNeeded()
}
await onConnected()
} catch {
await onDisconnected(error.localizedDescription)
throw error
@@ -169,7 +120,6 @@ public actor GatewayNodeSession {
self.activeURL = nil
self.activeToken = nil
self.activePassword = nil
self.resetConnectionState()
}
public func currentCanvasHostUrl() -> String? {
@@ -229,8 +179,7 @@ public actor GatewayNodeSession {
case let .snapshot(ok):
let raw = ok.canvashosturl?.trimmingCharacters(in: .whitespacesAndNewlines)
self.canvasHostUrl = (raw?.isEmpty == false) ? raw : nil
self.markSnapshotReceived()
await self.notifyConnectedIfNeeded()
await self.onConnected?()
case let .event(evt):
await self.handleEvent(evt)
default:
@@ -238,98 +187,28 @@ public actor GatewayNodeSession {
}
}
private func resetConnectionState() {
self.hasNotifiedConnected = false
self.snapshotReceived = false
if !self.snapshotWaiters.isEmpty {
let waiters = self.snapshotWaiters
self.snapshotWaiters.removeAll()
for waiter in waiters {
waiter.resume(returning: false)
}
}
}
private func markSnapshotReceived() {
self.snapshotReceived = true
if !self.snapshotWaiters.isEmpty {
let waiters = self.snapshotWaiters
self.snapshotWaiters.removeAll()
for waiter in waiters {
waiter.resume(returning: true)
}
}
}
private func waitForSnapshot(timeoutMs: Int) async -> Bool {
if self.snapshotReceived { return true }
let clamped = max(0, timeoutMs)
return await withCheckedContinuation { cont in
self.snapshotWaiters.append(cont)
Task { [weak self] in
guard let self else { return }
try? await Task.sleep(nanoseconds: UInt64(clamped) * 1_000_000)
await self.timeoutSnapshotWaiters()
}
}
}
private func timeoutSnapshotWaiters() {
guard !self.snapshotReceived else { return }
if !self.snapshotWaiters.isEmpty {
let waiters = self.snapshotWaiters
self.snapshotWaiters.removeAll()
for waiter in waiters {
waiter.resume(returning: false)
}
}
}
private func notifyConnectedIfNeeded() async {
guard !self.hasNotifiedConnected else { return }
self.hasNotifiedConnected = true
await self.onConnected?()
}
private func handleEvent(_ evt: EventFrame) async {
self.broadcastServerEvent(evt)
guard evt.event == "node.invoke.request" else { return }
self.logger.info("node invoke request received")
guard let payload = evt.payload else { return }
do {
let request = try self.decodeInvokeRequest(from: payload)
let timeoutLabel = request.timeoutMs.map(String.init) ?? "none"
self.logger.info("node invoke request decoded id=\(request.id, privacy: .public) command=\(request.command, privacy: .public) timeoutMs=\(timeoutLabel, privacy: .public)")
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)
self.logger.info("node invoke executing id=\(request.id, privacy: .public)")
let response = await Self.invokeWithTimeout(
request: req,
timeoutMs: request.timeoutMs,
onInvoke: onInvoke
)
self.logger.info("node invoke completed id=\(request.id, privacy: .public) ok=\(response.ok, privacy: .public)")
await self.sendInvokeResult(request: request, response: response)
} catch {
self.logger.error("node invoke decode failed: \(error.localizedDescription, privacy: .public)")
}
}
private func decodeInvokeRequest(from payload: OpenClawProtocol.AnyCodable) throws -> NodeInvokeRequestPayload {
do {
let data = try self.encoder.encode(payload)
return try self.decoder.decode(NodeInvokeRequestPayload.self, from: data)
} catch {
if let raw = payload.value as? String, let data = raw.data(using: .utf8) {
return try self.decoder.decode(NodeInvokeRequestPayload.self, from: data)
}
throw error
}
}
private func sendInvokeResult(request: NodeInvokeRequestPayload, response: BridgeInvokeResponse) async {
guard let channel = self.channel else { return }
self.logger.info("node invoke result sending id=\(request.id, privacy: .public) ok=\(response.ok, privacy: .public)")
var params: [String: AnyCodable] = [
"id": AnyCodable(request.id),
"nodeId": AnyCodable(request.nodeId),
@@ -347,7 +226,7 @@ public actor GatewayNodeSession {
do {
try await channel.send(method: "node.invoke.result", params: params)
} catch {
self.logger.error("node invoke result failed id=\(request.id, privacy: .public) error=\(error.localizedDescription, privacy: .public)")
self.logger.error("node invoke result failed: \(error.localizedDescription, privacy: .public)")
}
}

View File

@@ -73,11 +73,6 @@ public final class GatewayTLSPinningSession: NSObject, WebSocketSessioning, URLS
if let expected {
if fingerprint == expected {
completionHandler(.useCredential, URLCredential(trust: trust))
} else if params.allowTOFU {
if let storeKey = params.storeKey {
GatewayTLSStore.saveFingerprint(fingerprint, stableID: storeKey)
}
completionHandler(.useCredential, URLCredential(trust: trust))
} else {
completionHandler(.cancelAuthenticationChallenge, nil)
}

View File

@@ -1,95 +0,0 @@
import Foundation
public enum OpenClawMotionCommand: String, Codable, Sendable {
case activity = "motion.activity"
case pedometer = "motion.pedometer"
}
public struct OpenClawMotionActivityParams: Codable, Sendable, Equatable {
public var startISO: String?
public var endISO: String?
public var limit: Int?
public init(startISO: String? = nil, endISO: String? = nil, limit: Int? = nil) {
self.startISO = startISO
self.endISO = endISO
self.limit = limit
}
}
public struct OpenClawMotionActivityEntry: Codable, Sendable, Equatable {
public var startISO: String
public var endISO: String
public var confidence: String
public var isWalking: Bool
public var isRunning: Bool
public var isCycling: Bool
public var isAutomotive: Bool
public var isStationary: Bool
public var isUnknown: Bool
public init(
startISO: String,
endISO: String,
confidence: String,
isWalking: Bool,
isRunning: Bool,
isCycling: Bool,
isAutomotive: Bool,
isStationary: Bool,
isUnknown: Bool)
{
self.startISO = startISO
self.endISO = endISO
self.confidence = confidence
self.isWalking = isWalking
self.isRunning = isRunning
self.isCycling = isCycling
self.isAutomotive = isAutomotive
self.isStationary = isStationary
self.isUnknown = isUnknown
}
}
public struct OpenClawMotionActivityPayload: Codable, Sendable, Equatable {
public var activities: [OpenClawMotionActivityEntry]
public init(activities: [OpenClawMotionActivityEntry]) {
self.activities = activities
}
}
public struct OpenClawPedometerParams: Codable, Sendable, Equatable {
public var startISO: String?
public var endISO: String?
public init(startISO: String? = nil, endISO: String? = nil) {
self.startISO = startISO
self.endISO = endISO
}
}
public struct OpenClawPedometerPayload: Codable, Sendable, Equatable {
public var startISO: String
public var endISO: String
public var steps: Int?
public var distanceMeters: Double?
public var floorsAscended: Int?
public var floorsDescended: Int?
public init(
startISO: String,
endISO: String,
steps: Int?,
distanceMeters: Double?,
floorsAscended: Int?,
floorsDescended: Int?)
{
self.startISO = startISO
self.endISO = endISO
self.steps = steps
self.distanceMeters = distanceMeters
self.floorsAscended = floorsAscended
self.floorsDescended = floorsDescended
}
}

View File

@@ -1,41 +0,0 @@
import Foundation
public enum OpenClawPhotosCommand: String, Codable, Sendable {
case latest = "photos.latest"
}
public struct OpenClawPhotosLatestParams: Codable, Sendable, Equatable {
public var limit: Int?
public var maxWidth: Int?
public var quality: Double?
public init(limit: Int? = nil, maxWidth: Int? = nil, quality: Double? = nil) {
self.limit = limit
self.maxWidth = maxWidth
self.quality = quality
}
}
public struct OpenClawPhotoPayload: Codable, Sendable, Equatable {
public var format: String
public var base64: String
public var width: Int
public var height: Int
public var createdAt: String?
public init(format: String, base64: String, width: Int, height: Int, createdAt: String? = nil) {
self.format = format
self.base64 = base64
self.width = width
self.height = height
self.createdAt = createdAt
}
}
public struct OpenClawPhotosLatestPayload: Codable, Sendable, Equatable {
public var photos: [OpenClawPhotoPayload]
public init(photos: [OpenClawPhotoPayload]) {
self.photos = photos
}
}

View File

@@ -1,82 +0,0 @@
import Foundation
public enum OpenClawRemindersCommand: String, Codable, Sendable {
case list = "reminders.list"
case add = "reminders.add"
}
public enum OpenClawReminderStatusFilter: String, Codable, Sendable {
case incomplete
case completed
case all
}
public struct OpenClawRemindersListParams: Codable, Sendable, Equatable {
public var status: OpenClawReminderStatusFilter?
public var limit: Int?
public init(status: OpenClawReminderStatusFilter? = nil, limit: Int? = nil) {
self.status = status
self.limit = limit
}
}
public struct OpenClawRemindersAddParams: Codable, Sendable, Equatable {
public var title: String
public var dueISO: String?
public var notes: String?
public var listId: String?
public var listName: String?
public init(
title: String,
dueISO: String? = nil,
notes: String? = nil,
listId: String? = nil,
listName: String? = nil)
{
self.title = title
self.dueISO = dueISO
self.notes = notes
self.listId = listId
self.listName = listName
}
}
public struct OpenClawReminderPayload: Codable, Sendable, Equatable {
public var identifier: String
public var title: String
public var dueISO: String?
public var completed: Bool
public var listName: String?
public init(
identifier: String,
title: String,
dueISO: String? = nil,
completed: Bool,
listName: String? = nil)
{
self.identifier = identifier
self.title = title
self.dueISO = dueISO
self.completed = completed
self.listName = listName
}
}
public struct OpenClawRemindersListPayload: Codable, Sendable, Equatable {
public var reminders: [OpenClawReminderPayload]
public init(reminders: [OpenClawReminderPayload]) {
self.reminders = reminders
}
}
public struct OpenClawRemindersAddPayload: Codable, Sendable, Equatable {
public var reminder: OpenClawReminderPayload
public init(reminder: OpenClawReminderPayload) {
self.reminder = reminder
}
}

View File

@@ -123,10 +123,6 @@
"screen_record": {
"label": "screen record",
"detailKeys": ["node", "nodeId", "duration", "durationMs", "fps", "screenIndex"]
},
"invoke": {
"label": "invoke",
"detailKeys": ["node", "nodeId", "invokeCommand"]
}
}
},

View File

@@ -1,28 +0,0 @@
import Foundation
public enum OpenClawTalkCommand: String, Codable, Sendable {
case pttStart = "talk.ptt.start"
case pttStop = "talk.ptt.stop"
case pttCancel = "talk.ptt.cancel"
case pttOnce = "talk.ptt.once"
}
public struct OpenClawTalkPTTStartPayload: Codable, Sendable, Equatable {
public var captureId: String
public init(captureId: String) {
self.captureId = captureId
}
}
public struct OpenClawTalkPTTStopPayload: Codable, Sendable, Equatable {
public var captureId: String
public var transcript: String?
public var status: String
public init(captureId: String, transcript: String?, status: String) {
self.captureId = captureId
self.transcript = transcript
self.status = status
}
}

View File

@@ -1,310 +0,0 @@
import Foundation
import Testing
@testable import OpenClawKit
import OpenClawProtocol
@Suite struct GatewayNodeInvokeTests {
@Test
func nodeInvokeRequestSendsInvokeResult() async throws {
let task = TestWebSocketTask()
let session = TestWebSocketSession(task: task)
task.enqueue(Self.makeEventMessage(
event: "connect.challenge",
payload: ["nonce": "test-nonce"]))
let tracker = InvokeTracker()
let gateway = GatewayNodeSession()
try await gateway.connect(
url: URL(string: "ws://127.0.0.1:18789")!,
token: nil,
password: "test-password",
connectOptions: GatewayConnectOptions(
role: "node",
scopes: [],
caps: [],
commands: ["device.info"],
permissions: [:],
clientId: "openclaw-ios",
clientMode: "node",
clientDisplayName: "Test iOS Node"),
sessionBox: WebSocketSessionBox(session: session),
onConnected: {},
onDisconnected: { _ in },
onInvoke: { req in
await tracker.set(req)
return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: "{\"ok\":true}")
})
task.enqueue(Self.makeEventMessage(
event: "node.invoke.request",
payload: [
"id": "invoke-1",
"nodeId": "node-1",
"command": "device.info",
"timeoutMs": 15000,
"idempotencyKey": "abc123",
]))
let resultFrame = try await waitForSentMethod(
task,
method: "node.invoke.result",
timeoutSeconds: 1.0)
let sentParams = resultFrame.params?.value as? [String: OpenClawProtocol.AnyCodable]
#expect(sentParams?["id"]?.value as? String == "invoke-1")
#expect(sentParams?["nodeId"]?.value as? String == "node-1")
#expect(sentParams?["ok"]?.value as? Bool == true)
let captured = await tracker.get()
#expect(captured?.command == "device.info")
#expect(captured?.id == "invoke-1")
}
@Test
func nodeInvokeRequestHandlesStringPayload() async throws {
let task = TestWebSocketTask()
let session = TestWebSocketSession(task: task)
task.enqueue(Self.makeEventMessage(
event: "connect.challenge",
payload: ["nonce": "test-nonce"]))
let tracker = InvokeTracker()
let gateway = GatewayNodeSession()
try await gateway.connect(
url: URL(string: "ws://127.0.0.1:18789")!,
token: nil,
password: "test-password",
connectOptions: GatewayConnectOptions(
role: "node",
scopes: [],
caps: [],
commands: ["device.info"],
permissions: [:],
clientId: "openclaw-ios",
clientMode: "node",
clientDisplayName: "Test iOS Node"),
sessionBox: WebSocketSessionBox(session: session),
onConnected: {},
onDisconnected: { _ in },
onInvoke: { req in
await tracker.set(req)
return BridgeInvokeResponse(id: req.id, ok: true)
})
let payload = """
{"id":"invoke-2","nodeId":"node-1","command":"device.info"}
"""
task.enqueue(Self.makeEventMessage(
event: "node.invoke.request",
payload: payload))
let resultFrame = try await waitForSentMethod(
task,
method: "node.invoke.result",
timeoutSeconds: 1.0)
let sentParams = resultFrame.params?.value as? [String: OpenClawProtocol.AnyCodable]
#expect(sentParams?["id"]?.value as? String == "invoke-2")
#expect(sentParams?["nodeId"]?.value as? String == "node-1")
#expect(sentParams?["ok"]?.value as? Bool == true)
let captured = await tracker.get()
#expect(captured?.command == "device.info")
#expect(captured?.id == "invoke-2")
}
}
private enum TestError: Error {
case timeout
}
private func waitForSentMethod(
_ task: TestWebSocketTask,
method: String,
timeoutSeconds: Double
) async throws -> RequestFrame {
try await AsyncTimeout.withTimeout(
seconds: timeoutSeconds,
onTimeout: { TestError.timeout },
operation: {
while true {
let frames = task.sentRequests()
if let match = frames.first(where: { $0.method == method }) {
return match
}
try? await Task.sleep(nanoseconds: 50_000_000)
}
})
}
private actor InvokeTracker {
private var request: BridgeInvokeRequest?
func set(_ req: BridgeInvokeRequest) {
self.request = req
}
func get() -> BridgeInvokeRequest? {
self.request
}
}
private final class TestWebSocketSession: WebSocketSessioning {
private let task: TestWebSocketTask
init(task: TestWebSocketTask) {
self.task = task
}
func makeWebSocketTask(url: URL) -> WebSocketTaskBox {
WebSocketTaskBox(task: self.task)
}
}
private final class TestWebSocketTask: WebSocketTasking, @unchecked Sendable {
private let lock = NSLock()
private var _state: URLSessionTask.State = .suspended
private var receiveQueue: [URLSessionWebSocketTask.Message] = []
private var receiveContinuations: [CheckedContinuation<URLSessionWebSocketTask.Message, Error>] = []
private var receiveHandlers: [@Sendable (Result<URLSessionWebSocketTask.Message, Error>) -> Void] = []
private var sent: [URLSessionWebSocketTask.Message] = []
var state: URLSessionTask.State {
self.lock.withLock { self._state }
}
func resume() {
self.lock.withLock { self._state = .running }
}
func cancel(with closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?) {
self.lock.withLock { self._state = .canceling }
}
func send(_ message: URLSessionWebSocketTask.Message) async throws {
self.lock.withLock { self.sent.append(message) }
guard let frame = Self.decodeRequestFrame(message) else { return }
guard frame.method == "connect" else { return }
let id = frame.id
let response = Self.connectResponse(for: id)
self.enqueue(.data(response))
}
func receive() async throws -> URLSessionWebSocketTask.Message {
try await withCheckedThrowingContinuation { cont in
var next: URLSessionWebSocketTask.Message?
self.lock.withLock {
if !self.receiveQueue.isEmpty {
next = self.receiveQueue.removeFirst()
} else {
self.receiveContinuations.append(cont)
}
}
if let next { cont.resume(returning: next) }
}
}
func receive(completionHandler: @escaping @Sendable (Result<URLSessionWebSocketTask.Message, Error>) -> Void) {
var next: URLSessionWebSocketTask.Message?
self.lock.withLock {
if !self.receiveQueue.isEmpty {
next = self.receiveQueue.removeFirst()
} else {
self.receiveHandlers.append(completionHandler)
}
}
if let next {
completionHandler(.success(next))
}
}
func enqueue(_ message: URLSessionWebSocketTask.Message) {
var handler: (@Sendable (Result<URLSessionWebSocketTask.Message, Error>) -> Void)?
var continuation: CheckedContinuation<URLSessionWebSocketTask.Message, Error>?
self.lock.withLock {
if !self.receiveHandlers.isEmpty {
handler = self.receiveHandlers.removeFirst()
} else if !self.receiveContinuations.isEmpty {
continuation = self.receiveContinuations.removeFirst()
} else {
self.receiveQueue.append(message)
}
}
if let handler {
handler(.success(message))
} else if let continuation {
continuation.resume(returning: message)
}
}
func sentRequests() -> [RequestFrame] {
let messages = self.lock.withLock { self.sent }
return messages.compactMap(Self.decodeRequestFrame)
}
private static func decodeRequestFrame(_ message: URLSessionWebSocketTask.Message) -> RequestFrame? {
let data: Data?
switch message {
case let .data(raw): data = raw
case let .string(text): data = text.data(using: .utf8)
@unknown default: data = nil
}
guard let data else { return nil }
return try? JSONDecoder().decode(RequestFrame.self, from: data)
}
private static func connectResponse(for id: String) -> Data {
let payload: [String: Any] = [
"type": "hello-ok",
"protocol": 3,
"server": [
"version": "dev",
"connId": "test-conn",
],
"features": [
"methods": [],
"events": [],
],
"snapshot": [
"presence": [],
"health": ["ok": true],
"stateVersion": ["presence": 0, "health": 0],
"uptimeMs": 0,
],
"policy": [
"maxPayload": 1,
"maxBufferedBytes": 1,
"tickIntervalMs": 1000,
],
]
let frame: [String: Any] = [
"type": "res",
"id": id,
"ok": true,
"payload": payload,
]
return (try? JSONSerialization.data(withJSONObject: frame)) ?? Data()
}
}
private extension GatewayNodeInvokeTests {
static func makeEventMessage(event: String, payload: Any) -> URLSessionWebSocketTask.Message {
let frame: [String: Any] = [
"type": "event",
"event": event,
"payload": payload,
]
let data = try? JSONSerialization.data(withJSONObject: frame)
return .data(data ?? Data())
}
}
private extension NSLock {
func withLock<T>(_ body: () -> T) -> T {
self.lock()
defer { self.unlock() }
return body()
}
}