refactor(shared): dedupe common OpenClawKit helpers

This commit is contained in:
Peter Steinberger
2026-03-02 11:31:57 +00:00
parent 3dd01c3361
commit 2ca5722221
22 changed files with 685 additions and 129 deletions

View File

@@ -488,6 +488,20 @@ extension ChatTypingIndicatorBubble: @MainActor Equatable {
}
}
private extension View {
func assistantBubbleContainerStyle() -> some View {
self
.background(
RoundedRectangle(cornerRadius: 16, style: .continuous)
.fill(OpenClawChatTheme.assistantBubble))
.overlay(
RoundedRectangle(cornerRadius: 16, style: .continuous)
.strokeBorder(Color.white.opacity(0.08), lineWidth: 1))
.frame(maxWidth: ChatUIConstants.bubbleMaxWidth, alignment: .leading)
.focusable(false)
}
}
@MainActor
struct ChatStreamingAssistantBubble: View {
let text: String
@@ -498,14 +512,7 @@ struct ChatStreamingAssistantBubble: View {
ChatAssistantTextBody(text: self.text, markdownVariant: self.markdownVariant)
}
.padding(12)
.background(
RoundedRectangle(cornerRadius: 16, style: .continuous)
.fill(OpenClawChatTheme.assistantBubble))
.overlay(
RoundedRectangle(cornerRadius: 16, style: .continuous)
.strokeBorder(Color.white.opacity(0.08), lineWidth: 1))
.frame(maxWidth: ChatUIConstants.bubbleMaxWidth, alignment: .leading)
.focusable(false)
.assistantBubbleContainerStyle()
}
}
@@ -542,14 +549,7 @@ struct ChatPendingToolsBubble: View {
}
}
.padding(12)
.background(
RoundedRectangle(cornerRadius: 16, style: .continuous)
.fill(OpenClawChatTheme.assistantBubble))
.overlay(
RoundedRectangle(cornerRadius: 16, style: .continuous)
.strokeBorder(Color.white.opacity(0.08), lineWidth: 1))
.frame(maxWidth: ChatUIConstants.bubbleMaxWidth, alignment: .leading)
.focusable(false)
.assistantBubbleContainerStyle()
}
}

View File

@@ -0,0 +1,14 @@
import Foundation
public enum BonjourServiceResolverSupport {
public static func start(_ service: NetService, timeout: TimeInterval = 2.0) {
service.schedule(in: .main, forMode: .common)
service.resolve(withTimeout: timeout)
}
public static func normalizeHost(_ raw: String?) -> String? {
let trimmed = raw?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
guard !trimmed.isEmpty else { return nil }
return trimmed.hasSuffix(".") ? String(trimmed.dropLast()) : trimmed
}
}

View File

