Files
multi-simadmin/apps/ios/Sources/MultiSimAdmin/Features/Messages/MessagesViewModel.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

93 lines
3.5 KiB
Swift

import Foundation
@MainActor
final class MessagesViewModel: ObservableObject {
enum DirectionFilter: String, CaseIterable, Identifiable {
case all = "全部"
case incoming = "收件"
case outgoing = "发件"
var id: String { rawValue }
}
@Published var rows: [SMSMessage] = []
@Published var instances: [Instance] = []
@Published var selectedInstanceID: UUID?
@Published var searchText = ""
@Published var direction: DirectionFilter = .all
@Published var isLoading = false
@Published var errorMessage: String?
@Published var showingSendForm = false
private let repository: any InstanceRepository
private let service: MessageService
init(repository: any InstanceRepository, service: MessageService) {
self.repository = repository
self.service = service
}
var filteredRows: [SMSMessage] {
rows.filter { message in
selectedInstanceID == nil || message.instanceID == selectedInstanceID
}.filter { message in
switch direction {
case .all: return true
case .incoming: return message.direction == .incoming
case .outgoing: return message.direction == .outgoing
}
}.filter { message in
let query = searchText.trimmingCharacters(in: .whitespacesAndNewlines)
guard !query.isEmpty else { return true }
return message.phoneNumber.localizedCaseInsensitiveContains(query)
|| message.content.localizedCaseInsensitiveContains(query)
|| message.instanceName.localizedCaseInsensitiveContains(query)
}
}
var selectedInstance: Instance? {
instances.first { $0.id == selectedInstanceID }
}
func refresh() async {
isLoading = true
errorMessage = nil
do {
let loaded = try await repository.list(InstanceQuery())
instances = loaded
if selectedInstanceID == nil || !loaded.contains(where: { $0.id == selectedInstanceID }) {
selectedInstanceID = nil
}
let page = try await service.messages(loaded, limit: 100)
rows = page.messages
} catch MessageServiceError.unauthorized, UpstreamError.unauthorized {
errorMessage = "节点需要访问密码,请在编辑节点中填写后重试。"
} catch let error as UpstreamError {
// 中继模式下失败原因有四类以上,各自对应不同处理动作;
// 统一兜底文案会把"重试即可"和"需要重新配对"说成同一件事。
errorMessage = ErrorMessage.describe(error, nodeName: selectedInstance?.name ?? "节点")
} catch {
errorMessage = "所有节点都没有返回消息,可能节点离线或未开启短信接口。"
}
isLoading = false
}
func send(phoneNumber: String, content: String) async throws {
guard let instance = selectedInstance ?? instances.first else {
throw MessageServiceError.invalidInput
}
try await service.send(instance, phoneNumber: phoneNumber, content: content)
await refresh()
}
func delete(_ message: SMSMessage) async {
guard let instance = instances.first(where: { $0.id == message.instanceID }) else { return }
do {
try await service.delete([message.id], from: instance)
rows.removeAll { $0.id == message.id }
} catch {
errorMessage = "删除消息失败。"
}
}
}