| import Foundation |
| import Network |
| import OpenClawKit |
|
|
| enum A2UIReadyState { |
| case ready(String) |
| case hostNotConfigured |
| case hostUnavailable |
| } |
|
|
| extension NodeAppModel { |
| func resolveCanvasHostURL() async -> String? { |
| guard let raw = await self.gatewaySession.currentCanvasHostUrl() else { return nil } |
| let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines) |
| guard !trimmed.isEmpty, let base = URL(string: trimmed) else { return nil } |
| if let host = base.host, LoopbackHost.isLoopback(host) { |
| return nil |
| } |
| return base.appendingPathComponent("__openclaw__/canvas/").absoluteString |
| } |
|
|
| func _test_resolveA2UIHostURL() async -> String? { |
| await self.resolveA2UIHostURL() |
| } |
|
|
| func resolveA2UIHostURL() async -> String? { |
| guard let raw = await self.gatewaySession.currentCanvasHostUrl() else { return nil } |
| let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines) |
| guard !trimmed.isEmpty, let base = URL(string: trimmed) else { return nil } |
| if let host = base.host, LoopbackHost.isLoopback(host) { |
| return nil |
| } |
| return base.appendingPathComponent("__openclaw__/a2ui/").absoluteString + "?platform=ios" |
| } |
|
|
| func showA2UIOnConnectIfNeeded() async { |
| await MainActor.run { |
| |
| |
| self.lastAutoA2uiURL = nil |
| self.screen.showDefaultCanvas() |
| } |
| } |
|
|
| func ensureA2UIReadyWithCapabilityRefresh(timeoutMs: Int = 5000) async -> A2UIReadyState { |
| guard let initialUrl = await self.resolveA2UIHostURLWithCapabilityRefresh() else { |
| return .hostNotConfigured |
| } |
| self.screen.navigate(to: initialUrl) |
| if await self.screen.waitForA2UIReady(timeoutMs: timeoutMs) { |
| return .ready(initialUrl) |
| } |
|
|
| |
| guard await self.gatewaySession.refreshNodeCanvasCapability() else { return .hostUnavailable } |
| guard let refreshedUrl = await self.resolveA2UIHostURL() else { return .hostUnavailable } |
| self.screen.navigate(to: refreshedUrl) |
| if await self.screen.waitForA2UIReady(timeoutMs: timeoutMs) { |
| return .ready(refreshedUrl) |
| } |
| return .hostUnavailable |
| } |
|
|
| func showLocalCanvasOnDisconnect() { |
| self.lastAutoA2uiURL = nil |
| self.screen.showDefaultCanvas() |
| } |
|
|
| private func resolveA2UIHostURLWithCapabilityRefresh() async -> String? { |
| if let url = await self.resolveA2UIHostURL() { |
| return url |
| } |
| guard await self.gatewaySession.refreshNodeCanvasCapability() else { return nil } |
| return await self.resolveA2UIHostURL() |
| } |
|
|
| private func resolveCanvasHostURLWithCapabilityRefresh() async -> String? { |
| if let url = await self.resolveCanvasHostURL() { |
| return url |
| } |
| guard await self.gatewaySession.refreshNodeCanvasCapability() else { return nil } |
| return await self.resolveCanvasHostURL() |
| } |
|
|
| private static func probeTCP(url: URL, timeoutSeconds: Double) async -> Bool { |
| guard let host = url.host, !host.isEmpty else { return false } |
| let portInt = url.port ?? ((url.scheme ?? "").lowercased() == "wss" ? 443 : 80) |
| return await TCPProbe.probe( |
| host: host, |
| port: portInt, |
| timeoutSeconds: timeoutSeconds, |
| queueLabel: "a2ui.preflight") |
| } |
| } |
|
|