Implement full media crawler workflow with Flask backend and Vue frontend.

Add TMDB search and media detail pages, HDHive resource ingestion flow, unified error handling, Docker single-container runtime, and project docs/config updates for local deployment.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
renjue
2026-05-09 16:16:18 +08:00
parent d3550bf79b
commit 82581d2949
49 changed files with 4959 additions and 0 deletions

View File

@@ -0,0 +1,505 @@
{
"name": "[MEDIA]CMS入库115链接",
"nodes": [
{
"parameters": {
"workflowInputs": {
"values": [
{
"name": "url"
}
]
}
},
"type": "n8n-nodes-base.executeWorkflowTrigger",
"typeVersion": 1.1,
"position": [
0,
0
],
"id": "fa49a99a-e52d-48ee-a126-c7ad2a128f35",
"name": "When Executed by Another Workflow"
},
{
"parameters": {
"operation": "get",
"propertyName": "token",
"key": "CMS_TOKEN",
"keyType": "string",
"options": {}
},
"type": "n8n-nodes-base.redis",
"typeVersion": 1,
"position": [
224,
-80
],
"id": "01719d30-77b8-411d-a7d7-0116ffde8725",
"name": "Redis",
"credentials": {
"redis": {
"id": "0auGFn4EkjXee2ul",
"name": "Redis account"
}
}
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 3
},
"conditions": [
{
"id": "166573d5-cf9c-43a1-9337-39acf4338cb7",
"leftValue": "={{ $json.token }}",
"rightValue": "",
"operator": {
"type": "string",
"operation": "empty",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2.3,
"position": [
432,
-80
],
"id": "490b5aa2-2ba7-4037-af2a-f5407e51984e",
"name": "If"
},
{
"parameters": {
"workflowId": {
"__rl": true,
"value": "R9CpsJ3lkHFdx-Z3XMNnE",
"mode": "list",
"cachedResultUrl": "/workflow/R9CpsJ3lkHFdx-Z3XMNnE",
"cachedResultName": "CMS获取Token"
},
"workflowInputs": {
"mappingMode": "defineBelow",
"value": {},
"matchingColumns": [],
"schema": [],
"attemptToConvertTypes": false,
"convertFieldsToString": true
},
"options": {}
},
"type": "n8n-nodes-base.executeWorkflow",
"typeVersion": 1.3,
"position": [
640,
-176
],
"id": "aa9f03a2-4640-4b23-bc83-4b679a308daa",
"name": "Call 'CMS获取Token'"
},
{
"parameters": {
"mode": "raw",
"jsonOutput": "={\n \"token\": \"{{ $json.output }}\"\n}",
"options": {}
},
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
848,
-176
],
"id": "06971068-c7c4-414f-931c-e8cd4035e3c9",
"name": "Edit Fields"
},
{
"parameters": {
"numberInputs": 3
},
"type": "n8n-nodes-base.merge",
"typeVersion": 3.2,
"position": [
976,
-48
],
"id": "dc5ab1a6-e58c-4c7a-8d36-bf3c79465eb2",
"name": "Merge"
},
{
"parameters": {
"jsCode": "// Loop over input items and add a new field called 'myNewField' to the JSON of each one\nlet token = \"\";\nlet url = \"\";\nfor (const item of $input.all()) {\n if (\"token\" in item.json) {\n token = item.json.token;\n }\n if (\"url\" in item.json) {\n url = item.json.url;\n }\n}\n\nreturn {\n \"token\": token,\n \"url\": url\n};"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1168,
-32
],
"id": "a06796c0-0572-438e-a13f-12a216bc283b",
"name": "Code in JavaScript"
},
{
"parameters": {
"method": "POST",
"url": "https://cms.rc707blog.top/api/cloud/add_share_down",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "User-Agent",
"value": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:147.0) Gecko/20100101 Firefox/147.0"
},
{
"name": "Accept",
"value": "application/json, text/plain, */*"
},
{
"name": "Accept-Language",
"value": "zh-CN,zh;q=0.9,zh-TW;q=0.8,zh-HK;q=0.7,en-US;q=0.6,en;q=0.5"
},
{
"name": "Accept-Encoding",
"value": "gzip, deflate, br, zstd"
},
{
"name": "Authorization",
"value": "=Bearer {{ $json.token }}"
},
{
"name": "Origin",
"value": "https://cms.rc707blog.top"
},
{
"name": "DNT",
"value": "1"
},
{
"name": "Alt-Used",
"value": "cms.rc707blog.top"
},
{
"name": "Connection",
"value": "keep-alive"
},
{
"name": "Referer",
"value": "https://cms.rc707blog.top/incremental-sync"
},
{
"name": "Priority",
"value": "u=0"
}
]
},
"sendBody": true,
"bodyParameters": {
"parameters": [
{
"name": "url",
"value": "={{ $json.url }}"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.3,
"position": [
1376,
-32
],
"id": "a57cdbfc-dfea-4516-88ae-559dba2d71d8",
"name": "HTTP Request"
},
{
"parameters": {
"jsCode": "// Loop over input items and add a new field called 'myNewField' to the JSON of each one\nlet success = $input.first().json.code === 200;\nreturn {\n \"output\": success\n};"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2224,
32
],
"id": "3161bf30-aa02-46a3-aa27-5e93f51083a6",
"name": "Code in JavaScript1"
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 3
},
"conditions": [
{
"id": "e738a07c-9884-4ca3-a8de-079818a95c90",
"leftValue": "={{ $json.code }}",
"rightValue": 200,
"operator": {
"type": "number",
"operation": "notEquals"
}
},
{
"id": "f2ec4f79-1c9e-47be-a39c-48dfe19fbd7b",
"leftValue": "={{ $json.msg }}",
"rightValue": "提取分享链接失败",
"operator": {
"type": "string",
"operation": "notEquals"
}
}
],
"combinator": "and"
},
"options": {}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2.3,
"position": [
1552,
128
],
"id": "737632fb-47ed-4108-8bb2-560cb5daeb86",
"name": "If1"
},
{
"parameters": {
"operation": "delete",
"key": "CMS_TOKEN"
},
"type": "n8n-nodes-base.redis",
"typeVersion": 1,
"position": [
1776,
112
],
"id": "8287fa40-9f93-49a4-87f3-8b653ac2785d",
"name": "Redis1",
"credentials": {
"redis": {
"id": "0auGFn4EkjXee2ul",
"name": "Redis account"
}
}
},
{
"parameters": {
"operation": "set",
"key": "CMS_TOKEN",
"value": "={{ $json.output }}",
"keyType": "string",
"expire": true,
"ttl": 3600
},
"type": "n8n-nodes-base.redis",
"typeVersion": 1,
"position": [
848,
-336
],
"id": "1534f994-2207-402d-814a-5eda492e9437",
"name": "Redis2",
"credentials": {
"redis": {
"id": "0auGFn4EkjXee2ul",
"name": "Redis account"
}
}
},
{
"parameters": {},
"type": "n8n-nodes-base.merge",
"typeVersion": 3.2,
"position": [
2000,
32
],
"id": "72d832a4-f7ae-4d91-9d55-0dddd1f4c878",
"name": "Merge1"
}
],
"pinData": {
"When Executed by Another Workflow": [
{
"json": {
"url": "https://115cdn.com/s/swfl9s73zfr?password=0000"
}
}
]
},
"connections": {
"When Executed by Another Workflow": {
"main": [
[
{
"node": "Redis",
"type": "main",
"index": 0
},
{
"node": "Merge",
"type": "main",
"index": 2
}
]
]
},
"Redis": {
"main": [
[
{
"node": "If",
"type": "main",
"index": 0
}
]
]
},
"If": {
"main": [
[
{
"node": "Call 'CMS获取Token'",
"type": "main",
"index": 0
}
],
[
{
"node": "Merge",
"type": "main",
"index": 1
}
]
]
},
"Call 'CMS获取Token'": {
"main": [
[
{
"node": "Edit Fields",
"type": "main",
"index": 0
},
{
"node": "Redis2",
"type": "main",
"index": 0
}
]
]
},
"Edit Fields": {
"main": [
[
{
"node": "Merge",
"type": "main",
"index": 0
}
]
]
},
"Merge": {
"main": [
[
{
"node": "Code in JavaScript",
"type": "main",
"index": 0
}
]
]
},
"Code in JavaScript": {
"main": [
[
{
"node": "HTTP Request",
"type": "main",
"index": 0
}
]
]
},
"HTTP Request": {
"main": [
[
{
"node": "If1",
"type": "main",
"index": 0
},
{
"node": "Merge1",
"type": "main",
"index": 0
}
]
]
},
"If1": {
"main": [
[
{
"node": "Redis1",
"type": "main",
"index": 0
}
]
]
},
"Code in JavaScript1": {
"main": [
[]
]
},
"Redis1": {
"main": [
[
{
"node": "Merge1",
"type": "main",
"index": 1
}
]
]
},
"Merge1": {
"main": [
[
{
"node": "Code in JavaScript1",
"type": "main",
"index": 0
}
]
]
}
},
"active": true,
"settings": {
"executionOrder": "v1",
"binaryMode": "separate",
"availableInMCP": false
},
"versionId": "b2e168e5-9970-4116-ac54-b2644ac5c4dc",
"meta": {
"templateCredsSetupCompleted": true,
"instanceId": "66cf2dfa889c11bef16af6baad527d2d3bcad1ee4e7726db9de95c31e876ed5e"
},
"id": "pH51j4qlAcf_gNk2gYEgH",
"tags": []
}

