feat: align with upstream 1.5.9.9

This commit is contained in:
2026-09-12 18:21:11 +08:00
commit 34f0679117
805 changed files with 376275 additions and 0 deletions
@@ -0,0 +1,458 @@
From 171854222838fa3c5f01bef96ffc9b820bc4bf00 Mon Sep 17 00:00:00 2001
From: HenryDu8133 <813367384@qq.com>
Date: Sun, 6 Sep 2026 08:23:39 +0800
Subject: [PATCH 01/58] =?UTF-8?q?chore(deploy):=20=E4=BF=AE=E5=A4=8D=20Doc?=
=?UTF-8?q?ker=20=E5=90=AF=E5=8A=A8=E9=93=BE=20+=20=E8=A1=A5=20compose=20+?=
=?UTF-8?q?=20Gitea=20Actions?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.gitea/workflows/deploy.yml | 164 ++++++++++++++++++++++++++++++++
docker-compose.yml | 182 ++++++++++++++++++++++++++++--------
scripts/deploy.js | 30 +++---
3 files changed, 323 insertions(+), 53 deletions(-)
create mode 100644 .gitea/workflows/deploy.yml
diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml
new file mode 100644
index 0000000..ca3da1f
--- /dev/null
+++ b/.gitea/workflows/deploy.yml
@@ -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 }}"
diff --git a/docker-compose.yml b/docker-compose.yml
index c1ed74d..f89810e 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,57 +1,159 @@
+name: BYW VoiceHub (1.7.0.0)
services:
+ # ========================================================================
+ # 应用本体 - Nuxt 4 (Node SSR)
+ # - 使用两阶段构建 Dockerfile,构建阶段内存友好
+ # - PREBUILT=true:告诉 scripts/deploy.js 跳过 npm install 和 nuxt build
+ # (因为第一阶段已经 build 好了,产物在 .output/ 里)
+ # ========================================================================
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:
- - '4456:3000'
- environment:
- - DATABASE_URL=postgresql://user:password@postgres:5432/voicehub
- - 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
+ # 左边是宿主机端口(对外),右边是容器内端口(固定 3000)
+ # 如果要套 Nginx,就只监听 127.0.0.1:4456:3000 不直接暴露外网
+ - "4456:3000"
depends_on:
postgres:
condition: service_healthy
- restart: unless-stopped
-
- postgres:
- image: postgres:15-alpine
+ redis:
+ condition: service_healthy
+ env_file:
+ # 把密码、密钥都写在项目根的 .env 里,不要直接写进这个 yaml
+ - .env
environment:
- - POSTGRES_USER=user
- - POSTGRES_PASSWORD=password
- - POSTGRES_DB=voicehub
- volumes:
- - postgres_data:/var/lib/postgresql/data
- ports:
- - '5433:5432'
- mem_limit: 1200m
- mem_reservation: 256m
- cpus: '1.0'
+ # ===== 运行时基础(可以被 .env 覆盖) =====
+ NODE_ENV: production
+ PORT: 3000
+ HOST: 0.0.0.0
+ PREBUILT: "true"
+ SKIP_INSTALL: "true"
+ SKIP_BUILD: "true"
+ NITRO_PRESET: node-server
+
+ # ===== 数据库连接串(.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:
resources:
limits:
- cpus: '1'
- memory: 1200M
+ cpus: ${APP_CPUS_LIMIT:-"2"}
+ memory: ${APP_MEM_LIMIT:-1400M}
reservations:
- cpus: '0.25'
- memory: 256M
+ cpus: ${APP_CPUS_RESERVATION:-"0.5"}
+ memory: ${APP_MEM_RESERVATION:-384M}
+ healthcheck:
+ # 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 user -d voicehub']
+ test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-voicehub} -d ${POSTGRES_DB:-voicehub}"]
interval: 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
+ 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:
- 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
diff --git a/scripts/deploy.js b/scripts/deploy.js
index c20219f..9963ca4 100644
--- a/scripts/deploy.js
+++ b/scripts/deploy.js
@@ -106,26 +106,26 @@ async function deploy() {
logStep('📦', '安装依赖...')
let installed = false
- // 优先尝试 pnpm install --frozen-lockfile
- if (fileExists('pnpm-lock.yaml')) {
- log('尝试使用 pnpm install --frozen-lockfile 安装...', 'cyan')
- if (safeExec('pnpm install --frozen-lockfile')) {
+ // 优先使用 npm(用户要求:禁止 pnpm)—— 优先 package-lock.json 冻结安装
+ if (fileExists('package-lock.json')) {
+ log('尝试使用 npm ci(基于 package-lock.json 冻结安装)...', 'cyan')
+ if (safeExec('npm ci --no-audit --no-fund')) {
installed = true
- logSuccess('依赖安装完成 (pnpm install --frozen-lockfile)')
+ logSuccess('依赖安装完成 (npm ci)')
} else {
- logWarning('pnpm install --frozen-lockfile 安装失败,准备回退到 pnpm install...')
+ logWarning('npm ci 失败,准备回退到 npm install...')
}
} else {
- logWarning('未检测到 pnpm-lock.yaml,跳过冻结锁文件安装,直接使用 pnpm install...')
+ logWarning('未检测到 package-lock.json,直接使用 npm install...')
}
- // 如果冻结锁文件安装没运行或失败,使用 pnpm install
+ // 兜底用 npm install
if (!installed) {
- log('正在使用 pnpm install 安装...', 'cyan')
- if (!safeExec('pnpm install')) {
+ log('正在使用 npm install 安装...', 'cyan')
+ if (!safeExec('npm install --no-audit --no-fund')) {
throw new Error('依赖安装失败')
}
- logSuccess('依赖安装完成 (pnpm install)')
+ logSuccess('依赖安装完成 (npm install)')
}
}
@@ -166,7 +166,10 @@ async function deploy() {
// 4. 创建管理员账户
if (fileExists('scripts/create-admin.js') && dbSyncSuccess) {
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. 构建应用
@@ -178,7 +181,8 @@ async function deploy() {
...process.env,
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('应用构建失败')
}
logSuccess('应用构建完成')
--
2.45.1.windows.1