11 KiB
11 KiB
PAR Server 集成开发文档
版本: v1.0 | Base URL:
http://<host>:9012
目录
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
// 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
// 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-SignaturePSK 签名头可自动提权为 TRUSTED
POST /api/v1/auth/recover
// 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,无数据传输。
# 轮询示例
curl -H "If-None-Match: a1b2c3d4..." http://par:9012/api/v1/manifest
# → 304 Not Modified (内容未变)
// 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
// 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}
// 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 表示内容未变化。
// Response 200 (Content-Type: application/json)
"{\"rules\": [...]}" // 原始配置 JSON 内容
POST /api/v1/configs/{siteId}/submit
// 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. 数据结构
统一响应格式
{
"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
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)
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();
}