Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 40 additions & 22 deletions samples/CameraAccess/CameraAccess/OpenClaw/OpenClawBridge.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,31 +32,48 @@ class OpenClawBridge: ObservableObject {
self.sessionKey = OpenClawBridge.stableSessionKey
}

// MARK: - Endpoint resolution

/// Returns the first reachable gateway base URL, trying remote (Tailscale) before local.
/// - Returns: A reachable base URL string, or nil if all endpoints are unreachable.
private func resolveGatewayBaseURL() async -> String? {
var candidates: [String] = []

// 1. Remote URL (Tailscale / public) if configured
let remote = SettingsManager.shared.openClawRemoteURL
if !remote.isEmpty { candidates.append(remote) }

// 2. Local network URL
let local = "\(GeminiConfig.openClawHost):\(GeminiConfig.openClawPort)"
candidates.append(local)

for baseURL in candidates {
guard let url = URL(string: "\(baseURL)/health") else { continue }
var req = URLRequest(url: url)
req.httpMethod = "GET"
req.setValue("Bearer \(GeminiConfig.openClawGatewayToken)", forHTTPHeaderField: "Authorization")
if let (_, response) = try? await pingSession.data(for: req),
let http = response as? HTTPURLResponse,
(200...499).contains(http.statusCode) {
NSLog("[OpenClaw] Resolved gateway: %@", baseURL)
return baseURL
}
}
return nil
}

func checkConnection() async {
guard GeminiConfig.isOpenClawConfigured else {
connectionState = .notConfigured
return
}
connectionState = .checking
guard let url = URL(string: "\(GeminiConfig.openClawHost):\(GeminiConfig.openClawPort)/v1/chat/completions") else {
connectionState = .unreachable("Invalid URL")
return
}
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue("Bearer \(GeminiConfig.openClawGatewayToken)", forHTTPHeaderField: "Authorization")
request.setValue("glass", forHTTPHeaderField: "x-openclaw-message-channel")
do {
let (_, response) = try await pingSession.data(for: request)
if let http = response as? HTTPURLResponse, (200...499).contains(http.statusCode) {
connectionState = .connected
NSLog("[OpenClaw] Gateway reachable (HTTP %d)", http.statusCode)
} else {
connectionState = .unreachable("Unexpected response")
}
} catch {
connectionState = .unreachable(error.localizedDescription)
NSLog("[OpenClaw] Gateway unreachable: %@", error.localizedDescription)
if let base = await resolveGatewayBaseURL() {
connectionState = .connected
NSLog("[OpenClaw] Gateway reachable at %@", base)
} else {
connectionState = .unreachable("No reachable gateway (tried local and remote)")
NSLog("[OpenClaw] All gateway endpoints unreachable")
}
}

Expand All @@ -73,9 +90,10 @@ class OpenClawBridge: ObservableObject {
) async -> ToolResult {
lastToolCallStatus = .executing(toolName)

guard let url = URL(string: "\(GeminiConfig.openClawHost):\(GeminiConfig.openClawPort)/v1/chat/completions") else {
lastToolCallStatus = .failed(toolName, "Invalid URL")
return .failure("Invalid gateway URL")
guard let baseURL = await resolveGatewayBaseURL(),
let url = URL(string: "\(baseURL)/v1/chat/completions") else {
lastToolCallStatus = .failed(toolName, "No reachable gateway")
return .failure("No reachable gateway (local and remote both unavailable)")
}

// Append the new user message to conversation history
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ final class SettingsManager {
case geminiAPIKey
case openClawHost
case openClawPort
case openClawRemoteURL // e.g. Tailscale URL, used when off local network
case openClawHookToken
case openClawGatewayToken
case geminiSystemPrompt
Expand Down Expand Up @@ -39,6 +40,13 @@ final class SettingsManager {
set { defaults.set(newValue, forKey: Key.openClawHost.rawValue) }
}

/// Optional remote URL (e.g. Tailscale) used when the local gateway is unreachable.
/// If set, OpenClawBridge will try this before falling back to openClawHost.
var openClawRemoteURL: String {
get { defaults.string(forKey: Key.openClawRemoteURL.rawValue) ?? "" }
set { defaults.set(newValue, forKey: Key.openClawRemoteURL.rawValue) }
}

var openClawPort: Int {
get {
let stored = defaults.integer(forKey: Key.openClawPort.rawValue)
Expand Down
17 changes: 17 additions & 0 deletions samples/CameraAccess/CameraAccess/Settings/SettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ struct SettingsView: View {
@State private var geminiAPIKey: String = ""
@State private var openClawHost: String = ""
@State private var openClawPort: String = ""
@State private var openClawRemoteURL: String = ""
@State private var openClawHookToken: String = ""
@State private var openClawGatewayToken: String = ""
@State private var geminiSystemPrompt: String = ""
Expand Down Expand Up @@ -49,6 +50,20 @@ struct SettingsView: View {
.font(.system(.body, design: .monospaced))
}

VStack(alignment: .leading, spacing: 4) {
Text("Remote URL (Tailscale / Public)")
.font(.caption)
.foregroundColor(.secondary)
Text("Used when off local network. Leave empty to use local only.")
.font(.caption2)
.foregroundColor(.secondary)
TextField("http://100.x.x.x:18789", text: $openClawRemoteURL)
.autocapitalization(.none)
.disableAutocorrection(true)
.keyboardType(.URL)
.font(.system(.body, design: .monospaced))
}

VStack(alignment: .leading, spacing: 4) {
Text("Port")
.font(.caption)
Expand Down Expand Up @@ -147,6 +162,7 @@ struct SettingsView: View {
geminiSystemPrompt = settings.geminiSystemPrompt
openClawHost = settings.openClawHost
openClawPort = String(settings.openClawPort)
openClawRemoteURL = settings.openClawRemoteURL
openClawHookToken = settings.openClawHookToken
openClawGatewayToken = settings.openClawGatewayToken
webrtcSignalingURL = settings.webrtcSignalingURL
Expand All @@ -162,6 +178,7 @@ struct SettingsView: View {
if let port = Int(openClawPort.trimmingCharacters(in: .whitespacesAndNewlines)) {
settings.openClawPort = port
}
settings.openClawRemoteURL = openClawRemoteURL.trimmingCharacters(in: .whitespacesAndNewlines)
settings.openClawHookToken = openClawHookToken.trimmingCharacters(in: .whitespacesAndNewlines)
settings.openClawGatewayToken = openClawGatewayToken.trimmingCharacters(in: .whitespacesAndNewlines)
settings.webrtcSignalingURL = webrtcSignalingURL.trimmingCharacters(in: .whitespacesAndNewlines)
Expand Down