Files
openclaw/apps/macos/Sources/OpenClaw/GatewayConnection+ManagedImages.swift
Peter Steinberger ac71743ca4 fix(apple): load chat media behind reverse-proxy paths (#130755)
Share ticketed media URL resolution between iOS and macOS, retaining the connected gateway encoded context path and transport-owned TLS and credential policy. Remove the duplicate native URL and playback resolvers.

Fixes #130746. Related to Android #129957.
2026-08-27 00:21:17 -07:00

109 lines
4.5 KiB
Swift

import Foundation
import OpenClawChatUI
import OpenClawKit
import OpenClawProtocol
extension GatewayConnection {
func loadMediaArtifact(
sessionKey: String,
agentID: String?,
artifactId: String,
kind: OpenClawChatMediaKind,
playback: OpenClawChatPlaybackMode?,
ifCurrentServerLease lease: ServerLease) async throws -> OpenClawChatLoadedMedia?
{
guard kind.acceptsManagedArtifactID(artifactId) else { return nil }
let request = OpenClawChatGatewayRequests.artifactDownload(
sessionKey: sessionKey,
agentID: agentID,
artifactId: artifactId)
let responseData = try await self.request(
method: request.method,
params: request.params,
timeoutMs: request.timeoutMs,
ifCurrentServerLease: lease)
let response = try JSONDecoder().decode(ArtifactsDownloadResult.self, from: responseData)
let maximumBytes = Self.maximumManagedMediaBytes(for: kind)
let declaredMIME = response.artifact.mimetype?.lowercased()
if playback != .transcode,
let encoded = response.data?.trimmingCharacters(in: .whitespacesAndNewlines),
!encoded.isEmpty
{
guard response.encoding == "base64",
let declaredMIME,
declaredMIME.hasPrefix(kind.mimeTypePrefix),
let data = Data(base64Encoded: encoded),
data.count <= maximumBytes
else { return nil }
guard await self.isCurrentServerLease(lease) else {
throw OpenClawChatTransportSendError.notDispatched
}
return .data(OpenClawChatMediaData(data: data, mimeType: declaredMIME))
}
guard let ticketedPath = response.url?.trimmingCharacters(in: .whitespacesAndNewlines),
let url = OpenClawChatMediaURL.resolve(
gatewayURL: lease.route.url,
ticketedPath: ticketedPath,
playback: playback)
else { return nil }
let canStreamDirectly = kind == .video &&
url.scheme?.lowercased() == "https" &&
lease.route.tls == nil &&
declaredMIME?.hasPrefix(kind.mimeTypePrefix) == true
if canStreamDirectly, playback != .transcode, let declaredMIME {
guard await self.isCurrentServerLease(lease) else {
throw OpenClawChatTransportSendError.notDispatched
}
return .stream(OpenClawChatMediaStream(
url: url,
mimeType: declaredMIME,
sizeBytes: response.artifact.sizebytes))
}
var urlRequest = URLRequest(url: url)
urlRequest.timeoutInterval = kind == .video ? 60 : 20
urlRequest.setValue("\(kind.rawValue)/*", forHTTPHeaderField: "Accept")
if canStreamDirectly {
urlRequest.setValue("bytes=0-0", forHTTPHeaderField: "Range")
}
// Native macOS has no per-Gateway proxy-header configuration surface today. If one is
// added, carry its immutable snapshot on Route so the socket and ticket GET cannot diverge.
let tls = lease.route.tls?.params ?? GatewayTLSParams(
required: false,
expectedFingerprint: nil,
allowTOFU: false,
storeKey: nil)
let session = GatewayTLSPinningSession(params: tls)
defer { session.finishTasksAndInvalidate() }
let (data, urlResponse) = try await session.data(
for: urlRequest,
maximumBytes: maximumBytes)
guard await self.isCurrentServerLease(lease) else {
throw OpenClawChatTransportSendError.notDispatched
}
guard let http = urlResponse as? HTTPURLResponse else { return nil }
if http.statusCode == 202 {
return .preparing
}
guard (200..<300).contains(http.statusCode),
let mimeType = http.mimeType?.lowercased(),
mimeType.hasPrefix(kind.mimeTypePrefix)
else { return nil }
if canStreamDirectly {
return .stream(OpenClawChatMediaStream(
url: url,
mimeType: mimeType,
sizeBytes: response.artifact.sizebytes))
}
return .data(OpenClawChatMediaData(data: data, mimeType: mimeType))
}
private static func maximumManagedMediaBytes(for kind: OpenClawChatMediaKind) -> Int {
switch kind {
case .image: 12 * 1024 * 1024
case .audio, .video: 16 * 1024 * 1024
}
}
}