Files
multi-simadmin/apps/ios/Sources/MultiSimAdmin/Services/DefaultContainer.swift
T
chick 3f6090081f feat(ios): 中继通道 P4/P5——端点抽象、信封、传输、配对与分层错误
- InstanceEndpoint 取代 Instance.origin(旧数据 init(from:) 读成 .direct,
  写盘只输出 endpoint;dedupKey 直通用 origin、中继用 relay:<nodeID>)
- RelayWire:Swift 侧重算 contracts/test-vectors.json(CanonicalJSON 手写解析器,
  因为 Foundation 的解码器会抹掉'拼写',而 intent 摘要定义的正是拼写)
- NodeTransport 抽象 + DirectTransport / RelayTransport / NodeTransportRouter;
  DirectTransport 是薄适配,直通路径行为不变
- RelayTransport:信封构造、签名、加密、验签、解密;响应验签失败即丢弃,
  四类以上失败各有独立 UpstreamError 分支与用户文案
- DeviceIdentity(沙盒文件 0600,配对时由节点分配 deviceID)、RelayPairing、配对 sheet
- 卡片通道徽标 + freshness 副标题;危险写 intent 在发送瞬间生成
- scripts/export-app-routes.swift 生成 vendored 路由表,AppRoutesContractTests 反向守卫
- 测试 78→92:向量互算、迁移(字面量旧 JSON)、配对四类失败、路由器、
  传输往返与各类拒绝路径
2026-09-16 18:57:41 +08:00

110 lines
4.1 KiB
Swift

import Foundation
enum DefaultContainer {
private static let credentials: any CredentialStore = FileCredentialStore(
location: documentFileURL("credentials.json")
)
private static let logStore: any LogStore = FileLogStore(
location: documentFileURL("log-center.json")
)
/// The phone's relay identity, shared by every relay transport.
///
/// It is loaded once rather than per transport: the key pair must be the same one the node
/// paired with, and a second load that regenerated it would silently invalidate every pairing.
static let deviceIdentities = FileDeviceIdentityStore(
location: documentFileURL("device-identity.json")
)
static func documentFileURL(_ name: String) -> URL {
let directory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
return directory.appendingPathComponent(name)
}
static func makeInstanceRepository() -> FileInstanceRepository {
FileInstanceRepository(location: documentFileURL("instances.json"))
}
/// The transport pair every feature service uses: direct on the LAN, relay through the agent.
///
/// The relay side reads the identity file per request rather than holding a copy: pairing can
/// happen after a transport is built, and a held copy would sign with keys the node has never
/// seen, which fails as an unreadable request with no useful cause.
private static func makeTransport(
client: any SimAdminClientProtocol,
sessions: InstanceSessionStore
) -> NodeTransportRouter {
let identities = deviceIdentities
return NodeTransportRouter(
direct: DirectTransport(client: client, sessions: sessions, credentials: credentials),
relay: RelayTransport(identity: { try identities.loadOrCreate() })
)
}
@MainActor
static func makeFleetModel() -> FleetViewModel {
purgeLegacyGrouping()
let sessions = InstanceSessionStore()
return FleetViewModel(
repository: makeInstanceRepository(),
probe: ConnectionProbe(transport: makeTransport(client: SimAdminClient(), sessions: sessions)),
credentials: credentials,
deviceService: DeviceService(
transport: makeTransport(client: SimAdminClient(), sessions: sessions)
)
)
}
/// 0.2.2 起分组被标签取代,遗留的分组文件与偏好键一次性清掉,避免 Documents 里留死数据。
private static func purgeLegacyGrouping() {
try? FileManager.default.removeItem(at: documentFileURL("instance-groups.json"))
UserDefaults.standard.removeObject(forKey: "fleet.groupMode")
}
@MainActor
static func makeMessagesModel() -> MessagesViewModel {
let sessions = InstanceSessionStore()
return MessagesViewModel(
repository: makeInstanceRepository(),
service: MessageService(
transport: makeTransport(client: SimAdminClient(), sessions: sessions)
)
)
}
@MainActor
static func makeLogsModel(autoRefreshOnAppear: Bool = true) -> LogsViewModel {
let store = logStore
let sessions = InstanceSessionStore()
return LogsViewModel(
repository: makeInstanceRepository(),
service: LogCenterService(
transport: makeTransport(client: SimAdminClient(), sessions: sessions),
store: store
),
store: store,
autoRefreshOnAppear: autoRefreshOnAppear
)
}
@MainActor
static func makeSettingsModel() -> SettingsViewModel {
SettingsViewModel(store: logStore)
}
@MainActor
static func makeDeviceModel(instance: Instance) -> DeviceViewModel {
let sessions = InstanceSessionStore()
return DeviceViewModel(
instance: instance,
service: DeviceService(
transport: makeTransport(client: SimAdminClient(), sessions: sessions)
),
probe: ConnectionProbe(
transport: makeTransport(client: SimAdminClient(), sessions: sessions)
)
)
}
}