---
url: /guide/server.md
---
# Server 模式&#x20;

`yutto serve` 会启动一个只监听本机回环地址的 WebSocket server，供 WebUI、桌面应用或其他本地前端通过 JSON-RPC 2.0 调用。

## 启动

```bash
yutto serve --token-file ~/.config/yutto/server.token
```

默认监听 `ws://127.0.0.1:11223`。Token 依次从以下位置获取：

1. `YUTTO_SERVER_TOKEN` 环境变量；
2. `--token-file` 指定的文件；
3. 都未提供时，生成仅本次运行有效的随机 Token 并显示在终端。

若 `--token-file` 指向的文件尚不存在，yutto 会在首次启动时生成随机 Token 并以 `0600` 权限创建该文件，后续启动直接复用。在 POSIX 系统上，已有 Token 文件必须是权限不高于 `0600` 的普通文件，符号链接会被拒绝。

server 只接受回环地址。浏览器连接还需要用 `--allow-origin` 显式允许完整 Origin：

```bash
yutto serve \
  --token-file ~/.config/yutto/server.token \
  --allow-origin http://127.0.0.1:3000
```

若 FFmpeg 不在 `PATH` 中，可用 `--ffmpeg-path` 指定可执行文件的完整路径（默认按名从 `PATH` 解析）；server 进程内的下载合并会复用该路径：

```bash
yutto serve --token-file ~/.config/yutto/server.token --ffmpeg-path /opt/ffmpeg/ffmpeg
```

## 调用流程

连接建立后，第一条消息必须调用 `server.authenticate`。Token 不放在 URL 中。

```json
{ "jsonrpc": "2.0", "id": 1, "method": "server.authenticate", "params": { "token": "YOUR_TOKEN" } }
```

认证成功后，可以提交下载任务：

```json
{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "download.start",
  "params": {
    "request": {
      "source": { "url": "https://www.bilibili.com/video/BV..." },
      "output": { "directory": "番剧" }
    }
  }
}
```

`request` 使用与 yutto Core 相同的强类型请求模型。未提供的设置继承 server 启动时加载的 yutto 配置；认证 Cookie 不会放入请求，而是由 server 按 `access.auth_profile` 从 `--auth-file` 读取。配置里的 inline Cookie 与旧 `sessdata` 字段不会进入 server 请求边界。

可用方法如下：

| 方法               | 用途                                                         |
| ------------------ | ------------------------------------------------------------ |
| `server.info`      | 查询版本、协议版本和能力列表                                 |
| `download.start`   | 提交下载任务，返回任务快照                                   |
| `resolve.start`    | 提交解析任务，仅列出条目不下载（见「解析任务」一节）         |
| `task.get`         | 查询一个任务                                                 |
| `task.list`        | 分页查询当前进程中的任务摘要（包含 URL，不展开请求 payload） |
| `task.cancel`      | 取消排队中或运行中的任务                                     |
| `task.subscribe`   | 获取事件回放并订阅后续事件                                   |
| `task.unsubscribe` | 取消当前连接上的任务订阅                                     |

订阅后的实时事件以 `task.event` notification 推送。事件包含全局递增的 `seq`（download 与 resolve 任务共享同一序号空间）；从回放切换到实时流时，应按 `seq` 去重。任务快照（`download.start` / `resolve.start` / `task.get` / `task.list` 的返回值）均携带 `kind` 字段（`download` 或 `resolve`）标识任务类别。断开客户端不会自动取消任务。

下载完成后，`task.get` 的 `result` 会列出每个条目的目标路径和最终产物：

```json
{
  "items": [
    {
      "state": "done",
      "output_path": "/downloads/番剧/第 1 话.mp4",
      "skip_reason": null,
      "artifacts": [
        { "kind": "subtitle", "path": "/downloads/番剧/第 1 话.zh-CN.srt" },
        { "kind": "media", "path": "/downloads/番剧/第 1 话.mp4" }
      ]
    }
  ]
}
```

`state` 为 `done` 或 `skipped`；跳过原因目前包括 `already_exists` 和 `no_media_stream`。仅请求字幕、弹幕、描述文件或封面时，只要资源处理完成，条目同样为 `done`。`output_path` 是推导出的媒体目标路径，在未请求媒体或没有可用媒体流时不一定存在。

`artifacts` 包含本次生成或确认已存在的最终文件，不包含下载分片、临时封面或 FFmpeg 章节文件。`kind` 可能为 `media`、`subtitle`、`danmaku`、`metadata` 或 `cover`；客户端应按 `kind` 识别产物，不依赖数组顺序。一个 `skipped` 条目仍可能包含已经写入的 sidecar 产物。

除 runtime 自己产生的 `state` 外，下载事件的 `kind` 包括：

| `kind`             | `data`                                                     | 含义                                       |
| ------------------ | ---------------------------------------------------------- | ------------------------------------------ |
| `stage`            | `{ name, item? }`                                          | 进入解析、资源写入、下载或后处理阶段       |
| `progress`         | `{ phase, current, total, speed_per_second, unit, item? }` | 音视频字节下载进度                         |
| `media_selected`   | `{ item, video, audio }`                                   | 下载计划最终选中的音视频流                 |
| `item_skipped`     | `{ item, reason }`                                         | 条目因媒体已存在或没有请求到可用媒体流跳过 |
| `artifact_created` | `{ item, path }`                                           | 本次任务生成了最终媒体文件                 |

