From bdc63b5b7dcf54f428677549633e77a1ce08c387 Mon Sep 17 00:00:00 2001 From: Shuai-DaiDai <134567396+Shuai-DaiDai@users.noreply.github.com> Date: Sat, 14 Feb 2026 09:19:36 +0800 Subject: [PATCH] fix(macos): resolve dashboard basePath for local and remote (#15862) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 帅小呆1号 --- .../OpenClaw/GatewayEndpointStore.swift | 36 +++++++++++++++- .../Sources/OpenClaw/MenuContentView.swift | 2 +- .../GatewayEndpointStoreTests.swift | 42 +++++++++++++++++++ 3 files changed, 77 insertions(+), 3 deletions(-) diff --git a/apps/macos/Sources/OpenClaw/GatewayEndpointStore.swift b/apps/macos/Sources/OpenClaw/GatewayEndpointStore.swift index 20961e379..0edb2e651 100644 --- a/apps/macos/Sources/OpenClaw/GatewayEndpointStore.swift +++ b/apps/macos/Sources/OpenClaw/GatewayEndpointStore.swift @@ -619,7 +619,29 @@ actor GatewayEndpointStore { } extension GatewayEndpointStore { - static func dashboardURL(for config: GatewayConnection.Config) throws -> URL { + private static func normalizeDashboardPath(_ rawPath: String?) -> String { + let trimmed = (rawPath ?? "").trimmingCharacters(in: .whitespacesAndNewlines) + guard !trimmed.isEmpty else { return "/" } + let withLeadingSlash = trimmed.hasPrefix("/") ? trimmed : "/" + trimmed + guard withLeadingSlash != "/" else { return "/" } + return withLeadingSlash.hasSuffix("/") ? withLeadingSlash : withLeadingSlash + "/" + } + + private static func localControlUiBasePath() -> String { + let root = OpenClawConfigFile.loadDict() + guard let gateway = root["gateway"] as? [String: Any], + let controlUi = gateway["controlUi"] as? [String: Any] + else { + return "/" + } + return self.normalizeDashboardPath(controlUi["basePath"] as? String) + } + + static func dashboardURL( + for config: GatewayConnection.Config, + mode: AppState.ConnectionMode, + localBasePath: String? = nil) throws -> URL + { guard var components = URLComponents(url: config.url, resolvingAgainstBaseURL: false) else { throw NSError(domain: "Dashboard", code: 1, userInfo: [ NSLocalizedDescriptionKey: "Invalid gateway URL", @@ -633,7 +655,17 @@ extension GatewayEndpointStore { default: components.scheme = "http" } - components.path = "/" + + let urlPath = self.normalizeDashboardPath(components.path) + if urlPath != "/" { + components.path = urlPath + } else if mode == .local { + let fallbackPath = localBasePath ?? self.localControlUiBasePath() + components.path = self.normalizeDashboardPath(fallbackPath) + } else { + components.path = "/" + } + var queryItems: [URLQueryItem] = [] if let token = config.token?.trimmingCharacters(in: .whitespacesAndNewlines), !token.isEmpty diff --git a/apps/macos/Sources/OpenClaw/MenuContentView.swift b/apps/macos/Sources/OpenClaw/MenuContentView.swift index 6dec4d936..fd1b437cf 100644 --- a/apps/macos/Sources/OpenClaw/MenuContentView.swift +++ b/apps/macos/Sources/OpenClaw/MenuContentView.swift @@ -337,7 +337,7 @@ struct MenuContent: View { private func openDashboard() async { do { let config = try await GatewayEndpointStore.shared.requireConfig() - let url = try GatewayEndpointStore.dashboardURL(for: config) + let url = try GatewayEndpointStore.dashboardURL(for: config, mode: self.state.connectionMode) NSWorkspace.shared.open(url) } catch { let alert = NSAlert() diff --git a/apps/macos/Tests/OpenClawIPCTests/GatewayEndpointStoreTests.swift b/apps/macos/Tests/OpenClawIPCTests/GatewayEndpointStoreTests.swift index 8ab50b653..44c464c44 100644 --- a/apps/macos/Tests/OpenClawIPCTests/GatewayEndpointStoreTests.swift +++ b/apps/macos/Tests/OpenClawIPCTests/GatewayEndpointStoreTests.swift @@ -176,6 +176,48 @@ import Testing #expect(host == "192.168.1.10") } + @Test func dashboardURLUsesLocalBasePathInLocalMode() throws { + let config: GatewayConnection.Config = ( + url: try #require(URL(string: "ws://127.0.0.1:18789")), + token: nil, + password: nil + ) + + let url = try GatewayEndpointStore.dashboardURL( + for: config, + mode: .local, + localBasePath: " control ") + #expect(url.absoluteString == "http://127.0.0.1:18789/control/") + } + + @Test func dashboardURLSkipsLocalBasePathInRemoteMode() throws { + let config: GatewayConnection.Config = ( + url: try #require(URL(string: "ws://gateway.example:18789")), + token: nil, + password: nil + ) + + let url = try GatewayEndpointStore.dashboardURL( + for: config, + mode: .remote, + localBasePath: "/local-ui") + #expect(url.absoluteString == "http://gateway.example:18789/") + } + + @Test func dashboardURLPrefersPathFromConfigURL() throws { + let config: GatewayConnection.Config = ( + url: try #require(URL(string: "wss://gateway.example:443/remote-ui")), + token: nil, + password: nil + ) + + let url = try GatewayEndpointStore.dashboardURL( + for: config, + mode: .remote, + localBasePath: "/local-ui") + #expect(url.absoluteString == "https://gateway.example:443/remote-ui/") + } + @Test func normalizeGatewayUrlAddsDefaultPortForWs() { let url = GatewayRemoteConfig.normalizeGatewayUrl("ws://gateway") #expect(url?.port == 18789)