import Foundation public struct DeviceService: Sendable { private let client: any SimAdminClientProtocol private let sessions: InstanceSessionStore private let credentials: (any CredentialStore)? public init( client: any SimAdminClientProtocol, sessions: InstanceSessionStore, credentials: (any CredentialStore)? = nil ) { self.client = client self.sessions = sessions self.credentials = credentials } public func snapshot(_ instance: Instance) async throws -> DeviceSnapshot { let routes: [(id: String, title: String, route: UpstreamRoute)] = [ ("device", "设备", .device), ("sim", "SIM", .sim), ("network", "蜂窝网络", .network), ("stats", "系统资源", .stats), ("cpu", "CPU", .statsCpu), ("connectivity", "连接状态", .connectivity) ] var sections: [DeviceSection] = [] var successful = 0 var unauthorized = false for item in routes { do { let response = try await request(item.route, instance) let values = try Self.flatten(Self.dataObject(response.data)) sections.append(DeviceSection(id: item.id, title: item.title, values: values)) successful += 1 } catch UpstreamError.unauthorized, UpstreamError.forbidden { unauthorized = true sections.append(DeviceSection(id: item.id, title: item.title, values: [:])) } catch { sections.append(DeviceSection(id: item.id, title: item.title, values: [:])) } } if successful == 0 { throw unauthorized ? DeviceServiceError.unauthorized : DeviceServiceError.invalidResponse } return DeviceSnapshot(sections: sections) } private func request(_ route: UpstreamRoute, _ instance: Instance) async throws -> UpstreamResponse { var cookie = await sessions.cookie(for: instance.id, origin: instance.origin) do { return try await client.send(route, baseURL: instance.origin, cookie: cookie) } catch UpstreamError.unauthorized, UpstreamError.forbidden { cookie = try await login(instance) return try await client.send(route, baseURL: instance.origin, cookie: cookie) } } private func login(_ instance: Instance) async throws -> String { guard let credentials, let password = try credentials.get(instanceID: instance.id), !password.isEmpty else { throw DeviceServiceError.unauthorized } let sessionService = InstanceSessionService(store: sessions, client: client) let result = try await sessionService.login(instance, password: password) guard result.authenticated, let cookie = await sessionService.cookie(for: instance) else { throw DeviceServiceError.unauthorized } return cookie } static func dataObject(_ data: Data) throws -> [String: Any] { guard let root = try JSONSerialization.jsonObject(with: data) as? [String: Any] else { throw DeviceServiceError.invalidResponse } if let payload = root["data"] as? [String: Any] { return payload } if let response = root["response"] as? [String: Any], let payload = response["data"] as? [String: Any] { return payload } return root } static func flatten(_ source: [String: Any], maximum: Int = 40) -> [String: String] { var output: [String: String] = [:] flatten(source, prefix: "", into: &output, remaining: maximum) return output } private static func flatten( _ source: [String: Any], prefix: String, into output: inout [String: String], remaining: Int ) { for (key, value) in source.sorted(by: { $0.key < $1.key }) { guard output.count < remaining else { return } let fullKey = prefix.isEmpty ? key : "\(prefix)_\(key)" switch value { case let text as String: output[fullKey] = text case let flag as Bool: output[fullKey] = flag ? "是" : "否" case let number as Int: output[fullKey] = String(number) case let number as Double: output[fullKey] = String(format: "%.1f", number) case let array as [Any]: let text = array.compactMap { entry -> String? in if let record = entry as? [String: Any] { let preferred = ["name", "zone", "mount_point", "interface", "model_name"] .compactMap { record[$0] as? String } .first if let preferred { return preferred } } return entry as? String }.joined(separator: "、") output[fullKey] = text case let nested as [String: Any]: flatten(nested, prefix: fullKey, into: &output, remaining: remaining - output.count) default: break } } } }