// // H5ErrorRelay.swift // ylgamehall // // H5 日志/错误中继:把 WebView 内 `console.error` / `console.warn` / // `window.onerror` / `unhandledrejection` 通过 `webkit.messageHandlers.h5error` // 上报到原生 print;Debug 构建下额外转发 `console.log` / `console.info`。 // // Phase 9.2(Design §11.2)。msext 时代仅靠用户/运营手工反馈;本项目作为 // 原则 B「内部自由现代化」的一部分,开发期把 H5 异常打到 Xcode console, // 减少调试盲点。生产期无开关控制(输出 print 默认走 OSLog,对体积影响极小)。 // // **不修改 H5 一行**(契约原则 A):用 WKUserScript `.atDocumentStart` 在 // 业务 JS 跑前就 hook,不要求 H5 团队配合。 // // 仅在 BridgedWebView(大厅 + 子游戏)注入;OverlayViewController 是 // 第三方外链,错误不属于 ylgamehall 内部信号,故不挂。 // import Foundation import WebKit @MainActor public final class H5ErrorRelay: NSObject { public static let shared = H5ErrorRelay() /// WKScriptMessageHandler 名字。与下面 JS source 内 `webkit.messageHandlers.h5error` 必须一致。 public static let messageName = "h5error" public override init() { super.init() } /// 把 hook JS + message handler 注册到给定的 UserContentController。 /// BridgedWebView.init 在挂 WVJB 之后调一次。 /// /// - Parameter label: 该 WebView 的标识(大厅 / 子游戏),会作为输出前缀。 /// 同时存在多个 BridgedWebView 时,没有前缀就分不清哪条日志来自谁, /// 也就无法反推该在 Safari「开发」菜单里选哪个条目。 public func install(into controller: WKUserContentController, label: String) { let userScript = WKUserScript( source: Self.javaScriptSource, injectionTime: .atDocumentStart, // 业务 JS 之前 hook forMainFrameOnly: true // iframe 不抓 ) controller.addUserScript(userScript) controller.add(MessageProxy(target: self, label: label), name: Self.messageName) } fileprivate func handle(_ body: Any, label: String) { guard let dict = body as? [String: Any], let kind = dict["kind"] as? String else { return } let tag = "[H5:\(label)" switch kind { case "console.log", "console.info": // 仅 Debug 构建会有这两类(见 javaScriptSource 的 #if DEBUG)。 // ⚠️ 走裸 print 而不是 diagLog:H5 把 `Game_Config.Debugger.isDebugger` // 打开后每个收发包都会 console.log 一次,而 diagLog 会同步落盘到 // `Documents/diag.log`(无轮转、无大小上限,见 BridgeBus.swift:47), // firehose 灌进去会把设备磁盘吃光。Xcode console 看得到就够了。 let args = (dict["args"] as? [String]) ?? [] print("\(tag) \(kind)] " + args.joined(separator: " ")) case "console.error": let args = (dict["args"] as? [String]) ?? [] diagLog("\(tag) console.error] " + args.joined(separator: " ")) case "console.warn": // 桥诊断需要:WebViewJavascriptBridge.js 在「Native 发来的 handlerName // H5 侧没注册」时走 console.warn,之前不上报导致这类丢包完全不可见。 let args = (dict["args"] as? [String]) ?? [] diagLog("\(tag) console.warn] " + args.joined(separator: " ")) case "onerror": let msg = (dict["msg"] as? String) ?? "" let src = (dict["src"] as? String) ?? "" let line = (dict["line"] as? Int) ?? 0 let col = (dict["col"] as? Int) ?? 0 let stack = dict["stack"] as? String diagLog("\(tag) onerror] \(msg) at \(src):\(line):\(col)" + (stack.map { "\n\($0)" } ?? "")) case "resourceerror": // 资源 404 / 加载失败。H5 层完全静默(不会走 console.error), // 缺图 / 缺 js 在线上就是白屏或功能失灵,这里是唯一可观察点。 let tagName = (dict["tag"] as? String) ?? "?" let src = (dict["src"] as? String) ?? "" diagLog("\(tag) resourceerror] <\(tagName)> 加载失败:\(src)") case "unhandledrejection": let reason = (dict["reason"] as? String) ?? "" let stack = dict["stack"] as? String diagLog("\(tag) unhandledrejection] " + reason + (stack.map { "\n\($0)" } ?? "")) default: diagLog("\(tag) unknown] \(dict)") } } /// JS 端 hook 各类日志/异常源,统一通过 `webkit.messageHandlers.h5error.postMessage(payload)` /// 上报。所有 send 调用都包 try/catch,hook 自身永远不应抛错(避免污染业务流程)。 /// /// `console.log` / `console.info` 只在 Debug 构建注入:Release 包不该为每条业务 /// 日志付一次 JS→Native IPC,也不该把 H5 内部输出暴露给外部审阅。 private static var javaScriptSource: String { #if DEBUG let verboseHooks = verboseConsoleHookJS #else let verboseHooks = "" #endif return """ (function(){ function send(payload){ try { window.webkit.messageHandlers.h5error.postMessage(payload); } catch(e){} } // 参数格式化:直接 String(obj) 会得到无用的 "[object Object]",而大厅业务 // 大量使用 console.log(msg) / console.log(res) 打整包,故对象走 JSON.stringify。 // 循环引用时 stringify 抛错,回退 String();再抛就放弃,绝不让 hook 影响业务。 function fmt(v){ try { if (v === null) { return 'null'; } if (v === undefined) { return 'undefined'; } var t = typeof v; if (t === 'string') { return v; } if (t === 'number' || t === 'boolean' || t === 'function') { return String(v); } if (v instanceof Error) { return v.stack || (v.name + ': ' + v.message); } var s = JSON.stringify(v); return (s === undefined) ? String(v) : s; } catch(e) { try { return String(v); } catch(e2) { return '[unstringifiable]'; } } } function collect(a){ var out = []; for (var i=0; i//