feat: CI 流水线 — lint + 单测 + OpenAPI 校验 + 脱敏扫描 + 镜像构建 [tsk_P5b5nIrEsfrV]
新增 .gitea/workflows/ci.yml 五个 Job:
1. lint — shellcheck -S warning 扫描全部 deploy/ shell 脚本
2. unit-test — docker-compose config 语法校验 + nginx -t(桩证书)
3. openapi-check — openapi-spec-validator 验证 design/server/openapi.yaml
4. redline-scan — ci/scan-redline.sh 扫描 UI 文案红线词(design/ jsx/dart/html)
5. image-build — docker build pangolin-edge:ci
附带:
- ci/scan-redline.sh:脱敏扫描脚本,过滤注释行与外部渠道 handle
- ci/nginx-test.sh:自签桩证书 + nginx -t,CI 免依赖真实 Let's Encrypt
- design/server/openapi.yaml:依据 ARCHITECTURE.md §3 展开的 OAS 3.0 完整契约
- dparts.jsx / parts.jsx:修复 killSwitchSub EN 文案「the VPN drops」红线词
→ 改为「connection drops」(行为描述,不提产品类别)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,96 @@
|
|||||||
|
name: ci-pangolin
|
||||||
|
|
||||||
|
# 触发条件:deploy/ 或 design/ 或 ci/ 变更时,以及所有向 main 的 PR
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
paths:
|
||||||
|
- 'deploy/**'
|
||||||
|
- 'design/**'
|
||||||
|
- 'ci/**'
|
||||||
|
- '.gitea/workflows/ci.yml'
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
|
||||||
|
# ── Job 1: Lint (shellcheck) ─────────────────────────────────────────────
|
||||||
|
lint:
|
||||||
|
name: Lint — shellcheck
|
||||||
|
runs-on: nas
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: shellcheck deploy/scripts/*.sh
|
||||||
|
run: |
|
||||||
|
docker run --rm \
|
||||||
|
-v "$PWD/deploy:/mnt/deploy:ro" \
|
||||||
|
koalaman/shellcheck:stable \
|
||||||
|
shellcheck -S warning \
|
||||||
|
/mnt/deploy/scripts/gen-secrets.sh \
|
||||||
|
/mnt/deploy/scripts/print-clients.sh \
|
||||||
|
/mnt/deploy/scripts/deploy.sh \
|
||||||
|
/mnt/deploy/scripts/cutover.sh \
|
||||||
|
/mnt/deploy/scripts/rollback.sh
|
||||||
|
|
||||||
|
- name: shellcheck deploy/edge/pangolin-entrypoint.sh
|
||||||
|
run: |
|
||||||
|
docker run --rm \
|
||||||
|
-v "$PWD/deploy/edge:/mnt/edge:ro" \
|
||||||
|
koalaman/shellcheck:stable \
|
||||||
|
shellcheck -S warning /mnt/edge/pangolin-entrypoint.sh
|
||||||
|
|
||||||
|
# ── Job 2: Unit Tests ────────────────────────────────────────────────────
|
||||||
|
unit-test:
|
||||||
|
name: Unit Tests — nginx cfg + compose
|
||||||
|
runs-on: nas
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: validate docker-compose.yml (syntax)
|
||||||
|
run: docker-compose -f deploy/docker-compose.yml config -q
|
||||||
|
|
||||||
|
- name: nginx config lint with stub certs
|
||||||
|
run: bash ci/nginx-test.sh
|
||||||
|
|
||||||
|
# ── Job 3: OpenAPI Sync Check ────────────────────────────────────────────
|
||||||
|
openapi-check:
|
||||||
|
name: OpenAPI Sync Check
|
||||||
|
runs-on: nas
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
# openapi-spec-validator 是纯 Python、无外部依赖,校验 OAS 3.0 结构合法性。
|
||||||
|
- name: lint design/server/openapi.yaml
|
||||||
|
run: |
|
||||||
|
docker run --rm \
|
||||||
|
-v "$PWD/design/server:/spec:ro" \
|
||||||
|
python:3.12-alpine \
|
||||||
|
sh -c "pip install openapi-spec-validator --quiet && \
|
||||||
|
python -m openapi_spec_validator /spec/openapi.yaml"
|
||||||
|
|
||||||
|
# ── Job 4: Redline Word Scan (脱敏) ──────────────────────────────────────
|
||||||
|
redline-scan:
|
||||||
|
name: Redline Scan — 脱敏 (UI 文案)
|
||||||
|
runs-on: nas
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: scan UI text resources for prohibited words
|
||||||
|
run: bash ci/scan-redline.sh
|
||||||
|
|
||||||
|
# ── Job 5: Container Image Build ────────────────────────────────────────
|
||||||
|
image-build:
|
||||||
|
name: Image Build — pangolin-edge
|
||||||
|
runs-on: nas
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: build pangolin-edge:ci
|
||||||
|
run: docker build -t pangolin-edge:ci ./deploy/edge
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# nginx-test.sh — CI 环境下校验 nginx 配置,用自签桩证书代替真实 Let's Encrypt 证书。
|
||||||
|
# 依赖: docker、openssl
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
CERT_DIR="$(mktemp -d)"
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
rm -rf "$CERT_DIR"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT INT TERM
|
||||||
|
|
||||||
|
echo "→ 生成自签桩证书 ..."
|
||||||
|
|
||||||
|
# 三个需要证书的域名
|
||||||
|
for domain in vpn.51yanmei.com blog.51yanmei.com jiu.51yanmei.com; do
|
||||||
|
mkdir -p "${CERT_DIR}/live/${domain}"
|
||||||
|
openssl req -x509 -nodes -newkey ec -pkeyopt ec_paramgen_curve:P-256 -days 1 \
|
||||||
|
-keyout "${CERT_DIR}/live/${domain}/privkey.pem" \
|
||||||
|
-out "${CERT_DIR}/live/${domain}/fullchain.pem" \
|
||||||
|
-subj "/CN=${domain}" 2>/dev/null
|
||||||
|
done
|
||||||
|
|
||||||
|
# options-ssl-nginx.conf 桩(nginx -t 仅检查文件可读,不校验内容)
|
||||||
|
printf '# stub\n' > "${CERT_DIR}/options-ssl-nginx.conf"
|
||||||
|
|
||||||
|
# ssl-dhparams.pem 桩(生成 512-bit DH,足够 nginx 解析,CI 不要求安全强度)
|
||||||
|
openssl dhparam -out "${CERT_DIR}/ssl-dhparams.pem" 512 2>/dev/null
|
||||||
|
|
||||||
|
# 桩目录(nginx alias/root 在 -t 时不校验是否存在,但 volume 挂载需目录存在)
|
||||||
|
mkdir -p "${CERT_DIR}/jiu/images" "${CERT_DIR}/jiu/web" "${CERT_DIR}/jiu/marketing"
|
||||||
|
mkdir -p "${CERT_DIR}/www-pay"
|
||||||
|
|
||||||
|
echo "→ 运行 nginx -t ..."
|
||||||
|
|
||||||
|
docker run --rm \
|
||||||
|
-v "${REPO_ROOT}/deploy/edge/nginx.conf:/etc/nginx/nginx.conf:ro" \
|
||||||
|
-v "${REPO_ROOT}/deploy/edge/stream.conf:/etc/nginx/stream.conf:ro" \
|
||||||
|
-v "${REPO_ROOT}/deploy/edge/conf.d:/etc/nginx/conf.d:ro" \
|
||||||
|
-v "${CERT_DIR}:/etc/letsencrypt:ro" \
|
||||||
|
-v "${CERT_DIR}/jiu:/opt/jiu:ro" \
|
||||||
|
-v "${CERT_DIR}/www-pay:/var/www/pay:ro" \
|
||||||
|
nginx:1.27-alpine nginx -t
|
||||||
|
|
||||||
|
echo "✅ nginx 配置校验通过。"
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# scan-redline.sh — 脱敏红线词扫描(UI 文案资源)。
|
||||||
|
#
|
||||||
|
# 扫描范围: design/ui_kits/**/*.{jsx,js,html}
|
||||||
|
# design/flutter/**/*.{dart,html}
|
||||||
|
# 不扫描: README.md、CLAUDE.md 等纯文档文件
|
||||||
|
#
|
||||||
|
# 红线词(来源: design/CLAUDE.md §1 铁律 13):
|
||||||
|
# VPN 翻墙 科学上网 突破封锁 自由穿越 Go anywhere
|
||||||
|
#
|
||||||
|
# 允许例外:
|
||||||
|
# - 纯注释行 (// ... 或 /* ... 或 * ... 开头)
|
||||||
|
# - 外部渠道 handle (@PangolinVPN_bot / @pangolinvpn 等)
|
||||||
|
#
|
||||||
|
# 发现违规 → 非零退出码 (CI fail)。
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
cd "$REPO_ROOT"
|
||||||
|
|
||||||
|
VIOLATIONS=0
|
||||||
|
SCANNED=0
|
||||||
|
|
||||||
|
# 逐文件扫描函数
|
||||||
|
# 用法: scan_file <文件路径>
|
||||||
|
scan_file() {
|
||||||
|
local file="$1"
|
||||||
|
local file_violations=0
|
||||||
|
|
||||||
|
# 逐个红线词扫描
|
||||||
|
for word in 'VPN' '翻墙' '科学上网' '突破封锁' '自由穿越' '[Gg]o anywhere'; do
|
||||||
|
# 1. 找出含有红线词的行
|
||||||
|
# 2. 排除纯注释行 (行首可选空白 + // 或 /* 或 *)
|
||||||
|
# 3. 排除外部渠道 handle 行 (@PangolinVPN_bot / @pangolinvpn)
|
||||||
|
local hits
|
||||||
|
hits="$(grep -nE "$word" "$file" 2>/dev/null \
|
||||||
|
| grep -vE '^[0-9]+:[[:space:]]*(//|/\*|\*)' \
|
||||||
|
| grep -vE '@[Pp]angolin[Vv][Pp][Nn]_?[Bb]ot' \
|
||||||
|
| grep -vE '@pangolinvpn' \
|
||||||
|
|| true)"
|
||||||
|
|
||||||
|
if [ -n "$hits" ]; then
|
||||||
|
if [ "$file_violations" -eq 0 ]; then
|
||||||
|
echo "❌ $file" >&2
|
||||||
|
fi
|
||||||
|
echo "$hits" | while IFS= read -r line; do
|
||||||
|
printf ' 红线词 [%s] %s\n' "$word" "$line" >&2
|
||||||
|
done
|
||||||
|
file_violations=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
return "$file_violations"
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "→ 开始脱敏扫描 ..."
|
||||||
|
|
||||||
|
# 扫描 design/ui_kits 下的 jsx / js / html 文件
|
||||||
|
while IFS= read -r -d '' f; do
|
||||||
|
SCANNED=$((SCANNED + 1))
|
||||||
|
if ! scan_file "$f"; then
|
||||||
|
VIOLATIONS=$((VIOLATIONS + 1))
|
||||||
|
fi
|
||||||
|
done < <(find design/ui_kits -type f \( -name "*.jsx" -o -name "*.js" -o -name "*.html" \) -not -name "README.md" -print0)
|
||||||
|
|
||||||
|
# 扫描 design/flutter 下的 dart / html 文件
|
||||||
|
while IFS= read -r -d '' f; do
|
||||||
|
SCANNED=$((SCANNED + 1))
|
||||||
|
if ! scan_file "$f"; then
|
||||||
|
VIOLATIONS=$((VIOLATIONS + 1))
|
||||||
|
fi
|
||||||
|
done < <(find design/flutter -type f \( -name "*.dart" -o -name "*.html" \) -not -name "README.md" -print0)
|
||||||
|
|
||||||
|
echo "→ 扫描完成:共 ${SCANNED} 个文件,${VIOLATIONS} 个违规。"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if [ "$VIOLATIONS" -ne 0 ]; then
|
||||||
|
echo "脱敏检查失败!请根据 design/CLAUDE.md §1 铁律 13 修改以上文案:" >&2
|
||||||
|
echo " - 禁用词:VPN、翻墙、科学上网、突破封锁、自由穿越、Go anywhere" >&2
|
||||||
|
echo " - 代用词:网络加速、极速畅连、稳定、加速线路、隐私保护、无日志" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✅ 脱敏扫描通过 — 未发现红线词。"
|
||||||
@@ -0,0 +1,655 @@
|
|||||||
|
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]
|
||||||
|
|
||||||
|
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'
|
||||||
@@ -72,7 +72,7 @@ const DSTRINGS = {
|
|||||||
weekTraffic:{zh:'本周流量 (GB)',en:'This week (GB)'},
|
weekTraffic:{zh:'本周流量 (GB)',en:'This week (GB)'},
|
||||||
autostart:{zh:'开机自启动',en:'Launch at login'}, autostartSub:{zh:'登录系统时自动启动穿山甲',en:'Start Pangolin when you log in'},
|
autostart:{zh:'开机自启动',en:'Launch at login'}, autostartSub:{zh:'登录系统时自动启动穿山甲',en:'Start Pangolin when you log in'},
|
||||||
smartRoute:{zh:'智能分流',en:'Smart routing'}, smartRouteSub:{zh:'海外应用加速,本地直连',en:'Accelerate overseas apps, keep local direct'},
|
smartRoute:{zh:'智能分流',en:'Smart routing'}, smartRouteSub:{zh:'海外应用加速,本地直连',en:'Accelerate overseas apps, keep local direct'},
|
||||||
killSwitch:{zh:'Kill Switch',en:'Kill Switch'}, killSwitchSub:{zh:'断线时阻断网络,防止 IP 泄露',en:'Block traffic if the VPN drops'},
|
killSwitch:{zh:'Kill Switch',en:'Kill Switch'}, killSwitchSub:{zh:'断线时阻断网络,防止 IP 泄露',en:'Block all traffic if connection drops'},
|
||||||
language:{zh:'语言',en:'Language'}, darkAppearance:{zh:'深色外观',en:'Dark appearance'}, stateOn:{zh:'已开启',en:'On'}, followLight:{zh:'跟随浅色',en:'Off'},
|
language:{zh:'语言',en:'Language'}, darkAppearance:{zh:'深色外观',en:'Dark appearance'}, stateOn:{zh:'已开启',en:'On'}, followLight:{zh:'跟随浅色',en:'Off'},
|
||||||
protocol:{zh:'协议',en:'Protocol'}, version:{zh:'版本',en:'Version'},
|
protocol:{zh:'协议',en:'Protocol'}, version:{zh:'版本',en:'Version'},
|
||||||
days:{zh:['周一','周二','周三','周四','周五','周六','周日'],en:['Mon','Tue','Wed','Thu','Fri','Sat','Sun']},
|
days:{zh:['周一','周二','周三','周四','周五','周六','周日'],en:['Mon','Tue','Wed','Thu','Fri','Sat','Sun']},
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ const STRINGS = {
|
|||||||
smartRoute: { zh: '智能分流', en: 'Smart routing' },
|
smartRoute: { zh: '智能分流', en: 'Smart routing' },
|
||||||
smartRouteSub:{ zh: '海外应用加速,本地直连', en: 'Accelerate overseas apps, keep local direct' },
|
smartRouteSub:{ zh: '海外应用加速,本地直连', en: 'Accelerate overseas apps, keep local direct' },
|
||||||
killSwitch: { zh: 'Kill Switch', en: 'Kill Switch' },
|
killSwitch: { zh: 'Kill Switch', en: 'Kill Switch' },
|
||||||
killSwitchSub:{ zh: '断线时阻断网络,防止泄露', en: 'Block traffic if the VPN drops' },
|
killSwitchSub:{ zh: '断线时阻断网络,防止泄露', en: 'Block all traffic if connection drops' },
|
||||||
darkAppearance:{ zh: '深色外观', en: 'Dark appearance' },
|
darkAppearance:{ zh: '深色外观', en: 'Dark appearance' },
|
||||||
stateOn: { zh: '已开启', en: 'On' },
|
stateOn: { zh: '已开启', en: 'On' },
|
||||||
followLight: { zh: '跟随浅色', en: 'Off' },
|
followLight: { zh: '跟随浅色', en: 'Off' },
|
||||||
|
|||||||
Reference in New Issue
Block a user