API 文档
面向网页、脚本、客户端与自动更新器的公开下载接口说明。
下载接入方式
你可以按实际场景选择下载接入方式。如果你是在网页、官网、论坛、公告页或前端页面里放下载按钮,推荐跳转主站验证页;如果你是在脚本、客户端、CI、自动更新器或后端程序里自动下载文件,使用程序 API 链路。
接入前先看:稳定性与通用约定
本文明确列出的接口才属于面向第三方开发者的稳定接入契约。新客户端应优先使用项目/资产查询接口与下载授权协议 V2;未在本文列出的公开可访问端点,不应被视为长期兼容承诺。
稳定开发者接口
GET /api/public/v1/projects
GET /api/public/v1/projects/{project_id}/assets
POST /api/public/v2/api/challenges
POST /api/public/v2/api/authorizations
GET /api/public/v2/authorizations/{authorization_id}
GET /api/public/v1/blocklist.txt
GET /api/public/v1/blocklist.json
GET /api/public/v1/changelog
POST /api/developer/v1/projects/{project_id}/sync
注意:本文所说的“下载授权协议 V1”只指 /api/public/v1/api/challenges 与 /api/public/v1/api/authorizations 这两个旧版 PoW/授权接口,不代表整个 /api/public/v1/ 命名空间被弃用。项目、资产、封禁列表和更新日志等 V1 资源接口仍是当前稳定接口。
站点内部公共端点
/api/public/v1/catalog、/api/public/v1/stats、/api/public/v1/stats/details、/api/public/v2/web/* 与网页验证相关接口用于本站前端或内部展示。它们虽然可以从公网访问,但当前不作为第三方稳定 API 契约,字段和行为可能随前端实现调整。第三方程序不要依赖这些接口。
JSON 响应格式
除 TXT 封禁订阅、下载节点文件响应和上面明确标记为站点内部的数据端点外,本文中的 JSON API 使用统一 envelope。成功时读取 data;失败时根据 HTTP 状态码与稳定的 code 分支处理,并保留 request_id 用于排障。
{
"status": "success",
"message": "...",
"request_id": "req_...",
"data": {}
}
{
"status": "error",
"message": "...",
"request_id": "req_...",
"code": "STABLE_ERROR_CODE"
}
POST 接口发送 JSON 请求体。建议显式设置 Content-Type: application/json;公开下载 API 的 JSON 请求体上限为 16 KiB。
限流、重试与错误处理
公共接口受可配置的来源级资源限流与下载风控约束。收到 429 或 503 时,如果响应带 Retry-After,应按该秒数退避后重试,不要固定高频轮询。Developer API 另有 X-RateLimit-Limit、X-RateLimit-Remaining、X-RateLimit-Reset。
| HTTP | code | 描述 |
|---|---|---|
| 401 | DOWNLOAD_TOKEN_INVALID | 下载令牌无效、过期或不属于当前授权查询来源。 |
| 403 | CHALLENGE_FAILED / CLIENT_BLOCKED | 挑战不匹配、校验失败,或当前来源已被限制。 |
| 409 | NO_ROUTABLE_NODE / CHALLENGE_IN_PROGRESS | 当前没有可用节点,或同一挑战仍在处理。 |
| 410 | API_VERSION_RETIRED | 旧版下载授权协议 V1 已在当前部署中关闭。 |
| 429 | PUBLIC_RESOURCE_RATE_LIMITED / CLIENT_RATE_LIMITED / CHALLENGE_RATE_LIMITED / REQUEST_QUOTA_EXHAUSTED / TRAFFIC_LIMIT_EXCEEDED | 请求、挑战或流量额度受到限制;优先遵循 Retry-After。 |
| 503 | CHALLENGE_CAPACITY_REACHED / VDF_BUSY | 挑战服务暂时繁忙;通常会返回 Retry-After。 |
| 500 | PUBLIC_INTERNAL_ERROR | 服务端内部错误;记录 request_id 后重试或反馈。 |
客户端来源绑定
下载授权协议的 challenge 创建与 authorization 提交必须从同一个被服务端识别的客户端网络前缀完成;授权状态查询也要求与签发时的来源前缀一致。因此不要让不同出口 IP 的机器分别完成挑战创建、solution 提交和状态查询。授权成功后,download_token 是 Bearer 凭据;实际访问下载节点时允许出口发生变化,因此必须像密码一样保护该令牌。
方式一:跳转主站验证页下载
这种方式适合网页下载按钮。你不需要自己处理挑战、PoW、下载令牌,也不需要关心当前由哪个下载节点提供文件。
需要特别注意:这里要跳转的是主站地址,不是下载节点地址。外部前端应该把用户带到主站的验证页面,由主站完成验证、授权和节点选择,最后再跳转到真实下载节点开始下载。
/{project_id}/{version}/{file_name}验证页地址示例
https://fyhub.cn/fcl/1.3.0.9/FCL-release-1.3.0.9-arm64-v8a.apk
这里的 https://fyhub.cn 就是枫源镜像主站公共入口,不是某个下载节点的 public_download_base_url。
如果不知道项目是哪一个,可以先查询项目列表
这个接口可以用来获取 project_id、项目展示名和项目可用状态。前端一般只需要在初始化下载页、项目选择器或外部下载列表时调用它。
/api/public/v1/projectsExample Request
GET /api/public/v1/projects
/api/public/v1/projects/{project_id}/assets拿到 project_id 后,前端可以查询该项目下有哪些版本和文件。返回结果里会包含 version、file_name、architecture、system、size_bytes、digest_sha256 和 available 等字段。
Example Request
GET /api/public/v1/projects/example/assets
Example Response
{
"status": "success",
"data": {
"assets": [
{
"asset_id": "asset_123",
"version": "v1.2.3",
"file_name": "example-windows-amd64.zip",
"architecture": "amd64",
"system": "win",
"available": true
}
]
}
}
前端可以根据这些字段生成下载按钮。用户点击按钮时,跳转到主站验证页:
https://fyhub.cn/example/v1.2.3/example-windows-amd64.zip
- 展示下载验证页面。
- 在浏览器中完成下载挑战计算。
- 向主节点领取短时下载授权。
- 选择可用下载节点。
- 跳转到真实下载地址开始下载。
外部网站不要直接拼接节点下载地址,也不要调用程序下载用的 /api/public/v2/api/* 接口来替代这个流程。
方式二:程序调用 API 下载
这种方式适合命令行工具、自动更新器、CI 脚本、下载器或后端服务。程序需要自己查询资产、完成 API PoW 验证,然后携带下载令牌访问下载节点。
第一步:查询项目列表
/api/public/v1/projects查询当前已启用的项目列表并取得 project_id。项目即使暂时没有可路由下载资产也可能出现在结果中,应以 available 判断当前是否可下载。
| 参数 | 类型 | 描述 |
|---|---|---|
| 无 | ||
Example Request
GET /api/public/v1/projects
Example Response
{
"status": "success",
"message": "查询成功",
"request_id": "req_...",
"data": {
"projects": [
{
"project_id": "example",
"repository": "owner/example",
"display_name": "示例项目",
"description": "示例说明",
"homepage_url": "https://fyhub.cn",
"available": true
}
]
}
}
第二步:查询项目资产
/api/public/v1/projects/{project_id}/assets程序应选择一个 available=true 的资产,并记录它的 asset_id。如果 available=false,表示当前没有可用下载节点持有这个文件,程序应该稍后重试。
| 参数 | 类型 | 描述 |
|---|---|---|
| project_id | Path | 项目标识 |
Example Request
GET /api/public/v1/projects/example/assets
Example Response
{
"status": "success",
"data": {
"assets": [
{
"asset_id": "asset_123",
"version": "v1.2.3",
"download_path": "/example/v1.2.3/example.zip",
"prerelease": false,
"file_name": "example.zip",
"architecture": "amd64",
"system": "win",
"size_bytes": 10485760,
"digest_sha256": "0123456789abcdef...",
"available": true,
"unavailable_reason": ""
}
]
}
}
第三步:创建 API V2 顺序工作量挑战
旧版“下载授权协议 V1”弃用提醒:仅 /api/public/v1/api/challenges 与 /api/public/v1/api/authorizations 这两个旧 PoW/授权接口使用 SHA-256 前导零 nonce 搜索,并且当前部署可以直接关闭它们并返回 410 API_VERSION_RETIRED。这里不代表 /api/public/v1/ 下的项目、资产、封禁列表和更新日志接口被弃用。新客户端必须直接实现下面的 V2 repeated-squaring 流程,不能只替换接口路径。
/api/public/v2/api/challenges为指定资产创建 3072 位 RSA repeated-squaring 挑战。响应中的 modulus 和 base 是 384 字节无符号大端整数的无填充 base64url 编码。
| 参数 | 类型 | 描述 |
|---|---|---|
| asset_id | JSON | 要下载的资产标识 |
Example Request
POST /api/public/v2/api/challenges
{"asset_id":"asset_123"}
Example Response
{
"status": "success",
"data": {
"challenge_id": "challenge_123",
"asset_id": "asset_123",
"algorithm": "rsa-repeated-squaring-v1",
"modulus_id": "模数标识",
"modulus": "512 字符 base64url 整数",
"base": "512 字符 base64url 整数",
"iterations": 96000,
"encoding": "base64url-uint-be-384",
"expires_at": "2026-09-03T12:05:00Z"
}
}
第四步:顺序计算 solution
从 y = base 开始,严格执行 iterations 次 y = y² mod modulus,再把 y 编码为 384 字节定长大端 base64url。不得提交十进制、十六进制或可变长整数。
展开了解 RSA repeated-squaring 的计算原理
这一步计算的是顺序工作量证明,不是普通密码哈希。挑战给出 RSA 模数 N、起始值 base 和最终迭代数 iterations;客户端只能按顺序使用前一次结果继续模平方。
计算过程
y = decode_unsigned_big_endian(base)
重复 iterations 次:
y = (y × y) mod N
solution = base64url_no_padding(unsigned_big_endian_384(y))
modulus、base 和 solution 都必须是恰好 384 字节的无符号大端整数,再编码成无填充 base64url,因此线上字符串长度固定为 512。即使结果前面是零,也不能删掉前导零字节。
为什么必须顺序执行
第 i+1 次平方依赖第 i 次的完整结果。数学上最终值等于 base^(2^iterations) mod N,但客户端不知道 RSA 模数的陷门,不能把指数按欧拉函数化简;直接构造 2^iterations 也不能绕过这些依赖。多线程拆分不同区间后无法独立合并,所以应使用单条顺序循环。
服务端如何确认结果
主节点持有只存在于内存中的 RSA 陷门,可以快速得到同一最终值,并在创建挑战时保存定长结果的摘要。授权时服务端先检查挑战版本、算法、来源、资产、客户端前缀、有效期和一次性状态,再校验 0 < solution < N 以及结果摘要。RSA 陷门和预期答案不会发送给客户端或写入 PoW 遥测。
实现时最容易出错的地方
- 循环次数必须正好等于响应里的最终 iterations,不能使用本地默认值。
- 每轮都必须先平方再对 N 取模,不能改成哈希、乘以 base 或并行 nonce 搜索。
- 解码和编码都使用无符号大端;输出必须左侧补零到 384 字节。
- base64url 使用 -、_ 且不带 = padding。
- 挑战有有效期并绑定资产和客户端来源;失败后应重新创建挑战,不要跨资产或跨 API 版本复用。
# Mirror Server API V2 repeated-squaring 原理
## 用途
程序下载在调用 `POST /api/public/v2/api/challenges` 后,需要根据响应计算 `solution`,再提交到 `POST /api/public/v2/api/authorizations`。这是顺序工作量证明,不是 SHA 哈希 nonce 搜索。
## 挑战输入
- `algorithm` 必须是 `rsa-repeated-squaring-v1`。
- `encoding` 必须是 `base64url-uint-be-384`。
- `modulus`:RSA 模数 N,384 字节无符号大端整数的无填充 base64url,字符串长度 512。
- `base`:起始值,编码规则与 modulus 相同。
- `iterations`:服务端给出的最终迭代数,已经包含文件大小分档和风控倍率。
- `challenge_id` 和 `asset_id` 必须原样用于后续授权请求。
## 顺序算法
```text
N = decode_base64url_unsigned_big_endian_384(modulus)
y = decode_base64url_unsigned_big_endian_384(base)
repeat exactly iterations times:
y = (y * y) mod N
solution_bytes = unsigned_big_endian(y, exactly 384 bytes, left padded with zeroes)
solution = base64url_without_padding(solution_bytes)
```
最终值在数学上是 `base^(2^iterations) mod N`。但客户端不知道 RSA 模数的陷门,无法化简指数;每轮又依赖前一轮结果,因此不能把迭代区间拆给多个线程后再合并。正确实现是一条顺序模平方循环。
## 编码边界
1. modulus、base、solution 在线上都固定为 384 字节、512 个 base64url 字符。
2. 整数按无符号大端解释,不能使用十进制、十六进制或可变长字节串。
3. solution 前导零必须保留到 384 字节。
4. base64url 使用 `-` 和 `_`,不得包含 `=` padding。
5. solution 必须满足 `0 < solution < N`。
## 授权请求
```json
{
"challenge_id": "挑战响应中的 challenge_id",
"asset_id": "创建挑战时使用的 asset_id",
"solution": "512 字符定长 base64url 结果"
}
```
挑战会绑定 API V2、算法、资产、客户端网络前缀和有效期,并且成功授权后只能消费一次。不要跨资产、跨 Web/API 或跨 V1/V2 复用挑战;挑战过期或失败后应重新创建。
## 服务端校验原理
主节点持有仅存在于内存中的 RSA 陷门,可以快速计算相同最终值。创建挑战时保存的是定长预期结果的摘要;授权时先检查挑战绑定关系和生命周期,再检查 solution 的定长编码、数值范围和摘要。RSA 陷门、预期答案和 solution 都不会写入独立 PoW 遥测日志。
第五步:提交 solution 并领取下载授权
/api/public/v2/api/authorizations提交顺序工作量结果并领取短时、单节点绑定的下载授权。challenge_id、asset_id 与 solution 都必须来自同一条 V2 挑战链路。程序 API 当前没有需要客户端上报的 telemetry 字段。
| 参数 | 类型 | 描述 |
|---|---|---|
| challenge_id | JSON | 挑战标识 |
| asset_id | JSON | 资产标识 |
| solution | JSON | 512 字符的定长 base64url repeated-squaring 结果 |
Example Request
POST /api/public/v2/api/authorizations
{"challenge_id":"challenge_123","asset_id":"asset_123","solution":"512 字符 base64url 整数"}
Example Response
{
"status": "success",
"data": {
"authorization_id": "auth_123",
"download_url": "由服务端返回的实际下载节点 URL",
"download_token": "43 字符短时随机令牌",
"expires_at": "2026-09-03T12:05:00Z",
"range_concurrency_limit": 32,
"max_bytes": 20971520
}
}
第六步:请求下载节点
程序应直接访问授权响应里的 download_url,并通过请求头携带 download_token。download_url 通常指向下载节点;授权成功后实际文件请求允许从与挑战阶段不同的出口访问,但令牌属于 Bearer 凭据,泄露后可能被他人使用。客户端还应遵守响应中的 range_concurrency_limit 与 max_bytes。
{download_url}Example Request
GET {download_url}
Authorization: Bearer <download_token>
如果需要断点续传,可以使用单段 Range:
GET {download_url}
Authorization: Bearer <download_token>
Range: bytes=1048576-2097151
其他接口
下面这些接口不是程序下载流程的步骤,只用于订阅、状态查询或排障。
封禁列表订阅
/api/public/v1/blocklist.txt/api/public/v1/blocklist.json返回当前生效的公共下载封禁列表。内容包含 quota.yaml 静态黑名单和本站手动/自动封禁记录,不包含远程订阅源快照,响应在服务端缓存 60 秒。
TXT Example Response
# [枫源镜像封禁] 封禁原因: traffic_limit_exceeded, 来源: local_auto_ban, 封禁后尝试次数: 3
192.0.2.123
JSON Example Response
{
"status": "success",
"data": {
"blocks": [
{"entry": "192.0.2.123", "reason": "traffic_limit_exceeded", "attempts_after_block": 3, "blocked_at": "2026-06-21T12:00:00Z"}
]
}
}
项目开发者更新检测
/api/developer/v1/projects/{project_id}/sync供镜像项目所有者在发布 GitHub Release 后主动触发一次该项目的更新检测。Developer API Token 与单个项目绑定,只能触发对应项目;Token 可在管理后台项目卡片中生成或重置。
| 参数 | 类型 | 描述 |
|---|---|---|
| project_id | Path | 项目标识 |
| Authorization | Header | Bearer 项目 Developer API Token |
Example Request
POST /api/developer/v1/projects/example/sync
Authorization: Bearer <project_token>
默认每个项目每天允许 100 次有效触发。响应会返回 X-RateLimit-Limit、X-RateLimit-Remaining 和 X-RateLimit-Reset;额度耗尽时返回 429。
正常接受请求时返回 202。若同一项目已有扫描正在执行,同样返回 202,但不会启动第二次扫描,也不会再次消耗每日额度。
Example Response
{
"status": "success",
"message": "更新检测任务已接受",
"data": {"project_id": "example", "accepted": true, "already_running": false}
}
常见状态码:401 表示 Token 缺失或无效;403 表示 Token 与项目不匹配;404 表示项目不存在;409 表示项目已禁用;429 表示每日额度耗尽或认证失败次数过多。
更新日志
/api/public/v1/changelog按时间倒序查询本站更新记录。minimum_level 可选 info、notice、warn 或 critical;q 搜索标题和可见描述;limit 默认 20、最大 50。存在下一批时响应返回不透明的 next_cursor。
Example Request
GET /api/public/v1/changelog?minimum_level=notice&q=下载&limit=20
/api/public/v2/authorizations/{authorization_id}携带对应 download_token 查询 V2 授权状态、过期时间、公开节点名和已入账真实发送字节。旧的 /api/public/v1/authorizations/{authorization_id} 目前仅作为兼容别名保留,新客户端不要使用它。
| 参数 | 类型 | 描述 |
|---|---|---|
| authorization_id | Path | 授权标识 |
| Authorization | Header | Bearer <download_token> |
node_name 是新的明确字段;node_id 为兼容旧客户端暂时保留,目前同样返回公开节点名称,并不暴露内部节点 ID。新客户端请读取 node_name。
Example Request
GET /api/public/v2/authorizations/auth_123
Authorization: Bearer <download_token>
Example Response
{
"status": "success",
"data": {
"authorization_id": "auth_123",
"asset_id": "asset_123",
"node_name": "public-node-name",
"node_id": "public-node-name",
"state": "issued",
"expires_at": "2026-09-03T12:05:00Z",
"bytes_accounting_enabled": true,
"sent_bytes": 1048576,
"first_transfer_at": "2026-09-03T12:01:10Z"
}
}