diff --git a/client/macos/PacketTunnel/PacketTunnelProvider.swift b/client/macos/PacketTunnel/PacketTunnelProvider.swift index 9c2fe77..f1cd590 100644 --- a/client/macos/PacketTunnel/PacketTunnelProvider.swift +++ b/client/macos/PacketTunnel/PacketTunnelProvider.swift @@ -444,36 +444,30 @@ final class StatsCollector: NSObject { clashTimer = timer } + // 与 Windows(kernel_process.dart)完全同策略: + // · 显示源 = /proxies 里各 URLTest 成员的 history.last.delay(每拍读,缓存住,稳定)。 + // · /group//delay 只为「让内核重测并写进 history」,其结果也顺手缓存(双保险); + // 单次探测失败不影响显示——下一拍仍读到 history 里上次成功的值。 private func pollClash() { - // 先取组名(URLTest 类,跳过 GLOBAL 选择器),再对每组主动跑 /group//delay 取 - // 内核实测延迟。sing-box 的 /proxies 不带 history,故只能 active 取;成功就缓存,超时/空 - // 不覆盖旧值 → 显示稳定(congested 链路上探测时好时坏也不丢延迟)。 clashGet("/proxies", query: nil) { [weak self] obj, raw in guard let self else { return } + let hist = StatsCollector.extractUrltestResults(obj) let groups = StatsCollector.extractUrltestGroups(obj).filter { $0 != "GLOBAL" } self.queue.async { - if groups.isEmpty { self.setDiag("groups=[] raw=\(raw.prefix(100))") } + if !hist.isEmpty { + self.cacheUrltest(hist, src: "hist") + } else { + self.setDiag("hist= groups=\(groups) raw=\(raw.prefix(70))") + } for name in groups { self.clashGet("/group/\(name)/delay", query: ["url": "http://www.gstatic.com/generate_204", "timeout": "5000"]) { [weak self] dobj, draw in guard let self else { return } - var urltest: [(tag: String, delayMs: Int)] = [] - if let dict = dobj as? [String: Any] { - for (tag, v) in dict { - if let d = (v as? NSNumber)?.intValue, d > 0 { - urltest.append((tag: tag, delayMs: d)) - } - } - } + let active = StatsCollector.parseDelayMap(dobj) self.queue.async { - self.lock.lock() - if !urltest.isEmpty { - self.latestUrltest = urltest - self.latest["urltestResults"] = urltest.map { ["tag": $0.tag, "delayMs": $0.delayMs] } - } - self.latest["diag"] = "d(\(name)) n=\(urltest.count) raw=\(draw.prefix(80))" - self.lock.unlock() + if !active.isEmpty { self.cacheUrltest(active, src: "active") } + else { self.setDiag("active(\(name)) empty raw=\(draw.prefix(70))") } } } } @@ -481,6 +475,16 @@ final class StatsCollector: NSObject { } } + /// 缓存延迟(从 history 或 active 任一来源),非空才更新——失败/空不覆盖旧值。 + private func cacheUrltest(_ list: [(tag: String, delayMs: Int)], src: String) { + guard !list.isEmpty else { return } + lock.lock() + latestUrltest = list + latest["urltestResults"] = list.map { ["tag": $0.tag, "delayMs": $0.delayMs] } + latest["diag"] = "\(src)=\(list.map { "\($0.tag):\($0.delayMs)" }.joined(separator: ","))" + lock.unlock() + } + private func setDiag(_ s: String) { lock.lock(); latest["diag"] = s; lock.unlock() } @@ -517,6 +521,43 @@ final class StatsCollector: NSObject { } return names } + + /// 从 /proxies 提取各 URLTest/Fallback 组成员的最新 history 延迟(与 Windows + /// extractUrltestResults 字段完全一致:group.all → member.history.last.delay)。 + static func extractUrltestResults(_ obj: Any?) -> [(tag: String, delayMs: Int)] { + guard let root = obj as? [String: Any], + let proxies = root["proxies"] as? [String: Any] else { return [] } + var results: [(tag: String, delayMs: Int)] = [] + for (_, v) in proxies { + guard let group = v as? [String: Any], + let type = group["type"] as? String, type == "URLTest" || type == "Fallback", + let members = group["all"] as? [Any] else { continue } + for m in members { + guard let tag = m as? String, + let member = proxies[tag] as? [String: Any], + let history = member["history"] as? [Any], !history.isEmpty, + let last = history.last as? [String: Any], + let delay = (last["delay"] as? NSNumber)?.intValue, delay > 0 else { continue } + results.append((tag: tag, delayMs: delay)) + } + } + return results + } + + /// 解析 /group//delay 的 {tag: ms} 响应(含嵌套 {tag:{delay:ms}}),只留正延迟。 + static func parseDelayMap(_ obj: Any?) -> [(tag: String, delayMs: Int)] { + guard let dict = obj as? [String: Any] else { return [] } + var out: [(tag: String, delayMs: Int)] = [] + for (tag, v) in dict { + if let d = (v as? NSNumber)?.intValue, d > 0 { + out.append((tag: tag, delayMs: d)) + } else if let inner = v as? [String: Any], + let d = (inner["delay"] as? NSNumber)?.intValue, d > 0 { + out.append((tag: tag, delayMs: d)) + } + } + return out + } } extension StatsCollector: LibboxCommandClientHandlerProtocol { diff --git a/client/macos/Runner.xcodeproj/project.pbxproj b/client/macos/Runner.xcodeproj/project.pbxproj index fff4579..a9206ec 100644 --- a/client/macos/Runner.xcodeproj/project.pbxproj +++ b/client/macos/Runner.xcodeproj/project.pbxproj @@ -659,7 +659,7 @@ baseConfigurationReference = C57BBFD43F9175D1E23685EF /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; - CURRENT_PROJECT_VERSION = 50; + CURRENT_PROJECT_VERSION = 51; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.pangolin.pangolin.RunnerTests; @@ -674,7 +674,7 @@ baseConfigurationReference = F8904897A48DC81799B8752E /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; - CURRENT_PROJECT_VERSION = 50; + CURRENT_PROJECT_VERSION = 51; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.pangolin.pangolin.RunnerTests; @@ -689,7 +689,7 @@ baseConfigurationReference = B21E68FC1F5D33DD67A0DF5E /* Pods-RunnerTests.profile.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; - CURRENT_PROJECT_VERSION = 50; + CURRENT_PROJECT_VERSION = 51; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.pangolin.pangolin.RunnerTests; @@ -960,7 +960,7 @@ CODE_SIGN_ENTITLEMENTS = PacketTunnel/PacketTunnel.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 50; + CURRENT_PROJECT_VERSION = 51; DEVELOPMENT_TEAM = BYL4KQHMTN; ENABLE_APP_SANDBOX = YES; ENABLE_HARDENED_RUNTIME = YES; @@ -1010,7 +1010,7 @@ CODE_SIGN_ENTITLEMENTS = PacketTunnel/PacketTunnel.entitlements; CODE_SIGN_IDENTITY = "Developer ID Application"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 50; + CURRENT_PROJECT_VERSION = 51; DEVELOPMENT_TEAM = BYL4KQHMTN; ENABLE_APP_SANDBOX = YES; ENABLE_HARDENED_RUNTIME = YES; @@ -1059,7 +1059,7 @@ CODE_SIGN_ENTITLEMENTS = PacketTunnel/PacketTunnel.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 50; + CURRENT_PROJECT_VERSION = 51; DEVELOPMENT_TEAM = BYL4KQHMTN; ENABLE_APP_SANDBOX = YES; ENABLE_HARDENED_RUNTIME = YES;