# 网盘链接有效性检测 API — 使用文档 > 一个服务、**两个公开 API**、9 种网盘: > **夸克 · 百度 · 阿里 · UC · 天翼 · 123 · 115 · 迅雷 · 移动** - **版本**: v0.1.0 - **协议**: REST + Server-Sent Events (SSE) - **Swagger**: `$BASE/docs` · **ReDoc**: `$BASE/redoc` · **Health**: `$BASE/health` ## 两个公开 API | API | 用途 | 路径前缀 | 鉴权 | 典型调用方 | |----------------------|---------------------------------------------|--------------|-------------------|---------------------| | **① 检测 API** | 提交批量网盘 URL,订阅 SSE 收检测结果 | `/jobs*` | ✅ `X-API-Key` | 你的业务后端/爬虫 | | **② 处理池查询 API** | 查当前队列深度、累计有效/失效、网盘元数据 | `/pool*` | ❌ 公开无需密钥 | 监控/仪表盘/大屏 | 两个 API 完全独立:处理池 API **不暴露任何 URL / 用户数据**,只给计数与元信息,可 安全暴露给监控系统。 下文用 `$BASE` 代替服务地址(本机默认 `http://127.0.0.1:8787`),用 `$YOUR_API_KEY` 代替你自己的密钥。 --- ## 目录 1. [快速开始](#1-快速开始) 2. [支持的网盘 & URL 示例](#2-支持的网盘--url-示例) 3. [认证](#3-认证) 4. [端点总览](#4-端点总览) 5. [端点详解](#5-端点详解) - **① 检测 API(需密钥)** - 5.1 [POST /jobs — 提交检测任务](#51-post-jobs--提交检测任务) - 5.2 [GET /jobs/{id}/events — SSE 订阅结果](#52-get-jobsidevents--sse-订阅结果) - 5.3 [POST /jobs/{id}/urgent — 批量加急](#53-post-jobsidurgent--批量加急) - 5.4 [GET /jobs/{id} — 查任务概况](#54-get-jobsid--查任务概况) - 5.5 [DELETE /jobs/{id} — 取消任务](#55-delete-jobsid--取消任务) - **② 处理池查询 API(公开)** - 5.6 [GET /pool — 全网盘处理池快照](#56-get-pool--全网盘处理池快照) - 5.7 [GET /pool/{provider} — 单网盘处理池快照](#57-get-poolprovider--单网盘处理池快照) - **元信息** - 5.8 [GET /health — 健康检查](#58-get-health--健康检查) 6. [SSE 事件类型](#6-sse-事件类型) 7. [结果字段速查](#7-结果字段速查) 8. [错误码速查](#8-错误码速查) 9. [每个网盘的注意事项](#9-每个网盘的注意事项) 10. [最佳实践](#10-最佳实践) 11. [环境变量参考](#11-环境变量参考) 12. [常见问题 FAQ](#12-常见问题-faq) 13. [附录:完整 curl 一条龙脚本](#13-附录完整-curl-一条龙脚本) --- ## 1. 快速开始 **三步**:创建密钥 → 提交 job → 订阅 SSE。 ### 步骤 1:创建一把密钥 打开 `$BASE/admin`,填一个名字(比如 `dev`)点"生成密钥",**立即复制**(离开页面后明文不可再看)。 ### 步骤 2:提交检测任务 ```bash curl -X POST $BASE/jobs \ -H "Content-Type: application/json" \ -H "X-API-Key: $YOUR_API_KEY" \ -d '{ "items": [ {"url": "https://pan.quark.cn/s/xxxx"}, {"url": "https://pan.baidu.com/s/yyyy?pwd=8888"}, {"url": "https://www.alipan.com/s/zzzz"} ] }' ``` 响应示例: ```json { "job_id": "j_9f3c2a1b", "stream_url": "/jobs/j_9f3c2a1b/events", "items": [ {"item_id": "i_00000001", "url": "https://pan.quark.cn/s/xxxx", "provider": "quark"}, {"item_id": "i_00000002", "url": "https://pan.baidu.com/s/yyyy?pwd=8888", "provider": "baidu"}, {"item_id": "i_00000003", "url": "https://www.alipan.com/s/zzzz", "provider": "ali"} ] } ``` ### 步骤 3:订阅结果流(SSE) ```bash curl -N $BASE/jobs/j_9f3c2a1b/events \ -H "X-API-Key: $YOUR_API_KEY" ``` 即时输出(每条结果一段): ``` id: 1 event: result data: {"item_id":"i_00000001","url":"https://pan.quark.cn/s/xxxx","provider":"quark","valid":true,"reason":"all_checks_passed","share_name":"测试文件夹","expiration_label":"永久有效","elapsed_seconds":0.52,"priority_used":"normal"} id: 2 event: result data: {"item_id":"i_00000002","url":"https://pan.baidu.com/s/yyyy?pwd=8888","provider":"baidu","valid":false,"reason":"share_cancelled","elapsed_seconds":0.73,"priority_used":"normal"} id: 3 event: complete data: {"total":3,"valid":1,"invalid":2,"elapsed_seconds":1.84} ``` --- ## 2. 支持的网盘 & URL 示例 | 内部名 | 中文名 | 典型 URL 样例 | 默认并发 | |--------------|----------|---------------------------------------------------------|----------| | `quark` | 夸克网盘 | `https://pan.quark.cn/s/xxxxxxxxx` | 4 | | `baidu` | 百度网盘 | `https://pan.baidu.com/s/1xxxxxxxxx?pwd=abcd` | 3 | | `ali` | 阿里网盘 | `https://www.alipan.com/s/xxxxxxxxx` 或 `www.aliyundrive.com` | 2 | | `uc` | UC 网盘 | `https://drive.uc.cn/s/xxxxxxxxx` | 4 | | `tianyi` | 天翼网盘 | `https://cloud.189.cn/t/xxxxxxxxx` | 4 | | `p123` | 123 网盘 | `https://www.123pan.com/s/xxxxxxxxx` 或 `www.123865.com` | 4 | | `p115` | 115 网盘 | `https://115.com/s/xxxxxxxxx` 或 `115cdn.com` | 2 | | `xunlei` | 迅雷网盘 | `https://pan.xunlei.com/s/xxxxxxxxx` | 3 | | `mobile139` | 移动网盘 | `https://caiyun.139.com/m/i?xxx` 或 `yun.139.com` | 2 | 分类器按域名自动识别;未识别的链接会返回 `reason: "unsupported_provider"`。 ### URL 中的提取码 两种写法都支持,服务端会自动兼容: - **追加在 URL 末尾的查询参数**:`https://pan.baidu.com/s/yyyy?pwd=8888` - **独立字段**:`{"url": "https://pan.baidu.com/s/yyyy", "pwd": "8888"}` 如果两处都给了,以独立字段为准。 --- ## 3. 认证 所有 **业务接口**(`/jobs*`)都要求密钥。 ### 方式 A:请求头(推荐) ```http X-API-Key: sk_xxxxxxxxxxxxxxxxxxxxxxxx ``` 适用于:curl、Python、Node、PHP、Postman,任何可自定义 HTTP 头的客户端。 ### 方式 B:查询参数(仅 SSE 浏览器场景) ```http GET /jobs/j_xxx/events?api_key=sk_xxxxxxxxxxxxxxxxxxxxxxxx ``` **只对 SSE 端点生效**。原因:浏览器原生 `EventSource` API 不支持自定义头。 如果你用 `fetch` + `ReadableStream` 订阅 SSE,依然应优先用 `X-API-Key`。 ### 401 响应 密钥缺失 / 错误时: ```json {"detail": "missing api key", "code": "missing_api_key"} ``` 或 ```json {"detail": "invalid api key", "code": "invalid_api_key"} ``` ### 密钥管理 - 创建:`$BASE/admin` 界面,或 `POST /admin/keys` 接口 - 禁用:`PATCH /admin/keys/{id}` + `{"enabled": false}`(保留审计记录,随时可再启用) - 删除:`DELETE /admin/keys/{id}`(彻底清除) - 密钥 hash + 盐存在 `validator_api/data/api_keys.json`,明文**仅创建时返回一次** --- ## 4. 端点总览 ### ① 检测 API(需密钥) | 方法 | 路径 | 说明 | |--------|-----------------------------------|---------------------------------------| | POST | `/jobs` | 创建检测 job(混合 URL) | | GET | `/jobs/{id}/events` | SSE 结果流(支持 Last-Event-ID 重连) | | POST | `/jobs/{id}/urgent` | 批量加急(软抢占,未开始的会插队) | | GET | `/jobs/{id}` | 查 job 概况 / 各网盘进度 | | DELETE | `/jobs/{id}` | 取消 job(已完成的不受影响) | ### ② 处理池查询 API(公开) | 方法 | 路径 | 说明 | |--------|-----------------------------------|---------------------------------------| | GET | `/pool` | 全网盘处理池快照 + 每盘元数据 + 聚合 totals | | GET | `/pool/{provider}` | 单个网盘的处理池快照 | ### 元信息与管理 | 方法 | 路径 | 说明 | 密钥 | |--------|-----------------------------------|---------------------------------------|------| | GET | `/health` | 健康检查(兼容旧客户端;字段同 /pool) | — | | GET | `/admin` | 密钥管理 Web 界面 | —* | | GET | `/admin/keys` | 列出所有密钥 | —* | | POST | `/admin/keys` | 创建密钥 | —* | | PATCH | `/admin/keys/{id}` | 启用/禁用 / 改名 | —* | | DELETE | `/admin/keys/{id}` | 删除密钥 | —* | | GET | `/docs` | Swagger UI | — | | GET | `/redoc` | ReDoc | — | > \* 管理端点默认**无鉴权**(单机场景)。生产部署务必设置环境变量 > `VALIDATOR_API_ADMIN_TOKEN=<长随机串>`,之后访问 `/admin*` 需带 > `Authorization: Bearer ` 或 `?admin_token=`。 --- ## 5. 端点详解 ### 5.1 POST /jobs — 提交检测任务 **路径**:`POST $BASE/jobs` **响应**:`201 Created` #### 请求体 schema ```jsonc { "items": [ { "url": "https://pan.quark.cn/s/xxxx", // 必填 "pwd": "8888", // 可选:提取码 "item_id": "my-custom-id-42", // 可选:客户端自定义 ID(留空由服务端生成) "meta": { // 可选:任意透传字段,原样在 result 里返回 "source": "spider-run-2024-04-19", "batch_no": 7 } } // ... 最多 5000 条 / 次 ], "priority": "normal", // 可选,"normal"(默认) | "urgent" "timeout_ms": 15000 // 可选,单条上限;默认 15000 } ``` - `items` 数组元素数 ≤ `MAX_ITEMS_PER_JOB`(默认 5000) - 活跃 job 总数 ≤ `MAX_CONCURRENT_JOBS`(默认 32) - `item_id` 必须唯一;重复会返回 `400 duplicate_item_id` #### 响应体 schema ```jsonc { "job_id": "j_9f3c2a1b", // 服务端生成,字母+数字 "stream_url": "/jobs/j_9f3c2a1b/events", // 拼上 $BASE 直接可订阅 "items": [ { "item_id": "i_00000001", // 如果请求没给,服务端自动生成 "url": "https://pan.quark.cn/s/xxxx", "provider": "quark" // 分类器识别的网盘 } ] } ``` > 注意:**此时只返回分类信息,不含检测结果**。真正的结果通过 SSE 推送(见 5.2)。 #### 常见错误 | 状态 | 代码 | 含义 | |------|-------------------------|--------------------------------| | 400 | `empty_items` | items 为空 | | 400 | `too_many_items` | 超过 MAX_ITEMS_PER_JOB | | 400 | `duplicate_item_id` | 同一 job 里 item_id 冲突 | | 401 | `missing_api_key` 等 | 密钥问题 | | 429 | `too_many_jobs` | 活跃 job 数超过 MAX_CONCURRENT_JOBS | #### 代码示例
Python (httpx / aiohttp) ```python import httpx r = httpx.post( f"{BASE}/jobs", headers={"X-API-Key": KEY, "Content-Type": "application/json"}, json={"items": [ {"url": "https://pan.quark.cn/s/xxxx"}, {"url": "https://pan.baidu.com/s/yyyy", "pwd": "8888"}, {"url": "https://www.alipan.com/s/zzzz", "meta": {"tag": "batch-1"}}, ]}, ) r.raise_for_status() job = r.json() print("job_id:", job["job_id"]) ```
Node.js (fetch) ```javascript const r = await fetch(`${BASE}/jobs`, { method: "POST", headers: { "Content-Type": "application/json", "X-API-Key": KEY, }, body: JSON.stringify({ items: [ { url: "https://pan.quark.cn/s/xxxx" }, { url: "https://pan.baidu.com/s/yyyy", pwd: "8888" }, ], }), }); if (!r.ok) throw new Error(`HTTP ${r.status}`); const job = await r.json(); console.log("job_id:", job.job_id); ```
PHP (curl) ```php $ch = curl_init("$base/jobs"); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_HTTPHEADER => ["Content-Type: application/json", "X-API-Key: $key"], CURLOPT_POSTFIELDS => json_encode(["items" => [ ["url" => "https://pan.quark.cn/s/xxxx"], ["url" => "https://pan.baidu.com/s/yyyy", "pwd" => "8888"], ]]), CURLOPT_RETURNTRANSFER => true, ]); $job = json_decode(curl_exec($ch), true); curl_close($ch); echo $job["job_id"]; ```
--- ### 5.2 GET /jobs/{id}/events — SSE 订阅结果 **路径**:`GET $BASE/jobs/{job_id}/events` **Content-Type**:`text/event-stream` **支持**:`Last-Event-ID` 重连、心跳保活 ```bash curl -N $BASE/jobs/j_9f3c2a1b/events \ -H "X-API-Key: $YOUR_API_KEY" ``` #### SSE 帧格式(每帧以空行结束) ``` id: <单调递增数字,从 1 开始> event: <事件类型:result | urgent_ack | complete | gone> data: <单行 JSON> ``` 示例: ``` id: 1 event: result data: {"item_id":"i_00000001","url":"...","valid":true,"reason":"all_checks_passed",...} id: 2 event: result data: {"item_id":"i_00000002","url":"...","valid":false,"reason":"share_cancelled",...} id: 3 event: complete data: {"total":2,"valid":1,"invalid":1,"elapsed_seconds":1.23} ``` #### 心跳 无事件时每 `SSE_HEARTBEAT_SEC`(默认 15 秒)会发一条 **注释帧**(不是事件帧): ``` : heartbeat ``` 客户端解析时应忽略以 `:` 开头的行。 #### 断线重连 客户端在断线前记住最后收到的 `id`,重连时带上 `Last-Event-ID` 头,服务端 会从这个 id 的下一条开始重放(环形缓冲,容量 `EVENT_BUFFER_SIZE`,默认 10000): ```bash curl -N $BASE/jobs/j_xxx/events \ -H "X-API-Key: $YOUR_API_KEY" \ -H "Last-Event-ID: 42" ``` 如果 id 已超出环形缓冲范围,服务端会发一条 `gone` 事件然后关闭连接。 #### 浏览器原生 EventSource ```javascript // EventSource 不支持自定义 header,这里必须用查询参数传密钥 const es = new EventSource(`${BASE}/jobs/${jobId}/events?api_key=${KEY}`); es.addEventListener("result", (e) => console.log("result:", JSON.parse(e.data))); es.addEventListener("urgent_ack", (e) => console.log("urgent_ack:", JSON.parse(e.data))); es.addEventListener("complete", (e) => { console.log("done"); es.close(); }); es.addEventListener("gone", (e) => { console.warn("gone"); es.close(); }); ``` #### fetch + ReadableStream(跨语言通用骨架) ```javascript const r = await fetch(`${BASE}/jobs/${jobId}/events`, { headers: {"X-API-Key": KEY}, }); const reader = r.body.getReader(); const decoder = new TextDecoder(); let buf = ""; while (true) { const {value, done} = await reader.read(); if (done) break; buf += decoder.decode(value, {stream: true}); let idx; while ((idx = buf.indexOf("\n\n")) >= 0) { const frame = buf.slice(0, idx); buf = buf.slice(idx + 2); if (frame.startsWith(":")) continue; // 心跳 let id = "", ev = "message", data = ""; for (const line of frame.split("\n")) { if (line.startsWith("id:")) id = line.slice(3).trim(); if (line.startsWith("event:")) ev = line.slice(6).trim(); if (line.startsWith("data:")) data = line.slice(5).trim(); } console.log(id, ev, JSON.parse(data)); } } ``` --- ### 5.3 POST /jobs/{id}/urgent — 批量加急 **路径**:`POST $BASE/jobs/{job_id}/urgent` 场景:job 提交后想把某几条 URL 插队到队列前面(比如用户在前端点了"立即检测")。 只能加急 **还在队列里、未开始** 的 URL;已在跑 / 已完成的会忽略。 #### 请求体 ```jsonc { "urls": [ "https://pan.quark.cn/s/xxxx", "https://www.alipan.com/s/zzzz" ] } ``` 或按 item_id 加急: ```jsonc { "item_ids": ["i_00000003", "i_00000007"] } ``` `urls` 和 `item_ids` 至少填一个;都填则合并去重。 #### 响应 ```jsonc { "job_id": "j_9f3c2a1b", "promoted": 2, // 真正被推到高优先级队列的数量 "in_flight": 0, // 已经在跑(来不及加急) "already_done": 0, // 已完成 "not_found": 0 // URL/item_id 不在此 job 里 } ``` 一次加急请求后,SSE 流会额外推送一条 `urgent_ack` 事件: ``` id: 42 event: urgent_ack data: {"job_id":"j_9f3c2a1b","promoted":2,"in_flight":0,"already_done":0,"not_found":0} ``` > **软抢占**:加急是"把同一任务用更高优先级 **再入队一次**"。worker 拿到老的 > 低优先级副本时检查版本号会跳过。已经在跑的不会被 cancel(避免资源浪费)。 --- ### 5.4 GET /jobs/{id} — 查任务概况 **路径**:`GET $BASE/jobs/{job_id}` 不订阅 SSE,只想拉一次 snapshot: ```bash curl $BASE/jobs/j_9f3c2a1b -H "X-API-Key: $YOUR_API_KEY" ``` 响应: ```jsonc { "job_id": "j_9f3c2a1b", "created_at": 1713520000, "state": "running", // queued | running | done | cancelled "priority_default": "normal", "total": 50, "done": 23, "valid": 12, "invalid": 11, "by_provider": { "quark": {"total": 20, "done": 10, "valid": 6, "invalid": 4}, "baidu": {"total": 15, "done": 8, "valid": 4, "invalid": 4}, "ali": {"total": 15, "done": 5, "valid": 2, "invalid": 3} }, "elapsed_seconds": 12.4 } ``` --- ### 5.5 DELETE /jobs/{id} — 取消任务 **路径**:`DELETE $BASE/jobs/{job_id}` **响应**:`204 No Content` ```bash curl -X DELETE $BASE/jobs/j_9f3c2a1b -H "X-API-Key: $YOUR_API_KEY" ``` - 未开始的任务:**立即从队列移除**,不会再跑。 - 正在跑的任务:**不会中断**(避免浪费已建立的 HTTP/浏览器会话),但结果会丢弃 不再推送到 SSE。 - SSE 订阅会收到一条 `gone` 事件然后连接关闭。 --- ### 5.6 GET /pool — 全网盘处理池快照 **路径**:`GET $BASE/pool` **无需密钥**(公开接口,仅返回计数与静态元数据,不暴露任何 URL / 用户数据) 这是 **处理池查询 API 的主入口**。适合: - 仪表盘 / 大屏 / 监控(建议 2–5 秒一次) - Prometheus exporter(把 counters 转成指标暴露) - 告警脚本(例如 `pending` 长期高于某阈值) - 容量规划(看哪个网盘常年打爆队列) #### 请求 ```bash curl $BASE/pool ``` #### 响应(PoolSnapshot) ```jsonc { "ok": true, "timestamp": 1713520000, // 服务端 Unix 秒,用于新鲜度判断 "active_jobs": 3, // 当前活跃 job 数 "supported_providers": [ // 推荐的 UI 展示顺序 "quark", "baidu", "ali", "uc", "tianyi", "p123", "p115", "xunlei", "mobile139" ], "providers": { "quark": { "provider": "quark", "label": "夸克网盘", "queue_depth": 12, // 实时:队列里排队的任务数 "in_flight": 4, // 实时:worker 正在跑的任务数 "pending": 16, // 实时:queue_depth + in_flight "total": 320, // 累积:进程启动以来跑完的总数 "valid": 210, // 累积:有效计数 "invalid": 110, // 累积:无效计数 "workers": 4, // worker 协程数(= 该网盘的并发上限) "info": { "method": "HTTP 直接请求(POST token → GET detail)", "endpoints": ["POST /1/clouddrive/share/sharepage/token", ...], "latency_hint": "≈ 0.4–0.9 秒 / 条", "rate_limit": "", // 空串 = 无已知限流问题 "notes": ["两点判定:expired_type 未过期 + 文件数 & 总大小都 > 0。", ...] } }, // ... 其余 8 个网盘 }, "totals": { // 跨网盘聚合 "queue_depth": 34, "in_flight": 10, "pending": 44, "total": 1000, "valid": 650, "invalid": 350, "workers": 29 } } ``` 字段语义说明: - **实时字段**:`queue_depth` / `in_flight` / `pending` 反映当前这一瞬间。 - **累积字段**:`total` / `valid` / `invalid` 从**进程启动**开始计数,服务重启归零。 如需跨重启持久化请用外部存储(Prometheus / InfluxDB)。 - **`info` 字段**:静态元数据,跨请求不变;不需要每次都取的可以缓存。 #### 轻量版(可选) 如果你只想要计数不想要 `info`(比如 Prometheus exporter 高频抓取),用 `GET /health` 即可 —— 同样开放,字段更紧凑。 #### 代码示例
Python(同步拉一次) ```python import httpx, time while True: pool = httpx.get(f"{BASE}/pool").json() print(f"active_jobs={pool['active_jobs']}, pending={pool['totals']['pending']}") for name, p in pool["providers"].items(): if p["pending"] > 100: print(f"⚠️ {p['label']} 积压 {p['pending']} 条") time.sleep(5) ```
Node.js(Prometheus 风格 metrics exporter 骨架) ```javascript // 把 /pool 转为 Prometheus 文本 import http from "node:http"; http.createServer(async (req, res) => { if (req.url !== "/metrics") { res.writeHead(404); return res.end(); } const pool = await (await fetch(`${BASE}/pool`)).json(); const lines = []; for (const [name, p] of Object.entries(pool.providers)) { const tag = `provider="${name}"`; lines.push(`validator_pool_pending{${tag}} ${p.pending}`); lines.push(`validator_pool_total{${tag}} ${p.total}`); lines.push(`validator_pool_valid{${tag}} ${p.valid}`); lines.push(`validator_pool_invalid{${tag}} ${p.invalid}`); lines.push(`validator_pool_workers{${tag}} ${p.workers}`); } res.writeHead(200, {"Content-Type": "text/plain; version=0.0.4"}); res.end(lines.join("\n") + "\n"); }).listen(9100); ```
Shell(简易告警) ```bash #!/usr/bin/env bash # 每条网盘 pending > 200 就报警 threshold=200 curl -sS $BASE/pool | python -c " import sys, json p = json.load(sys.stdin)['providers'] for k, v in p.items(): if v['pending'] > $threshold: print(f'ALERT {v[\"label\"]} pending={v[\"pending\"]}') " ```
--- ### 5.7 GET /pool/{provider} — 单网盘处理池快照 **路径**:`GET $BASE/pool/{provider}` **无需密钥** 只拉某个网盘的快照。响应体就是 `PoolSnapshot.providers[]` 单独返回(`PoolProviderSnapshot`)。 #### 参数 `provider` 必须是以下之一: `quark` · `baidu` · `ali` · `uc` · `tianyi` · `p123` · `p115` · `xunlei` · `mobile139` #### 示例 ```bash curl $BASE/pool/ali ``` 响应: ```jsonc { "provider": "ali", "label": "阿里网盘", "queue_depth": 5, "in_flight": 2, "pending": 7, "total": 120, "valid": 80, "invalid": 40, "workers": 2, "info": { "method": "HTTP 直接请求(get_share_by_anonymous)", "endpoints": ["POST /adrive/v3/share_link/get_share_by_anonymous"], "latency_hint": "≈ 0.4–0.8 秒 / 条", "rate_limit": "⚠️ 频繁请求会返回 HTTP 429;已启用 UA 轮换 + 抖动 + 指数退避", "notes": ["判定 A:expiration 未过期;...", ...] } } ``` #### 错误 - **404** — provider 不在支持列表里: ```json {"detail": "unknown provider: 'weiyun'. supported: quark, baidu, ali, uc, tianyi, p123, p115, xunlei, mobile139"} ``` --- ### 5.8 GET /health — 健康检查 **路径**:`GET $BASE/health` **无需密钥** 兼容性保留端点。响应字段与 `/pool` 部分重合,但结构略旧(独立的 `provider_labels` / `provider_info`,没有 `totals` 和 `timestamp`)。 > **新代码优先用 `/pool`**;`/health` 主要给 k8s liveness / 老监控脚本用。 ```bash curl $BASE/health ``` 响应: ```jsonc { "status": "ok", "started": true, "active_jobs": 3, "providers": { "quark": {"queue_depth": 12, "in_flight": 4, "pending": 16, "total": 320, "valid": 210, "invalid": 110, "workers": 4}, // ... }, "provider_labels": {"quark": "夸克网盘", ...}, "provider_info": {...}, "supported_providers": ["quark", "baidu", ...] } ``` --- ## 6. SSE 事件类型 每条 SSE 帧的 `event:` 字段必然是下列 4 种之一: ### `result` — 单条 URL 的检测结果 **频率**:每条 URL 完成检测就发一条。 **data 字段详解**: | 字段 | 类型 | 说明 | |---------------------|---------|------------------------------------------------------------------------| | `item_id` | string | 创建 job 时分配的唯一 ID,用于关联原始 URL | | `url` | string | 原始链接(跟请求里保持一致) | | `provider` | string | 识别到的网盘:`quark`/`baidu`/`ali`/`uc`/`tianyi`/`p123`/`p115`/`xunlei`/`mobile139`/`unknown` | | `valid` | bool | 是否有效 | | `reason` | string | 结论细分码,见 [结果字段速查](#7-结果字段速查) | | `share_name` | string? | 分享名(若能解析到) | | `expiration_label` | string? | 人类可读的有效期:"永久有效" / "剩余 7 天" / "已过期" | | `elapsed_seconds` | number | 该条检测耗时(秒,保留两位小数) | | `priority_used` | string | `normal` / `urgent`(实际跑时的优先级,不是提交时的) | | `meta` | object? | 创建时客户端透传的自定义字段,原样返回 | | `error` | string? | 运行时异常的 `Type: message`(仅错误场景) | | `http_status` | int? | 上游 HTTP 状态(如阿里 429 时会带) | ### `urgent_ack` — 加急请求的反馈 ```jsonc { "job_id": "j_9f3c2a1b", "promoted": 2, "in_flight": 0, "already_done": 0, "not_found": 0 } ``` ### `complete` — 整个 job 跑完 **总是最后一条**(除非被取消)。 ```jsonc { "total": 50, "valid": 32, "invalid": 18, "elapsed_seconds": 18.7 } ``` 收到这条后可以主动 `close()` SSE 连接。 ### `gone` — job 已不存在 / 已被取消 ```jsonc { "job_id": "j_9f3c2a1b", "message": "job cancelled" } ``` 收到这条后连接会被服务端主动关闭。客户端应停止重试。 --- ## 7. 结果字段速查 ### `reason` 常见取值 | reason | valid | 说明 | |--------------------------|-------|----------------------------------------------------------| | `all_checks_passed` | ✅ true | 分享有效、未过期、内容非空 | | `empty_share` | ❌ false | 分享存在但内容为空(多见于阿里空目录) | | `share_cancelled` | ❌ false | 分享者已取消 / 撤销分享 | | `share_expired` | ❌ false | 已过期(按分享本身的 expiration) | | `share_not_found` | ❌ false | 分享码不存在(多见于误拼 URL) | | `wrong_pwd` | ❌ false | 需要提取码但没给 / 给错了 | | `unsupported_provider` | ❌ false | URL 无法识别为任何已知网盘 | | `worker_error` | ❌ false | 检测器抛异常(配合 `error` 字段看栈) | | `timeout` | ❌ false | 单条检测超过 `timeout_ms` | | `rate_limited` | ❌ false | 上游 429 且重试耗尽(主要是阿里) | | `upstream_error` | ❌ false | 上游返回非预期状态 / 结构 | ### `expiration_label` 示例 - `"永久有效"` — 无过期时间 - `"剩余 7 天"` / `"剩余 3 小时"` — 即将过期 - `"已过期"` — 虽然分享还在,但明确返回已过期 - `null` — 上游没给过期信息(有些网盘不暴露) --- ## 8. 错误码速查 ### HTTP 状态 | 状态 | 场景 | |------|------------------------------------------------| | 200 | SSE 正常建立 / Health OK | | 201 | 创建 job 成功 | | 204 | 取消 job 成功 | | 400 | 参数错误(items 为空、超量、item_id 冲突等) | | 401 | 密钥缺失 / 错误 / 已禁用 | | 404 | job_id 不存在(已被清理 / 拼错) | | 409 | (预留)并发冲突 | | 429 | 活跃 job 超过 MAX_CONCURRENT_JOBS | | 500 | 服务端未预期异常(查日志 `validator_api.*`) | | 503 | 服务正在启动 / 关闭,Scheduler 未就绪 | ### 错误响应体 所有 4xx/5xx 都是统一结构: ```json { "detail": "job not found", "code": "job_not_found" } ``` 客户端应以 `code` 字段做分支,`detail` 用于日志打印。 --- ## 9. 每个网盘的注意事项 > 下述元数据与首页悬浮模块"处理池 → 点行展开"看到的一致,也可通过 `GET /health` 里的 `provider_info` 拉到。 ### 夸克 `quark` - **检测方式**:HTTP 直接请求(POST token → GET detail) - **判定逻辑**:`expired_type` 未过期 + `file_count > 0` + `total_size > 0` - **频率限制**:无已知限制;默认并发 4 - **失效标志**:HTTP 400 / 403 / 404 ### 百度 `baidu` - **检测方式**:HTTP 直接请求(`/share/verify` + HTML 兜底) - **频率限制**:⚠️ 轻度频率敏感,集中刷会偶发 403 - **特殊支持**:尾随提取码文本(`"url 8888"`、`"url?pwd=8888"`、`"url 提取码:8888"`)全兼容 ### 阿里 `ali` - **检测方式**:HTTP 直接请求(`/adrive/v3/share_link/get_share_by_anonymous`) - **判定 A**:`expiration` 未过期 - **判定 B**:`file_infos` 非空(空目录按 `empty_share` 判无效) - **频率限制**:⚠️ **强烈限流**,HTTP 429;已启用 UA 轮换 + 抖动 + 指数退避 - **建议**:单批 ≤ 200 条;超过请分批提交以避免 IP 层面限流 ### UC `uc` - **检测方式**:HTTP 直接请求(`/1/clouddrive/share/sharepage/v2/detail`) - **主判定锚点**:`data.token_info.author.nick_name` - **失效标志**:HTTP 404 + code 41006 - **频率限制**:无;默认并发 4 ### 天翼 `tianyi` - **检测方式**:HTTP 直接请求(XML 响应) - **接口**:`/api/open/share/getShareInfoByCodeV2.action` - **响应格式**:XML(个别场景 JSON),已双格式兼容 - **失效标志**:`ShareInfoNotFound` 或 HTTP 302 重定向到 404 页 ### 123 `p123` - **检测方式**:HTTP 直接请求(`/gsb/s/{shareKey}`) - **判定**:`info.code==0` 且 `data.Expired==false` - **失效标志**:code=5104 / `message='分享链接已失效'` ### 115 `p115` - **检测方式**:HTTP 直接请求(`/webapi/share/snap`) - **频率限制**:⚠️ 有频次限制,默认并发压至 2 - **跳转**:`pan.115.com` 自动跟随到 `115cdn.com` - **提取码**:需要时通过 `share_code + receive_code` 组合判定 ### 迅雷 `xunlei` - **检测方式**:HTTP-like(Playwright 引导拿 captcha token 后重放) - **bootstrap**:首次请求启动隐藏页面抓 `x-captcha-token`,后续复用 - **耗时**:≈ 0.7–3.0 秒 / 条(含 bootstrap) ### 移动 `mobile139` - **检测方式**:**Playwright 浏览器**(响应体加密,hook `JSON.parse` 解码) - **接口**:`/yun-share/.../IOutLink/getOutLinkGeneral`、`/getOutLinkInfoV6` - **频率限制**:⚠️ 接口加密 + 需浏览器,并发与速度都受限 - **耗时**:≈ 1.5–5.0 秒 / 条,**最慢的一个** - **建议**:单批 ≤ 50 条 --- ## 10. 最佳实践 ### 10.1 批量大小 - **推荐**:单个 job 100–500 条混合 URL - **硬顶**:5000 条 / job - **阿里超 200 条** 或 **移动超 50 条** 分批提交 ### 10.2 优先级 & 加急 - 常规业务用 `priority: "normal"`(默认) - 如果你的产品有"前台用户主动点击检测"场景: 1. 先把完整批次用 `normal` 提交(后台慢慢跑) 2. 用户点击时调 `POST /jobs/{id}/urgent` 把前台那几条插队 3. 前台订阅同一个 SSE,看到 `result` 就更新 UI ### 10.3 SSE 重连 - 客户端本地记最后收到的 `id` - 断线时 sleep 1-2s 再重连,带 `Last-Event-ID: ` - 如果收到 `gone` 事件,**不要重试**(job 已被取消或清理) ### 10.4 缓存结果 服务端**不缓存**(每次都真跑一遍)。客户端可以按 `url` 做 LRU / TTL 缓存: - ✅ 结果:缓存 1-24 小时(分享有效期内结果一般不变) - ❌ 结果:缓存 5-30 分钟(用户可能重新分享) - `rate_limited` / `worker_error`:**不要缓存**(临时性错误) ### 10.5 错误处理策略 ```python # 伪代码 for event in sse_stream: if event.type == "result": if event.reason in {"rate_limited", "timeout", "upstream_error"}: # 临时错误:排入重试队列,下次再提交一次 retry_queue.push(event.url, delay=60) elif event.reason == "worker_error": # 程序性错误:立即告警 alert(event) else: # 业务结果:写库 db.write(event) ``` ### 10.6 监控指标 从 `/health` 抓: - `providers..queue_depth` 持续攀升 → 该网盘处理不过来,考虑增加并发或限流上游提交 - `providers..invalid / total` 比例突变 → 可能上游接口变了,需排查 - `active_jobs` 接近 `MAX_CONCURRENT_JOBS` → 客户端积压过多,考虑合并 --- ## 11. 环境变量参考 | 变量 | 默认 | 说明 | |-------------------------------|----------------------|----------------------------------------------------------| | `VALIDATOR_API_KEYS` | _(空)_ | 预置密钥,格式 `sk1=name1,sk2=name2`(可选,主要用 /admin) | | `VALIDATOR_API_ADMIN_TOKEN` | _(空=无鉴权)_ | **生产必设**。之后 `/admin*` 必须带 Bearer token | | `VALIDATOR_API_HOST` | `127.0.0.1` | 监听地址 | | `VALIDATOR_API_PORT` | `8787` | 监听端口 | | `VALIDATOR_API_TIMEOUT_MS` | `15000` | 单条检测默认超时(毫秒) | | `VALIDATOR_API_HEARTBEAT_SEC` | `15` | SSE 心跳间隔(秒) | | `VALIDATOR_API_MAX_ITEMS` | `5000` | `MAX_ITEMS_PER_JOB` | | `VALIDATOR_API_MAX_JOBS` | `32` | `MAX_CONCURRENT_JOBS` | | `VALIDATOR_API_LOG_LEVEL` | `INFO` | `DEBUG`/`INFO`/`WARNING`/`ERROR` | 启动命令示例: ```bash # Windows PowerShell $env:VALIDATOR_API_ADMIN_TOKEN="a_long_random_string_here" python -m validator_api serve --host 0.0.0.0 --port 8787 # Linux/macOS export VALIDATOR_API_ADMIN_TOKEN="a_long_random_string_here" python -m validator_api serve --host 0.0.0.0 --port 8787 ``` --- ## 12. 常见问题 FAQ ### Q1. 为什么我提交后 `GET /jobs/{id}` 看到的 `done=0`? 因为异步。job 刚创建时所有 item 都在队列里,worker 还没开跑。用 SSE 订阅 流是最及时的方式;轮询 `GET /jobs/{id}` 也行,但建议 ≥ 2 秒一次。 ### Q2. SSE 不断掉但 10 分钟没数据,是不是卡了? 不是。心跳帧 `: heartbeat` 每 15 秒一次。如果连这个都没有,才是连接真的 断了(中间代理关了)。 ### Q3. 批量 500 条,结果推了一大半后断了 这是常见的代理超时(nginx 默认 60s)。解决: - 部署时给 nginx 加 `proxy_read_timeout 3600; proxy_buffering off;` - 或者客户端实现 `Last-Event-ID` 自动重连 ### Q4. 同一批里混入的迅雷/移动 URL 拖慢了整体速度吗? 不会。每个网盘独立队列 + 独立 worker 池。夸克快的链接不会等迅雷的慢链接。 ### Q5. `meta` 字段有大小限制吗? 本身没有硬性限制,但**强烈建议 < 2KB / 条**。整个 request body 受 FastAPI 默认 body 大小(通常 10MB)限制。 ### Q6. 我能把 API 部署到公网吗? 可以,但务必: 1. 设 `VALIDATOR_API_ADMIN_TOKEN` 保护 `/admin` 2. 在 nginx / CDN 层加 IP 白名单或速率限制 3. 不要在 URL 查询参数里漏用户密钥(永远用 `X-API-Key` 头) --- ## 13. 附录:完整 curl 一条龙脚本 把提交 → 订阅串起来的懒人脚本: ```bash #!/usr/bin/env bash set -euo pipefail BASE="${BASE:-http://127.0.0.1:8787}" KEY="${VALIDATOR_API_KEY:?please export VALIDATOR_API_KEY}" # 1. 提交,抓出 job_id JOB=$(curl -sS -X POST "$BASE/jobs" \ -H "Content-Type: application/json" \ -H "X-API-Key: $KEY" \ -d '{ "items": [ {"url": "https://pan.quark.cn/s/xxxx"}, {"url": "https://pan.baidu.com/s/yyyy", "pwd": "8888"}, {"url": "https://www.alipan.com/s/zzzz"} ] }' | python -c "import sys,json;print(json.load(sys.stdin)['job_id'])") echo "job_id=$JOB" # 2. 订阅流(收到 complete 后 Ctrl-C 或脚本 exit) curl -N -sS "$BASE/jobs/$JOB/events" \ -H "X-API-Key: $KEY" | while IFS= read -r line; do case "$line" in event:*) EV="${line#event: }" ;; data:*) echo "[$EV] ${line#data: }" ;; '') [[ "${EV:-}" == "complete" ]] && break ;; esac done ``` --- ## 版权与变更 - 源码仓库:本地 `validator_api/` 目录 - 详尽设计文档:`impl实现说明.md` / `api实现说明.md` / `需求文档.md` - Swagger:随代码自动生成,见 `$BASE/docs` 反馈与改进:直接改代码 → 跑 `python -m unittest validator_api.tests.test_smoke` 确保 23/23 通过 → 重启服务即可。 _last updated: 2026-04-19_