feat(ios): add device details and reliable ipa build
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user