Mac: finish Moltbot rename

This commit is contained in:
Shadow
2026-01-27 14:12:17 -06:00
parent 3fe4b2595a
commit cc72498b46
77 changed files with 18956 additions and 44 deletions

View File

@@ -0,0 +1,171 @@
import MoltbotKit
import Foundation
import OSLog
@MainActor
final class MacNodeModeCoordinator {
static let shared = MacNodeModeCoordinator()
private let logger = Logger(subsystem: "bot.molt", category: "mac-node")
private var task: Task<Void, Never>?
private let runtime = MacNodeRuntime()
private let session = GatewayNodeSession()
func start() {
guard self.task == nil else { return }
self.task = Task { [weak self] in
await self?.run()
}
}
func stop() {
self.task?.cancel()
self.task = nil
Task { await self.session.disconnect() }
}
func setPreferredGatewayStableID(_ stableID: String?) {
GatewayDiscoveryPreferences.setPreferredStableID(stableID)
Task { await self.session.disconnect() }
}
private func run() async {
var retryDelay: UInt64 = 1_000_000_000
var lastCameraEnabled: Bool?
let defaults = UserDefaults.standard
while !Task.isCancelled {
if await MainActor.run(body: { AppStateStore.shared.isPaused }) {
try? await Task.sleep(nanoseconds: 1_000_000_000)
continue
}
let cameraEnabled = defaults.object(forKey: cameraEnabledKey) as? Bool ?? false
if lastCameraEnabled == nil {
lastCameraEnabled = cameraEnabled
} else if lastCameraEnabled != cameraEnabled {
lastCameraEnabled = cameraEnabled
await self.session.disconnect()
try? await Task.sleep(nanoseconds: 200_000_000)
}
do {
let config = try await GatewayEndpointStore.shared.requireConfig()
let caps = self.currentCaps()
let commands = self.currentCommands(caps: caps)
let permissions = await self.currentPermissions()
let connectOptions = GatewayConnectOptions(
role: "node",
scopes: [],
caps: caps,
commands: commands,
permissions: permissions,
clientId: "moltbot-macos",
clientMode: "node",
clientDisplayName: InstanceIdentity.displayName)
let sessionBox = self.buildSessionBox(url: config.url)
try await self.session.connect(
url: config.url,
token: config.token,
password: config.password,
connectOptions: connectOptions,
sessionBox: sessionBox,
onConnected: { [weak self] in
guard let self else { return }
self.logger.info("mac node connected to gateway")
let mainSessionKey = await GatewayConnection.shared.mainSessionKey()
await self.runtime.updateMainSessionKey(mainSessionKey)
await self.runtime.setEventSender { [weak self] event, payload in
guard let self else { return }
await self.session.sendEvent(event: event, payloadJSON: payload)
}
},
onDisconnected: { [weak self] reason in
guard let self else { return }
await self.runtime.setEventSender(nil)
self.logger.error("mac node disconnected: \(reason, privacy: .public)")
},
onInvoke: { [weak self] req in
guard let self else {
return BridgeInvokeResponse(
id: req.id,
ok: false,
error: MoltbotNodeError(code: .unavailable, message: "UNAVAILABLE: node not ready"))
}
return await self.runtime.handleInvoke(req)
})
retryDelay = 1_000_000_000
try? await Task.sleep(nanoseconds: 1_000_000_000)
} catch {
self.logger.error("mac node gateway connect failed: \(error.localizedDescription, privacy: .public)")
try? await Task.sleep(nanoseconds: min(retryDelay, 10_000_000_000))
retryDelay = min(retryDelay * 2, 10_000_000_000)
}
}
}
private func currentCaps() -> [String] {
var caps: [String] = [MoltbotCapability.canvas.rawValue, MoltbotCapability.screen.rawValue]
if UserDefaults.standard.object(forKey: cameraEnabledKey) as? Bool ?? false {
caps.append(MoltbotCapability.camera.rawValue)
}
let rawLocationMode = UserDefaults.standard.string(forKey: locationModeKey) ?? "off"
if MoltbotLocationMode(rawValue: rawLocationMode) != .off {
caps.append(MoltbotCapability.location.rawValue)
}
return caps
}
private func currentPermissions() async -> [String: Bool] {
let statuses = await PermissionManager.status()
return Dictionary(uniqueKeysWithValues: statuses.map { ($0.key.rawValue, $0.value) })
}
private func currentCommands(caps: [String]) -> [String] {
var commands: [String] = [
MoltbotCanvasCommand.present.rawValue,
MoltbotCanvasCommand.hide.rawValue,
MoltbotCanvasCommand.navigate.rawValue,
MoltbotCanvasCommand.evalJS.rawValue,
MoltbotCanvasCommand.snapshot.rawValue,
MoltbotCanvasA2UICommand.push.rawValue,
MoltbotCanvasA2UICommand.pushJSONL.rawValue,
MoltbotCanvasA2UICommand.reset.rawValue,
MacNodeScreenCommand.record.rawValue,
MoltbotSystemCommand.notify.rawValue,
MoltbotSystemCommand.which.rawValue,
MoltbotSystemCommand.run.rawValue,
MoltbotSystemCommand.execApprovalsGet.rawValue,
MoltbotSystemCommand.execApprovalsSet.rawValue,
]
let capsSet = Set(caps)
if capsSet.contains(MoltbotCapability.camera.rawValue) {
commands.append(MoltbotCameraCommand.list.rawValue)
commands.append(MoltbotCameraCommand.snap.rawValue)
commands.append(MoltbotCameraCommand.clip.rawValue)
}
if capsSet.contains(MoltbotCapability.location.rawValue) {
commands.append(MoltbotLocationCommand.get.rawValue)
}
return commands
}
private func buildSessionBox(url: URL) -> WebSocketSessionBox? {
guard url.scheme?.lowercased() == "wss" else { return nil }
let host = url.host ?? "gateway"
let port = url.port ?? 443
let stableID = "\(host):\(port)"
let stored = GatewayTLSStore.loadFingerprint(stableID: stableID)
let params = GatewayTLSParams(
required: true,
expectedFingerprint: stored,
allowTOFU: stored == nil,
storeKey: stableID)
let session = GatewayTLSPinningSession(params: params)
return WebSocketSessionBox(session: session)
}
}