合并自建歌曲源支线(selfhost-source)+ 版本号按约定修正为 1.6.0

版本号:
- 1.5.11 越界(规则 3 规定 patch ≤ 10)→ 修正为 1.6.0(进位到 minor、patch 归 0)
- versionCode 43 不变:43 从未对外发布,本次修正只是把命名拉回约定

合并支线(原在工作区根目录、App 仓库之外,DESIGN.md 自称"支线"):
- selfhost-source/server/   自建歌曲源服务器(纯 Python 标准库、零 pip 依赖)
- selfhost-source/deploy/、DESIGN.md、LIBRARY.md、build_inventory.py
- selfhost-source/inventory/ 小清单(playlists.csv、song-stats.md 等)
- ⚠️ inventory 生成物(songs-*.csv / singers-top.csv,共 84MB)不入库,
  已写入 .gitignore;需要时用 build_inventory.py 依据 muse.db 重新生成

同时入库的 App 侧支线改动(此前为工作树未提交状态):
- Whoami.kt(whoami 设备心跳)、DeviceIdentity.kt(设备身份伪装)
- PushDownload.kt / PushDownloadClient.kt(点歌推送)
- AdVideoFilter 带 query/fragment 的地址闸门;KTV_BRIDGE_API 11 → 13
- 修 ktv_api.js 的 global.KtvBridgeApiVersion(12 → 13,与注释及 Kotlin 两处校验一致)
- 对应单测:WhoamiTest / DeviceIdentityTest / PushDownloadTest / AdVideoFilterTest(共 39 例)

文档:
- DESIGN.md 的"在 App 仓库之外"约定改为"已并入仓库统一版本化"
- AGENTS.md 版本号行更新为 1.6.0/43,并新增支线并入说明

