V1.4.8: 按现场日志复盘优化取址等待与远程日志(含 15 秒白等修复)

依据用户提供的 1.4.7 现场日志(21 行,5 次点歌全部成功)复盘出四点:

1. 【行为,最大】每次点歌白等 15 秒。
   日志里每首歌前都有一条「JS-BRIDGE 15s 未就绪」,紧跟着「NATIVE-SONG 取址成功 用时=3xx~7xxms」——
   即取址本身只要 0.3~0.7 秒,但用户从按下点歌到开始下载要等 15.7 秒。
   原因: 桥一旦不可用,SongApiClient 每次都仍等满 15 秒。
   修复: KtvJsBridge 新增「已判定不可用」状态(markUnavailable/isUnavailable),
   首次判定后后续点歌直接走原生通道;桥万一恢复(热更新重载成功)自动清除该状态。

2. 【日志去噪】持久状态只记一次。
   原来「15s 未就绪」按每次点歌一条 WARN 输出,一天几百条且无新增信息。
   现在只在状态首次变化时记一条 ERROR。

3. 【可定位】补上"为什么不可用"。
   旧日志里没有任何 KtvApi 未导出/WebView 初始化失败的行,说明 init 回调根本没触发。
   KtvJsBridge.init 增加 20 秒看门狗: onPageFinished 未回调时明确上报
   「初始化超时:onPageFinished 未回调或 KtvApi 未导出」;
   init 异常与 KtvApi 未导出也统一收敛到 markUnavailable。

4. 【上下文完整】启动关键行不再有 undefined。
   旧日志的 STORAGE 行 serverip/clientip/user/traceid 全是 undefined(记在设备上下文之前),
   而且 AppPaths.storageFacts() 会真实建目录、写探针、读 /proc/mounts —— 却跑在主线程。
   现在把 STORAGE 移入 reportDeviceBootInfo() 的后台线程、在 refreshLogDeviceContext()
   与 MiliLog.awaitServerIp() 之后上报: 上下文完整且不再阻塞主线程。

5. 【新增状态样本】每 6 小时一条 HEALTH(版本/在线/可用空间/曲库是否存在及大小/根目录)。
   日志此前只有事件没有状态样本,无法排查"随时间变化"的问题(空间变少、曲库丢失、长期离线)。

6. 【字段一致】「请求源=」不再为空: 走原生兜底时 JS 的 reportSongResult 不会触发,
   现在 SongUrlResolver 记录 lastHost,SongApiClient.lastSongSourceHost() 依次回退。

AGENTS.md 增补「写日志的五条约定」(只记一次/上下文完整/不做重活/可自我定位/状态样本+事件)。

