From 211fda37e31ceab12c371bd5a453eb1da1ecc8ef Mon Sep 17 00:00:00 2001 From: wangjia <809946525@qq.com> Date: Tue, 30 Jun 2026 14:45:14 +0800 Subject: [PATCH] =?UTF-8?q?fix(client/macos):=20urltest=20=E5=AE=8C?= =?UTF-8?q?=E5=85=A8=E7=85=A7=E6=90=AC=20Windows=20=E7=AD=96=E7=95=A5?= =?UTF-8?q?=E2=80=94=E2=80=94=E8=AF=BB=20/proxies=20history=20+=20active?= =?UTF-8?q?=20=E5=88=B7=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Windows(kernel_process.dart)的策略:显示源是 /proxies 各 URLTest 成员的 history.last.delay (每拍读、缓存住、稳定);/group//delay 只为让内核重测并写进 history,其结果不直接用。 我之前直接用 active 结果,拥塞链路上 active 恒返回 {} 就全空了。 本次照搬:主显示读 /proxies history(extractUrltestResults 字段与 Windows 一致),active /group/delay 既刷新 history 又顺手缓存(双保险);非空才更新,单次失败不覆盖旧值。 CURRENT_PROJECT_VERSION 50→51。 Co-Authored-By: Claude Opus 4.8 --- .../PacketTunnel/PacketTunnelProvider.swift | 79 ++++++++++++++----- client/macos/Runner.xcodeproj/project.pbxproj | 12 +-- 2 files changed, 66 insertions(+), 25 deletions(-) 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;