# PAR Server 集成开发文档 > 版本: v1.0 | Base URL: `http://: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 通过 `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", "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 ```json // 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} ```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`) | | 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 ```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) ```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, 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(); } ```