版本 1.4.7(27) 到 1.4.8(28);签名证书不变。ktv_api.js 未改动,KTV_BRIDGE_API 仍为 8。
This commit is contained in:
zijunchiang
2026-09-26 20:38:52 +08:00
parent a7a28fc45d
commit 906fa210e4
6 changed files with 111 additions and 11 deletions
+43 -2
View File
@@ -45,6 +45,8 @@ class KtvJsBridge(context: Context) {
"https://gitea.kasugano.cn/zijunchiang/maidong-ktv/raw/branch/master/app/src/main/assets/mobile/ktv_api.js"
private const val MAX_RESPONSE_BYTES = 2 * 1024 * 1024
private const val JS_REQUEST_TIMEOUT_MS = 180_000
/** WebView 初始化看门狗:超过这个时间还没就绪就判定本机 WebView 不可用。 */
private const val BRIDGE_INIT_TIMEOUT_MS = 20_000L
// 歌曲源(w.w345.my 与 e.ac19.cn 同后台)按 device 识别; 随机/变化的 device 会被风控。
// mac 取本机「当前在用网卡」的硬件地址(读不到时回退历史抓包值, 见 DeviceMac);
// sn 沿用抓包注册值, 保证 mac 变化后仍能对上原有账号体系。
@@ -75,6 +77,17 @@ class KtvJsBridge(context: Context) {
@Volatile private var webView: WebView? = null
@Volatile private var ready = false
@Volatile private var destroyed = false
/**
* 已判定「本机 WebView 不可用」。
*
* 一旦置位,`SongApiClient` 就不再为取址白等 15 秒,直接走原生通道
* (极米 H1S 实测每次点歌都要等满 15 秒,日志里表现为每首歌前一条
* 「JS-BRIDGE 15s 未就绪」)。桥万一后来就绪(热更新重载等)会自动清除。
*/
@Volatile private var unavailable = false
private var unavailableReason: String = ""
private var loadedFromCache = false
private var assetRetryAttempted = false
@@ -96,9 +109,17 @@ class KtvJsBridge(context: Context) {
loadedFromCache = cachedScript != null
createWebView(cachedScript ?: readAssetScript(), callback)
updateCache(cached)
// 看门狗:onPageFinished 一直不回调(WebView 静默失效)时给出**明确原因**。
// 否则远程日志里只会看到反复的「15s 未就绪」,不知道卡在哪一步。
mainHandler.postDelayed({
if (!ready && !destroyed && !unavailable) {
markUnavailable("初始化超时:onPageFinished 未回调或 KtvApi 未导出")
callback(false)
}
}, BRIDGE_INIT_TIMEOUT_MS)
} catch (error: Throwable) {
Log.e(TAG, "WebView init failed", error)
MiliLog.error("JS-BRIDGE WebView 初始化失败: ${error.javaClass.simpleName}: ${error.message}")
markUnavailable("WebView 初始化失败: ${error.javaClass.simpleName}: ${error.message}")
callback(false)
}
}
@@ -138,6 +159,12 @@ class KtvJsBridge(context: Context) {
view.evaluateJavascript("typeof window.KtvApi === 'function'") { result ->
if (result == "true") {
ready = true
// 桥后来就绪(例如热更新重载成功)→ 清除"不可用"标记,恢复 JS 主路径。
if (unavailable) {
unavailable = false
MiliLog.info("JS-BRIDGE 已恢复可用(${unavailableReason.ifEmpty { "?" }} 已解除)")
unavailableReason = ""
}
Log.i(TAG, "JS runtime ready (${if (loadedFromCache) "cache" else "asset"})")
MiliLog.info("JS-BRIDGE ready (${if (loadedFromCache) "cache" else "asset"})")
callback(true)
@@ -150,7 +177,7 @@ class KtvJsBridge(context: Context) {
Log.e(TAG, "KtvApi was not exported by JS")
// 老电视盒子上 WebView 不可用/被裁剪时常走这里:桥永远不就绪,
// 取址只能回退 DB → 「无法生成下载地址」。必须能远程看到。
MiliLog.error("JS-BRIDGE KtvApi 未导出,WebView 不可用")
markUnavailable("KtvApi 未导出")
callback(false)
}
}
@@ -163,6 +190,20 @@ class KtvJsBridge(context: Context) {
/** 桥当前是否可用。热更新重载期间会短暂为 false,调用方应据此等待而非立即回退。 */
fun isReady(): Boolean = ready && !destroyed
/** 是否已判定本机 WebView 不可用(用于跳过必然失败的等待)。 */
fun isUnavailable(): Boolean = unavailable && !destroyed && !ready
/**
* 标记桥在本机不可用。**只在状态首次变化时上报一次**:
* 这是持久状态,若每次点歌都记一条会把远程日志刷满(实测每首歌一条)。
*/
fun markUnavailable(reason: String) {
if (unavailable) return
unavailable = true
unavailableReason = reason
MiliLog.error("JS-BRIDGE 判定本机 WebView 不可用($reason),后续点歌直接走原生取址通道")
}
/**
* 供**原生兜底取址解析器**([SongUrlResolver])复用的「干净请求」通道。
*