View File

@@ -0,0 +1,147 @@
{
"name": "[MEDIA]CMS获取Token",
"nodes": [
{
"parameters": {
"inputSource": "passthrough"
},
"type": "n8n-nodes-base.executeWorkflowTrigger",
"typeVersion": 1.1,
"position": [
0,
0
],
"id": "8b0db7d2-9dfa-43f6-abc1-04751f022c67",
"name": "When Executed by Another Workflow"
},
{
"parameters": {
"method": "POST",
"url": "https://cms.rc707blog.top/api/auth/login",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "User-Agent",
"value": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:147.0) Gecko/20100101 Firefox/147.0"
},
{
"name": "Accept",
"value": "application/json, text/plain, */*"
},
{
"name": "Accept-Language",
"value": "zh-CN,zh;q=0.9,zh-TW;q=0.8,zh-HK;q=0.7,en-US;q=0.6,en;q=0.5"
},
{
"name": "Accept-Encoding",
"value": "gzip, deflate, br, zstd"
},
{
"name": "Origin",
"value": "https://cms.rc707blog.top"
},
{
"name": "DNT",
"value": "1"
},
{
"name": "Alt-Used",
"value": "cms.rc707blog.top"
},
{
"name": "Connection",
"value": "keep-alive"
},
{
"name": "Referer",
"value": "https://cms.rc707blog.top/login"
},
{
"name": "Priority",
"value": "u=0"
},
{
"name": "TE",
"value": "trailers"
}
]
},
"sendBody": true,
"bodyParameters": {
"parameters": [
{
"name": "username",
"value": "rose_cat707"
},
{
"name": "password",
"value": "ro983364"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.3,
"position": [
208,
0
],
"id": "06e9804f-6bbd-43d7-a192-081394f9ee9e",
"name": "HTTP Request"
},
{
"parameters": {
"mode": "raw",
"jsonOutput": "={\n\"output\": \"{{ $json.data.token }}\"\n}",
"options": {}
},
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
416,
0
],
"id": "c93efa65-9381-403a-8d2c-3d3446e79bfa",
"name": "Edit Fields"
}
],
"pinData": {},
"connections": {
"When Executed by Another Workflow": {
"main": [
[
{
"node": "HTTP Request",
"type": "main",
"index": 0
}
]
]
},
"HTTP Request": {
"main": [
[
{
"node": "Edit Fields",
"type": "main",
"index": 0
}
]
]
}
},
"active": true,
"settings": {
"executionOrder": "v1",
"binaryMode": "separate",
"availableInMCP": false
},
"versionId": "683c7ff7-4b28-454f-8215-98f5d6b055aa",
"meta": {
"instanceId": "66cf2dfa889c11bef16af6baad527d2d3bcad1ee4e7726db9de95c31e876ed5e"
},
"id": "R9CpsJ3lkHFdx-Z3XMNnE",
"tags": []
}

