# 协议与数据模型

[文档首页](./README.md)

独立部署说明：文中官方域名为原服务示例，实际使用配置的实例域名；后台ADMIN_ORIGIN要求HTTPS且包含实际非默认端口。可信代理通过TRUSTED_PROXY_IPS显式配置，内部服务/数据路径也已环境化，详见部署文档。初次实例不带旧数据库/密钥/默认插件或原音源启停策略。

## 1. HTTP 约定

- 公开和管理接口基地址均为 `https://wristhyme.tolike.cn`，路径区分大小写。
- 音乐元数据读取为 GET，查询参数采用 URL 编码；不要把中文、`ref` 或歌单链接手工拼接进 URL。
- 写入接口为 POST，JSON 根必须是对象，`Content-Type: application/json`，须提供一个明确的 `Content-Length`。设备与后台实现拒绝 `Transfer-Encoding` 分块上传。
- 成功 JSON 没有统一 `data` 外层，也不是所有接口都返回 `ok`；按各接口实际结构解析。
- 网关 JSON 通常为 `application/json; charset=utf-8`；音频是二进制，不可调用 JSON 解析器。
- 网关/后台通常返回 `Cache-Control: no-store`、`X-Content-Type-Options: nosniff`、`Referrer-Policy: no-referrer`、`X-Frame-Options: DENY` 及 CSP。静态发布文件遵循反向代理的缓存策略。
- 不存在通用的 Bearer/JWT/API Key 鉴权。不要给音乐请求携带管理员 Cookie、内部密钥或平台登录凭证。
- 当前代码没有通用 CORS/OPTIONS 支持。原生客户端可访问，浏览器建议同源调用；不能假定跨域前端能直接使用。
- 没有统一 HEAD 支持，也没有公共 PUT/PATCH/DELETE 合同。Python 默认处理可能返回 HTML 501，而非 JSON 错误。
- 查询解析只使用同名参数的首个值，空值可能被忽略；因此不要依赖重复参数或空参数来表达列表。
- 未知查询字段不一定被拒绝，但不能据此声称支持该功能。

### 可选客户端身份头

| 头 | 格式 | 用途 |
| --- | --- | --- |
| `X-Wristhyme-Device` | 32 位小写十六进制 ID + `.` + 64 位小写十六进制签名 | 已自愿注册实例的统计及设备封禁识别 |
| `X-Wristhyme-Version` | 字符串，观察逻辑最多保留 32 字符 | 应用版本 |
| `X-Wristhyme-Variant` | 字符串，最多保留 16 字符 | 如 `phone`、`watch`、`desktop`、`ios` |
| `Range` | 单个 `bytes=start-end` 范围 | 仅音频流请求 |

设备头不是管理员身份，也不证明物理设备或自然人身份。客户端只应向本站 HTTPS `/api/` 发送设备凭证，并阻止跨站重定向。

`X-Real-IP` 由可信反向代理覆盖，仅特定代理地址受信任；不要依靠客户端伪造此头改变限流或封禁。服务端不信任任意 `X-Forwarded-For`。

## 2. 标识与有效期

| 字段 | 含义 | 生命周期 |
| --- | --- | --- |
| `source.id` | 客户端可选音源目录 ID，可为自定义别名 | 后台可改名、启停、删除自定义项 |
| `source.backend` / `song.source` | 实际平台身份，例如 `netease` | 歌曲协议必须保留真实平台，不能替换为别名 |
| `song.key` | 服务器已知歌曲的短期索引，当前生成算法为 SHA256 前 32 位小写 hex | 写入歌曲缓存后 21600 秒；最多 1200 条，可能提前淘汰；重启丢失 |
| `song.ref` | 可恢复歌曲的签名引用 | 无内置时间戳过期；依赖签名密钥、合法结构和音源仍启用 |
| `plugin.id` | 脚本原始字节 SHA256 的前 16 位小写 hex | 内容变化会改变 ID；目录启停影响可用性 |
| `0000000000000000` | 跟随后台默认插件的稳定别名 | 每次解析映射至当时有效默认，非真实脚本 ID |
| `ticket` | 插件解析后的随机音频访问票据 | 3600 秒；最多 512 条，可能提前淘汰；重启丢失 |
| `device_id` | 随机匿名安装实例 ID | 非硬件 ID；撤回统计后需客户端停止使用旧凭证 |
| 管理 Cookie | 管理会话随机令牌 | 8 小时固定有效期；退出、改密码等会使其失效 |

### `key` 与 `ref`

服务器依据歌曲完整内部对象计算 key，并保存歌曲。客户端不能提交任意平台 ID、标题或 URL 来代替 key。

`ref` 目前为 `base64url(JSON歌曲).HMAC-SHA256`，是防篡改引用，**不是加密数据**。客户端应将其视为不透明字符串，不自行解码、修改、生成或提取第三方信息。原有“签名过期”错误文案不表示 ref 有独立 TTL。

收藏应保存 `ref`；下次播放可先 `/api/restore` 获取当前 key。恢复返回的新 key 有可能与原 key 相同，但重新注册缓存项；不要要求 key 一定变化。签名密钥被替换、引用损坏或音源禁用时恢复会失败。

