refactor: simplify cron job editor payloads

This commit is contained in:
Peter Steinberger
2026-01-13 05:13:49 +00:00
parent 38244b8e94
commit d8f14078f0
2 changed files with 71 additions and 46 deletions

View File

@@ -61,19 +61,60 @@ extension CronJobEditor {
} }
func buildPayload() throws -> [String: AnyCodable] { func buildPayload() throws -> [String: AnyCodable] {
let name = self.name.trimmingCharacters(in: .whitespacesAndNewlines) let name = try self.requireName()
let description = self.trimmed(self.description)
let agentId = self.trimmed(self.agentId)
let schedule = try self.buildSchedule()
let payload = try self.buildSelectedPayload()
try self.validateSessionTarget(payload)
try self.validatePayloadRequiredFields(payload)
var root: [String: Any] = [
"name": name,
"enabled": self.enabled,
"schedule": schedule,
"sessionTarget": self.sessionTarget.rawValue,
"wakeMode": self.wakeMode.rawValue,
"payload": payload,
]
self.applyDeleteAfterRun(to: &root)
if !description.isEmpty { root["description"] = description }
if !agentId.isEmpty {
root["agentId"] = agentId
} else if self.job?.agentId != nil {
root["agentId"] = NSNull()
}
if self.sessionTarget == .isolated {
let trimmed = self.postPrefix.trimmingCharacters(in: .whitespacesAndNewlines)
root["isolation"] = [
"postToMainPrefix": trimmed.isEmpty ? "Cron" : trimmed,
]
}
return root.mapValues { AnyCodable($0) }
}
func trimmed(_ value: String) -> String {
value.trimmingCharacters(in: .whitespacesAndNewlines)
}
func requireName() throws -> String {
let name = self.trimmed(self.name)
if name.isEmpty { if name.isEmpty {
throw NSError( throw NSError(
domain: "Cron", domain: "Cron",
code: 0, code: 0,
userInfo: [NSLocalizedDescriptionKey: "Name is required."]) userInfo: [NSLocalizedDescriptionKey: "Name is required."])
} }
let description = self.description.trimmingCharacters(in: .whitespacesAndNewlines) return name
let agentId = self.agentId.trimmingCharacters(in: .whitespacesAndNewlines) }
let schedule: [String: Any]
func buildSchedule() throws -> [String: Any] {
switch self.scheduleKind { switch self.scheduleKind {
case .at: case .at:
schedule = ["kind": "at", "atMs": Int(self.atDate.timeIntervalSince1970 * 1000)] return ["kind": "at", "atMs": Int(self.atDate.timeIntervalSince1970 * 1000)]
case .every: case .every:
guard let ms = Self.parseDurationMs(self.everyText) else { guard let ms = Self.parseDurationMs(self.everyText) else {
throw NSError( throw NSError(
@@ -81,34 +122,35 @@ extension CronJobEditor {
code: 0, code: 0,
userInfo: [NSLocalizedDescriptionKey: "Invalid every duration (use 10m, 1h, 1d)."]) userInfo: [NSLocalizedDescriptionKey: "Invalid every duration (use 10m, 1h, 1d)."])
} }
schedule = ["kind": "every", "everyMs": ms] return ["kind": "every", "everyMs": ms]
case .cron: case .cron:
let expr = self.cronExpr.trimmingCharacters(in: .whitespacesAndNewlines) let expr = self.trimmed(self.cronExpr)
if expr.isEmpty { if expr.isEmpty {
throw NSError( throw NSError(
domain: "Cron", domain: "Cron",
code: 0, code: 0,
userInfo: [NSLocalizedDescriptionKey: "Cron expression is required."]) userInfo: [NSLocalizedDescriptionKey: "Cron expression is required."])
} }
let tz = self.cronTz.trimmingCharacters(in: .whitespacesAndNewlines) let tz = self.trimmed(self.cronTz)
if tz.isEmpty { if tz.isEmpty {
schedule = ["kind": "cron", "expr": expr] return ["kind": "cron", "expr": expr]
} else {
schedule = ["kind": "cron", "expr": expr, "tz": tz]
} }
return ["kind": "cron", "expr": expr, "tz": tz]
} }
}
let payload: [String: Any] = { func buildSelectedPayload() throws -> [String: Any] {
if self.sessionTarget == .isolated { return self.buildAgentTurnPayload() } if self.sessionTarget == .isolated { return self.buildAgentTurnPayload() }
switch self.payloadKind { switch self.payloadKind {
case .systemEvent: case .systemEvent:
let text = self.systemEventText.trimmingCharacters(in: .whitespacesAndNewlines) let text = self.trimmed(self.systemEventText)
return ["kind": "systemEvent", "text": text] return ["kind": "systemEvent", "text": text]
case .agentTurn: case .agentTurn:
return self.buildAgentTurnPayload() return self.buildAgentTurnPayload()
} }
}() }
func validateSessionTarget(_ payload: [String: Any]) throws {
if self.sessionTarget == .main, payload["kind"] as? String == "agentTurn" { if self.sessionTarget == .main, payload["kind"] as? String == "agentTurn" {
throw NSError( throw NSError(
domain: "Cron", domain: "Cron",
@@ -125,7 +167,9 @@ extension CronJobEditor {
code: 0, code: 0,
userInfo: [NSLocalizedDescriptionKey: "Isolated jobs require agentTurn payloads."]) userInfo: [NSLocalizedDescriptionKey: "Isolated jobs require agentTurn payloads."])
} }
}
func validatePayloadRequiredFields(_ payload: [String: Any]) throws {
if payload["kind"] as? String == "systemEvent" { if payload["kind"] as? String == "systemEvent" {
if (payload["text"] as? String ?? "").isEmpty { if (payload["text"] as? String ?? "").isEmpty {
throw NSError( throw NSError(
@@ -133,7 +177,8 @@ extension CronJobEditor {
code: 0, code: 0,
userInfo: [NSLocalizedDescriptionKey: "System event text is required."]) userInfo: [NSLocalizedDescriptionKey: "System event text is required."])
} }
} else if payload["kind"] as? String == "agentTurn" { }
if payload["kind"] as? String == "agentTurn" {
if (payload["message"] as? String ?? "").isEmpty { if (payload["message"] as? String ?? "").isEmpty {
throw NSError( throw NSError(
domain: "Cron", domain: "Cron",
@@ -141,35 +186,14 @@ extension CronJobEditor {
userInfo: [NSLocalizedDescriptionKey: "Agent message is required."]) userInfo: [NSLocalizedDescriptionKey: "Agent message is required."])
} }
} }
}
var root: [String: Any] = [ func applyDeleteAfterRun(to root: inout [String: Any]) {
"name": name,
"enabled": self.enabled,
"schedule": schedule,
"sessionTarget": self.sessionTarget.rawValue,
"wakeMode": self.wakeMode.rawValue,
"payload": payload,
]
if self.scheduleKind == .at { if self.scheduleKind == .at {
root["deleteAfterRun"] = self.deleteAfterRun root["deleteAfterRun"] = self.deleteAfterRun
} else if self.job?.deleteAfterRun != nil { } else if self.job?.deleteAfterRun != nil {
root["deleteAfterRun"] = false root["deleteAfterRun"] = false
} }
if !description.isEmpty { root["description"] = description }
if !agentId.isEmpty {
root["agentId"] = agentId
} else if self.job?.agentId != nil {
root["agentId"] = NSNull()
}
if self.sessionTarget == .isolated {
let trimmed = self.postPrefix.trimmingCharacters(in: .whitespacesAndNewlines)
root["isolation"] = [
"postToMainPrefix": trimmed.isEmpty ? "Cron" : trimmed,
]
}
return root.mapValues { AnyCodable($0) }
} }
func buildAgentTurnPayload() -> [String: Any] { func buildAgentTurnPayload() -> [String: Any] {

View File

@@ -118,7 +118,8 @@ private func printUsage() {
clawdbot-mac-wizard clawdbot-mac-wizard
Usage: Usage:
clawdbot-mac-wizard [--url <ws://host:port>] [--token <token>] [--password <password>] [--mode <local|remote>] [--workspace <path>] [--json] clawdbot-mac-wizard [--url <ws://host:port>] [--token <token>] [--password <password>]
[--mode <local|remote>] [--workspace <path>] [--json]
Options: Options:
--url <url> Gateway WebSocket URL (overrides config) --url <url> Gateway WebSocket URL (overrides config)