合并自建歌曲源支线(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
@@ -0,0 +1,245 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
麦动KTV 自建歌曲源 —— IIS **API 站**配置(站点 `mdmuse.kasugano.cn`,HTTPS 443)
把这个文件放到该站点的根目录,文件名必须是 `web.config`。
┌──────────────────────────────────────────────────────────────────────────────┐
│ 本目录一共有三份配置,**这一份只管 API**(另一个站点只管文件下载): │
│ * `api-site/web.config` ← 你正在看的:mdmuse.kasugano.cn(HTTPS 443) │
│ * 同级的「媒体站」目录 md.kasugano.cn(HTTPS **4433**)只提供文件下载 │
│ * `web.config`(上级目录) 只是"该看哪两份"的索引,本身不含任何规则 │
│ 文件名与完整步骤见 `../README.md`(第 5 节「建两个站点」)。 │
└──────────────────────────────────────────────────────────────────────────────┘
作用:把 App 需要的接口反代到内网 Linux 上跑的本服务
(默认 `http://192.168.100.232:6677`,**必须替换成你自己的 IP:端口**)。
前置条件(缺一个就会 500.19 或 502,完整步骤见 `../README.md`):
① IIS 装好 **URL Rewrite 2.1** 与 **Application Request Routing (ARR) 3.0**;
② 启用 ARR 代理(ARR → Server Proxy Settings → 勾 Enable proxy → Apply),
对应 `../applicationHost.config.snippet.xml` 里的 `<proxy enabled="true" ... />`;
③ 放行服务器变量 HTTP_X_FORWARDED_FOR / HTTP_X_FORWARDED_PROTO / HTTP_X_REAL_IP /
HTTP_X_FORWARDED_HOST,否则本文件的 `<serverVariables>` 会让站点报 500.19。
⚠️ 必须替换的占位值:
- 后端地址 `http://192.168.100.232:6677` → 你内网 Linux 服务器的真实 IP:端口
(本文件共 7 条反代规则 + 说明里的示例;用编辑器全局替换即可);
- 域名 `mdmuse.kasugano.cn` → 你的 API 域名(站点绑定里也要一致);
- `HTTP_X_FORWARDED_PROTO` 写死 `https`:只有本站点绑定了 HTTPS 证书才这样写;
若站点同时有 HTTP 绑定,按文件末尾「HTTP 绑定」说明调整。
⚠️ 为什么这个站点里**一个文件下载路径都没有**:文件下载走另一个域名(另一个站点),
这样"批量拉文件"的流量与"调接口"的流量在 IIS 层就分开了,互不影响;
后端还会按 `X-Forwarded-Host` 再做一次宿主隔离(配置项见 `../README.md` 第 10 节),
双重保险:谁也别想用 API 域名批量拖文件、也别想用下载域名调接口。
⚠️ 管理端口(**本文件里没有任何规则指向它**):公网因此访问不到管理界面 —— 这是
"管理端口不对公网暴露"的第一道闸门(另外两道:IIS 里用 IP 限制 / 只绑内网地址,
以及 Linux 侧防火墙;端口号与做法见 `../README.md` 的「管理端口」一节)。
⚠️ 查询串:URL Rewrite 默认 appendQueryString="true",会把原始 ?a=1&b=2 原样转给后端;
这里显式写出来,避免以后有人改成 false 踩坑。{R:0} = 规则 pattern 匹配到的**整段路径**
(不含开头的 /),所以路径原样透传。
⚠️ 请求方法:URL Rewrite 对**所有方法**(GET/HEAD/POST/PUT/DELETE…)都生效,
不需要额外配置;本服务实际只用 GET/HEAD/POST。若站点装了 WebDAV 模块,它会在
Rewrite 之前拦下 PUT/DELETE 并回 405(本项目用不到这两个方法)。
-->
<configuration>
<system.webServer>
<!-- ══════════════ 反向代理规则(URL Rewrite + ARR)══════════════
写法:<action type="Rewrite" url="http://后端/{R:0}" /> —— 目标写成绝对 URL 时,
URL Rewrite 就会交给 ARR 做**反向代理**(不是 302 重定向)。
每个规则都带 <serverVariables>,把真实客户端信息告诉后端:
X-Forwarded-For = 客户端 IP(**有意只写 {REMOTE_ADDR}**:不拼接客户端自己传来的
XFF,后端拿到的就是真实来源 IP,杜绝伪造链;
后端还有 trusted_proxies 白名单兜底)
X-Real-IP = 同上(有些反代/后端只认这一个)
X-Forwarded-Proto = https(让后端把 /music/do.php 返回的下载地址拼成
https://域名/…,而不是 http 内网地址)
X-Forwarded-Host = 原始 Host(域名;既是后端拼绝对地址的依据,
也是后端**宿主隔离**判定的依据 —— 媒体站靠它认出自己)
-->
<rewrite>
<rules>
<!-- ① GET/POST /token(App 令牌:随机串,默认 10 分钟时效,可绑定客户端 IP)
自有 App 的流程:先取令牌,再带请求头 X-App-Token 调 do.php / push_download。
⚠️ 响应是 Cache-Control: no-store,中间层不要缓存它。 -->
<rule name="ktv-app-token" stopProcessing="true">
<match url="^token$" />
<serverVariables>
<set name="HTTP_X_FORWARDED_FOR" value="{REMOTE_ADDR}" />
<set name="HTTP_X_REAL_IP" value="{REMOTE_ADDR}" />
<set name="HTTP_X_FORWARDED_PROTO" value="https" />
<set name="HTTP_X_FORWARDED_HOST" value="{HTTP_HOST}" />
</serverVariables>
<action type="Rewrite" url="http://192.168.100.232:6677/{R:0}" appendQueryString="true" />
</rule>
<!-- ② POST /api.php(App 启动配置) -->
<rule name="ktv-api-php" stopProcessing="true">
<match url="^api\.php$" />
<serverVariables>
<set name="HTTP_X_FORWARDED_FOR" value="{REMOTE_ADDR}" />
<set name="HTTP_X_REAL_IP" value="{REMOTE_ADDR}" />
<set name="HTTP_X_FORWARDED_PROTO" value="https" />
<set name="HTTP_X_FORWARDED_HOST" value="{HTTP_HOST}" />
</serverVariables>
<action type="Rewrite" url="http://192.168.100.232:6677/{R:0}" appendQueryString="true" />
</rule>
<!-- ③ GET /i.php(老 App 取 token;**不带我们的 App 令牌**,后端默认不强制,
但参数与签名算法一个字都没改,照常可用) -->
<rule name="ktv-i-php" stopProcessing="true">
<match url="^i\.php$" />
<serverVariables>
<set name="HTTP_X_FORWARDED_FOR" value="{REMOTE_ADDR}" />
<set name="HTTP_X_REAL_IP" value="{REMOTE_ADDR}" />
<set name="HTTP_X_FORWARDED_PROTO" value="https" />
<set name="HTTP_X_FORWARDED_HOST" value="{HTTP_HOST}" />
</serverVariables>
<action type="Rewrite" url="http://192.168.100.232:6677/{R:0}" appendQueryString="true" />
</rule>
<!-- ④ GET /music/do.php(点歌取址:返回的下载地址由后端按 X-Forwarded-* 拼成
https 公网地址;老 App 带的 token 参数是它自己的老协议令牌,后端认得) -->
<rule name="ktv-music-do-php" stopProcessing="true">
<match url="^music/do\.php$" />
<serverVariables>
<set name="HTTP_X_FORWARDED_FOR" value="{REMOTE_ADDR}" />
<set name="HTTP_X_REAL_IP" value="{REMOTE_ADDR}" />
<set name="HTTP_X_FORWARDED_PROTO" value="https" />
<set name="HTTP_X_FORWARDED_HOST" value="{HTTP_HOST}" />
</serverVariables>
<action type="Rewrite" url="http://192.168.100.232:6677/{R:0}" appendQueryString="true" />
</rule>
<!-- ⑤ GET /push_download/status(推送计数,运维核对;注意放在 ⑥ 之前,
虽然两条 pattern 互不重叠,但"长路径在前"是好习惯) -->
<rule name="ktv-push-download-status" stopProcessing="true">
<match url="^push_download/status$" />
<serverVariables>
<set name="HTTP_X_FORWARDED_FOR" value="{REMOTE_ADDR}" />
<set name="HTTP_X_REAL_IP" value="{REMOTE_ADDR}" />
<set name="HTTP_X_FORWARDED_PROTO" value="https" />
<set name="HTTP_X_FORWARDED_HOST" value="{HTTP_HOST}" />
</serverVariables>
<action type="Rewrite" url="http://192.168.100.232:6677/{R:0}" appendQueryString="true" />
</rule>
<!-- ⑥ POST /push_download(App 点歌推送:鉴权 = 合法 App 令牌 **或** push_token;
限流按真实客户端 IP,靠上面的 XFF) -->
<rule name="ktv-push-download" stopProcessing="true">
<match url="^push_download$" />
<serverVariables>
<set name="HTTP_X_FORWARDED_FOR" value="{REMOTE_ADDR}" />
<set name="HTTP_X_REAL_IP" value="{REMOTE_ADDR}" />
<set name="HTTP_X_FORWARDED_PROTO" value="https" />
<set name="HTTP_X_FORWARDED_HOST" value="{HTTP_HOST}" />
</serverVariables>
<action type="Rewrite" url="http://192.168.100.232:6677/{R:0}" appendQueryString="true" />
</rule>
<!-- ⑦ GET /healthz(探活:给 IIS/负载均衡用;后端是纯内存快照,微秒级返回) -->
<rule name="ktv-healthz" stopProcessing="true">
<match url="^healthz$" />
<serverVariables>
<set name="HTTP_X_FORWARDED_FOR" value="{REMOTE_ADDR}" />
<set name="HTTP_X_REAL_IP" value="{REMOTE_ADDR}" />
<set name="HTTP_X_FORWARDED_PROTO" value="https" />
<set name="HTTP_X_FORWARDED_HOST" value="{HTTP_HOST}" />
</serverVariables>
<action type="Rewrite" url="http://192.168.100.232:6677/{R:0}" appendQueryString="true" />
</rule>
<!-- ⑧ 其余路径一律 404(不反代、不读本地文件):管理界面/管理接口/未列出的接口
全部挡在外面。要临时放通别的路径,就在这条规则**之前**再复制一条上面的规则。 -->
<rule name="ktv-block-everything-else" stopProcessing="true">
<match url=".*" />
<action type="CustomResponse" statusCode="404" statusReason="Not Found"
statusDescription="404 Not Found" />
</rule>
<!-- ⑨【可选·仅排查时启用】GET /api/client_ip:看服务端认定的客户端 IP / 是否来自
可信代理 / 宿主判定结果(host、是否命中媒体宿主/API 宿主)。
默认**故意不放通**(它会返回内网代理地址列表,属于内部信息);
排查 X-Forwarded-For 是否生效时,把下面这段的注释去掉、排完再注释回去。
另一种不改配置的做法:在 Linux 上直连服务端口自测(见 ../README.md「验证」)。
<rule name="ktv-optional-client-ip" stopProcessing="true">
<match url="^api/client_ip$" />
<serverVariables>
<set name="HTTP_X_FORWARDED_FOR" value="{REMOTE_ADDR}" />
<set name="HTTP_X_REAL_IP" value="{REMOTE_ADDR}" />
<set name="HTTP_X_FORWARDED_PROTO" value="https" />
<set name="HTTP_X_FORWARDED_HOST" value="{HTTP_HOST}" />
</serverVariables>
<action type="Rewrite" url="http://192.168.100.232:6677/{R:0}" appendQueryString="true" />
</rule>
-->
<!-- ⑩【可选·仅运维时启用】GET /token/status:令牌计数(签发/校验失败/限流)。
同样是内部信息,默认不放通。临时启用时用请求头 X-App-Token 调(开了强制的话)。
<rule name="ktv-optional-token-status" stopProcessing="true">
<match url="^token/status$" />
<serverVariables>
<set name="HTTP_X_FORWARDED_FOR" value="{REMOTE_ADDR}" />
<set name="HTTP_X_REAL_IP" value="{REMOTE_ADDR}" />
<set name="HTTP_X_FORWARDED_PROTO" value="https" />
<set name="HTTP_X_FORWARDED_HOST" value="{HTTP_HOST}" />
</serverVariables>
<action type="Rewrite" url="http://192.168.100.232:6677/{R:0}" appendQueryString="true" />
</rule>
-->
</rules>
</rewrite>
<!-- ══════════════ 上传/请求体限制 ══════════════
maxAllowedContentLength 是 IIS 自己先卡的一道(单位**字节**,默认 30MB)。
本站点公开的接口请求体都很小(推送是一条表单),这里仍给到 32MiB 作为余量。
⚠️ 它必须 **≥ 后端配置 max_body_bytes(默认 8MiB)**,否则超限时是 IIS 先返回它自己的
HTML 413,而不是后端的 JSON + 中文说明,两边行为就不一致了。 -->
<security>
<requestFiltering>
<requestLimits maxAllowedContentLength="33554432" />
</requestFiltering>
</security>
<!-- 整站关掉静态/动态压缩:本站点没有可压缩的大响应,保持与另一个站点一致,
避免以后误加规则时把分块/长度搞乱(压缩会让 Content-Length 与 Range 语义失真)。 -->
<urlCompression doStaticCompression="false" doDynamicCompression="false" />
<!-- 站点若装了 WebDAV,它会在 URL Rewrite 之前拦下 PUT/DELETE 并回 405。
本项目只用 GET/HEAD/POST,一般无需处理;确实遇到 405 就取消下面的注释。
<modules>
<remove name="WebDAVModule" />
</modules>
<handlers>
<remove name="WebDAV" />
</handlers>
-->
</system.webServer>
</configuration>
<!--
══════════════ 若站点同时绑定了 HTTP(不带证书)══════════════
上面每条规则的 HTTP_X_FORWARDED_PROTO 都写死了 "https"。如果这个站点**同时**有 http 和
https 两个绑定,需要让 http 走 http、https 走 https。两种做法:
做法 A(推荐):把 proto 写成条件分支 —— 删掉每条规则里的
<set name="HTTP_X_FORWARDED_PROTO" value="https" />
改成在规则里加条件与两条设置(URL Rewrite 不支持条件赋值,所以要拆成两条规则):
<conditions><add input="{HTTPS}" pattern="^ON$" /></conditions> → value="https"
<conditions><add input="{HTTPS}" pattern="^ON$" negate="true" /></conditions> → value="http"
做法 B(更省事):干脆只保留 HTTPS 绑定(HTTP 绑定删掉或改成 301 跳转到 HTTPS),
那么写死 "https" 就是对的。
⚠️ 忘记改的后果:HTTP 访问时会话里返回的下载地址是 https://…,或者 HTTPS 站点返回
http://内网主机名(App 拒收)。定位办法:看后端的 `GET /api/client_ip`
(默认不对外暴露;临时放通 / 内网直连两种方式见 ../README.md),
它会给出服务端认定的真实客户端 IP / proto / host,一眼就能看出是谁给的。
-->