Files
par/docs/API.md

454 lines
11 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",
"latestVersion": "1.2.0",
"configHash": "a1b2c3d4...",
"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"
}
}
```
---
## 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 | 是否最新版本 |
---
## 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 配置更新
```
① GET /api/v1/configs/{siteId} 查看当前版本
② 准备新配置 JSON → POST /api/v1/configs/{siteId}/submit
③ 等待管理员审核通过
```
---
## 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();
}
```