fix(client/macos): urltest 主源改读 /proxies history(稳定),修连接页延迟

v48 验证延迟已打通(urltest=reality-out=576 到达主 app),但 active /group/delay 在拥塞
链路上常超时返回 {}。改为与 Windows extractUrltestResults 完全一致:主源读 /proxies 里各
URLTest 组成员的 history 缓存延迟(稳定,active 探测超时也不丢),并对 URLTest 组(跳过
GLOBAL 选择器)主动跑 /group/<name>/delay 刷新 history。延迟显示由此稳定。

扩展代码变更 → CURRENT_PROJECT_VERSION 48→49。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-06-30 14:13:58 +08:00
parent 70e8f67702
commit a228a1d221
2 changed files with 43 additions and 47 deletions
@@ -380,7 +380,6 @@ final class StatsCollector: NSObject {
// clash API(,127.0.0.1 ): urltest
private var clashBase = ""
private var clashSecret = ""
private var groupNames: [String] = []
private let lock = NSLock()
private var latest: [String: Any] = StatsCollector.zero
@@ -424,7 +423,6 @@ final class StatsCollector: NSObject {
if let c = self.statusClient { try? c.disconnect() }
self.statusClient = nil
self.started = false
self.groupNames = []
self.lock.lock()
self.latest = StatsCollector.zero
self.latestUrltest = []
@@ -447,48 +445,25 @@ final class StatsCollector: NSObject {
}
private func pollClash() {
if groupNames.isEmpty {
clashGet("/proxies", query: nil) { [weak self] obj, raw in
guard let self else { return }
let names = StatsCollector.extractUrltestGroups(obj)
self.queue.async {
self.groupNames = names
if names.isEmpty { self.setDiag("groups=[] raw=\(raw.prefix(120))") }
else { self.setDiag("groups=\(names)") }
self.triggerGroupDelays()
}
}
} else {
triggerGroupDelays()
}
}
private func triggerGroupDelays() {
for name in groupNames {
clashGet("/group/\(name)/delay",
query: ["url": "http://www.gstatic.com/generate_204", "timeout": "3000"]) { [weak self] obj, raw in
guard let self else { return }
var urltest: [(tag: String, delayMs: Int)] = []
if let dict = obj as? [String: Any] {
for (tag, v) in dict {
if let d = (v as? NSNumber)?.intValue {
urltest.append((tag: tag, delayMs: d)) // {tag:ms}
} else if let inner = v as? [String: Any],
let d = (inner["delay"] as? NSNumber)?.intValue {
urltest.append((tag: tag, delayMs: d)) // {tag:{delay:ms}}
}
}
}
NSLog("[pangolin/collector] kernel delay(%@) n=%d raw=%@", name, urltest.count, raw)
self.queue.async {
// : /proxies URLTest history (,active )
// URLTest /group/<name>/delay history( GLOBAL )
clashGet("/proxies", query: nil) { [weak self] obj, _ in
guard let self else { return }
let results = StatsCollector.extractUrltestResults(obj)
let groups = StatsCollector.extractUrltestGroups(obj).filter { $0 != "GLOBAL" }
self.queue.async {
if !results.isEmpty {
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=\(raw.prefix(120))"
self.latestUrltest = results
self.latest["urltestResults"] = results.map { ["tag": $0.tag, "delayMs": $0.delayMs] }
self.lock.unlock()
}
self.setDiag("hist=\(results.map { "\($0.tag):\($0.delayMs)" }.joined(separator: ","))")
for name in groups {
self.clashGet("/group/\(name)/delay",
query: ["url": "http://www.gstatic.com/generate_204",
"timeout": "5000"]) { _, _ in }
}
}
}
}
@@ -529,6 +504,27 @@ final class StatsCollector: NSObject {
}
return names
}
/// /proxies URLTest history ( Windows extractUrltestResults )
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",
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],
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
}
}
extension StatsCollector: LibboxCommandClientHandlerProtocol {
@@ -659,7 +659,7 @@
baseConfigurationReference = C57BBFD43F9175D1E23685EF /* Pods-RunnerTests.debug.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CURRENT_PROJECT_VERSION = 48;
CURRENT_PROJECT_VERSION = 49;
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 = 48;
CURRENT_PROJECT_VERSION = 49;
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 = 48;
CURRENT_PROJECT_VERSION = 49;
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 = 48;
CURRENT_PROJECT_VERSION = 49;
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 = 48;
CURRENT_PROJECT_VERSION = 49;
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 = 48;
CURRENT_PROJECT_VERSION = 49;
DEVELOPMENT_TEAM = BYL4KQHMTN;
ENABLE_APP_SANDBOX = YES;
ENABLE_HARDENED_RUNTIME = YES;