Files
par/docs/API.md

620 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# PAR Server 集成开发文档
> 版本: v1.0 | Base URL: `http://<host>:9012`
## 目录
- [1. 认证鉴权](#1-认证鉴权)
- [2. 接口参考](#2-接口参考)
- [3. 数据结构](#3-数据结构)
- [4. 工作流程](#4-工作流程)
- [5. 代码示例](#5-代码示例)
---
## 1. 认证鉴权
### PSK 签名方式
```
X-Timestamp: 1782800000
X-Signature: Base64( HMAC-SHA256(PSK, "METHOD:path:timestamp") )
X-Email: your@email.com (身份标识,可传多个)
```
#### 签名步骤
```
① 获取服务端时间
GET /api/v1/time → {"timestamp": 1782800000}
② 拼接签名内容
signContent = "POST:/api/v1/sites/pthome/submit:1782800000"
③ 计算签名
signature = Base64( HMAC-SHA256(PSK, signContent) )
④ 发送请求
curl -H "X-Timestamp: 1782800000" \
-H "X-Signature: oNTk7GIA..." \
-H "X-Email: mediabot@example.com" \
...
```
#### 注意事项
- 时间窗口默认 60 秒(可配置)
- 建议先调 `/api/v1/time` 获取服务端时间
- PSK 由管理员在 PAR 管理后台生成和分发
- `X-Email` 不参与签名,仅用于审计和身份关联
### Bearer Token 方式(管理后台用)
```
Authorization: Bearer <token>
```
Token 通过 `POST /api/v1/auth/login` 获取,24 小时有效。
---
## 2. 接口参考
### 2.1 系统
| 方法 | 路径 | 认证 | 说明 |
|------|------|------|------|
| GET | `/health` | 无 | 健康检查 |
| GET | `/api/v1/time` | 无 | 服务端时间戳,签名前获取避免时钟偏差 |
#### GET /api/v1/time
```json
// Response 200
{ "timestamp": 1782800000, "unit": "seconds" }
```
### 2.2 认证
| 方法 | 路径 | 认证 | 说明 |
|------|------|------|------|
| POST | `/api/v1/auth/register` | 无 | 注册账户(密码可选) |
| POST | `/api/v1/auth/login` | 无 | 登录,返回 Bearer token |
| POST | `/api/v1/auth/recover` | PSK | PSK 签名恢复账户(自动创建+提权) |
#### POST /api/v1/auth/register
```json
// Request
{ "email": "mediabot@example.com", "password": "optional" }
// Response 200
{
"code": 200,
"data": {
"account": {
"id": 5,
"email": "mediabot@example.com",
"trustLevel": "MEMBER",
"isActive": true,
"createdAt": "2026-06-29T14:00:00"
}
}
}
```
> 💡 携带 `X-Medibot-Timestamp` + `X-Medibot-Signature` PSK 签名头可自动提权为 TRUSTED
#### POST /api/v1/auth/recover
```json
// Request (带 PSK 签名头)
{ "email": "mediabot@example.com" }
// Response 200
{
"code": 200,
"data": {
"account": { "id": 5, "email": "...", "trustLevel": "TRUSTED", ... }
}
}
```
### 2.3 全局清单
#### GET /api/v1/manifest
支持 `If-None-Match` 条件请求(ETag = 所有站点+配置哈希的 SHA-256)。
无变化时返回 `304 Not Modified`,无数据传输。
```bash
# 轮询示例
curl -H "If-None-Match: a1b2c3d4..." http://par:9012/api/v1/manifest
# → 304 Not Modified (内容未变)
```
```json
// Response 200 ETag: "a1b2c3d4..."
{
"code": 200,
"data": {
"version": "1.0",
"sites": [
{
"siteId": "pthome",
"names": ["PThome", "PThome镜像"],
"urls": ["https://pthome.net", "https://pthome.mirror"],
"siteType": "private",
"schemaVersion": "1.0.0",
"status": "active",
"siteType": "nexusphp",
"latestVersion": "1.2.0",
"configHash": "a1b2c3d4...",
"hasScript": false,
"updatedAt": "2026-06-29T14:00:00"
}
]
}
}
```
### 2.4 站点
| 方法 | 路径 | 认证 | 说明 |
|------|------|------|------|
| GET | `/api/v1/sites` | 无 | 所有活跃站点 |
| GET | `/api/v1/sites/{siteId}` | 无 | 单个站点详情 |
| POST | `/api/v1/sites/{siteId}/submit` | PSK | 提交站点收录 |
#### POST /api/v1/sites/{siteId}/submit
```json
// Request (多名称/多地址)
{
"names": ["PThome", "PThome镜像"], // 必填
"urls": ["https://pthome.net", "https://pthome.mirror"], // 必填
// 或向后兼容单值: "name": "PThome", "baseUrl": "https://..."
"description": "PT 资源站",
"iconUrl": "https://...",
"siteType": "private"
}
// Response 200
{
"code": 200,
"data": {
"id": 1,
"siteId": "pthome",
"names": ["PThome", "PThome镜像"],
"urls": ["https://pthome.net", "https://pthome.mirror"],
"siteType": "private",
"schemaVersion": "1.0.0",
"status": "active",
"createdAt": "2026-06-29T14:00:00"
}
}
```
### 2.5 配置/适配脚本
| 方法 | 路径 | 认证 | 说明 |
|------|------|------|------|
| GET | `/api/v1/configs/{siteId}` | 无 | 已批准配置版本列表 |
| GET | `/api/v1/configs/{siteId}/{version}` | 无 | 下载指定版本配置 |
| POST | `/api/v1/configs/{siteId}/submit` | PSK | 提交新配置版本 |
#### GET /api/v1/configs/{siteId}
```json
// Response 200
{
"code": 200,
"data": [
{
"id": 10,
"siteId": "pthome",
"version": "1.2.0",
"configHash": "a1b2c3...",
"fileSize": 8192,
"schemaVersion": "1.0.0",
"status": "approved",
"isLatest": true,
"createdAt": "2026-06-29T14:00:00"
}
]
}
```
#### GET /api/v1/configs/{siteId}/{version}
支持 `If-None-Match` 条件请求(ETag = configHash),返回 304 表示内容未变化。
```json
// Response 200 (Content-Type: application/json)
"{\"rules\": [...]}" // 原始配置 JSON 内容
```
#### POST /api/v1/configs/{siteId}/submit
```json
// Request
{
"version": "1.3.0", // 必填
"config": "{...json...}", // 必填:适配脚本 JSON
"schemaVersion": "1.0.0"
}
// Response 200
{
"code": 200,
"data": {
"id": 11,
"siteId": "pthome",
"version": "1.3.0",
"configHash": "e5f6a7...",
"status": "pending",
"createdAt": "2026-06-29T14:00:00"
}
}
```
### 2.6 站点快照(统一配置 Bundle)⭐ 推荐
| 方法 | 路径 | 认证 | 说明 |
|------|------|------|------|
| GET | `/api/v1/snapshots/{siteId}` | 无 | 获取站点完整配置快照(脚本+页面+规则) |
| GET | `/api/v1/snapshots/{siteId}/check` | 无 | 轻量级检查是否有配置更新 |
#### GET /api/v1/snapshots/{siteId}
**推荐使用**。一次请求获取站点的脚本 + 页面配置 + 风险规则合并快照。
支持 `If-None-Match` 条件请求(ETag = checksum),无变化返回 `304 Not Modified`。
| 参数 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `preview` | boolean | `false` | `true`=预览版优先(draft→published fallback),`false`=仅正式版 |
```bash
# 获取正式版快照
curl http://par:9012/api/v1/snapshots/pthome
# 获取预览版(供开发/测试验证)
curl "http://par:9012/api/v1/snapshots/pthome?preview=true"
# ETag 增量更新
curl -H "If-None-Match: abc123..." http://par:9012/api/v1/snapshots/pthome
# → 304 Not Modified (内容未变,零流量)
```
```json
// Response 200 (Content-Type: application/json)
{
"siteId": "pthome",
"version": 5,
"checksum": "abc123def456...",
"generatedAt": "2026-07-07T10:30:00",
"script": "// Groovy 脚本完整源码...",
"scriptVersion": "3",
"scriptSource": "custom",
"pages": [{"pageKey": "INDEX", "url": "https://...", ...}],
"pagesVersion": "2",
"pagesSource": "custom",
"riskRules": [{"field": "title", "operator": "contains", ...}],
"riskRulesVersion": 1
}
```
> 💡 **可用性保障**:数据库优先 → CDN 灾备 → 实时构建兜底。MediaBot 也可直接缓存 CDN URL 作为终极 fallback。
#### GET /api/v1/snapshots/{siteId}/check
轻量级检查是否有更新,不传输配置内容。
```json
// Response 200
{
"code": 200,
"data": {
"checksum": "abc123def456...",
"version": 5,
"publishedAt": "2026-07-07T10:30:00"
}
}
```
#### 兼容旧接口
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | `/api/v1/configs/{siteId}/bundle` | 兼容旧路径,底层已委托给快照服务 |
### 2.7 Groovy 适配脚本
| 方法 | 路径 | 认证 | 说明 |
|------|------|------|------|
| GET | `/api/v1/configs/{siteId}/script` | 无 | 下载最新脚本(推荐) |
| GET | `/api/v1/configs/{siteId}/{version}/script` | 无 | 下载指定版本脚本 |
#### GET /api/v1/configs/{siteId}/script
**推荐使用**。不需要指定版本号,自动返回该站点可用的最新脚本。
优先级:自定义脚本 > 站点类型默认模板 > 404
```bash
# 下载 hdfans 脚本
curl http://par:9012/api/v1/configs/hdfans/script
# 有自定义脚本 → 返回自定义 Groovy
# 无自定义脚本 → 按 siteType 返回默认模板(如 nexusphp 类型)
# siteType 未知 → 404
```
返回 `Content-Type: text/plain;charset=UTF-8`。
#### GET /api/v1/configs/{siteId}/{version}/script
指定版本下载(仅返回该版本的自定义脚本,不回退默认模板)。
#### 集成流程
```
mediabot 启动
│
▼
GET /api/v1/manifest
│ { "siteId":"hdfans","siteType":"nexusphp","hasScript":false,... }
│ { "siteId":"mteam","siteType":"mtorrent","hasScript":true,... }
│
▼
GET /api/v1/configs/{siteId}/script ← 不传版本号,自动选最优
│
├─ 自定义脚本存在 → 返回自定义 Groovy
└─ 无自定义 → 按 siteType 返回默认模板(nexusphp/gazelle/unit3d/mtorrent)
│
▼
编译 Groovy 脚本 → 调用 sync(ctx) / search(ctx,params) / sign(ctx)
```
#### Manifest 新增字段
```json
{
"siteId": "mteam",
"latestVersion": "1",
"hasScript": true, // 新增:是否有 Groovy 脚本
"configHash": "abc...", // JSON 配置 hash(可能为 null)
"updatedAt": "2026-07-01T10:00:00"
}
```
#### 脚本入口
Groovy 脚本定义以下方法,mediabot 按需调用:
| 方法 | 参数 | 返回值 | 说明 |
|------|------|--------|------|
| `parse(content, url)` | content=页面内容, url=访问URL | Map{pageName,fields,extracted} | PAR 校验用(mediabot 不调) |
| `sync(ctx)` | ctx=PluginContext | Map{upload,download,bonus,seeding,...} | 同步站点数据 |
| `search(ctx, params)` | ctx=PluginContext, params={keyword,page} | List[Map] | 搜索种子 |
| `sign(ctx)` | ctx=PluginContext | Map{success,message} | 签到 |
> `PluginContext` 由 mediabot 提供,包含 `site.auth`(认证信息)、`siteUrl`、`httpClient` 等。
---
## 3. 数据结构
### 统一响应格式
```json
{
"code": 200,
"message": "success",
"data": { ... },
"timestamp": 1782800000
}
```
| code | 说明 |
|------|------|
| 200 | 成功 |
| 400 | 参数错误 |
| 401 | 认证失败 |
| 403 | 权限不足 |
| 404 | 资源不存在 |
| 500 | 服务器错误 |
### 站点 (Site)
| 字段 | 类型 | 说明 |
|------|------|------|
| id | Long | 内部 ID |
| siteId | String | 站点唯一标识(如 `pthome`) |
| names | String[] | 站点名称列表 |
| urls | String[] | 站点地址列表 |
| description | String | 描述 |
| iconUrl | String | 图标 URL |
| siteType | String | `private` / `public` |
| schemaVersion | String | 当前 Schema 版本 |
| status | String | `active` / `pending` / `deprecated` |
| createdAt | DateTime | 创建时间 |
### 配置版本 (SiteConfig)
| 字段 | 类型 | 说明 |
|------|------|------|
| id | Long | 内部 ID |
| siteId | String | 关联站点 |
| version | String | 版本号(如 `1.2.3`) |
| configHash | String | 内容 SHA-256(用于 ETag) |
| fileSize | Long | 文件大小(字节) |
| schemaVersion | String | Schema 版本 |
| status | String | `pending` / `approved` / `rejected` |
| isLatest | Boolean | 是否最新版本 |
### 站点快照 (SiteSnapshot / Bundle)
| 字段 | 类型 | 说明 |
|------|------|------|
| siteId | String | 站点唯一标识 |
| version | Integer | 快照版本号(数字递增) |
| checksum | String | 完整内容 SHA-256(用于 ETag) |
| generatedAt | DateTime | 快照生成/发布时间 |
| script | String | Groovy 脚本完整源码 |
| scriptVersion | String | 脚本版本号 |
| scriptSource | String | `custom` 或 `default` |
| pages | Array | 页面入口配置条目列表 |
| pagesVersion | String | 页面配置版本号 |
| pagesSource | String | `custom` 或 `default` |
| riskRules | Array | 风险规则列表 |
| riskRulesVersion | Integer | 风险规则版本号 |
---
## 4. 工作流程
### 4.1 首次接入
```
① 管理员在 PAR 生成 PSK → 提供给 mediabot
② medibot 用 PSK 注册(自动提权)
POST /api/v1/auth/register
Headers: X-Medibot-Timestamp, X-Medibot-Signature
Body: {"email": "mediabot@example.com"}
③ 提交站点
POST /api/v1/sites/pthome/submit (带 PSK 签名)
④ 提交适配脚本
POST /api/v1/configs/pthome/submit (带 PSK 签名)
⑤ 配置经管理员审核后变为 approved
```
### 4.2 系统重建恢复
```
① medibot 检测 401 → 调 POST /api/v1/auth/recover
(PSK 签名 → 自动重建账户 + TRUSTED)
② 所有旧凭据无需修改,签名方式不变
```
### 4.3 配置更新(两阶段发布)
```
① 开发者提交配置 → POST /api/v1/configs/{siteId}/submit
② 管理员审核通过 → 自动生成预览版快照(draft 状态)
③ 管理员在后台编辑站点 → 点击"保存预览版"(可选,手动触发生成预览版)
④ MediaBot 通过 ?preview=true 验证预览版效果
⑤ 管理员确认无误 → 点击"发布正式版"(draft → published,同步上传 CDN)
⑥ MediaBot 下次拉取正式版时自动获取最新配置
```
---
## 5. 代码示例
### Python
```python
import hashlib, hmac, base64, time, requests
PSK = "your-psk-from-admin"
BASE = "http://par-server:9012/api/v1"
EMAIL = "mediabot@example.com"
def get_server_time():
return requests.get(f"{BASE}/time").json()["timestamp"]
def hmac_headers(method, path):
ts = get_server_time()
sign_content = f"{method}:{path}:{ts}"
sig = base64.b64encode(
hmac.new(PSK.encode(), sign_content.encode(), hashlib.sha256).digest()
).decode()
return {
"X-Timestamp": str(ts),
"X-Signature": sig,
"X-Email": EMAIL
}
def submit_site(site_id, names, urls, site_type="private"):
path = f"/api/v1/sites/{site_id}/submit"
return requests.post(f"{BASE}{path}",
json={"names": names, "urls": urls, "siteType": site_type},
headers=hmac_headers("POST", path)
).json()
def submit_config(site_id, version, config_json, schema_version="1.0.0"):
path = f"/api/v1/configs/{site_id}/submit"
return requests.post(f"{BASE}{path}",
json={"version": version, "config": config_json, "schemaVersion": schema_version},
headers=hmac_headers("POST", path)
).json()
def list_configs(site_id):
return requests.get(f"{BASE}/configs/{site_id}").json()
def download_config(site_id, version):
resp = requests.get(f"{BASE}/configs/{site_id}/{version}")
if resp.status_code == 304:
return None # 未变化
return resp.json()
# ---- 使用 ----
submit_site("pthome", ["PThome", "PThome镜像"], ["https://pthome.net", "https://pthome.mirror"])
submit_config("pthome", "1.0.0", '{"rules":[...]}')
print(list_configs("pthome"))
```
### JavaScript (Node.js)
```javascript
const crypto = require('crypto');
const PSK = 'your-psk-from-admin';
const BASE = 'http://par-server:9012/api/v1';
const EMAIL = 'mediabot@example.com';
async function getServerTime() {
const res = await fetch(`${BASE}/time`);
return (await res.json()).timestamp;
}
function sign(method, path, timestamp) {
const content = `${method}:${path}:${timestamp}`;
const sig = crypto.createHmac('sha256', PSK).update(content).digest('base64');
return sig;
}
async function hmacHeaders(method, path) {
const ts = await getServerTime();
return {
'X-Timestamp': String(ts),
'X-Signature': sign(method, path, ts),
'X-Email': EMAIL
};
}
async function submitSite(siteId, names, urls, siteType = 'private') {
const path = `/api/v1/sites/${siteId}/submit`;
const res = await fetch(`${BASE}${path}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...await hmacHeaders('POST', path) },
body: JSON.stringify({ names, urls, siteType })
});
return res.json();
}
async function submitConfig(siteId, version, config, schemaVersion = '1.0.0') {
const path = `/api/v1/configs/${siteId}/submit`;
const res = await fetch(`${BASE}${path}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...await hmacHeaders('POST', path) },
body: JSON.stringify({ version, config, schemaVersion })
});
return res.json();
}
```