# 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", "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(); } ```