refactor: streamline node invoke handling
This commit is contained in:
@@ -8,10 +8,9 @@ actor MacNodeRuntime {
|
||||
@MainActor private let screenRecorder = ScreenRecordService()
|
||||
@MainActor private let locationService = MacNodeLocationService()
|
||||
|
||||
// swiftlint:disable:next function_body_length cyclomatic_complexity
|
||||
func handleInvoke(_ req: BridgeInvokeRequest) async -> BridgeInvokeResponse {
|
||||
let command = req.command
|
||||
if command.hasPrefix("canvas.") || command.hasPrefix("canvas.a2ui."), !Self.canvasEnabled() {
|
||||
if self.isCanvasCommand(command), !Self.canvasEnabled() {
|
||||
return BridgeInvokeResponse(
|
||||
id: req.id,
|
||||
ok: false,
|
||||
@@ -21,251 +20,28 @@ actor MacNodeRuntime {
|
||||
}
|
||||
do {
|
||||
switch command {
|
||||
case ClawdbotCanvasCommand.present.rawValue:
|
||||
let params = (try? Self.decodeParams(ClawdbotCanvasPresentParams.self, from: req.paramsJSON)) ??
|
||||
ClawdbotCanvasPresentParams()
|
||||
let urlTrimmed = params.url?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
let url = urlTrimmed.isEmpty ? nil : urlTrimmed
|
||||
let placement = params.placement.map {
|
||||
CanvasPlacement(x: $0.x, y: $0.y, width: $0.width, height: $0.height)
|
||||
}
|
||||
try await MainActor.run {
|
||||
_ = try CanvasManager.shared.showDetailed(
|
||||
sessionKey: "main",
|
||||
target: url,
|
||||
placement: placement)
|
||||
}
|
||||
return BridgeInvokeResponse(id: req.id, ok: true)
|
||||
|
||||
case ClawdbotCanvasCommand.hide.rawValue:
|
||||
await MainActor.run {
|
||||
CanvasManager.shared.hide(sessionKey: "main")
|
||||
}
|
||||
return BridgeInvokeResponse(id: req.id, ok: true)
|
||||
|
||||
case ClawdbotCanvasCommand.navigate.rawValue:
|
||||
let params = try Self.decodeParams(ClawdbotCanvasNavigateParams.self, from: req.paramsJSON)
|
||||
try await MainActor.run {
|
||||
_ = try CanvasManager.shared.show(sessionKey: "main", path: params.url)
|
||||
}
|
||||
return BridgeInvokeResponse(id: req.id, ok: true)
|
||||
|
||||
case ClawdbotCanvasCommand.evalJS.rawValue:
|
||||
let params = try Self.decodeParams(ClawdbotCanvasEvalParams.self, from: req.paramsJSON)
|
||||
let result = try await CanvasManager.shared.eval(
|
||||
sessionKey: "main",
|
||||
javaScript: params.javaScript)
|
||||
let payload = try Self.encodePayload(["result": result] as [String: String])
|
||||
return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: payload)
|
||||
|
||||
case ClawdbotCanvasCommand.snapshot.rawValue:
|
||||
let params = try? Self.decodeParams(ClawdbotCanvasSnapshotParams.self, from: req.paramsJSON)
|
||||
let format = params?.format ?? .jpeg
|
||||
let maxWidth: Int? = {
|
||||
if let raw = params?.maxWidth, raw > 0 { return raw }
|
||||
return switch format {
|
||||
case .png: 900
|
||||
case .jpeg: 1600
|
||||
}
|
||||
}()
|
||||
let quality = params?.quality ?? 0.9
|
||||
|
||||
let path = try await CanvasManager.shared.snapshot(sessionKey: "main", outPath: nil)
|
||||
defer { try? FileManager.default.removeItem(atPath: path) }
|
||||
let data = try Data(contentsOf: URL(fileURLWithPath: path))
|
||||
guard let image = NSImage(data: data) else {
|
||||
return Self.errorResponse(req, code: .unavailable, message: "canvas snapshot decode failed")
|
||||
}
|
||||
let encoded = try Self.encodeCanvasSnapshot(
|
||||
image: image,
|
||||
format: format,
|
||||
maxWidth: maxWidth,
|
||||
quality: quality)
|
||||
let payload = try Self.encodePayload([
|
||||
"format": format == .jpeg ? "jpeg" : "png",
|
||||
"base64": encoded.base64EncodedString(),
|
||||
])
|
||||
return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: payload)
|
||||
|
||||
case ClawdbotCanvasA2UICommand.reset.rawValue:
|
||||
return try await self.handleA2UIReset(req)
|
||||
|
||||
case ClawdbotCanvasA2UICommand.push.rawValue, ClawdbotCanvasA2UICommand.pushJSONL.rawValue:
|
||||
return try await self.handleA2UIPush(req)
|
||||
|
||||
case ClawdbotCameraCommand.snap.rawValue:
|
||||
guard Self.cameraEnabled() else {
|
||||
return BridgeInvokeResponse(
|
||||
id: req.id,
|
||||
ok: false,
|
||||
error: ClawdbotNodeError(
|
||||
code: .unavailable,
|
||||
message: "CAMERA_DISABLED: enable Camera in Settings"))
|
||||
}
|
||||
let params = (try? Self.decodeParams(ClawdbotCameraSnapParams.self, from: req.paramsJSON)) ??
|
||||
ClawdbotCameraSnapParams()
|
||||
let delayMs = min(10_000, max(0, params.delayMs ?? 2000))
|
||||
let res = try await self.cameraCapture.snap(
|
||||
facing: CameraFacing(rawValue: params.facing?.rawValue ?? "") ?? .front,
|
||||
maxWidth: params.maxWidth,
|
||||
quality: params.quality,
|
||||
deviceId: params.deviceId,
|
||||
delayMs: delayMs)
|
||||
struct SnapPayload: Encodable {
|
||||
var format: String
|
||||
var base64: String
|
||||
var width: Int
|
||||
var height: Int
|
||||
}
|
||||
let payload = try Self.encodePayload(SnapPayload(
|
||||
format: (params.format ?? .jpg).rawValue,
|
||||
base64: res.data.base64EncodedString(),
|
||||
width: Int(res.size.width),
|
||||
height: Int(res.size.height)))
|
||||
return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: payload)
|
||||
|
||||
case ClawdbotCameraCommand.clip.rawValue:
|
||||
guard Self.cameraEnabled() else {
|
||||
return BridgeInvokeResponse(
|
||||
id: req.id,
|
||||
ok: false,
|
||||
error: ClawdbotNodeError(
|
||||
code: .unavailable,
|
||||
message: "CAMERA_DISABLED: enable Camera in Settings"))
|
||||
}
|
||||
let params = (try? Self.decodeParams(ClawdbotCameraClipParams.self, from: req.paramsJSON)) ??
|
||||
ClawdbotCameraClipParams()
|
||||
let res = try await self.cameraCapture.clip(
|
||||
facing: CameraFacing(rawValue: params.facing?.rawValue ?? "") ?? .front,
|
||||
durationMs: params.durationMs,
|
||||
includeAudio: params.includeAudio ?? true,
|
||||
deviceId: params.deviceId,
|
||||
outPath: nil)
|
||||
defer { try? FileManager.default.removeItem(atPath: res.path) }
|
||||
let data = try Data(contentsOf: URL(fileURLWithPath: res.path))
|
||||
struct ClipPayload: Encodable {
|
||||
var format: String
|
||||
var base64: String
|
||||
var durationMs: Int
|
||||
var hasAudio: Bool
|
||||
}
|
||||
let payload = try Self.encodePayload(ClipPayload(
|
||||
format: (params.format ?? .mp4).rawValue,
|
||||
base64: data.base64EncodedString(),
|
||||
durationMs: res.durationMs,
|
||||
hasAudio: res.hasAudio))
|
||||
return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: payload)
|
||||
|
||||
case ClawdbotCameraCommand.list.rawValue:
|
||||
guard Self.cameraEnabled() else {
|
||||
return BridgeInvokeResponse(
|
||||
id: req.id,
|
||||
ok: false,
|
||||
error: ClawdbotNodeError(
|
||||
code: .unavailable,
|
||||
message: "CAMERA_DISABLED: enable Camera in Settings"))
|
||||
}
|
||||
let devices = await self.cameraCapture.listDevices()
|
||||
let payload = try Self.encodePayload(["devices": devices])
|
||||
return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: payload)
|
||||
|
||||
case ClawdbotCanvasCommand.present.rawValue,
|
||||
ClawdbotCanvasCommand.hide.rawValue,
|
||||
ClawdbotCanvasCommand.navigate.rawValue,
|
||||
ClawdbotCanvasCommand.evalJS.rawValue,
|
||||
ClawdbotCanvasCommand.snapshot.rawValue:
|
||||
return try await self.handleCanvasInvoke(req)
|
||||
case ClawdbotCanvasA2UICommand.reset.rawValue,
|
||||
ClawdbotCanvasA2UICommand.push.rawValue,
|
||||
ClawdbotCanvasA2UICommand.pushJSONL.rawValue:
|
||||
return try await self.handleA2UIInvoke(req)
|
||||
case ClawdbotCameraCommand.snap.rawValue,
|
||||
ClawdbotCameraCommand.clip.rawValue,
|
||||
ClawdbotCameraCommand.list.rawValue:
|
||||
return try await self.handleCameraInvoke(req)
|
||||
case ClawdbotLocationCommand.get.rawValue:
|
||||
let mode = Self.locationMode()
|
||||
guard mode != .off else {
|
||||
return BridgeInvokeResponse(
|
||||
id: req.id,
|
||||
ok: false,
|
||||
error: ClawdbotNodeError(
|
||||
code: .unavailable,
|
||||
message: "LOCATION_DISABLED: enable Location in Settings"))
|
||||
}
|
||||
let params = (try? Self.decodeParams(ClawdbotLocationGetParams.self, from: req.paramsJSON)) ??
|
||||
ClawdbotLocationGetParams()
|
||||
let desired = params.desiredAccuracy ??
|
||||
(Self.locationPreciseEnabled() ? .precise : .balanced)
|
||||
let status = await self.locationService.authorizationStatus()
|
||||
if status != .authorizedAlways && status != .authorizedWhenInUse {
|
||||
return BridgeInvokeResponse(
|
||||
id: req.id,
|
||||
ok: false,
|
||||
error: ClawdbotNodeError(
|
||||
code: .unavailable,
|
||||
message: "LOCATION_PERMISSION_REQUIRED: grant Location permission"))
|
||||
}
|
||||
do {
|
||||
let location = try await self.locationService.currentLocation(
|
||||
desiredAccuracy: desired,
|
||||
maxAgeMs: params.maxAgeMs,
|
||||
timeoutMs: params.timeoutMs)
|
||||
let isPrecise = await self.locationService.accuracyAuthorization() == .fullAccuracy
|
||||
let payload = ClawdbotLocationPayload(
|
||||
lat: location.coordinate.latitude,
|
||||
lon: location.coordinate.longitude,
|
||||
accuracyMeters: location.horizontalAccuracy,
|
||||
altitudeMeters: location.verticalAccuracy >= 0 ? location.altitude : nil,
|
||||
speedMps: location.speed >= 0 ? location.speed : nil,
|
||||
headingDeg: location.course >= 0 ? location.course : nil,
|
||||
timestamp: ISO8601DateFormatter().string(from: location.timestamp),
|
||||
isPrecise: isPrecise,
|
||||
source: nil)
|
||||
let json = try Self.encodePayload(payload)
|
||||
return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: json)
|
||||
} catch MacNodeLocationService.Error.timeout {
|
||||
return BridgeInvokeResponse(
|
||||
id: req.id,
|
||||
ok: false,
|
||||
error: ClawdbotNodeError(
|
||||
code: .unavailable,
|
||||
message: "LOCATION_TIMEOUT: no fix in time"))
|
||||
} catch {
|
||||
return BridgeInvokeResponse(
|
||||
id: req.id,
|
||||
ok: false,
|
||||
error: ClawdbotNodeError(
|
||||
code: .unavailable,
|
||||
message: "LOCATION_UNAVAILABLE: \(error.localizedDescription)"))
|
||||
}
|
||||
|
||||
return try await self.handleLocationInvoke(req)
|
||||
case MacNodeScreenCommand.record.rawValue:
|
||||
let params = (try? Self.decodeParams(MacNodeScreenRecordParams.self, from: req.paramsJSON)) ??
|
||||
MacNodeScreenRecordParams()
|
||||
if let format = params.format?.lowercased(), !format.isEmpty, format != "mp4" {
|
||||
return Self.errorResponse(
|
||||
req,
|
||||
code: .invalidRequest,
|
||||
message: "INVALID_REQUEST: screen format must be mp4")
|
||||
}
|
||||
let res = try await self.screenRecorder.record(
|
||||
screenIndex: params.screenIndex,
|
||||
durationMs: params.durationMs,
|
||||
fps: params.fps,
|
||||
includeAudio: params.includeAudio,
|
||||
outPath: nil)
|
||||
defer { try? FileManager.default.removeItem(atPath: res.path) }
|
||||
let data = try Data(contentsOf: URL(fileURLWithPath: res.path))
|
||||
struct ScreenPayload: Encodable {
|
||||
var format: String
|
||||
var base64: String
|
||||
var durationMs: Int?
|
||||
var fps: Double?
|
||||
var screenIndex: Int?
|
||||
var hasAudio: Bool
|
||||
}
|
||||
let payload = try Self.encodePayload(ScreenPayload(
|
||||
format: "mp4",
|
||||
base64: data.base64EncodedString(),
|
||||
durationMs: params.durationMs,
|
||||
fps: params.fps,
|
||||
screenIndex: params.screenIndex,
|
||||
hasAudio: res.hasAudio))
|
||||
return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: payload)
|
||||
|
||||
return try await self.handleScreenRecordInvoke(req)
|
||||
case ClawdbotSystemCommand.run.rawValue:
|
||||
return try await self.handleSystemRun(req)
|
||||
|
||||
case ClawdbotSystemCommand.notify.rawValue:
|
||||
return try await self.handleSystemNotify(req)
|
||||
|
||||
default:
|
||||
return Self.errorResponse(req, code: .invalidRequest, message: "INVALID_REQUEST: unknown command")
|
||||
}
|
||||
@@ -274,6 +50,247 @@ actor MacNodeRuntime {
|
||||
}
|
||||
}
|
||||
|
||||
private func isCanvasCommand(_ command: String) -> Bool {
|
||||
command.hasPrefix("canvas.") || command.hasPrefix("canvas.a2ui.")
|
||||
}
|
||||
|
||||
private func handleCanvasInvoke(_ req: BridgeInvokeRequest) async throws -> BridgeInvokeResponse {
|
||||
switch req.command {
|
||||
case ClawdbotCanvasCommand.present.rawValue:
|
||||
let params = (try? Self.decodeParams(ClawdbotCanvasPresentParams.self, from: req.paramsJSON)) ??
|
||||
ClawdbotCanvasPresentParams()
|
||||
let urlTrimmed = params.url?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
let url = urlTrimmed.isEmpty ? nil : urlTrimmed
|
||||
let placement = params.placement.map {
|
||||
CanvasPlacement(x: $0.x, y: $0.y, width: $0.width, height: $0.height)
|
||||
}
|
||||
try await MainActor.run {
|
||||
_ = try CanvasManager.shared.showDetailed(
|
||||
sessionKey: "main",
|
||||
target: url,
|
||||
placement: placement)
|
||||
}
|
||||
return BridgeInvokeResponse(id: req.id, ok: true)
|
||||
case ClawdbotCanvasCommand.hide.rawValue:
|
||||
await MainActor.run {
|
||||
CanvasManager.shared.hide(sessionKey: "main")
|
||||
}
|
||||
return BridgeInvokeResponse(id: req.id, ok: true)
|
||||
case ClawdbotCanvasCommand.navigate.rawValue:
|
||||
let params = try Self.decodeParams(ClawdbotCanvasNavigateParams.self, from: req.paramsJSON)
|
||||
try await MainActor.run {
|
||||
_ = try CanvasManager.shared.show(sessionKey: "main", path: params.url)
|
||||
}
|
||||
return BridgeInvokeResponse(id: req.id, ok: true)
|
||||
case ClawdbotCanvasCommand.evalJS.rawValue:
|
||||
let params = try Self.decodeParams(ClawdbotCanvasEvalParams.self, from: req.paramsJSON)
|
||||
let result = try await CanvasManager.shared.eval(
|
||||
sessionKey: "main",
|
||||
javaScript: params.javaScript)
|
||||
let payload = try Self.encodePayload(["result": result] as [String: String])
|
||||
return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: payload)
|
||||
case ClawdbotCanvasCommand.snapshot.rawValue:
|
||||
let params = try? Self.decodeParams(ClawdbotCanvasSnapshotParams.self, from: req.paramsJSON)
|
||||
let format = params?.format ?? .jpeg
|
||||
let maxWidth: Int? = {
|
||||
if let raw = params?.maxWidth, raw > 0 { return raw }
|
||||
return switch format {
|
||||
case .png: 900
|
||||
case .jpeg: 1600
|
||||
}
|
||||
}()
|
||||
let quality = params?.quality ?? 0.9
|
||||
|
||||
let path = try await CanvasManager.shared.snapshot(sessionKey: "main", outPath: nil)
|
||||
defer { try? FileManager.default.removeItem(atPath: path) }
|
||||
let data = try Data(contentsOf: URL(fileURLWithPath: path))
|
||||
guard let image = NSImage(data: data) else {
|
||||
return Self.errorResponse(req, code: .unavailable, message: "canvas snapshot decode failed")
|
||||
}
|
||||
let encoded = try Self.encodeCanvasSnapshot(
|
||||
image: image,
|
||||
format: format,
|
||||
maxWidth: maxWidth,
|
||||
quality: quality)
|
||||
let payload = try Self.encodePayload([
|
||||
"format": format == .jpeg ? "jpeg" : "png",
|
||||
"base64": encoded.base64EncodedString(),
|
||||
])
|
||||
return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: payload)
|
||||
default:
|
||||
return Self.errorResponse(req, code: .invalidRequest, message: "INVALID_REQUEST: unknown command")
|
||||
}
|
||||
}
|
||||
|
||||
private func handleA2UIInvoke(_ req: BridgeInvokeRequest) async throws -> BridgeInvokeResponse {
|
||||
switch req.command {
|
||||
case ClawdbotCanvasA2UICommand.reset.rawValue:
|
||||
try await self.handleA2UIReset(req)
|
||||
case ClawdbotCanvasA2UICommand.push.rawValue,
|
||||
ClawdbotCanvasA2UICommand.pushJSONL.rawValue:
|
||||
try await self.handleA2UIPush(req)
|
||||
default:
|
||||
Self.errorResponse(req, code: .invalidRequest, message: "INVALID_REQUEST: unknown command")
|
||||
}
|
||||
}
|
||||
|
||||
private func handleCameraInvoke(_ req: BridgeInvokeRequest) async throws -> BridgeInvokeResponse {
|
||||
guard Self.cameraEnabled() else {
|
||||
return BridgeInvokeResponse(
|
||||
id: req.id,
|
||||
ok: false,
|
||||
error: ClawdbotNodeError(
|
||||
code: .unavailable,
|
||||
message: "CAMERA_DISABLED: enable Camera in Settings"))
|
||||
}
|
||||
switch req.command {
|
||||
case ClawdbotCameraCommand.snap.rawValue:
|
||||
let params = (try? Self.decodeParams(ClawdbotCameraSnapParams.self, from: req.paramsJSON)) ??
|
||||
ClawdbotCameraSnapParams()
|
||||
let delayMs = min(10000, max(0, params.delayMs ?? 2000))
|
||||
let res = try await self.cameraCapture.snap(
|
||||
facing: CameraFacing(rawValue: params.facing?.rawValue ?? "") ?? .front,
|
||||
maxWidth: params.maxWidth,
|
||||
quality: params.quality,
|
||||
deviceId: params.deviceId,
|
||||
delayMs: delayMs)
|
||||
struct SnapPayload: Encodable {
|
||||
var format: String
|
||||
var base64: String
|
||||
var width: Int
|
||||
var height: Int
|
||||
}
|
||||
let payload = try Self.encodePayload(SnapPayload(
|
||||
format: (params.format ?? .jpg).rawValue,
|
||||
base64: res.data.base64EncodedString(),
|
||||
width: Int(res.size.width),
|
||||
height: Int(res.size.height)))
|
||||
return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: payload)
|
||||
case ClawdbotCameraCommand.clip.rawValue:
|
||||
let params = (try? Self.decodeParams(ClawdbotCameraClipParams.self, from: req.paramsJSON)) ??
|
||||
ClawdbotCameraClipParams()
|
||||
let res = try await self.cameraCapture.clip(
|
||||
facing: CameraFacing(rawValue: params.facing?.rawValue ?? "") ?? .front,
|
||||
durationMs: params.durationMs,
|
||||
includeAudio: params.includeAudio ?? true,
|
||||
deviceId: params.deviceId,
|
||||
outPath: nil)
|
||||
defer { try? FileManager.default.removeItem(atPath: res.path) }
|
||||
let data = try Data(contentsOf: URL(fileURLWithPath: res.path))
|
||||
struct ClipPayload: Encodable {
|
||||
var format: String
|
||||
var base64: String
|
||||
var durationMs: Int
|
||||
var hasAudio: Bool
|
||||
}
|
||||
let payload = try Self.encodePayload(ClipPayload(
|
||||
format: (params.format ?? .mp4).rawValue,
|
||||
base64: data.base64EncodedString(),
|
||||
durationMs: res.durationMs,
|
||||
hasAudio: res.hasAudio))
|
||||
return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: payload)
|
||||
case ClawdbotCameraCommand.list.rawValue:
|
||||
let devices = await self.cameraCapture.listDevices()
|
||||
let payload = try Self.encodePayload(["devices": devices])
|
||||
return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: payload)
|
||||
default:
|
||||
return Self.errorResponse(req, code: .invalidRequest, message: "INVALID_REQUEST: unknown command")
|
||||
}
|
||||
}
|
||||
|
||||
private func handleLocationInvoke(_ req: BridgeInvokeRequest) async throws -> BridgeInvokeResponse {
|
||||
let mode = Self.locationMode()
|
||||
guard mode != .off else {
|
||||
return BridgeInvokeResponse(
|
||||
id: req.id,
|
||||
ok: false,
|
||||
error: ClawdbotNodeError(
|
||||
code: .unavailable,
|
||||
message: "LOCATION_DISABLED: enable Location in Settings"))
|
||||
}
|
||||
let params = (try? Self.decodeParams(ClawdbotLocationGetParams.self, from: req.paramsJSON)) ??
|
||||
ClawdbotLocationGetParams()
|
||||
let desired = params.desiredAccuracy ??
|
||||
(Self.locationPreciseEnabled() ? .precise : .balanced)
|
||||
let status = await self.locationService.authorizationStatus()
|
||||
if status != .authorizedAlways {
|
||||
return BridgeInvokeResponse(
|
||||
id: req.id,
|
||||
ok: false,
|
||||
error: ClawdbotNodeError(
|
||||
code: .unavailable,
|
||||
message: "LOCATION_PERMISSION_REQUIRED: grant Location permission"))
|
||||
}
|
||||
do {
|
||||
let location = try await self.locationService.currentLocation(
|
||||
desiredAccuracy: desired,
|
||||
maxAgeMs: params.maxAgeMs,
|
||||
timeoutMs: params.timeoutMs)
|
||||
let isPrecise = await self.locationService.accuracyAuthorization() == .fullAccuracy
|
||||
let payload = ClawdbotLocationPayload(
|
||||
lat: location.coordinate.latitude,
|
||||
lon: location.coordinate.longitude,
|
||||
accuracyMeters: location.horizontalAccuracy,
|
||||
altitudeMeters: location.verticalAccuracy >= 0 ? location.altitude : nil,
|
||||
speedMps: location.speed >= 0 ? location.speed : nil,
|
||||
headingDeg: location.course >= 0 ? location.course : nil,
|
||||
timestamp: ISO8601DateFormatter().string(from: location.timestamp),
|
||||
isPrecise: isPrecise,
|
||||
source: nil)
|
||||
let json = try Self.encodePayload(payload)
|
||||
return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: json)
|
||||
} catch MacNodeLocationService.Error.timeout {
|
||||
return BridgeInvokeResponse(
|
||||
id: req.id,
|
||||
ok: false,
|
||||
error: ClawdbotNodeError(
|
||||
code: .unavailable,
|
||||
message: "LOCATION_TIMEOUT: no fix in time"))
|
||||
} catch {
|
||||
return BridgeInvokeResponse(
|
||||
id: req.id,
|
||||
ok: false,
|
||||
error: ClawdbotNodeError(
|
||||
code: .unavailable,
|
||||
message: "LOCATION_UNAVAILABLE: \(error.localizedDescription)"))
|
||||
}
|
||||
}
|
||||
|
||||
private func handleScreenRecordInvoke(_ req: BridgeInvokeRequest) async throws -> BridgeInvokeResponse {
|
||||
let params = (try? Self.decodeParams(MacNodeScreenRecordParams.self, from: req.paramsJSON)) ??
|
||||
MacNodeScreenRecordParams()
|
||||
if let format = params.format?.lowercased(), !format.isEmpty, format != "mp4" {
|
||||
return Self.errorResponse(
|
||||
req,
|
||||
code: .invalidRequest,
|
||||
message: "INVALID_REQUEST: screen format must be mp4")
|
||||
}
|
||||
let res = try await self.screenRecorder.record(
|
||||
screenIndex: params.screenIndex,
|
||||
durationMs: params.durationMs,
|
||||
fps: params.fps,
|
||||
includeAudio: params.includeAudio,
|
||||
outPath: nil)
|
||||
defer { try? FileManager.default.removeItem(atPath: res.path) }
|
||||
let data = try Data(contentsOf: URL(fileURLWithPath: res.path))
|
||||
struct ScreenPayload: Encodable {
|
||||
var format: String
|
||||
var base64: String
|
||||
var durationMs: Int?
|
||||
var fps: Double?
|
||||
var screenIndex: Int?
|
||||
var hasAudio: Bool
|
||||
}
|
||||
let payload = try Self.encodePayload(ScreenPayload(
|
||||
format: "mp4",
|
||||
base64: data.base64EncodedString(),
|
||||
durationMs: params.durationMs,
|
||||
fps: params.fps,
|
||||
screenIndex: params.screenIndex,
|
||||
hasAudio: res.hasAudio))
|
||||
return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: payload)
|
||||
}
|
||||
|
||||
private func handleA2UIReset(_ req: BridgeInvokeRequest) async throws -> BridgeInvokeResponse {
|
||||
try await self.ensureA2UIHost()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user