Include full contents of all nested repositories

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-27 16:25:02 +01:00
parent 14ff8fd54c
commit 2401ed446f
7271 changed files with 1310112 additions and 6 deletions

View File

@@ -0,0 +1,202 @@
import OpenClawKit
import CoreLocation
import Foundation
@MainActor
final class LocationService: NSObject, CLLocationManagerDelegate {
enum Error: Swift.Error {
case timeout
case unavailable
}
private let manager = CLLocationManager()
private var authContinuation: CheckedContinuation<CLAuthorizationStatus, Never>?
private var locationContinuation: CheckedContinuation<CLLocation, Swift.Error>?
private var updatesContinuation: AsyncStream<CLLocation>.Continuation?
private var isStreaming = false
private var significantLocationCallback: (@Sendable (CLLocation) -> Void)?
private var isMonitoringSignificantChanges = false
override init() {
super.init()
self.manager.delegate = self
self.manager.desiredAccuracy = kCLLocationAccuracyBest
}
func authorizationStatus() -> CLAuthorizationStatus {
self.manager.authorizationStatus
}
func accuracyAuthorization() -> CLAccuracyAuthorization {
if #available(iOS 14.0, *) {
return self.manager.accuracyAuthorization
}
return .fullAccuracy
}
func ensureAuthorization(mode: OpenClawLocationMode) async -> CLAuthorizationStatus {
guard CLLocationManager.locationServicesEnabled() else { return .denied }
let status = self.manager.authorizationStatus
if status == .notDetermined {
self.manager.requestWhenInUseAuthorization()
let updated = await self.awaitAuthorizationChange()
if mode != .always { return updated }
}
if mode == .always {
let current = self.manager.authorizationStatus
if current == .authorizedWhenInUse {
self.manager.requestAlwaysAuthorization()
return await self.awaitAuthorizationChange()
}
return current
}
return self.manager.authorizationStatus
}
func currentLocation(
params: OpenClawLocationGetParams,
desiredAccuracy: OpenClawLocationAccuracy,
maxAgeMs: Int?,
timeoutMs: Int?) async throws -> CLLocation
{
let now = Date()
if let maxAgeMs,
let cached = self.manager.location,
now.timeIntervalSince(cached.timestamp) * 1000 <= Double(maxAgeMs)
{
return cached
}
self.manager.desiredAccuracy = Self.accuracyValue(desiredAccuracy)
let timeout = max(0, timeoutMs ?? 10000)
return try await self.withTimeout(timeoutMs: timeout) {
try await self.requestLocation()
}
}
private func requestLocation() async throws -> CLLocation {
try await withCheckedThrowingContinuation { cont in
self.locationContinuation = cont
self.manager.requestLocation()
}
}
private func awaitAuthorizationChange() async -> CLAuthorizationStatus {
await withCheckedContinuation { cont in
self.authContinuation = cont
}
}
private func withTimeout<T: Sendable>(
timeoutMs: Int,
operation: @escaping @Sendable () async throws -> T) async throws -> T
{
try await AsyncTimeout.withTimeoutMs(timeoutMs: timeoutMs, onTimeout: { Error.timeout }, operation: operation)
}
private static func accuracyValue(_ accuracy: OpenClawLocationAccuracy) -> CLLocationAccuracy {
switch accuracy {
case .coarse:
kCLLocationAccuracyKilometer
case .balanced:
kCLLocationAccuracyHundredMeters
case .precise:
kCLLocationAccuracyBest
}
}
func startLocationUpdates(
desiredAccuracy: OpenClawLocationAccuracy,
significantChangesOnly: Bool) -> AsyncStream<CLLocation>
{
self.stopLocationUpdates()
self.manager.desiredAccuracy = Self.accuracyValue(desiredAccuracy)
self.manager.pausesLocationUpdatesAutomatically = true
self.manager.allowsBackgroundLocationUpdates = true
self.isStreaming = true
if significantChangesOnly {
self.manager.startMonitoringSignificantLocationChanges()
} else {
self.manager.startUpdatingLocation()
}
return AsyncStream(bufferingPolicy: .bufferingNewest(1)) { continuation in
self.updatesContinuation = continuation
continuation.onTermination = { @Sendable _ in
Task { @MainActor in
self.stopLocationUpdates()
}
}
}
}
func stopLocationUpdates() {
guard self.isStreaming else { return }
self.isStreaming = false
self.manager.stopUpdatingLocation()
self.manager.stopMonitoringSignificantLocationChanges()
self.updatesContinuation?.finish()
self.updatesContinuation = nil
}
func startMonitoringSignificantLocationChanges(onUpdate: @escaping @Sendable (CLLocation) -> Void) {
self.significantLocationCallback = onUpdate
guard !self.isMonitoringSignificantChanges else { return }
self.isMonitoringSignificantChanges = true
self.manager.startMonitoringSignificantLocationChanges()
}
func stopMonitoringSignificantLocationChanges() {
guard self.isMonitoringSignificantChanges else { return }
self.isMonitoringSignificantChanges = false
self.significantLocationCallback = nil
self.manager.stopMonitoringSignificantLocationChanges()
}
nonisolated func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
let status = manager.authorizationStatus
Task { @MainActor in
if let cont = self.authContinuation {
self.authContinuation = nil
cont.resume(returning: status)
}
}
}
nonisolated func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
let locs = locations
Task { @MainActor in
// Resolve the one-shot continuation first (if any).
if let cont = self.locationContinuation {
self.locationContinuation = nil
if let latest = locs.last {
cont.resume(returning: latest)
} else {
cont.resume(throwing: Error.unavailable)
}
// Don't return also forward to significant-change callback below
// so both consumers receive updates when both are active.
}
if let callback = self.significantLocationCallback, let latest = locs.last {
callback(latest)
}
if let latest = locs.last, let updates = self.updatesContinuation {
updates.yield(latest)
}
}
}
nonisolated func locationManager(_ manager: CLLocationManager, didFailWithError error: Swift.Error) {
let err = error
Task { @MainActor in
guard let cont = self.locationContinuation else { return }
self.locationContinuation = nil
cont.resume(throwing: err)
}
}
}

View File

@@ -0,0 +1,42 @@
import CoreLocation
import Foundation
import OpenClawKit
/// Monitors significant location changes and pushes `location.update`
/// events to the gateway so the severance hook can determine whether
/// the user is at their configured work location.
@MainActor
enum SignificantLocationMonitor {
static func startIfNeeded(
locationService: any LocationServicing,
locationMode: OpenClawLocationMode,
gateway: GatewayNodeSession,
beforeSend: (@MainActor @Sendable () async -> Void)? = nil
) {
guard locationMode == .always else { return }
let status = locationService.authorizationStatus()
guard status == .authorizedAlways else { return }
locationService.startMonitoringSignificantLocationChanges { location in
struct Payload: Codable {
var lat: Double
var lon: Double
var accuracyMeters: Double
var source: String?
}
let payload = Payload(
lat: location.coordinate.latitude,
lon: location.coordinate.longitude,
accuracyMeters: location.horizontalAccuracy,
source: "ios-significant-location")
guard let data = try? JSONEncoder().encode(payload),
let json = String(data: data, encoding: .utf8)
else { return }
Task { @MainActor in
if let beforeSend {
await beforeSend()
}
await gateway.sendEvent(event: "location.update", payloadJSON: json)
}
}
}
}