验证:assembleRelease + testReleaseUnitTest 全过(39 例)
This commit is contained in:
zijunchiang
2026-10-14 02:06:28 +08:00
parent e060ae5ebe
commit 87aa979abd
68 changed files with 21305 additions and 105 deletions
+30 -2
View File
@@ -195,8 +195,8 @@ $BT/apksigner verify 目标名.apk
### 版本号
- `app/build.gradle`:`versionName "1.5.10"` / `versionCode 42`
(`1.5.5`/`37` 已发布;`1.5.6` = 更新源加 Gitee、`1.5.7` = 删除「歌曲源临时受限」弹窗、`1.5.8` = 精简远程日志、`1.5.9` = 按 HAR 对齐原版协议、`1.5.10` = 源列表只留 zk.ktv1.top:6677,均尚未发布。18~21 曾被本机临时诊断包占用)
- `app/build.gradle`:`versionName "1.6.0"` / `versionCode 43`
(`1.5.5`/`37` 已发布;`1.5.6` = 更新源加 Gitee、`1.5.7` = 删除「歌曲源临时受限」弹窗、`1.5.8` = 精简远程日志、`1.5.9` = 按 HAR 对齐原版协议、`1.5.10` = 源列表只留 zk.ktv1.top:6677、`1.5.11`(命名越界,已改) / `1.6.0` = whoami 心跳/设备身份/点歌推送/带 query 的地址闸门 + 并入 selfhost-source 支线,均尚未发布。18~21 曾被本机临时诊断包占用)
- App 内显示统一用纯版本号(不加 `(versionCode)` 括号)
- ⚠️ **发布构建已开启 NewApi 闸门**(`lint { checkReleaseBuilds true; fatal += ['NewApi'] }`):
任何「调用了高于 `minSdk 21` 且未做版本守卫的 API」都会让 `assembleRelease` 直接失败。
@@ -292,6 +292,11 @@ $BT/apksigner verify 目标名.apk
`android.log` → **`MiliLog.local("JS …")`(只写本地文件,不上报远程)**。
逐步过程(Try source / Token OK/FAIL / do.php code=)在本地 `logs/ktv.log` 里看;
远程日志只看一行汇总 **`点歌失败 编号=… 请求源=… 详情=…`**。
- **⚠️ 自建歌曲源支线(`selfhost-source/`,2026-10-13 并入本仓库)**:自建歌曲源服务器(纯 Python 标准库,零 pip 依赖)
已从"App 仓库之外的工作区目录"**并入本仓库**统一版本化;`inventory/*.csv` 等生成物由
`selfhost-source/build_inventory.py` 依据 `muse.db` 重新生成,**不入库**。
App 侧对应模块:`Whoami.kt`(whoami 心跳)、`DeviceIdentity.kt`(设备身份)、`PushDownload*.kt`(点歌推送)。
规则照旧:**支线不改动 App 主体逻辑**,只通过上述独立模块接入。
- **歌曲源**:`app/src/main/assets/mobile/ktv_api.js` 的 `CONFIG.HOSTS`(多源自动排序,失败源沉底)
- ⚠️ **`gz.ac16.vip` 是"源站入口"而不是"广告主机"的典型例子(2026-10 实测)**:
该域名对任何 `/<编号>.ts` 都返回**同一段广告视频**,因此它留在 `AdVideoFilter.AD_HOSTS`
@@ -341,6 +346,21 @@ $BT/apksigner verify 目标名.apk
现在 `code == 403` 也置 `sawForbidden`(i.php 与 do.php 两处)。
- 日志分工照旧:远程一条 `SOURCE-CONFIG trial_enabled=… trial_times=… …`;
原始 JSON 与"要求卡密"提示只写本地。
- **⚠️ whoami 设备心跳(2026-10-11 抓包补齐,第三个请求)**:官方客户端启动后先
`POST hbeat.ktvsky.com/whoami?<24 个字段>`(空 body、`Dalvik/2.1.0 …` UA)登记设备身份,
再 `i.php` → `do.php`;响应 `{"code":200,"next_interval":3600}` 决定下次上报间隔。
约定:
1. 字段构造/响应解析抽到 **`Whoami.kt`(纯 JVM 逻辑,有单测 `WhoamiTest`)**,
`MainActivity.sendDeviceHeartbeat()` 只负责填 `Build.*`、发请求、按 `next_interval` 重排
(`_boot=1` 只在**启动那一次**,之后为 0);
2. `_pkn` 用设置项 **`KtvStore.whoamiPkn`**(默认 `com.origjoy.local.ktv` =
`Whoami.DEFAULT_PKN`,单一来源);`_mac`/`androidid`/`_cid` 三者同源见「设备 MAC」;
3. **必须在后台线程 + 8 秒超时**,失败只写本地日志(`MiliLog.localWarn("WHOAMI …")`),
绝不影响取址/播放,也绝不阻塞启动;
4. 服务端对应实现 `selfhost-source/server/ktv/whoami.py`(配置 `whoami_*`,
`GET /api/whoami` / `POST /api/whoami/send`),字段口径与 App 保持一致;
5. 参数拼接与取址协议一样**逐字节对齐抓包**(顺序、编码都别改);
回归证据在服务端 `tests/test_basic.py::TestCanonicalSign`(现网 URL 与 HAR 逐字节相同)。
- **⚠️ 取址有两条实现,改协议必须同时改**:JS 主路径(`ktv_api.js`)+ 原生兜底
(`SongUrlResolver.kt`,供 **WebView 不可用**的老盒子使用 —— 实测极米 H1S/GMUI2.0 上
WebView 导不出 `window.KtvApi`,桥永远 not ready,只靠 JS 时表现为
@@ -368,6 +388,14 @@ $BT/apksigner verify 目标名.apk
**URL 里的 `.ts` 文件名必须等于点歌编号**(真源恒为 `/<musicno>.ts`,已实测
`txog.ktvsky.com` / `download.origjoy.com` 两个 CDN 域)。旧规则只比对"纯数字文件名",
`my_ad_video.ts` 这类非数字名字直接漏过;
⚠️ **2026-10-11 补充(带 query 的地址)**:媒体地址恒为 `…/<编号>.ts?token=…&t=…`,
因此判定**必须先剥离 query/fragment**:`.ts` 比对只看 **path**、广告特征只看 **host + path**
(用 `AdVideoFilter.hostOf()/pathOf()`,`java.net.URI` 失败会手工兜底)。
旧实现在 `#fragment` 上会**整条文件名规则失效(漏放广告)**,拿整串搜 `wb66`/`demo`
又会误杀 query 里的签名串("地址不可信"的隐蔽来源)。裸 IP / `pub-*.r2.dev/ss/`
也已收进这道闸门 —— 四条路径不再各判一套。JS 侧同步改(`splitUrl()`),
`KTV_BRIDGE_API` 已升到 **13**;回归用例见 `AdVideoFilterTest`(带 query/fragment 的
真源放行、广告仍拒)与服务端 `tests/test_basic.py::TestQueryStringGate`;
3. **内容校验挡不住广告**:广告视频能通过 `SongFileValidator`(≥6MB / ≥60s / TS 包完整),
所以"取址即校验"是唯一有效防线。一旦判定地址不可信,必须**同时删除已落盘的歌曲文件**
(`purgeCachedSongFile`),否则 `isDownloaded()` 会一直复用那个广告文件,广告反复播;