From c92cd11cc535c72835be5a52a80adcfe3ce86b8d Mon Sep 17 00:00:00 2001 From: wangjia <809946525@qq.com> Date: Sat, 13 Jun 2026 01:19:28 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20CI=20=E6=B5=81=E6=B0=B4=E7=BA=BF=20?= =?UTF-8?q?=E2=80=94=20lint=20+=20=E5=8D=95=E6=B5=8B=20+=20OpenAPI=20?= =?UTF-8?q?=E6=A0=A1=E9=AA=8C=20+=20=E8=84=B1=E6=95=8F=E6=89=AB=E6=8F=8F?= =?UTF-8?q?=20+=20=E9=95=9C=E5=83=8F=E6=9E=84=E5=BB=BA=20[tsk=5FP5b5nIrEsf?= =?UTF-8?q?rV]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 .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 --- .gitea/workflows/ci.yml | 96 +++++ ci/nginx-test.sh | 46 +++ ci/scan-redline.sh | 84 ++++ design/server/openapi.yaml | 655 ++++++++++++++++++++++++++++++ design/ui_kits/desktop/dparts.jsx | 2 +- design/ui_kits/mobile/parts.jsx | 2 +- 6 files changed, 883 insertions(+), 2 deletions(-) create mode 100644 .gitea/workflows/ci.yml create mode 100644 ci/nginx-test.sh create mode 100644 ci/scan-redline.sh create mode 100644 design/server/openapi.yaml diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..53f08c6 --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -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 diff --git a/ci/nginx-test.sh b/ci/nginx-test.sh new file mode 100644 index 0000000..a579246 --- /dev/null +++ b/ci/nginx-test.sh @@ -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 配置校验通过。" diff --git a/ci/scan-redline.sh b/ci/scan-redline.sh new file mode 100644 index 0000000..05f44cc --- /dev/null +++ b/ci/scan-redline.sh @@ -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 "✅ 脱敏扫描通过 — 未发现红线词。" diff --git a/design/server/openapi.yaml b/design/server/openapi.yaml new file mode 100644 index 0000000..f1785c4 --- /dev/null +++ b/design/server/openapi.yaml @@ -0,0 +1,655 @@ +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] + + 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' diff --git a/design/ui_kits/desktop/dparts.jsx b/design/ui_kits/desktop/dparts.jsx index f0e1455..9dd45b8 100644 --- a/design/ui_kits/desktop/dparts.jsx +++ b/design/ui_kits/desktop/dparts.jsx @@ -72,7 +72,7 @@ const DSTRINGS = { weekTraffic:{zh:'本周流量 (GB)',en:'This week (GB)'}, 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'}, - 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'}, protocol:{zh:'协议',en:'Protocol'}, version:{zh:'版本',en:'Version'}, days:{zh:['周一','周二','周三','周四','周五','周六','周日'],en:['Mon','Tue','Wed','Thu','Fri','Sat','Sun']}, diff --git a/design/ui_kits/mobile/parts.jsx b/design/ui_kits/mobile/parts.jsx index 3114eff..a8fb961 100644 --- a/design/ui_kits/mobile/parts.jsx +++ b/design/ui_kits/mobile/parts.jsx @@ -150,7 +150,7 @@ const STRINGS = { smartRoute: { zh: '智能分流', en: 'Smart routing' }, smartRouteSub:{ zh: '海外应用加速,本地直连', en: 'Accelerate overseas apps, keep local direct' }, 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' }, stateOn: { zh: '已开启', en: 'On' }, followLight: { zh: '跟随浅色', en: 'Off' },