View File

@@ -0,0 +1,19 @@
# HDHive OpenAPI 公开文档
本压缩包由当前前端 OpenAPI 文档内容生成,面向个人脚本、内部工具和第三方应用接入方。
## 文件
- [概述](./overview.md)
- [认证与授权](./authentication.md)
- [限制与错误处理](./limits.md)
- [通用响应格式](./response-format.md)
- [接口列表](./endpoints.md)
## SDK 下载
SDK 下载页:`/api-docs/sdk`
## 限制说明
本文档不公开具体 QPS 或每日额度。接入方应按 `429``Retry-After``limit_scope``retry_after_seconds` 做退避处理。

View File

@@ -0,0 +1,112 @@
# 认证与授权
## API Key 认证
所有 OpenAPI 请求都必须携带 `X-API-Key`
```http
X-API-Key: your-api-key
```
### 请求示例
```bash
curl -H "X-API-Key: your-api-key" https://hdhive.com/api/open/ping
```
## 用户身份来源
业务接口需要能确定调用用户。系统按以下顺序识别用户:
1. `Authorization: Bearer <OpenAPI 用户 Access Token>`:第三方应用授权后获得,必须属于当前应用。
2. `Authorization: Bearer <站内用户 JWT>`:站内登录态 Token。
3. API Key 绑定用户:个人 API Key 或管理员绑定用户的 OpenAPI 应用可回退到绑定用户。
`meta` 类接口如果无法确定用户,会返回 `OPENAPI_USER_REQUIRED`
## 第三方应用授权流程
第三方应用应使用管理员创建的 OpenAPI 应用完成授权:
1. 管理员创建 OpenAPI 应用配置应用名称、绑定用户、scope、IP 策略、回调地址等。
2. 第三方应用引导用户进入前端授权页 `/openapi/authorize`,并传入 `client_id``redirect_uri``scope``state`
3. 用户确认授权后HDHive 前端调用 `POST /api/public/openapi/oauth/authorize`,后端以 JSON 返回一次性授权码。
4. HDHive 前端把用户重定向到第三方 `redirect_uri`,并在 query 中追加 `code` 和原始 `state`
5. 第三方应用服务端用授权码调用 `POST /api/public/openapi/oauth/token` 换取用户 Access Token 和 Refresh Token。
6. 调用业务接口时同时传入应用 Secret 和用户 Access Token。
```http
X-API-Key: app-secret
Authorization: Bearer user-access-token
```
### 用户授权页
第三方应用应把用户跳转到站点前端授权页,而不是直接展示后端 API
```text
https://hdhive.com/openapi/authorize?client_id=app_xxx&redirect_uri=https%3A%2F%2Fclient.example.com%2Fcallback&scope=query%20unlock&state=opaque-state
```
| 参数 | 必填 | 说明 |
|---|---|---|
| `client_id` | 是 | OpenAPI 应用公开 Client ID不是 Secret |
| `redirect_uri` | 是 | 回调地址,必须在应用配置的回调地址列表内 |
| `scope` | 否 | 请求的用户授权 scope空格分隔为空时按应用允许范围处理 |
| `state` | 否 | 第三方应用自定义状态,建议用于 CSRF 防护和回跳上下文 |
### 授权码换 Token
后端换取 Token 时必须使用应用 Secret
```bash
curl -X POST https://hdhive.com/api/public/openapi/oauth/token \\
-H "X-API-Key: app-secret" \\
-H "Content-Type: application/json" \\
-d '{
"grant_type": "authorization_code",
"code": "authorization-code",
"redirect_uri": "https://client.example.com/callback"
}'
```
返回的 `access_token` 用于业务 OpenAPI 调用,`refresh_token` 用于刷新用户授权 Token。
`redirect_uri` 在换取 Token 时不会触发跳转,它是授权码安全校验字段,必须与创建授权码时传入的 `redirect_uri` 完全一致。后端会用它确认授权码确实来自同一次授权请求,避免授权码被截获后在其他回调上下文中兑换。
### 授权码回调形式
用户确认授权后,第三方应用收到的是浏览器跳转请求:
```text
https://client.example.com/callback?code=authorization-code&state=opaque-state
```
`code` 是一次性授权码,第三方应用应该在自己的服务端用应用 Secret 换取 Token不要在浏览器里暴露应用 Secret。
## Scope
| scope | 说明 | 典型能力 |
|---|---|---|
| `meta` | 元信息接口 | 健康检查、配额、用量查询 |
| `query` | 查询接口 | 资源查询、分享详情、资源链接检查 |
| `write` | 写入接口 | 创建、更新、删除分享 |
| `unlock` | 解锁接口 | 解锁资源并获取链接 |
| `vip` | VIP 接口 | 用户信息、签到、VIP 用量 |
应用必须拥有对应 scope用户授权 Token 如果限制了 scope也必须包含对应 scope。
## 认证错误码
| 错误码 | HTTP 状态码 | 说明 |
|---|---|---|
| `MISSING_API_KEY` | 401 | 未提供 `X-API-Key` |
| `INVALID_API_KEY` | 401 | API Key 无效或不存在 |
| `DISABLED_API_KEY` | 401 | API Key 已被禁用 |
| `EXPIRED_API_KEY` | 401 | API Key 已过期 |
| `INVALID_OPENAPI_USER_TOKEN` | 401 | OpenAPI 用户 Token 无效 |
| `OPENAPI_TOKEN_APP_MISMATCH` | 401 | 用户 Token 不属于当前应用 |
| `OPENAPI_USER_REQUIRED` | 403 | 业务接口缺少用户身份 |
| `SCOPE_NOT_ALLOWED` | 403 | 应用未授权该接口组 |
| `USER_SCOPE_NOT_ALLOWED` | 403 | 用户授权 Token scope 不足 |
| `VIP_REQUIRED` | 403 | 当前用户不满足 Premium 要求 |

