取址改为官方源优先+自建源兜底,只有官方源成功才上报(V1.6.2/45,未发布)

需求一:流程反转 + 上报绑定官方源成功
- 默认 sourceHosts 反转为 ["http://zk.ktv1.top:6677", "https://mdmuse.kasugano.cn"];
  SourceHosts.DEFAULT / SongUrlResolver.DEFAULT_HOSTS(收敛为引用前者)/ ktv_api.js
  CONFIG.HOSTS+HOST 三处一致。
- 取址结果改为 SourcedSongUrl(url, sourceHost)(**显式来源标志**,不靠 URL 猜):
  新增 SongSourceFlow(回落链 firstAvailable / 上报判定 shouldReportToSelfHost /
  全失败信号);JS 侧新增 android.onSongSource(requestId, host) 回传命中的源。
- 汇合点 SongApiClient.getSongDownloadUrl 只在"地址来自官方源"时才 POST /push_download;
  自建源自己给的地址与 DB 回落都不上报。
- 推送默认目标改为"从歌曲源列表里找第一个自建源"(自建源不再排第一,旧写法会直接关掉推送)。
- 1.6.1 落盘的旧默认顺序(自建源优先)在 KtvStore.load() 里一次性迁移;自定义列表不动。
- 业务码 404(自建源没有这首歌)仍只当"该源没地址",不置 sawForbidden、不进冷却、不污染受限统计。

需求二:6 条隐患
1) 设置页改源对 JS 立即生效:KtvJsBridge.applySourceSettingsNow 重新注入 KtvApiConfig.*
   并把已构造实例的 window.api.sourceOrder 重置为新列表;SongApiClient.applySourceSettings 接线。
2) JS 路径识别业务码 403:新增 KtvJsBridge.riskForbidden(status, body)(纯函数,手写判定
   避免 org.json 在单测里是空壳),HTTP 403 与 HTTP 200+{"code":403} 都算风控,与原生 sawForbidden 同口径。
3) isTrustedDownloadUrl 移除广告域名 ac16.vip 白名单,并改为黑名单优先(不再依赖调用顺序)。
4) MainActivity.download(song)(第五条地址消费点)补 AdVideoFilter.isTrustedSongUrl 闸门,
   不可信按"源失效"处理(Eac19WindowGuard.noteAllSourcesFailed),不再静默下载。
5) SongApiClient.fetchFromDb 不再静默吞异常:曲库打不开/查询异常各记一条 MiliLog.localWarn(含编号与异常)。
6) 删除死代码 streamDownloadAndPlay(全仓 grep 无调用点,且内部取址不过最终闸门)。

需求三:死代码/漂移清理(均先全仓搜索确认无引用)
- AdVideoFilter.replacementUrl 无任何写入点(恒为 null)→ 删除,resolve() 只保留"屏蔽"语义。
- ktv_api.js CONFIG.VER/VN 由"从不生效的死值"改为实际使用值 2.0 / 4.1.3.03281430。
- CONFIG.HOST 改为被 getToken 真正读取(不再"注入但从未被读")。
- KTV_BRIDGE_API 14 → 15(JS 两处 + KtvJsBridge 两处校验);AGENTS.md / README.md / README.en.md
  的陈旧标记统一到 15。
- do.php 的 limit 与 i.php 的 retry/ordertype/end_date/heart_time 确认无消费方 → 不新增解析(保持现状)。

需求四:测试与打包
- 新增 SourceOrderConsistencyTest / SongSourceFlowTest / JsBridgeGateTest / DownloadEntryGateTest,
  更新 SourceHostsTest / AdVideoFilterTest;app/build.gradle 打开 unitTests.returnDefaultValues。
- testReleaseUnitTest --rerun-tasks --offline:12 个测试类 / 87 例全过。
- assembleRelease --offline 通过(lintVital NewApi 0 条);版本 1.6.1/44 → 1.6.2/45。

