mirror of
https://github.com/openclaw/openclaw.git
synced 2026-02-19 18:39:20 -05:00
- Enhanced the delivery configuration logic in CronJobEditor to explicitly set the bestEffort property based on job settings. - Refactored the CLI command to streamline delivery object creation, ensuring proper handling of optional fields like channel and to. - Improved code readability and maintainability by restructuring delivery assignment logic. This update clarifies the delivery configuration process, enhancing the reliability of job settings in both the editor and CLI.
272 lines
9.6 KiB
Swift
272 lines
9.6 KiB
Swift
import OpenClawProtocol
|
|
import Foundation
|
|
import SwiftUI
|
|
|
|
extension CronJobEditor {
|
|
func gridLabel(_ text: String) -> some View {
|
|
Text(text)
|
|
.foregroundStyle(.secondary)
|
|
.frame(width: self.labelColumnWidth, alignment: .leading)
|
|
}
|
|
|
|
func hydrateFromJob() {
|
|
guard let job else { return }
|
|
self.name = job.name
|
|
self.description = job.description ?? ""
|
|
self.agentId = job.agentId ?? ""
|
|
self.enabled = job.enabled
|
|
self.deleteAfterRun = job.deleteAfterRun ?? false
|
|
self.sessionTarget = job.sessionTarget
|
|
self.wakeMode = job.wakeMode
|
|
|
|
switch job.schedule {
|
|
case let .at(at):
|
|
self.scheduleKind = .at
|
|
if let date = CronSchedule.parseAtDate(at) {
|
|
self.atDate = date
|
|
}
|
|
case let .every(everyMs, _):
|
|
self.scheduleKind = .every
|
|
self.everyText = self.formatDuration(ms: everyMs)
|
|
case let .cron(expr, tz):
|
|
self.scheduleKind = .cron
|
|
self.cronExpr = expr
|
|
self.cronTz = tz ?? ""
|
|
}
|
|
|
|
switch job.payload {
|
|
case let .systemEvent(text):
|
|
self.payloadKind = .systemEvent
|
|
self.systemEventText = text
|
|
case let .agentTurn(message, thinking, timeoutSeconds, _, _, _, _):
|
|
self.payloadKind = .agentTurn
|
|
self.agentMessage = message
|
|
self.thinking = thinking ?? ""
|
|
self.timeoutSeconds = timeoutSeconds.map(String.init) ?? ""
|
|
}
|
|
|
|
if let delivery = job.delivery {
|
|
self.deliveryMode = delivery.mode == .announce ? .announce : .none
|
|
let trimmed = (delivery.channel ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
|
|
self.channel = trimmed.isEmpty ? "last" : trimmed
|
|
self.to = delivery.to ?? ""
|
|
self.bestEffortDeliver = delivery.bestEffort ?? false
|
|
} else if self.sessionTarget == .isolated {
|
|
self.deliveryMode = .announce
|
|
}
|
|
}
|
|
|
|
func save() {
|
|
do {
|
|
self.error = nil
|
|
let payload = try self.buildPayload()
|
|
self.onSave(payload)
|
|
} catch {
|
|
self.error = error.localizedDescription
|
|
}
|
|
}
|
|
|
|
func buildPayload() throws -> [String: AnyCodable] {
|
|
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 {
|
|
root["delivery"] = self.buildDelivery()
|
|
}
|
|
|
|
return root.mapValues { AnyCodable($0) }
|
|
}
|
|
|
|
func buildDelivery() -> [String: Any] {
|
|
let mode = self.deliveryMode == .announce ? "announce" : "none"
|
|
var delivery: [String: Any] = ["mode": mode]
|
|
if self.deliveryMode == .announce {
|
|
let trimmed = self.channel.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
delivery["channel"] = trimmed.isEmpty ? "last" : trimmed
|
|
let to = self.to.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
if !to.isEmpty { delivery["to"] = to }
|
|
if self.bestEffortDeliver {
|
|
delivery["bestEffort"] = true
|
|
} else if self.job?.delivery?.bestEffort == true {
|
|
delivery["bestEffort"] = false
|
|
}
|
|
}
|
|
return delivery
|
|
}
|
|
|
|
func trimmed(_ value: String) -> String {
|
|
value.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
}
|
|
|
|
func requireName() throws -> String {
|
|
let name = self.trimmed(self.name)
|
|
if name.isEmpty {
|
|
throw NSError(
|
|
domain: "Cron",
|
|
code: 0,
|
|
userInfo: [NSLocalizedDescriptionKey: "Name is required."])
|
|
}
|
|
return name
|
|
}
|
|
|
|
func buildSchedule() throws -> [String: Any] {
|
|
switch self.scheduleKind {
|
|
case .at:
|
|
return ["kind": "at", "at": CronSchedule.formatIsoDate(self.atDate)]
|
|
case .every:
|
|
guard let ms = Self.parseDurationMs(self.everyText) else {
|
|
throw NSError(
|
|
domain: "Cron",
|
|
code: 0,
|
|
userInfo: [NSLocalizedDescriptionKey: "Invalid every duration (use 10m, 1h, 1d)."])
|
|
}
|
|
return ["kind": "every", "everyMs": ms]
|
|
case .cron:
|
|
let expr = self.trimmed(self.cronExpr)
|
|
if expr.isEmpty {
|
|
throw NSError(
|
|
domain: "Cron",
|
|
code: 0,
|
|
userInfo: [NSLocalizedDescriptionKey: "Cron expression is required."])
|
|
}
|
|
let tz = self.trimmed(self.cronTz)
|
|
if tz.isEmpty {
|
|
return ["kind": "cron", "expr": expr]
|
|
}
|
|
return ["kind": "cron", "expr": expr, "tz": tz]
|
|
}
|
|
}
|
|
|
|
func buildSelectedPayload() throws -> [String: Any] {
|
|
if self.sessionTarget == .isolated { return self.buildAgentTurnPayload() }
|
|
switch self.payloadKind {
|
|
case .systemEvent:
|
|
let text = self.trimmed(self.systemEventText)
|
|
return ["kind": "systemEvent", "text": text]
|
|
case .agentTurn:
|
|
return self.buildAgentTurnPayload()
|
|
}
|
|
}
|
|
|
|
func validateSessionTarget(_ payload: [String: Any]) throws {
|
|
if self.sessionTarget == .main, payload["kind"] as? String == "agentTurn" {
|
|
throw NSError(
|
|
domain: "Cron",
|
|
code: 0,
|
|
userInfo: [
|
|
NSLocalizedDescriptionKey:
|
|
"Main session jobs require systemEvent payloads (switch Session target to isolated).",
|
|
])
|
|
}
|
|
|
|
if self.sessionTarget == .isolated, payload["kind"] as? String == "systemEvent" {
|
|
throw NSError(
|
|
domain: "Cron",
|
|
code: 0,
|
|
userInfo: [NSLocalizedDescriptionKey: "Isolated jobs require agentTurn payloads."])
|
|
}
|
|
}
|
|
|
|
func validatePayloadRequiredFields(_ payload: [String: Any]) throws {
|
|
if payload["kind"] as? String == "systemEvent" {
|
|
if (payload["text"] as? String ?? "").isEmpty {
|
|
throw NSError(
|
|
domain: "Cron",
|
|
code: 0,
|
|
userInfo: [NSLocalizedDescriptionKey: "System event text is required."])
|
|
}
|
|
}
|
|
if payload["kind"] as? String == "agentTurn" {
|
|
if (payload["message"] as? String ?? "").isEmpty {
|
|
throw NSError(
|
|
domain: "Cron",
|
|
code: 0,
|
|
userInfo: [NSLocalizedDescriptionKey: "Agent message is required."])
|
|
}
|
|
}
|
|
}
|
|
|
|
func applyDeleteAfterRun(
|
|
to root: inout [String: Any],
|
|
scheduleKind: ScheduleKind? = nil,
|
|
deleteAfterRun: Bool? = nil)
|
|
{
|
|
let resolvedSchedule = scheduleKind ?? self.scheduleKind
|
|
let resolvedDelete = deleteAfterRun ?? self.deleteAfterRun
|
|
if resolvedSchedule == .at {
|
|
root["deleteAfterRun"] = resolvedDelete
|
|
} else if self.job?.deleteAfterRun != nil {
|
|
root["deleteAfterRun"] = false
|
|
}
|
|
}
|
|
|
|
func buildAgentTurnPayload() -> [String: Any] {
|
|
let msg = self.agentMessage.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
var payload: [String: Any] = ["kind": "agentTurn", "message": msg]
|
|
let thinking = self.thinking.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
if !thinking.isEmpty { payload["thinking"] = thinking }
|
|
if let n = Int(self.timeoutSeconds), n > 0 { payload["timeoutSeconds"] = n }
|
|
return payload
|
|
}
|
|
|
|
static func parseDurationMs(_ input: String) -> Int? {
|
|
let raw = input.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
if raw.isEmpty { return nil }
|
|
|
|
let rx = try? NSRegularExpression(pattern: "^(\\d+(?:\\.\\d+)?)(ms|s|m|h|d)$", options: [.caseInsensitive])
|
|
guard let match = rx?.firstMatch(in: raw, range: NSRange(location: 0, length: raw.utf16.count)) else {
|
|
return nil
|
|
}
|
|
func group(_ idx: Int) -> String {
|
|
let range = match.range(at: idx)
|
|
guard let r = Range(range, in: raw) else { return "" }
|
|
return String(raw[r])
|
|
}
|
|
let n = Double(group(1)) ?? 0
|
|
if !n.isFinite || n <= 0 { return nil }
|
|
let unit = group(2).lowercased()
|
|
let factor: Double = switch unit {
|
|
case "ms": 1
|
|
case "s": 1000
|
|
case "m": 60000
|
|
case "h": 3_600_000
|
|
default: 86_400_000
|
|
}
|
|
return Int(floor(n * factor))
|
|
}
|
|
|
|
func formatDuration(ms: Int) -> String {
|
|
if ms < 1000 { return "\(ms)ms" }
|
|
let s = Double(ms) / 1000.0
|
|
if s < 60 { return "\(Int(round(s)))s" }
|
|
let m = s / 60.0
|
|
if m < 60 { return "\(Int(round(m)))m" }
|
|
let h = m / 60.0
|
|
if h < 48 { return "\(Int(round(h)))h" }
|
|
let d = h / 24.0
|
|
return "\(Int(round(d)))d"
|
|
}
|
|
}
|