View File

@@ -0,0 +1,518 @@
# 接口列表
OpenAPI 分为两类接口:
- **用户授权接口**:以 `/api/public/openapi/oauth` 为前缀,用于授权预览、确认授权、换取 Token 和撤销 Token。
- **业务接口**:以 `/api/open` 为前缀,请求必须携带 `X-API-Key`。需要代表具体用户执行的接口,还应携带用户 Access Token个人 API Key 或绑定用户的应用可回退到绑定用户。
## 接口总览
### 用户授权接口
| 方法 | 路径 | 认证 | 说明 |
|---|---|---|---|
| GET | `/api/public/openapi/oauth/authorize` | 站内登录 Cookie | 授权页预览信息 |
| POST | `/api/public/openapi/oauth/authorize` | 站内登录 Cookie | 用户确认授权并生成授权码 |
| POST | `/api/public/openapi/oauth/token` | `X-API-Key` 应用 Secret | 授权码或刷新令牌换取用户 Token |
| POST | `/api/public/openapi/oauth/revoke` | `X-API-Key` 应用 Secret | 撤销刷新令牌 |
### 业务接口
| scope | 方法 | 路径 | 说明 |
|---|---|---|---|
| `meta` | GET | `/api/open/ping` | 健康检查 |
| `meta` | GET | `/api/open/quota` | 查询当前凭证配额状态 |
| `meta` | GET | `/api/open/usage` | 查询历史用量 |
| `meta` | GET | `/api/open/usage/today` | 查询今日用量 |
| `query` | GET | `/api/open/resources/:type/:tmdb_id` | 根据 TMDB ID 获取资源列表 |
| `query` | GET | `/api/open/shares/:slug` | 获取分享详情 |
| `query` | POST | `/api/open/check/resource` | 检查资源链接类型 |
| `unlock` | POST | `/api/open/resources/unlock` | 解锁资源 |
| `write` | GET | `/api/open/shares` | 获取我的分享列表 |
| `write` | POST | `/api/open/shares` | 创建分享 |
| `write` | PATCH | `/api/open/shares/:slug` | 更新分享 |
| `write` | DELETE | `/api/open/shares/:slug` | 删除分享 |
| `vip` | GET | `/api/open/me` | 获取当前用户信息 |
| `vip` | POST | `/api/open/checkin` | 每日签到 |
| `vip` | GET | `/api/open/vip/weekly-free-quota` | 获取长期 Premium 每周免费解锁状态 |
---
## 用户授权流程接口
第三方应用的用户授权入口应使用前端页面:
```text
GET /openapi/authorize?client_id=app_xxx&redirect_uri=https%3A%2F%2Fclient.example.com%2Fcallback&scope=query%20unlock&state=opaque-state
```
前端页面会要求用户登录,并调用下面的后端授权接口。
---
## GET /api/public/openapi/oauth/authorize
获取授权页展示信息。该接口需要站内登录 Cookie未登录会返回 `401`,前端授权页会跳转到登录页。
### Query 参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `client_id` | string | 是 | OpenAPI 应用公开 Client ID |
| `redirect_uri` | string | 是 | 授权完成回调地址,必须在应用配置中 |
| `scope` | string | 否 | 请求 scope空格分隔例如 `query unlock` |
| `state` | string | 否 | 第三方应用自定义状态 |
### 成功响应字段
| 字段 | 类型 | 说明 |
|---|---|---|
| `data.client_id` | string | 应用 Client ID |
| `data.app_name` | string | 应用名称 |
| `data.description` | string | 应用描述 |
| `data.auth_level` | string | 应用授权等级 |
| `data.redirect_uri` | string | 本次授权回调地址 |
| `data.requested_scopes` | string[] | 本次请求 scope |
| `data.allowed_scopes` | string[] | 应用实际允许 scope |
| `data.user` | object | 当前登录用户摘要 |
| `data.expires_in` | integer | 授权码有效期秒数 |
---
## POST /api/public/openapi/oauth/authorize
用户确认授权并生成授权码。该接口需要站内登录 Cookie。
该接口本身返回 JSON 给 HDHive 前端授权页。HDHive 前端收到 `data.code` 后,会重定向到第三方 `redirect_uri`,并追加 `code` 和原始 `state`
```text
https://client.example.com/callback?code=authorization-code&state=opaque-state
```
### Body 参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `client_id` | string | 是 | OpenAPI 应用公开 Client ID |
| `redirect_uri` | string | 是 | 授权完成回调地址 |
| `scope` | string | 否 | 请求 scope空格分隔 |
| `state` | string | 否 | 第三方应用自定义状态 |
### 请求示例
```bash
curl -X POST https://hdhive.com/api/public/openapi/oauth/authorize \\
-H "Content-Type: application/json" \\
-d '{
"client_id": "app_xxx",
"redirect_uri": "https://client.example.com/callback",
"scope": "query unlock",
"state": "opaque-state"
}'
```
### 成功响应
```json
{
"success": true,
"code": "200",
"message": "success",
"data": {
"code": "authorization-code",
"redirect_uri": "https://client.example.com/callback",
"expires_in": 600
}
}
```
第三方应用通过自己的回调地址收到授权码后,应在服务端用应用 Secret 换取用户 Token。
---
## POST /api/public/openapi/oauth/token
使用授权码或刷新令牌换取 OpenAPI 用户 Token。该接口必须携带 OpenAPI 应用 Secret。
`redirect_uri` 只在 `authorization_code` 模式下需要。它不是跳转地址,而是授权码校验字段,必须与用户授权阶段传入的 `redirect_uri` 完全一致;后端会同时校验授权码、应用 Secret 和 `redirect_uri` 的绑定关系。
### Header
```http
X-API-Key: app-secret
Content-Type: application/json
```
### authorization_code Body
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `grant_type` | string | 是 | 固定为 `authorization_code` |
| `code` | string | 是 | 用户确认授权后得到的授权码 |
| `redirect_uri` | string | 是 | 安全校验字段,必须与授权码创建时传入的回调地址完全一致;不会触发跳转 |
### refresh_token Body
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `grant_type` | string | 是 | 固定为 `refresh_token` |
| `refresh_token` | string | 是 | 刷新令牌 |
### 请求示例
```bash
curl -X POST https://hdhive.com/api/public/openapi/oauth/token \\
-H "X-API-Key: app-secret" \\
-H "Content-Type: application/json" \\
-d '{
"grant_type": "authorization_code",
"code": "authorization-code",
"redirect_uri": "https://client.example.com/callback"
}'
```
### 成功响应字段
| 字段 | 类型 | 说明 |
|---|---|---|
| `data.access_token` | string | OpenAPI 用户 Access Token |
| `data.refresh_token` | string | Refresh Token刷新时可能返回新值 |
| `data.token_type` | string | 固定为 `Bearer` |
| `data.expires_in` | integer | Access Token 有效期秒数 |
| `data.refresh_expires_in` | integer | Refresh Token 有效期秒数 |
| `data.scope` | string | 空格分隔 scope |
| `data.scopes` | string[] | scope 数组 |
---
## POST /api/public/openapi/oauth/revoke
撤销某个 Refresh Token。撤销后该刷新令牌不能再换取新的用户 Access Token。
### Header
```http
X-API-Key: app-secret
Content-Type: application/json
```
### Body 参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `refresh_token` | string | 是 | 要撤销的刷新令牌 |
### 请求示例
```bash
curl -X POST https://hdhive.com/api/public/openapi/oauth/revoke \\
-H "X-API-Key: app-secret" \\
-H "Content-Type: application/json" \\
-d '{"refresh_token": "refresh-token"}'
```
---
## GET /api/open/ping
健康检查接口,用于验证 API Key 是否有效。
### 请求示例
```bash
curl -H "X-API-Key: your-api-key" https://hdhive.com/api/open/ping
```
### 成功响应
```json
{
"success": true,
"code": "200",
"message": "success",
"data": {
"message": "pong",
"api_key_id": 1,
"name": "My App"
}
}
```
---
## GET /api/open/quota
查询当前 API Key 的配额状态。该接口用于展示服务端返回的当前状态,不应在客户端写死限制值。
### 请求示例
```bash
curl -H "X-API-Key: your-api-key" https://hdhive.com/api/open/quota
```
### 响应字段
| 字段 | 类型 | 说明 |
|---|---|---|
| `data.daily_reset` | integer | 配额重置时间 |
| `data.endpoint_limit` | integer / null | 当前接口限制状态,未配置时为空 |
| `data.endpoint_remaining` | integer / null | 当前接口剩余状态,未配置时为空 |
---
## GET /api/open/usage
查询当前 API Key 的历史用量统计。
### 请求参数
| 参数 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `start_date` | query | string | 否 | 开始日期,格式 `YYYY-MM-DD` |
| `end_date` | query | string | 否 | 结束日期,格式 `YYYY-MM-DD` |
### 请求示例
```bash
curl -H "X-API-Key: your-api-key" \\
"https://hdhive.com/api/open/usage?start_date=2026-04-01&end_date=2026-04-25"
```
### 响应字段
| 字段 | 类型 | 说明 |
|---|---|---|
| `data.daily_stats` | array | 每日调用统计 |
| `data.endpoint_stats` | array | 按接口聚合的统计 |
| `data.summary` | object | 汇总统计 |
---
## GET /api/open/resources/:type/:tmdb_id
根据媒体类型和 TMDB ID 获取资源列表。
### 请求参数
| 参数 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `type` | path | string | 是 | 媒体类型,`movie``tv` |
| `tmdb_id` | path | string | 是 | TMDB ID |
### 请求示例
```bash
curl -H "X-API-Key: your-api-key" \\
-H "Authorization: Bearer user-access-token" \\
https://hdhive.com/api/open/resources/movie/550
```
### 响应字段
| 字段 | 类型 | 说明 |
|---|---|---|
| `data[].slug` | string | 资源唯一标识 |
| `data[].title` | string / null | 资源标题 |
| `data[].pan_type` | string / null | 网盘类型 |
| `data[].share_size` | string / null | 分享文件大小 |
| `data[].video_resolution` | string[] | 视频分辨率 |
| `data[].source` | string[] | 片源 |
| `data[].subtitle_language` | string[] | 字幕语言 |
| `data[].subtitle_type` | string[] | 字幕类型 |
| `data[].unlock_points` | integer / null | 解锁所需积分 |
| `data[].is_unlocked` | boolean | 当前用户是否已解锁 |
| `data[].user` | object / null | 分享者信息 |
| `meta.total` | integer | 返回数量 |
---
## POST /api/open/resources/unlock
代表当前用户解锁资源并获取链接。第三方应用调用时应携带用户 Access Token。
### 请求参数
| 参数 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `slug` | body | string | 是 | 资源 slug支持带横杠或不带横杠的 UUID |
### 请求示例
```bash
curl -X POST \\
-H "X-API-Key: app-secret" \\
-H "Authorization: Bearer user-access-token" \\
-H "Content-Type: application/json" \\
-d '{"slug": "a1b2c3d4e5f647898765432112345678"}' \\
https://hdhive.com/api/open/resources/unlock
```
### 成功响应
```json
{
"success": true,
"code": "200",
"message": "解锁成功",
"data": {
"url": "https://pan.example.com/s/abc123",
"access_code": "x1y2",
"full_url": "https://pan.example.com/s/abc123?pwd=x1y2",
"already_owned": false
}
}
```
### 错误响应
| 场景 | HTTP 状态码 | 错误码 |
|---|---|---|
| 参数无效 | 400 | `400` |
| 缺少用户身份 | 403 | `OPENAPI_USER_REQUIRED` |
| 资源不存在 | 404 | `404` |
| 积分不足 | 402 | `INSUFFICIENT_POINTS` |
| 用户请求过快 | 429 | `RATE_LIMIT_EXCEEDED` |
> unlock 接口保留用户阶梯风控。第三方应用触发时,处罚对象是当前授权用户,不是整个应用。
---
## POST /api/open/check/resource
检查资源链接所属网盘类型,并尝试解析部分网盘的访问码。
### 请求参数
| 参数 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `url` | body | string | 是 | 资源分享链接 |
### 请求示例
```bash
curl -X POST \\
-H "X-API-Key: your-api-key" \\
-H "Authorization: Bearer user-access-token" \\
-H "Content-Type: application/json" \\
-d '{"url": "https://115.com/s/example 访问码:1234"}' \\
https://hdhive.com/api/open/check/resource
```
---
## GET /api/open/me
获取当前用户信息。该接口需要 `vip` scope并要求当前用户满足 Premium 条件。
### 请求示例
```bash
curl -H "X-API-Key: app-secret" \\
-H "Authorization: Bearer user-access-token" \\
https://hdhive.com/api/open/me
```
### 响应字段
| 字段 | 类型 | 说明 |
|---|---|---|
| `data.id` | number | 用户 ID |
| `data.nickname` | string | 昵称 |
| `data.username` | string | 用户名 |
| `data.email` | string | 邮箱 |
| `data.avatar_url` | string | 头像 |
| `data.is_vip` | boolean | 是否 Premium |
| `data.user_meta` | object | 用户积分、签到、分享等元信息 |
---
## POST /api/open/checkin
代表当前用户执行每日签到。该接口需要 `vip` scope并要求当前用户满足 Premium 条件。
### 请求参数
| 参数 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `is_gambler` | body | boolean | 否 | 是否使用高波动签到模式 |
---
## GET /api/open/vip/weekly-free-quota
获取当前用户长期 Premium 每周免费解锁状态。该接口只返回状态,不建议客户端假设配置固定。
### 响应字段
| 字段 | 类型 | 说明 |
|---|---|---|
| `is_forever_vip` | boolean | 是否长期 Premium |
| `limit` | int | 服务端当前配置的基础状态值 |
| `used` | int | 本周已使用次数 |
| `remaining` | int | 当前可用状态 |
| `unlimited` | boolean | 是否不限制 |
| `bonus_quota` | int | 累积状态值 |
| `bonus_quota_max` | int | 累积上限状态值 |
---
## GET /api/open/shares
获取当前用户的分享列表。需要 `write` scope。
### 请求参数
| 参数 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `page` | query | integer | 否 | 页码 |
| `page_size` | query | integer | 否 | 每页数量 |
---
## GET /api/open/shares/:slug
获取指定分享详情。该接口不返回资源下载链接和访问码,需要调用 unlock 接口解锁后获取。
---
## POST /api/open/shares
创建新的资源分享。需要 `write` scope。
### 关键参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `url` | string | 是 | 分享链接 |
| `tmdb_id` | string | 条件必填 | TMDB ID需配合 `media_type` |
| `media_type` | string | 条件必填 | `movie``tv` |
| `movie_id` | integer | 条件必填 | 系统电影 ID |
| `tv_id` | integer | 条件必填 | 系统电视剧 ID |
| `collection_id` | integer | 否 | 系统合集 ID |
| `title` | string | 否 | 资源标题 |
| `access_code` | string | 否 | 访问码 |
| `unlock_points` | integer | 否 | 解锁积分,未传或 0 表示免费 |
| `is_anonymous` | boolean | 否 | 是否匿名分享 |
| `hide_link` | boolean | 否 | 是否在通知中隐藏链接 |
> 影视关联至少需要提供 `tmdb_id + media_type`、`movie_id`、`tv_id` 或 `collection_id` 之一。
---
## PATCH /api/open/shares/:slug
部分更新已有分享。需要 `write` scope。只能更新自己创建的分享或当前用户具备对应权限的分享。
---
## DELETE /api/open/shares/:slug
删除分享。需要 `write` scope。只能删除自己创建的分享或当前用户具备对应权限的分享。
---
# 变更日志
| 日期 | 变更内容 |
|---|---|
| 2026-04-25 | 文档更新为新版 OpenAPI 应用授权、用户授权 Token、scope、限制与错误码口径 |

