chore(deploy): 修复 Docker 启动链 + 补 compose + Gitea Actions
This commit is contained in:
@@ -0,0 +1,164 @@
|
|||||||
|
name: 构建并部署 VoiceHub
|
||||||
|
run-name: ${{ gitea.actor }} 触发部署 #${{{{ gitea.run_number }}}}
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- master
|
||||||
|
# 允许在 Gitea 页面手动点「运行工作流」
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
deploy_target:
|
||||||
|
description: 是否在构建后自动部署到服务器
|
||||||
|
type: boolean
|
||||||
|
default: true
|
||||||
|
required: false
|
||||||
|
|
||||||
|
# 同一时刻只跑一个部署,避免部署竞态
|
||||||
|
concurrency:
|
||||||
|
group: deploy-production
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
env:
|
||||||
|
# ====== 按需修改(改成你的 Gitea 域名,不要 http:// 前缀) ======
|
||||||
|
GITEA_REGISTRY: gitea.example.com
|
||||||
|
# ====== 一般不改 ======
|
||||||
|
IMAGE_NAME: byw-voicehub
|
||||||
|
DEPLOY_PATH: /opt/byw-voicehub
|
||||||
|
NODE_VERSION: "22"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# ============================================================
|
||||||
|
# Job 1: 构建 & 推送 Docker 镜像到 Gitea 自带容器仓库
|
||||||
|
# ============================================================
|
||||||
|
build-image:
|
||||||
|
name: 构建 Docker 镜像
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
image_tag: ${{ steps.meta.outputs.tag }}
|
||||||
|
image_full: ${{ steps.meta.outputs.full }}
|
||||||
|
steps:
|
||||||
|
- name: 📥 拉取代码
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 1
|
||||||
|
|
||||||
|
- name: 🔧 设置 Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
|
registry-url: https://registry.npmmirror.com # 国内加速,不需要可删
|
||||||
|
|
||||||
|
- name: 🧪 环境校验
|
||||||
|
run: |
|
||||||
|
set -euxo pipefail
|
||||||
|
node -v
|
||||||
|
npm -v
|
||||||
|
npm ci --no-audit --no-fund
|
||||||
|
|
||||||
|
- name: 🔐 登录 Gitea 容器仓库
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.GITEA_REGISTRY }}
|
||||||
|
username: ${{ gitea.repository_owner }}
|
||||||
|
password: ${{ secrets.REGISTRY_TOKEN || gitea.token }}
|
||||||
|
|
||||||
|
- name: 🏷️ 生成镜像标签
|
||||||
|
id: meta
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
SHORT_SHA="${GITEA_SHA:0:7}"
|
||||||
|
DATE_TAG="$(date +%Y%m%d)-${SHORT_SHA}"
|
||||||
|
FULL="${GITEA_REGISTRY}/${GITEA_REPO_OWNER}/${IMAGE_NAME}:${DATE_TAG}"
|
||||||
|
LATEST="${GITEA_REGISTRY}/${GITEA_REPO_OWNER}/${IMAGE_NAME}:latest"
|
||||||
|
echo "tag=${DATE_TAG}" >> "$GITEA_OUTPUT"
|
||||||
|
echo "full=${FULL}" >> "$GITEA_OUTPUT"
|
||||||
|
echo "latest=${LATEST}" >> "$GITEA_OUTPUT"
|
||||||
|
echo "✅ 镜像 tag: ${DATE_TAG}"
|
||||||
|
echo "✅ 镜像完整: ${FULL}"
|
||||||
|
|
||||||
|
- name: 🐳 构建并推送镜像(BuildKit 多阶段 + 缓存)
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./Dockerfile
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
${{ steps.meta.outputs.full }}
|
||||||
|
${{ steps.meta.outputs.latest }}
|
||||||
|
cache-from: type=registry,ref=${{ env.GITEA_REGISTRY }}/${{ gitea.repository_owner }}/${{ env.IMAGE_NAME }}:buildcache
|
||||||
|
cache-to: type=registry,ref=${{ env.GITEA_REGISTRY }}/${{ gitea.repository_owner }}/${{ env.IMAGE_NAME }}:buildcache,mode=max
|
||||||
|
build-args: |
|
||||||
|
BUILDKIT_INLINE_CACHE=1
|
||||||
|
labels: |
|
||||||
|
org.opencontainers.image.source=${{ gitea.server_url }}/${{ gitea.repository }}
|
||||||
|
org.opencontainers.image.revision=${{ gitea.sha }}
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Job 2: SSH 到服务器拉取新镜像并重启
|
||||||
|
# ============================================================
|
||||||
|
deploy:
|
||||||
|
name: 部署到服务器
|
||||||
|
needs: build-image
|
||||||
|
if: ${{ !cancelled() && !failure() && (inputs.deploy_target == true || github.event_name == 'push') }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
environment: production
|
||||||
|
steps:
|
||||||
|
- name: 🌐 SSH 部署到生产服务器
|
||||||
|
uses: https://github.com/appleboy/ssh-action@v1.2.0
|
||||||
|
with:
|
||||||
|
host: ${{ secrets.DEPLOY_HOST }}
|
||||||
|
username: ${{ secrets.DEPLOY_USER }}
|
||||||
|
key: ${{ secrets.DEPLOY_SSH_KEY }}
|
||||||
|
port: ${{ secrets.DEPLOY_PORT || 22 }}
|
||||||
|
script_stop: true
|
||||||
|
command_timeout: 30m
|
||||||
|
script: |
|
||||||
|
set -euxo pipefail
|
||||||
|
|
||||||
|
# 1) 首次部署时自动创建目录 & 空的 docker-compose.yml/.env
|
||||||
|
mkdir -p "${DEPLOY_PATH}"
|
||||||
|
cd "${DEPLOY_PATH}"
|
||||||
|
|
||||||
|
# 2) 登录 Gitea 镜像仓库(用一个仅 pull 权限的 token 更安全)
|
||||||
|
echo "${{ secrets.REGISTRY_PULL_TOKEN || gitea.token }}" \
|
||||||
|
| docker login "${GITEA_REGISTRY}" -u "${{ secrets.REGISTRY_PULL_USER || gitea.repository_owner }}" --password-stdin 2>/dev/null || true
|
||||||
|
|
||||||
|
# 3) 拉最新镜像
|
||||||
|
IMAGE_LATEST="${GITEA_REGISTRY}/${GITEA_REPO_OWNER}/${IMAGE_NAME}:latest"
|
||||||
|
docker pull "$IMAGE_LATEST"
|
||||||
|
|
||||||
|
# 4) 写一个最小的 docker-compose.override.yml,指向刚拉的镜像
|
||||||
|
# (避免改动服务器上已配置好的 docker-compose.yml 和 .env)
|
||||||
|
cat > docker-compose.override.yml <<'YAML'
|
||||||
|
services:
|
||||||
|
voicehub:
|
||||||
|
image: __REPLACE_WITH_IMAGE__
|
||||||
|
YAML
|
||||||
|
sed -i "s|__REPLACE_WITH_IMAGE__|${IMAGE_LATEST}|g" docker-compose.override.yml
|
||||||
|
|
||||||
|
# 5) 启动(如果没有 docker-compose.yml 就自动从你的仓库默认模板里下载一份)
|
||||||
|
if [ ! -f docker-compose.yml ]; then
|
||||||
|
echo "ℹ️ 首次部署:写入默认 docker-compose.yml 模板(请稍后在服务器上编辑 .env)"
|
||||||
|
curl -fsSL "${{ gitea.server_url }}/${{ gitea.repository }}/raw/branch/main/docker-compose.yml" \
|
||||||
|
-o docker-compose.yml || echo "下载失败,跳过(你需要手动上传 docker-compose.yml)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 6) 健康拉取 + 滚动重启(先拉好再 up,downtime 最小)
|
||||||
|
docker compose pull --quiet voicehub 2>/dev/null || true
|
||||||
|
docker compose up -d --remove-orphans
|
||||||
|
|
||||||
|
# 7) 等健康检查通过,最多等 120s
|
||||||
|
for i in $(seq 1 24); do
|
||||||
|
if docker compose ps --format json 2>/dev/null | grep -q '"Health":"healthy"'; then
|
||||||
|
echo "✅ 容器健康"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 5
|
||||||
|
done
|
||||||
|
|
||||||
|
# 8) 清理旧镜像(只留最近 2 个 tag,省磁盘)
|
||||||
|
docker image prune -af --filter "until=72h" || true
|
||||||
|
|
||||||
|
echo "🎉 部署完成!镜像=${{ needs.build-image.outputs.image_full }}"
|
||||||
+142
-40
@@ -1,57 +1,159 @@
|
|||||||
|
name: BYW VoiceHub (1.7.0.0)
|
||||||
services:
|
services:
|
||||||
|
# ========================================================================
|
||||||
|
# 应用本体 - Nuxt 4 (Node SSR)
|
||||||
|
# - 使用两阶段构建 Dockerfile,构建阶段内存友好
|
||||||
|
# - PREBUILT=true:告诉 scripts/deploy.js 跳过 npm install 和 nuxt build
|
||||||
|
# (因为第一阶段已经 build 好了,产物在 .output/ 里)
|
||||||
|
# ========================================================================
|
||||||
voicehub:
|
voicehub:
|
||||||
build: .
|
image: byw-voicehub:1.7.0.0
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
args:
|
||||||
|
BUILDKIT_INLINE_CACHE: "1"
|
||||||
|
container_name: voicehub-app
|
||||||
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- '4456:3000'
|
# 左边是宿主机端口(对外),右边是容器内端口(固定 3000)
|
||||||
environment:
|
# 如果要套 Nginx,就只监听 127.0.0.1:4456:3000 不直接暴露外网
|
||||||
- DATABASE_URL=postgresql://user:password@postgres:5432/voicehub
|
- "4456:3000"
|
||||||
- JWT_SECRET=your-jwt-secret-here
|
|
||||||
- NODE_ENV=production
|
|
||||||
- NODE_OPTIONS=--max-old-space-size=512
|
|
||||||
- SKIP_BUILD=true
|
|
||||||
mem_limit: 1200m
|
|
||||||
mem_reservation: 256m
|
|
||||||
cpus: '1.0'
|
|
||||||
deploy:
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
cpus: '1'
|
|
||||||
memory: 1200M
|
|
||||||
reservations:
|
|
||||||
cpus: '0.25'
|
|
||||||
memory: 256M
|
|
||||||
depends_on:
|
depends_on:
|
||||||
postgres:
|
postgres:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
restart: unless-stopped
|
redis:
|
||||||
|
condition: service_healthy
|
||||||
postgres:
|
env_file:
|
||||||
image: postgres:15-alpine
|
# 把密码、密钥都写在项目根的 .env 里,不要直接写进这个 yaml
|
||||||
|
- .env
|
||||||
environment:
|
environment:
|
||||||
- POSTGRES_USER=user
|
# ===== 运行时基础(可以被 .env 覆盖) =====
|
||||||
- POSTGRES_PASSWORD=password
|
NODE_ENV: production
|
||||||
- POSTGRES_DB=voicehub
|
PORT: 3000
|
||||||
volumes:
|
HOST: 0.0.0.0
|
||||||
- postgres_data:/var/lib/postgresql/data
|
PREBUILT: "true"
|
||||||
ports:
|
SKIP_INSTALL: "true"
|
||||||
- '5433:5432'
|
SKIP_BUILD: "true"
|
||||||
mem_limit: 1200m
|
NITRO_PRESET: node-server
|
||||||
mem_reservation: 256m
|
|
||||||
cpus: '1.0'
|
# ===== 数据库连接串(.env 写了就用 .env,没写就自动拼 compose 内部 postgres 服务) =====
|
||||||
|
DATABASE_URL: ${DATABASE_URL:-postgresql://${POSTGRES_USER:-voicehub}:${POSTGRES_PASSWORD:-voicehub}@postgres:5432/${POSTGRES_DB:-voicehub}?sslmode=disable}
|
||||||
|
|
||||||
|
# ===== Redis 连接串(同上,自动拼内部 redis 服务) =====
|
||||||
|
REDIS_URL: ${REDIS_URL:-redis://${REDIS_PASSWORD:+:${REDIS_PASSWORD}@}redis:6379/0}
|
||||||
|
|
||||||
|
# ===== 会话/Token(必须在 .env 里设,这里给一个占位提醒) =====
|
||||||
|
JWT_SECRET: ${JWT_SECRET:-请在.env里设置JWT_SECRET用openssl rand -hex 32生成}
|
||||||
|
|
||||||
|
# ===== 应用默认(可在 .env 覆盖,不会覆盖 .env 里同名字段) =====
|
||||||
|
NUXT_PUBLIC_SITE_URL: ${NUXT_PUBLIC_SITE_URL:-http://127.0.0.1:4456}
|
||||||
|
NUXT_PUBLIC_SITE_TITLE: ${NUXT_PUBLIC_SITE_TITLE:-校园广播站点歌系统}
|
||||||
|
NUXT_PUBLIC_DEFAULT_THEME: ${NUXT_PUBLIC_DEFAULT_THEME:-dark}
|
||||||
|
NUXT_PUBLIC_ENABLED_THEMES: ${NUXT_PUBLIC_ENABLED_THEMES:-dark,light}
|
||||||
|
|
||||||
|
# ===== 日志/性能 =====
|
||||||
|
NODE_OPTIONS: ${NODE_OPTIONS:-"--max-old-space-size=768"}
|
||||||
|
# 2C4G 机器的推荐配置(.env 可覆盖);小机器 1C2G 也能跑
|
||||||
|
mem_limit: ${APP_MEM_LIMIT:-1400m}
|
||||||
|
mem_reservation: ${APP_MEM_RESERVATION:-384m}
|
||||||
|
cpus: ${APP_CPUS:-"1.5"}
|
||||||
deploy:
|
deploy:
|
||||||
resources:
|
resources:
|
||||||
limits:
|
limits:
|
||||||
cpus: '1'
|
cpus: ${APP_CPUS_LIMIT:-"2"}
|
||||||
memory: 1200M
|
memory: ${APP_MEM_LIMIT:-1400M}
|
||||||
reservations:
|
reservations:
|
||||||
cpus: '0.25'
|
cpus: ${APP_CPUS_RESERVATION:-"0.5"}
|
||||||
memory: 256M
|
memory: ${APP_MEM_RESERVATION:-384M}
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ['CMD-SHELL', 'pg_isready -U user -d voicehub']
|
# 30s 后开始检查,每 15s 一次,连续 3 次失败就重启
|
||||||
|
test:
|
||||||
|
[
|
||||||
|
"CMD",
|
||||||
|
"node",
|
||||||
|
"-e",
|
||||||
|
"fetch('http://127.0.0.1:3000/').then(r=>{process.exit(r.ok?0:1)}).catch(()=>process.exit(1))"
|
||||||
|
]
|
||||||
|
interval: 15s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
start_period: 30s
|
||||||
|
networks:
|
||||||
|
- voicehub-net
|
||||||
|
volumes:
|
||||||
|
# (可选)把自定义 logo/上传资源挂到宿主机
|
||||||
|
- voicehub-uploads:/app/app/public/uploads:rw
|
||||||
|
|
||||||
|
# ========================================================================
|
||||||
|
# PostgreSQL 16 - 主数据库
|
||||||
|
# ========================================================================
|
||||||
|
postgres:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
container_name: voicehub-postgres
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: ${POSTGRES_USER:-voicehub}
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-请在.env里改这个密码}
|
||||||
|
POSTGRES_DB: ${POSTGRES_DB:-voicehub}
|
||||||
|
PGDATA: /var/lib/postgresql/data/pgdata
|
||||||
|
# .env 里没设 DATABASE_URL 时,自动拼一份给 app 用
|
||||||
|
# (如果 .env 里已经写了外部 Postgres,就以 .env 为准)
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-voicehub} -d ${POSTGRES_DB:-voicehub}"]
|
||||||
interval: 5s
|
interval: 5s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 10
|
||||||
|
volumes:
|
||||||
|
- postgres-data:/var/lib/postgresql/data
|
||||||
|
# 如需从宿主机用 Navicat 连,取消下一行注释
|
||||||
|
# ports:
|
||||||
|
# - "127.0.0.1:5433:5432"
|
||||||
|
mem_limit: ${PG_MEM_LIMIT:-1200m}
|
||||||
|
mem_reservation: ${PG_MEM_RESERVATION:-256m}
|
||||||
|
cpus: ${PG_CPUS:-"1.0"}
|
||||||
|
networks:
|
||||||
|
- voicehub-net
|
||||||
|
|
||||||
|
# ========================================================================
|
||||||
|
# Redis 7 - 缓存 / 会话 / 限流 / 队列
|
||||||
|
# ========================================================================
|
||||||
|
redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
container_name: voicehub-redis
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
command: >
|
||||||
|
sh -c "
|
||||||
|
if [ -n \"$$REDIS_PASSWORD\" ]; then
|
||||||
|
redis-server --appendonly yes --requirepass \"$$REDIS_PASSWORD\"
|
||||||
|
else
|
||||||
|
redis-server --appendonly yes
|
||||||
|
fi
|
||||||
|
"
|
||||||
|
environment:
|
||||||
|
REDIS_PASSWORD: ${REDIS_PASSWORD:-}
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "redis-cli", "ping"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 10
|
||||||
|
volumes:
|
||||||
|
- redis-data:/data
|
||||||
|
mem_limit: ${REDIS_MEM_LIMIT:-384m}
|
||||||
|
mem_reservation: ${REDIS_MEM_RESERVATION:-64m}
|
||||||
|
cpus: ${REDIS_CPUS:-"0.5"}
|
||||||
|
networks:
|
||||||
|
- voicehub-net
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
postgres_data:
|
postgres-data:
|
||||||
|
name: voicehub-postgres-data
|
||||||
|
redis-data:
|
||||||
|
name: voicehub-redis-data
|
||||||
|
voicehub-uploads:
|
||||||
|
name: voicehub-uploads
|
||||||
|
|
||||||
|
networks:
|
||||||
|
voicehub-net:
|
||||||
|
name: voicehub-net
|
||||||
|
driver: bridge
|
||||||
|
|||||||
+17
-13
@@ -106,26 +106,26 @@ async function deploy() {
|
|||||||
logStep('📦', '安装依赖...')
|
logStep('📦', '安装依赖...')
|
||||||
let installed = false
|
let installed = false
|
||||||
|
|
||||||
// 优先尝试 pnpm install --frozen-lockfile
|
// 优先使用 npm(用户要求:禁止 pnpm)—— 优先 package-lock.json 冻结安装
|
||||||
if (fileExists('pnpm-lock.yaml')) {
|
if (fileExists('package-lock.json')) {
|
||||||
log('尝试使用 pnpm install --frozen-lockfile 安装...', 'cyan')
|
log('尝试使用 npm ci(基于 package-lock.json 冻结安装)...', 'cyan')
|
||||||
if (safeExec('pnpm install --frozen-lockfile')) {
|
if (safeExec('npm ci --no-audit --no-fund')) {
|
||||||
installed = true
|
installed = true
|
||||||
logSuccess('依赖安装完成 (pnpm install --frozen-lockfile)')
|
logSuccess('依赖安装完成 (npm ci)')
|
||||||
} else {
|
} else {
|
||||||
logWarning('pnpm install --frozen-lockfile 安装失败,准备回退到 pnpm install...')
|
logWarning('npm ci 失败,准备回退到 npm install...')
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
logWarning('未检测到 pnpm-lock.yaml,跳过冻结锁文件安装,直接使用 pnpm install...')
|
logWarning('未检测到 package-lock.json,直接使用 npm install...')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果冻结锁文件安装没运行或失败,使用 pnpm install
|
// 兜底用 npm install
|
||||||
if (!installed) {
|
if (!installed) {
|
||||||
log('正在使用 pnpm install 安装...', 'cyan')
|
log('正在使用 npm install 安装...', 'cyan')
|
||||||
if (!safeExec('pnpm install')) {
|
if (!safeExec('npm install --no-audit --no-fund')) {
|
||||||
throw new Error('依赖安装失败')
|
throw new Error('依赖安装失败')
|
||||||
}
|
}
|
||||||
logSuccess('依赖安装完成 (pnpm install)')
|
logSuccess('依赖安装完成 (npm install)')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,7 +166,10 @@ async function deploy() {
|
|||||||
// 4. 创建管理员账户
|
// 4. 创建管理员账户
|
||||||
if (fileExists('scripts/create-admin.js') && dbSyncSuccess) {
|
if (fileExists('scripts/create-admin.js') && dbSyncSuccess) {
|
||||||
logStep('👤', '检查管理员账户...')
|
logStep('👤', '检查管理员账户...')
|
||||||
safeExec('pnpm run create-admin')
|
// 优先 npm run create-admin,兜底直接用 node_modules/.bin/tsx 调用(用户禁用 pnpm)
|
||||||
|
if (!safeExec('npm run create-admin')) {
|
||||||
|
safeExec('./node_modules/.bin/tsx scripts/create-admin.js')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. 构建应用
|
// 5. 构建应用
|
||||||
@@ -178,7 +181,8 @@ async function deploy() {
|
|||||||
...process.env,
|
...process.env,
|
||||||
NODE_OPTIONS: process.env.NODE_OPTIONS || DEFAULT_NODE_OPTIONS
|
NODE_OPTIONS: process.env.NODE_OPTIONS || DEFAULT_NODE_OPTIONS
|
||||||
}
|
}
|
||||||
if (!safeExec('pnpm exec nuxt build', { env: buildEnv })) {
|
// 用 npm run build(= nuxt build),避免用 pnpm exec
|
||||||
|
if (!safeExec('npm run build', { env: buildEnv })) {
|
||||||
throw new Error('应用构建失败')
|
throw new Error('应用构建失败')
|
||||||
}
|
}
|
||||||
logSuccess('应用构建完成')
|
logSuccess('应用构建完成')
|
||||||
|
|||||||
Reference in New Issue
Block a user