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。

HTTPcode描述
401DOWNLOAD_TOKEN_INVALID下载令牌无效、过期或不属于当前授权查询来源。
403CHALLENGE_FAILED / CLIENT_BLOCKED挑战不匹配、校验失败,或当前来源已被限制。
409NO_ROUTABLE_NODE / CHALLENGE_IN_PROGRESS当前没有可用节点,或同一挑战仍在处理。
410API_VERSION_RETIRED旧版下载授权协议 V1 已在当前部署中关闭。
429PUBLIC_RESOURCE_RATE_LIMITED / CLIENT_RATE_LIMITED / CHALLENGE_RATE_LIMITED / REQUEST_QUOTA_EXHAUSTED / TRAFFIC_LIMIT_EXCEEDED请求、挑战或流量额度受到限制;优先遵循 Retry-After。
503CHALLENGE_CAPACITY_REACHED / VDF_BUSY挑战服务暂时繁忙;通常会返回 Retry-After。
500PUBLIC_INTERNAL_ERROR服务端内部错误;记录 request_id 后重试或反馈。

客户端来源绑定

下载授权协议的 challenge 创建与 authorization 提交必须从同一个被服务端识别的客户端网络前缀完成;授权状态查询也要求与签发时的来源前缀一致。因此不要让不同出口 IP 的机器分别完成挑战创建、solution 提交和状态查询。授权成功后,download_token 是 Bearer 凭据;实际访问下载节点时允许出口发生变化,因此必须像密码一样保护该令牌。

方式一:跳转主站验证页下载

这种方式适合网页下载按钮。你不需要自己处理挑战、PoW、下载令牌,也不需要关心当前由哪个下载节点提供文件。

需要特别注意:这里要跳转的是主站地址,不是下载节点地址。外部前端应该把用户带到主站的验证页面,由主站完成验证、授权和节点选择,最后再跳转到真实下载节点开始下载。

GET/{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、项目展示名和项目可用状态。前端一般只需要在初始化下载页、项目选择器或外部下载列表时调用它。

GET/api/public/v1/projects

Example Request

GET /api/public/v1/projects
GET/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
  1. 展示下载验证页面。
  2. 在浏览器中完成下载挑战计算。
  3. 向主节点领取短时下载授权。
  4. 选择可用下载节点。
  5. 跳转到真实下载地址开始下载。

外部网站不要直接拼接节点下载地址,也不要调用程序下载用的 /api/public/v2/api/* 接口来替代这个流程。

方式二:程序调用 API 下载

这种方式适合命令行工具、自动更新器、CI 脚本、下载器或后端服务。程序需要自己查询资产、完成 API PoW 验证,然后携带下载令牌访问下载节点。

第一步:查询项目列表

GET/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
      }
    ]
  }
}

第二步:查询项目资产

GET/api/public/v1/projects/{project_id}/assets

程序应选择一个 available=true 的资产,并记录它的 asset_id。如果 available=false,表示当前没有可用下载节点持有这个文件,程序应该稍后重试。

参数类型描述
project_idPath项目标识

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 顺序工作量挑战

POST/api/public/v2/api/challenges

为指定资产创建 3072 位 RSA repeated-squaring 挑战。响应中的 modulus 和 base 是 384 字节无符号大端整数的无填充 base64url 编码。

参数类型描述
asset_idJSON要下载的资产标识

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 版本复用。

第五步:提交 solution 并领取下载授权

POST/api/public/v2/api/authorizations

提交顺序工作量结果并领取短时、单节点绑定的下载授权。challenge_id、asset_id 与 solution 都必须来自同一条 V2 挑战链路。程序 API 当前没有需要客户端上报的 telemetry 字段。

参数类型描述
challenge_idJSON挑战标识
asset_idJSON资产标识
solutionJSON512 字符的定长 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。

GET{download_url}

Example Request

GET {download_url}
Authorization: Bearer <download_token>

如果需要断点续传,可以使用单段 Range:

GET {download_url}
Authorization: Bearer <download_token>
Range: bytes=1048576-2097151

其他接口

下面这些接口不是程序下载流程的步骤,只用于订阅、状态查询或排障。

封禁列表订阅

GET/api/public/v1/blocklist.txt
GET/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"}
    ]
  }
}

项目开发者更新检测

POST/api/developer/v1/projects/{project_id}/sync

供镜像项目所有者在发布 GitHub Release 后主动触发一次该项目的更新检测。Developer API Token 与单个项目绑定,只能触发对应项目;Token 可在管理后台项目卡片中生成或重置。

参数类型描述
project_idPath项目标识
AuthorizationHeaderBearer 项目 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 表示每日额度耗尽或认证失败次数过多。

更新日志

GET/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
GET/api/public/v2/authorizations/{authorization_id}

携带对应 download_token 查询 V2 授权状态、过期时间、公开节点名和已入账真实发送字节。旧的 /api/public/v1/authorizations/{authorization_id} 目前仅作为兼容别名保留,新客户端不要使用它。

参数类型描述
authorization_idPath授权标识
AuthorizationHeaderBearer <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"
  }
}