100 lines
3.8 KiB
Swift
100 lines
3.8 KiB
Swift
import Foundation
|
|
|
|
public enum ClawdbotCanvasA2UIAction: Sendable {
|
|
public struct AgentMessageContext: Sendable {
|
|
public struct Session: Sendable {
|
|
public var key: String
|
|
public var surfaceId: String
|
|
|
|
public init(key: String, surfaceId: String) {
|
|
self.key = key
|
|
self.surfaceId = surfaceId
|
|
}
|
|
}
|
|
|
|
public struct Component: Sendable {
|
|
public var id: String
|
|
public var host: String
|
|
public var instanceId: String
|
|
|
|
public init(id: String, host: String, instanceId: String) {
|
|
self.id = id
|
|
self.host = host
|
|
self.instanceId = instanceId
|
|
}
|
|
}
|
|
|
|
public var actionName: String
|
|
public var session: Session
|
|
public var component: Component
|
|
public var contextJSON: String?
|
|
|
|
public init(actionName: String, session: Session, component: Component, contextJSON: String?) {
|
|
self.actionName = actionName
|
|
self.session = session
|
|
self.component = component
|
|
self.contextJSON = contextJSON
|
|
}
|
|
}
|
|
|
|
public static func extractActionName(_ userAction: [String: Any]) -> String? {
|
|
let keys = ["name", "action"]
|
|
for key in keys {
|
|
if let raw = userAction[key] as? String {
|
|
let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
if !trimmed.isEmpty { return trimmed }
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
public static func sanitizeTagValue(_ value: String) -> String {
|
|
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
let nonEmpty = trimmed.isEmpty ? "-" : trimmed
|
|
let normalized = nonEmpty.replacingOccurrences(of: " ", with: "_")
|
|
let allowed = CharacterSet(charactersIn: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-.:")
|
|
let scalars = normalized.unicodeScalars.map { allowed.contains($0) ? Character($0) : "_" }
|
|
return String(scalars)
|
|
}
|
|
|
|
public static func compactJSON(_ obj: Any?) -> String? {
|
|
guard let obj else { return nil }
|
|
guard JSONSerialization.isValidJSONObject(obj) else { return nil }
|
|
guard let data = try? JSONSerialization.data(withJSONObject: obj, options: []),
|
|
let str = String(data: data, encoding: .utf8)
|
|
else { return nil }
|
|
return str
|
|
}
|
|
|
|
public static func formatAgentMessage(_ context: AgentMessageContext) -> String {
|
|
let ctxSuffix = context.contextJSON.flatMap { $0.isEmpty ? nil : " ctx=\($0)" } ?? ""
|
|
return [
|
|
"CANVAS_A2UI",
|
|
"action=\(self.sanitizeTagValue(context.actionName))",
|
|
"session=\(self.sanitizeTagValue(context.session.key))",
|
|
"surface=\(self.sanitizeTagValue(context.session.surfaceId))",
|
|
"component=\(self.sanitizeTagValue(context.component.id))",
|
|
"host=\(self.sanitizeTagValue(context.component.host))",
|
|
"instance=\(self.sanitizeTagValue(context.component.instanceId))\(ctxSuffix)",
|
|
"default=update_canvas",
|
|
].joined(separator: " ")
|
|
}
|
|
|
|
public static func jsDispatchA2UIActionStatus(actionId: String, ok: Bool, error: String?) -> String {
|
|
let payload: [String: Any] = [
|
|
"id": actionId,
|
|
"ok": ok,
|
|
"error": error ?? "",
|
|
]
|
|
let json: String = {
|
|
if let data = try? JSONSerialization.data(withJSONObject: payload, options: []),
|
|
let str = String(data: data, encoding: .utf8)
|
|
{
|
|
return str
|
|
}
|
|
return "{\"id\":\"\(actionId)\",\"ok\":\(ok ? "true" : "false"),\"error\":\"\"}"
|
|
}()
|
|
return "window.dispatchEvent(new CustomEvent('clawdbot:a2ui-action-status', { detail: \(json) }));"
|
|
}
|
|
}
|