























@@ -0,0 +1,120 @@
1+import AppKit
2+import Foundation
3+import OpenClawKit
4+import OSLog
5+6+private let dashboardManagerLogger = Logger(subsystem: "ai.openclaw", category: "DashboardManager")
7+8+@MainActor
9+final class DashboardManager {
10+static let shared = DashboardManager()
11+12+private var controller: DashboardWindowController?
13+14+private init() {}
15+16+@discardableResult
17+func showConfiguredWindowIfPossible() -> Bool {
18+let mode = AppStateStore.shared.connectionMode
19+guard let config = self.immediateDashboardConfig(mode: mode),
20+let url = try? GatewayEndpointStore.dashboardURL(
21+ for: config,
22+ mode: mode,
23+ authToken: config.token)
24+else {
25+return false
26+}
27+let auth = DashboardWindowAuth(
28+ gatewayUrl: Self.websocketURLString(for: url),
29+ token: config.token,
30+ password: config.password?.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty)
31+guard auth.hasCredential else {
32+return false
33+}
34+if let controller {
35+ controller.show(url: url, auth: auth)
36+} else {
37+let controller = DashboardWindowController(url: url, auth: auth)
38+self.controller = controller
39+ controller.show(url: url, auth: auth)
40+}
41+Task { _ = try? await ControlChannel.shared.health(timeout: 3) }
42+return true
43+}
44+45+func show() async throws {
46+let mode = AppStateStore.shared.connectionMode
47+ dashboardManagerLogger.info("dashboard show requested mode=\(String(describing: mode), privacy: .public)")
48+let config = try await self.dashboardConfig(mode: mode)
49+ dashboardManagerLogger.info("dashboard config url=\(config.url.absoluteString, privacy: .public)")
50+let token = await GatewayConnection.shared.controlUiAutoAuthToken(config: config)
51+let url = try GatewayEndpointStore.dashboardURL(for: config, mode: mode, authToken: token)
52+let auth = DashboardWindowAuth(
53+ gatewayUrl: Self.websocketURLString(for: url),
54+ token: token,
55+ password: config.password?.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty)
56+57+if let controller {
58+ dashboardManagerLogger.info("dashboard reuse window url=\(url.absoluteString, privacy: .public)")
59+ controller.show(url: url, auth: auth)
60+return
61+}
62+63+ dashboardManagerLogger.info("dashboard create window url=\(url.absoluteString, privacy: .public)")
64+let controller = DashboardWindowController(url: url, auth: auth)
65+self.controller = controller
66+ controller.show(url: url, auth: auth)
67+68+ // Refresh the cached hello payload without blocking window creation.
69+Task { _ = try? await ControlChannel.shared.health(timeout: 3) }
70+}
71+72+func close() {
73+self.controller?.closeDashboard()
74+}
75+76+private static func websocketURLString(for dashboardURL: URL) -> String {
77+guard var components = URLComponents(url: dashboardURL, resolvingAgainstBaseURL: false) else {
78+return dashboardURL.absoluteString
79+}
80+switch components.scheme?.lowercased() {
81+case "https":
82+ components.scheme = "wss"
83+default:
84+ components.scheme = "ws"
85+}
86+ components.queryItems = nil
87+ components.fragment = nil
88+return components.url?.absoluteString ?? dashboardURL.absoluteString
89+}
90+91+private func dashboardConfig(mode: AppState.ConnectionMode) async throws -> GatewayConnection.Config {
92+if let config = self.immediateDashboardConfig(mode: mode) {
93+return config
94+}
95+96+return try await Task.detached(priority: .userInitiated) {
97+await GatewayEndpointStore.shared.refresh()
98+return try await GatewayEndpointStore.shared.requireConfig()
99+}.value
100+}
101+102+private func immediateDashboardConfig(mode: AppState.ConnectionMode) -> GatewayConnection.Config? {
103+let root = OpenClawConfigFile.loadDict()
104+if mode == .remote,
105+GatewayRemoteConfig.resolveTransport(root: root) == .direct,
106+let url = GatewayRemoteConfig.resolveGatewayUrl(root: root)
107+{
108+return (
109+ url,
110+GatewayRemoteConfig.resolveTokenString(root: root),
111+GatewayRemoteConfig.resolvePasswordString(root: root))
112+}
113+114+if mode == .local {
115+return GatewayEndpointStore.localConfig()
116+}
117+118+return nil
119+}
120+}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。