From 97ca1ae28e82034d889fad9ad79a0905fcc44a12 Mon Sep 17 00:00:00 2001 From: zijunchiang Date: Wed, 7 Oct 2026 07:53:48 +0800 Subject: [PATCH] =?UTF-8?q?V1.5.5:=20=E5=8A=A0=E5=85=A5=E6=BA=90=20a.ktv8.?= =?UTF-8?q?my=EF=BC=9B"=E6=89=80=E6=9C=89=E6=BA=90=E5=A4=B1=E6=95=88"?= =?UTF-8?q?=E6=94=B9=E4=B8=BA=E6=98=8E=E7=A1=AE=E5=BC=B9=E7=AA=97=E6=8F=90?= =?UTF-8?q?=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 一、加入歌曲源 a.ktv8.my(置于 gz.ac16.vip 之后) 实测(代理修复后,DNS 直连真实 IP 20~42ms): - 真实 IP 154.219.102.181 —— 与 w345.my **同一台服务器**(同一后台的兄弟入口); - GET / → 200(917B,主机面板默认占位页); - i.php → 200,170B,与其它 5 个入口响应**逐字节一致**: code=200 / msg=ok / ordertype=0 / end_date=2026-10-05 14:58:21 / retry=5 / heart_time=5400,token 正常下发; - 自身媒体路径 /<编号>.ts 与 /ad_files/my_ad_video.ts 均 404(138B,与 e.ac19.cn 的 404 逐字节相同) → 它不托管广告媒体; - ⚠️ do.php 取址行为未能定性: 测试期间本机出口 IP(与门店设备同一个 188.253.112.231) 的配额被反复探测耗尽,表现为"成功一两次后长时间静默丢包"(与代码里记录的 trial_times:2 × trial_duration:5 吻合)。故置于次位,由运行时升降序自动调整。 KTV_BRIDGE_API 10 → 11(ktv_api.js 两处标记 + KtvJsBridge 两处校验)。 二、"所有源都取不到地址"给出明确提示 新增 Eac19WindowGuard.onAllFailed ← noteAllSourcesFailed(),由 SongUrlResolver.resolve() 在**本轮彻底失败**时调用(403 / 只返回广告 / 网络异常都算),弹窗文案: 「远程源已全部失效,等待新版本修复」 - 与既有的 onBlocked(风控 403 → "换公网 IP")常常同时触发,因此在 onBlocked 处理器里 用 lastAllFailedAt() 做 60 秒去重,不会同时弹两个框;两者均 15 分钟节流。 - 以前这里只有一句"无法生成下载地址"的 toast,用户看起来像界面没反应。 版本 1.5.4(36) → 1.5.5(37);签名证书不变。测试 9 例全通过。 --- AGENTS.md | 16 +++++++++- app/build.gradle | 4 +-- app/src/main/assets/mobile/ktv_api.js | 7 +++-- .../java/com/local/ktv/Eac19WindowGuard.kt | 30 +++++++++++++++++++ .../main/java/com/local/ktv/KtvJsBridge.kt | 4 +-- .../main/java/com/local/ktv/MainActivity.kt | 15 ++++++++++ .../java/com/local/ktv/SongUrlResolver.kt | 5 ++++ 7 files changed, 74 insertions(+), 7 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 23011c9..31bcda7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -165,7 +165,7 @@ $BT/apksigner verify 目标名.apk ### 版本号 -- `app/build.gradle`:`versionName "1.5.4"` / `versionCode 36`(18~21 被本机临时诊断包占用过) +- `app/build.gradle`:`versionName "1.5.5"` / `versionCode 37`(18~21 被本机临时诊断包占用过) - App 内显示统一用纯版本号(不加 `(versionCode)` 括号) - ⚠️ **发布构建已开启 NewApi 闸门**(`lint { checkReleaseBuilds true; fatal += ['NewApi'] }`): 任何「调用了高于 `minSdk 21` 且未做版本守卫的 API」都会让 `assembleRelease` 直接失败。 @@ -271,6 +271,13 @@ $BT/apksigner verify 目标名.apk **判定原则**:广告黑名单只作用于**返回地址(媒体 URL)的 host**, **不作用于"取址入口"的 host** —— 两者不能混为一谈,否则会误杀唯一可用的源。 - 该改动同时把 `KTV_BRIDGE_API` 由 9 提升到 **10**(`ktv_api.js` 两处标记 + `KtvJsBridge` 两处校验)。 + - **`a.ktv8.my`(2026-10 加入,置于 `gz.ac16.vip` 之后)**:实测与 `w345.my` **同一台服务器** + (`154.219.102.181`),`GET /` 是主机面板默认页,`i.php` 返回与其它入口**逐字节一致**的 + 170B 响应(`code=200/end_date=2026-10-05/retry=5`)→ 同一后台的兄弟入口; + 它自身媒体路径(`/<编号>.ts`、`/ad_files/…`)均 404 → **不托管广告媒体**。 + ⚠️ 其 `do.php` 取址行为**尚未定性**(测试时本机出口 IP 配额已被消耗殆尽), + 因此置于次位、由运行时自动升降序。 + - 该条改动把 `KTV_BRIDGE_API` 由 10 提升到 **11**(同样四处)。 - **⚠️ 取址有两条实现,改协议必须同时改**:JS 主路径(`ktv_api.js`)+ 原生兜底 (`SongUrlResolver.kt`,供 **WebView 不可用**的老盒子使用 —— 实测极米 H1S/GMUI2.0 上 WebView 导不出 `window.KtvApi`,桥永远 not ready,只靠 JS 时表现为 @@ -388,6 +395,13 @@ $BT/apksigner verify 目标名.apk `APP-INIT ... logFile=<路径>` 指明位置; 4. `MiliLog.init` 已提到 **`KtvApplication.onCreate`**(早于 `MainActivity`), 这样即使更早的初始化抛异常,也已经有日志可查。 + - ⚠️ **"所有源都取不到地址"必须给用户一句明确的话(2026-10 用户要求)**: + `Eac19WindowGuard.onAllFailed` ← `noteAllSourcesFailed()`, + 由 `SongUrlResolver.resolve()` 在**本轮彻底失败**时调用(403 / 只发广告 / 网络异常都算), + 弹窗文案固定为 **「远程源已全部失效,等待新版本修复」**。 + 它与 `onBlocked`(风控 403 → "换公网 IP")常常同时触发,因此 `onBlocked` 处理器里 + 用 `lastAllFailedAt()` 做了 60 秒去重,**不允许同时弹两个框**;两者都按 15 分钟节流。 + 以前这里只有一句"无法生成下载地址"的 toast,用户看着像界面没反应。 - 一段典型现场日志应为:`BOOT` → `STORAGE` → (点歌)`点歌编号=... 请求源=... 状态=成功 urlHost=...` → `PLAY ... src=local` → 每 6h `HEALTH ...`;异常时才有 `RAW-ERR`/`NET-ERR`/`NATIVE-SONG ...失败`。 - **网络状态与断网恢复**:`KtvNetworkMonitor`(object,走 `ConnectivityManager` 回调、不轮询) diff --git a/app/build.gradle b/app/build.gradle index 90df804..79f653a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,8 +11,8 @@ android { applicationId "com.local.ktv" minSdk 21 targetSdk 25 - versionCode 36 - versionName "1.5.4" + versionCode 37 + versionName "1.5.5" ndk { // 双 ABI 打包:兼容 64 位(Exynos 1580 等 arm64-v8a)与 32 位设备(极米 Play 超越版 // 的 Amlogic 950X2/S905X2 采用 32 位 Android 用户态,必须包含 armeabi-v7a,否则 diff --git a/app/src/main/assets/mobile/ktv_api.js b/app/src/main/assets/mobile/ktv_api.js index 1f3f6b8..0a66868 100644 --- a/app/src/main/assets/mobile/ktv_api.js +++ b/app/src/main/assets/mobile/ktv_api.js @@ -12,7 +12,7 @@ (function(global) { 'use strict'; - // KTV_BRIDGE_API: 10 (Android 缓存校验标记,请勿删除) + // KTV_BRIDGE_API: 11 (Android 缓存校验标记,请勿删除) // ─── 配置 ────────────────────────────────────────── const CONFIG = { @@ -26,6 +26,9 @@ // 2026-10 实测: 其余入口对每首歌统一返回广告/测试资源时,本入口仍稳定下发真源 CDN 地址, // 故置于首位;它自己域名下的媒体始终是广告,由 AdVideoFilter 继续拦截。 'http://gz.ac16.vip', + // 与 w345.my 同一台服务器(154.219.102.181)的兄弟入口,登录层正常; + // 取址行为尚未定性 → 置于次位,由运行时自动升降序。 + 'http://a.ktv8.my', 'http://w.w345.my', 'http://desktok.top', 'http://mm.kk456.top', @@ -444,7 +447,7 @@ // ─── 导出 ────────────────────────────────────────── global.KtvApi = KtvApi; global.KtvApiConfig = CONFIG; - global.KtvBridgeApiVersion = 10; + global.KtvBridgeApiVersion = 11; if (typeof module !== 'undefined' && module.exports) { module.exports = {KtvApi: KtvApi, CONFIG: CONFIG}; diff --git a/app/src/main/java/com/local/ktv/Eac19WindowGuard.kt b/app/src/main/java/com/local/ktv/Eac19WindowGuard.kt index 24f0352..773090e 100644 --- a/app/src/main/java/com/local/ktv/Eac19WindowGuard.kt +++ b/app/src/main/java/com/local/ktv/Eac19WindowGuard.kt @@ -27,12 +27,22 @@ object Eac19WindowGuard { /** UI 层订阅: 窗口到期(风控 403)时回调(已在主线程)。 */ @Volatile var onBlocked: (() -> Unit)? = null + /** + * UI 层订阅:**所有源都取不到可用地址**时回调(已在主线程)。 + * + * 与 [onBlocked](风控 403 / 只发广告)的区别:这里是"这一轮取址彻底失败"的总信号, + * 无论原因是 403、只返回广告、还是网络异常 —— 用户此时什么都点不了, + * 需要一句明确的话,而不是让界面看起来像没反应。 + */ + @Volatile var onAllFailed: (() -> Unit)? = null + /** 距离上次提示的最小间隔, 防止连续失败时反复弹窗。 */ private const val MIN_PROMPT_INTERVAL_MS = 15 * 60_000L private val lock = Any() private var lastPromptAt = 0L private var blockedEpisodeActive = false + private var lastAllFailedAt = 0L /** 由 JS 桥在 do.php 返回风控 403 时调用(任意线程)。 */ fun noteRiskBlocked() { @@ -54,6 +64,26 @@ object Eac19WindowGuard { } } + /** + * 由取址层在"**所有源都失败**"时调用(任意线程)。 + * 与 [noteRiskBlocked] 同样按 15 分钟节流,避免每次点歌都弹。 + */ + fun noteAllSourcesFailed() { + val now = System.currentTimeMillis() + val shouldNotify: Boolean + synchronized(lock) { + shouldNotify = now - lastAllFailedAt >= MIN_PROMPT_INTERVAL_MS + if (shouldNotify) lastAllFailedAt = now + } + if (shouldNotify) { + Log.w(TAG, "all song sources failed (no usable url)") + onAllFailed?.invoke() + } + } + + /** 最近一次"全部源失效"提示的时间(0 = 从未);供 403 提示去重,避免同时弹两个框。 */ + fun lastAllFailedAt(): Long = lastAllFailedAt + /** 取到有效歌曲 URL(窗口恢复)时调用, 复位本次到期 episode。 */ fun noteUrlOk() { synchronized(lock) { diff --git a/app/src/main/java/com/local/ktv/KtvJsBridge.kt b/app/src/main/java/com/local/ktv/KtvJsBridge.kt index ac5b266..4ae2ac1 100644 --- a/app/src/main/java/com/local/ktv/KtvJsBridge.kt +++ b/app/src/main/java/com/local/ktv/KtvJsBridge.kt @@ -335,7 +335,7 @@ class KtvJsBridge(context: Context) { runCatching { file.isFile && file.length() in 1_001..500_000 && file.readText(Charsets.UTF_8).let { - it.contains("KtvApi") && it.contains("KTV_BRIDGE_API: 10") + it.contains("KtvApi") && it.contains("KTV_BRIDGE_API: 11") } }.getOrDefault(false) @@ -361,7 +361,7 @@ class KtvJsBridge(context: Context) { val script = String(bytes, StandardCharsets.UTF_8) require( script.length > 1000 && script.contains("KtvApi") && - script.contains("KTV_BRIDGE_API: 10"), + script.contains("KTV_BRIDGE_API: 11"), ) { "Invalid or incompatible JS file" } tempFile.writeText(script, Charsets.UTF_8) if (!tempFile.renameTo(cacheFile)) { diff --git a/app/src/main/java/com/local/ktv/MainActivity.kt b/app/src/main/java/com/local/ktv/MainActivity.kt index e9bc2a0..708ec68 100644 --- a/app/src/main/java/com/local/ktv/MainActivity.kt +++ b/app/src/main/java/com/local/ktv/MainActivity.kt @@ -524,6 +524,8 @@ class MainActivity : AppCompatActivity() { Eac19WindowGuard.onBlocked = { main.post { if (isFinishing || isDestroyed) return@post + // 刚刚弹过"全部源失效"就不再叠加第二个框(两者常常同时触发)。 + if (System.currentTimeMillis() - Eac19WindowGuard.lastAllFailedAt() < 60_000L) return@post AlertDialog.Builder(this) .setTitle("歌曲源临时受限") .setMessage( @@ -538,6 +540,19 @@ class MainActivity : AppCompatActivity() { .showForTv(DialogInterface.BUTTON_POSITIVE) } } + // 所有源都取不到地址(403 / 只发广告 / 网络异常都算):给用户一句明确的话。 + // 以前这里只有"无法生成下载地址"的 toast,用户看着像没反应。 + Eac19WindowGuard.onAllFailed = { + main.post { + if (isFinishing || isDestroyed) return@post + AlertDialog.Builder(this) + .setTitle("歌曲源不可用") + .setMessage("远程源已全部失效,等待新版本修复") + .setPositiveButton("知道了", null) + .create() + .showForTv(DialogInterface.BUTTON_POSITIVE) + } + } // 初始化新布局的 UI 组件 initTvLayout() val startupSong = restoredState.currentSong ?: orderQueue!!.firstOrNull() diff --git a/app/src/main/java/com/local/ktv/SongUrlResolver.kt b/app/src/main/java/com/local/ktv/SongUrlResolver.kt index 0e7c903..7a99253 100644 --- a/app/src/main/java/com/local/ktv/SongUrlResolver.kt +++ b/app/src/main/java/com/local/ktv/SongUrlResolver.kt @@ -49,6 +49,8 @@ object SongUrlResolver { private val DEFAULT_HOSTS = listOf( // 与其他入口同一后台、同一协议;2026-10 实测唯一仍下发真源 CDN 地址的入口,放首位。 "http://gz.ac16.vip", + // 与 w345.my 同机(154.219.102.181)的兄弟入口;实测登录层正常,取址行为待观察。 + "http://a.ktv8.my", "http://w.w345.my", "http://desktok.top", "http://mm.kk456.top", @@ -199,6 +201,9 @@ object SongUrlResolver { Eac19WindowGuard.noteRiskBlocked() } } + // 本轮取址彻底失败(无论原因是 403、只返回广告、还是网络异常): + // 发出"全部源失效"总信号,让界面明确告知用户,而不是看起来没反应。 + Eac19WindowGuard.noteAllSourcesFailed() return null }