mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-17 10:31:37 +00:00
* refactor(apple): share chat gateway requests * fix(ci): sync native app inventory * docs: keep Apple release note in PR body
419 lines
14 KiB
Swift
419 lines
14 KiB
Swift
import Foundation
|
|
import OpenClawKit
|
|
import OpenClawProtocol
|
|
|
|
public struct OpenClawChatGatewayRequest: Sendable, Equatable {
|
|
public let method: String
|
|
public let params: [String: AnyCodable]
|
|
public let timeoutMs: Double
|
|
|
|
public init(method: String, params: [String: AnyCodable] = [:], timeoutMs: Double) {
|
|
self.method = method
|
|
self.params = params
|
|
self.timeoutMs = timeoutMs
|
|
}
|
|
}
|
|
|
|
public enum OpenClawChatSessionTargetPolicy: Sendable {
|
|
case preserveBareKeys
|
|
case scopeBareKeysToSelectedAgent
|
|
}
|
|
|
|
public struct OpenClawChatSessionTarget: Sendable, Equatable {
|
|
public let sessionKey: String
|
|
public let agentID: String?
|
|
|
|
public init(sessionKey: String, agentID: String?) {
|
|
self.sessionKey = sessionKey
|
|
self.agentID = agentID
|
|
}
|
|
|
|
public static func resolve(
|
|
_ rawSessionKey: String,
|
|
selectedAgentID: String?,
|
|
overrideAgentID: String? = nil,
|
|
policy: OpenClawChatSessionTargetPolicy) -> Self
|
|
{
|
|
let sessionKey = rawSessionKey.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
let selected = self.normalizedAgentID(selectedAgentID)
|
|
let override = self.normalizedAgentID(overrideAgentID)
|
|
|
|
if OpenClawChatSessionKey.agentID(from: sessionKey) != nil {
|
|
return Self(sessionKey: sessionKey, agentID: override)
|
|
}
|
|
let lowercasedKey = sessionKey.lowercased()
|
|
if lowercasedKey.hasPrefix("agent:") || lowercasedKey == "unknown" {
|
|
return Self(sessionKey: sessionKey, agentID: nil)
|
|
}
|
|
if lowercasedKey == "global" {
|
|
return Self(sessionKey: sessionKey, agentID: override ?? selected)
|
|
}
|
|
|
|
switch policy {
|
|
case .preserveBareKeys:
|
|
return Self(sessionKey: sessionKey, agentID: override)
|
|
case .scopeBareKeysToSelectedAgent:
|
|
guard let agentID = override ?? selected else {
|
|
return Self(sessionKey: sessionKey, agentID: nil)
|
|
}
|
|
return Self(sessionKey: "agent:\(agentID):\(sessionKey)", agentID: nil)
|
|
}
|
|
}
|
|
|
|
private static func normalizedAgentID(_ agentID: String?) -> String? {
|
|
let normalized = agentID?.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
|
return normalized?.isEmpty == false ? normalized : nil
|
|
}
|
|
}
|
|
|
|
public enum OpenClawChatGatewayRequests {
|
|
private static let defaultTimeoutMs: Double = 15000
|
|
private static let mutationTimeoutMs: Double = 15000
|
|
private static let shortTimeoutMs: Double = 10000
|
|
private static let compactionTimeoutMs: Double = 0
|
|
|
|
public static func agentsList(timeoutMs: Double = 15000) -> OpenClawChatGatewayRequest {
|
|
OpenClawChatGatewayRequest(method: "agents.list", timeoutMs: timeoutMs)
|
|
}
|
|
|
|
public static func modelsList() -> OpenClawChatGatewayRequest {
|
|
OpenClawChatGatewayRequest(method: "models.list", timeoutMs: self.defaultTimeoutMs)
|
|
}
|
|
|
|
public static func sessionsList(
|
|
limit: Int?,
|
|
search: String?,
|
|
archived: Bool,
|
|
includeGlobal: Bool = true,
|
|
includeUnknown: Bool = false,
|
|
activeMinutes: Int? = nil,
|
|
timeoutMs: Double = 15000) -> OpenClawChatGatewayRequest
|
|
{
|
|
var params: [String: AnyCodable] = [
|
|
"includeGlobal": AnyCodable(includeGlobal),
|
|
"includeUnknown": AnyCodable(includeUnknown),
|
|
]
|
|
if let limit {
|
|
params["limit"] = AnyCodable(limit)
|
|
}
|
|
if let activeMinutes {
|
|
params["activeMinutes"] = AnyCodable(activeMinutes)
|
|
}
|
|
let normalizedSearch = self.normalized(search)
|
|
if let normalizedSearch {
|
|
params["search"] = AnyCodable(normalizedSearch)
|
|
}
|
|
if archived {
|
|
params["archived"] = AnyCodable(true)
|
|
}
|
|
return OpenClawChatGatewayRequest(
|
|
method: "sessions.list",
|
|
params: params,
|
|
timeoutMs: timeoutMs)
|
|
}
|
|
|
|
public static func createSession(
|
|
key: String,
|
|
agentID: String?,
|
|
label: String?,
|
|
parentSessionKey: String?,
|
|
worktree: Bool?) -> OpenClawChatGatewayRequest
|
|
{
|
|
var params = ["key": AnyCodable(key)]
|
|
self.add(agentID, to: ¶ms, key: "agentId")
|
|
self.add(label, to: ¶ms, key: "label", trim: false)
|
|
self.add(parentSessionKey, to: ¶ms, key: "parentSessionKey", trim: false)
|
|
if let worktree {
|
|
params["worktree"] = AnyCodable(worktree)
|
|
}
|
|
return OpenClawChatGatewayRequest(
|
|
method: "sessions.create",
|
|
params: params,
|
|
timeoutMs: self.mutationTimeoutMs)
|
|
}
|
|
|
|
public static func abortRun(
|
|
sessionKey: String,
|
|
agentID: String?,
|
|
runID: String,
|
|
requestTimeoutMs: Int = 10000) -> OpenClawChatGatewayRequest
|
|
{
|
|
var params: [String: AnyCodable] = [
|
|
"sessionKey": AnyCodable(sessionKey),
|
|
"runId": AnyCodable(runID),
|
|
]
|
|
self.add(agentID, to: ¶ms, key: "agentId")
|
|
return OpenClawChatGatewayRequest(
|
|
method: "chat.abort",
|
|
params: params,
|
|
timeoutMs: Double(requestTimeoutMs))
|
|
}
|
|
|
|
public static func patchSessionModel(
|
|
sessionKey: String,
|
|
agentID: String?,
|
|
model: String?) -> OpenClawChatGatewayRequest
|
|
{
|
|
var params = self.sessionParams(sessionKey: sessionKey, agentID: agentID)
|
|
params["model"] = model.map(AnyCodable.init) ?? AnyCodable(NSNull())
|
|
return OpenClawChatGatewayRequest(
|
|
method: "sessions.patch",
|
|
params: params,
|
|
timeoutMs: self.mutationTimeoutMs)
|
|
}
|
|
|
|
public static func patchSessionPreferences(
|
|
sessionKey: String,
|
|
agentID: String?,
|
|
thinkingLevel: String?? = nil,
|
|
verboseLevel: String?? = nil) -> OpenClawChatGatewayRequest
|
|
{
|
|
var params = self.sessionParams(sessionKey: sessionKey, agentID: agentID)
|
|
if let thinkingLevel {
|
|
params["thinkingLevel"] = thinkingLevel.map(AnyCodable.init) ?? AnyCodable(NSNull())
|
|
}
|
|
if let verboseLevel {
|
|
params["verboseLevel"] = verboseLevel.map(AnyCodable.init) ?? AnyCodable(NSNull())
|
|
}
|
|
return OpenClawChatGatewayRequest(
|
|
method: "sessions.patch",
|
|
params: params,
|
|
timeoutMs: self.mutationTimeoutMs)
|
|
}
|
|
|
|
public static func patchSession(
|
|
sessionKey: String,
|
|
agentID: String?,
|
|
label: String??,
|
|
category: String??,
|
|
pinned: Bool?,
|
|
archived: Bool?,
|
|
unread: Bool?) -> OpenClawChatGatewayRequest
|
|
{
|
|
var params = self.sessionParams(sessionKey: sessionKey, agentID: agentID)
|
|
if let label {
|
|
params["label"] = label.map(AnyCodable.init) ?? AnyCodable(NSNull())
|
|
}
|
|
if let category {
|
|
params["category"] = category.map(AnyCodable.init) ?? AnyCodable(NSNull())
|
|
}
|
|
if let pinned {
|
|
params["pinned"] = AnyCodable(pinned)
|
|
}
|
|
if let archived {
|
|
params["archived"] = AnyCodable(archived)
|
|
}
|
|
if let unread {
|
|
params["unread"] = AnyCodable(unread)
|
|
}
|
|
return OpenClawChatGatewayRequest(
|
|
method: "sessions.patch",
|
|
params: params,
|
|
timeoutMs: self.mutationTimeoutMs)
|
|
}
|
|
|
|
public static func deleteSession(
|
|
sessionKey: String,
|
|
agentID: String?) -> OpenClawChatGatewayRequest
|
|
{
|
|
var params = self.sessionParams(sessionKey: sessionKey, agentID: agentID)
|
|
params["deleteTranscript"] = AnyCodable(true)
|
|
return OpenClawChatGatewayRequest(
|
|
method: "sessions.delete",
|
|
params: params,
|
|
timeoutMs: self.mutationTimeoutMs)
|
|
}
|
|
|
|
public static func forkSession(
|
|
parentSessionKey: String,
|
|
agentID: String?) -> OpenClawChatGatewayRequest
|
|
{
|
|
var params: [String: AnyCodable] = [
|
|
"parentSessionKey": AnyCodable(parentSessionKey),
|
|
"fork": AnyCodable(true),
|
|
]
|
|
self.add(agentID, to: ¶ms, key: "agentId")
|
|
return OpenClawChatGatewayRequest(
|
|
method: "sessions.create",
|
|
params: params,
|
|
timeoutMs: self.mutationTimeoutMs)
|
|
}
|
|
|
|
public static func subscribeSessionMessages(
|
|
sessionKey: String,
|
|
agentID: String?) -> OpenClawChatGatewayRequest
|
|
{
|
|
OpenClawChatGatewayRequest(
|
|
method: "sessions.messages.subscribe",
|
|
params: self.sessionParams(sessionKey: sessionKey, agentID: agentID),
|
|
timeoutMs: self.shortTimeoutMs)
|
|
}
|
|
|
|
public static func resetSession(
|
|
sessionKey: String,
|
|
agentID: String?) -> OpenClawChatGatewayRequest
|
|
{
|
|
OpenClawChatGatewayRequest(
|
|
method: "sessions.reset",
|
|
params: self.sessionParams(sessionKey: sessionKey, agentID: agentID),
|
|
timeoutMs: self.shortTimeoutMs)
|
|
}
|
|
|
|
public static func compactSession(
|
|
sessionKey: String,
|
|
agentID: String?,
|
|
maxLines: Int? = nil) -> OpenClawChatGatewayRequest
|
|
{
|
|
var params = self.sessionParams(sessionKey: sessionKey, agentID: agentID)
|
|
if let maxLines {
|
|
params["maxLines"] = AnyCodable(maxLines)
|
|
}
|
|
return OpenClawChatGatewayRequest(
|
|
method: "sessions.compact",
|
|
params: params,
|
|
timeoutMs: self.compactionTimeoutMs)
|
|
}
|
|
|
|
public static func history(
|
|
sessionKey: String,
|
|
agentID: String?,
|
|
limit: Int? = nil,
|
|
maxChars: Int? = nil,
|
|
timeoutMs: Int? = nil) -> OpenClawChatGatewayRequest
|
|
{
|
|
var params: [String: AnyCodable] = ["sessionKey": AnyCodable(sessionKey)]
|
|
self.add(agentID, to: ¶ms, key: "agentId")
|
|
if let limit {
|
|
params["limit"] = AnyCodable(limit)
|
|
}
|
|
if let maxChars {
|
|
params["maxChars"] = AnyCodable(maxChars)
|
|
}
|
|
return OpenClawChatGatewayRequest(
|
|
method: "chat.history",
|
|
params: params,
|
|
timeoutMs: timeoutMs.map(Double.init) ?? self.defaultTimeoutMs)
|
|
}
|
|
|
|
public static func commandsList(
|
|
sessionKey: String?,
|
|
fallbackAgentID: String?) -> OpenClawChatGatewayRequest
|
|
{
|
|
var params: [String: AnyCodable] = [
|
|
"scope": AnyCodable("text"),
|
|
"includeArgs": AnyCodable(true),
|
|
]
|
|
self.add(
|
|
sessionKey.flatMap(OpenClawChatSessionKey.agentID) ?? fallbackAgentID,
|
|
to: ¶ms,
|
|
key: "agentId")
|
|
return OpenClawChatGatewayRequest(
|
|
method: "commands.list",
|
|
params: params,
|
|
timeoutMs: self.defaultTimeoutMs)
|
|
}
|
|
|
|
public static func sendMessage(
|
|
sessionKey: String,
|
|
agentID: String?,
|
|
expectedSessionRoutingContract: String?,
|
|
message: String,
|
|
thinking: String?,
|
|
idempotencyKey: String,
|
|
attachments: [OpenClawChatAttachmentPayload],
|
|
runTimeoutMs: Int? = nil,
|
|
requestTimeoutMs: Int = 30000) -> OpenClawChatGatewayRequest
|
|
{
|
|
var params: [String: AnyCodable] = [
|
|
"sessionKey": AnyCodable(sessionKey),
|
|
"message": AnyCodable(message),
|
|
"idempotencyKey": AnyCodable(idempotencyKey),
|
|
]
|
|
self.add(agentID, to: ¶ms, key: "agentId")
|
|
self.add(
|
|
expectedSessionRoutingContract,
|
|
to: ¶ms,
|
|
key: "expectedSessionRoutingContract")
|
|
self.add(thinking, to: ¶ms, key: "thinking")
|
|
if let runTimeoutMs {
|
|
params["timeoutMs"] = AnyCodable(runTimeoutMs)
|
|
}
|
|
if !attachments.isEmpty {
|
|
let encoded = attachments.map { attachment in
|
|
[
|
|
"type": attachment.type,
|
|
"mimeType": attachment.mimeType,
|
|
"fileName": attachment.fileName,
|
|
"content": attachment.content,
|
|
]
|
|
}
|
|
params["attachments"] = AnyCodable(encoded)
|
|
}
|
|
return OpenClawChatGatewayRequest(
|
|
method: "chat.send",
|
|
params: params,
|
|
timeoutMs: Double(requestTimeoutMs))
|
|
}
|
|
|
|
public static func agentWait(
|
|
runID: String,
|
|
timeoutMs: Int,
|
|
requestGraceMs: Int = 5000) -> OpenClawChatGatewayRequest
|
|
{
|
|
OpenClawChatGatewayRequest(
|
|
method: "agent.wait",
|
|
params: [
|
|
"runId": AnyCodable(runID),
|
|
"timeoutMs": AnyCodable(timeoutMs),
|
|
],
|
|
timeoutMs: Double(timeoutMs + requestGraceMs))
|
|
}
|
|
|
|
public static func health(timeoutMs: Int) -> OpenClawChatGatewayRequest {
|
|
OpenClawChatGatewayRequest(
|
|
method: "health",
|
|
timeoutMs: Double(max(1, timeoutMs)))
|
|
}
|
|
|
|
private static func sessionParams(
|
|
sessionKey: String,
|
|
agentID: String?) -> [String: AnyCodable]
|
|
{
|
|
var params = ["key": AnyCodable(sessionKey)]
|
|
self.add(agentID, to: ¶ms, key: "agentId")
|
|
return params
|
|
}
|
|
|
|
private static func add(
|
|
_ value: String?,
|
|
to params: inout [String: AnyCodable],
|
|
key: String,
|
|
trim: Bool = true)
|
|
{
|
|
let value = trim ? self.normalized(value) : value
|
|
if let value {
|
|
params[key] = AnyCodable(value)
|
|
}
|
|
}
|
|
|
|
private static func normalized(_ value: String?) -> String? {
|
|
let normalized = value?.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
return normalized?.isEmpty == false ? normalized : nil
|
|
}
|
|
}
|
|
|
|
extension GatewayNodeSession {
|
|
public func request(
|
|
_ request: OpenClawChatGatewayRequest,
|
|
ifCurrentRoute expectedRoute: GatewayNodeSessionRoute? = nil,
|
|
distinguishPreDispatchRouteChange: Bool = false) async throws -> Data
|
|
{
|
|
try await self.request(
|
|
method: request.method,
|
|
params: request.params,
|
|
timeoutMs: request.timeoutMs,
|
|
ifCurrentRoute: expectedRoute,
|
|
distinguishPreDispatchRouteChange: distinguishPreDispatchRouteChange)
|
|
}
|
|
}
|