@@ -5,17 +5,7 @@ public enum OpenClawCalendarCommand: String, Codable, Sendable {
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 typealias OpenClawCalendarEventsParams = OpenClawDateRangeLimitParams
public struct OpenClawCalendarAddParams: Codable, Sendable, Equatable {
public var title: String

View File

@@ -0,0 +1,21 @@
import AVFoundation
public enum CameraAuthorization {
public static func isAuthorized(for mediaType: AVMediaType) async -> Bool {
let status = AVCaptureDevice.authorizationStatus(for: mediaType)
switch status {
case .authorized:
return true
case .notDetermined:
return await withCheckedContinuation(isolation: nil) { cont in
AVCaptureDevice.requestAccess(for: mediaType) { granted in
cont.resume(returning: granted)
}
}
case .denied, .restricted:
return false
@unknown default:
return false
}
}
}

View File

@@ -0,0 +1,151 @@
import AVFoundation
import Foundation
public enum CameraCapturePipelineSupport {
public static func preparePhotoSession(
preferFrontCamera: Bool,
deviceId: String?,
pickCamera: (_ preferFrontCamera: Bool, _ deviceId: String?) -> AVCaptureDevice?,
cameraUnavailableError: @autoclosure () -> Error,
mapSetupError: (CameraSessionConfigurationError) -> Error) throws
-> (session: AVCaptureSession, device: AVCaptureDevice, output: AVCapturePhotoOutput)
{
let session = AVCaptureSession()
session.sessionPreset = .photo
guard let device = pickCamera(preferFrontCamera, deviceId) else {
throw cameraUnavailableError()
}
do {
try CameraSessionConfiguration.addCameraInput(session: session, camera: device)
let output = try CameraSessionConfiguration.addPhotoOutput(session: session)
return (session, device, output)
} catch let setupError as CameraSessionConfigurationError {
throw mapSetupError(setupError)
}
}
public static func prepareMovieSession(
preferFrontCamera: Bool,
deviceId: String?,
includeAudio: Bool,
durationMs: Int,
pickCamera: (_ preferFrontCamera: Bool, _ deviceId: String?) -> AVCaptureDevice?,
cameraUnavailableError: @autoclosure () -> Error,
mapSetupError: (CameraSessionConfigurationError) -> Error) throws
-> (session: AVCaptureSession, output: AVCaptureMovieFileOutput)
{
let session = AVCaptureSession()
session.sessionPreset = .high
guard let camera = pickCamera(preferFrontCamera, deviceId) else {
throw cameraUnavailableError()
}
do {
try CameraSessionConfiguration.addCameraInput(session: session, camera: camera)
let output = try CameraSessionConfiguration.addMovieOutput(
session: session,
includeAudio: includeAudio,
durationMs: durationMs)
return (session, output)
} catch let setupError as CameraSessionConfigurationError {
throw mapSetupError(setupError)
}
}
public static func prepareWarmMovieSession(
preferFrontCamera: Bool,
deviceId: String?,
includeAudio: Bool,
durationMs: Int,
pickCamera: (_ preferFrontCamera: Bool, _ deviceId: String?) -> AVCaptureDevice?,
cameraUnavailableError: @autoclosure () -> Error,
mapSetupError: (CameraSessionConfigurationError) -> Error) async throws
-> (session: AVCaptureSession, output: AVCaptureMovieFileOutput)
{
let prepared = try self.prepareMovieSession(
preferFrontCamera: preferFrontCamera,
deviceId: deviceId,
includeAudio: includeAudio,
durationMs: durationMs,
pickCamera: pickCamera,
cameraUnavailableError: cameraUnavailableError(),
mapSetupError: mapSetupError)
prepared.session.startRunning()
await self.warmUpCaptureSession()
return prepared
}
public static func withWarmMovieSession<T>(
preferFrontCamera: Bool,
deviceId: String?,
includeAudio: Bool,
durationMs: Int,
pickCamera: (_ preferFrontCamera: Bool, _ deviceId: String?) -> AVCaptureDevice?,
cameraUnavailableError: @autoclosure () -> Error,
mapSetupError: (CameraSessionConfigurationError) -> Error,
operation: (AVCaptureMovieFileOutput) async throws -> T) async throws -> T
{
let prepared = try await self.prepareWarmMovieSession(
preferFrontCamera: preferFrontCamera,
deviceId: deviceId,
includeAudio: includeAudio,
durationMs: durationMs,
pickCamera: pickCamera,
cameraUnavailableError: cameraUnavailableError(),
mapSetupError: mapSetupError)
defer { prepared.session.stopRunning() }
return try await operation(prepared.output)
}
public static func mapMovieSetupError<E: Error>(
_ setupError: CameraSessionConfigurationError,
microphoneUnavailableError: @autoclosure () -> E,
captureFailed: (String) -> E) -> E
{
if case .microphoneUnavailable = setupError {
return microphoneUnavailableError()
}
return captureFailed(setupError.localizedDescription)
}
public static func makePhotoSettings(output: AVCapturePhotoOutput) -> AVCapturePhotoSettings {
let settings: AVCapturePhotoSettings = {
if output.availablePhotoCodecTypes.contains(.jpeg) {
return AVCapturePhotoSettings(format: [AVVideoCodecKey: AVVideoCodecType.jpeg])
}
return AVCapturePhotoSettings()
}()
settings.photoQualityPrioritization = .quality
return settings
}
public static func capturePhotoData(
output: AVCapturePhotoOutput,
makeDelegate: (CheckedContinuation<Data, Error>) -> any AVCapturePhotoCaptureDelegate) async throws -> Data
{
var delegate: (any AVCapturePhotoCaptureDelegate)?
let rawData: Data = try await withCheckedThrowingContinuation { cont in
let captureDelegate = makeDelegate(cont)
delegate = captureDelegate
output.capturePhoto(with: self.makePhotoSettings(output: output), delegate: captureDelegate)
}
withExtendedLifetime(delegate) {}
return rawData
}
public static func warmUpCaptureSession() async {
// A short delay after `startRunning()` significantly reduces "blank first frame" captures on some devices.
try? await Task.sleep(nanoseconds: 150_000_000) // 150ms
}
public static func positionLabel(_ position: AVCaptureDevice.Position) -> String {
switch position {
case .front: "front"
case .back: "back"
default: "unspecified"
}
}
}

View File

@@ -0,0 +1,70 @@
import AVFoundation
import CoreMedia
public enum CameraSessionConfigurationError: LocalizedError {
case addCameraInputFailed
case addPhotoOutputFailed
case microphoneUnavailable
case addMicrophoneInputFailed
case addMovieOutputFailed
public var errorDescription: String? {
switch self {
case .addCameraInputFailed:
"Failed to add camera input"
case .addPhotoOutputFailed:
"Failed to add photo output"
case .microphoneUnavailable:
"Microphone unavailable"
case .addMicrophoneInputFailed:
"Failed to add microphone input"
case .addMovieOutputFailed:
"Failed to add movie output"
}
}
}
public enum CameraSessionConfiguration {
public static func addCameraInput(session: AVCaptureSession, camera: AVCaptureDevice) throws {
let input = try AVCaptureDeviceInput(device: camera)
guard session.canAddInput(input) else {
throw CameraSessionConfigurationError.addCameraInputFailed
}
session.addInput(input)
}
public static func addPhotoOutput(session: AVCaptureSession) throws -> AVCapturePhotoOutput {
let output = AVCapturePhotoOutput()
guard session.canAddOutput(output) else {
throw CameraSessionConfigurationError.addPhotoOutputFailed
}
session.addOutput(output)
output.maxPhotoQualityPrioritization = .quality
return output
}
public static func addMovieOutput(
session: AVCaptureSession,
includeAudio: Bool,
durationMs: Int) throws -> AVCaptureMovieFileOutput
{
if includeAudio {
guard let mic = AVCaptureDevice.default(for: .audio) else {
throw CameraSessionConfigurationError.microphoneUnavailable
}
let micInput = try AVCaptureDeviceInput(device: mic)
guard session.canAddInput(micInput) else {
throw CameraSessionConfigurationError.addMicrophoneInputFailed
}
session.addInput(micInput)
}
let output = AVCaptureMovieFileOutput()
guard session.canAddOutput(output) else {
throw CameraSessionConfigurationError.addMovieOutputFailed
}
session.addOutput(output)
output.maxRecordedDuration = CMTime(value: Int64(durationMs), timescale: 1000)
return output
}
}

View File

@@ -0,0 +1,24 @@
import Foundation
public enum CaptureRateLimits {
public static func clampDurationMs(
_ ms: Int?,
defaultMs: Int = 10_000,
minMs: Int = 250,
maxMs: Int = 60_000) -> Int
{
let value = ms ?? defaultMs
return min(maxMs, max(minMs, value))
}
public static func clampFps(
_ fps: Double?,
defaultFps: Double = 10,
minFps: Double = 1,
maxFps: Double) -> Double
{
let value = fps ?? defaultFps
guard value.isFinite else { return defaultFps }
return min(maxFps, max(minFps, value))
}
}

View File

@@ -1,5 +1,4 @@
import Foundation
import Network
public enum DeepLinkRoute: Sendable, Equatable {
case agent(AgentDeepLink)
@@ -21,40 +20,6 @@ public struct GatewayConnectDeepLink: Codable, Sendable, Equatable {
self.password = password
}
fileprivate static func isLoopbackHost(_ raw: String) -> Bool {
var host = raw
.trimmingCharacters(in: .whitespacesAndNewlines)
.lowercased()
.trimmingCharacters(in: CharacterSet(charactersIn: "[]"))
if host.hasSuffix(".") {
host.removeLast()
}
if let zoneIndex = host.firstIndex(of: "%") {
host = String(host[..<zoneIndex])
}
if host.isEmpty {
return false
}
if host == "localhost" || host == "0.0.0.0" || host == "::" {
return true
}
if let ipv4 = IPv4Address(host) {
return ipv4.rawValue.first == 127
}
if let ipv6 = IPv6Address(host) {
let bytes = Array(ipv6.rawValue)
let isV6Loopback = bytes[0..<15].allSatisfy { $0 == 0 } && bytes[15] == 1
if isV6Loopback {
return true
}
let isMappedV4 = bytes[0..<10].allSatisfy { $0 == 0 } && bytes[10] == 0xFF && bytes[11] == 0xFF
return isMappedV4 && bytes[12] == 127
}
return false
}
public var websocketURL: URL? {
let scheme = self.tls ? "wss" : "ws"
return URL(string: "\(scheme)://\(self.host):\(self.port)")
@@ -72,7 +37,7 @@ public struct GatewayConnectDeepLink: Codable, Sendable, Equatable {
let scheme = (parsed.scheme ?? "ws").lowercased()
guard scheme == "ws" || scheme == "wss" else { return nil }
let tls = scheme == "wss"
if !tls, !Self.isLoopbackHost(hostname) {
if !tls, !LoopbackHost.isLoopbackHost(hostname) {
return nil
}
let port = parsed.port ?? (tls ? 443 : 18789)
@@ -167,7 +132,7 @@ public enum DeepLinkParser {
}
let port = query["port"].flatMap { Int($0) } ?? 18789
let tls = (query["tls"] as NSString?)?.boolValue ?? false
if !tls, !GatewayConnectDeepLink.isLoopbackHost(hostParam) {
if !tls, !LoopbackHost.isLoopbackHost(hostParam) {
return nil
}
return .gateway(

View File

@@ -45,11 +45,7 @@ public struct WebSocketTaskBox: @unchecked Sendable {
public func sendPing() async throws {
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
self.task.sendPing { error in
if let error {
continuation.resume(throwing: error)
} else {
continuation.resume(returning: ())
}
ThrowingContinuationSupport.resumeVoid(continuation, error: error)
}
}
}
@@ -560,8 +556,7 @@ public actor GatewayChannelActor {
guard let frame = try? self.decoder.decode(GatewayFrame.self, from: data) else { continue }
if case let .event(evt) = frame, evt.event == "connect.challenge",
let payload = evt.payload?.value as? [String: ProtoAnyCodable],
let nonce = payload["nonce"]?.value as? String,
nonce.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false
let nonce = GatewayConnectChallengeSupport.nonce(from: payload)
{
return nonce
}

View File

@@ -0,0 +1,28 @@
import Foundation
import OpenClawProtocol
public enum GatewayConnectChallengeSupport {
public static func nonce(from payload: [String: OpenClawProtocol.AnyCodable]?) -> String? {
guard let nonce = payload?["nonce"]?.value as? String else { return nil }
let trimmed = nonce.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return nil }
return trimmed
}
public static func waitForNonce<E: Error>(
timeoutSeconds: Double,
onTimeout: @escaping @Sendable () -> E,
receiveNonce: @escaping @Sendable () async throws -> String?) async throws -> String
{
try await AsyncTimeout.withTimeout(
seconds: timeoutSeconds,
onTimeout: onTimeout,
operation: {
while true {
if let nonce = try await receiveNonce() {
return nonce
}
}
})
}
}

View File

@@ -0,0 +1,32 @@
import Foundation
import Network
public enum GatewayDiscoveryBrowserSupport {
@MainActor
public static func makeBrowser(
serviceType: String,
domain: String,
queueLabelPrefix: String,
onState: @escaping @MainActor (NWBrowser.State) -> Void,
onResults: @escaping @MainActor (Set<NWBrowser.Result>) -> Void) -> NWBrowser
{
let params = NWParameters.tcp
params.includePeerToPeer = true
let browser = NWBrowser(
for: .bonjour(type: serviceType, domain: domain),
using: params)
browser.stateUpdateHandler = { state in
Task { @MainActor in
onState(state)
}
}
browser.browseResultsChangedHandler = { results, _ in
Task { @MainActor in
onResults(results)
}
}
browser.start(queue: DispatchQueue(label: "\(queueLabelPrefix).\(domain)"))
return browser
}
}

View File

@@ -293,13 +293,7 @@ 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)
}
}
self.drainSnapshotWaiters(returning: false)
}
private func handleChannelDisconnected(_ reason: String) async {
@@ -311,13 +305,7 @@ public actor GatewayNodeSession {
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)
}
}
self.drainSnapshotWaiters(returning: true)
}
private func waitForSnapshot(timeoutMs: Int) async -> Bool {
@@ -335,11 +323,15 @@ public actor GatewayNodeSession {
private func timeoutSnapshotWaiters() {
guard !self.snapshotReceived else { return }
self.drainSnapshotWaiters(returning: false)
}
private func drainSnapshotWaiters(returning value: Bool) {
if !self.snapshotWaiters.isEmpty {
let waiters = self.snapshotWaiters
self.snapshotWaiters.removeAll()
for waiter in waiters {
waiter.resume(returning: false)
waiter.resume(returning: value)
}
}
}

View File

@@ -0,0 +1,13 @@
import Foundation
public enum LocalNetworkURLSupport {
public static func isLocalNetworkHTTPURL(_ url: URL) -> Bool {
guard let scheme = url.scheme?.lowercased(), scheme == "http" || scheme == "https" else {
return false
}
guard let host = url.host?.trimmingCharacters(in: .whitespacesAndNewlines), !host.isEmpty else {
return false
}
return LoopbackHost.isLocalNetworkHost(host)
}
}

View File

@@ -0,0 +1,44 @@
import CoreLocation
import Foundation
public enum LocationCurrentRequest {
public typealias TimeoutRunner = @Sendable (
_ timeoutMs: Int,
_ operation: @escaping @Sendable () async throws -> CLLocation
) async throws -> CLLocation
@MainActor
public static func resolve(
manager: CLLocationManager,
desiredAccuracy: OpenClawLocationAccuracy,
maxAgeMs: Int?,
timeoutMs: Int?,
request: @escaping @Sendable () async throws -> CLLocation,
withTimeout: TimeoutRunner) async throws -> CLLocation
{
let now = Date()
if let maxAgeMs,
let cached = manager.location,
now.timeIntervalSince(cached.timestamp) * 1000 <= Double(maxAgeMs)
{
return cached
}
manager.desiredAccuracy = self.accuracyValue(desiredAccuracy)
let timeout = max(0, timeoutMs ?? 10000)
return try await withTimeout(timeout) {
try await request()
}
}
public static func accuracyValue(_ accuracy: OpenClawLocationAccuracy) -> CLLocationAccuracy {
switch accuracy {
case .coarse:
kCLLocationAccuracyKilometer
case .balanced:
kCLLocationAccuracyHundredMeters
case .precise:
kCLLocationAccuracyBest
}
}
}

View File

@@ -0,0 +1,49 @@
import CoreLocation
import Foundation
@MainActor
public protocol LocationServiceCommon: AnyObject, CLLocationManagerDelegate {
var locationManager: CLLocationManager { get }
var locationRequestContinuation: CheckedContinuation<CLLocation, Error>? { get set }
}
public extension LocationServiceCommon {
func configureLocationManager() {
self.locationManager.delegate = self
self.locationManager.desiredAccuracy = kCLLocationAccuracyBest
}
func authorizationStatus() -> CLAuthorizationStatus {
self.locationManager.authorizationStatus
}
func accuracyAuthorization() -> CLAccuracyAuthorization {
LocationServiceSupport.accuracyAuthorization(manager: self.locationManager)
}
func requestLocationOnce() async throws -> CLLocation {
try await LocationServiceSupport.requestLocation(manager: self.locationManager) { continuation in
self.locationRequestContinuation = continuation
}
}
}
public enum LocationServiceSupport {
public static func accuracyAuthorization(manager: CLLocationManager) -> CLAccuracyAuthorization {
if #available(iOS 14.0, macOS 11.0, *) {
return manager.accuracyAuthorization
}
return .fullAccuracy
}
@MainActor
public static func requestLocation(
manager: CLLocationManager,
setContinuation: @escaping (CheckedContinuation<CLLocation, Error>) -> Void) async throws -> CLLocation
{
try await withCheckedThrowingContinuation { continuation in
setContinuation(continuation)
manager.requestLocation()
}
}
}

View File

@@ -0,0 +1,80 @@
import Foundation
import Network
public enum LoopbackHost {
public static func isLoopback(_ rawHost: String) -> Bool {
self.isLoopbackHost(rawHost)
}
public static func isLoopbackHost(_ rawHost: String) -> Bool {
var host = rawHost
.trimmingCharacters(in: .whitespacesAndNewlines)
.lowercased()
.trimmingCharacters(in: CharacterSet(charactersIn: "[]"))
if host.hasSuffix(".") {
host.removeLast()
}
if let zoneIndex = host.firstIndex(of: "%") {
host = String(host[..<zoneIndex])
}
if host.isEmpty {
return false
}
if host == "localhost" || host == "0.0.0.0" || host == "::" {
return true
}
if let ipv4 = IPv4Address(host) {
return ipv4.rawValue.first == 127
}
if let ipv6 = IPv6Address(host) {
let bytes = Array(ipv6.rawValue)
let isV6Loopback = bytes[0..<15].allSatisfy { $0 == 0 } && bytes[15] == 1
if isV6Loopback {
return true
}
let isMappedV4 = bytes[0..<10].allSatisfy { $0 == 0 } && bytes[10] == 0xFF && bytes[11] == 0xFF
return isMappedV4 && bytes[12] == 127
}
return false
}
public static func isLocalNetworkHost(_ rawHost: String) -> Bool {
let host = rawHost.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
guard !host.isEmpty else { return false }
if self.isLoopbackHost(host) { return true }
if host.hasSuffix(".local") { return true }
if host.hasSuffix(".ts.net") { return true }
if host.hasSuffix(".tailscale.net") { return true }
// Allow MagicDNS / LAN hostnames like "peters-mac-studio-1".
if !host.contains("."), !host.contains(":") { return true }
guard let ipv4 = self.parseIPv4(host) else { return false }
return self.isLocalNetworkIPv4(ipv4)
}
private static func parseIPv4(_ host: String) -> (UInt8, UInt8, UInt8, UInt8)? {
let parts = host.split(separator: ".", omittingEmptySubsequences: false)
guard parts.count == 4 else { return nil }
let bytes: [UInt8] = parts.compactMap { UInt8($0) }
guard bytes.count == 4 else { return nil }
return (bytes[0], bytes[1], bytes[2], bytes[3])
}
private static func isLocalNetworkIPv4(_ ip: (UInt8, UInt8, UInt8, UInt8)) -> Bool {
let (a, b, _, _) = ip
// 10.0.0.0/8
if a == 10 { return true }
// 172.16.0.0/12
if a == 172, (16...31).contains(Int(b)) { return true }
// 192.168.0.0/16
if a == 192, b == 168 { return true }
// 127.0.0.0/8
if a == 127 { return true }
// 169.254.0.0/16 (link-local)
if a == 169, b == 254 { return true }
// Tailscale: 100.64.0.0/10
if a == 100, (64...127).contains(Int(b)) { return true }
return false
}
}

View File

@@ -5,17 +5,7 @@ public enum OpenClawMotionCommand: String, Codable, Sendable {
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 typealias OpenClawMotionActivityParams = OpenClawDateRangeLimitParams
public struct OpenClawMotionActivityEntry: Codable, Sendable, Equatable {
public var startISO: String

View File

@@ -0,0 +1,43 @@
import Darwin
import Foundation
public enum NetworkInterfaceIPv4 {
public struct AddressEntry: Sendable {
public let name: String
public let ip: String
}
public static func addresses() -> [AddressEntry] {
var addrList: UnsafeMutablePointer<ifaddrs>?
guard getifaddrs(&addrList) == 0, let first = addrList else { return [] }
defer { freeifaddrs(addrList) }
var entries: [AddressEntry] = []
for ptr in sequence(first: first, next: { $0.pointee.ifa_next }) {
let flags = Int32(ptr.pointee.ifa_flags)
let isUp = (flags & IFF_UP) != 0
let isLoopback = (flags & IFF_LOOPBACK) != 0
let family = ptr.pointee.ifa_addr.pointee.sa_family
if !isUp || isLoopback || family != UInt8(AF_INET) { continue }
var addr = ptr.pointee.ifa_addr.pointee
var buffer = [CChar](repeating: 0, count: Int(NI_MAXHOST))
let result = getnameinfo(
&addr,
socklen_t(ptr.pointee.ifa_addr.pointee.sa_len),
&buffer,
socklen_t(buffer.count),
nil,
0,
NI_NUMERICHOST)
guard result == 0 else { continue }
let len = buffer.prefix { $0 != 0 }
let bytes = len.map { UInt8(bitPattern: $0) }
guard let ip = String(bytes: bytes, encoding: .utf8) else { continue }
let name = String(cString: ptr.pointee.ifa_name)
entries.append(AddressEntry(name: name, ip: ip))
}
return entries
}
}

View File

@@ -1,43 +1,17 @@
import Darwin
import Foundation
public enum NetworkInterfaces {
public static func primaryIPv4Address() -> String? {
var addrList: UnsafeMutablePointer<ifaddrs>?
guard getifaddrs(&addrList) == 0, let first = addrList else { return nil }
defer { freeifaddrs(addrList) }
var fallback: String?
var en0: String?
for ptr in sequence(first: first, next: { $0.pointee.ifa_next }) {
let flags = Int32(ptr.pointee.ifa_flags)
let isUp = (flags & IFF_UP) != 0
let isLoopback = (flags & IFF_LOOPBACK) != 0
let name = String(cString: ptr.pointee.ifa_name)
let family = ptr.pointee.ifa_addr.pointee.sa_family
if !isUp || isLoopback || family != UInt8(AF_INET) { continue }
var addr = ptr.pointee.ifa_addr.pointee
var buffer = [CChar](repeating: 0, count: Int(NI_MAXHOST))
let result = getnameinfo(
&addr,
socklen_t(ptr.pointee.ifa_addr.pointee.sa_len),
&buffer,
socklen_t(buffer.count),
nil,
0,
NI_NUMERICHOST)
guard result == 0 else { continue }
let len = buffer.prefix { $0 != 0 }
let bytes = len.map { UInt8(bitPattern: $0) }
guard let ip = String(bytes: bytes, encoding: .utf8) else { continue }
if name == "en0" { en0 = ip; break }
if fallback == nil { fallback = ip }
for entry in NetworkInterfaceIPv4.addresses() {
if entry.name == "en0" {
en0 = entry.ip
break
}
if fallback == nil { fallback = entry.ip }
}
return en0 ?? fallback
}
}

View File

@@ -0,0 +1,13 @@
import Foundation
public struct OpenClawDateRangeLimitParams: 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
}
}

View File

@@ -0,0 +1,11 @@
import Foundation
public enum ThrowingContinuationSupport {
public static func resumeVoid(_ continuation: CheckedContinuation<Void, Error>, error: Error?) {
if let error {
continuation.resume(throwing: error)
} else {
continuation.resume(returning: ())
}
}
}

View File

@@ -0,0 +1,57 @@
import Foundation
import WebKit
public enum WebViewJavaScriptSupport {
@MainActor
public static func applyDebugStatus(
webView: WKWebView,
enabled: Bool,
title: String?,
subtitle: String?)
{
let js = """
(() => {
try {
const api = globalThis.__openclaw;
if (!api) return;
if (typeof api.setDebugStatusEnabled === 'function') {
api.setDebugStatusEnabled(\(enabled ? "true" : "false"));
}
if (!\(enabled ? "true" : "false")) return;
if (typeof api.setStatus === 'function') {
api.setStatus(\(self.jsValue(title)), \(self.jsValue(subtitle)));
}
} catch (_) {}
})()
"""
webView.evaluateJavaScript(js) { _, _ in }
}
@MainActor
public static func evaluateToString(webView: WKWebView, javaScript: String) async throws -> String {
try await withCheckedThrowingContinuation { cont in
webView.evaluateJavaScript(javaScript) { result, error in
if let error {
cont.resume(throwing: error)
return
}
if let result {
cont.resume(returning: String(describing: result))
} else {
cont.resume(returning: "")
}
}
}
}
public static func jsValue(_ value: String?) -> String {
guard let value else { return "null" }
if let data = try? JSONSerialization.data(withJSONObject: [value]),
let encoded = String(data: data, encoding: .utf8),
encoded.count >= 2
{
return String(encoded.dropFirst().dropLast())
}
return "null"
}
}