网易云 `search_signals` 被排除在歌曲缓存和签名之外，避免搜索排名变化改变歌曲身份。经恢复得到的歌曲通常没有该查询的排序信号。

### 默认插件

- 后台目录 `default_plugin` 是管理员配置值；`effective_default_plugin` 是当前实际生效值。
- 公开 `/api/sources.default_plugin` 和 `/api/plugins.default_plugin` 返回**有效值**，不是必须等于原始配置值。
- 没有配置默认时有效值为空，不自动挑插件；原平台 `/api/media` 仍可使用。
- 已配置的插件禁用后，按后台插件创建时间/ID顺序选择其他启用项；全部禁用时为空。原配置插件重新启用后恢复原默认。
- 公开插件接口中 `plugin` 省略、`default` 或稳定别名都会映射有效默认；没有默认返回 503，**不会自动转为原平台解析**。
- 当前 Android 2.2.2 在解析支持的平台前读 `/api/sources`；有效默认为空才走原平台。获取配置或插件解析失败会报告错误，不静默换源。

## 3. 公共数据模型

### 3.1 `Source`

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `id` | string | 目录 ID；通常 `[a-z0-9_]{1,48}` |
| `name` | string | 展示名称，可被后台修改 |
| `backend` | string | 基础平台 ID；旧网关可能缺少 |
| `plugin` | string | 关联插件推荐 ID；未关联或已禁用时为空；不等于自动解析插件 |

已知基础平台：`qq`、`netease`、`kugou`、`kuwo`、`migu`、`soda`、`bilibili`、`qianqian`、`joox`、`jamendo`、`fivesing`、`apple`。后台支持配置不代表当前公开目录全部启用。

LX 映射：`netease → wy`、`qq → tx`、`kugou → kg`、`kuwo → kw`、`migu → mg`；汽水和哔哩哔哩不在当前网关 LX 映射中。

### 3.2 `Song`

| 字段 | 类型 | 是否通常存在 | 说明 |
| --- | --- | --- | --- |
| `key` | string | 是 | 服务器歌曲索引 |
| `ref` | string | 是 | 签名恢复引用 |
| `name` | string | 是 | 标题 |
| `artist` | string | 是 | 歌手/上传者，多人可能以 `、` 分隔 |
| `album` | string | 是 | 专辑；哔哩哔哩可能为 BV 号 |
| `duration` | string | 是 | 上游通常为秒数字符串，可为空；不是毫秒整数 |
| `source` | string | 是 | 基础平台，而非自定义目录别名 |
| `original` | boolean | 是 | 保留的历史标记；false 不证明是翻唱，true 也不构成独立音频鉴定 |
| `cover` | string | 是 | 封面地址或空字符串；不保证能访问，客户端有自己的允许策略 |
| `access_hint` | string | 否 | 平台权限提示，不是权限验证结果 |
| `search_signals` | object | 否 | 网易云搜索的查询相关信号 |

`Song` 不返回原始平台 `id`、内部 `extra` 和上游音频 URL 字段。ref 不透明，不应作为这些字段的公共替代查询接口。

### 3.3 `Playlist`

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `id` | string | 平台公共歌单 ID，不是歌曲 key |
| `source` | string | 基础平台 |
| `name` | string | 歌单标题 |
| `creator` | string | 创建者名称，可为空 |
| `track_count` | string | HTML 元数据保留为字符串，可能为空，不应强制要求 number |

### 3.4 网易云 `search_signals`

| 字段 | 类型/固定值 | 意义 |
| --- | --- | --- |
| `policy` | `netease-signals-221` | 信号合同标识 |
| `original_recording` | object | `status: unknown`、`rank: 0`、`references: []`、`candidate_fields: {}` |
| `popularity` | object | `status: unknown`、`value: null`、`metric: null`、`reason`、`candidate_fields` |
| `version` | object | `status`、`kinds: string[]`、`evidence: object[]` |
| `upstream_index` | integer | 原始上游位置，0 起始，包含页偏移 |
| `upstream_order_role` | `stable_tiebreak_only_not_popularity` | 上游位置仅用于稳定兜底 |
| `ranking` | object，非空查询时存在 | `relevance`、`version_intent_misses`、`original_rank`、`popularity_rank`、`version_penalty` |

`version.status` 为 `metadata_hint` 或 `unmarked_not_original`。已识别 kinds：`live`、`cover`、`remix`、`instrumental`、`piano`、`sped_up`、`slowed`、`acoustic`、`remaster`、`interlude`、`rnb`。

证据对象：`{kind, field, text, confidence: "metadata_hint"}`，来自明确标题版本段或 `alia`/`alias`。原唱引用对象：`{artist_text, field, text, confidence: "reference_only_not_recording_proof"}`。

候选字段只是未经解释的观察值；标题、权限码、上游次序都不应被展示为真实热度。未标注版本不等于原版。排序范围是当前页，不是全库热度排序。

## 4. 错误与状态码

常见错误：

