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)、配对四类失败、路由器、 传输往返与各类拒绝路径
This commit is contained in:
@@ -5,6 +5,8 @@ struct FleetView: View {
|
||||
@State private var showsDeleteDialog = false
|
||||
@State private var editingInstance: Instance?
|
||||
@State private var isSummaryCollapsed = true
|
||||
@State private var showingPairing = false
|
||||
@StateObject private var pairingModel: RelayPairingViewModel
|
||||
init(model: FleetViewModel? = nil) {
|
||||
// StateObject 的 wrappedValue 是 autoclosure,在 init 里改它会写到另一个实例上,
|
||||
// 所以调试开关必须先落到这个具体的 model 对象。
|
||||
@@ -32,6 +34,11 @@ struct FleetView: View {
|
||||
}
|
||||
#endif
|
||||
_model = StateObject(wrappedValue: target)
|
||||
// 配对视图模型与列表共用同一个仓库,否则配对成功后列表看不到新节点。
|
||||
_pairingModel = StateObject(wrappedValue: RelayPairingViewModel(
|
||||
repository: DefaultContainer.makeInstanceRepository(),
|
||||
identities: DefaultContainer.deviceIdentities
|
||||
))
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
@@ -134,6 +141,11 @@ struct FleetView: View {
|
||||
}
|
||||
.disabled(model.visibleRows.isEmpty)
|
||||
|
||||
AIIconButton("配对中继节点", systemImage: "link.badge.plus") {
|
||||
showingPairing = true
|
||||
}
|
||||
.disabled(model.isLoading)
|
||||
|
||||
AIIconButton("添加实例", systemImage: "plus") {
|
||||
model.showingForm = true
|
||||
}
|
||||
@@ -150,6 +162,9 @@ struct FleetView: View {
|
||||
instance: editingInstance
|
||||
)
|
||||
}
|
||||
.sheet(isPresented: $showingPairing) {
|
||||
RelayPairingView(model: pairingModel, fleet: model)
|
||||
}
|
||||
.onChange(of: model.showingForm) { showing in
|
||||
if !showing {
|
||||
editingInstance = nil
|
||||
@@ -839,11 +854,20 @@ private extension FleetView {
|
||||
tint: statusColor(row.status)
|
||||
)
|
||||
}
|
||||
Text(row.instance.origin.absoluteString)
|
||||
.font(.caption)
|
||||
.foregroundStyle(AITheme.muted)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.middle)
|
||||
HStack(spacing: 6) {
|
||||
Text(row.instance.endpoint.displayAddress)
|
||||
.font(.caption)
|
||||
.foregroundStyle(AITheme.muted)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.middle)
|
||||
// 通道徽标:没有它,"这台为什么慢"无法回答。
|
||||
channelBadge(row.channel)
|
||||
if !row.testing, row.freshness != .fresh {
|
||||
Text(row.freshness.label)
|
||||
.font(.caption2)
|
||||
.foregroundStyle(AITheme.muted)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 四条量表把"要不要担心这台"变成一次扫视:信号、延迟、CPU、内存。
|
||||
@@ -1039,16 +1063,35 @@ private extension FleetView {
|
||||
}
|
||||
|
||||
/// 只解释"为什么不是正常状态"。在线节点的细节交给指标栅格,上游原词(online 之类)不外泄。
|
||||
/// 通道徽标是刻意低调的:它解释延迟来源,不是需要用户处理的状态。
|
||||
private func channelBadge(_ channel: InstanceChannel) -> some View {
|
||||
Text(channel.label)
|
||||
.font(.caption2.weight(.semibold))
|
||||
.foregroundStyle(AITheme.muted)
|
||||
.padding(.horizontal, 6)
|
||||
.padding(.vertical, 1)
|
||||
.background(AITheme.canvas, in: Capsule())
|
||||
.overlay(Capsule().stroke(AITheme.line, lineWidth: 1))
|
||||
.accessibilityLabel(channel == .relay ? "经由中继" : "内网直通")
|
||||
}
|
||||
|
||||
private func statusLine(_ row: FleetViewModel.Row) -> String? {
|
||||
if row.testing { return "正在读取运行信息" }
|
||||
switch row.status {
|
||||
case .online:
|
||||
return nil
|
||||
case .degraded:
|
||||
// 中继模式下"部分接口不可用"可能有具体得多的原因,优先给能照做的说法。
|
||||
if let error = row.probeError {
|
||||
return ErrorMessage.describe(error, nodeName: row.instance.name)
|
||||
}
|
||||
return "部分接口不可用,可在详情查看失败接口"
|
||||
case .authRequired:
|
||||
return "节点开启了访问密码,进入详情登录"
|
||||
case .offline:
|
||||
if let error = row.probeError {
|
||||
return ErrorMessage.describe(error, nodeName: row.instance.name)
|
||||
}
|
||||
return "节点不可达,最后结果已过期"
|
||||
case .unknown:
|
||||
return "尚未检测"
|
||||
|
||||
Reference in New Issue
Block a user