# 接入、版本与维护

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

## 1. 推荐调用流程

### 搜索到播放

1. GET sources读取实时目录，处理空目录和禁用，保留id/backend区别。
2. 用目录id发搜索。普通查询用search；10条增量列表用watch/search，不能混用两套page偏移。
3. 保存Song.ref及展示字段，保留响应歌曲真实source。网易云排序信号仅与当前查询及policy一致时使用。
4. 播放既有收藏时先restore；新搜索可直接使用key。410后允许有限恢复，400坏签名需要重新搜索。
5. 当前受管解析策略：五个LX平台先sources读有效默认；非空则plugin_resolve使用稳定别名、consent=1、quality=128k；为空才走media。非LX平台用media。
6. 插件返回的相对地址必须是本站plugin_media及ticket，禁止外部URL、凭证、fragment或重定向。直接媒体同样只能官方路径。
7. 媒体请求带同一个已自愿注册的设备身份头，核验HTTP与Content-Type，交给播放器实际判断解码/播放。
8. 原平台lyric与插件lyric独立；需要保存原平台歌词快照时使用snapshot。不要自动创造逐字时间或声称解析源歌词完全一致。

默认配置读取失败或插件失败不等于“没有配置”。不要吞错误然后静默换平台/插件；让用户知道当前失败，按产品策略提供明确重试或选择。

### 下载与续传

先确定解析方式，取得原平台media或plugin_media，再由客户端创建下载任务。本项目没有公开“创建服务器下载任务”接口。

原平台可download=1获取附件名；插件媒体自行命名。保存前确认是音频而非JSON/HTML，续传检查206/Content-Range/资源标记，200时从头处理。完整性无法只依inspect的size字符串判断；断流、缺长度、票据过期或资源变化应报告，不把文件自动标为完整。

### 分页刷新

列表仅进入页面/明确操作时加载，遵守has_more。首页refresh=1在轻量/榜单模块有清理快照作用；后续页refresh不等于重建全源快照。切源/切查询需取消旧任务或通过generation防止迟到响应覆盖当前列表。

重复页或去重后空页不应无限加载；通用网易云最多20页、轻量网易云第20页停止；推荐最多100个快照条目，B站轻量最多20个视频元数据条目。

## 2. curl 示例

以下代码块采用POSIX shell续行。Windows PowerShell请使用`curl.exe`，将命令写为单行或换为PowerShell反引号续行；不要把PowerShell内置curl别名当成curl程序。示例会访问线上接口，**本次整理并未执行这些请求**。

### 读取目录/搜索/公共歌单

```sh
curl --fail-with-body 'https://wristhyme.tolike.cn/api/sources'

curl --fail-with-body --get 'https://wristhyme.tolike.cn/api/search' \
  --data-urlencode 'source=netease' \
  --data-urlencode 'q=示例歌曲' \
  --data-urlencode 'page=1'

curl --fail-with-body --get 'https://wristhyme.tolike.cn/api/watch/search' \
  --data-urlencode 'source=qq' \
  --data-urlencode 'q=示例歌曲' \
  --data-urlencode 'page=1' \
  --data-urlencode 'refresh=1'

curl --fail-with-body --get 'https://wristhyme.tolike.cn/api/playlist' \
  --data-urlencode 'link=https://music.163.com/#/playlist?id=123456789' \
  --data-urlencode 'page=1'

curl --fail-with-body --get 'https://wristhyme.tolike.cn/api/charts/hot' \
  --data-urlencode 'source=netease' \
  --data-urlencode 'page=1'
```

### 引用恢复与插件解析

`REF`/`KEY`由真实搜索响应读取，不能拿文档示例key尝试播放。

```sh
curl --fail-with-body --get 'https://wristhyme.tolike.cn/api/restore' \
  --data-urlencode "ref=$REF"

curl --fail-with-body --get 'https://wristhyme.tolike.cn/api/plugin_resolve' \
  --data-urlencode "key=$KEY" \
  --data-urlencode 'plugin=0000000000000000' \
  --data-urlencode 'consent=1' \
  --data-urlencode 'quality=128k'
```

