openapi: 3.0.3 info: title: 穿山甲 (Pangolin) Control-Plane API description: | 穿山甲网络加速 控制面 API v1。 **设计原则** - 控制面仅管账户、套餐、节点目录、兑换;用户流量走加速节点(WireGuard),不经过本 API。 - App 内无支付表单;资金流全走外部(激活码渠道)。 - 最小数据:不记录目的地址;仅保留计费所需元数据。 **认证**:除 `/auth/*` 外,全部接口需 `Authorization: Bearer `。 **错误体**:`{ 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'