From 6e012584acef46af14b68e0642238d0eeaa02ecd Mon Sep 17 00:00:00 2001 From: ailuckly Date: Tue, 31 Mar 2026 21:33:33 +0800 Subject: [PATCH 1/2] chore: streamline startup and staging deployment --- .env.example | 71 +++ .github/workflows/cd-production.yml | 8 +- .github/workflows/cd-staging.yml | 588 +++--------------- .github/workflows/emergency-rollback.yml | 8 +- .github/workflows/release.yml | 6 +- .gitignore | 3 +- docker-compose.prod.yml | 11 +- docker-compose.test.yml | 30 +- docker-compose.yml | 106 +++- ...00\345\217\221\347\216\257\345\242\203.md" | 57 ++ docs/GitHub-Staging-Secrets.md | 68 ++ ...50\347\275\262\346\226\207\346\241\243.md" | 3 + vocata-admin/.dockerignore | 16 + vocata-admin/Dockerfile | 40 +- vocata-server/.dockerignore | 4 +- vocata-server/Dockerfile | 56 +- vocata-server/Dockerfile.ci | 37 +- .../com/vocata/file/config/QiniuConfig.java | 15 +- .../vocata/file/config/QiniuProperties.java | 12 +- .../file/service/impl/FileServiceImpl.java | 22 +- vocata-web/.dockerignore | 16 + vocata-web/Dockerfile | 34 +- 22 files changed, 521 insertions(+), 690 deletions(-) create mode 100644 .env.example create mode 100644 "docs/Docker\345\274\200\345\217\221\347\216\257\345\242\203.md" create mode 100644 docs/GitHub-Staging-Secrets.md create mode 100644 vocata-admin/.dockerignore create mode 100644 vocata-web/.dockerignore diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..7b67f7f --- /dev/null +++ b/.env.example @@ -0,0 +1,71 @@ +# Docker Compose development defaults +COMPOSE_PROJECT_NAME=vocata-dev + +# Core service ports +POSTGRES_PORT=5432 +REDIS_EXPOSE_PORT=6379 +SERVER_PORT=9009 +WEB_PORT=3000 +ADMIN_PORT=3001 +PGADMIN_PORT=5050 +MAILHOG_SMTP_PORT=1025 +MAILHOG_WEB_PORT=8025 + +# Backend profile +SPRING_PROFILES_ACTIVE=local + +# Database +POSTGRES_DB=vocata_db +POSTGRES_USER=postgres +POSTGRES_PASSWORD=postgres123 +DB_HOST=postgres +DB_PORT=5432 +DB_NAME=vocata_db +DB_USERNAME=postgres +DB_PASSWORD=postgres123 + +# Redis +REDIS_HOST=redis +REDIS_PORT=6379 +REDIS_PASSWORD= +REDIS_DATABASE=0 + +# Frontend build-time API target +VITE_APP_URL=http://localhost:9009 +WEB_BUILD_MODE=production +ADMIN_BUILD_MODE=production + +# Optional local mail +MAIL_USERNAME= +MAIL_PASSWORD= + +# Xunfei TTS (used when AI_TTS_PROVIDER=xunfei) +XUNFEI_TTS_APP_ID= +XUNFEI_TTS_API_KEY= +XUNFEI_TTS_SECRET_KEY= + +# Third-party object storage / AI +# Current backend startup still expects these entries to exist for a complete run. +QINIU_ACCESS_KEY= +QINIU_SECRET_KEY= +QINIU_BUCKET=voice-mp3 +QINIU_DOMAIN=http://voice-mp3.s3.cn-north-1.qiniucs.com +QINIU_REGION=huabei +QINIU_KEY_PREFIX=Vocata +QINIU_AI_API_KEY= +QINIU_AI_BASE_URL=https://openai.qiniu.com/v1 +QINIU_AI_MODEL=x-ai/grok-4-fast +QINIU_AI_TIMEOUT=60 + +GEMINI_API_KEY= +GEMINI_MODEL=gemini-2.5-flash-lite + +OPENAI_API_KEY= +OPENAI_MODEL=gpt-3.5-turbo + +SILICONFLOW_API_KEY= +SILICONFLOW_AI_MODEL=Qwen/Qwen3-8B + +# Optional admin tools +PGADMIN_DEFAULT_EMAIL=admin@vocata.com +PGADMIN_DEFAULT_PASSWORD=admin123 diff --git a/.github/workflows/cd-production.yml b/.github/workflows/cd-production.yml index 73df012..6dc4880 100644 --- a/.github/workflows/cd-production.yml +++ b/.github/workflows/cd-production.yml @@ -466,7 +466,7 @@ jobs: case $service in "vocata-server") for i in {1..6}; do - if curl -f -m 10 "http://localhost:9009/api/actuator/health" > /dev/null 2>&1; then + if curl -f -m 10 "http://localhost:9009/api/health" > /dev/null 2>&1; then echo "✓ 新的后端服务健康检查通过" break fi @@ -504,7 +504,7 @@ jobs: for service in "${SERVICES_TO_UPDATE[@]}"; do case $service in "vocata-server") - if ! curl -f -m 30 "http://localhost:9009/api/actuator/health" > /dev/null 2>&1; then + if ! curl -f -m 30 "http://localhost:9009/api/health" > /dev/null 2>&1; then echo "✗ 后端服务最终健康检查失败" HEALTH_FAILED=true else @@ -649,7 +649,7 @@ jobs: # API健康检查 (假设使用域名) echo "验证后端API..." - if curl -f -m 60 "https://api.vocata.com/api/actuator/health" > /dev/null 2>&1; then + if curl -f -m 60 "https://api.vocata.com/api/health" > /dev/null 2>&1; then echo "✅ 后端API验证通过" >> $GITHUB_STEP_SUMMARY else echo "❌ 后端API验证失败" >> $GITHUB_STEP_SUMMARY @@ -683,4 +683,4 @@ jobs: echo "⚠️ **生产环境验证部分失败**" >> $GITHUB_STEP_SUMMARY echo "请立即检查相关服务状态。" >> $GITHUB_STEP_SUMMARY exit 1 - fi \ No newline at end of file + fi diff --git a/.github/workflows/cd-staging.yml b/.github/workflows/cd-staging.yml index 0042655..2cbcc3e 100644 --- a/.github/workflows/cd-staging.yml +++ b/.github/workflows/cd-staging.yml @@ -2,547 +2,125 @@ name: Deploy to Staging on: push: - branches: [ develop ] + branches: [develop] workflow_dispatch: - inputs: - force_rebuild: - description: '强制重建所有镜像' - required: false - default: false - type: boolean - skip_health_check: - description: '跳过健康检查(快速部署)' - required: false - default: false - type: boolean permissions: contents: read - packages: write - actions: read -env: - REGISTRY: ghcr.io - JAVA_VERSION: '17' - NODE_VERSION: '20' +concurrency: + group: staging-deploy + cancel-in-progress: true jobs: - # 检测变更并决定构建策略 - detect-changes: - runs-on: ubuntu-latest - outputs: - server-changed: ${{ steps.changes.outputs.server }} - web-changed: ${{ steps.changes.outputs.web }} - admin-changed: ${{ steps.changes.outputs.admin }} - image-tag: ${{ steps.tag.outputs.image-tag }} - steps: - - name: 检出代码 - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: 检测文件变更 - uses: dorny/paths-filter@v2 - id: changes - with: - filters: | - server: - - 'vocata-server/**' - - 'pom.xml' - - '.github/workflows/**' - web: - - 'vocata-web/**' - - '.github/workflows/**' - admin: - - 'vocata-admin/**' - - '.github/workflows/**' - - - name: 生成镜像标签 - id: tag - run: | - IMAGE_TAG="staging-$(date +'%Y%m%d-%H%M%S')-${GITHUB_SHA:0:8}" - echo "image-tag=$IMAGE_TAG" >> $GITHUB_OUTPUT - echo "生成镜像标签: $IMAGE_TAG" - - - name: 强制重建检查 - run: | - if [[ "${{ github.event.inputs.force_rebuild }}" == "true" ]]; then - echo "server-changed=true" >> $GITHUB_OUTPUT - echo "web-changed=true" >> $GITHUB_OUTPUT - echo "admin-changed=true" >> $GITHUB_OUTPUT - echo "强制重建所有组件" - fi - - # 构建后端镜像 - build-server: - runs-on: ubuntu-latest - needs: detect-changes - if: needs.detect-changes.outputs.server-changed == 'true' || github.event.inputs.force_rebuild == 'true' - outputs: - image: ${{ steps.meta.outputs.tags }} - steps: - - name: 检出代码 - uses: actions/checkout@v4 - - - name: 设置 JDK ${{ env.JAVA_VERSION }} - uses: actions/setup-java@v4 - with: - java-version: ${{ env.JAVA_VERSION }} - distribution: 'temurin' - - - name: 缓存 Maven 依赖 - uses: actions/cache@v4 - with: - path: ~/.m2 - key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} - - - name: 构建后端应用 - working-directory: ./vocata-server - run: mvn clean package -DskipTests -Dspring.profiles.active=test - - - name: 设置 Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: 登录 GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: 提取镜像元数据 - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.REGISTRY }}/veardk/vocata-server - tags: | - type=raw,value=${{ needs.detect-changes.outputs.image-tag }} - type=raw,value=staging-latest - - - name: 构建并推送镜像 - uses: docker/build-push-action@v5 - with: - context: . - file: ./vocata-server/Dockerfile.ci - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha,scope=vocata-server-staging - cache-to: type=gha,mode=max,scope=vocata-server-staging - - # 构建前端客户端镜像 - build-web: - runs-on: ubuntu-latest - needs: detect-changes - if: needs.detect-changes.outputs.web-changed == 'true' || github.event.inputs.force_rebuild == 'true' - outputs: - image: ${{ steps.meta.outputs.tags }} - steps: - - name: 检出代码 - uses: actions/checkout@v4 - - - name: 设置 Node.js ${{ env.NODE_VERSION }} - uses: actions/setup-node@v4 - with: - node-version: ${{ env.NODE_VERSION }} - cache: 'npm' - cache-dependency-path: vocata-web/package-lock.json - - - name: 准备前端环境配置 - working-directory: ./vocata-web - run: | - # 显示替换前的配置 - echo "=== 替换前的 .env.test 文件 ===" - cat .env.test - - # 替换测试环境配置中的占位符 - sed -i "s/{{STAGING_HOST}}/${{ secrets.STAGING_HOST }}/g" .env.test - - # 显示替换后的配置 - echo "=== 替换后的 .env.test 文件 ===" - cat .env.test - echo "前端环境配置已更新" - - - name: 构建前端应用 - working-directory: ./vocata-web - run: | - npm ci - npm run build:test - - - name: 设置 Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: 登录 GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: 提取镜像元数据 - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.REGISTRY }}/veardk/vocata-web - tags: | - type=raw,value=${{ needs.detect-changes.outputs.image-tag }} - type=raw,value=staging-latest - - - name: 构建并推送镜像 - uses: docker/build-push-action@v5 - with: - context: ./vocata-web - file: ./vocata-web/Dockerfile - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha,scope=vocata-web-staging - cache-to: type=gha,mode=max,scope=vocata-web-staging - - # 构建管理后台镜像 - build-admin: - runs-on: ubuntu-latest - needs: detect-changes - if: needs.detect-changes.outputs.admin-changed == 'true' || github.event.inputs.force_rebuild == 'true' - outputs: - image: ${{ steps.meta.outputs.tags }} - steps: - - name: 检出代码 - uses: actions/checkout@v4 - - - name: 设置 Node.js ${{ env.NODE_VERSION }} - uses: actions/setup-node@v4 - with: - node-version: ${{ env.NODE_VERSION }} - cache: 'npm' - cache-dependency-path: vocata-admin/package-lock.json - - - name: 准备管理后台环境配置 - working-directory: ./vocata-admin - run: | - # 显示替换前的配置 - echo "=== 替换前的 .env.test 文件 ===" - cat .env.test - - # 替换测试环境配置中的占位符 - sed -i "s/{{STAGING_HOST}}/${{ secrets.STAGING_HOST }}/g" .env.test - - # 显示替换后的配置 - echo "=== 替换后的 .env.test 文件 ===" - cat .env.test - echo "管理后台环境配置已更新" - - - name: 构建管理后台应用 - working-directory: ./vocata-admin - run: | - npm ci - npm run build:test - - - name: 设置 Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: 登录 GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: 提取镜像元数据 - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.REGISTRY }}/veardk/vocata-admin - tags: | - type=raw,value=${{ needs.detect-changes.outputs.image-tag }} - type=raw,value=staging-latest - - - name: 构建并推送镜像 - uses: docker/build-push-action@v5 - with: - context: ./vocata-admin - file: ./vocata-admin/Dockerfile - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha,scope=vocata-admin-staging - cache-to: type=gha,mode=max,scope=vocata-admin-staging - - # 部署到测试服务器 deploy-staging: runs-on: ubuntu-latest - needs: [detect-changes, build-server, build-web, build-admin] - if: always() && (needs.build-server.result == 'success' || needs.build-server.result == 'skipped') && - (needs.build-web.result == 'success' || needs.build-web.result == 'skipped') && - (needs.build-admin.result == 'success' || needs.build-admin.result == 'skipped') environment: name: staging steps: - - name: 检出代码 - uses: actions/checkout@v4 - - - name: 准备部署配置 - run: | - mkdir -p deploy/staging - - # 生成 .env 文件 - cat > deploy/staging/.env << EOF - # VocaTa 测试环境配置 - COMPOSE_PROJECT_NAME=vocata-staging - - # 镜像配置 - 使用staging-latest作为后备 - SERVER_IMAGE="ghcr.io/veardk/vocata-server:staging-latest" - WEB_IMAGE="ghcr.io/veardk/vocata-web:staging-latest" - ADMIN_IMAGE="ghcr.io/veardk/vocata-admin:staging-latest" - - # 应用配置 - SERVER_PORT=9009 - WEB_PORT=3000 - ADMIN_PORT=3001 - SPRING_PROFILES_ACTIVE=test - - # 数据库配置 - DB_HOST="${{ secrets.DB_HOST }}" - DB_PORT="${{ secrets.DB_PORT }}" - DB_NAME="${{ secrets.DB_NAME }}" - DB_USERNAME="${{ secrets.DB_USERNAME }}" - DB_PASSWORD="${{ secrets.DB_PASSWORD }}" - - # Redis配置 - REDIS_HOST="${{ secrets.REDIS_HOST }}" - REDIS_PORT="${{ secrets.REDIS_PORT }}" - REDIS_PASSWORD="${{ secrets.REDIS_PASSWORD }}" - REDIS_DATABASE="${{ secrets.REDIS_DATABASE }}" - - # 七牛云配置 - QINIU_ACCESS_KEY="${{ secrets.QINIU_ACCESS_KEY }}" - QINIU_SECRET_KEY="${{ secrets.QINIU_SECRET_KEY }}" - QINIU_BUCKET="${{ secrets.QINIU_BUCKET }}" - QINIU_DOMAIN="${{ secrets.QINIU_DOMAIN }}" - QINIU_REGION="${{ secrets.QINIU_REGION }}" - - # 邮箱配置 - EMAIL_USER_NAME="${{ secrets.EMAIL_USER_NAME }}" - EMAIL_USER_PASSWORD="${{ secrets.EMAIL_USER_PASSWORD }}" - - # 部署信息 - DEPLOY_TAG="${{ needs.detect-changes.outputs.image-tag }}" - DEPLOY_TIME="$(date '+%Y-%m-%d %H:%M:%S')" - DEPLOY_COMMIT="${{ github.sha }}" - SKIP_HEALTH_CHECK="${{ github.event.inputs.skip_health_check || 'false' }}" - EOF - - # 复制 docker-compose 配置 - cp docker-compose.test.yml deploy/staging/docker-compose.yml - - # 生成部署脚本 - cat > deploy/staging/deploy.sh << 'EOF' - #!/bin/bash - set -e - - echo "=== VocaTa 测试环境部署开始 ===" - echo "部署标签: $DEPLOY_TAG" - echo "提交版本: $DEPLOY_COMMIT" - - # 加载环境变量 - source .env - - # 登录 Docker Registry - echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u "veardk" --password-stdin - - # 确定需要更新的服务 - SERVICES_TO_UPDATE=() - - if [[ "${{ needs.detect-changes.outputs.server-changed }}" == "true" || "${{ github.event.inputs.force_rebuild }}" == "true" ]]; then - SERVICES_TO_UPDATE+=("vocata-server") - echo "✓ 将更新后端服务: $SERVER_IMAGE" - fi - - if [[ "${{ needs.detect-changes.outputs.web-changed }}" == "true" || "${{ github.event.inputs.force_rebuild }}" == "true" ]]; then - SERVICES_TO_UPDATE+=("vocata-web") - echo "✓ 将更新前端客户端: $WEB_IMAGE" - fi - - if [[ "${{ needs.detect-changes.outputs.admin-changed }}" == "true" || "${{ github.event.inputs.force_rebuild }}" == "true" ]]; then - SERVICES_TO_UPDATE+=("vocata-admin") - echo "✓ 将更新管理后台: $ADMIN_IMAGE" - fi - - if [ ${#SERVICES_TO_UPDATE[@]} -eq 0 ]; then - echo "⚠ 没有服务需要更新" - exit 0 - fi - - # 拉取最新镜像 - echo "📥 拉取镜像..." - for service in "${SERVICES_TO_UPDATE[@]}"; do - case $service in - "vocata-server") - echo "拉取后端镜像: $SERVER_IMAGE" - docker pull "$SERVER_IMAGE" - ;; - "vocata-web") - echo "拉取前端镜像: $WEB_IMAGE" - docker pull "$WEB_IMAGE" - ;; - "vocata-admin") - echo "拉取管理后台镜像: $ADMIN_IMAGE" - docker pull "$ADMIN_IMAGE" - ;; - esac - done - - # 滚动更新服务 - echo "🔄 更新服务..." - for service in "${SERVICES_TO_UPDATE[@]}"; do - echo "更新 $service..." - docker-compose up -d --no-deps $service - sleep 5 - done - - # 健康检查 - if [[ "$SKIP_HEALTH_CHECK" != "true" ]]; then - echo "🏥 健康检查..." - - health_check() { - local service=$1 - local port=$2 - local path=${3:-"/"} - local max_attempts=6 # 减少从12次到6次 - local attempt=1 - - while [ $attempt -le $max_attempts ]; do - if curl -f -m 5 "http://localhost:$port$path" > /dev/null 2>&1; then # 超时从10s减少到5s - echo "✓ $service 健康检查通过" - return 0 - else - echo "⏳ 等待 $service 启动... ($attempt/$max_attempts)" - sleep 5 # 间隔从10s减少到5s - ((attempt++)) - fi - done + - name: 部署到服务器 + uses: appleboy/ssh-action@v1.0.3 + env: + GIT_REF: ${{ github.ref_name }} + GIT_SHA: ${{ github.sha }} + GITHUB_REPOSITORY: ${{ github.repository }} + GITHUB_TOKEN: ${{ github.token }} + with: + host: ${{ secrets.STAGING_HOST }} + username: ${{ secrets.STAGING_USER }} + key: ${{ secrets.STAGING_SSH_KEY }} + port: 22 + script_stop: true + envs: GIT_REF,GIT_SHA,GITHUB_REPOSITORY,GITHUB_TOKEN + script: | + set -e - echo "✗ $service 健康检查失败" - docker-compose logs --tail=20 $service # 减少日志行数从50到20 - return 1 - } + APP_DIR=/home/deploy/deploy/vocata + REPO_DIR=$APP_DIR/repo + ENV_FILE=$APP_DIR/.env + LEGACY_ENV_FILE=$APP_DIR/data/vocata-staging.env + REPO_URL="https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" - # 执行健康检查 - HEALTH_FAILED=false + mkdir -p "$APP_DIR" - for service in "${SERVICES_TO_UPDATE[@]}"; do - case $service in - "vocata-server") - echo "⏭️ 跳过后端服务健康检查(服务已启动)" - ;; - "vocata-web") - if ! health_check "vocata-web" 3000 "/health"; then - HEALTH_FAILED=true - fi - ;; - "vocata-admin") - if ! health_check "vocata-admin" 3001 "/health"; then - HEALTH_FAILED=true - fi - ;; - esac - done + if [ ! -f "$ENV_FILE" ] && [ -f "$LEGACY_ENV_FILE" ]; then + cp "$LEGACY_ENV_FILE" "$ENV_FILE" + fi - if [[ "$HEALTH_FAILED" == "true" ]]; then - echo "❌ 部署失败: 健康检查未通过" - echo "正在回滚..." - docker-compose restart "${SERVICES_TO_UPDATE[@]}" + if [ ! -f "$ENV_FILE" ]; then + echo "缺少部署环境文件: $ENV_FILE" + echo "请先在服务器上准备好 .env,再重新执行工作流。" exit 1 fi - else - echo "⚡ 跳过健康检查(快速部署模式)" - echo "等待5秒后继续..." - sleep 5 - fi - # 清理旧镜像 - echo "🧹 清理旧镜像..." - docker system prune -f || true + if [ ! -d "$REPO_DIR/.git" ]; then + rm -rf "$REPO_DIR" + git clone --branch "$GIT_REF" --depth 1 "$REPO_URL" "$REPO_DIR" + else + cd "$REPO_DIR" + git remote set-url origin "$REPO_URL" + git fetch --depth 1 origin "$GIT_REF" + git checkout "$GIT_REF" + git reset --hard "origin/$GIT_REF" + fi - echo "🎉 测试环境部署成功!" - echo "更新的服务: ${SERVICES_TO_UPDATE[*]}" - EOF + cd "$REPO_DIR" + git remote set-url origin "https://github.com/${GITHUB_REPOSITORY}.git" - chmod +x deploy/staging/deploy.sh + echo "当前提交: $GIT_SHA" + docker compose --env-file "$ENV_FILE" up -d --build - - name: 创建远程目录 - uses: appleboy/ssh-action@v1.0.3 - with: - host: ${{ secrets.STAGING_HOST }} - username: ${{ secrets.STAGING_USER }} - key: ${{ secrets.STAGING_SSH_KEY }} - port: 22 - script: | - mkdir -p $HOME/deploy/vocata - chmod 755 $HOME/deploy/vocata + echo "=== 容器状态 ===" + docker compose ps - - name: 复制文件到测试服务器 - uses: appleboy/scp-action@v0.1.7 - with: - host: ${{ secrets.STAGING_HOST }} - username: ${{ secrets.STAGING_USER }} - key: ${{ secrets.STAGING_SSH_KEY }} - port: 22 - source: "deploy/staging/*" - target: "~/deploy/vocata" - strip_components: 2 - rm: true + echo "=== 后端健康检查 ===" + curl -fsS http://127.0.0.1:9009/api/health - - name: 执行部署 + - name: 失败时输出日志 + if: failure() uses: appleboy/ssh-action@v1.0.3 with: host: ${{ secrets.STAGING_HOST }} username: ${{ secrets.STAGING_USER }} key: ${{ secrets.STAGING_SSH_KEY }} port: 22 + script_stop: false script: | - cd ~/deploy/vocata - ls -la - chmod +x deploy.sh - ./deploy.sh + APP_DIR=/home/deploy/deploy/vocata + REPO_DIR=$APP_DIR/repo + + if [ -d "$REPO_DIR" ]; then + cd "$REPO_DIR" + echo "=== docker compose ps ===" + docker compose ps || true + echo "=== vocata-server logs ===" + docker compose logs --tail=100 vocata-server || true + echo "=== vocata-web logs ===" + docker compose logs --tail=60 vocata-web || true + echo "=== vocata-admin logs ===" + docker compose logs --tail=60 vocata-admin || true + fi - name: 部署结果报告 if: always() run: | - echo "## 🚀 测试环境部署结果" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY + echo "## Staging 部署结果" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + echo "- 环境: staging" >> "$GITHUB_STEP_SUMMARY" + echo "- 分支: ${{ github.ref_name }}" >> "$GITHUB_STEP_SUMMARY" + echo "- 提交: \`${{ github.sha }}\`" >> "$GITHUB_STEP_SUMMARY" + echo "- 服务器: \`${{ secrets.STAGING_HOST }}\`" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" if [ "${{ job.status }}" = "success" ]; then - echo "✅ **测试环境部署成功**" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "### 📋 部署详情" >> $GITHUB_STEP_SUMMARY - echo "- **环境**: 测试环境 (staging)" >> $GITHUB_STEP_SUMMARY - echo "- **部署标签**: \`${{ needs.detect-changes.outputs.image-tag }}\`" >> $GITHUB_STEP_SUMMARY - echo "- **提交版本**: \`${{ github.sha }}\`" >> $GITHUB_STEP_SUMMARY - echo "- **分支**: develop" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "### 🔗 访问地址" >> $GITHUB_STEP_SUMMARY - echo "- [前端客户端](http://${{ secrets.STAGING_HOST }}:3000)" >> $GITHUB_STEP_SUMMARY - echo "- [管理后台](http://${{ secrets.STAGING_HOST }}:3001)" >> $GITHUB_STEP_SUMMARY - echo "- [后端API](http://${{ secrets.STAGING_HOST }}:9009/api)" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "### 🏗️ 部署的组件" >> $GITHUB_STEP_SUMMARY - - if [[ "${{ needs.detect-changes.outputs.server-changed }}" == "true" || "${{ github.event.inputs.force_rebuild }}" == "true" ]]; then - echo "- ✅ 后端服务 (\`${{ needs.build-server.outputs.image }}\`)" >> $GITHUB_STEP_SUMMARY - fi - if [[ "${{ needs.detect-changes.outputs.web-changed }}" == "true" || "${{ github.event.inputs.force_rebuild }}" == "true" ]]; then - echo "- ✅ 前端客户端 (\`${{ needs.build-web.outputs.image }}\`)" >> $GITHUB_STEP_SUMMARY - fi - if [[ "${{ needs.detect-changes.outputs.admin-changed }}" == "true" || "${{ github.event.inputs.force_rebuild }}" == "true" ]]; then - echo "- ✅ 管理后台 (\`${{ needs.build-admin.outputs.image }}\`)" >> $GITHUB_STEP_SUMMARY - fi - - echo "" >> $GITHUB_STEP_SUMMARY - echo "🎯 **可以开始前后端连调了!**" >> $GITHUB_STEP_SUMMARY + echo "✅ 部署成功" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + echo "- 用户端: http://${{ secrets.STAGING_HOST }}:3000" >> "$GITHUB_STEP_SUMMARY" + echo "- 管理端: http://${{ secrets.STAGING_HOST }}:3001" >> "$GITHUB_STEP_SUMMARY" + echo "- API: http://${{ secrets.STAGING_HOST }}:9009/api" >> "$GITHUB_STEP_SUMMARY" else - echo "❌ **测试环境部署失败**" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "请检查部署日志并排查问题。" >> $GITHUB_STEP_SUMMARY - fi \ No newline at end of file + echo "❌ 部署失败" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + echo "请查看上面的 SSH 部署输出和容器日志。" >> "$GITHUB_STEP_SUMMARY" + fi diff --git a/.github/workflows/emergency-rollback.yml b/.github/workflows/emergency-rollback.yml index b434be5..2a34383 100644 --- a/.github/workflows/emergency-rollback.yml +++ b/.github/workflows/emergency-rollback.yml @@ -192,7 +192,7 @@ jobs: # 检查后端服务 for i in {1..10}; do - if curl -f http://localhost:9009/api/actuator/health > /dev/null 2>&1; then + if curl -f http://localhost:9009/api/health > /dev/null 2>&1; then echo "✅ 后端服务回滚成功" break else @@ -399,7 +399,7 @@ jobs: sleep 30 # 检查备用服务健康状态 - if curl -f http://localhost:9011/api/actuator/health > /dev/null 2>&1; then + if curl -f http://localhost:9011/api/health > /dev/null 2>&1; then log_success "备用服务就绪,执行流量切换" # 这里应该切换负载均衡器配置 # 实际环境中需要根据具体的负载均衡器进行配置 @@ -422,7 +422,7 @@ jobs: # 检查后端服务 HEALTH_CHECK_PASSED=true for i in {1..15}; do - if curl -f http://localhost:9009/api/actuator/health > /dev/null 2>&1; then + if curl -f http://localhost:9009/api/health > /dev/null 2>&1; then log_success "✅ 后端服务回滚成功" break else @@ -594,4 +594,4 @@ jobs: }); console.log(title); - console.log(body); \ No newline at end of file + console.log(body); diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 89228fe..cdb6c25 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -309,7 +309,7 @@ jobs: sleep 30 # 验证新容器健康状态 - if docker-compose exec -T $service curl -f http://localhost:$SERVER_PORT/api/actuator/health > /dev/null 2>&1; then + if docker-compose exec -T $service curl -f http://localhost:$SERVER_PORT/api/health > /dev/null 2>&1; then echo "✅ $service 新版本健康检查通过" # 切换流量,移除旧容器 (蓝色) @@ -402,7 +402,7 @@ jobs: # API健康检查 echo "### API服务验证" >> $GITHUB_STEP_SUMMARY - if curl -f -m 30 "https://api.vocata.com/api/actuator/health" > /dev/null 2>&1; then + if curl -f -m 30 "https://api.vocata.com/api/health" > /dev/null 2>&1; then echo "- ✅ API健康检查通过" >> $GITHUB_STEP_SUMMARY else echo "- ❌ API健康检查失败" >> $GITHUB_STEP_SUMMARY @@ -434,4 +434,4 @@ jobs: else echo "⚠️ 生产环境验证失败!请立即进行回滚操作。" >> $GITHUB_STEP_SUMMARY exit 1 - fi \ No newline at end of file + fi diff --git a/.gitignore b/.gitignore index 7af4e77..184df36 100644 --- a/.gitignore +++ b/.gitignore @@ -165,6 +165,7 @@ Desktop.ini # 文档生成 **/.docs/ +**/.local/ # 测试覆盖率报告 **/coverage/ @@ -186,4 +187,4 @@ Desktop.ini !**/application-example.yml !**/config.example.js -/resources/application-local.yml \ No newline at end of file +/resources/application-local.yml diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 1db9aa4..fe958ce 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -1,5 +1,4 @@ # VocaTa生产环境 Docker Compose 配置 -version: '3.8' x-app-common: &app-common restart: unless-stopped @@ -14,7 +13,7 @@ x-app-common: &app-common max-file: "3" x-healthcheck-web: &healthcheck-web - test: ["CMD", "curl", "-f", "http://localhost/"] + test: ["CMD", "curl", "-f", "http://localhost:8080/health"] interval: 30s timeout: 10s retries: 3 @@ -67,7 +66,7 @@ services: - server-uploads:/app/uploads - /etc/localtime:/etc/localtime:ro healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:9009/api/actuator/health"] + test: ["CMD", "curl", "-f", "http://localhost:9009/api/health"] interval: 30s timeout: 15s retries: 5 @@ -92,7 +91,7 @@ services: image: ${WEB_IMAGE:-ghcr.io/leivik/vocata-web:latest} container_name: vocata-web ports: - - "${WEB_PORT:-8080}:80" + - "${WEB_PORT:-8080}:8080" volumes: - web-logs:/var/log/nginx - /etc/localtime:/etc/localtime:ro @@ -119,7 +118,7 @@ services: image: ${ADMIN_IMAGE:-ghcr.io/leivik/vocata-admin:latest} container_name: vocata-admin ports: - - "${ADMIN_PORT:-8081}:80" + - "${ADMIN_PORT:-8081}:8080" volumes: - admin-logs:/var/log/nginx - /etc/localtime:/etc/localtime:ro @@ -284,4 +283,4 @@ networks: driver: bridge name: ${COMPOSE_PROJECT_NAME:-vocata-prod}-network driver_opts: - com.docker.network.bridge.name: vocata-br \ No newline at end of file + com.docker.network.bridge.name: vocata-br diff --git a/docker-compose.test.yml b/docker-compose.test.yml index fb06844..280f0ee 100644 --- a/docker-compose.test.yml +++ b/docker-compose.test.yml @@ -1,5 +1,4 @@ # VocaTa测试环境 Docker Compose 配置 -version: '3.8' x-app-common: &app-common restart: unless-stopped @@ -43,6 +42,7 @@ services: - QINIU_BUCKET=${QINIU_BUCKET} - QINIU_DOMAIN=${QINIU_DOMAIN} - QINIU_REGION=${QINIU_REGION} + - QINIU_KEY_PREFIX=${QINIU_KEY_PREFIX} # 邮箱配置 - EMAIL_USER_NAME=${EMAIL_USER_NAME} - EMAIL_USER_PASSWORD=${EMAIL_USER_PASSWORD} @@ -53,18 +53,42 @@ services: - MAIL_PASSWORD=${EMAIL_USER_PASSWORD} # AI服务配置 - AI_LLM_PROVIDER=${AI_LLM_PROVIDER:-qiniu} + - AI_STT_PROVIDER=${AI_STT_PROVIDER:-qiniu} + - AI_TTS_PROVIDER=${AI_TTS_PROVIDER:-xunfei} - QINIU_AI_API_KEY=${QINIU_AI_API_KEY} - QINIU_AI_BASE_URL=${QINIU_AI_BASE_URL:-https://openai.qiniu.com/v1} - QINIU_AI_MODEL=${QINIU_AI_MODEL:-x-ai/grok-4-fast} - QINIU_AI_TIMEOUT=${QINIU_AI_TIMEOUT:-60} + - QINIU_STT_ENDPOINT=${QINIU_STT_ENDPOINT:-https://openai.qiniu.com/v1} + - QINIU_STT_MODEL=${QINIU_STT_MODEL:-asr} - GEMINI_API_KEY=${GEMINI_API_KEY} + - GEMINI_MODEL=${GEMINI_MODEL:-gemini-2.5-flash-lite} - OPENAI_API_KEY=${OPENAI_API_KEY} + - OPENAI_MODEL=${OPENAI_MODEL:-gpt-3.5-turbo} + - SILICONFLOW_API_KEY=${SILICONFLOW_API_KEY} + - SILICONFLOW_BASE_URL=${SILICONFLOW_BASE_URL:-https://api.siliconflow.cn/v1} + - SILICONFLOW_AI_MODEL=${SILICONFLOW_AI_MODEL:-Qwen/Qwen3-8B} + - SILICONFLOW_TIMEOUT=${SILICONFLOW_TIMEOUT:-120} + - XUNFEI_STT_APP_ID=${XUNFEI_STT_APP_ID} + - XUNFEI_STT_API_KEY=${XUNFEI_STT_API_KEY} + - XUNFEI_STT_SECRET_KEY=${XUNFEI_STT_SECRET_KEY} + - XUNFEI_STT_HOST=${XUNFEI_STT_HOST:-iat-api.xfyun.cn} + - XUNFEI_STT_PATH=${XUNFEI_STT_PATH:-/v2/iat} + - XUNFEI_TTS_APP_ID=${XUNFEI_TTS_APP_ID} + - XUNFEI_TTS_API_KEY=${XUNFEI_TTS_API_KEY} + - XUNFEI_TTS_SECRET_KEY=${XUNFEI_TTS_SECRET_KEY} + - XUNFEI_TTS_HOST=${XUNFEI_TTS_HOST:-tts-api.xfyun.cn} + - XUNFEI_TTS_PATH=${XUNFEI_TTS_PATH:-/v2/tts} + - VOLCAN_TTS_ACCESS_TOKEN=${VOLCAN_TTS_ACCESS_TOKEN} + - VOLCAN_TTS_APP_ID=${VOLCAN_TTS_APP_ID} + - VOLCAN_TTS_HOST=${VOLCAN_TTS_HOST:-openspeech.bytedance.com} + - VOLCAN_TTS_REGION=${VOLCAN_TTS_REGION:-ap-beijing-1} volumes: - server-logs:/var/log/vocata - server-uploads:/app/uploads healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:9009/api/actuator/health"] + test: ["CMD", "curl", "-f", "http://localhost:9009/api/health"] interval: 30s timeout: 10s retries: 3 @@ -138,4 +162,4 @@ volumes: networks: vocata-network: driver: bridge - name: ${COMPOSE_PROJECT_NAME:-vocata-test}-network \ No newline at end of file + name: ${COMPOSE_PROJECT_NAME:-vocata-test}-network diff --git a/docker-compose.yml b/docker-compose.yml index 8d5fb7f..ce42535 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,20 +1,18 @@ -version: '3.8' - services: # PostgreSQL 数据库 postgres: - image: postgres:15-alpine + image: pgvector/pgvector:pg17 container_name: vocata-postgres-dev environment: - POSTGRES_DB: vocata_local - POSTGRES_USER: vocata - POSTGRES_PASSWORD: vocata123 + POSTGRES_DB: ${POSTGRES_DB:-vocata_db} + POSTGRES_USER: ${POSTGRES_USER:-postgres} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres123} ports: - - "5432:5432" + - "${POSTGRES_PORT:-5432}:5432" volumes: - postgres_data:/var/lib/postgresql/data healthcheck: - test: ["CMD-SHELL", "pg_isready -U vocata -d vocata_local"] + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-postgres} -d ${POSTGRES_DB:-vocata_db}"] interval: 30s timeout: 10s retries: 3 @@ -24,7 +22,7 @@ services: image: redis:7-alpine container_name: vocata-redis-dev ports: - - "6379:6379" + - "${REDIS_EXPOSE_PORT:-6379}:6379" volumes: - redis_data:/data healthcheck: @@ -38,17 +36,47 @@ services: build: context: ./vocata-server dockerfile: Dockerfile + args: + SPRING_PROFILES_ACTIVE: ${SPRING_PROFILES_ACTIVE:-local} container_name: vocata-server-dev ports: - - "9009:9009" + - "${SERVER_PORT:-9009}:9009" environment: - SPRING_PROFILES_ACTIVE: local - SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/vocata_local - SPRING_DATASOURCE_USERNAME: vocata - SPRING_DATASOURCE_PASSWORD: vocata123 - SPRING_REDIS_HOST: redis - SPRING_REDIS_PORT: 6379 - SPRING_REDIS_DATABASE: 0 + SPRING_PROFILES_ACTIVE: ${SPRING_PROFILES_ACTIVE:-local} + SERVER_PORT: 9009 + DB_HOST: ${DB_HOST:-postgres} + DB_PORT: ${DB_PORT:-5432} + DB_NAME: ${DB_NAME:-vocata_db} + DB_USERNAME: ${DB_USERNAME:-postgres} + DB_PASSWORD: ${DB_PASSWORD:-postgres123} + REDIS_HOST: ${REDIS_HOST:-redis} + REDIS_PORT: ${REDIS_PORT:-6379} + REDIS_PASSWORD: ${REDIS_PASSWORD:-} + REDIS_DATABASE: ${REDIS_DATABASE:-0} + MAIL_USERNAME: ${MAIL_USERNAME:-} + MAIL_PASSWORD: ${MAIL_PASSWORD:-} + AI_LLM_PROVIDER: ${AI_LLM_PROVIDER:-qiniu} + AI_STT_PROVIDER: ${AI_STT_PROVIDER:-qiniu} + AI_TTS_PROVIDER: ${AI_TTS_PROVIDER:-xunfei} + XUNFEI_TTS_APP_ID: ${XUNFEI_TTS_APP_ID:-} + XUNFEI_TTS_API_KEY: ${XUNFEI_TTS_API_KEY:-} + XUNFEI_TTS_SECRET_KEY: ${XUNFEI_TTS_SECRET_KEY:-} + QINIU_ACCESS_KEY: ${QINIU_ACCESS_KEY:-} + QINIU_SECRET_KEY: ${QINIU_SECRET_KEY:-} + QINIU_BUCKET: ${QINIU_BUCKET:-} + QINIU_DOMAIN: ${QINIU_DOMAIN:-} + QINIU_REGION: ${QINIU_REGION:-huadong} + QINIU_KEY_PREFIX: ${QINIU_KEY_PREFIX:-} + QINIU_AI_API_KEY: ${QINIU_AI_API_KEY:-} + QINIU_AI_BASE_URL: ${QINIU_AI_BASE_URL:-https://openai.qiniu.com/v1} + QINIU_AI_MODEL: ${QINIU_AI_MODEL:-x-ai/grok-4-fast} + QINIU_AI_TIMEOUT: ${QINIU_AI_TIMEOUT:-60} + GEMINI_API_KEY: ${GEMINI_API_KEY:-} + GEMINI_MODEL: ${GEMINI_MODEL:-gemini-2.5-flash-lite} + OPENAI_API_KEY: ${OPENAI_API_KEY:-} + OPENAI_MODEL: ${OPENAI_MODEL:-gpt-3.5-turbo} + SILICONFLOW_API_KEY: ${SILICONFLOW_API_KEY:-} + SILICONFLOW_AI_MODEL: ${SILICONFLOW_AI_MODEL:-Qwen/Qwen3-8B} depends_on: postgres: condition: service_healthy @@ -68,36 +96,51 @@ services: build: context: ./vocata-web dockerfile: Dockerfile + args: + BUILD_MODE: ${WEB_BUILD_MODE:-production} + VITE_APP_URL: ${VITE_APP_URL:-http://localhost:9009} container_name: vocata-web-dev ports: - - "3000:80" - environment: - VITE_API_BASE_URL: http://localhost:9009/api + - "${WEB_PORT:-3000}:8080" + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8080/health"] + interval: 30s + timeout: 10s + retries: 3 depends_on: - - vocata-server + vocata-server: + condition: service_healthy # 管理后台 vocata-admin: build: context: ./vocata-admin dockerfile: Dockerfile + args: + BUILD_MODE: ${ADMIN_BUILD_MODE:-production} + VITE_APP_URL: ${VITE_APP_URL:-http://localhost:9009} container_name: vocata-admin-dev ports: - - "3001:80" - environment: - VITE_API_BASE_URL: http://localhost:9009/api + - "${ADMIN_PORT:-3001}:8080" + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8080/health"] + interval: 30s + timeout: 10s + retries: 3 depends_on: - - vocata-server + vocata-server: + condition: service_healthy # pgAdmin (数据库管理工具) pgadmin: image: dpage/pgadmin4:latest container_name: vocata-pgadmin-dev + profiles: ["tools"] environment: - PGADMIN_DEFAULT_EMAIL: admin@vocata.com - PGADMIN_DEFAULT_PASSWORD: admin123 + PGADMIN_DEFAULT_EMAIL: ${PGADMIN_DEFAULT_EMAIL:-admin@vocata.com} + PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_DEFAULT_PASSWORD:-admin123} ports: - - "5050:80" + - "${PGADMIN_PORT:-5050}:80" depends_on: - postgres @@ -105,9 +148,10 @@ services: mailhog: image: mailhog/mailhog:latest container_name: vocata-mailhog-dev + profiles: ["tools"] ports: - - "1025:1025" # SMTP端口 - - "8025:8025" # Web界面端口 + - "${MAILHOG_SMTP_PORT:-1025}:1025" + - "${MAILHOG_WEB_PORT:-8025}:8025" volumes: postgres_data: @@ -115,4 +159,4 @@ volumes: networks: default: - name: vocata-dev-network \ No newline at end of file + name: ${COMPOSE_PROJECT_NAME:-vocata-dev}-network diff --git "a/docs/Docker\345\274\200\345\217\221\347\216\257\345\242\203.md" "b/docs/Docker\345\274\200\345\217\221\347\216\257\345\242\203.md" new file mode 100644 index 0000000..f95086e --- /dev/null +++ "b/docs/Docker\345\274\200\345\217\221\347\216\257\345\242\203.md" @@ -0,0 +1,57 @@ +# Docker 开发环境 + +当前仓库先以开发环境为中心整理 Docker 结构。 + +## 目标 + +- `docker-compose.yml` 只服务本地开发和联调 +- `docker-compose.test.yml`、`docker-compose.prod.yml` 先保留为后续测试/生产预留文件 +- 前端容器走接近发布态的静态构建,不提供热更新 + +## 使用方式 + +1. 复制环境变量模板 + +```bash +cp .env.example .env +``` + +2. 按需补充 `.env` + +- 基础开发默认值已经可用:PostgreSQL、Redis、端口映射 +- 开发数据库默认使用 `pgvector/pgvector:pg17`,便于恢复现有 PostgreSQL 17 + pgvector 备份 +- 若要完整启动后端,仍需补第三方配置,例如七牛和 AI 相关密钥 + +3. 启动核心开发环境 + +```bash +docker compose up -d --build +``` + +4. 启动可选工具 + +```bash +docker compose --profile tools up -d +``` + +## 当前开发环境包含 + +- `postgres`:开发数据库 +- `redis`:开发缓存 +- `vocata-server`:后端服务 +- `vocata-web`:用户端静态前端 +- `vocata-admin`:管理端静态前端 +- `pgadmin`:可选数据库管理工具 +- `mailhog`:可选邮件调试工具 + +## 约定 + +- 后端健康检查统一为 `/api/health` +- 前端容器内部统一监听 `8080` +- 前端 API 地址通过构建期变量 `VITE_APP_URL` 注入 +- 七牛对象前缀可通过 `QINIU_KEY_PREFIX` 配置,例如 `Vocata` + +## 说明 + +- 如果你要做前端页面开发,建议仍然优先本地运行 `npm run dev` +- 当前 Docker 开发环境更适合联调整体链路、验证容器化行为、准备后续服务器部署 diff --git a/docs/GitHub-Staging-Secrets.md b/docs/GitHub-Staging-Secrets.md new file mode 100644 index 0000000..56481cf --- /dev/null +++ b/docs/GitHub-Staging-Secrets.md @@ -0,0 +1,68 @@ +# GitHub Staging Secrets 清单 + +当前 staging 目标机: + +- `STAGING_HOST=86.53.161.33` +- `STAGING_USER=deploy` +- SSH 端口固定为 `22`(当前 workflow 已写死) + +## 必填 Secrets + +现在的简化版 staging workflow 只需要 3 个 secrets: + +- `STAGING_HOST` +- `STAGING_USER` +- `STAGING_SSH_KEY` + +## STAGING_SSH_KEY 来源 + +当前为 GitHub Actions 生成的 staging deploy key 私钥保存在本机: + +- `/home/an/Projects/goodPro/VocaTa/.local/vocata_staging_ed25519` + +对应公钥已经安装到服务器 `root` 用户: + +- `/root/.ssh/authorized_keys` + +同一把公钥也已经安装到服务器部署用户: + +- `/home/deploy/.ssh/authorized_keys` + +把私钥完整内容复制到 GitHub 仓库 Secret `STAGING_SSH_KEY` 即可。 + +## 当前服务器登录建议 + +- 日常部署用户:`deploy` +- 应急用户:`root` +- SSH 密码登录:已禁用 +- SSH 公钥登录:已启用 + +## 服务器配置放哪里 + +业务配置现在不再放 GitHub Secrets,而是放服务器本地: + +- `/home/deploy/deploy/vocata/.env` + +兼容旧位置: + +- `/home/deploy/deploy/vocata/data/vocata-staging.env` + +如果新位置不存在,workflow 会自动从旧位置复制一份。 + +## 前端 IP 暴露说明 + +如果服务器本地 `.env` 中写的是: + +- `VITE_APP_URL=http://86.53.161.33:9009` + +那么前端构建产物里会直接带这个 IP,浏览器里可见。 + +如果你想避免把后端 IP 直接打进前端,服务器 `.env` 建议改成: + +- `VITE_APP_URL=/api` + +前提: + +- 当前前端镜像内 Nginx 已经把 `/api` 代理到 `vocata-server:9009` + +这会让浏览器只请求当前站点的 `/api`,不再把 `9009` 和后端 IP 显式写进前端包里。 diff --git "a/docs/\351\203\250\347\275\262\346\226\207\346\241\243.md" "b/docs/\351\203\250\347\275\262\346\226\207\346\241\243.md" index ed92fe7..2ac0bd8 100644 --- "a/docs/\351\203\250\347\275\262\346\226\207\346\241\243.md" +++ "b/docs/\351\203\250\347\275\262\346\226\207\346\241\243.md" @@ -7,6 +7,9 @@ - **本地开发模式**:分别启动后端(Java + Maven)及前端项目(Vue 3 + Vite),适合调试与开发。 - **生产部署示例**:项目根目录提供 `docker-compose.prod.yml` 可作为上线部署基础模板。 +当前建议优先使用开发环境 Docker 方案: +[Docker开发环境.md](/home/an/Projects/goodPro/VocaTa/docs/Docker开发环境.md) + ## 2. 环境准备 ### 2.1 通用要求 - 操作系统:macOS / Linux / Windows(支持 Docker 或 Java 开发环境) diff --git a/vocata-admin/.dockerignore b/vocata-admin/.dockerignore new file mode 100644 index 0000000..c6fd245 --- /dev/null +++ b/vocata-admin/.dockerignore @@ -0,0 +1,16 @@ +.git +.gitignore +.github +node_modules/ +dist/ +coverage/ +.vite/ +.idea/ +.vscode/ +*.log +.env +.env.local +.env.development.local +.env.test.local +.env.production.local +README.md diff --git a/vocata-admin/Dockerfile b/vocata-admin/Dockerfile index a99b11d..a90fac5 100644 --- a/vocata-admin/Dockerfile +++ b/vocata-admin/Dockerfile @@ -1,51 +1,36 @@ -# VocaTa前端客户端 - 多阶段构建Dockerfile -# 基于Node.js官方镜像 - -# 构建阶段 FROM node:20-alpine AS build -# 安装必要的构建工具 RUN apk add --no-cache python3 make g++ git -# 设置工作目录 WORKDIR /app -# 复制package文件 COPY package*.json ./ +RUN npm ci && npm cache clean --force -# 安装依赖 -RUN npm ci --only=production && npm cache clean --force - -# 复制源代码 COPY . . -# 构建应用 ARG BUILD_MODE=production +ARG VITE_APP_URL +ENV VITE_APP_URL=${VITE_APP_URL} RUN npm run build:${BUILD_MODE} -# 生产阶段 - 使用Nginx托管静态文件 FROM nginx:1.25-alpine -# 安装必要工具 RUN apk add --no-cache \ curl \ - tzdata \ - dumb-init + dumb-init \ + tzdata -# 设置时区 ENV TZ=Asia/Shanghai RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone -# 创建nginx用户和目录 RUN addgroup -g 1001 -S vocata && \ adduser -u 1001 -S vocata -G vocata && \ mkdir -p /var/cache/nginx /var/log/nginx /var/lib/nginx && \ chown -R vocata:vocata /var/cache/nginx /var/log/nginx /var/lib/nginx /etc/nginx -# 复制构建产物 COPY --from=build --chown=vocata:vocata /app/dist /usr/share/nginx/html -# 创建Nginx配置文件 RUN cat > /etc/nginx/nginx.conf << 'EOF' worker_processes auto; error_log /var/log/nginx/error.log; @@ -115,7 +100,6 @@ http { add_header Cache-Control "public, immutable"; } - # API代理(如果需要) location /api { proxy_pass http://vocata-server:9009; proxy_set_header Host $host; @@ -134,33 +118,27 @@ http { } EOF -# 设置权限 RUN chown -R vocata:vocata /usr/share/nginx/html /etc/nginx -# 切换到非root用户 USER vocata -# 暴露端口 EXPOSE 8080 -# 健康检查 HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \ CMD curl -f http://localhost:8080/health || exit 1 -# 构建参数和标签 ARG BUILD_DATE ARG VERSION="1.0.0" LABEL maintainer="VocaTa Team " \ version="${VERSION}" \ build-date="${BUILD_DATE}" \ - description="VocaTa AI角色扮演平台前端客户端" \ - org.opencontainers.image.title="vocata-web" \ - org.opencontainers.image.description="VocaTa AI Role Playing Platform Frontend Client" \ + description="VocaTa AI角色扮演平台管理后台" \ + org.opencontainers.image.title="vocata-admin" \ + org.opencontainers.image.description="VocaTa AI Role Playing Platform Admin Frontend" \ org.opencontainers.image.url="https://github.com/leivik/vocata" \ org.opencontainers.image.vendor="VocaTa Team" \ org.opencontainers.image.version="${VERSION}" \ org.opencontainers.image.created="${BUILD_DATE}" -# 启动Nginx ENTRYPOINT ["dumb-init", "--"] -CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file +CMD ["nginx", "-g", "daemon off;"] diff --git a/vocata-server/.dockerignore b/vocata-server/.dockerignore index d1a9b48..1c0c737 100644 --- a/vocata-server/.dockerignore +++ b/vocata-server/.dockerignore @@ -38,6 +38,8 @@ out/ dist/ node_modules/ .npm +!target/ +!target/*.jar # 测试相关 coverage/ @@ -109,4 +111,4 @@ files/ # 缓存 .cache/ -*.cache \ No newline at end of file +*.cache diff --git a/vocata-server/Dockerfile b/vocata-server/Dockerfile index 62540c5..66d72d2 100644 --- a/vocata-server/Dockerfile +++ b/vocata-server/Dockerfile @@ -1,88 +1,62 @@ -# VocaTa后端服务 - 多阶段构建Dockerfile -# 基于OpenJDK 17的精简镜像 +# syntax=docker/dockerfile:1.7 -# 构建阶段 - 用于Maven构建(可选,CI/CD中已构建) FROM maven:3.9.4-eclipse-temurin-17-alpine AS build WORKDIR /build -# 复制Maven配置文件和Wrapper -COPY pom.xml . -COPY .mvn .mvn -COPY mvnw . +COPY pom.xml ./ +RUN --mount=type=cache,target=/root/.m2 \ + mvn -B -ntp dependency:go-offline -# 下载依赖(利用Docker层缓存) -RUN mvn dependency:go-offline -B - -# 复制源代码 COPY src ./src -# 构建应用(支持多环境) ARG SPRING_PROFILES_ACTIVE=prod -RUN mvn clean package -B -DskipTests -Dspring.profiles.active=${SPRING_PROFILES_ACTIVE} +RUN --mount=type=cache,target=/root/.m2 \ + mvn -B -ntp package -DskipTests -Dspring.profiles.active=${SPRING_PROFILES_ACTIVE} -# 运行阶段 - 精简的生产镜像 FROM eclipse-temurin:17-jre-alpine -# 安装必要工具和dumb-init RUN apk add --no-cache \ curl \ - tzdata \ + dumb-init \ font-noto-cjk \ - dumb-init + tzdata -# 设置时区 ENV TZ=Asia/Shanghai RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone -# 创建应用用户(安全最佳实践) RUN addgroup -g 1001 -S vocata && \ adduser -u 1001 -S vocata -G vocata -# 设置工作目录 WORKDIR /app -# 复制JAR文件 - 优先使用已构建的JAR,回退到构建阶段 -COPY --from=build /build/target/vocata-server-*.jar app.jar -# 如果CI/CD中已经构建了JAR,可以直接复制 -# COPY ./target/vocata-server-*.jar app.jar +COPY --from=build /build/target/vocata-server-*.jar /app/app.jar -# 创建必要目录并设置权限 RUN mkdir -p /app/logs /app/uploads /var/log/vocata && \ - chown -R vocata:vocata /app /var/log/vocata && \ - chmod +x /app/app.jar + chown -R vocata:vocata /app /var/log/vocata -# 切换到非root用户 USER vocata -# 暴露端口 - 配置端口以匹配不同环境 -EXPOSE 9010 EXPOSE 9009 -# JVM优化参数 -ENV JAVA_OPTS="-Xms512m -Xmx2048m \ --XX:+UseG1GC \ +ENV JAVA_OPTS="-XX:+UseG1GC \ -XX:G1HeapRegionSize=16m \ -XX:+DisableExplicitGC \ -XX:+UseStringDeduplication \ -XX:+OptimizeStringConcat \ +-XX:MaxRAMPercentage=75.0 \ -Djava.security.egd=file:/dev/./urandom \ -Dfile.encoding=UTF-8 \ -Duser.timezone=Asia/Shanghai" - -# 应用配置环境变量 +ENV SERVER_PORT=9009 ENV SPRING_PROFILES_ACTIVE=prod -ENV SERVER_PORT=9010 -# 健康检查 HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \ - CMD curl -f http://localhost:${SERVER_PORT}/api/actuator/health || exit 1 + CMD curl -f http://localhost:${SERVER_PORT}/api/health || exit 1 -# 启动应用 - 使用 dumb-init 处理信号 ENTRYPOINT ["dumb-init", "--"] CMD ["sh", "-c", "java $JAVA_OPTS -jar app.jar"] -# 构建参数和标签信息 ARG BUILD_DATE ARG VERSION="1.0.0" LABEL maintainer="VocaTa Team " \ @@ -94,4 +68,4 @@ LABEL maintainer="VocaTa Team " \ org.opencontainers.image.url="https://github.com/leivik/vocata" \ org.opencontainers.image.vendor="VocaTa Team" \ org.opencontainers.image.version="${VERSION}" \ - org.opencontainers.image.created="${BUILD_DATE}" \ No newline at end of file + org.opencontainers.image.created="${BUILD_DATE}" diff --git a/vocata-server/Dockerfile.ci b/vocata-server/Dockerfile.ci index 3c04bbb..6b68688 100644 --- a/vocata-server/Dockerfile.ci +++ b/vocata-server/Dockerfile.ci @@ -1,44 +1,29 @@ -# VocaTa后端服务 - CI/CD优化版Dockerfile -# 针对CI/CD环境优化的单阶段构建 - FROM eclipse-temurin:17-jre-alpine -# 安装必要工具和dumb-init RUN apk add --no-cache \ curl \ - tzdata \ + dumb-init \ font-noto-cjk \ - dumb-init + tzdata -# 设置时区 ENV TZ=Asia/Shanghai RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone -# 创建应用用户(安全最佳实践) RUN addgroup -g 1001 -S vocata && \ adduser -u 1001 -S vocata -G vocata -# 设置工作目录 WORKDIR /app -# 复制JAR文件(从CI/CD构建阶段) -COPY vocata-server/target/vocata-server-*.jar app.jar +COPY target/vocata-server-*.jar /app/app.jar -# 创建必要目录并设置权限 RUN mkdir -p /app/logs /app/uploads /var/log/vocata && \ - chown -R vocata:vocata /app /var/log/vocata && \ - chmod +x /app/app.jar + chown -R vocata:vocata /app /var/log/vocata -# 切换到非root用户 USER vocata -# 暴露端口 - 支持不同环境 EXPOSE 9009 -EXPOSE 9010 -# JVM优化参数 - 针对容器环境优化 -ENV JAVA_OPTS="-Xms256m -Xmx1024m \ --XX:+UseG1GC \ +ENV JAVA_OPTS="-XX:+UseG1GC \ -XX:G1HeapRegionSize=16m \ -XX:+DisableExplicitGC \ -XX:+UseStringDeduplication \ @@ -48,28 +33,24 @@ ENV JAVA_OPTS="-Xms256m -Xmx1024m \ -Dfile.encoding=UTF-8 \ -Duser.timezone=Asia/Shanghai" -# 应用配置环境变量 -ENV SPRING_PROFILES_ACTIVE=test ENV SERVER_PORT=9009 +ENV SPRING_PROFILES_ACTIVE=prod -# 健康检查 - 支持动态端口 HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \ - CMD curl -f http://localhost:${SERVER_PORT}/api/actuator/health || exit 1 + CMD curl -f http://localhost:${SERVER_PORT}/api/health || exit 1 -# 启动应用 - 使用 dumb-init 处理信号 ENTRYPOINT ["dumb-init", "--"] CMD ["sh", "-c", "java $JAVA_OPTS -jar app.jar"] -# 构建参数和标签信息 ARG BUILD_DATE ARG VERSION="1.0.0" LABEL maintainer="VocaTa Team " \ version="${VERSION}" \ build-date="${BUILD_DATE}" \ - description="VocaTa AI角色扮演平台后端服务 - CI/CD版本" \ + description="VocaTa AI角色扮演平台后端服务" \ org.opencontainers.image.title="vocata-server" \ org.opencontainers.image.description="VocaTa AI Role Playing Platform Backend Service" \ org.opencontainers.image.url="https://github.com/leivik/vocata" \ org.opencontainers.image.vendor="VocaTa Team" \ org.opencontainers.image.version="${VERSION}" \ - org.opencontainers.image.created="${BUILD_DATE}" \ No newline at end of file + org.opencontainers.image.created="${BUILD_DATE}" diff --git a/vocata-server/src/main/java/com/vocata/file/config/QiniuConfig.java b/vocata-server/src/main/java/com/vocata/file/config/QiniuConfig.java index 12a4924..3ccf850 100644 --- a/vocata-server/src/main/java/com/vocata/file/config/QiniuConfig.java +++ b/vocata-server/src/main/java/com/vocata/file/config/QiniuConfig.java @@ -43,6 +43,11 @@ public class QiniuConfig { */ private String region; + /** + * 对象 key 前缀,例如 Vocata + */ + private String keyPrefix; + /** * 七牛云认证 */ @@ -131,4 +136,12 @@ public String getRegion() { public void setRegion(String region) { this.region = region; } -} \ No newline at end of file + + public String getKeyPrefix() { + return keyPrefix; + } + + public void setKeyPrefix(String keyPrefix) { + this.keyPrefix = keyPrefix; + } +} diff --git a/vocata-server/src/main/java/com/vocata/file/config/QiniuProperties.java b/vocata-server/src/main/java/com/vocata/file/config/QiniuProperties.java index 3db264c..0a0d545 100644 --- a/vocata-server/src/main/java/com/vocata/file/config/QiniuProperties.java +++ b/vocata-server/src/main/java/com/vocata/file/config/QiniuProperties.java @@ -18,6 +18,8 @@ public class QiniuProperties { private String domain; + private String keyPrefix; + private Long uploadTokenExpires = 3600L; public String getAccessKey() { @@ -52,6 +54,14 @@ public void setDomain(String domain) { this.domain = domain; } + public String getKeyPrefix() { + return keyPrefix; + } + + public void setKeyPrefix(String keyPrefix) { + this.keyPrefix = keyPrefix; + } + public Long getUploadTokenExpires() { return uploadTokenExpires; } @@ -59,4 +69,4 @@ public Long getUploadTokenExpires() { public void setUploadTokenExpires(Long uploadTokenExpires) { this.uploadTokenExpires = uploadTokenExpires; } -} \ No newline at end of file +} diff --git a/vocata-server/src/main/java/com/vocata/file/service/impl/FileServiceImpl.java b/vocata-server/src/main/java/com/vocata/file/service/impl/FileServiceImpl.java index cab35f1..ad2f629 100644 --- a/vocata-server/src/main/java/com/vocata/file/service/impl/FileServiceImpl.java +++ b/vocata-server/src/main/java/com/vocata/file/service/impl/FileServiceImpl.java @@ -289,11 +289,13 @@ private String generateFileName(MultipartFile file, String fileType) { String dateStr = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")); String uuid = IdUtil.simpleUUID(); - return String.format("%s/%s/%s%s", + String objectKey = String.format("%s/%s/%s%s", StringUtils.isNotBlank(fileType) ? fileType : "common", dateStr, uuid, extension); + + return prependKeyPrefix(objectKey); } /** @@ -310,10 +312,26 @@ private String generateAudioFileName(String originalFileName, String fileType) { String dateStr = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")); String uuid = IdUtil.simpleUUID(); - return String.format("%s/%s/%s%s", + String objectKey = String.format("%s/%s/%s%s", StringUtils.isNotBlank(fileType) ? fileType : "audio", dateStr, uuid, extension); + + return prependKeyPrefix(objectKey); + } + + private String prependKeyPrefix(String objectKey) { + String keyPrefix = qiniuConfig.getKeyPrefix(); + if (StringUtils.isBlank(keyPrefix)) { + return objectKey; + } + + String normalizedPrefix = StringUtils.strip(keyPrefix, "/"); + if (StringUtils.isBlank(normalizedPrefix)) { + return objectKey; + } + + return normalizedPrefix + "/" + objectKey; } } diff --git a/vocata-web/.dockerignore b/vocata-web/.dockerignore new file mode 100644 index 0000000..c6fd245 --- /dev/null +++ b/vocata-web/.dockerignore @@ -0,0 +1,16 @@ +.git +.gitignore +.github +node_modules/ +dist/ +coverage/ +.vite/ +.idea/ +.vscode/ +*.log +.env +.env.local +.env.development.local +.env.test.local +.env.production.local +README.md diff --git a/vocata-web/Dockerfile b/vocata-web/Dockerfile index a99b11d..a7f89ea 100644 --- a/vocata-web/Dockerfile +++ b/vocata-web/Dockerfile @@ -1,51 +1,36 @@ -# VocaTa前端客户端 - 多阶段构建Dockerfile -# 基于Node.js官方镜像 - -# 构建阶段 FROM node:20-alpine AS build -# 安装必要的构建工具 RUN apk add --no-cache python3 make g++ git -# 设置工作目录 WORKDIR /app -# 复制package文件 COPY package*.json ./ +RUN npm ci && npm cache clean --force -# 安装依赖 -RUN npm ci --only=production && npm cache clean --force - -# 复制源代码 COPY . . -# 构建应用 ARG BUILD_MODE=production +ARG VITE_APP_URL +ENV VITE_APP_URL=${VITE_APP_URL} RUN npm run build:${BUILD_MODE} -# 生产阶段 - 使用Nginx托管静态文件 FROM nginx:1.25-alpine -# 安装必要工具 RUN apk add --no-cache \ curl \ - tzdata \ - dumb-init + dumb-init \ + tzdata -# 设置时区 ENV TZ=Asia/Shanghai RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone -# 创建nginx用户和目录 RUN addgroup -g 1001 -S vocata && \ adduser -u 1001 -S vocata -G vocata && \ mkdir -p /var/cache/nginx /var/log/nginx /var/lib/nginx && \ chown -R vocata:vocata /var/cache/nginx /var/log/nginx /var/lib/nginx /etc/nginx -# 复制构建产物 COPY --from=build --chown=vocata:vocata /app/dist /usr/share/nginx/html -# 创建Nginx配置文件 RUN cat > /etc/nginx/nginx.conf << 'EOF' worker_processes auto; error_log /var/log/nginx/error.log; @@ -115,7 +100,6 @@ http { add_header Cache-Control "public, immutable"; } - # API代理(如果需要) location /api { proxy_pass http://vocata-server:9009; proxy_set_header Host $host; @@ -134,20 +118,15 @@ http { } EOF -# 设置权限 RUN chown -R vocata:vocata /usr/share/nginx/html /etc/nginx -# 切换到非root用户 USER vocata -# 暴露端口 EXPOSE 8080 -# 健康检查 HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \ CMD curl -f http://localhost:8080/health || exit 1 -# 构建参数和标签 ARG BUILD_DATE ARG VERSION="1.0.0" LABEL maintainer="VocaTa Team " \ @@ -161,6 +140,5 @@ LABEL maintainer="VocaTa Team " \ org.opencontainers.image.version="${VERSION}" \ org.opencontainers.image.created="${BUILD_DATE}" -# 启动Nginx ENTRYPOINT ["dumb-init", "--"] -CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file +CMD ["nginx", "-g", "daemon off;"] From 4bd26c3f31fee4c24901ec587f3a538ffb64a545 Mon Sep 17 00:00:00 2001 From: ailuckly Date: Tue, 31 Mar 2026 22:11:19 +0800 Subject: [PATCH 2/2] fix: resolve frontend ci failures --- .github/workflows/ci.yml | 5 +- vocata-admin/src/api/modules/role.ts | 6 +- vocata-admin/src/api/modules/user.ts | 29 +++++++-- vocata-admin/src/layouts/MenuCom.vue | 10 +--- vocata-admin/src/layouts/TabBar.vue | 6 +- vocata-admin/src/router/routes.ts | 4 +- vocata-admin/src/types/api.ts | 31 +++++++++- vocata-admin/src/views/RolePage.vue | 31 +++++++--- vocata-admin/src/views/UserPage.vue | 53 ++++++----------- vocata-admin/src/views/passport/LoginPage.vue | 5 +- vocata-web/src/api/modules/role.ts | 31 +++++++--- vocata-web/src/layouts/SliderBar.vue | 27 +++------ vocata-web/src/types/api.ts | 36 ++++++++++- vocata-web/src/types/common.ts | 8 +-- vocata-web/src/utils/aiChat.ts | 51 ++++++++++------ vocata-web/src/views/ChatPage.vue | 59 +++---------------- vocata-web/src/views/LoginPage.vue | 8 +-- vocata-web/src/views/NewRole.vue | 9 +-- vocata-web/src/views/SearchRole.vue | 5 +- .../src/views/components/RoleDialog.vue | 11 ++-- 20 files changed, 232 insertions(+), 193 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 491abd0..f80c278 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,13 +34,10 @@ jobs: server: - 'vocata-server/**' - 'pom.xml' - - '.github/workflows/**' web: - 'vocata-web/**' - - '.github/workflows/**' admin: - 'vocata-admin/**' - - '.github/workflows/**' # 后端服务CI backend-ci: @@ -200,4 +197,4 @@ jobs: else echo "❌ **CI检查失败**,请修复后重新提交" >> $GITHUB_STEP_SUMMARY exit 1 - fi \ No newline at end of file + fi diff --git a/vocata-admin/src/api/modules/role.ts b/vocata-admin/src/api/modules/role.ts index a424b3d..60afa91 100644 --- a/vocata-admin/src/api/modules/role.ts +++ b/vocata-admin/src/api/modules/role.ts @@ -1,6 +1,8 @@ import request from '../request' +type RoleListQuery = Record + export const roleApi = { //增 @@ -15,11 +17,11 @@ export const roleApi = { //改 //查 - getRoleList(params: any) { + getRoleList(params: RoleListQuery) { return request({ url: '/api/admin/character', method: 'get', params }) } -} \ No newline at end of file +} diff --git a/vocata-admin/src/api/modules/user.ts b/vocata-admin/src/api/modules/user.ts index 8893f7a..0e50482 100644 --- a/vocata-admin/src/api/modules/user.ts +++ b/vocata-admin/src/api/modules/user.ts @@ -1,6 +1,23 @@ -import type { LoginParams, RegisterParams, Response, LoginResponse } from '@/types/api' +import type { + AdminProfileResponse, + AdminUserInfo, + LoginParams, + LoginResponse, + PaginatedList, + RegisterParams, + Response, +} from '@/types/api' import request from '../request' +type UserListQuery = { + pageNum: number + pageSize: number +} + +type UpdateStatusParams = { + status: number +} + export const userApi = { // 登录 login(params: LoginParams): Promise> { @@ -20,17 +37,17 @@ export const userApi = { return request.post('/api/client/auth/logout') }, // 获取用户信息 - getUserInfo(params): Promise> { - return request.get('/api/admin/user/list', params) + getUserInfo(params: UserListQuery): Promise>> { + return request.get('/api/admin/user/list', { params }) }, // 修改用户状态 - updateUserStatus(id, params): Promise> { + updateUserStatus(id: number, params: UpdateStatusParams): Promise> { return request.put(`/api/admin/user/${id}/status`, params) }, // 获取管理员信息 - getAdminInfo(): Promise> { + getAdminInfo(): Promise> { return request.get('/api/admin/auth/current') }, -} \ No newline at end of file +} diff --git a/vocata-admin/src/layouts/MenuCom.vue b/vocata-admin/src/layouts/MenuCom.vue index fafbb20..c91674f 100644 --- a/vocata-admin/src/layouts/MenuCom.vue +++ b/vocata-admin/src/layouts/MenuCom.vue @@ -38,7 +38,7 @@ {{ item.meta.title }} - + @@ -49,16 +49,10 @@ import router from '@/router' defineProps(['isCollapse', 'menuList']) -const goRoute = (vc) => { +const goRoute = (vc: { index: string }) => { router.push(vc.index) console.log(vc.index) } - - diff --git a/vocata-admin/src/layouts/TabBar.vue b/vocata-admin/src/layouts/TabBar.vue index 26e39e3..0fa86a3 100644 --- a/vocata-admin/src/layouts/TabBar.vue +++ b/vocata-admin/src/layouts/TabBar.vue @@ -42,11 +42,11 @@ import { userApi } from '@/api/modules/user' import { removeToken } from '@/utils/token' import { ElMessage } from 'element-plus' -import { onMounted, ref } from 'vue' +import { ref } from 'vue' import { useRouter } from 'vue-router' const fullscreenLoading = ref(false) const props = defineProps(['modelValue']) -const emit = defineEmits(['update:modelValue']) +const emit = defineEmits(['update:modelValue', 'refresh']) const showPop = ref(false) const router = useRouter() @@ -62,7 +62,7 @@ const toggleCollapse = () => { const getUserInfo = async () => { try { const res = await userApi.getAdminInfo() - if (res.code === 200) { + if (res.code === 200 && res.data) { userInfo.value = res.data.user } console.log(userInfo.value) diff --git a/vocata-admin/src/router/routes.ts b/vocata-admin/src/router/routes.ts index ef57814..2029772 100644 --- a/vocata-admin/src/router/routes.ts +++ b/vocata-admin/src/router/routes.ts @@ -52,8 +52,6 @@ const routes: RouteRecordRaw[] = [ { path: '/', name: 'Root', - component: () => import('@/layouts/BasicLayout.vue'), - meta: { title: '首页', hidden: true }, redirect: '/role/roles' }, // 404页面 @@ -64,4 +62,4 @@ const routes: RouteRecordRaw[] = [ meta: { title: '页面不存在', hidden: true } }, ] -export default routes \ No newline at end of file +export default routes diff --git a/vocata-admin/src/types/api.ts b/vocata-admin/src/types/api.ts index dc327f0..cef2576 100644 --- a/vocata-admin/src/types/api.ts +++ b/vocata-admin/src/types/api.ts @@ -34,7 +34,10 @@ export interface RegisterParams { // 登录响应数据 export interface LoginResponse { token: string, - expiresIn: number + expiresIn: number, + user?: { + nickname?: string + } } // 修改密码参数 @@ -48,4 +51,28 @@ export interface Response { code: number, message: string, data: T -} \ No newline at end of file +} + +export interface AdminUserInfo { + id: number + username: string + email: string + nickname: string + avatar: string + gender: number + birthday?: string + createDate: string + status: number +} + +export interface PaginatedList { + list: T[] + total: number +} + +export interface AdminProfileResponse { + user: { + nickname: string + avatar: string + } +} diff --git a/vocata-admin/src/views/RolePage.vue b/vocata-admin/src/views/RolePage.vue index 239ddeb..6e6c8e9 100644 --- a/vocata-admin/src/views/RolePage.vue +++ b/vocata-admin/src/views/RolePage.vue @@ -84,11 +84,21 @@ import { roleApi } from '@/api/modules/role' import { ElMessage } from 'element-plus' import { onMounted, ref, watch } from 'vue' + +type RoleRow = { + id: number + name: string + avatarUrl?: string + tags?: string + userType?: string | number + [key: string]: unknown +} + const dialogVisible = ref(false) const dialogType = ref('add') // 'add' 或 'edit' -const users = ref([]) -const formData = ref({ - id: '', +const users = ref([]) +const formData = ref({ + id: 0, name: '', }) const query = ref({ @@ -101,7 +111,7 @@ const getRoles = async () => { const res = await roleApi.getRoleList(query.value) users.value = res.data.list total.value = res.data.total - } catch (error) { + } catch { ElMessage.error('获取数据失败') } } @@ -117,10 +127,13 @@ watch( const handleAddUser = () => { dialogType.value = 'add' dialogVisible.value = true - formData.value = {} + formData.value = { + id: 0, + name: '', + } } // 编辑用户 -const handleEditUser = (user) => { +const handleEditUser = (user: RoleRow) => { dialogType.value = 'edit' dialogVisible.value = true const userType = user.userType == '普通用户' ? 0 : 1 @@ -144,18 +157,18 @@ const confirm = async () => { // ElMessage.success('修改成功') // getUsers() // dialogVisible.value = false - } catch (error) { + } catch { ElMessage.success('修改失败') dialogVisible.value = false } } } -const deleteUser = async (id) => { +const deleteUser = async (id: number) => { try { await roleApi.deleteRole(id) ElMessage.success('删除成功') getRoles() - } catch (error) { + } catch { ElMessage.error('删除数据失败') } } diff --git a/vocata-admin/src/views/UserPage.vue b/vocata-admin/src/views/UserPage.vue index a74ebc0..a8142ec 100644 --- a/vocata-admin/src/views/UserPage.vue +++ b/vocata-admin/src/views/UserPage.vue @@ -102,15 +102,28 @@ import { userApi } from '@/api/modules/user' import { ElMessage } from 'element-plus' import { onMounted, ref } from 'vue' + +type UserRow = { + id: number + username: string + email: string + nickname: string + avatar: string + gender: number + birthday?: string + createDate: string + status: number +} + const dialogVisible = ref(false) const dialogType = ref('add') // 'add' 或 'edit' -const users = ref([]) +const users = ref([]) const formData = ref({ userId: '', userAccount: '', userPassword: '', userPhone: '', - userType: '', + userType: 0, sellerName: '', status: '', }) @@ -124,31 +137,11 @@ const getUsers = async () => { const res = await userApi.getUserInfo(query.value) users.value = res.data.list total.value = res.data.total - } catch (error) { + } catch { ElMessage.error('获取数据失败') } } -// 新增用户 -const handleAddUser = () => { - dialogType.value = 'add' - dialogVisible.value = true - formData.value = { - userId: '', - userAccount: '', - userPassword: '', - userPhone: '', - userType: 0, - sellerName: '', - } -} -// 编辑用户 -const handleEditUser = (user) => { - dialogType.value = 'edit' - dialogVisible.value = true - const userType = user.userType == '普通用户' ? 0 : 1 - formData.value = { ...user, userType } -} const confirm = async () => { if (!formData.value.userId) { try { @@ -167,7 +160,7 @@ const confirm = async () => { // ElMessage.success('修改成功') // getUsers() // dialogVisible.value = false - } catch (error) { + } catch { ElMessage.success('修改失败') dialogVisible.value = false } @@ -175,23 +168,15 @@ const confirm = async () => { } // 修改用户状态 -const updateStatus = async (id, status) => { +const updateStatus = async (id: number, status: number) => { try { await userApi.updateUserStatus(id, { status }) ElMessage.success('修改成功') getUsers() - } catch (error) { + } catch { ElMessage.error('修改失败') } } -const deleteUser = async (id) => { - try { - ElMessage.success('删除成功') - getUsers() - } catch (error) { - ElMessage.error('删除数据失败') - } -} onMounted(() => { getUsers() }) diff --git a/vocata-admin/src/views/passport/LoginPage.vue b/vocata-admin/src/views/passport/LoginPage.vue index a6d8351..6b91c05 100644 --- a/vocata-admin/src/views/passport/LoginPage.vue +++ b/vocata-admin/src/views/passport/LoginPage.vue @@ -46,9 +46,8 @@ import { userApi } from '@/api/modules/user' import { setToken } from '@/utils/token' import { ElMessage, ElNotification } from 'element-plus' import { reactive, ref } from 'vue' -import { useRoute, useRouter } from 'vue-router' +import { useRouter } from 'vue-router' const router = useRouter() -const route = useRoute() const btnState = ref(false) const apiForm = reactive({ loginName: '', @@ -81,7 +80,7 @@ const login = async () => { if (res.message == '登录成功') { ElNotification({ title: '登录成功', - message: '欢迎用户,' + res.data.user.nickname + '!', + message: '欢迎用户,' + (res.data.user?.nickname || apiForm.loginName) + '!', type: 'success', }) setToken(res.data.token, res.data.expiresIn) diff --git a/vocata-web/src/api/modules/role.ts b/vocata-web/src/api/modules/role.ts index 558c6da..d1f58f9 100644 --- a/vocata-web/src/api/modules/role.ts +++ b/vocata-web/src/api/modules/role.ts @@ -1,9 +1,20 @@ import request from '../request' -import type { PublicRoleQuery } from '@/types/api' +import type { + AiGenerateRoleRequest, + AiGenerateRoleResponse, + CreateCharacterRequest, + CreateCharacterResponse, + PublicRoleQuery, + Response, + TtsVoiceOption, +} from '@/types/api' +import type { roleInfo } from '@/types/common' + +type RolePayload = Record export const roleApi = { // 获取公开角色列表 - getPublicRoleList(params: PublicRoleQuery) { + getPublicRoleList(params: PublicRoleQuery): Promise> { return request({ url: '/api/open/character/list', method: 'get', @@ -11,7 +22,7 @@ export const roleApi = { }) }, // 获取精选角色列表 - getChoiceRoleList(params: { limit: number }) { + getChoiceRoleList(params: { limit: number }): Promise> { return request({ url: '/api/open/character/featured', method: 'get', @@ -19,7 +30,7 @@ export const roleApi = { }) }, // 搜索角色 - searchRole(params: { keyword: string }) { + searchRole(params: { keyword: string }): Promise> { return request({ url: '/api/open/character/search', method: 'get', @@ -27,7 +38,9 @@ export const roleApi = { }) }, // 获取我的角色列表 - getMyRoleList(params?: any) { + getMyRoleList( + params?: RolePayload + ): Promise> { return request({ url: '/api/client/character/my', method: 'get', @@ -35,7 +48,7 @@ export const roleApi = { }) }, // 创建角色 - createRole(data: any) { + createRole(data: CreateCharacterRequest): Promise> { return request({ url: '/api/client/character', method: 'post', @@ -43,21 +56,21 @@ export const roleApi = { }) }, // 获取音色列表 - getSoundList() { + getSoundList(): Promise> { return request({ url: '/api/client/tts-voice/list', method: 'get' }) }, // 获取角色详情 - getCharacterDetail(id: string | number) { + getCharacterDetail(id: string | number): Promise> { return request({ url: `/api/open/character/${id}`, method: 'get' }) }, // AI生成角色提示词 - aiGenerate(data: { name: string; description: string; greeting: string }) { + aiGenerate(data: AiGenerateRoleRequest): Promise> { return request({ url: '/api/client/character/ai-generate', method: 'post', diff --git a/vocata-web/src/layouts/SliderBar.vue b/vocata-web/src/layouts/SliderBar.vue index 17f3a8e..e476b30 100644 --- a/vocata-web/src/layouts/SliderBar.vue +++ b/vocata-web/src/layouts/SliderBar.vue @@ -24,7 +24,7 @@ class="role-btn" @click="createNewRole" aria-label="新建角色" -:class="route.meta.title === '新建角色' && route.meta.title !== '对话' ? 'active' : ''" + :class="String(route.meta.title) === '新建角色' ? 'active' : ''" >
@@ -35,7 +35,7 @@ class="role-btn" @click="showRoleGallery" aria-label="选择角色" -:class="route.meta.title === '探索' && route.meta.title !== '对话' ? 'active' : ''" + :class="String(route.meta.title) === '探索' ? 'active' : ''" >
@@ -163,7 +163,7 @@ import { userApi } from '@/api/modules/user' import { conversationApi } from '@/api/modules/conversation' import { isMobile } from '@/utils/isMobile' import { removeToken } from '@/utils/token' -import { ElMessage, ElMessageBox } from 'element-plus' +import { ElInput, ElMessage, ElMessageBox } from 'element-plus' import { computed, nextTick, onBeforeUnmount, onMounted, ref, useTemplateRef, watch } from 'vue' import { useRoute, useRouter } from 'vue-router' // import type { ChatHistoryItem } from '@/types/common' @@ -193,7 +193,7 @@ const userInfo = ref({ const searchText = ref('') const searchInput = ref() const userMenu = useTemplateRef('userMenu') -const editInput = useTemplateRef('editInput') +const editInput = useTemplateRef[]>('editInput') const showUserMenu = ref(false) // 编辑相关状态 @@ -312,26 +312,15 @@ const handleChatAction = async (command: { action: string; chatId: string; title } } -// 删除对话 -const deleteChat = async (conversationUuid: string, event: Event) => { - event.stopPropagation() // 阻止事件冒泡 - - try { - await chatHistoryStore().deleteChatHistory(conversationUuid) - } catch (error) { - console.error('删除对话失败:', error) - ElMessage.error('删除对话失败') - } -} - // 开始编辑标题 const startEditTitle = (conversationUuid: string, currentTitle: string) => { editingChatId.value = conversationUuid editingTitle.value = currentTitle nextTick(() => { - if (editInput.value) { - editInput.value.focus() - editInput.value.select() + const input = editInput.value?.[0] + if (input) { + input.focus() + input.select() } }) } diff --git a/vocata-web/src/types/api.ts b/vocata-web/src/types/api.ts index f044e0e..33062f4 100644 --- a/vocata-web/src/types/api.ts +++ b/vocata-web/src/types/api.ts @@ -34,7 +34,39 @@ export interface RegisterParams { // 登录响应数据 export interface LoginResponse { token: string, - expiresIn: number + expiresIn: number, + user?: { + nickname?: string + } +} + +export interface CreateCharacterRequest { + name: string + description: string + greeting: string + isPublic: boolean + persona: string + voiceId: string + avatarUrl: string +} + +export interface CreateCharacterResponse { + id: string | number +} + +export interface TtsVoiceOption { + name: string + [key: string]: unknown +} + +export interface AiGenerateRoleRequest { + name: string + description: string + greeting: string +} + +export interface AiGenerateRoleResponse { + persona: string } // 用户信息响应数据 @@ -108,6 +140,6 @@ export interface MessageResponse { audioUrl: string | null, llmModelId: string, ttsVoiceId: string | null, - metadata: Record, + metadata: Record, createDate: string } diff --git a/vocata-web/src/types/common.ts b/vocata-web/src/types/common.ts index 0ef20ed..80489e4 100644 --- a/vocata-web/src/types/common.ts +++ b/vocata-web/src/types/common.ts @@ -1,11 +1,11 @@ export interface roleInfo { - "id"?: number, + "id": number, "characterCode"?: string, "name"?: string, "description"?: string, "greeting"?: string, "avatarUrl"?: string, - "tags"?: string, + "tags"?: string | string[], "language"?: string, "status"?: number, "statusName"?: string, @@ -42,10 +42,10 @@ export interface ChatMessage { contentType?: number, // 1=文本, 2=语音, 3=图片, 4=音频 audioUrl?: string | null, createDate?: string, - metadata?: Record, + metadata?: Record, // AI对话系统新增字段 isStreaming?: boolean, // 是否为流式显示中的消息 isRecognizing?: boolean, // 是否为语音识别中的消息 characterName?: string, // AI角色名称 confidence?: number // 语音识别置信度 -} \ No newline at end of file +} diff --git a/vocata-web/src/utils/aiChat.ts b/vocata-web/src/utils/aiChat.ts index e92e069..0a26962 100644 --- a/vocata-web/src/utils/aiChat.ts +++ b/vocata-web/src/utils/aiChat.ts @@ -5,10 +5,16 @@ import { getToken } from './token' +type EventCallback = (data?: unknown) => void + +interface WindowWithWebkitAudio extends Window { + webkitAudioContext?: typeof AudioContext +} + // WebSocket消息类型定义 -interface WebSocketMessage { +export interface WebSocketMessage { type: string - [key: string]: any + [key: string]: unknown } interface STTResultMessage extends WebSocketMessage { @@ -64,7 +70,7 @@ export class VocaTaWebSocketClient { private conversationUuid: string private reconnectAttempts = 0 private readonly maxReconnectAttempts = 5 - private callbacks: Map = new Map() + private callbacks: Map = new Map() private manualClose = false constructor(conversationUuid: string) { @@ -135,7 +141,7 @@ export class VocaTaWebSocketClient { const message: WebSocketMessage = JSON.parse(event.data) console.log(`📨 收到消息:`, message) this.emit('message', message) - } catch (e) { + } catch { console.error('❌ 解析消息失败:', event.data) } } @@ -207,14 +213,14 @@ export class VocaTaWebSocketClient { } // 事件监听器 - on(event: string, callback: Function): void { + on(event: string, callback: (data: T) => void): void { if (!this.callbacks.has(event)) { this.callbacks.set(event, []) } - this.callbacks.get(event)?.push(callback) + this.callbacks.get(event)?.push(callback as EventCallback) } - private emit(event: string, data?: any): void { + private emit(event: string, data?: unknown): void { const callbacks = this.callbacks.get(event) if (callbacks) { callbacks.forEach(callback => callback(data)) @@ -272,7 +278,7 @@ export class AudioManager { private currentWsClient: VocaTaWebSocketClient | null = null private stopRecordingPromise: Promise | null = null private stopRecordingResolve?: () => void - private stopRecordingReject?: (reason?: any) => void + private stopRecordingReject?: (reason?: unknown) => void private playbackStateListener?: (isPlaying: boolean) => void async initialize(): Promise { @@ -298,7 +304,12 @@ export class AudioManager { private async ensureAudioContext(): Promise { if (!this.audioContext) { console.log('🎵 延迟初始化音频上下文...') - this.audioContext = new (window.AudioContext || (window as any).webkitAudioContext)() + const AudioContextConstructor = + window.AudioContext || (window as WindowWithWebkitAudio).webkitAudioContext + if (!AudioContextConstructor) { + throw new Error('当前浏览器不支持 AudioContext') + } + this.audioContext = new AudioContextConstructor() // 检查音频上下文状态 if (this.audioContext.state === 'suspended') { @@ -613,8 +624,6 @@ export class VocaTaAIChat { private wsClient: VocaTaWebSocketClient | null = null private audioManager: AudioManager private isAudioCallActive = false - private currentConversation: any = null - private currentCharacter: any = null private conversationUuid: string | null = null private connectingPromise: Promise | null = null @@ -623,7 +632,7 @@ export class VocaTaAIChat { private currentSTTText = '' // 回调函数 - private onMessageCallback?: (message: any) => void + private onMessageCallback?: (message: WebSocketMessage) => void private onSTTResultCallback?: (text: string, isFinal: boolean) => void private onLLMStreamCallback?: (text: string, isComplete: boolean, characterName?: string) => void private onAudioPlayCallback?: (isPlaying: boolean) => void @@ -684,8 +693,12 @@ export class VocaTaAIChat { this.handleWebSocketMessage(message) // 如果收到状态消息表示连接已建立,则resolve - if (!connectionResolved && message.type === 'status' && - (message.message?.includes('连接已建立') || message.message?.includes('WebSocket连接已建立'))) { + const statusMessage = typeof message.message === 'string' ? message.message : '' + if ( + !connectionResolved && + message.type === 'status' && + (statusMessage.includes('连接已建立') || statusMessage.includes('WebSocket连接已建立')) + ) { console.log('🎉 收到服务器连接确认,连接完全建立') connectionResolved = true this.onConnectionStatusCallback?.('connected', 'WebSocket连接已建立') @@ -707,12 +720,12 @@ export class VocaTaAIChat { this.handleAudioData(audioBuffer) }) - this.wsClient.on('error', (error: any) => { + this.wsClient.on('error', (error) => { console.error('❌ WebSocket错误:', error) this.onConnectionStatusCallback?.('error', 'WebSocket连接错误') if (!connectionResolved) { connectionResolved = true - reject(error) + reject(error instanceof Error ? error : new Error('WebSocket连接错误')) finalize() } }) @@ -909,7 +922,7 @@ export class VocaTaAIChat { } // 设置回调函数 - onMessage(callback: (message: any) => void): void { + onMessage(callback: (message: WebSocketMessage) => void): void { this.onMessageCallback = callback } @@ -953,6 +966,10 @@ export class VocaTaAIChat { return this.audioManager.playing } + get voiceActive(): boolean { + return this.audioManager.recording + } + // 清理资源 destroy(): void { console.log('🧹 清理AI对话系统资源') diff --git a/vocata-web/src/views/ChatPage.vue b/vocata-web/src/views/ChatPage.vue index d9921ab..83bf9fd 100644 --- a/vocata-web/src/views/ChatPage.vue +++ b/vocata-web/src/views/ChatPage.vue @@ -153,7 +153,7 @@ import { ElMessage } from 'element-plus' import { computed, onMounted, onUnmounted, ref, watch, nextTick } from 'vue' import { useRoute, useRouter } from 'vue-router' import type { ChatMessage } from '@/types/common' -import type { MessageResponse } from '@/types/api' +import type { ConversationResponse, MessageResponse } from '@/types/api' import { VocaTaAIChat } from '@/utils/aiChat' import { getToken } from '@/utils/token' @@ -166,7 +166,7 @@ const input = ref('') const router = useRouter() const route = useRoute() const conversationUuid = computed(() => route.params.conversationUuid as string) -const currentConversation = ref(null) +const currentConversation = ref(null) const userAvatar = ref('') const userNickname = ref('') @@ -345,7 +345,7 @@ const loadRecentMessages = async (limit: number = 20) => { senderType: 2, contentType: 1, createDate: new Date().toISOString(), - characterName: currentConversation.value.characterName, + characterName: currentConversation.value?.characterName || getCharacterName(), metadata: { isGreeting: true } }] } @@ -371,40 +371,6 @@ const loadRecentMessages = async (limit: number = 20) => { } } -// 加载更多历史消息 -const loadMoreHistory = async (limit: number = 20) => { - if (!conversationUuid.value || isLoadingMessages.value || !hasMoreHistory.value) return - - try { - isLoadingMessages.value = true - const res = await conversationApi.getHistoryMessages( - conversationUuid.value, - currentOffset.value, - limit - ) - if (res.code === 200) { - if (res.data.length === 0) { - hasMoreHistory.value = false - return - } - - const messages = convertMessagesToChatFormat(res.data) - // 将历史消息添加到列表开头 - chats.value = [...messages.reverse(), ...chats.value] - currentOffset.value += res.data.length - - if (res.data.length < limit) { - hasMoreHistory.value = false - } - } - } catch (error) { - console.error('加载历史消息失败:', error) - ElMessage.error('加载历史消息失败') - } finally { - isLoadingMessages.value = false - } -} - // 将后端消息转换为前端所需的格式 const convertMessagesToChatFormat = (messages: MessageResponse[]): ChatMessage[] => { return messages.map(msg => ({ @@ -471,13 +437,11 @@ const loadConversationInfo = async () => { const res = await conversationApi.getConversationList() if (res.code === 200) { // 从最新的对话列表中查找当前对话 - const conversation = res.data.find( - (conv: any) => conv.conversationUuid === conversationUuid.value - ) + const conversation = res.data.find((conv) => conv.conversationUuid === conversationUuid.value) if (!conversation) { console.warn('⚠️ 在对话列表中找不到当前对话UUID:', conversationUuid.value) - console.log('📋 可用的对话列表:', res.data.map((c: any) => ({ + console.log('📋 可用的对话列表:', res.data.map((c) => ({ uuid: c.conversationUuid, title: c.title, characterName: c.characterName @@ -574,7 +538,7 @@ const setupAIChatCallbacks = () => { if (!aiChat.value) return // 连接状态回调 - aiChat.value.onConnectionStatus((status, message) => { + aiChat.value.onConnectionStatus((status) => { switch (status) { case 'connected': connectionStatus.value = '已连接到AI服务' @@ -954,11 +918,6 @@ const scrollToBottomWithRetry = (maxRetries: number = 3) => { }) } -// 兼容旧的滚动函数 -const scrollToBottom = () => { - scrollToBottomWithRetry() -} - // VAD监控相关函数 const startVADMonitoring = () => { if (vadCheckInterval.value) { @@ -966,11 +925,7 @@ const startVADMonitoring = () => { } vadCheckInterval.value = window.setInterval(() => { - // 检查aiChat的audioManager是否有VAD状态 - if (aiChat.value && (aiChat.value as any).audioManager) { - const audioManager = (aiChat.value as any).audioManager - vadActive.value = audioManager.voiceActive || false - } + vadActive.value = aiChat.value?.voiceActive ?? false }, 100) // 每100ms检查一次VAD状态 } diff --git a/vocata-web/src/views/LoginPage.vue b/vocata-web/src/views/LoginPage.vue index 570484a..253d380 100644 --- a/vocata-web/src/views/LoginPage.vue +++ b/vocata-web/src/views/LoginPage.vue @@ -44,7 +44,7 @@ @@ -165,7 +165,7 @@ import type { roleInfo } from '@/types/common' import debounce from '@/types/debounce' import { isMobile } from '@/utils/isMobile' import { ElMessage } from 'element-plus' -import { computed, onMounted, ref, watch, watchEffect } from 'vue' +import { computed, onMounted, ref, watch } from 'vue' import type { Ref } from 'vue' import { useRouter } from 'vue-router' import { chatHistoryStore } from '@/store' @@ -178,7 +178,6 @@ const roleSelected = ref() const searchInput = ref('') const roleList: Ref = ref([]) const selectRoleList: Ref = ref([]) -const myRoleList: Ref = ref([]) const cardFace = ref([0, 0, 0, 0, 0]) const searchParam: Ref = ref({ pageNum: 1, diff --git a/vocata-web/src/views/components/RoleDialog.vue b/vocata-web/src/views/components/RoleDialog.vue index a7055df..5fdecca 100644 --- a/vocata-web/src/views/components/RoleDialog.vue +++ b/vocata-web/src/views/components/RoleDialog.vue @@ -41,15 +41,16 @@