仅在产品具有相应第三方服务同意/受管解析政策时发送consent=1；匿名统计consent是另一个独立功能。没有默认插件时上述解析返回503。不要用curl `-L`无条件跟随重定向传播身份头。

### 后台同源请求形状

登录body由本地安全输入提供，避免把密码写进命令历史。这里只展示结构，不自动创建账号或运行管理操作：

```http
POST /admin/api/login HTTP/1.1
Host: wristhyme.tolike.cn
Origin: https://wristhyme.tolike.cn
Content-Type: application/json
Content-Length: <UTF8_BODY_LENGTH>

{"username":"admin","password":"<ADMIN_PASSWORD>"}
```

随后一个设置默认插件请求：

```http
POST /admin/api/default-plugin HTTP/1.1
Host: wristhyme.tolike.cn
Origin: https://wristhyme.tolike.cn
Cookie: __Secure-wristhyme_admin=<SESSION_TOKEN>
X-CSRF-Token: <CSRF_FROM_LOGIN_OR_SESSION>
Content-Type: application/json
Content-Length: <UTF8_BODY_LENGTH>

{"id":"<ENABLED_PLUGIN_ID>"}
```

这是HTTP结构模板，不是字节级可直接发送报文；Content-Length由HTTP库根据UTF-8实际body设置。不要把后台Cookie用于/api/media或第三方请求。

## 3. JavaScript接入示例

示例可用于同源前端或支持fetch的Node环境；跨域浏览器还受CORS限制。只演示JSON/解析链路，不含完整播放器或下载续传。

```javascript
const ORIGIN = 'https://wristhyme.tolike.cn';
const LX_SOURCES = new Set(['netease', 'qq', 'kugou', 'kuwo', 'migu']);

async function getJSON(path, parameters = {}, signal) {
  const url = new URL(path, ORIGIN);
  if (url.origin !== ORIGIN || url.username || url.password ||
      url.hash || !url.pathname.startsWith('/api/')) {
    throw new Error('Invalid gateway URL');
  }
  for (const [key, value] of Object.entries(parameters)) {
    url.searchParams.set(key, String(value));
  }
  const response = await fetch(url, { redirect: 'error', signal });
  const result = await response.json();
  if (!response.ok) {
    const error = new Error(result.error || result.detail || `HTTP ${response.status}`);
    error.status = response.status;
    throw error;
  }
  return result;
}

async function mediaURL(savedSong, signal) {
  const song = savedSong.ref
    ? await getJSON('/api/restore', { ref: savedSong.ref }, signal)
    : savedSong;
  if (!song.key) throw new Error('Missing song key');
  if (LX_SOURCES.has(song.source)) {
    const catalog = await getJSON('/api/sources', {}, signal);
    if (catalog.default_plugin) {
      const resolved = await getJSON('/api/plugin_resolve', {
        key: song.key, plugin: '0000000000000000', consent: '1', quality: '128k'
      }, signal);
      if (typeof resolved.url !== 'string' ||
          !resolved.url.startsWith('/api/plugin_media?ticket=')) {
        throw new Error('Invalid plugin media path');
      }
      const media = new URL(resolved.url, ORIGIN);
      if (media.origin !== ORIGIN || media.username || media.password ||
          media.hash || media.pathname !== '/api/plugin_media' ||
          !media.searchParams.get('ticket')) {
        throw new Error('Invalid plugin media URL');
      }
      return media.href;
    }
  }
  const media = new URL('/api/media', ORIGIN);
  media.searchParams.set('key', song.key);
  return media.href;
}
```

生产客户端还应限制JSON响应大小、设置超时、持久化ref、清理已过期票据、保护设备头并校验实际音频。本站禁用检查由网关执行；客户端不要用旧目录绕过服务端错误。

## 4. 版本差异

