Files
2026-10-06 18:54:03 +08:00

2059 lines
82 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ProxyCat 接口文档
本文是 ProxyCat 对外 HTTP 接口的**唯一权威文档**,覆盖两套接口:
- **面板接口**(`/api/*`)—— 面板自身的接口,由 `app.py` 提供。
- **代理池接口**(`/api/pool/*`)—— 由 `modules/proxypool_api.py` 注册的 Flask 蓝图。
两套接口**共用同一个端口、同一套鉴权**,但**错误响应的约定不同**(面板多为
`HTTP 200 + status: "error"`,代理池用真实状态码 + `error_code`)。两套约定都在本文中
写明,调用前请先读 [一、通用约定](#一通用约定)。
> 历史版本把代理池接口的字段细节放在 `modules/proxypool/API.md`,该文件现已并入本文,
> 只剩一个指向本页的指针。
## 目录
- [分组导航](#分组导航)
- [一、通用约定](#一通用约定)
- [二、面板接口](#二面板接口)
- [三、代理池接口](#三代理池接口)
- [四、端到端调用示例](#四端到端调用示例)
- [五、注意事项](#五注意事项)
- [附录 A:错误码总表](#附录-a错误码总表)
- [附录 B:端点总索引](#附录-b端点总索引)
- [附录 C:字段字典](#附录-c字段字典)
## 分组导航
| 组 | 端点数 | 说明 |
|---|---|---|
| [1. 通用约定](#一通用约定) | — | 地址、鉴权、两套错误约定、参数解析规则、通用上限 |
| [2. 面板接口](#二面板接口) | 33 | 面板自身的 `/api/*`,含状态、配置、日志、访问记录 |
| [3. 代理池接口](#三代理池接口) | 38 | 代理池的 `/api/pool/*`,含抓取、验证、任务与数据库维护 |
按用途快速定位:
| 我想…… | 去这里 |
|---|---|
| 看服务当前在用什么代理 | [获取运行状态](#获取运行状态get-apistatus) |
| 改配置 | [保存配置](#保存配置post-apiconfig) |
| 看某个请求为什么失败 | [查询访问记录](#查询访问记录get-apilogsrecords) |
| 看某个域名走哪个上游成功率如何 | [查询域名统计](#查询域名统计get-apilogsdomains) |
| 拿一个可用代理给别的程序用 | [取一个代理](#取一个代理get-apipoolrandom) |
| 手动触发一次抓取 | [立即执行插件](#立即执行插件post-apipoolpluginsnamerun) |
| 看一个跑得久的任务到哪了 | [查询任务状态](#查询任务状态get-apipooltaskstask_id) |
---
# 一、通用约定
## 服务地址与端口
- 默认地址:`http://localhost:5001`,端口取自 `config/config.ini` 的 `[Server] web_port`。
- 面板与代理池**没有各自独立的进程或端口**,都运行在 ProxyCat 进程内,随进程一起启停。
- 代理监听端口(默认 `1080`)是另一个端口,只接受 HTTP 与 SOCKS5 代理请求,
**不提供任何 HTTP 接口**。
## 鉴权
**除下文明确标注「免鉴权」的端点外,所有接口都需要带上 token:**
```
GET http://localhost:5001/api/status?token=<你的token>
```
token 在 `config/config.ini` 的 `[Server] token`。**校验只读查询串参数 `token`**,
不读请求头,也不读 Cookie。
**token 留空时一律放行**,即不启用鉴权。此时任何能连上面板端口的人都可以读写配置
(包括上游凭据)、查看访问记录、启停服务。仅在本机使用时可这样配置;要暴露到网络上
必须设置 token。
> **仓库自带的 `config/config.ini` 里 token 非空**,克隆下来直接用会得到 401。
> 见 [README 的快速部署](README.md#快速部署)。
**免鉴权的 API 端点只有 4 个**(`/` 与 `/static/<path>` 也不校验 token,见[附录 B](#附录-b端点总索引)):
| 方法 | 路径 |
|---|---|
| GET | `/api/version` |
| GET | `/api/ads` |
| POST | `/api/ads/dismiss` |
| POST | `/api/ads/reopen` |
鉴权失败统一返回 **HTTP 401**:
```json
{
"status": "error",
"message": "无效的访问令牌"
}
```
## 两套错误约定
**这是本文最容易踩的地方:两套接口的错误表达方式不同,必须分别对待。**
| 情形 | 面板 `/api/*` | 代理池 `/api/pool/*` |
|---|---|---|
| 成功 | `200` + `status: "success"`;少数只读端点**直接返回数据、没有 `status` 字段** | `200` + 数据;错误时才带 `status` |
| 业务失败 | **`200`** + `status: "error"` + `message` | **真实状态码**(4xx/5xx)+ `status: "error"` + `message` + `error_code` |
| 参数错误 | `400`,仅 `POST /api/config` 会这样返回 | `400` + `error_code: "invalid_parameter"` |
| 未鉴权 | `401` + `status: "error"` | `401` + `status: "error"`(同一个钩子) |
| 池未运行 / 池太慢 | 不适用 | `503` `pool_unavailable` / `504` `pool_timeout` |
由此得出两条硬规则:
1. **永远同时判断 HTTP 状态码与 `status` 字段。** 面板侧只看状态码会漏掉业务失败,
只看 `status` 又会漏掉鉴权与池侧错误。
2. **程序化分支只能依赖 `error_code`(池侧)或 HTTP 状态码 + 端点自有标志(面板侧),
绝不能用 `message`。** `message` 的文案跟随当前界面语言(`[Server] language`),
同一个错误在不同语言下返回不同文本。
## 参数解析规则(仅代理池接口)
代理池适配层按**处理函数的签名**解析参数,规则如下:
| 规则 | 说明 |
|---|---|
| 查询参数是**白名单** | 只有出现在处理函数签名里的查询参数才会被解析,**未知参数被静默忽略**。写错参数名不会报错,只会得到未筛选的全量结果 |
| 类型强制 | 依据类型注解,只识别 `bool` / `int` / `float`,其余按字符串原样传递 |
| 空串 | 非字符串类型的参数传空串(如 `?status=`)等同于**不传**该参数 |
| 请求体 | 必须是 JSON 对象;缺失必填字段返回 `400 invalid_parameter`,并在 `message` 里列出字段名 |
| 路径参数 | 如 `/api/pool/proxies/{id}/favorite` 中的 `{id}`,由路由本身解析 |
## 通用上限与截断
| 位置 | 上限 / 默认 | 说明 |
|---|---|---|
| `GET /api/logs` 的 `limit` | 钳制到 1-2000,默认 200 | 超出被静默钳制,不报错 |
| `GET /api/logs/file` 的 `lines` | 钳制到 1-5000,默认 500 | 同上 |
| `GET /api/logs/records` 的 `limit` | 钳制到 1-500,默认 100 | 同上;`since` 默认「1 小时前」 |
| 访问记录导出 | 单次最多 50000 条 | 被截断的标记只有 JSON 格式的元信息里有(`truncated`,条数达到上限即为 `true`),CSV 没有该标记 |
| 代理池 `page_size` / `limit` / 导出 | 统一受 50000 约束 | `limit` 存在时 `page` 被强制为 1,等效「取前 limit 条」 |
| `GET /api/version` | 只读缓存,**不发起请求** | 检查由进程启动时与每 24 小时的后台任务完成 |
| `POST /api/version/check` | **会阻塞当前 Web 线程**至本次检查结束(最长约 15 秒) | 并发调用合并到正在跑的那一次;会重置 24 小时计时 |
## 阻塞与并发
面板由 **waitress 以 16 个线程**提供服务。每次调用代理池,**当前 Web 线程会一直被占用
到池返回结果**,最长可达 60 秒(HEAVY 档)。因此 16 个并发的大批量操作就会把面板
拖到无响应。调用重接口请控制并发,并给客户端留足够长的超时。
代理池的三档内部超时(会体现在错误码上):
| 档 | 时限 | 用于 |
|---|---|---|
| QUERY | 10 秒 | 查询、统计、状态 |
| MUTATION | 20 秒 | 启停、单条修改 |
| HEAVY | 60 秒 | 导入、导出、数据库优化、手动执行插件、批量验证/删除 |
---
# 二、面板接口
## 页面与静态资源
### 打开面板(GET /web)
**鉴权:需要 token。**
`GET /web` 返回面板单页。token 校验与其他接口一致,所以**设置了 token 时,
直接访问 `http://127.0.0.1:5001/` 会在跳转后得到 401**。正确地址是:
```
http://127.0.0.1:5001/?token=<你的token>
```
`GET /` 本身不校验,只把 `?token=` 原样带上去跳转到 `/web`:
| 请求 | 响应 |
|---|---|
| `GET /?token=abc` | `302` → `/web?token=abc` |
| `GET /`(无 token) | `302` → `/web` |
`GET /static/<path>` 提供 `web/static/` 下的前端资源。带 `?v=<版本>` 查询参数的请求
返回 `Cache-Control: public, max-age=31536000, immutable`(页面里的每个静态引用都带该
参数,改版后参数变化、浏览器自然取新文件);不带参数的请求返回 `Cache-Control: no-cache`,
每次回源校验。
## 运行状态
### 获取运行状态(GET /api/status)
无请求参数。返回出口代理服务的实时快照。
```json
{
"current_proxy": "http://1.2.3.4:8080",
"mode": "cycle",
"port": 1080,
"interval": 300,
"time_left": 123.4,
"switch_countdown_mode": "time",
"total_proxies": 10,
"active_proxies": [
{ "url": "http://1.2.3.4:8080", "inflight": 3, "active": 2, "capacity": 0, "failures": 0, "suspect": false, "failure_ratio": 0.0, "last_used": 12.5 }
],
"proxy_source_mode": "local",
"exit_count": 5,
"elastic_active": true,
"auth_required": false,
"display_level": 1,
"service_status": "running",
"language": "cn",
"request_interval": 0,
"pool_running": true,
"source_error": null
}
```
| 字段 | 说明 |
|---|---|
| `current_proxy` | **最近一次分配出去的**上游出口(已脱敏),不是「当前那个代理」—— 多出口下请求是按负载摊到整组出口上的;本地来源没有可用出口时是该语言的「无」文案,非本地来源此时为空串。整组状态见 `active_proxies` |
| `mode` | 运行模式。**同一个键在两种来源下是两件事**:本地来源问「挑谁」——`cycle`(按清单顺序,忽略负载)/ `loadbalance`(谁压力小先用谁);API 与维护池问「什么时候换」——`continuous`(持续更换)/ `request`(触发更换)。按来源归一后的值,换来源后不属于新那组的值会退回该组默认值 |
| `port` | **实际绑定的**代理端口(取自监听套接字),服务未运行时回落到配置值 |
| `interval` | 时间轮换间隔(秒) |
| `time_left` | **最近到期的那个出口还剩多久**,单位随 `switch_countdown_mode` 变化;名册为空时为 `0`。改出口由请求驱动还是后台主动做,取决于 `mode`:`continuous`(持续更换)下后台每拍都在换,空闲时这个数照样走;`request`(触发更换)与本地来源下空闲时到期的出口会被移除,池子可能为空 |
| `switch_countdown_mode` | 倒计时口径:`time`(秒,按出口各自的寿命;`interval` 与 `request_interval` 都填时按这个报 —— 秒数能画进度条,剩余请求数由每个出口的 `requests_left` 单独给出)/ `request_count`(只填了 `request_interval`,报还剩多少次请求)/ `per_request`(两项都没填,只在缺人或失效时补货)。**不会再返回 `none`**:寿命对所有来源、四种运行模式都生效 |
| `total_proxies` | 当前在用的出口条数。目标是 `exit_count`(`[Server] exit_count`),某个出口被判失效、或还没补上时这个数会小于目标;高峰自动扩容期间会大于它(同时 `elastic_active` 为真) |
| `active_proxies` | 各上游出口的实时状态:`url`(已脱敏)、`active` **正在用这个出口的连接数**(请求在途 + 最近有中继数据的隧道,面板与 CLI 的「在用」取它)、`inflight` 还占着名额的连接数(含空闲隧道,恒 ≥ `active`)、`capacity` 单出口活跃连接额度(`0` 表示不限制,空闲隧道不计入)、`failures` 连续失败次数、`suspect` 近期失败是否居多(居多的会被优先让路,但仍然可用)、`failure_ratio` 近期失败占比、`expires_in` 剩余寿命秒数、`requests_left` 剩余可服务的请求数(两个都是 `null` 表示没有启用该口径)、`last_used` / `added_at` / `expires_at` / `requests_served`(`time.monotonic()` 与计数,仅供同进程内比较) |
| `proxy_source_mode` | 代理来源:`local` / `api` / `pool` |
| `exit_count` | 出口池的目标容量(`[Server] exit_count`),与 `total_proxies` 成对读。**换的时机由运行模式决定**:持续更换(`continuous`)模式下空闲时也保持这么多;触发更换(`request`)与本地来源由请求驱动,空闲时到期的出口会被移除、池子可能空掉,下一个请求到达时才重新取货填满 |
| `elastic_active` | 出口数是否高于目标:突发扩容/高峰自动扩容中,或扩容已停止但多出来的出口还没到寿命(它们用满自己的寿命才逐个撤下) |
| `auth_required` | 出口代理是否要求用户名密码认证 |
| `display_level` | 控制台详细程度(`[Server] display_level`) |
| `service_status` | `running` / `stopped` |
| `language` | 当前界面语言 |
| `request_interval` | 按请求数切换的阈值(分配次数,不是「服务成功」的次数)。它与 `interval` 同时生效,谁先到谁换 |
| `pool_running` | 代理池是否在运行 |
| `source_error` | 最近一次从来源取出口失败的原因与时间(`{"message": ..., "at": ...}`);没有失败时为 `null` |
## 配置管理
### 读取配置(GET /api/config)
返回 `[Server]` 与 `[Pool]` 两段的全部键值。
```json
{
"status": "success",
"server": { "port": "1080", "web_port": "5001", "mode": "cycle" },
"pool": { "validator.timeout_seconds": "10" }
}
```
- **`token` 键已被剔除**,不会出现在响应里 —— 面板不需要回显它,避免它出现在页面源码、
浏览器缓存或前端日志中。
- `[Users]` 与 `[api_credentials]` 不在这里返回,各有独立端点。
- 布尔项已被归一化成 `"true"` / `"false"`:`config.ini` 里写 `yes` / `on` / `1` 在运行期
都算真,而界面只认 `"true"`,不归一化会让复选框显示成未勾选 —— 用户随手保存一次就把
开关关掉了。
### 保存配置(POST /api/config)
请求体中的两段都可省略,但至少要有一段非空:
```json
{
"server": { "mode": "loadbalance", "interval": "600" },
"pool": { "validator.timeout_seconds": "15" }
}
```
键名经白名单校验,未知键与非法取值会被拒绝,返回 **HTTP 400**。
> `token` 也是合法的 `server` 键:**面板不提供它的编辑入口,但本接口接受它**,
> 因此改 token 可以走 `POST /api/config`(立即生效),也可以直接改 `config.ini`
> 后重启进程。
```json
{
"status": "success",
"port_changed": false,
"web_port_changed": false,
"service_status": "running",
"applied": { "proxy": true, "pool": true }
}
```
| 字段 | 说明 |
|---|---|
| `port_changed` | 代理监听端口是否变了。配置热重载本身不重新绑定端口;**面板会据此自动重启出口服务**(`POST /api/service` 的 `restart`)以换到新端口,用命令行入口时需重启进程 |
| `web_port_changed` | 面板端口是否变了。**必须重启整个进程**才生效,当前面板仍运行在旧端口 |
| `applied.proxy` | 新配置是否成功推送给出口代理服务 |
| `applied.pool` | 新配置是否成功推送给代理池 |
`applied` 里的某一项为 `false` 表示**配置已落盘、但推送失败**,应答本身仍是 `success`。
此时应以 `applied` 为准判断是否真的生效。
校验失败时(HTTP 400)除 `message` 外还带 `details`,指明是哪个键、以及按界面语言渲染
好的原因,面板据此把提示落到具体输入框上:
```json
{
"status": "error",
"message": "配置项 port 非法(当前值: 70000):不能大于 65535",
"details": { "key": "port", "reason": "不能大于 65535" }
}
```
`details` 在 `key` 与 `reason_key` 齐备时才会出现(`[Server]` 与 `[Pool]` 的校验异常
都带这两个属性),其余异常只有 `message`。`message` 始终保留,老客户端不受影响。
保存配置时,若当前代理来源为 `pool`、未配置外部池地址(`pool_remote_url`)且池未运行,
会顺带自动拉起池服务。
## 语言
### 切换界面语言(POST /api/language)
语言是**服务端设置**,不是浏览器设置:它写回 `config.ini` 的 `[Server] language`,
并切换整个进程的文案(含面板、控制台横幅、接口 `message`、导出文件的表头与状态列)。
```json
{ "language": "en" }
```
| 字段 | 类型 | 必填 | 取值 |
|---|---|---|---|
| `language` | string | 否 | `cn` / `en`,缺省按 `cn` 处理 |
```json
{ "status": "success", "language": "en" }
```
取值不在 `cn` / `en` 之内时**仍返回 HTTP 200**,靠 `status` 判断:
```json
{ "status": "error", "message": "不支持的语言" }
```
> 该端点走的是面板侧的「业务失败返回 200」约定,不要只看状态码。
## 本地代理与名单
### 读写本地代理列表(GET/POST /api/proxies)
**GET** 返回 `config/` 下代理列表文件的内容。
```json
{ "proxies": ["http://1.2.3.4:8080", "socks5://5.6.7.8:1080"] }
```
读失败时返回错误,**不是**空列表——空列表会让界面显示成「一个代理都没有」,
用户随手一保存就把代理文件清空了:
```json
{ "status": "error", "message": "加载代理文件失败: ..." }
```
**POST** 整份覆盖写入,并立即热载入出口服务(不重新检测可用性):
```json
{ "proxies": ["http://1.2.3.4:8080"] }
```
`proxies` **必须是数组**(传字符串会被逐字符拆行、传对象只会写进键名,两种都会
把文件覆盖成垃圾),否则:
```json
{ "status": "error", "message": "代理列表必须是数组" }
```
文件被其他进程占用时(Windows 上常见)可能失败:
```json
{ "status": "error", "message": "..." }
```
### 检测代理可用性(GET /api/check_proxies)
| 参数 | 必填 | 默认 | 说明 |
|---|---|---|---|
| `test_url` | 否 | `https://www.baidu.com` | 存活检测的目标地址 |
对**源清单**逐个检测,并发数取自 `[Server] check_concurrency`:
- `local` 来源:`proxy_file` 里的全部条目 —— 故意不查活名册,因为出口因失败被停用后
就从名册里消失了,而那恰恰是最需要复查的那些。
- `api` / `pool` 来源:没有可读的本地清单,检测当前在用的出口。
```json
{
"status": "success",
"valid_proxies": ["http://1.2.3.4:8080"],
"checked": 3,
"total": 1,
"message": "代理检查完成,有效代理:1个"
}
```
| 字段 | 说明 |
|---|---|
| `valid_proxies` | 检测通过的地址 |
| `checked` | 本次实际检测了几个(源清单的条数) |
| `total` | 检测通过的数量 |
检测只报告结论,不改变活名册 —— 想把新验证过的出口投入使用,等下一次批次刷新即可。
检测耗时随代理数量增长,请求可能等待较久(内部超时 30 秒)。
### 读写 IP 白名单 / 黑名单(GET/POST /api/ip_lists)
**GET**:
```json
{ "whitelist": ["127.0.0.1"], "blacklist": ["1.2.3.4"] }
```
**POST**:
```json
{ "type": "whitelist", "list": ["127.0.0.1"] }
```
| 字段 | 说明 |
|---|---|
| `type` | `whitelist` 或 `blacklist`,其他取值返回错误 |
| `list` | 完整名单,整份覆盖写入 |
写盘后立即热载入出口服务,无需重启。两份名单校验的是**客户端来源 IP**(连到代理端口
的那一端),在每个新连接建立时先判定,未通过直接返回 403 并记录「未授权的IP尝试访问」,
连接不会被转发。同时命中两份名单时的判定优先级由 `[Server] ip_auth_priority` 决定
(`whitelist` 放行 / `blacklist` 拦截)。
### 读写绕过名单(GET/POST /api/bypass_whitelist)
**GET**:
```json
{ "list": ["192.168.0.0/16"] }
```
**POST**:
```json
{ "list": ["192.168.0.0/16"] }
```
名单内的**目标地址**不做任何上游转发,直接由本机连接(不参与出口调度)。它与上面
按客户端来源 IP 判定的黑白名单是两回事:把某个目标加进本名单,不会跳过对客户端
IP 的黑白名单校验。
## 服务控制
### 启停出口代理服务(POST /api/service)
```json
{ "action": "start" }
```
| action | 说明 |
|---|---|
| `start` | 服务未运行时启动;已在运行则直接返回成功 |
| `stop` | 停止服务 |
| `restart` | 先停后起 |
```json
{ "status": "success", "message": "服务启动成功", "service_status": "running" }
```
`start` / `stop` 的响应会带 `service_status`(取值 `running` / `stopped`);`restart` 成功时不带该字段。
**这个接口是串行且带超时的**:启动要等旧线程退出(最多 10 秒)再等服务就绪(最多 5 秒),
停止最多等 10 秒。超时即返回 `status` 为 `error`,**不会静默改用其他端口**。因此调用方
应留出足够长的请求超时。
端口被占用时返回错误,且不会启动服务:
```json
{
"status": "error",
"message": "端口 1080 已被其他程序占用,请更换其他端口后重试",
"service_status": "stopped"
}
```
### 手动切换上游代理(GET /api/switch_proxy)
丢开现有候补、立刻调一次来源:**池子不满就补满;启用了出口寿命(`interval` 或
`request_interval` 非 0)时,满员则换掉最近到期的那个出口**(只填了
`request_interval` 时各出口没有时间寿命,实际换掉的是名册里的第一个)。
不是「换到下一个 IP」(所有出口本来就同时在用),也不是整批替换。响应有四种形态:
**切换成功**
```json
{
"status": "success",
"current_proxy": "http://1.2.3.4:8080",
"active_proxies": [
{ "url": "http://1.2.3.4:8080", "inflight": 0, "active": 0, "capacity": 0, "failures": 0, "suspect": false, "failure_ratio": 0.0, "last_used": 0.0 }
],
"total_proxies": 1,
"message": "出口批次已刷新"
}
```
| 字段 | 说明 |
|---|---|
| `current_proxy` | 当前出口(最近一次分配出去的出口,已脱敏);它被换掉时回退到名册里的第一个 |
| `active_proxies` | 当前名册的快照,字段含义同 `/api/status` |
| `total_proxies` | 当前名册里的出口数量 |
「成功」按刷新后**名册是否非空**判定 —— 已经是满员且这次只轮换了一个出口,同样算成功。
**处于冷却期**(距上次切换过近,`[Server] switch_cooldown` 内不再切换)
```json
{
"status": "error",
"cooldown": true,
"cooldown_remaining": 12,
"message": "..."
}
```
**已有切换在进行中**
```json
{ "status": "error", "switching": true, "message": "..." }
```
**切换失败**
```json
{ "status": "error", "current_proxy": "...", "message": "..." }
```
调用方应据 `cooldown` / `switching` 字段区分「暂时不切」与「真的失败」,两者都是
`status: error`。
## 日志
日志按用途分为四个类别:`access`(代理访问)、`proxy`(代理生命周期)、`main`(主程序)、
`pool`(代理池)。其中 `access` / `proxy` / `main` 三个类别有对应的日志文件,`pool` 只进
内存环形缓冲与 `error.log`,其自身文件在 `modules/proxypool/logs/` 下,不通过本组接口浏览。
### 查询内存日志(GET /api/logs)
| 参数 | 必填 | 默认 | 说明 |
|---|---|---|---|
| `category` | 否 | `all` | `all` / `access` / `proxy` / `main` / `pool`,其他取值返回错误 |
| `start` | 否 | `0` | **从最新一条往前跳过的条数** |
| `limit` | 否 | `200` | 返回条数,被限制在 1-2000 |
| `level` | 否 | `ALL` | 按级别过滤 |
| `search` | 否 | 空 | 关键词过滤 |
```json
{
"logs": [
{
"seq": 8821,
"time": "2026-09-30 12:00:00",
"level": "INFO",
"category": "main",
"message": "..."
}
],
"total": 1234,
"status": "success"
}
```
- `seq` 是**跨类别唯一的全局递增序号**,多个类别合并返回时按它还原真实先后。
条目按时间正序排列。
- `total` 是**过滤后的总数**,不是本次返回的条数。
- `start` 的语义与常规分页相反:它是从最新一条往回数的偏移量,配 `limit` 用于
「翻页看更早的日志」。
### 日志统计(GET /api/logs/stats)
```json
{
"status": "success",
"stats": {
"total": 170,
"levels": { "INFO": 150, "WARNING": 20 },
"categories": { "main": 100, "access": 50, "proxy": 20, "pool": 0 },
"capacity": { "main": 20000, "access": 5000, "proxy": 20000, "pool": 20000 },
"first_time": "2026-09-30 10:00:00",
"last_time": "2026-09-30 12:00:00"
}
}
```
`total` 是四个环加起来的条数,`capacity` 是各环容量(`access` 环更小)。
`first_time` / `last_time` 在缓冲为空时不出现。
### 列出日志文件(GET /api/logs/files)
```json
{
"status": "success",
"files": [
{ "name": "main.log", "category": "main", "size": 20480,
"modified": "2026-10-02 15:30:00", "current": true, "rotated": false }
]
}
```
| 字段 | 说明 |
|---|---|
| `name` | 文件名(面板拿它调 `/api/logs/file`) |
| `category` | `main` / `proxy` / `access` / **`error`**。`error` 是 ERROR 及以上的汇总文件,**不能**当作 `GET /api/logs` 的 `category` 参数传 |
| `size` / `modified` | 字节数 / 最后修改时间 |
| `current` | 是不是当前在写的那个;`rotated` 是它的反面(轮转出来的历史份) |
只列出 `logs/` 目录下的白名单文件(含轮转份),不含代理池自己的 `proxy_pool.log`。
### 读取日志文件(GET /api/logs/file)
| 参数 | 必填 | 默认 | 说明 |
|---|---|---|---|
| `name` | 是 | 空 | 文件名,取白名单内的名字 |
| `lines` | 否 | `500` | 读取末尾行数,被限制在 1-5000 |
```json
{ "status": "success", "name": "main.log", "lines": ["..."], "truncated": true }
```
`truncated` 为 `true` 表示文件的行数**不少于**请求的行数(恰好相等时也为 `true`),
本次只返回了末尾部分。文件名不在白名单内返回错误(**按白名单校验而非路径拼接,
杜绝目录穿越**)。
### 导出日志(GET /api/logs/export)
| 参数 | 必填 | 说明 |
|---|---|---|
| `file` | 否 | 指定文件名时**下载该文件的附件**;省略则导出内存日志 |
| `category` | 否 | 省略 `file` 时生效,同 `GET /api/logs` |
| `level` / `search` | 否 | 同上 |
省略 `file` 时返回纯文本附件,文件名为 `proxycat_export.log`。
### 清空日志(POST /api/logs/clear)
```json
{ "category": "all" }
```
清空对应类别的内存环形缓冲,并截断对应的日志文件。`category` 为 `all` 时清空全部。
```json
{ "status": "success", "message": "日志已清除" }
```
**会删掉磁盘上的日志内容**,不可恢复。
## 访问记录
逐条记录每一次代理请求。与「日志」的区别是它面向查询与统计:按列存储、可过滤、可导出。
### 查询访问记录(GET /api/logs/records)
| 参数 | 必填 | 默认 | 说明 |
|---|---|---|---|
| `since` | 否 | 1 小时前 | 起始时间,接受 `YYYY-MM-DD`、`YYYY-MM-DD HH:MM`、`YYYY-MM-DD HH:MM:SS`,也接受 `T` 分隔 |
| `until` | 否 | 不限 | 结束时间,粒度到分钟或日期时自动补到该时刻的最后一秒 |
| `outcome` | 否 | 不限 | `success` / `failure` / `aborted`,其他取值返回错误 |
| `upstream` | 否 | 不限 | 按上游代理过滤 |
| `host` | 否 | 不限 | 按目标主机过滤 |
| `search` | 否 | 空 | 关键词过滤 |
| `limit` | 否 | `100` | 被限制在 1-500 |
| `offset` | 否 | `0` | 分页偏移 |
| `order` | 否 | `desc` | 时间排序方向 |
```json
{
"status": "success",
"enabled": true,
"range": { "since": "2026-09-30 11:00:00", "until": "" },
"outcome_counts": { "success": 100, "failure": 3, "aborted": 1 },
"total": 104,
"export_limit": 50000,
"records": [
{
"id": 12345,
"ts": "2026-09-30 12:00:00",
"kind": "http",
"method": "GET",
"client": "127.0.0.1/-",
"client_ip": "127.0.0.1",
"client_user": "-",
"host": "example.com",
"port": 443,
"target": "example.com:443",
"upstream": "http://1.2.3.4:8080",
"upstream_label": "http://1.2.3.4:8080",
"real_ip": "203.0.113.7",
"outcome": "success",
"status_code": 200,
"elapsed_ms": 123,
"reason": ""
}
]
}
```
- `id` 是自增主键;`client`(`client_ip/client_user`)与 `target`(`host:port`)是**派生
字段**,只是把相邻两列拼成界面直接可用的形态,与 `client_ip` / `host` 同源。
- `upstream` 为特殊值时用 `upstream_label` 给出可读文案:直连(`direct`)与未知上游
(`unknown`)各有一种取值。
- 失败(`outcome=failure`)时 `status_code` 一律为空:隧道先建好、之后才失败的行不会
带着建连那一下的 200 出现在这里(历史库里存过这种行,读取时统一收敛)。
- `real_ip` 是**这个出口实际用的出口 IP**(目标站看到的那个),网关型出口的地址与它
不是一回事。它是展示字段:代理池来源由池收录时验证得出,API 来源在
`[Server] access_records_real_ip_probe` 打开时由后台探测得出,本地来源恒为空串。
探不到或没开启时是 `""`,界面显示成 `--`;直连的请求也是 `""`。
它**不参与**出口调度、寿命与故障判定,老库里的历史记录同样是 `""`。
- 响应里另有三个落库状态字段与 `records` 平级返回:`pending`(尚未落盘的缓冲条数)、
`dropped`(因缓冲写满而丢弃的条数)、`oldest_ts`(库里最早一条记录的时间,空串表示没有)。
`dropped` 非零说明落盘跟不上,记录有丢失。
### 导出访问记录(GET /api/logs/records/export)
过滤参数与查询接口相同,额外一个:
| 参数 | 必填 | 默认 | 说明 |
|---|---|---|---|
| `format` | 否 | `csv` | `csv` 或 `json`,其他取值返回错误 |
- **CSV**:带 UTF-8 BOM,Excel 直接打开不乱码;**表头按当前界面语言渲染**。
- **JSON**:UTF-8,含 `exported_at`、过滤条件、`count` 与 `truncated` 元信息。
两种格式都受 `export_limit`(50000)限制;被截断的标记只有 JSON 的元信息里有
(`truncated`,取到的条数达到 50000 即为 `true`),CSV 只有正文、没有该标记。响应为
附件下载,文件名形如 `access_records_20260930_120000.csv`。
### 列出过滤候选项(GET /api/logs/records/options)
返回当前记录里出现过的上游代理与目标主机取值,供前端渲染下拉框。
```json
{ "status": "success", "upstreams": [{ "value": "http://1.2.3.4:8080", "label": "..." }] }
```
### 清空访问记录(POST /api/logs/records/clear)
```json
{ "status": "success", "removed": 1234, "message": "已清除 1234 条访问记录" }
```
**不可恢复。**
## 域名统计
按「目标域名 × 上游代理」累计请求的成败次数。与访问记录的区别:这里是聚合计数,
访问记录是逐条明细;因此访问记录可以清空而这里的累计值仍在,反之亦然。
### 查询域名统计(GET /api/logs/domains)
| 参数 | 必填 | 默认 | 说明 |
|---|---|---|---|
| `upstream` | 否 | 空 | **指定时下钻到该上游的域名明细,省略时按上游汇总** |
| `sort` | 否 | `last_seen` | 排序字段 |
| `order` | 否 | `desc` | 排序方向 |
| `search` | 否 | 空 | 关键词过滤 |
| `limit` | 否 | `100` | 被限制在 1-1000 |
| `offset` | 否 | `0` | 分页偏移 |
**省略 `upstream`——按上游汇总:**
```json
{
"status": "success",
"enabled": true,
"summary": { "pairs": 42, "hosts": 12, "upstreams": 5, "success": 3400, "failure": 56 },
"mode": "proxies",
"proxies": [
{
"upstream": "http://1.2.3.4:8080",
"upstream_label": "http://1.2.3.4:8080",
"host_count": 7,
"success": 100,
"failure": 2,
"total": 102,
"rate": 98,
"first_seen": "2026-09-30 10:00:00",
"last_seen": "2026-09-30 12:00:00",
"last_error": ""
}
],
"total": 1
}
```
**指定 `upstream`——下钻到域名:**
```json
{
"status": "success",
"enabled": true,
"summary": { "pairs": 42, "hosts": 12, "upstreams": 5, "success": 3400, "failure": 56 },
"mode": "domains",
"upstream": "http://1.2.3.4:8080",
"upstream_label": "http://1.2.3.4:8080",
"domains": [
{
"host": "example.com",
"upstream": "http://1.2.3.4:8080",
"success": 50,
"failure": 1,
"first_seen": "2026-09-30 10:00:00",
"last_seen": "2026-09-30 12:00:00",
"last_status": 200,
"last_error": ""
}
],
"total": 1
}
```
- `summary` 的 `pairs` 是「域名 × 上游」的组合数,`rate` 是成功率百分比(取整)。
- **用返回的 `mode` 字段区分两种形态,不要靠猜。**
- `enabled` 为 `false` 表示域名统计功能未开启(`[Server] domain_stats_enabled`),
此时没有数据可查。
### 清空域名统计(POST /api/logs/domains/clear)
```json
{ "status": "success", "removed": 42, "message": "已清除 42 个域名的访问统计" }
```
## 用户与 API 凭据
### 读写出口代理账号(GET/POST /api/users)
**GET**:
```json
{ "status": "success", "users": { "neko": "123456" } }
```
读失败时返回 `status: "error"` 与 `users: null`,**不是空对象**——空对象会让界面
显示成「一个用户都没有」,管理员加一个用户再保存就用空表覆盖原账号,等于把出口
认证关掉。**响应里是明文密码**。
**POST** 整份覆盖 `[Users]` 段:
```json
{ "users": { "neko": "123456" } }
```
**传空对象即可关闭出口认证**(账号表为空时 `auth_required` 为 `false`)。
```json
{ "status": "success", "message": "用户保存成功" }
```
### 管理上游 API 凭据(GET/POST /api/api_credentials)
存多套「取代理接口」的凭据,并指定当前生效的一套。用 `set_active` 切换生效项、
或删除的正是生效项时,会**同步改写 `[Server] api_proxy_url` / `proxy_username` /
`proxy_password`**,因此代理来源为 `api` 时会立即用上新凭据;`save` 不同步这三键 ——
它只更新凭据集合与生效项标记,要让新保存的值真正被代理来源使用,需要对它再调一次
`set_active`(或由删除生效项触发的自动切换)把三键写回 `[Server]`。
**GET**:
```json
{
"status": "success",
"sets": [{ "name": "闪臣", "url": "https://...", "username": "", "password": "" }],
"active_credential": "闪臣"
}
```
**响应含明文凭据。**
**POST**:
```json
{ "action": "save", "name": "闪臣", "url": "https://...", "username": "", "password": "" }
```
| action | 需要字段 | 说明 |
|---|---|---|
| `save` | `name`、`url`、`username`、`password` | 按 `name` 存在则更新,否则新增;`name` 为空返回错误。若当前没有生效项,或生效项正是这一条,则把它设为生效项 |
| `delete` | `name` | 按 `name` 删除;若删掉的正是生效项,则把第一条剩余项设为生效项、**并把它的三个键同步写回 `[Server]`**(没有剩余则清空这三键)—— 不换的话运行中的服务会继续用已删除的接口 |
| `set_active` | `name` | 切换生效项,并把它的 `url`/`username`/`password` 写进 `[Server]`;`name` 不在凭据集合里时返回错误,不会写一个「未选用」的中间态 |
`save` 与 `delete` 返回更新后的 `sets` 与 `active_credential`;`set_active` 返回
`active_credential` 与生效项的 `credential`,**不返回 `sets`**。未知 `action` 与空 `name`
返回错误(`{ "status": "error", "message": "..." }`),`set_active` 的 `name` 不在凭据集合
里时也返回错误。`save` 传一个不存在的 `name` 是新增;`delete` 不校验名字是否存在,
删除一个不存在的凭据名照常返回 `success`(`sets` 原样返回)。
## 版本检查与广告
> **除 `POST /api/version/check` 外,本节端点无鉴权**,配置了 token 也无需携带。
### 检查版本(GET /api/version)
返回**最近一次**版本检查的结果。**本端点只读缓存,不会发起任何外部请求** ——
请求它永远不会产生网络等待。
```json
{
"status": "success",
"is_latest": true,
"current_version": "ProxyCat-V3.0.0",
"latest_version": "ProxyCat-V3.0.0"
}
```
| 字段 | 说明 |
|---|---|
| `is_latest` | 远端版本是否不高于当前版本;**每次读取时按当前的 `CURRENT_VERSION` 重算** |
| `current_version` | 本进程的版本 |
| `latest_version` | 最近一次检查取到的远端版本 |
**检查的节奏由进程自己掌握,不由本端点触发:**
- **进程启动时查一次**,之后**每 24 小时一次**。
- **24 小时内重启不会重复检查**:上次检查的时间写在 `logs/version_check.json` 里,
重启后读回来继续用。两个入口(`app.py` 与 `ProxyCat.py`)都是这个节奏。
- **无论成败都记录时间**,因此对外最多 24 小时发起一次请求;断网时不会再出现
「每次打开面板都要等一次超时」的情况。
- 检查在后台线程里做,不阻塞启动,也不占用 Web 工作线程。
- 需要立刻知道结果时用下面的 `POST /api/version/check`。
**版本取自 GitHub 仓库的发布记录**,默认并发请求三个地址,**谁先返回可用的版本号就用谁**:
| 地址 | 说明 |
|---|---|
| `github.com/…/releases.atom` | 官方发布源,无需认证、无频率限制 |
| `gh-proxy.com/https://api.github.com/…` | 第三方镜像,国内可达性更好;会看到请求 IP,且可能返回缓存的旧版本 |
| `api.github.com/repos/…/releases` | 官方 API,未认证时每 IP 每小时限 60 次 |
并发而非顺序重试:被墙或超时的地址不会拖慢整体,可用时通常 1 秒内返回;三者全部失败时
约 8 秒返回。请求沿用 httpx 默认行为,**读取 `HTTP(S)_PROXY` 环境变量**。
配置项 [`version_check_url`](ProxyCat-Manual/Configuration.md#高级选项) 非空时**只用该地址**,
内置的三个源不再请求。版本号从标签里取 `ProxyCat[-_]v?数字.数字[.数字]` 这一段前缀,
`-测试版V5` 之类的后缀被丢弃。
### 立即检查版本(POST /api/version/check)
**需鉴权**(与上面只读缓存的 GET 相反:本端点会真的发起外部请求)。
无参数、无请求体。**当前 Web 线程会等到本次检查结束才返回**(最长约 15 秒),
同一时刻只有一次检查在跑 —— 并发调用会等正在跑的那一次,而不是各自再发一轮请求。
```json
{
"status": "success",
"current_version": "ProxyCat-V3.0.0",
"latest_version": "ProxyCat-V3.0.0",
"is_latest": true,
"source": "github.com",
"checked_at": "2026-10-06 18:39:42"
}
```
| 字段 | 说明 |
|---|---|
| `status` | **本次检查**的成败,这是与 GET 的关键差别 |
| `source` | 本次胜出的地址主机名;仅成功时出现 |
| `checked_at` | 本次检查的时间 |
| `latest_version` / `is_latest` | 只要内存里有成功的版本号就会出现 |
| `message` | **本次失败**的原因,仅失败时出现 |
**本次检查失败、但此前成功过**时,`status` 为 `error`、带 `message`,同时仍带上一次成功
的 `latest_version`。这正是它与 GET 的区别:GET 只报「最后已知状态」,本端点报「这一次
成不成」。两者都会把本次检查的时间写进 `logs/version_check.json`,因此**手动检查同样会
重置 24 小时的计时**。
**既没有成功结果、也没有失败记录时**(启动后第一次检查还没跑完)返回:
```json
{
"status": "error",
"message": "尚未完成版本检查"
}
```
**检查失败时**(断网、远端页面结构变化)返回 `status` 为 `error`,`message` 说明原因;
但若此前成功过,则**继续返回上一次成功的结果** —— 面板不是监控工具,展示最后已知的
版本状态比展示错误更有用。失败本身记在日志里。
> 升级到新版本后,即使距上次检查不足 24 小时也会立即重查:状态文件里存着
> 「当时检查的当前版本」,与进程里的版本不一致即视为过期。
### 获取广告(GET /api/ads)
读取 `config/ads/` 目录下所有 `*.json`,返回 `enabled` 不为 `false` 的条目。
解析失败的文件静默跳过。
```json
{
"status": "success",
"ads": [
{
"id": "example",
"title": "...",
"body": "...",
"image_url": "",
"link_url": "",
"link_text": "",
"display_time": 15
}
],
"total": 1,
"dismissed": false
}
```
`dismissed` 是**进程内的关闭状态,重启即丢失**。
### 关闭 / 重新开启广告(POST /api/ads/dismiss、POST /api/ads/reopen)
两个端点都无参数、无鉴权,响应体是 `{ "status": "success", "dismissed": <布尔值> }`,
关闭置 `true`、重新开启置 `false`:
```json
{ "status": "success", "dismissed": true }
```
---
# 三、代理池接口
全部挂在 `/api/pool` 前缀下,**鉴权与面板一致**(同一个 `before_request` 钩子)。
## 池侧约定
### 成功响应没有统一外壳
适配层把处理函数的返回值**直接序列化成响应体**,因此:
- `GET /api/pool/get` 返回一个 **JSON 数组**(不是 `{"status": ..., "data": [...]}`)。
- `GET /api/pool/count` 返回 `{"count": 12}`,**没有 `status` 字段**。
- `GET /api/pool/random?format=text` 与导出接口返回**纯文本或附件**,不是 JSON。
- 只有部分端点会在返回值里自带 `status` 与 `message`(如生命周期端点);插件操作返回的是
`success` 与 `message`,没有 `status` 字段。
**判断成功请看 HTTP 状态码与响应内容本身,不要假设一定有 `status` 字段。**
### 错误响应
```json
{
"status": "error",
"message": "错误描述",
"error_code": "invalid_parameter"
}
```
| error_code | HTTP | 含义 |
|---|---|---|
| `invalid_parameter` | 400 | 请求参数非法(含缺失必填字段、取值越界) |
| `api_error` | 4xx / 5xx | 业务错误,如代理不存在、规格不匹配。HTTP 状态码随错误自带(如插件操作失败、备份恢复失败为 500,归属地解析服务未启用为 503),不要按 4xx 写死分支 |
| `pool_running` | 409 | 池运行中,不允许该操作(**仅备份恢复**会返回) |
| `pool_unavailable` | 503 | 代理池未运行或正在重启 |
| `pool_timeout` | 504 | 代理池响应超时 |
| `queue_full` | 503 | 写入队列已满,请求被拒(背压) |
| `internal_error` | 500 | 服务端内部错误 |
**两个例外要知道:**
- `POST /api/pool/start|stop|restart` 失败时返回 **500 且不带 `error_code`**,
只有 `status` / `message` / `is_running`。
- `message` 跟随界面语言,**不可用于程序判断**。
### 池未运行时
池未启动时,除状态查询、生命周期端点、备份列表/恢复与 `/schema` 外,**其余端点一律返回
`503 pool_unavailable`**。调用方应先查 `GET /api/pool/status` 确认 `is_running`。
### 任务模型
所有耗时操作(批量验证、导入、抓取、归属地更新/重算)都**立即返回 `task_id`**,
真正的执行在后台,进度按 `task_id` 轮询。
- 任务状态是**进程内内存态**:进程重启即全部丢失。
- 已完成/失败的任务保留 **1 小时**(TTL),超过即被淘汰;状态表上限 200 条。
- 创建时返回的 `status`(`queued` / `running`)**只是创建瞬间的快照,不代表执行结果**;
任务表里的条目从创建起就直接写 `running`,不存在 `queued` 状态的条目。
- 任务表里实际出现的状态:`running` / `completed` / `failed` / `cancelled`
(`queued` 只在部分创建响应里出现,用它过滤任务永远筛不到条目)。
### 没有上报使用反馈的接口
「使用期反馈」是出口代理在真实转发过程中**在进程内部**回报给池的(见
[功能特性里的「代理池机制」](ProxyCat-Manual/Features.md#代理池机制)),**没有任何 HTTP 端点可以上报使用结果**,
不必去找。
## 池状态与生命周期
### 获取池状态(GET /api/pool/status)
```json
{
"status": "success",
"is_running": true,
"last_error": null,
"stats": {
"total_proxies": 120,
"valid_proxies": 86,
"invalid_proxies": 34,
"anonymity_fallback_proxies": 5,
"protocol_distribution": { "http": 60, "socks5": 26 },
"region_distribution": { "中国": 40, "美国": 12 }
}
}
```
| 字段 | 说明 |
|---|---|
| `is_running` | 池是否在运行 |
| `last_error` | 最近一次错误文案,无错误时为 `null` |
| `stats` | 全局统计;**池未运行时为 `null`**。统计本身失败也不影响本端点返回成功,只是 `stats` 为 `null` |
| `anonymity_fallback_proxies` | 匿名度被判成兜底档的条数 |
| `region_distribution` | 只取数量最多的前 10 个地区 |
### 获取池配置字段结构(GET /api/pool/schema)
返回 `[Pool]` 段的字段分组、类型、说明与取值区间,**面板的「池设置」表单就是按它渲染的**。
文案跟随当前界面语言。
```json
{
"status": "success",
"groups": [
{
"title": "数据库",
"sub": "数据存在哪、怎么备份",
"id": "database",
"fields": [
{ "key": "database.path", "doc": "代理池所有数据存放的文件(SQLite 格式)", "type": "text",
"title": "数据库文件路径", "hint": "改动需重启池服务;相对路径从 proxypool 目录起算" },
{ "key": "database.pool_max_readers", "doc": "...", "type": "int", "low": 1, "high": 64,
"title": "读连接数上限", "tier": "advanced" },
{ "key": "database.backup_interval_hours", "doc": "...", "type": "int", "low": 1,
"title": "备份间隔", "unit": "小时", "tier": "advanced", "visible_when": "database.backup_enabled=true" }
]
}
]
}
```
| 字段 | 说明 |
|---|---|
| `type` | 字段类型,取值 `bool` / `int` / `float` / `list` / `text`(字符串字段一律是 `text`,没有 `str`) |
| `doc` | 完整说明,与 `config.ini` 里的注释逐字一致 |
| `title` | 短标题,直接当表单的字段标签用 |
| `hint` | 控件下方的一行提示;是否出现取决于字段是否写了提示 |
| `unit` | 计量单位,由面板渲染在输入框右侧。**单位不写进 `title`**,语言包无对应值时该键不输出 |
| `tier` | 为 `"advanced"` 时归入「高级选项」折叠区 |
| `visible_when` | 声明式显隐规则 `键=值1\|值2;键2!=值`(分号是「与」、竖线是「或」)。带此规则的字段内联显示、不折叠。隐藏只影响显示,字段照常收集与保存 |
| `sub`(分组级) | 分组副标题,显示在卡片标题右侧 |
`low` / `high` 只在配置项有取值区间时出现。**本端点不返回当前值** —— 当前值走
`GET /api/config` 的 `pool` 字段。
### 启动池(POST /api/pool/start)
无参数。
```json
{ "status": "success", "message": "维护池启动成功", "is_running": true }
```
### 停止池(POST /api/pool/stop)
无参数。**会等写队列排空**,可能耗时数秒。
```json
{ "status": "success", "message": "维护池停止成功", "is_running": false }
```
### 重启池(POST /api/pool/restart)
无参数。先停后起,停止与启动各最多等 30 秒。
失败时(三个生命周期端点相同):
```json
{ "status": "error", "message": "...", "is_running": false }
```
HTTP 状态码为 **500,且不带 `error_code`**。
## 代理查询与单条操作
### 查询代理列表(GET /api/pool/get)
**返回一个 JSON 数组**,元素为[代理对象](#附录-c字段字典)。
| 参数 | 类型 | 默认 | 取值与语义 |
|---|---|---|---|
| `protocol` | string | 不限 | `http` / `https` / `socks5`,其他取值返回 400 |
| `region` | string | 不限 | 归属地模糊匹配(中、英两列都匹配);**以 `-` 开头表示排除**,如 `-美国` 表示排除归属地含「美国」的代理 |
| `min_delay` | int | 不限 | 最小延迟(毫秒),负数报错 |
| `max_delay` | int | 不限 | 最大延迟(毫秒),负数报错;与 `min_delay` 顺序颠倒报错 |
| `status` | string | 不限 | `valid` / `invalid`,其他取值返回 400 |
| `source` | string | 不限 | 来源插件名 |
| `sort_by` | string | `delay_ms` | 见下方白名单 |
| `sort_order` | string | `asc` | `asc` / `desc` |
| `limit` | int | 不限 | 只取前 N 条;设置后 `page` 被强制为 1。上限 50000 |
| `page` | int | `1` | 页码,必须 ≥ 1 |
| `page_size` | int | `50` | 每页条数,1 到 50000 |
| `format` | string | `json` | `json` 或 `text`;`text` 返回每行一个代理地址的纯文本 |
| `ip` | string | 不限 | 按 IP 模糊匹配 |
| `is_favorite` | bool | 不限 | 只看收藏 / 只看未收藏 |
| `anonymity_level` | string | 不限 | `transparent` / `anonymous` / `elite` / `unverified` |
| `min_health_score` | float | 不限 | 最低健康分,负数报错 |
| `supports_https` | bool | 不限 | 只看支持 / 不支持 HTTPS 的代理 |
`sort_by` 白名单:`delay_ms`、`protocol`、`region`、`validated_at`、`created_at`、
`health_score`、`anonymity_level`、`success_count`、`total_checks`、`avg_delay_ms`、
`failure_count`、`is_favorite`、`supports_https`、`success_rate`、`region_en`。
> **写错 `sort_by` 不会报错**:不在白名单内会**静默回落到 `delay_ms`**;`sort_order`
> 不是 `asc` / `desc` 时静默回落到 `asc`。排序结果不对时先检查参数名拼写。
> **未知查询参数被静默忽略**(见 [参数解析规则](#参数解析规则仅代理池接口))。
> 参数名写错时不会报错,只会返回未筛选的结果。
### 取一个代理(GET /api/pool/random)
随机返回一个符合条件的代理,筛选参数与 `/get` 相同(**没有** `sort_by` / `sort_order` /
`limit` / `page` / `page_size`),另有:
| 参数 | 类型 | 默认 | 说明 |
|---|---|---|---|
| `format` | string | `json` | `json` 返回完整[代理对象](#附录-c字段字典);`text` 只返回 `协议://ip:端口` 一行 |
```json
{
"id": 42,
"proxy_url": "http://1.2.3.4:8080",
"protocol": "http",
"ip": "1.2.3.4",
"port": 8080,
"delay_ms": 320.5,
"health_score": 87.3,
"anonymity_level": "elite",
"region": "中国 广东 深圳"
}
```
(上例为节选,完整字段见[附录 C](#附录-c字段字典)。)
纯文本形态的 `format=text` 响应体就是一行:
```
http://1.2.3.4:8080
```
**错误与边界:**
- 没有符合条件的代理 → **404 `api_error`**。
- `format` 不是 `json` / `text` → 400 `api_error`。
> **随机不是严格等概率**:实现是「先计数、随机挑一页(每页 100 条)、再随机挑一条」,
> 以页为粒度。池子很小时各条被取到的概率略有差异,但对取用没有实际影响。
> 这个端点的 `format=text` 正是 [`pool_remote_url`](ProxyCat-Manual/Configuration.md#来源设置) 期望的响应形态 ——
> 可以用另一个 ProxyCat 实例的 `/api/pool/random?format=text&token=...` 作为外部池地址。
### 统计代理数量(GET /api/pool/count)
筛选参数与 `/get` 相同(去掉排序与分页)。
```json
{ "count": 86 }
```
### 导出代理(GET /api/pool/export)
筛选参数与 `/get` 相同(去掉排序与分页),另有:
| 参数 | 类型 | 默认 | 说明 |
|---|---|---|---|
| `format` | string | `txt` | `txt` / `json` / `csv`,其他取值返回 400 |
- **`txt`**:每行一个代理地址,附件名 `proxies_YYYYMMDD_HHMMSS.txt`。
- **`json`**:附件,字段为 `protocol`、`ip`、`port`、`username`、`password`、`url`、
`region`、`region_en`、`delay_ms`、`is_valid`、`real_ip`、`source_plugin`、`validated_at`。
- **`csv`**:附件,带 UTF-8 BOM;**表头列数与顺序是对外契约**(协议、IP、端口、用户名、
密码、代理地址、归属地、延迟、状态、真实 IP、来源、验证时间),表头与状态列文案跟随
界面语言。英文归属地列**刻意不在导出里**。
**一次最多导出 50000 条。**
**错误与边界:** 没有符合条件的代理 → **404 `api_error`**。
### 导入代理(POST /api/pool/import)
请求体:
```json
{ "proxies": ["http://1.2.3.4:8080", "socks5://user:[email protected]:1080"] }
```
导入与插件抓取走**同一条流水线**:先端口初筛、再做完整验证,**只有验证通过的才入库**,
来源固定记为 `manual_import`。本端点为 HEAVY 档。
响应有三种结果,**调用方必须同时看 `message` 与 `proxy_count`,不能只看 `success`**:
| 情形 | 响应 |
|---|---|
| 正常受理 | `{ "success": true, "message": "正在后台验证 N 个代理,通过后才入库", "proxy_count": N, "task_id": "..." }` |
| 全部解析失败 | `{ "success": false, "message": "...", "proxy_count": 0 }` |
| 全部已存在 | `{ "success": true, "message": "所有代理已存在,未导入新代理", "proxy_count": 0 }` |
- 空列表 → **400 `invalid_parameter`**。
- 「已存在」的判据是逐条按 `ip:port` 查库,不走去重缓存。
- 受理后进度按返回的 `task_id` 轮询。
### 切换收藏状态(POST /api/pool/proxies/{id}/favorite)
无请求体。
```json
{
"success": true,
"is_favorite": true,
"message": "代理已收藏"
}
```
**这是「切换」而不是「设置」,重复调用会在两个状态之间来回翻转**,没有幂等参数。
代理不存在 → 404 `api_error`。收藏状态代表用户选择,抓取与验证写入不会覆盖它。
## 统计与主机信息
### 获取池统计(GET /api/pool/stats)
```json
{
"total_proxies": 120,
"valid_proxies": 86,
"invalid_proxies": 34,
"anonymity_fallback_proxies": 5,
"protocol_distribution": { "http": 60, "socks5": 26 },
"region_distribution": { "中国": 40, "美国": 12 }
}
```
**响应没有 `status` 字段**,直接就是统计对象(与 `/status` 里的 `stats` 同构)。
### 获取来源列表(GET /api/pool/sources)
```json
{ "sources": ["geonode_plugin", "github_proxy_plugin", "manual_import"] }
```
返回库里出现过的全部来源插件名,包含手动导入用的伪插件名 `manual_import`。
### 获取本机出口 IP(GET /api/pool/host-ip)
返回本机公网出口 IP,它是判定「透明代理」的对比基准。
```json
{
"host_public_ip": "203.0.113.7",
"host_public_ipv4": "203.0.113.7",
"host_public_ipv6": "240e:37a:2563:cb00:9424:56ad:5734:d8",
"source": "detected",
"check_anonymity": true
}
```
| 字段 | 说明 |
|---|---|
| `host_public_ip` | 本机出口 IP 的单值形态,**优先 IPv4**,只有 IPv6 时给 IPv6;**拿不到时为 `null`,这是正常状态而非故障** |
| `host_public_ipv4` | 该族的出口 IP,拿不到时为 `null` |
| `host_public_ipv6` | 该族的出口 IP,拿不到时为 `null` |
| `source` | 该值的来源(`configured` 手动指定 / `detected` 自动查询),拿不到时为空串 |
| `check_anonymity` | 匿名度检测是否开启 |
双栈主机两族各有一个出口 IP,**两族都会参与匿名度比对**,所以这里分族给出。
基准值优先取 `[Pool] validator.host_public_ip`(可填逗号分隔的一到两个,IPv4 与 IPv6 各至多一个);留空时由程序自行查询 —— 查询会**钉死地址族各问一次**,不是取第一个应答的端点,否则拿到的族别会随端点可用性漂移。每 30 分钟最多一次。
## 插件管理
### 列出插件(GET /api/pool/plugins)
```json
[
{
"name": "geonode_plugin",
"enabled": true,
"interval_minutes": 60,
"last_run": "2026-09-30T11:00:00",
"next_run": "2026-09-30T12:00:00",
"last_error": null,
"is_loaded": true,
"test_url": "",
"reval_enabled": true,
"reval_interval_minutes": 0,
"skip_validation": false
}
]
```
| 字段 | 说明 |
|---|---|
| `name` | 插件名,**等于插件文件的文件名(不含扩展名)** |
| `enabled` | 是否参与自动抓取调度 |
| `interval_minutes` | 抓取周期(分钟) |
| `last_run` / `next_run` | ISO 8601 字符串或 `null` |
| `last_error` | 最近一次错误;**若是内置文案键则按当前语言渲染,若是插件自己抛的异常文本则原样给出** |
| `is_loaded` | 模块是否已成功加载。**响应只包含加载成功的插件,因此恒为 `true`**;加载失败的插件不会出现在列表里(错误只记在池的日志中) |
| `test_url` | 该插件专用的存活检测地址,空串表示继承全局 `[Server] test_url` |
| `reval_enabled` | 是否参与自动重验证 |
| `reval_interval_minutes` | 重验证周期,`0` 表示继承全局 |
| `skip_validation` | 是否跳过入库验证(**高危**,见下) |
**响应是 JSON 数组,没有 `status` 字段。**
### 启用 / 禁用插件(POST /api/pool/plugins/{name}/enable、POST /api/pool/plugins/{name}/disable)
无请求体。
```json
{ "success": true, "message": "插件 geonode_plugin 已启用" }
```
失败时返回 500 `api_error`。
### 设置抓取周期(PUT /api/pool/plugins/{name}/interval)
```json
{ "minutes": 30 }
```
`minutes` 必须 ≥ 1,否则 400 `invalid_parameter`。
```json
{ "success": true, "message": "插件 geonode_plugin 运行周期已设置为 30 分钟" }
```
### 设置插件检测地址(PUT /api/pool/plugins/{name}/test-url)
```json
{ "test_url": "https://example.com" }
```
**`test_url` 为空串或全空白表示继承全局** `[Server] test_url`:
```json
{ "success": true, "message": "插件 geonode_plugin 的测试地址已恢复为继承全局地址" }
```
### 设置插件验证策略(PUT /api/pool/plugins/{name}/validation)
```json
{ "reval_enabled": true, "reval_interval_minutes": 0, "skip_validation": false }
```
| 字段 | 类型 | 默认 | 说明 |
|---|---|---|---|
| `reval_enabled` | bool | `true` | 是否参与自动重验证 |
| `reval_interval_minutes` | int | `0` | 重验证周期(分钟);**`0` 表示继承全局**。传字符串也能接受,非整数返回 400 |
| `skip_validation` | bool | `false` | 跳过入库验证 |
```json
{ "success": true, "message": "插件 geonode_plugin 的验证策略已更新" }
```
> **`skip_validation` 是危险开关**:打开后该插件抓到的代理**不经验证直接入库**,
> 池的质量评分与自动清理都会失去意义。它只对**已加载的真实插件**开放,对伪插件名
> `manual_import` 会返回 404 —— 否则一次性打开就会让此后所有手动导入都不再验证。
### 立即执行插件(POST /api/pool/plugins/{name}/run)
无请求体。**只创建后台任务便返回**,被抓取本身受 `[Pool] plugins.execution_timeout_seconds`
约束。
```json
{
"task_id": "8f14e45f-ea6b-4c1e-9c1a-0f3b2d5e7a91",
"status": "running",
"message": "插件 geonode_plugin 抓取任务已创建"
}
```
`status` 只是创建瞬间的快照,进度与结果按 `task_id` 轮询。插件未加载 → 404 `api_error`。
### 热重载插件(POST /api/pool/plugins/{name}/reload)
无请求体。改完插件源码后无需重启进程。
**可预期的失败返回 200 + `success: false`,只有未预期异常才报 500:**
```json
{ "success": true, "message": "插件 geonode_plugin 热重载成功" }
```
```json
{ "success": false, "message": "插件 geonode_plugin 热重载失败(可能正在执行中或文件不存在)" }
```
插件文件不存在、正在执行、或加载返回空,都会走 `success: false`。
## 批量操作
**本节所有端点都立即返回 `task_id`**(除两个删除端点),进度按
[任务管理](#任务管理)轮询。
### 重新验证有效代理(POST /api/pool/validate/all-valid)
无参数。对当前所有 `is_valid = true` 的代理做一轮重验证。
```json
{ "task_id": "...", "status": "queued", "message": "验证任务已创建" }
```
### 重新验证失效代理(POST /api/pool/validate/all-invalid)
无参数。对当前所有 `is_valid = false` 的代理做一轮重验证。
> **这是把「被使用反馈标记失效」的代理立刻重验的入口**:被标记失效时 `total_checks`
> 会一并清零(日志记为「列入下一轮重验证候选」),而自动重验证每轮都会把
> `total_checks = 0` 的代理补充进候选(不受 `auto_revalidation.only_valid_proxies`
> 限制,但仍按各自的重验证间隔排期),所以这些代理不依赖本端点也会被重新验证。
```json
{ "task_id": "...", "status": "queued", "message": "验证任务已创建" }
```
### 全面检测并补归属地(POST /api/pool/update-geo/all)
无参数。对全部代理做完整评估(延迟、出口 IP、匿名度、协议支持),顺带补齐归属地。
比日常重验证重得多。
```json
{ "task_id": "...", "status": "queued", "message": "代理全面检测任务已创建" }
```
### 删除全部失效代理(DELETE /api/pool/delete/invalid)
无参数。**删完不可恢复。**
```json
{ "success": true, "message": "成功删除 34 个失效代理", "deleted_count": 34 }
```
没有失效代理时同样返回成功,`deleted_count` 为 0。本端点为 HEAVY 档。
### 批量验证指定代理(POST /api/pool/proxies/batch/validate)
```json
{ "proxy_ids": [1, 2, 3], "mode": "liveness" }
```
| 字段 | 类型 | 必填 | 默认 | 说明 |
|---|---|---|---|---|
| `proxy_ids` | int[] | 是 | — | 代理 id 列表;为空返回 400 |
| `mode` | string | 否 | `auto` | `auto` / `liveness` / `full`,其他取值返回 400 |
```json
{ "task_id": "...", "status": "running", "message": "正在验证 3 个代理" }
```
- **不存在的 id 会被静默忽略**,只有**一个都不存在**时才返回 404 `api_error`。
- 三种模式:`liveness` 只判通不通;`full` 做完整评估;`auto` 按代理已有信息自动选。
### 批量删除指定代理(DELETE /api/pool/proxies/batch/delete)
**注意:这是 DELETE 请求,但参数在请求体里。**
```json
{ "proxy_ids": [1, 2, 3] }
```
```json
{ "success": true, "message": "成功删除 3 个代理", "deleted_count": 3 }
```
`proxy_ids` 为空返回 400;不存在的 id 被静默忽略,`deleted_count` 取实际删除行数。
## 任务管理
### 列出任务(GET /api/pool/tasks)
| 参数 | 类型 | 默认 | 说明 |
|---|---|---|---|
| `status` | string | 不限 | 按状态过滤:`running` / `completed` / `failed` / `cancelled`(任务表里没有 `queued`,传它筛不到条目) |
```json
{
"tasks": [
{ "task_id": "8f14e45f-...", "status": "running", "progress": 40, "total": 120, "message": "..." }
],
"total": 1
}
```
### 查询任务状态(GET /api/pool/tasks/{task_id})
**响应就是任务条目本身,没有 `status` 外层字段**(条目内自带 `status` 表示任务状态)。
```json
{
"status": "running",
"message": "正在验证代理...",
"stage": "validate",
"progress": 40,
"total": 120,
"stage_done": 40,
"stage_total": 120,
"rate": 12.5,
"eta_seconds": 6,
"started_at": "2026-09-30T12:00:00",
"valid_count": 22,
"invalid_count": 10
}
```
字段随任务类型不同:只有导入/抓取类任务带 `stage` 与全套计数;批量验证与全面检测类
任务另有 `inconclusive_count`。**任务不存在或已被 TTL 淘汰 → 404 `api_error`。**
> 任务条目是**内存态**:进程重启后查不到任何历史任务。
### 取消任务(POST /api/pool/tasks/{task_id}/cancel)
无请求体。
```json
{ "success": true, "message": "任务已取消" }
```
**任务已经结束时返回 `success: false`,这不是错误**:
```json
{ "success": false, "message": "任务已经结束,无需取消" }
```
任务 id 不存在 → 404 `api_error`。
## 归属地维护
归属地解析用**离线库**(GeoLite2 与纯真库),不发外部查询请求。本节三个端点都是维护动作。
### 查询离线库状态(GET /api/pool/geo/status)
```json
{
"available": true,
"files": [ { "filename": "GeoLite2-City.mmdb", "exists": true,
"size": 65383808, "modified_at": "2026-10-02 12:00:00" } ],
"pending_recompute": 42
}
```
`modified_at` 在文件不存在时为 `null`。
| 字段 | 说明 |
|---|---|
| `available` | 离线库是否可用(**两个数据文件都在才算可用**) |
| `files` | 各数据文件的状态 |
| `pending_recompute` | 待重算归属地的记录数;查不到时按 0 返回,不报错 |
### 更新离线库(POST /api/pool/geo/update)
无参数。**会下载约 100 MB 的数据文件**,做成后台任务。
```json
{ "task_id": "...", "status": "queued", "message": "离线归属地库更新任务已创建" }
```
- 两个文件合计约 **105 MB**(GeoLite2 约 65 MB、纯真库约 40 MB)。
- **不校验磁盘空间**,空间不足会在下载中途失败。
- 一个文件失败不阻断另一个,失败项在任务结果的 `message` 里如实回报。
### 重算归属地(POST /api/pool/geo/recompute)
无参数。按离线库重算存量记录的归属地。
```json
{ "task_id": "...", "status": "queued", "message": "归属地重算任务已创建" }
```
**错误与边界:**
- **只重算 `country_code` 为空的记录**(即从未用离线库解析过的出口 IP)。已经解析过的
记录不会被重算;更新数据文件后没有专门的立即刷新操作,但 `POST /api/pool/update-geo/all`
的全面检测会把各出口 IP 重新交给解析器、可间接写回新归属地(受进程内查询缓存与耗时
限制,并非即时生效)。
- 归属地解析服务未启用时直接返回 **503 `api_error`**,不创建任务。
## 数据库与备份
### 列出备份(GET /api/pool/backups)
```json
{
"backups": [
{ "filename": "proxies_backup_20260930_030000.db", "size": 1048576, "created_at": "2026-09-30 03:00:00" }
]
}
```
备份由 `[Pool] database.backup_enabled` 控制的定时循环创建,本端点只列出已有备份。
**备份功能未启用时返回 404 `api_error`。** 本组备份列表与恢复是纯文件操作,
**池停止时同样可用**。
### 从备份恢复(POST /api/pool/backups/restore)
```json
{ "filename": "proxies_backup_20260930_030000.db" }
```
```json
{ "success": true, "message": "备份恢复成功,请重启代理池以加载恢复后的数据" }
```
**语义约定(三条都很重要):**
1. **池运行中返回 409 `pool_running`** —— 覆盖正在打开的数据库文件会造成损坏,
必须先停池。这是**唯一**会返回 `pool_running` 的端点。
2. 恢复**只覆盖库文件,不校验备份与当前库的版本兼容性**。
3. 恢复成功后**必须重启池**才会加载新数据。端点本身不重启池;面板的
「数据库维护」页会在恢复完成后自动重启(恢复前先停池、恢复后按原状态启回),
直接调接口的客户端要自己调 `POST /api/pool/start`。
`filename` 缺失 → 400 `invalid_parameter`。
### 数据库统计(GET /api/pool/database/stats)
```json
{ "stats": { "total_proxies": 120, "valid_proxies": 96, "invalid_proxies": 24,
"db_size_mb": 1.05 } }
```
需要池在运行。维护功能未启用时返回 404 `api_error`。
### 优化数据库(POST /api/pool/database/optimize)
无参数。执行 `ANALYZE` + `REINDEX` + `VACUUM`,**HEAVY 档,且 VACUUM 会长时间独占
数据库锁**,期间其他数据库操作会被阻塞。
```json
{ "success": true, "message": "数据库优化完成" }
```
## 代理池错误码
见[池侧约定 → 错误响应](#错误响应)。汇总表见
[附录 A](#附录-a错误码总表)。
---
# 四、端到端调用示例
## Python
```python
import requests
HOST = "http://localhost:5001"
TOKEN = "你的token"
PARAMS = {"token": TOKEN}
# 面板:看运行状态
status = requests.get(f"{HOST}/api/status", params=PARAMS).json()
print(status["current_proxy"], status["proxy_source_mode"])
# 面板:手动切换一次上游代理
switched = requests.get(f"{HOST}/api/switch_proxy", params=PARAMS).json()
if switched["status"] != "success":
# 冷却中和真的失败都是 error,靠字段区分
print("冷却中" if switched.get("cooldown") else switched.get("message"))
# 面板:查询最近一小时的失败请求
records = requests.get(
f"{HOST}/api/logs/records",
params={**PARAMS, "outcome": "failure", "limit": 50},
).json()
for row in records["records"]:
print(row["ts"], row["host"], row["upstream_label"], row["reason"])
# 代理池:取一个可用代理(纯文本)
proxy = requests.get(
f"{HOST}/api/pool/random",
params={**PARAMS, "status": "valid", "format": "text"},
).text.strip()
print(proxy) # 例如 http://1.2.3.4:8080
# 代理池:创建批量验证任务并轮询
task = requests.post(
f"{HOST}/api/pool/proxies/batch/validate",
params=PARAMS,
json={"proxy_ids": [1, 2, 3], "mode": "liveness"},
).json()
progress = requests.get(
f"{HOST}/api/pool/tasks/{task['task_id']}", params=PARAMS
).json()
print(progress["status"], progress.get("progress"), progress.get("total"))
```
## cURL
```bash
# 面板:运行状态
curl "http://localhost:5001/api/status?token=YOUR_TOKEN"
# 面板:保存配置(改轮换模式)
curl -X POST "http://localhost:5001/api/config?token=YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"server": {"mode": "loadbalance"}}'
# 面板:重启出口代理服务
curl -X POST "http://localhost:5001/api/service?token=YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"action": "restart"}'
# 面板:切换界面语言
curl -X POST "http://localhost:5001/api/language?token=YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"language": "en"}'
# 面板:导出最近一小时的访问记录为 CSV
curl -OJ "http://localhost:5001/api/logs/records/export?token=YOUR_TOKEN&format=csv"
# 代理池:筛选查询
curl "http://localhost:5001/api/pool/get?protocol=http&status=valid&limit=10&token=YOUR_TOKEN"
# 代理池:取一个可用代理(纯文本)
curl "http://localhost:5001/api/pool/random?status=valid&format=text&token=YOUR_TOKEN"
# 代理池:手动运行插件
curl -X POST "http://localhost:5001/api/pool/plugins/geonode_plugin/run?token=YOUR_TOKEN"
# 代理池:批量导入
curl -X POST "http://localhost:5001/api/pool/import?token=YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"proxies": ["http://1.2.3.4:8080"]}'
```
---
# 五、注意事项
1. **鉴权只在查询串里比对单一 token**,无 CSRF 防护,也不区分调用方权限:能访问面板即可
修改配置(含下游凭据)。不要把面板直接暴露到公网。
2. **两套错误约定并存**:面板侧多数业务失败是 `200 + status: "error"`,代理池侧是真实
4xx/5xx + `error_code`。调用方两个都要判。
3. **`message` 随界面语言变化**,只可用于展示,不可用于程序判断。
4. **运行状态在进程内存中**(当前代理、出口名册、任务表、广告关闭状态),进程重启即重置;
访问记录与域名统计落在 SQLite,重启后仍在。
5. **`web_port` 改动需重启整个进程;`port` 改动需重启出口服务**(走 `POST /api/config`
保存时面板会自动重启出口服务;手工编辑与命令行入口需自行重启),其余配置项保存后
即时生效,但个别项例外:如访问记录缓冲 `access_records_buffer_size` 要等代理服务下次
启动时才按新容量重建缓冲。手工编辑 `config.ini` 的 `[Server]` 改动也会被自动热重载
(两个入口都在轮询配置文件),但端口换绑不会自动发生;`[Pool]` 段由池自行热加载。
6. **多数**批量操作返回 `task_id`(两个删除端点直接返回 `success` / `deleted_count`),
用 `GET /api/pool/tasks/{task_id}` 查进度;任务动辄跑几十分钟,可随时用
`POST /api/pool/tasks/{task_id}/cancel` 中止。注意离线归属地库更新任务的下载跑在
不可取消的线程里,取消只会写出状态、下载仍会跑完。任务表是内存态且有 1 小时 TTL,
进程重启后不可查。
7. **代理池的查询参数是签名白名单**:参数名写错会被静默忽略,返回未筛选的结果而不报错。
8. 修改 `/api/pool/*` 的端点或字段时,**必须同步更新本文档** —— 适配层是手写的 Flask 蓝图,
不像 FastAPI 那样能自动生成 OpenAPI 描述。
---
# 附录 A:错误码总表
## 面板接口错误约定
| 情形 | HTTP | 响应体 |
|---|---|---|
| 成功 | 200 | `{"status": "success", ...}` |
| 成功(部分只读端点) | 200 | 直接返回数据,**无 `status` 字段** |
| 鉴权失败 | 401 | `{"status": "error", "message": "..."}` |
| 配置校验未通过(仅 `POST /api/config`) | 400 | `{"status": "error", "message": "...", "details": {"key": "...", "reason": "..."}}` |
| 其余业务失败 | **200** | `{"status": "error", "message": "..."}` |
## 代理池接口错误约定
| error_code | HTTP | 含义 |
|---|---|---|
| `invalid_parameter` | 400 | 请求参数非法 |
| `api_error` | 4xx / 5xx | 业务错误(代理不存在、无匹配结果、规格不符、插件操作失败、恢复失败等);状态码随 `ApiError` 自带 |
| `pool_running` | 409 | 池运行中,不允许该操作(仅 `POST /api/pool/backups/restore`) |
| `pool_unavailable` | 503 | 代理池未运行或正在重启 |
| `pool_timeout` | 504 | 代理池响应超时 |
| `queue_full` | 503 | 写入队列已满 |
| `internal_error` | 500 | 服务端内部错误 |
| *(无)* | 500 | 池启停失败:`POST /api/pool/start\|stop\|restart` 失败时**不带 `error_code`** |
---
# 附录 B:端点总索引
## 页面路由(非 `api`)
| 方法 | 路径 | 鉴权 | 说明 |
|---|---|---|---|
| GET | `/` | 否 | 带 token 跳转到 `/web` |
| GET | `/web` | 是 | 面板单页 |
| GET | `/static/<path>` | 否 | 前端静态资源 |
## 面板接口(34)
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | `/api/status` | 运行状态 |
| GET | `/api/config` | 读取 `[Server]` / `[Pool]` 配置 |
| POST | `/api/config` | 保存配置 |
| POST | `/api/language` | 切换界面语言 |
| GET | `/api/proxies` | 读本地代理列表 |
| POST | `/api/proxies` | 写本地代理列表 |
| GET | `/api/check_proxies` | 检测源清单里的代理可用性(local 来源即 `ip.txt` 全部条目) |
| GET | `/api/ip_lists` | 读 IP 黑白名单 |
| POST | `/api/ip_lists` | 写 IP 黑白名单 |
| GET | `/api/bypass_whitelist` | 读绕过名单 |
| POST | `/api/bypass_whitelist` | 写绕过名单 |
| POST | `/api/service` | 启停 / 重启出口代理服务 |
| GET | `/api/switch_proxy` | 刷新出口池:不满则补满,满员则换掉最近到期的那个出口 |
| GET | `/api/logs` | 查询内存日志 |
| GET | `/api/logs/stats` | 日志统计 |
| GET | `/api/logs/files` | 列出日志文件 |
| GET | `/api/logs/file` | 读取日志文件 |
| GET | `/api/logs/export` | 导出日志 |
| POST | `/api/logs/clear` | 清空日志 |
| GET | `/api/logs/records` | 查询访问记录 |
| GET | `/api/logs/records/export` | 导出访问记录 |
| GET | `/api/logs/records/options` | 访问记录过滤候选项 |
| POST | `/api/logs/records/clear` | 清空访问记录 |
| GET | `/api/logs/domains` | 查询域名统计 |
| POST | `/api/logs/domains/clear` | 清空域名统计 |
| GET | `/api/users` | 读出口代理账号 |
| POST | `/api/users` | 写出口代理账号 |
| GET | `/api/api_credentials` | 读上游 API 凭据 |
| POST | `/api/api_credentials` | 管理上游 API 凭据 |
| GET | `/api/version` | 版本检查结果(只读缓存,**免鉴权**) |
| POST | `/api/version/check` | 立即触发一次版本检查(**需鉴权**) |
| GET | `/api/ads` | 获取广告(**免鉴权**) |
| POST | `/api/ads/dismiss` | 关闭广告(**免鉴权**) |
| POST | `/api/ads/reopen` | 重新开启广告(**免鉴权**) |
## 代理池接口(38)
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | `/api/pool/status` | 池状态与全局统计 |
| GET | `/api/pool/schema` | `[Pool]` 字段结构(面板表单同源) |
| POST | `/api/pool/start` | 启动池 |
| POST | `/api/pool/stop` | 停止池(等写队列排空) |
| POST | `/api/pool/restart` | 重启池 |
| GET | `/api/pool/get` | 筛选代理列表(`json` / `text`) |
| GET | `/api/pool/random` | 随机取一个代理(`json` / `text`) |
| GET | `/api/pool/count` | 统计符合条件的代理数 |
| GET | `/api/pool/export` | 导出代理(`txt` / `json` / `csv`) |
| POST | `/api/pool/import` | 批量导入(先验证再入库) |
| POST | `/api/pool/proxies/{id}/favorite` | 切换收藏状态 |
| GET | `/api/pool/stats` | 池统计 |
| GET | `/api/pool/sources` | 来源插件名列表 |
| GET | `/api/pool/host-ip` | 本机公网出口 IP |
| GET | `/api/pool/plugins` | 插件列表与状态 |
| POST | `/api/pool/plugins/{name}/enable` | 启用插件 |
| POST | `/api/pool/plugins/{name}/disable` | 禁用插件 |
| PUT | `/api/pool/plugins/{name}/interval` | 设置抓取周期 |
| PUT | `/api/pool/plugins/{name}/test-url` | 设置插件专用检测地址 |
| PUT | `/api/pool/plugins/{name}/validation` | 设置插件验证策略 |
| POST | `/api/pool/plugins/{name}/run` | 立即执行插件 |
| POST | `/api/pool/plugins/{name}/reload` | 热重载插件 |
| POST | `/api/pool/validate/all-valid` | 重新验证所有有效代理 |
| POST | `/api/pool/validate/all-invalid` | 重新验证所有失效代理 |
| POST | `/api/pool/update-geo/all` | 全面检测并补归属地 |
| DELETE | `/api/pool/delete/invalid` | 删除全部失效代理 |
| POST | `/api/pool/proxies/batch/validate` | 批量验证指定代理 |
| DELETE | `/api/pool/proxies/batch/delete` | 批量删除指定代理(参数在请求体) |
| GET | `/api/pool/tasks` | 列出任务 |
| GET | `/api/pool/tasks/{task_id}` | 查询任务状态 |
| POST | `/api/pool/tasks/{task_id}/cancel` | 取消任务 |
| GET | `/api/pool/geo/status` | 离线归属地库状态 |
| POST | `/api/pool/geo/update` | 更新离线归属地库(约 100 MB) |
| POST | `/api/pool/geo/recompute` | 重算存量记录的归属地 |
| GET | `/api/pool/backups` | 列出备份 |
| POST | `/api/pool/backups/restore` | 从备份恢复(需先停池) |
| GET | `/api/pool/database/stats` | 数据库统计 |
| POST | `/api/pool/database/optimize` | 优化数据库(ANALYZE / REINDEX / VACUUM) |
---
# 附录 C:字段字典
## 代理对象
`GET /api/pool/get`、`GET /api/pool/random`(`format=json`)返回的对象。
| 字段 | 类型 | 说明 |
|---|---|---|
| `id` | int | 主键 |
| `proxy_url` | string | 完整代理地址;用户名与密码**都存在**时形如 `协议://用户:密码@ip:端口` |
| `protocol` | string | `http` / `https` / `socks5` |
| `ip` | string | 入口 IP |
| `port` | int | 端口 |
| `username` / `password` | string \| null | 代理认证凭据 |
| `real_ip` | string \| null | 出口 IP(经代理访问时对端看到的 IP) |
| `exit_ip_differs` | bool | 入口 IP 与出口 IP 是否不同。**查询与随机响应都提供**(两者共用同一序列化),导出路径刻意不带 |
| `region` | string | 中文归属地全名,如「中国 广东 深圳」;查不到为「未知」 |
| `country` / `province` / `city` | string | 中文归属地分级 |
| `region_en` / `country_en` / `province_en` / `city_en` | string \| null | 英文归属地分级 |
| `delay_ms` | float \| null | 最近一次测得的延迟(毫秒) |
| `validated_at` | string \| null | 最近一次验证时间(ISO 8601) |
| `source_plugin` | string | 来源插件名;手动导入为 `manual_import` |
| `is_favorite` | bool | 是否收藏(用户选择,抓取与验证不会覆盖) |
| `is_valid` | bool | 当前是否可用;列表用它给失效行打「失效」徽标。导出走自己的状态列 |
| `health_score` | float | 健康分,**0-100**,保留 1 位小数 |
| `anonymity_level` | string | `transparent` / `anonymous` / `elite` / `unverified` |
| `anonymity_fallback` | bool | 匿名度是否被判成兜底档 |
| `supports_https` / `supports_http` | bool | 是否支持对应协议 |
| `quality_assessed_at` | string \| null | 最近一次质量评估时间(ISO 8601) |
| `success_rate` | float | 验证成功率,0-1,保留 3 位小数 |
| `avg_delay_ms` | float \| null | 历史平均延迟(毫秒) |
| `total_checks` | int | 累计验证次数;缺值以 0 兜底 |
| `probe_success_rate` | float | 探测成功率,0-1,保留 3 位小数 |
| `probe_total_count` | int | 累计探测次数;缺值以 0 兜底 |
> **归属地空值的语义**:中英两套按列给出,`null` 表示**还没查过**(不是「未知」)。
> 英文列缺失时前端回退到中文列;两列相同表示该级没有对应译名。不要往这些列里写
> 「未知」文案 —— 「未知」是中文列查询过但没查到的取值。
## 任务对象
`GET /api/pool/tasks/{task_id}` 的响应,也是 `GET /api/pool/tasks` 列表里的元素。
| 字段 | 类型 | 说明 |
|---|---|---|
| `task_id` | string | 任务 id;**只在 `GET /api/pool/tasks` 的列表元素里出现**(列表逐条注入),单查接口的响应里没有 |
| `status` | string | `running` / `completed` / `failed` / `cancelled`(`queued` 只出现在部分创建响应里,任务表里没有) |
| `message` | string | 当前阶段的可读描述,跟随界面语言 |
| `progress` / `total` | int | 已完成 / 总数 |
| `started_at` / `completed_at` | string | 起止时间(ISO 8601);未结束则无 `completed_at` |
| `stage` | string | `prescreen` / `validate` / `ingest` / `done`(完成时写 `done`),**仅导入与抓取类任务有** |
| `stage_done` / `stage_total` | int | 当前阶段的进度,同上 |
| `in_flight` | int | 当前阶段正在并发处理中的条数,同上 |
| `rate` | float | 处理速率(条/秒) |
| `eta_seconds` | int \| null | 预计剩余秒数 |
| `valid_count` / `invalid_count` / `inconclusive_count` | int | 验证结果分类计数 |
| `prescreened_out` | int | 端口初筛即淘汰的条数 |
| `incomplete_count` / `failed_count` | int | 未完成 / 失败条数 |
| `results` | object | 数据文件名到结果文案(`已更新` / `失败: ...`)的映射,**仅离线归属地库更新任务的完成态有** |
**字段集合随任务类型与端点不同**,上表是各处的并集;只依赖 `status`、`progress`、
`total`、`message` 这四个字段最稳妥。
## 备份对象
`GET /api/pool/backups` 列表里的元素。
| 字段 | 类型 | 说明 |
|---|---|---|
| `filename` | string | 备份文件名,作为 `POST /api/pool/backups/restore` 的参数 |
| `size` | int | 文件大小(字节) |
| `created_at` | string | 创建时间,格式固定为 `YYYY-MM-DD HH:MM:SS` |