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 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' # ── 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'