不发布:未推 Gitea/Gitee、未动 Release/tag。
This commit is contained in:
zijunchiang
2026-10-14 03:45:07 +08:00
parent e00a1ace79
commit d01269e917
22 changed files with 1138 additions and 287 deletions
+26 -11
View File
@@ -12,25 +12,29 @@
(function(global) {
'use strict';
// KTV_BRIDGE_API: 14 (Android 缓存校验标记,请勿删除)
// KTV_BRIDGE_API: 15 (Android 缓存校验标记,请勿删除)
// ─── 配置 ──────────────────────────────────────────
const CONFIG = {
APP_ID: 'd4eeacc6cec3434fbc8c41608a3056a0',
APP_KEY: '024210cba40d4385a93e6c2d3249bfb5',
SDK_KEY: '19042303a8374f67ae3fe1e25c97936f',
VN: '4.100.5.11121504',
VER: '4',
// 歌曲源入口:**自建源优先、官方源兜底**(2026-10-14)。
// https://mdmuse.kasugano.cn = 自建歌曲源(App 令牌 / 点歌推送的宿主;媒体站 md.kasugano.cn:4433);
// http://zk.ktv1.top:6677 = 官方源兜底(自建源取不到地址时仍能回落,老用户不受影响)。
// ⚠️ 与 Android 侧 SongUrlResolver.DEFAULT_HOSTS(SourceHosts.DEFAULT)**逐项一致**;
// ⚠️ 下面两个值与 App 侧**实际使用**的值一致(`KtvJsBridge` 注入时也是这两个)。
// 旧文件里写的是 '4.100.5.11121504' / '4',那是**从不生效的死值** ——
// 被桥覆写后就成了历史遗留,直接读本文件会得出错误的协议版本结论。
// 现在即使不经过桥注入(单独调试本文件)也是同一份协议版本。
VN: '4.1.3.03281430',
VER: '2.0',
// 歌曲源入口:**官方源优先、自建源兜底**(2026-10-14 反转)。
// http://zk.ktv1.top:6677 = 官方源(成功率最稳,先试;成功后的地址会上报自建源);
// https://mdmuse.kasugano.cn = 自建歌曲源兜底(官方源取不到时才用它;它自己给的地址**不上报**)。
// ⚠️ 与 Android 侧 SourceHosts.DEFAULT(SongUrlResolver.DEFAULT_HOSTS 直接引用它)**逐项一致**;
// 运行时由 App 设置项注入覆盖(window.KtvApiConfig.HOSTS / HOST)。
HOSTS: [
'https://mdmuse.kasugano.cn',
'http://zk.ktv1.top:6677',
'https://mdmuse.kasugano.cn',
],
HOST: 'https://mdmuse.kasugano.cn',
HOST: 'http://zk.ktv1.top:6677',
// 媒体地址允许清单(App 设置项注入覆盖):只放行这些主机的"非域名"限制 ——
// 直连内网自建源时媒体地址就是裸 IP(http://192.168.100.232:6677/media/<编号>.ts)。
// ⚠️ 广告黑名单(wb66/demo/ac16.vip/ad_files/my_ad_video/pub-*.r2.dev/ss)优先级更高,命中清单也要拒。
@@ -282,6 +286,10 @@
// 歌曲源自动排序: 维护一份"当前优先级"列表(运行时可变)。
// 某源解析失败时把它挪到队尾, 下次点歌优先尝试更可靠的源。
this.sourceOrder = (options.hosts && options.hosts.length ? options.hosts : CONFIG.HOSTS).slice();
// 最近一次取址**命中的源**(成功时写入;失败/未开始时为空)。
// 供 Android 侧判定"这条地址是否来自官方源"(只有官方源成功才上报自建服务器)——
// 由桥在返回地址之前用 `android.onSongSource(requestId, host)` 显式取走。
this.lastSourceHost = '';
// 与 test_ktv_unified.py 一致:首次设备加最多 3 次换设备重试。
this.maxDeviceRetries = Number.isFinite(Number(options.maxDeviceRetries))
? Math.max(0, Number(options.maxDeviceRetries) | 0) : 3;
@@ -318,6 +326,7 @@
this.sn = randomHex(16);
this.tokens = {};
this.token = '';
this.lastSourceHost = '';
this._log('Device regenerated: ' + this.mac + '_' + this.sn);
},
@@ -355,7 +364,8 @@
// 2. Get Auth Token
getToken: async function(host, forceRefresh) {
var self = this;
host = host || CONFIG.HOSTS[0];
// 默认源:CONFIG.HOST(与 HOSTS[0] 同值,由 Android 侧一致注入/校验)优先,其次 HOSTS[0]。
host = host || CONFIG.HOST || CONFIG.HOSTS[0];
if (!forceRefresh && self.tokens[host]) {
self.token = self.tokens[host];
return self.token;
@@ -430,6 +440,9 @@
var self = this;
var lastDetail = '';
var lastAttemptHost = '';
// 本次取址命中的源(成功才写入)——由桥在返回地址前用 android.onSongSource 取走,
// 供"只有官方源成功才上报自建服务器"的判定(见 Kotlin SongSourceFlow)。
self.lastSourceHost = '';
for (var deviceRound = 0; deviceRound <= self.maxDeviceRetries; deviceRound++) {
if (deviceRound > 0) self.regenerateDevice();
// 每一轮使用当时的优先级快照; demote 动态修改 sourceOrder。
@@ -492,6 +505,8 @@
if (songUrl && !isDemoUrl(songUrl, musicno) && /^https?:\/\//i.test(songUrl)) {
self._log('URL OK: ' + musicno + ' ls=' + ls + ' via ' + host);
self.promoteSource(host);
// 本次命中的源(Android 侧据此决定是否上报自建服务器)。
self.lastSourceHost = host;
hostOk = true;
// 任务3: 点歌成功上报命中的源(Android 侧写入远程日志)。
reportSongResult(true, host, musicno, 'ok');
@@ -532,7 +547,7 @@
// ─── 导出 ──────────────────────────────────────────
global.KtvApi = KtvApi;
global.KtvApiConfig = CONFIG;
global.KtvBridgeApiVersion = 14;
global.KtvBridgeApiVersion = 15;
if (typeof module !== 'undefined' && module.exports) {
module.exports = {KtvApi: KtvApi, CONFIG: CONFIG};