fix(client/macos): urltest 完全照搬 Windows 策略——读 /proxies history + active 刷新
Windows(kernel_process.dart)的策略:显示源是 /proxies 各 URLTest 成员的 history.last.delay
(每拍读、缓存住、稳定);/group/<name>/delay 只为让内核重测并写进 history,其结果不直接用。
我之前直接用 active 结果,拥塞链路上 active 恒返回 {} 就全空了。
本次照搬:主显示读 /proxies history(extractUrltestResults 字段与 Windows 一致),active
/group/delay 既刷新 history 又顺手缓存(双保险);非空才更新,单次失败不覆盖旧值。
CURRENT_PROJECT_VERSION 50→51。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -444,36 +444,30 @@ final class StatsCollector: NSObject {
|
||||
clashTimer = timer
|
||||
}
|
||||
|
||||
// 与 Windows(kernel_process.dart)完全同策略:
|
||||
// · 显示源 = /proxies 里各 URLTest 成员的 history.last.delay(每拍读,缓存住,稳定)。
|
||||
// · /group/<name>/delay 只为「让内核重测并写进 history」,其结果也顺手缓存(双保险);
|
||||
// 单次探测失败不影响显示——下一拍仍读到 history 里上次成功的值。
|
||||
private func pollClash() {
|
||||
// 先取组名(URLTest 类,跳过 GLOBAL 选择器),再对每组主动跑 /group/<name>/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/<name>/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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user