| 证据/阶段 | 协议变化或限制 |
| --- | --- |
| server-next历史网关 | 固定来源目录、搜索最多60、原平台媒体、LX基础合同；不能代表最新后台策略 |
| search221-signals | 网易云独立search_signals、当前页排序，信号不进key/ref |
| admin219及followup | 动态来源、启停/封禁、匿名统计、Cookie/CSRF后台；用户撤回标记及基础平台过滤 |
| restore-default225 | 默认真实ID与有效ID、稳定别名、禁用回退；隐藏新增/导入UI但保留API |
| netease-page20 | 通用网易云搜索上游单次60改20，不只是客户端截断 |
| watch220-light/backend | 新增watch/search、recommend、playlist；10项分页，网易云helper支持10/20 |
| desktop-baseline221/new-backend | 独立charts/hot路径、QQ/网易云每页10；不能误当推荐歌单 |
| android当前2.2.2 | 手机/手表受管默认解析、自动发送解析consent=1、去掉手动插件设置；本地私人库仍本地保存 |
| wristhyme-ios及桌面测试版 | 仍有各自手动插件/同意流程；不能假定采用Android2.2.2全部策略 |

老客户端硬编码来源可能不反映禁用目录；服务端仍可拒绝。排序支持也因客户端版本而异，服务端返回次序不保证旧客户端不二次重排。

独立包已只读取得包含动态策略、轻量分发和热榜的现有服务器源码，提取哈希见PROVENANCE.json；包装修改与历史合同分别记录，不再要求外部补丁目录。

## 5. 源码索引

所有链接相对文档目录，ZIP解压后可直接查看。原服务器来源及提取前哈希见PROVENANCE.json。

| 内容 | 来源 |
| --- | --- |
| 通用网关、key/ref、歌词、媒体及分发 | [server.py](../../gateway/server.py) |
| 轻量列表 | [watch_browse.py](../../gateway/watch_browse.py) |
| 网易云10/20参数合同 | [netease_catalog.py](../../gateway/netease_catalog.py) |
| 搜索信号 | [netease_signals.py](../../gateway/netease_signals.py) |
| 去重元数据 | [catalog_dedup.py](../../gateway/catalog_dedup.py) |
| 插件及默认别名 | [plugin_routes.py](../../gateway/plugin_routes.py) |
| 媒体验证 | [media_validation.py](../../gateway/media_validation.py) |
| 歌词规范化 | [lyric_normalize.py](../../gateway/lyric_normalize.py) |
| 热榜 | [hot_charts.py](../../gateway/hot_charts.py) |
| 设备策略与动态来源 | [admin_gateway.py](../../gateway/admin_gateway.py) |
| 后台HTTP | [admin_server.py](../../admin/admin_server.py) |
| 后台存储与默认配置 | [admin_store.py](../../admin/admin_store.py) |
| 脚本导入 | [admin_import.py](../../admin/admin_import.py) |
| 插件注册表 | [plugin_registry.py](../../lx-runtime/plugin_registry.py) |
| 运行时 | [runtime.py](../../lx-runtime/runtime.py) |
| 工作进程 | [worker.py](../../lx-runtime/worker.py) |
| 反向代理 | [Caddyfile](../../deploy/Caddyfile) |
| 上游注册路由 | [internal/web目录](../../vendor/go-music-dl/internal/web) |

原工作区历史STATUS/客户端证据仅用于版本比较，不随本包分发；本包运行版本测试另见VERIFICATION.md，不替代生产音乐在线检查。

## 6. 文档验证

工作区提供仅离线的文档检查：

```powershell
node .\docs\api\verify-docs.cjs
```

检查本地Markdown链接、JSON示例语法、代码块闭合、公开/管理/内部路由的文档覆盖，并解析JavaScript示例语法。它不访问网络、不运行部署脚本、不登录后台、不验证所有业务行为。

后续后端变更时应同步参数表、错误分支、模型与来源索引；若需要线上验收，应另行明确测试环境和允许范围。只读搜索也会触发上游联网；plugin_probe会执行脚本；register/forget和后台POST会改变状态，不能当作无副作用探测。

**建议维护验证项：**动态目录为空/自定义别名/禁用、key淘汰与ref恢复、默认回退与无默认、两套分页边界、empty与error区别、consent/quality校验、旧票据禁用、Range200/206/416/非音频、统计撤回与封禁例外、登录CSRF/会话过期/改密码、GitHub固定commit及导入哈希。