View File

@@ -0,0 +1,78 @@
# 限制与错误处理
## 限制维度
OpenAPI 会按多个维度保护系统稳定性:
- **应用级限制**:按 API Key / OpenAPI 应用维度限制请求频率和突发流量。
- **用户级限制**:按实际调用用户维度限制请求频率和每日成功业务请求。
- **接口组限制**:按 `meta/query/write/unlock/vip` 分组控制访问频率和是否计量。
- **IP 策略**:支持不限制 IP、固定 IP 白名单、动态 IP 风控。
- **unlock 风控**:资源解锁接口保留旧的用户阶梯处罚监测,处罚对象是实际授权用户,不是整个第三方应用。
具体阈值由管理员配置,不在开发者文档中公开。接入方只需要按响应头和错误码处理退避。
## 429 处理规则
当请求返回 `429` 时,客户端应读取 `Retry-After` 头,并在等待后重试。不要在冷却期内持续重试。
```http
HTTP/1.1 429 Too Many Requests
Retry-After: 300
```
429 错误响应会通过 `limit_scope` 明确说明限制对象:
```json
{
"success": false,
"code": "USER_RATE_LIMIT_EXCEEDED",
"message": "User OpenAPI rate limit exceeded",
"description": "用户 OpenAPI 请求频率过高,当前授权用户已进入冷却,请稍后重试",
"limit_scope": "user",
"limit_scope_label": "授权用户",
"retry_after_seconds": 300
}
```
`limit_scope=app` 表示当前应用或 API Key 被限制,所有使用该应用的请求都应退避;`limit_scope=user` 表示当前授权用户被限制,不代表整个第三方应用不可用。
## 常见限制错误码
| 错误码 | HTTP 状态码 | 说明 |
|---|---|---|
| `OPENAPI_COOLDOWN` | 429 | 应用或用户处于 OpenAPI 冷却期,具体对象读取 `limit_scope` |
| `IP_COUNT_EXCEEDED` | 429 | 动态 IP 风控触发 |
| `IP_NOT_ALLOWED` | 403 | 请求 IP 不在白名单内 |
| `APP_RATE_LIMIT_EXCEEDED` | 429 | 应用请求频率过高 |
| `APP_BURST_LIMIT_EXCEEDED` | 429 | 应用突发请求过高 |
| `APP_IP_RATE_LIMIT_EXCEEDED` | 429 | 应用单 IP 请求频率过高 |
| `ENDPOINT_GROUP_RATE_LIMIT_EXCEEDED` | 429 | 接口组请求频率过高 |
| `USER_RATE_LIMIT_EXCEEDED` | 429 | 用户 OpenAPI 请求频率过高 |
| `APP_DAILY_QUOTA_EXCEEDED` | 429 | 应用今日成功业务请求额度耗尽 |
| `USER_DAILY_QUOTA_EXCEEDED` | 429 | 用户今日成功业务请求额度耗尽 |
| `RATE_LIMIT_EXCEEDED` | 429 | unlock 用户阶梯风控触发 |
## 配额响应头
部分响应会返回当前用户或应用的剩余信息。客户端可以展示这些信息,但不能假设阈值固定不变。
| Header | 说明 |
|---|---|
| `Retry-After` | 建议等待秒数 |
| `X-OpenAPI-User-Daily-Limit` | 用户当日额度上限,存在时返回 |
| `X-OpenAPI-User-Daily-Remaining` | 用户当日剩余额度,存在时返回 |
| `X-OpenAPI-App-Daily-Limit` | 应用当日额度上限,存在时返回 |
| `X-OpenAPI-App-Daily-Remaining` | 应用当日剩余额度,存在时返回 |
## 限制字段
| 字段 | 类型 | 说明 |
|---|---|---|
| `limit_scope` | string | 限制对象,`app` 表示应用,`user` 表示授权用户 |
| `limit_scope_label` | string | 限制对象中文说明 |
| `retry_after_seconds` | integer | 建议等待秒数,与 `Retry-After` Header 对齐 |
## 计量说明
`meta` 类接口用于健康检查、配额和用量查询,通常不计入用户每日业务额度。非成功响应会回滚本次已预占的每日额度。

