mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-16 01:06:06 +00:00
117 lines
3.8 KiB
Swift
117 lines
3.8 KiB
Swift
import ApplicationServices
|
|
import Foundation
|
|
|
|
@MainActor
|
|
final class MacNodePresenceReporter {
|
|
typealias Sender = @MainActor @Sendable (_ event: String, _ payloadJSON: String) async -> Bool
|
|
|
|
private struct Payload: Codable {
|
|
let idleSeconds: Int
|
|
let saturated: Bool?
|
|
}
|
|
|
|
private struct IdleSample {
|
|
let seconds: Int
|
|
let saturated: Bool
|
|
}
|
|
|
|
private struct DeliveryState {
|
|
let sentAtMs: Int64
|
|
let lastActiveAtMs: Int64
|
|
}
|
|
|
|
private static let eventName = "node.presence.activity"
|
|
private static let sampleInterval = Duration.seconds(2)
|
|
private static let activeReportIntervalMs: Int64 = 15000
|
|
private static let keepaliveIntervalMs: Int64 = 180_000
|
|
private static let maximumIdleSeconds = 30 * 24 * 60 * 60
|
|
|
|
private var task: Task<Void, Never>?
|
|
|
|
func start(sender: @escaping Sender) {
|
|
self.stop()
|
|
self.task = Task {
|
|
var delivery: DeliveryState?
|
|
while !Task.isCancelled {
|
|
if let sample = Self.currentIdleSample(),
|
|
Self.shouldSend(
|
|
idleSeconds: sample.seconds,
|
|
saturated: sample.saturated,
|
|
nowMs: Self.nowMs(),
|
|
delivery: delivery)
|
|
{
|
|
let nowMs = Self.nowMs()
|
|
let lastActiveAtMs = max(0, nowMs - Int64(sample.seconds) * 1000)
|
|
let payload = Payload(
|
|
idleSeconds: sample.seconds,
|
|
saturated: sample.saturated ? true : nil)
|
|
if let data = try? JSONEncoder().encode(payload),
|
|
let payloadJSON = String(data: data, encoding: .utf8),
|
|
await sender(Self.eventName, payloadJSON)
|
|
{
|
|
delivery = DeliveryState(sentAtMs: nowMs, lastActiveAtMs: lastActiveAtMs)
|
|
}
|
|
}
|
|
try? await Task.sleep(for: Self.sampleInterval)
|
|
}
|
|
}
|
|
}
|
|
|
|
func stop() {
|
|
self.task?.cancel()
|
|
self.task = nil
|
|
}
|
|
|
|
private static func currentIdleSample() -> IdleSample? {
|
|
// Presence is intentionally opt-in through the app's existing Accessibility grant.
|
|
guard AXIsProcessTrusted(), let seconds = SystemPresenceInfo.lastHardwareInputSeconds() else { return nil }
|
|
let bounded = min(max(0, seconds), self.maximumIdleSeconds)
|
|
return IdleSample(seconds: bounded, saturated: seconds > self.maximumIdleSeconds)
|
|
}
|
|
|
|
private static func shouldSend(
|
|
idleSeconds: Int,
|
|
saturated: Bool,
|
|
nowMs: Int64,
|
|
delivery: DeliveryState?) -> Bool
|
|
{
|
|
guard let delivery else { return true }
|
|
let elapsedMs = nowMs - delivery.sentAtMs
|
|
if elapsedMs >= self.keepaliveIntervalMs {
|
|
return true
|
|
}
|
|
if saturated {
|
|
return false
|
|
}
|
|
let lastActiveAtMs = max(0, nowMs - Int64(idleSeconds) * 1000)
|
|
return lastActiveAtMs > delivery.lastActiveAtMs && elapsedMs >= self.activeReportIntervalMs
|
|
}
|
|
|
|
private static func nowMs() -> Int64 {
|
|
Int64(Date().timeIntervalSince1970 * 1000)
|
|
}
|
|
}
|
|
|
|
#if DEBUG
|
|
extension MacNodePresenceReporter {
|
|
static func _testShouldSend(
|
|
idleSeconds: Int,
|
|
nowMs: Int64,
|
|
lastSentAtMs: Int64?,
|
|
lastSentActiveAtMs: Int64?,
|
|
saturated: Bool = false) -> Bool
|
|
{
|
|
let delivery: DeliveryState? = if let lastSentAtMs, let lastSentActiveAtMs {
|
|
DeliveryState(sentAtMs: lastSentAtMs, lastActiveAtMs: lastSentActiveAtMs)
|
|
} else {
|
|
nil
|
|
}
|
|
return self.shouldSend(
|
|
idleSeconds: idleSeconds,
|
|
saturated: saturated,
|
|
nowMs: nowMs,
|
|
delivery: delivery)
|
|
}
|
|
}
|
|
#endif
|