diag(client/macos): 把 /group/delay 原始响应塞进 diag,定位 urltest 为空

v47 诊断:clash 注入生效(组名 auto 已识别),但 /group/auto/delay 返回无数字延迟。
本次把该接口原始响应体(含 HTTP 状态码)透出到 diag,并兼容嵌套 {tag:{delay:ms}} 形态,
一眼区分:内核探测失败(raw={} / 错误消息)还是返回结构不符(解析问题)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-06-30 13:28:55 +08:00
parent 03dbbf8db9
commit 70e8f67702
2 changed files with 35 additions and 24 deletions
@@ -448,12 +448,13 @@ final class StatsCollector: NSObject {
private func pollClash() {
if groupNames.isEmpty {
clashGet("/proxies", query: nil) { [weak self] obj in
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
self.setDiag("groups=\(names)")
if names.isEmpty { self.setDiag("groups=[] raw=\(raw.prefix(120))") }
else { self.setDiag("groups=\(names)") }
self.triggerGroupDelays()
}
}
@@ -465,19 +466,27 @@ final class StatsCollector: NSObject {
private func triggerGroupDelays() {
for name in groupNames {
clashGet("/group/\(name)/delay",
query: ["url": "http://www.gstatic.com/generate_204", "timeout": "3000"]) { [weak self] obj in
guard let self, let dict = obj as? [String: Any] else { return }
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)] = []
for (tag, v) in dict {
if let d = (v as? NSNumber)?.intValue { urltest.append((tag: tag, delayMs: d)) }
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}}
}
}
}
let summary = urltest.map { "\($0.tag):\($0.delayMs)" }.joined(separator: ",")
NSLog("[pangolin/collector] kernel delay(%@)=%@", name, summary) //
NSLog("[pangolin/collector] kernel delay(%@) n=%d raw=%@", name, urltest.count, raw)
self.queue.async {
self.lock.lock()
self.latestUrltest = urltest
self.latest["urltestResults"] = urltest.map { ["tag": $0.tag, "delayMs": $0.delayMs] }
self.latest["diag"] = "delay(\(name))=\(summary)"
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.lock.unlock()
}
}
@@ -489,19 +498,21 @@ final class StatsCollector: NSObject {
}
private func clashGet(_ path: String, query: [String: String]?,
completion: @escaping (Any?) -> Void) {
guard var comps = URLComponents(string: clashBase + path) else { completion(nil); return }
completion: @escaping (Any?, String) -> Void) {
guard var comps = URLComponents(string: clashBase + path) else { completion(nil, "badurl"); return }
if let query { comps.queryItems = query.map { URLQueryItem(name: $0.key, value: $0.value) } }
guard let url = comps.url else { completion(nil); return }
guard let url = comps.url else { completion(nil, "badurl"); return }
var req = URLRequest(url: url)
req.timeoutInterval = 8
if !clashSecret.isEmpty {
req.setValue("Bearer \(clashSecret)", forHTTPHeaderField: "Authorization")
}
URLSession.shared.dataTask(with: req) { data, _, err in
if let err { NSLog("[pangolin/collector] clash %@ err=%@", path, err.localizedDescription) }
guard let data else { completion(nil); return }
completion(try? JSONSerialization.jsonObject(with: data))
URLSession.shared.dataTask(with: req) { data, resp, err in
if let err { completion(nil, "err:\(err.localizedDescription)"); return }
let code = (resp as? HTTPURLResponse)?.statusCode ?? -1
guard let data else { completion(nil, "http\(code):nodata"); return }
let raw = String(data: data, encoding: .utf8) ?? "<\(data.count)B>"
completion(try? JSONSerialization.jsonObject(with: data), "http\(code):\(raw)")
}.resume()
}
@@ -659,7 +659,7 @@
baseConfigurationReference = C57BBFD43F9175D1E23685EF /* Pods-RunnerTests.debug.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CURRENT_PROJECT_VERSION = 47;
CURRENT_PROJECT_VERSION = 48;
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 = 47;
CURRENT_PROJECT_VERSION = 48;
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 = 47;
CURRENT_PROJECT_VERSION = 48;
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 = 47;
CURRENT_PROJECT_VERSION = 48;
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 = 47;
CURRENT_PROJECT_VERSION = 48;
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 = 47;
CURRENT_PROJECT_VERSION = 48;
DEVELOPMENT_TEAM = BYL4KQHMTN;
ENABLE_APP_SANDBOX = YES;
ENABLE_HARDENED_RUNTIME = YES;