```json
{"error":"结果已过期，请重新搜索"}
```

可选附加字段有 `code`、`source`、`upstream_status`、`state`、`songs`。插件 probe 失败可能返回 `{ok:false, detail, error}`；客户端可按 `error`、`detail`、HTTP 状态依次降级。**不存在统一业务数字错误码表**；`code: access_blocked` 是明确存在的例外。

| HTTP | 典型含义 | 客户端处理 |
| --- | --- | --- |
| 200 | 正常结果；允许空列表、空歌词、`valid:false`、内部 `ok:false` | 检查具体业务字段 |
| 206 | 上游实际支持的局部音频响应 | 核对 Content-Range 后处理续传 |
| 400 | 参数/JSON/签名/consent/导入验证失败 | 修正请求；不要原样无限重试 |
| 401 | 管理会话失效或设备凭证缺失/无效 | 重新登录；设备流程先处理同意和凭证 |
| 403 | 设备/IP封禁、插件/音源禁用、同源/CSRF失败 | 停止当前操作；不自动绕过策略 |
| 404 | 路径不存在；内部接口对未授权隐藏 | 检查接口、方法及部署版本 |
| 410 | key 或 ticket 失效；key 查找还可能因音源禁用失败 | restore 或重新搜索/解析，避免无界循环 |
| 416 | Range 格式不支持 | 改用单范围；根据播放器/下载器策略重试 |
| 422 | 插件执行/初始化失败 | 查看 detail 或显式切换可用解析方式 |
| 429 | 并发/频率限制 | 指数退避加随机抖动；不要并发重试 |
| 502 | 上游/第三方失败、返回非音频或资源变化 | 保留真实错误，允许有限手动重试 |
| 503 | 策略、后台或运行时暂不可用；默认插件未设置 | 区分配置问题与暂时失败 |
| 504 | 原平台音频连接失败/超时分支 | 有限重试，不把 JSON 错误保存为音频 |

没有保证的 `Retry-After`。原平台上游 401/403/404/429 等通常被包装为外层 502，原值在 `upstream_status`，不可只看 HTTP 判断平台原因。

音频响应一旦发送头部，后续断流不能再转为 JSON 错误。HTTP 200 不等于文件完整；完整播放与下载完整性要由客户端核验。

## 5. 资源与缓存限制

以下为找到的源码默认值，按进程计算，并非公开吞吐 SLA。

| 项目 | 限制 |
| --- | --- |
| 普通网关慢请求 | 6 个并发槽；通常包括搜索、歌单、歌词、inspect、snapshot、原平台媒体 |
| 插件 probe/resolve/lyric/media | 共用 2 个并发槽，媒体流期间占槽 |
| 插件操作频率 | 每真实 IP 60 秒 24 次；probe/resolve/lyric 在同意校验后计数，media 不计入此窗口 |
| 轻量列表 | 2 并发槽；缓存 300 秒、最多 40 项 |
| 热榜 | QQ/网易云分别一个互斥加载锁；缓存 300 秒、最多 48 项 |
| 普通搜索 | 非空结果缓存 90 秒、最多 100 项；refresh=1 绕过，空结果不提供有效 TTL |
| 设备注册 | 同一 IP 每小时最多 5 次 |
| 活动观察 | 每实例/未识别IP约30秒聚合一次；队列1024，满时可丢弃，不阻塞播放 |
| 后台 | 12 个并发槽；导入另限 1 个任务 |
| 后台登录 | 每IP 15分钟8次，整体15分钟80次，失败尝试也占用 |
| 后台写入 | 每会话60秒80次 |
| 设备 POST body | 1..4096 字节；反向代理也有4KB层限制 |
| 登录 body | 1..4096 字节 |
| 后台普通 body | 1..1450000 字节；代理还有1500KB限制 |
| 内部统计 body | 最多65536字节 |
| LX `/run` body | 1..32768字节 |
| 歌词读取 | 通常最多131072字节；snapshot另读1字节检测超限 |

服务器搜索/元数据超时通常 12..22 秒；原平台音频连接 35 秒；LX 运行时请求 32 秒、工作进程 28 秒、第三方取流 15 秒。实际端到端可叠加，不应把其中某个值当固定 SLA。

## 6. 安全与兼容

- 网关仅接受服务器已知歌曲，插件媒体仅接受票据；没有公开任意 URL 代理接口。
- 歌单链接只提取受支持主机的 ID，不任意抓取用户 URL；短链不展开。
- 插件 HTTP 使用 SSRF 防护、公共地址校验及资源限制；启用脚本需要管理员确认信任。
- 管理统计包含真实 IP 等敏感运营数据，不应公开导出示例或备份。
- GET 插件 probe/resolve 会执行第三方请求；不要因其是 GET 就进行预取、爬取或浏览器自动探测。
- 多实例网关的进程内 key/ticket 需要共享存储或粘性路由，签名密钥也须一致。当前代码不提供这套协调机制。
- 接口接受新增字段；客户端应忽略未知字段，但不能把缺失字段填成已确认的热度、原唱、可播放或无损结论。
