Files
par/docs/API.md
2026-06-29 17:22:04 +08:00

10 KiB
Raw Blame History

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-Signature PSK 签名头可自动提权为 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",
        "name": "PThome",
        "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
{
  "name":        "PThome",           // 必填
  "baseUrl":     "https://pthome.net", // 必填
  "description": "PT 资源站",
  "iconUrl":     "https://...",
  "siteType":    "private"           // private/public
}

// Response 200
{
  "code": 200,
  "data": {
    "id": 1,
    "siteId": "pthome",
    "name": "PThome",
    "baseUrl": "https://pthome.net",
    "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)
name String 显示名称
description String 描述
baseUrl String 站点 URL
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, name, base_url, site_type="private"):
    path = f"/api/v1/sites/{site_id}/submit"
    return requests.post(f"{BASE}{path}",
        json={"name": name, "baseUrl": base_url, "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", "https://pthome.net")
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, name, baseUrl, 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({ name, baseUrl, 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();
}