View File

@@ -0,0 +1,32 @@
# HDHive OpenAPI 文档
## 概述
HDHive OpenAPI 面向个人脚本、内部工具和第三方应用,提供资源查询、资源解锁、分享管理、用户信息和用量查询能力。
新版 OpenAPI 使用统一平台凭证:
- **个人 API Key**:用户自己创建,适合个人脚本和自用自动化。
- **OpenAPI 应用**:管理员创建,适合第三方应用或合作方系统。
- **用户授权 Token**:用户通过第三方应用授权后获得,代表具体用户执行业务操作。
## 基本信息
- **业务接口 Base URL**`/api/open`
- **用户授权接口 Base URL**`/api/public/openapi/oauth`
- **用户授权页**`/openapi/authorize`
- **认证方式**`X-API-Key` 必填,业务接口可附加 `Authorization: Bearer <token>`
- **响应格式**JSON
- **权限模型**:应用 scope + 用户授权 scope + 用户身份共同决定可访问范围
- **SDK 与文档下载**[打开 SDK 下载页](/api-docs/sdk),可下载 Go / Python / Node SDK 和公开版 API 文档 ZIP
## 文档原则
本文档只说明接入方式、接口语义、认证流程和错误码,不展示具体 QPS 或每日额度。具体限制由管理员在「OpenAPI 策略」和「OpenAPI 应用」中配置,接入方应按 `429` 响应和 `Retry-After` 头进行退避重试。
## 目录
- **认证与授权** — API Key、用户 Token、scope、用户身份来源
- **限制与错误处理** — 429、冷却、IP 策略、额度响应头
- **通用响应格式** — 成功/错误响应结构、错误码汇总
- **接口列表** — 主要 OpenAPI 接口说明、请求参数与响应示例

