import Foundation /// 一个 Tailscale 连接(= 一个常驻 tsnet 节点)。可添加多个;host 可绑定其一作为出口。 /// 每连接独立 stateDir(`tsnet-`,创建即固化、终身不变——tsnet 无文件锁,同 Dir 双 Up 会损坏状态)。 struct TailscaleConnection: Identifiable, Codable, Equatable { let id: UUID var name: String /// tailnet 内展示的主机名。 var hostname: String /// tsnet 状态目录名(Application Support 下),创建即固化。 let stateDirName: String /// true = 从未注册 或 被 tailnet 清理 → 需重填 authKey 才能 Up。 var needsAuthKey: Bool /// 展示用缓存:上次 Up 后的本节点 tailnet IP。 var lastSelfIP: String? init(id: UUID = UUID(), name: String, hostname: String = "terminalx-ipad", needsAuthKey: Bool = true, lastSelfIP: String? = nil) { self.id = id self.name = name self.hostname = hostname self.stateDirName = "tsnet-\(id.uuidString)" self.needsAuthKey = needsAuthKey self.lastSelfIP = lastSelfIP } } /// Tailscale 连接列表持久化(tailnets.json)。本体无机密(authKey 默认注册后即丢弃)。 @MainActor final class TailscaleStore: ObservableObject { @Published private(set) var connections: [TailscaleConnection] = [] private let url: URL init() { let base = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0] try? FileManager.default.createDirectory(at: base, withIntermediateDirectories: true) url = base.appendingPathComponent("tailnets.json") load() } func add(_ conn: TailscaleConnection) { connections.append(conn); save() } func update(_ conn: TailscaleConnection) { guard let i = connections.firstIndex(where: { $0.id == conn.id }) else { return } connections[i] = conn save() } func remove(_ conn: TailscaleConnection) { connections.removeAll { $0.id == conn.id } save() // 删连接 → 删其 tsnet 状态目录,避免残留身份。 let base = NSSearchPathForDirectoriesInDomains(.applicationSupportDirectory, .userDomainMask, true)[0] try? FileManager.default.removeItem(atPath: base + "/" + conn.stateDirName) } func connection(id: UUID?) -> TailscaleConnection? { guard let id else { return nil } return connections.first { $0.id == id } } #if DEBUG /// 无头 UI 验证:注入示意 tailnet(**不写盘**,不污染真实 tailnets.json)。 func injectFixtureConnections(_ list: [TailscaleConnection]) { connections = list } #endif private func load() { guard let data = try? Data(contentsOf: url), let decoded = try? JSONDecoder().decode([TailscaleConnection].self, from: data) else { return } connections = decoded } private func save() { guard let data = try? JSONEncoder().encode(connections) else { return } try? data.write(to: url, options: .atomic) } }