feat: share to openclaw ios app (#19424)

Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 0a7ab8589a
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Reviewed-by: @mbelinky
This commit is contained in:
Mariano
2026-02-17 20:08:50 +00:00
committed by GitHub
parent 81c5c02e53
commit bfc9736366
19 changed files with 1300 additions and 108 deletions

View File

@@ -0,0 +1,62 @@
import Foundation
public struct ShareGatewayRelayConfig: Codable, Sendable, Equatable {
public let gatewayURLString: String
public let token: String?
public let password: String?
public let sessionKey: String
public let deliveryChannel: String?
public let deliveryTo: String?
public init(
gatewayURLString: String,
token: String?,
password: String?,
sessionKey: String,
deliveryChannel: String? = nil,
deliveryTo: String? = nil)
{
self.gatewayURLString = gatewayURLString
self.token = token
self.password = password
self.sessionKey = sessionKey
self.deliveryChannel = deliveryChannel
self.deliveryTo = deliveryTo
}
}
public enum ShareGatewayRelaySettings {
private static let suiteName = "group.ai.openclaw.shared"
private static let relayConfigKey = "share.gatewayRelay.config.v1"
private static let lastEventKey = "share.gatewayRelay.event.v1"
private static var defaults: UserDefaults {
UserDefaults(suiteName: self.suiteName) ?? .standard
}
public static func loadConfig() -> ShareGatewayRelayConfig? {
guard let data = self.defaults.data(forKey: self.relayConfigKey) else { return nil }
return try? JSONDecoder().decode(ShareGatewayRelayConfig.self, from: data)
}
public static func saveConfig(_ config: ShareGatewayRelayConfig) {
guard let data = try? JSONEncoder().encode(config) else { return }
self.defaults.set(data, forKey: self.relayConfigKey)
}
public static func clearConfig() {
self.defaults.removeObject(forKey: self.relayConfigKey)
}
public static func saveLastEvent(_ message: String) {
let timestamp = ISO8601DateFormatter().string(from: Date())
let payload = "[\(timestamp)] \(message)"
self.defaults.set(payload, forKey: self.lastEventKey)
}
public static func loadLastEvent() -> String? {
let value = self.defaults.string(forKey: self.lastEventKey)?
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
return value.isEmpty ? nil : value
}
}

View File

@@ -0,0 +1,62 @@
import Foundation
public struct SharedContentPayload: Sendable, Equatable {
public let title: String?
public let url: URL?
public let text: String?
public init(title: String?, url: URL?, text: String?) {
self.title = title
self.url = url
self.text = text
}
}
public enum ShareToAgentDeepLink {
public static func buildURL(from payload: SharedContentPayload, instruction: String? = nil) -> URL? {
let message = self.buildMessage(from: payload, instruction: instruction)
guard !message.isEmpty else { return nil }
var components = URLComponents()
components.scheme = "openclaw"
components.host = "agent"
components.queryItems = [
URLQueryItem(name: "message", value: message),
URLQueryItem(name: "thinking", value: "low"),
]
return components.url
}
public static func buildMessage(from payload: SharedContentPayload, instruction: String? = nil) -> String {
let title = self.clean(payload.title)
let text = self.clean(payload.text)
let urlText = payload.url?.absoluteString.trimmingCharacters(in: .whitespacesAndNewlines)
let resolvedInstruction = self.clean(instruction) ?? ShareToAgentSettings.loadDefaultInstruction()
var lines: [String] = ["Shared from iOS."]
if let title, !title.isEmpty {
lines.append("Title: \(title)")
}
if let urlText, !urlText.isEmpty {
lines.append("URL: \(urlText)")
}
if let text, !text.isEmpty {
lines.append("Text:\n\(text)")
}
lines.append(resolvedInstruction)
let message = lines.joined(separator: "\n\n")
return self.limit(message, maxCharacters: 2400)
}
private static func clean(_ value: String?) -> String? {
guard let value else { return nil }
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
return trimmed.isEmpty ? nil : trimmed
}
private static func limit(_ value: String, maxCharacters: Int) -> String {
guard value.count > maxCharacters else { return value }
return String(value.prefix(maxCharacters))
}
}

View File

@@ -0,0 +1,29 @@
import Foundation
public enum ShareToAgentSettings {
private static let suiteName = "group.ai.openclaw.shared"
private static let defaultInstructionKey = "share.defaultInstruction"
private static let fallbackInstruction = "Please help me with this."
private static var defaults: UserDefaults {
UserDefaults(suiteName: suiteName) ?? .standard
}
public static func loadDefaultInstruction() -> String {
let raw = self.defaults.string(forKey: self.defaultInstructionKey)?
.trimmingCharacters(in: .whitespacesAndNewlines)
if let raw, !raw.isEmpty {
return raw
}
return self.fallbackInstruction
}
public static func saveDefaultInstruction(_ value: String?) {
let trimmed = value?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
if trimmed.isEmpty {
self.defaults.removeObject(forKey: self.defaultInstructionKey)
return
}
self.defaults.set(trimmed, forKey: self.defaultInstructionKey)
}
}