803 lines
23 KiB
YAML
803 lines
23 KiB
YAML
openapi: 3.0.3
|
||
info:
|
||
title: 穿山甲 (Pangolin) Control-Plane API
|
||
description: |
|
||
穿山甲网络加速 控制面 API v1。
|
||
|
||
**设计原则**
|
||
- 控制面仅管账户、套餐、节点目录、兑换;用户流量走加速节点(WireGuard),不经过本 API。
|
||
- App 内无支付表单;资金流全走外部(激活码渠道)。
|
||
- 最小数据:不记录目的地址;仅保留计费所需元数据。
|
||
|
||
**认证**:除 `/auth/*` 外,全部接口需 `Authorization: Bearer <access_token>`。
|
||
|
||
**错误体**:`{ code: string, message_zh: string, message_en: string }`
|
||
version: 1.0.0
|
||
contact:
|
||
email: support@pangolin.app
|
||
|
||
servers:
|
||
- url: https://api.pangolin.app/v1
|
||
description: 生产环境
|
||
|
||
security:
|
||
- bearerAuth: []
|
||
|
||
components:
|
||
securitySchemes:
|
||
bearerAuth:
|
||
type: http
|
||
scheme: bearer
|
||
bearerFormat: JWT
|
||
|
||
schemas:
|
||
Error:
|
||
type: object
|
||
required: [code, message_zh, message_en]
|
||
properties:
|
||
code:
|
||
type: string
|
||
example: INVALID_CODE
|
||
message_zh:
|
||
type: string
|
||
example: 兑换码无效
|
||
message_en:
|
||
type: string
|
||
example: Invalid activation code
|
||
|
||
JWT:
|
||
type: object
|
||
required: [access_token, refresh_token]
|
||
properties:
|
||
access_token:
|
||
type: string
|
||
description: 有效期 15 分钟
|
||
refresh_token:
|
||
type: string
|
||
description: 有效期 30 天
|
||
|
||
UserProfile:
|
||
type: object
|
||
properties:
|
||
id:
|
||
type: string
|
||
format: uuid
|
||
email:
|
||
type: string
|
||
format: email
|
||
subscription:
|
||
$ref: '#/components/schemas/Subscription'
|
||
usage:
|
||
$ref: '#/components/schemas/UsageSummary'
|
||
|
||
Subscription:
|
||
type: object
|
||
properties:
|
||
plan:
|
||
type: string
|
||
enum: [free, pro, team]
|
||
expires_at:
|
||
type: string
|
||
format: date-time
|
||
nullable: true
|
||
description: null 表示无限期(免费版)
|
||
source:
|
||
type: string
|
||
enum: [trial, code, admin, free]
|
||
|
||
Device:
|
||
type: object
|
||
properties:
|
||
id:
|
||
type: string
|
||
format: uuid
|
||
name:
|
||
type: string
|
||
platform:
|
||
type: string
|
||
example: ios
|
||
pubkey:
|
||
type: string
|
||
description: WireGuard 公钥 (base64)
|
||
last_seen:
|
||
type: string
|
||
format: date-time
|
||
|
||
Plan:
|
||
type: object
|
||
properties:
|
||
id:
|
||
type: string
|
||
format: uuid
|
||
code:
|
||
type: string
|
||
enum: [free, pro, team]
|
||
description: free=免费版; pro=25¥/月; team=99¥/月(10席位)
|
||
max_devices:
|
||
type: integer
|
||
max_routes:
|
||
type: integer
|
||
daily_minutes:
|
||
type: integer
|
||
nullable: true
|
||
description: null 表示无限制;免费版为 10 分钟/天
|
||
ad_gate:
|
||
type: boolean
|
||
description: 是否需要每日激励视频解锁
|
||
|
||
Node:
|
||
type: object
|
||
properties:
|
||
id:
|
||
type: string
|
||
format: uuid
|
||
region:
|
||
type: string
|
||
description: 2 字母地区码,如 HK / JP / SG
|
||
example: HK
|
||
name_zh:
|
||
type: string
|
||
example: 香港 01
|
||
name_en:
|
||
type: string
|
||
example: Hong Kong 01
|
||
tier:
|
||
type: string
|
||
enum: [free, pro]
|
||
status:
|
||
type: string
|
||
enum: [up, down, draining]
|
||
weight:
|
||
type: integer
|
||
latency_ms:
|
||
type: integer
|
||
nullable: true
|
||
description: 由客户端测速后本地排序;此字段为服务端建议值
|
||
|
||
NodeList:
|
||
type: object
|
||
required: [version, nodes]
|
||
properties:
|
||
version:
|
||
type: integer
|
||
description: 节点目录版本号,递增;客户端带 if_version 实现 304
|
||
nodes:
|
||
type: array
|
||
items:
|
||
$ref: '#/components/schemas/Node'
|
||
|
||
WireGuardConfig:
|
||
type: object
|
||
required: [endpoint, server_pubkey, client_ip, dns]
|
||
properties:
|
||
endpoint:
|
||
type: string
|
||
description: 节点 WireGuard 端点,格式 host:port
|
||
example: hk1.pangolin.app:51820
|
||
server_pubkey:
|
||
type: string
|
||
description: 服务端 WireGuard 公钥 (base64)
|
||
client_ip:
|
||
type: string
|
||
description: 分配给客户端的隧道内网 IP
|
||
example: 10.66.0.2/32
|
||
dns:
|
||
type: string
|
||
example: 1.1.1.1
|
||
ttl_seconds:
|
||
type: integer
|
||
nullable: true
|
||
description: peer TTL;免费版 = 今日剩余秒数,付费版 = 86400
|
||
|
||
UsageSummary:
|
||
type: object
|
||
properties:
|
||
bytes_up:
|
||
type: integer
|
||
bytes_down:
|
||
type: integer
|
||
minutes_used:
|
||
type: integer
|
||
ad_unlocked_at:
|
||
type: string
|
||
format: date-time
|
||
nullable: true
|
||
description: 今日激励视频解锁时刻;null = 未解锁(免费版)
|
||
|
||
DailyUsage:
|
||
type: object
|
||
properties:
|
||
date:
|
||
type: string
|
||
format: date
|
||
bytes_up:
|
||
type: integer
|
||
bytes_down:
|
||
type: integer
|
||
minutes_used:
|
||
type: integer
|
||
|
||
RoutingRule:
|
||
type: object
|
||
required: [type, value, action, enabled]
|
||
properties:
|
||
type:
|
||
type: string
|
||
enum: [domain, domain_suffix, domain_keyword, ip_cidr, geoip, geosite]
|
||
value:
|
||
type: string
|
||
description: geoip/geosite 目前仅白名单 cn(自托管规则集);ip_cidr 需合法 CIDR
|
||
example: x.com
|
||
action:
|
||
type: string
|
||
enum: [direct, proxy, reject]
|
||
note:
|
||
type: string
|
||
enabled:
|
||
type: boolean
|
||
|
||
RoutingBuiltin:
|
||
type: object
|
||
properties:
|
||
china_direct:
|
||
type: boolean
|
||
description: 国内域名/IP 直连
|
||
lan_direct:
|
||
type: boolean
|
||
readOnly: true
|
||
description: >-
|
||
reserved/always-on:局域网直连由系统层恒渲染(route_exclude_address +
|
||
固定 LAN 直连规则),渲染器不读此开关,当前始终生效;字段保留供未来扩展
|
||
per-user 关闭,目前无实际效果。
|
||
private_via_tunnel:
|
||
type: boolean
|
||
readOnly: true
|
||
description: >-
|
||
reserved/always-on:配置了 PANGOLIN_PRIVATE_SPLIT_DOMAINS 时,这些私有
|
||
服务域名恒强制走隧道,渲染器不读此开关,当前始终生效;字段保留供未来扩展
|
||
per-user 关闭,目前无实际效果。
|
||
|
||
RoutingProfile:
|
||
type: object
|
||
required: [mode, builtin, rules, final]
|
||
properties:
|
||
mode:
|
||
type: string
|
||
enum: [rule, global, direct]
|
||
builtin:
|
||
$ref: '#/components/schemas/RoutingBuiltin'
|
||
rules:
|
||
type: array
|
||
maxItems: 200
|
||
items:
|
||
$ref: '#/components/schemas/RoutingRule'
|
||
final:
|
||
type: string
|
||
enum: [proxy, direct]
|
||
|
||
RoutingProfileResponse:
|
||
description: GET /me/routing 响应体:档案字段 + 只读的系统强制域名清单
|
||
allOf:
|
||
- $ref: '#/components/schemas/RoutingProfile'
|
||
- type: object
|
||
required: [system_locked_domains]
|
||
properties:
|
||
system_locked_domains:
|
||
type: array
|
||
readOnly: true
|
||
items:
|
||
type: string
|
||
description: >-
|
||
系统强制走隧道的私有服务域名(PANGOLIN_PRIVATE_SPLIT_DOMAINS)。
|
||
用户对这些域名的自定义规则会静默失效;仅供客户端提示用户,不可写入
|
||
(POST 请求体中出现该字段会被忽略,不会持久化)。
|
||
example: [nas.example.com]
|
||
|
||
RoutingValidationError:
|
||
type: object
|
||
required: [code, message_zh, message_en, errors]
|
||
properties:
|
||
code:
|
||
type: string
|
||
example: routing_invalid
|
||
message_zh:
|
||
type: string
|
||
example: 规则校验未通过
|
||
message_en:
|
||
type: string
|
||
example: Rule validation failed
|
||
errors:
|
||
type: array
|
||
items:
|
||
type: object
|
||
required: [index, field, reason]
|
||
properties:
|
||
index:
|
||
type: integer
|
||
description: -1 表示档案级字段(mode/final/rules 数量),否则为 rules 下标
|
||
field:
|
||
type: string
|
||
reason:
|
||
type: string
|
||
|
||
paths:
|
||
# ── Auth ──────────────────────────────────────────────────────────────────
|
||
|
||
/auth/code:
|
||
post:
|
||
summary: 发送邮箱验证码
|
||
description: 触发邮件发送 6 位数字验证码;有效期 10 分钟,限 1 次/分钟(Redis 滑窗)。
|
||
security: []
|
||
requestBody:
|
||
required: true
|
||
content:
|
||
application/json:
|
||
schema:
|
||
type: object
|
||
required: [email]
|
||
properties:
|
||
email:
|
||
type: string
|
||
format: email
|
||
responses:
|
||
'204':
|
||
description: 验证码已发送
|
||
'422':
|
||
description: 参数格式错误
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/Error'
|
||
'429':
|
||
description: 请求频率超限
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/Error'
|
||
|
||
/auth/register:
|
||
post:
|
||
summary: 注册账户(自动开 7 天体验期)
|
||
security: []
|
||
requestBody:
|
||
required: true
|
||
content:
|
||
application/json:
|
||
schema:
|
||
type: object
|
||
required: [email, code, password]
|
||
properties:
|
||
email:
|
||
type: string
|
||
format: email
|
||
code:
|
||
type: string
|
||
minLength: 6
|
||
maxLength: 6
|
||
pattern: '^\d{6}$'
|
||
password:
|
||
type: string
|
||
minLength: 8
|
||
responses:
|
||
'200':
|
||
description: 注册成功,返回 JWT
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/JWT'
|
||
'400':
|
||
description: 验证码错误 / 已过期 / 邮箱已注册
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/Error'
|
||
'422':
|
||
description: 参数格式错误
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/Error'
|
||
|
||
/auth/login:
|
||
post:
|
||
summary: 登录
|
||
security: []
|
||
requestBody:
|
||
required: true
|
||
content:
|
||
application/json:
|
||
schema:
|
||
type: object
|
||
required: [email, password]
|
||
properties:
|
||
email:
|
||
type: string
|
||
format: email
|
||
password:
|
||
type: string
|
||
responses:
|
||
'200':
|
||
description: 登录成功,返回 JWT
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/JWT'
|
||
'401':
|
||
description: 邮箱或密码错误
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/Error'
|
||
|
||
/auth/refresh:
|
||
post:
|
||
summary: 刷新 access_token
|
||
security: []
|
||
requestBody:
|
||
required: true
|
||
content:
|
||
application/json:
|
||
schema:
|
||
type: object
|
||
required: [refresh_token]
|
||
properties:
|
||
refresh_token:
|
||
type: string
|
||
responses:
|
||
'200':
|
||
description: 新 JWT
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/JWT'
|
||
'401':
|
||
description: refresh_token 无效或已过期
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/Error'
|
||
|
||
# ── Me ────────────────────────────────────────────────────────────────────
|
||
|
||
/me:
|
||
get:
|
||
summary: 获取当前用户信息(账户 + 订阅 + 用量摘要)
|
||
responses:
|
||
'200':
|
||
description: 用户信息
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/UserProfile'
|
||
'401':
|
||
description: 未认证
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/Error'
|
||
|
||
/me/devices:
|
||
get:
|
||
summary: 获取设备列表
|
||
responses:
|
||
'200':
|
||
description: 设备列表
|
||
content:
|
||
application/json:
|
||
schema:
|
||
type: array
|
||
items:
|
||
$ref: '#/components/schemas/Device'
|
||
'401':
|
||
description: 未认证
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/Error'
|
||
|
||
/me/devices/{id}:
|
||
delete:
|
||
summary: 移除设备(回收节点 peer)
|
||
parameters:
|
||
- name: id
|
||
in: path
|
||
required: true
|
||
schema:
|
||
type: string
|
||
format: uuid
|
||
responses:
|
||
'204':
|
||
description: 已移除
|
||
'401':
|
||
description: 未认证
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/Error'
|
||
'404':
|
||
description: 设备不存在
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/Error'
|
||
|
||
/me/routing:
|
||
get:
|
||
summary: 获取当前用户的分流配置(可配置分流;未自定义时返回默认档案)
|
||
responses:
|
||
'200':
|
||
description: 分流档案(含只读 system_locked_domains)
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/RoutingProfileResponse'
|
||
'401':
|
||
description: 未认证
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/Error'
|
||
post:
|
||
summary: 保存当前用户的分流配置(整份覆盖;校验失败不部分保存)
|
||
requestBody:
|
||
required: true
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/RoutingProfile'
|
||
responses:
|
||
'200':
|
||
description: 保存成功,返回规整化后的档案
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/RoutingProfile'
|
||
'400':
|
||
description: 校验失败(逐条错误,不部分保存)
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/RoutingValidationError'
|
||
'401':
|
||
description: 未认证
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/Error'
|
||
|
||
# ── Plans & Subscriptions ─────────────────────────────────────────────────
|
||
|
||
/plans:
|
||
get:
|
||
summary: 获取套餐目录(供客户端展示价格页)
|
||
security: []
|
||
responses:
|
||
'200':
|
||
description: 套餐列表
|
||
content:
|
||
application/json:
|
||
schema:
|
||
type: array
|
||
items:
|
||
$ref: '#/components/schemas/Plan'
|
||
|
||
/redeem:
|
||
post:
|
||
summary: 兑换激活码(幂等;同码重复提交返回首次结果)
|
||
requestBody:
|
||
required: true
|
||
content:
|
||
application/json:
|
||
schema:
|
||
type: object
|
||
required: [code]
|
||
properties:
|
||
code:
|
||
type: string
|
||
description: Crockford Base32,16 位,含校验位
|
||
example: 0A1B2C3D4E5F6G7H
|
||
responses:
|
||
'200':
|
||
description: 兑换成功,返回最新订阅信息
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/Subscription'
|
||
'400':
|
||
description: 激活码无效、已使用或已作废
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/Error'
|
||
'401':
|
||
description: 未认证
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/Error'
|
||
'429':
|
||
description: 兑换失败频率超限(5次/小时后锁 1 小时)
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/Error'
|
||
|
||
# ── Ads ───────────────────────────────────────────────────────────────────
|
||
|
||
/ads/unlock:
|
||
post:
|
||
summary: 免费版:激励视频解锁当日时长
|
||
requestBody:
|
||
required: true
|
||
content:
|
||
application/json:
|
||
schema:
|
||
type: object
|
||
required: [device_id, ad_token]
|
||
properties:
|
||
device_id:
|
||
type: string
|
||
format: uuid
|
||
ad_token:
|
||
type: string
|
||
description: 广告 SDK 服务端回执 token(用于服务端验证)
|
||
responses:
|
||
'200':
|
||
description: 当日时长已解锁
|
||
content:
|
||
application/json:
|
||
schema:
|
||
type: object
|
||
required: [ad_unlocked_at]
|
||
properties:
|
||
ad_unlocked_at:
|
||
type: string
|
||
format: date-time
|
||
'400':
|
||
description: ad_token 无效 / 今日已解锁
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/Error'
|
||
'401':
|
||
description: 未认证
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/Error'
|
||
|
||
# ── Nodes ─────────────────────────────────────────────────────────────────
|
||
|
||
/nodes:
|
||
get:
|
||
summary: 获取节点目录(按当前订阅过滤;支持 304)
|
||
parameters:
|
||
- name: if_version
|
||
in: query
|
||
required: false
|
||
schema:
|
||
type: integer
|
||
description: 客户端持有的版本号;服务端未变更时返回 304
|
||
responses:
|
||
'200':
|
||
description: 节点列表(含版本号)
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/NodeList'
|
||
'304':
|
||
description: 节点目录未更新
|
||
'401':
|
||
description: 未认证
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/Error'
|
||
|
||
/nodes/{id}/connect:
|
||
post:
|
||
summary: 连接节点,下发 WireGuard 配置
|
||
description: |
|
||
校验:订阅状态、设备数限制;免费版额外校验当日 ad_unlocked_at 与剩余分钟。
|
||
通过后经 gRPC 让节点 agent 注册 peer,返回 WireGuard 配置。
|
||
parameters:
|
||
- name: id
|
||
in: path
|
||
required: true
|
||
schema:
|
||
type: string
|
||
format: uuid
|
||
requestBody:
|
||
required: true
|
||
content:
|
||
application/json:
|
||
schema:
|
||
type: object
|
||
required: [device_pubkey]
|
||
properties:
|
||
device_pubkey:
|
||
type: string
|
||
description: 客户端 WireGuard 公钥 (base64)
|
||
responses:
|
||
'200':
|
||
description: WireGuard 配置
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/WireGuardConfig'
|
||
'401':
|
||
description: 未认证
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/Error'
|
||
'402':
|
||
description: 订阅已到期 / 设备数超限 / 免费版未解锁当日时长
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/Error'
|
||
'404':
|
||
description: 节点不存在或已下线
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/Error'
|
||
|
||
/nodes/{id}/disconnect:
|
||
post:
|
||
summary: 断开节点(回收 WireGuard peer)
|
||
parameters:
|
||
- name: id
|
||
in: path
|
||
required: true
|
||
schema:
|
||
type: string
|
||
format: uuid
|
||
responses:
|
||
'204':
|
||
description: 已断开
|
||
'401':
|
||
description: 未认证
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/Error'
|
||
'404':
|
||
description: 节点不存在
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/Error'
|
||
|
||
# ── Usage ─────────────────────────────────────────────────────────────────
|
||
|
||
/usage:
|
||
get:
|
||
summary: 获取用量曲线(统计页)
|
||
parameters:
|
||
- name: days
|
||
in: query
|
||
required: false
|
||
schema:
|
||
type: integer
|
||
minimum: 1
|
||
maximum: 30
|
||
default: 7
|
||
description: 返回最近 N 天数据
|
||
responses:
|
||
'200':
|
||
description: 每日用量数组(含字节数与时长,不含目的地址)
|
||
content:
|
||
application/json:
|
||
schema:
|
||
type: array
|
||
items:
|
||
$ref: '#/components/schemas/DailyUsage'
|
||
'401':
|
||
description: 未认证
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/Error'
|