import Foundation import SwiftUI import TsnetBridge struct TsnetPeer: Codable, Identifiable { let name: String let ip: String let online: Bool let os: String var id: String { name + ip } } /// M1 自检:app 进程内用 tsnet 加入 tailnet,显示分配 IP + 枚举 tailnet 对端(发现可连主机)。 @MainActor final class TsnetProbe: ObservableObject { @Published var status = "准备中…" @Published var selfIP = "" @Published var peers: [TsnetPeer] = [] @Published var done = false func run(authKey: String) { let base = NSSearchPathForDirectoriesInDomains(.applicationSupportDirectory, .userDomainMask, true)[0] let dir = base + "/tsnet-probe" try? FileManager.default.createDirectory(atPath: dir, withIntermediateDirectories: true) status = "加入 tailnet 中…(up 阻塞等待 Running)" Task.detached { guard let node = TsnetbridgeNewNode(dir, "terminalx-ipad") else { await MainActor.run { self.status = "NewNode 失败"; self.done = true } return } do { try node.up(withAuthKey: authKey, timeoutMs: 45000) let ip = node.selfIP() try? await Task.sleep(nanoseconds: 5_000_000_000) // 等 netmap 同步 let peersJSON = node.peersJSON() let decoded = (try? JSONDecoder().decode([TsnetPeer].self, from: Data(peersJSON.utf8))) ?? [] await MainActor.run { self.selfIP = ip self.peers = decoded.sorted { $0.online && !$1.online } self.status = ip.isEmpty ? "已 Up 但未取到 IP" : "已加入 tailnet ✓" self.done = true } } catch { await MainActor.run { self.status = "失败:\(error.localizedDescription)" self.done = true } } } } } struct TsnetProbeView: View { let authKey: String @StateObject private var probe = TsnetProbe() var body: some View { VStack(spacing: 12) { Text("terminalX · tsnet 自检 (M1)").font(.headline) ProgressView().opacity(probe.done ? 0 : 1) Text(probe.status).foregroundStyle(probe.selfIP.isEmpty ? Color.secondary : Color.green) if !probe.selfIP.isEmpty { Text("本机 Tailscale IP:\(probe.selfIP)") .font(.system(.body, design: .monospaced)).bold().foregroundStyle(.green) } if probe.done { Text("tailnet 对端:\(probe.peers.count)").font(.subheadline).padding(.top, 8) } if !probe.peers.isEmpty { ScrollView { VStack(alignment: .leading, spacing: 4) { ForEach(probe.peers) { p in HStack(spacing: 8) { Circle().fill(p.online ? .green : .gray).frame(width: 8, height: 8) Text(p.ip).font(.system(.caption, design: .monospaced)) Text(p.name).font(.caption).foregroundStyle(.secondary).lineLimit(1) Text(p.os).font(.caption2).foregroundStyle(.tertiary) } } }.frame(maxWidth: .infinity, alignment: .leading) }.frame(maxHeight: 500) } } .padding(28) .task { probe.run(authKey: authKey) } } }