View File

@@ -0,0 +1,63 @@
# 通用响应格式
所有 OpenAPI 接口均采用统一 JSON 响应结构。
## 成功响应
```json
{
"success": true,
"code": "200",
"message": "success",
"data": { "...": "..." },
"meta": { "...": "..." }
}
```
| 字段 | 类型 | 说明 |
|---|---|---|
| `success` | boolean | 是否成功 |
| `code` | string | 状态码或业务码 |
| `message` | string | 响应消息 |
| `data` | object / array / null | 响应数据 |
| `meta` | object / null | 分页、统计等元信息 |
## 错误响应
```json
{
"success": false,
"code": "ERROR_CODE",
"message": "Error message",
"description": "详细错误描述",
"limit_scope": "app",
"limit_scope_label": "应用",
"retry_after_seconds": 300
}
```
| 字段 | 类型 | 说明 |
|---|---|---|
| `success` | boolean | 固定为 `false` |
| `code` | string | 错误码 |
| `message` | string | 错误消息 |
| `description` | string | 面向用户或开发者的中文说明 |
| `limit_scope` | string / undefined | 限制类错误的对象,可能为 `app``user` |
| `limit_scope_label` | string / undefined | 限制对象中文说明 |
| `retry_after_seconds` | integer / undefined | 建议等待秒数 |
## 业务错误码
| 错误码 | HTTP 状态码 | 说明 |
|---|---|---|
| `400` | 400 | 请求参数错误 |
| `401` | 401 | 未授权 |
| `403` | 403 | 无权访问 |
| `404` | 404 | 资源不存在 |
| `500` | 500 | 服务内部错误 |
| `INSUFFICIENT_POINTS` | 402 | 积分不足 |
| `VIP_REQUIRED` | 403 | 需要 Premium |
| `SCOPE_NOT_ALLOWED` | 403 | 应用 scope 不足 |
| `USER_SCOPE_NOT_ALLOWED` | 403 | 用户授权 scope 不足 |
| `OPENAPI_USER_REQUIRED` | 403 | 缺少用户身份 |
| `RATE_LIMIT_EXCEEDED` | 429 | 请求触发限制,应按 `Retry-After` 退避 |