`stage.name` 目前可能为 `resolving`、`preparing`、`writing_resources`、`downloading` 或 `postprocessing`。`media_selected` 在下载计划确定后、资源写入与媒体传输前发送；`video` 包含 `codec` / `quality` / `width` / `height` / `save_codec`，`audio` 包含 `codec` / `quality` / `save_codec`，没有选择对应轨道时为 `null`。其中 `quality` 是原始质量编号，`save_codec` 是计划保存时使用的编码。事件只包含最终选择，不包含候选流、播放 URL、镜像 URL 或认证信息。`artifact_created` 只在本次新生成最终媒体文件时发送；其他 sidecar 产物通过完成结果获取。

## 解析任务

`resolve.start` 把「解析」作为独立任务提交（仅当 `server.info` 能力列表包含它时可用）：在不触发任何下载的前提下列出条目的稳定信息，供前端渲染选择器，选中后再逐条 `download.start`。

```json
{
  "jsonrpc": "2.0",
  "id": 3,
  "method": "resolve.start",
  "params": {
    "request": {
      "source": { "url": "https://www.bilibili.com/video/BV..." },
      "scope": { "batch": true }
    }
  }
}
```

`request` 与 `download.start` 使用完全相同的请求模型。解析任务运行在独立的单 worker runtime 中，轻量的列表解析不会排在长下载任务之后；`task.get` / `task.list` / `task.cancel` / `task.subscribe` 对两类任务统一路由。

解析过程中每列出一个条目会推送一条 `item_listed` 事件。对合集 / 收藏夹 / 视频列表 / 空间投稿 / 稍后再看这类列表型批量来源，事件在解析进行中就按各视频完成的先后逐条推送（长列表边解析边出现，不必等整批完成），因此事件的到达顺序可能与最终顺序不同；每个条目恰好推送一次。任务完成后 `result.items` 仍按来源的原始顺序包含全部条目，需要稳定顺序的客户端应以 `result.items` 为准。条目与 `item_listed` 事件的 `data` 携带相同字段：

* `avid` / `cid`：条目标识；
* `url`：单集原子 URL，可直接用于后续 `download.start`；
* `name` / `title`：分集名与视频标题；
* `uploader` / `description` / `tags`：UP 主、简介与标签，listing 元数据缺失时为空；
* `cover_url`：封面 URL；
* `pubdate`：发布时间（Unix 秒时间戳），未知时为 `0`；
* `duration`：视频时长（秒），未知时为 `0`；
* `planned_path`：按模板推导的计划路径（POSIX 风格），实际下载时可能因去重而调整；
* `display_group`：批量解析时的分组名，无分组时为 `null`。

两条输出都来自同一个不可变 listing snapshot；对于 PGC 条目，展示字段与下载期描述文件使用同一份 season / episode 元数据投影。`planned_path` 固定记录 listing 时的计划路径，下载期去重不会回写它。snapshot 中的 `url` 是稳定的条目页面 URL，不包含易过期的音视频资源 URL、资源 bytes 或认证信息。

错误语义：解析过程中预期内的失败（视频不存在 / 无访问权限 / 请求重试耗尽等）会以结构化形式保留 —— 任务完成时 `result.failures` 逐项列出 `type` / `message` / `code`（与任务级错误同构，`code` 来自 yutto 的稳定错误码表）。没有任何条目解析成功且存在失败时，任务以 `failed` 结束且错误码保持稳定（单一失败即原始失败的错误码，多个失败聚合为 `RESOLVE_FAILED_ERROR`），不会伪装成空成功；来源本就为空、或条目全部被过滤（如发布时间过滤）时，任务以 `completed` 返回空 `items` 且 `failures` 为空；部分失败时任务为 `completed`，成功条目在 `items`、失败记录在 `failures`。

## 本地资源边界

* 同一 server 进程默认一次只运行一个下载任务；可用 `-j/--jobs` 设置同时运行的下载任务数。
* 指向相同最终文件或临时文件命名空间的任务即使并发提交也会串行执行，避免覆盖彼此的下载产物。
* 解析任务运行在独立的单 worker runtime 中，一次只运行一个解析，但不会排在下载任务之后。
* 请求中的 `output.directory` 和 `output.temporary_directory` 必须是相对路径，并分别限制在 `--download-root` 与 `--tmp-root` 内。
* `output.subpath_template` 不能使用绝对路径或 `..` 逃逸根目录。
* `--max-fetch-workers` 与 `--max-download-workers` 限制单任务并发数。
* 分块大小限制在 64 KiB 至 64 MiB，避免单任务创建过量分块。
* `--task-limit` 限制排队任务与近期任务记录的总量（download 与 resolve 任务合并计算）；达到上限时优先淘汰全局最早的已结束任务。
* 任务事件仅保留有界的近期回放；`truncated: true` 表示更早的事件已经被丢弃。

完整启动选项可通过 `yutto serve -h` 查看。
