import Foundation public struct DeviceService: Sendable { private let transport: any NodeTransport public init(transport: any NodeTransport) { self.transport = transport } /// Direct-mode construction, kept for the existing call sites and tests. public init( client: any SimAdminClientProtocol, sessions: InstanceSessionStore, credentials: (any CredentialStore)? = nil ) { self.init(transport: DirectTransport(client: client, sessions: sessions, 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) } public func summary(_ instance: Instance) async throws -> DeviceSummary { let routes: [(id: String, route: UpstreamRoute)] = [ ("device", .device), ("sim", .sim), ("network", .network), ("stats", .stats), ("cpu", .statsCpu), ("connectivity", .connectivity) ] var sections: [(String, [String: String])] = [] var successful = 0 for item in routes { do { let response = try await request(item.route, instance) let values = try Self.flatten(Self.dataObject(response.data)) sections.append((item.id, values)) successful += 1 } catch { continue } } guard successful > 0 else { throw DeviceServiceError.invalidResponse } return Self.summary(from: sections) } private func request(_ route: UpstreamRoute, _ instance: Instance) async throws -> UpstreamResponse { try await transport.send(route, instanceID: instance.id, endpoint: instance.endpoint) } 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 summary(from sections: [(String, [String: String])]) -> DeviceSummary { func key(_ sectionID: String, _ names: [String]) -> String? { guard let section = sections.first(where: { $0.0 == sectionID }) else { return nil } return names.lazy.compactMap { section.1[$0] }.first { !$0.isEmpty } } func number(_ text: String?) -> Double? { guard let text = text?.replacingOccurrences(of: "%", with: "") else { return nil } return Double(text) } func bool(_ text: String?) -> Bool? { switch text?.lowercased() { case "是", "true", "yes", "1": return true case "否", "false", "no", "0": return false default: return nil } } let linkState = DeviceLinkState( hardwareOnline: bool(key("device", ["powered"])), controlOnline: bool(key("device", ["online"])), simPresent: sections.contains { $0.0 == "sim" }, cellularOnline: key("network", ["registration_status"])? .lowercased() .hasPrefix("registered") ) return DeviceSummary( deviceName: key("device", ["model", "name", "device"]), phoneNumber: key("sim", ["phone_numbers", "phone_number"]), operatorName: key("network", ["operator_name", "operator", "carrier"]), networkTechnology: key( "network", ["network_type", "technology_preference", "technology", "radio_mode"] ), signalPercent: number(key("network", ["signal_percent", "signal_strength"])) .flatMap { Int($0.rounded()) }, cpuPercent: number(key("cpu", ["cpu_percent", "cpu_load", "usage"])), memoryPercent: number(key("stats", ["memory_percent", "usage"])), cpuCount: number(key("cpu", ["core_count", "cores"])).flatMap { Int($0.rounded()) }, temperatureCelsius: number(key("stats", ["temperature", "max_temperature_c"])), batteryPercent: number(key("stats", ["battery_percent", "battery_level"])), ipAddress: key("network", ["ipv4", "ip_address", "ipv6"]), latencyMilliseconds: number(key("connectivity", ["latency_ms", "latency"])).flatMap { Int($0.rounded()) }, connectivity: key("connectivity", ["state", "connected", "latency_ms"]), firmwareVersion: key("device", ["revision", "firmware_version"]), totalStorageBytes: Self.byteCount( key("stats", ["disk_total_bytes", "storage_total_bytes", "total_bytes"]) ), availableStorageBytes: Self.byteCount( key("stats", ["disk_available_bytes", "storage_available_bytes", "available_bytes"]) ), downloadRateBytesPerSecond: Self.byteRate( key("connectivity", ["download", "download_bytes_per_second", "rx_rate"]) ), uploadRateBytesPerSecond: Self.byteRate( key("connectivity", ["upload", "upload_bytes_per_second", "tx_rate"]) ), messagesToday: number( key("connectivity", ["messages_today", "messages", "sms_today"]) ).flatMap { Int($0.rounded()) }, uptimeSeconds: number( key("stats", ["uptime_seconds", "uptime", "system_uptime_seconds"]) ) ?? number(key("stats", ["uptime_uptime_seconds"])) ?? number(key("stats", ["system_uptime_seconds"])), linkState: linkState ) } private static func byteCount(_ text: String?) -> Int64? { guard let text, let value = Double(text.replacingOccurrences(of: "%", with: "")), value >= 0 else { return nil } return Int64(value.rounded()) } private static func byteRate(_ text: String?) -> Double? { guard let text, let value = Double(text.replacingOccurrences(of: "%", with: "")), value >= 0 else { return nil } return value } 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 } } } }