diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..e6c4d5a --- /dev/null +++ b/.env.example @@ -0,0 +1,97 @@ +# ================================ +# VocaTa Docker 开发环境模板 +# 复制为 `.env` 后,再按本地实际情况补齐第三方配置 +# ================================ + +# Compose 项目标识 +COMPOSE_PROJECT_NAME=vocata-dev + +# 本地暴露端口 +POSTGRES_PORT=55433 +REDIS_EXPOSE_PORT=6380 +SERVER_PORT=9009 +WEB_PORT=3000 +ADMIN_PORT=3001 +PGADMIN_PORT=5050 +MAILHOG_SMTP_PORT=1025 +MAILHOG_WEB_PORT=8025 + +# Spring Profile +SPRING_PROFILES_ACTIVE=local + +# PostgreSQL +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 + +# 前端构建参数 +VITE_APP_URL=http://localhost:9009 +WEB_BUILD_MODE=production +ADMIN_BUILD_MODE=production + +# 邮件 +MAIL_USERNAME= +MAIL_PASSWORD= + +# 科大讯飞 TTS +XUNFEI_TTS_APP_ID= +XUNFEI_TTS_API_KEY= +XUNFEI_TTS_SECRET_KEY= + +# AI provider 选择 +AI_LLM_PROVIDER=qiniu +AI_STT_PROVIDER=qiniu +AI_TTS_PROVIDER=xunfei + +# 七牛云对象存储 +QINIU_ACCESS_KEY= +QINIU_SECRET_KEY= +QINIU_BUCKET= +QINIU_DOMAIN= +QINIU_REGION=huadong +QINIU_KEY_PREFIX=Vocata + +# 七牛云 AI +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 +QINIU_STT_ENDPOINT=https://openai.qiniu.com/v1 +QINIU_STT_MODEL=asr + + +# Gemini +GEMINI_API_KEY= +GEMINI_BASE_URL=https://generativelanguage.googleapis.com +GEMINI_MODEL=gemini-2.5-flash-lite +GEMINI_TIMEOUT=60 + + +# OpenAI +OPENAI_API_KEY= +OPENAI_BASE_URL=https://api.openai.com +OPENAI_MODEL=gpt-3.5-turbo +OPENAI_TIMEOUT=60 + + +# SiliconFlow +SILICONFLOW_API_KEY= +SILICONFLOW_BASE_URL=https://api.siliconflow.cn/v1 +SILICONFLOW_AI_MODEL=Qwen/Qwen3-8B +SILICONFLOW_TIMEOUT=60 + +# 可选开发工具 +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..c334e86 100644 --- a/.github/workflows/cd-staging.yml +++ b/.github/workflows/cd-staging.yml @@ -2,547 +2,124 @@ 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: 准备部署配置 + - name: 准备 SSH 密钥 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" + install -m 600 /dev/null "$RUNNER_TEMP/staging_key" + printf '%s\n' '${{ secrets.STAGING_SSH_KEY }}' > "$RUNNER_TEMP/staging_key" - # 应用配置 - 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 + - name: 部署到服务器 + run: | + ssh -i "$RUNNER_TEMP/staging_key" \ + -o StrictHostKeyChecking=no \ + -o UserKnownHostsFile=/dev/null \ + ${{ secrets.STAGING_USER }}@${{ secrets.STAGING_HOST }} <<'EOF' set -e - echo "=== VocaTa 测试环境部署开始 ===" - echo "部署标签: $DEPLOY_TAG" - echo "提交版本: $DEPLOY_COMMIT" - - # 加载环境变量 - source .env + 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 + GIT_REF="${{ github.ref_name }}" + GIT_SHA="${{ github.sha }}" + REPO_URL="https://github.com/${{ github.repository }}.git" - # 登录 Docker Registry - echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u "veardk" --password-stdin + mkdir -p "$APP_DIR" - # 确定需要更新的服务 - 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" + if [ ! -f "$ENV_FILE" ] && [ -f "$LEGACY_ENV_FILE" ]; then + cp "$LEGACY_ENV_FILE" "$ENV_FILE" fi - if [[ "${{ needs.detect-changes.outputs.admin-changed }}" == "true" || "${{ github.event.inputs.force_rebuild }}" == "true" ]]; then - SERVICES_TO_UPDATE+=("vocata-admin") - echo "✓ 将更新管理后台: $ADMIN_IMAGE" + if [ ! -f "$ENV_FILE" ]; then + echo "缺少部署环境文件: $ENV_FILE" + echo "请先在服务器上准备好 .env,再重新执行工作流。" + exit 1 fi - if [ ${#SERVICES_TO_UPDATE[@]} -eq 0 ]; then - echo "⚠ 没有服务需要更新" - exit 0 + if [ ! -d "$REPO_DIR/.git" ]; then + echo "[1/4] clone fresh repository" + rm -rf "$REPO_DIR" + git clone --branch "$GIT_REF" --depth 1 "$REPO_URL" "$REPO_DIR" + else + echo "[1/4] update repository" + 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 "📥 拉取镜像..." - 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 + cd "$REPO_DIR" + git remote set-url origin "$REPO_URL" - # 健康检查 - if [[ "$SKIP_HEALTH_CHECK" != "true" ]]; then - echo "🏥 健康检查..." + echo "[2/4] deploy compose stack" + echo "当前提交: $GIT_SHA" + docker compose --env-file "$ENV_FILE" up -d --build --wait - health_check() { - local service=$1 - local port=$2 - local path=${3:-"/"} - local max_attempts=6 # 减少从12次到6次 - local attempt=1 + echo "[3/4] inspect compose status" + docker compose --env-file "$ENV_FILE" ps - 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 - - echo "✗ $service 健康检查失败" - docker-compose logs --tail=20 $service # 减少日志行数从50到20 - return 1 - } - - # 执行健康检查 - HEALTH_FAILED=false - - 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 + echo "[4/4] check backend health" + curl -fsS http://127.0.0.1:9009/api/health + EOF - if [[ "$HEALTH_FAILED" == "true" ]]; then - echo "❌ 部署失败: 健康检查未通过" - echo "正在回滚..." - docker-compose restart "${SERVICES_TO_UPDATE[@]}" - exit 1 - fi - else - echo "⚡ 跳过健康检查(快速部署模式)" - echo "等待5秒后继续..." - sleep 5 + - name: 失败时输出日志 + if: failure() + run: | + ssh -i "$RUNNER_TEMP/staging_key" \ + -o StrictHostKeyChecking=no \ + -o UserKnownHostsFile=/dev/null \ + ${{ secrets.STAGING_USER }}@${{ secrets.STAGING_HOST }} <<'EOF' + APP_DIR=/home/deploy/deploy/vocata + REPO_DIR=$APP_DIR/repo + ENV_FILE=$APP_DIR/.env + + if [ -d "$REPO_DIR" ]; then + cd "$REPO_DIR" + echo "=== docker compose ps ===" + docker compose --env-file "$ENV_FILE" ps || true + echo "=== vocata-server logs ===" + docker compose --env-file "$ENV_FILE" logs --tail=100 vocata-server || true + echo "=== vocata-web logs ===" + docker compose --env-file "$ENV_FILE" logs --tail=60 vocata-web || true + echo "=== vocata-admin logs ===" + docker compose --env-file "$ENV_FILE" logs --tail=60 vocata-admin || true fi - - # 清理旧镜像 - echo "🧹 清理旧镜像..." - docker system prune -f || true - - echo "🎉 测试环境部署成功!" - echo "更新的服务: ${SERVICES_TO_UPDATE[*]}" EOF - chmod +x deploy/staging/deploy.sh - - - 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 - - - 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 - - - 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: | - cd ~/deploy/vocata - ls -la - chmod +x deploy.sh - ./deploy.sh - - 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/ci.yml b/.github/workflows/ci.yml index 491abd0..b621ecd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,22 +34,16 @@ jobs: server: - 'vocata-server/**' - 'pom.xml' - - '.github/workflows/**' web: - 'vocata-web/**' - - '.github/workflows/**' admin: - 'vocata-admin/**' - - '.github/workflows/**' # 后端服务CI backend-ci: runs-on: ubuntu-latest needs: detect-changes if: needs.detect-changes.outputs.server-changed == 'true' - defaults: - run: - working-directory: ./vocata-server steps: - name: 检出代码 @@ -64,28 +58,18 @@ jobs: - name: 缓存 Maven 依赖 uses: actions/cache@v4 with: - path: ~/.m2 + path: /tmp/juhao_m2repo key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} restore-keys: ${{ runner.os }}-m2 - - name: 编译项目 - run: mvn clean compile -DskipTests - - - name: 运行单元测试 - run: mvn test - continue-on-error: true - - - name: 构建 JAR 包 - run: mvn package -DskipTests + - name: 运行后端验证脚本 + run: ./scripts/validate-backend.sh # 前端客户端CI frontend-web-ci: runs-on: ubuntu-latest needs: detect-changes if: needs.detect-changes.outputs.web-changed == 'true' - defaults: - run: - working-directory: ./vocata-web steps: - name: 检出代码 @@ -101,22 +85,14 @@ jobs: - name: 安装依赖 run: npm ci - - name: 代码检查 - run: | - npm run lint - npm run type-check - - - name: 构建应用 - run: npm run build:test + - name: 运行用户端验证脚本 + run: ./scripts/validate-web.sh # 管理后台CI frontend-admin-ci: runs-on: ubuntu-latest needs: detect-changes if: needs.detect-changes.outputs.admin-changed == 'true' - defaults: - run: - working-directory: ./vocata-admin steps: - name: 检出代码 @@ -132,13 +108,8 @@ jobs: - name: 安装依赖 run: npm ci - - name: 代码检查 - run: | - npm run lint - npm run type-check - - - name: 构建应用 - run: npm run build:test + - name: 运行管理端验证脚本 + run: ./scripts/validate-admin.sh # CI 结果汇总 ci-summary: @@ -200,4 +171,4 @@ jobs: else echo "❌ **CI检查失败**,请修复后重新提交" >> $GITHUB_STEP_SUMMARY exit 1 - fi \ No newline at end of file + 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..a63ab4c 100644 --- a/.gitignore +++ b/.gitignore @@ -165,6 +165,7 @@ Desktop.ini # 文档生成 **/.docs/ +**/.local/ # 测试覆盖率报告 **/coverage/ @@ -186,4 +187,8 @@ Desktop.ini !**/application-example.yml !**/config.example.js -/resources/application-local.yml \ No newline at end of file +/resources/application-local.yml + +# local workspace artifacts +/.codex +/test.sh diff --git a/README.md b/README.md index 13a3dd6..659d7ab 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,26 @@ -# 一. 功能演示 +# VocaTa + +AI 驱动的实时语音角色扮演平台,支持语音/文本对话、角色创建、历史对话管理,以及多模型服务接入。 + +## 当前仓库 + +- 用户端前端:`vocata-web` +- 管理后台:`vocata-admin` +- 后端服务:`vocata-server` +- 本地开发编排:`docker-compose.yml` +- CI:`.github/workflows/ci.yml` +- Staging 部署:`.github/workflows/cd-staging.yml` + +## 开发与部署入口 + +- 开发环境说明:[`docs/开发环境说明.md`](docs/开发环境说明.md) +- 部署环境说明:[`docs/部署环境说明.md`](docs/部署环境说明.md) +- 验证清单:[`docs/验证清单.md`](docs/验证清单.md) +- 开发工作流:[`docs/开发工作流.md`](docs/开发工作流.md) +- 提交规范:[`docs/提交规范.md`](docs/提交规范.md) +- 重构边界清单:[`docs/重构边界清单.md`](docs/重构边界清单.md) + +## 一. 功能演示 演示视频:[http://t313actv0.hb-bkt.clouddn.com/bandicam%202025-09-28%2023-45-36-297.mp4](http://t313actv0.hb-bkt.clouddn.com/bandicam%202025-09-28%2023-45-36-297.mp4) @@ -147,4 +169,3 @@ - 李白:诗词创作工具 - 哈利:魔法故事生成器 - 苏格拉底:论文思路梳理 - 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..1a182d7 --- /dev/null +++ "b/docs/Docker\345\274\200\345\217\221\347\216\257\345\242\203.md" @@ -0,0 +1,26 @@ +# Docker 开发环境 + +环境变量、启动顺序和 provider 选择统一见 `docs/开发环境说明.md`。本文只记录 `docker-compose.yml` 的编排职责和 Docker 约定。 + +## 编排职责 + +- `docker-compose.yml` 负责本地开发和联调容器 +- `docker-compose.test.yml`、`docker-compose.prod.yml` 先保留为后续测试/生产预留文件 +- 前端容器采用接近发布态的静态构建,不提供热更新 + +## 服务与 Profile + +- `postgres`:开发数据库 +- `redis`:开发缓存 +- `vocata-server`:后端服务 +- `vocata-web`:用户端静态前端 +- `vocata-admin`:管理端静态前端 +- `pgadmin`:数据库管理工具,使用 `tools` profile +- `mailhog`:邮件调试工具,使用 `tools` profile + +## 端口与健康检查 + +- 宿主机端口通过 `.env` 配置,容器内端口由 compose 固定 +- 后端健康检查统一为 `/api/health` +- 前端容器内部统一监听 `8080` +- 前端 API 地址通过构建期变量 `VITE_APP_URL` 注入 diff --git a/docs/GitHub-Staging-Secrets.md b/docs/GitHub-Staging-Secrets.md new file mode 100644 index 0000000..7fd3eca --- /dev/null +++ b/docs/GitHub-Staging-Secrets.md @@ -0,0 +1,74 @@ +# GitHub Staging Secrets 清单 + +当前 staging 目标机: + +- `STAGING_HOST=86.53.161.33` +- `STAGING_USER=deploy` +- SSH 端口固定为 `22`(当前 workflow 已写死) + +## 必填 Secrets + +现在的 staging workflow 只使用仓库级 `Repository secrets`,不再依赖 `Environments -> staging`。 + +只需要 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`: + +```bash +cat /home/an/Projects/goodPro/VocaTa/.local/vocata_staging_ed25519 +``` + +## 当前服务器登录建议 + +- 日常部署用户:`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/superpowers/plans/2026-04-01-refactor-preparation-plan.md b/docs/superpowers/plans/2026-04-01-refactor-preparation-plan.md new file mode 100644 index 0000000..0b8b4b3 --- /dev/null +++ b/docs/superpowers/plans/2026-04-01-refactor-preparation-plan.md @@ -0,0 +1,626 @@ +# Refactor Preparation Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Build a minimal-risk preparation baseline for the upcoming refactor by first stabilizing configuration, validation, workflow rules, and structural boundaries across `vocata-server`, `vocata-web`, and `vocata-admin`. + +**Architecture:** This plan avoids business-feature rewrites. It treats refactor preparation as a documentation-and-guardrail project: freeze the current baseline, normalize configuration entry points, lock validation commands, narrow CI behavior, and define structure boundaries before any module-level refactor starts. + +**Tech Stack:** Spring Boot 3.1.4, Java 17, Maven, Vue 3, TypeScript, Vite, ESLint, vue-tsc, Docker Compose, GitHub Actions + +--- + +## File Map + +**Create:** +- `docs/开发环境说明.md` — single source of truth for local startup, required config, optional config, and common startup failures. +- `docs/部署环境说明.md` — server layout, `.env` location, staging deployment, rollback path. +- `docs/开发工作流.md` — personal branch flow, merge discipline, pre-push checks, forbidden mixed commits. +- `docs/提交规范.md` — conventional commit usage for this repo with examples. +- `docs/验证清单.md` — exact validation commands for server, web, admin, Docker, and staging checks. +- `docs/重构边界清单.md` — per-module allowed changes, forbidden changes, and directory ownership rules for phase 1. + +**Modify:** +- `README.md` — replace outdated startup/deployment guidance with links to the new docs. +- `.env.example` — align naming and comments with the actual development baseline. +- `docs/Docker开发环境.md` — narrow to Docker-specific concerns and link to the main environment doc. +- `docs/部署文档.md` — either fold into the new deployment doc or reduce to a redirect note. +- `.github/workflows/ci.yml` — keep CI scoped to relevant modules and ensure validation steps reflect the agreed baseline. +- `.github/workflows/cd-staging.yml` — confirm staging deploy assumptions match the new docs-only deployment model. + +**Inspect Only:** +- `.env` +- `docker-compose.yml` +- `vocata-web/package.json` +- `vocata-admin/package.json` +- `vocata-server/pom.xml` +- `CLAUDE.md` + +--- + +### Task 1: Freeze The Current Baseline + +**Files:** +- Create: `docs/验证清单.md` +- Modify: `README.md` +- Inspect: `.env`, `docker-compose.yml`, `.github/workflows/ci.yml`, `.github/workflows/cd-staging.yml` + +- [ ] **Step 1: Capture the current runnable baseline** + +Run: + +```bash +git status --short --branch +docker compose ps +curl -fsS http://127.0.0.1:9009/api/health +curl -fsS "http://127.0.0.1:9009/api/open/character/list?pageNum=1&pageSize=2" +``` + +Expected: +- Git branch is `develop` +- Docker services for postgres, redis, server, web, and admin are visible if local stack is up +- Health endpoint returns success +- Character list endpoint returns success + +- [ ] **Step 2: Write the validation checklist with exact commands** + +Create `docs/验证清单.md` with this initial structure: + +~~~markdown +# 验证清单 + +## 后端 + +```bash +cd vocata-server +mvn -Dmaven.repo.local=/tmp/juhao_m2repo -Dmaven.test.skip=true package +``` + +## 用户端 + +```bash +cd vocata-web +npm run lint +npm run type-check +npm run build +``` + +## 管理端 + +```bash +cd vocata-admin +npm run lint +npm run type-check +npm run build +``` + +## Docker 开发环境 + +```bash +docker compose config -q +docker compose ps +``` + +## Staging 部署后检查 + +```bash +curl -fsS http://127.0.0.1:9009/api/health +``` +~~~ + +- [ ] **Step 3: Add README pointers instead of duplicating startup knowledge** + +Update `README.md` to link to: + +```markdown +## 开发与部署入口 + +- 开发环境说明:`docs/开发环境说明.md` +- 部署环境说明:`docs/部署环境说明.md` +- 验证清单:`docs/验证清单.md` +- 开发工作流:`docs/开发工作流.md` +- 提交规范:`docs/提交规范.md` +- 重构边界清单:`docs/重构边界清单.md` +``` + +- [ ] **Step 4: Verify the docs-only baseline changes** + +Run: + +```bash +git diff --check +``` + +Expected: no output + +- [ ] **Step 5: Commit** + +```bash +git add README.md docs/验证清单.md +git commit -m "docs: record validation baseline" +``` + +--- + +### Task 2: Normalize Development Configuration Documentation + +**Files:** +- Create: `docs/开发环境说明.md` +- Modify: `.env.example`, `docs/Docker开发环境.md` +- Inspect: `.env`, `docker-compose.yml`, `vocata-server/src/main/resources/application.yml` + +- [ ] **Step 1: Diff the real `.env` against `.env.example`** + +Run: + +```bash +diff -u .env.example .env || true +``` + +Expected: +- Differences identify missing comments, stale keys, or naming drift +- No direct secret values should be copied from `.env` into `.env.example` + +- [ ] **Step 2: Write the development environment guide** + +Create `docs/开发环境说明.md` with this structure: + +```markdown +# 开发环境说明 + +## 启动入口 + +- 根目录 `.env` +- `docker-compose.yml` +- 后端 `application.yml` +- 前端各自 `package.json` 脚本 + +## 启动必需配置 + +- PostgreSQL +- Redis +- 七牛存储 +- 当前启用的 AI/TTS provider + +## 功能可选配置 + +- 邮件 +- 非默认 AI provider + +## 启动顺序 + +1. `docker compose up -d postgres redis` +2. 启动后端 +3. 启动 `vocata-web` +4. 启动 `vocata-admin` + +## 常见问题 + +- 端口冲突 +- `.env` 未同步 +- 第三方 key 缺失 +- 数据库备份未恢复 +``` + +- [ ] **Step 3: Align `.env.example` comments with actual runtime behavior** + +Update `.env.example` so that each section follows this pattern: + +```env +# PostgreSQL development container port +POSTGRES_PORT=55433 + +# Redis development container port +REDIS_EXPOSE_PORT=6380 + +# Frontend API base used at build time +VITE_APP_URL=/api +``` + +Rule: +- keep placeholder values non-sensitive +- keep comments operational, not marketing +- remove deprecated key names + +- [ ] **Step 4: Reduce Docker doc duplication** + +Update `docs/Docker开发环境.md` so the top section becomes: + +```markdown +> 本文只说明 Docker 开发编排本身。 +> 环境变量、启动顺序、第三方配置说明统一见 `docs/开发环境说明.md`。 +``` + +- [ ] **Step 5: Verify the config documentation changes** + +Run: + +```bash +git diff --check +docker compose config -q +``` + +Expected: +- no diff formatting issues +- Docker config validates successfully + +- [ ] **Step 6: Commit** + +```bash +git add .env.example docs/开发环境说明.md docs/Docker开发环境.md +git commit -m "docs: normalize development environment guidance" +``` + +--- + +### Task 3: Document Deployment And Personal Workflow + +**Files:** +- Create: `docs/部署环境说明.md`, `docs/开发工作流.md`, `docs/提交规范.md` +- Modify: `docs/部署文档.md` +- Inspect: `.github/workflows/cd-staging.yml`, `.github/workflows/ci.yml` + +- [ ] **Step 1: Write the deployment environment guide** + +Create `docs/部署环境说明.md` with this structure: + +```markdown +# 部署环境说明 + +## 服务器目录约定 + +- `/home/deploy/deploy/vocata/repo` +- `/home/deploy/deploy/vocata/.env` + +## Staging 部署方式 + +- push `develop` +- GitHub Actions 通过 SSH 登录 +- 服务器本地 `git pull` +- `docker compose up -d --build` + +## 回滚方式 + +- 回退到上一个 commit +- 重新执行 staging workflow + +## 故障定位入口 + +- `docker compose ps` +- `docker compose logs` +- `curl http://127.0.0.1:9009/api/health` +``` + +- [ ] **Step 2: Write the personal workflow guide** + +Create `docs/开发工作流.md` with this structure: + +```markdown +# 开发工作流 + +## 分支规则 + +- `develop` 只接收已验证改动 +- 功能和治理任务从 `feature/*` 分支开发 + +## 提交规则 + +- 一个提交只解决一类问题 +- 禁止环境、结构、业务逻辑混改 + +## 合并前检查 + +- 后端验证 +- 用户端验证 +- 管理端验证 +- 需要时验证 Docker 和 staging +``` + +- [ ] **Step 3: Write the commit conventions guide** + +Create `docs/提交规范.md` with this structure: + +```markdown +# 提交规范 + +## 类型 + +- `docs:` +- `chore:` +- `fix:` +- `refactor:` +- `test:` + +## 示例 + +- `docs: add deployment environment guide` +- `chore: normalize environment variable naming` +- `refactor: remove duplicate frontend utility types` +``` + +- [ ] **Step 4: Turn old deployment doc into a redirect or trimmed legacy note** + +Update `docs/部署文档.md` top section to: + +```markdown +# 部署文档 + +> 当前有效部署说明统一见 `docs/部署环境说明.md`。 +> 本文仅保留历史背景或补充说明,不再作为执行入口。 +``` + +- [ ] **Step 5: Verify link consistency** + +Run: + +```bash +rg -n "开发环境说明|部署环境说明|开发工作流|提交规范" README.md docs +git diff --check +``` + +Expected: +- new docs are referenced +- no formatting errors + +- [ ] **Step 6: Commit** + +```bash +git add docs/部署环境说明.md docs/开发工作流.md docs/提交规范.md docs/部署文档.md +git commit -m "docs: add personal development workflow guides" +``` + +--- + +### Task 4: Lock Validation Commands Into CI Expectations + +**Files:** +- Modify: `.github/workflows/ci.yml`, `docs/验证清单.md` +- Inspect: `vocata-web/package.json`, `vocata-admin/package.json`, `vocata-server/pom.xml` + +- [ ] **Step 1: Confirm current runnable validation commands** + +Run: + +```bash +cd vocata-web && npm run lint && npm run type-check && npm run build +cd ../vocata-admin && npm run lint && npm run type-check && npm run build +cd ../vocata-server && mvn -Dmaven.repo.local=/tmp/juhao_m2repo test +``` + +Expected: +- frontends complete lint/type-check/build +- backend test command either passes or exposes the exact minimum gap to document + +- [ ] **Step 2: Update `docs/验证清单.md` if backend command needs a staged baseline** + +If full `mvn test` is too unstable, change backend section to: + +~~~markdown +## 后端当前基线 + +```bash +cd vocata-server +mvn -Dmaven.repo.local=/tmp/juhao_m2repo -Dmaven.test.skip=true package +``` + +## 后端下一阶段目标 + +```bash +cd vocata-server +mvn -Dmaven.repo.local=/tmp/juhao_m2repo test +``` +~~~ + +- [ ] **Step 3: Align CI job descriptions with the agreed baseline** + +In `.github/workflows/ci.yml`, ensure: +- server job explains whether it is running compile/package vs. test baseline +- web/admin jobs explicitly run `lint`, `type-check`, `build` +- path filters do not trigger all modules for unrelated doc-only changes + +Use this edit pattern in comments or step names: + +```yaml +- name: 运行代码检查 + run: npm run lint + +- name: 运行类型检查 + run: npm run type-check + +- name: 构建应用 + run: npm run build +``` + +- [ ] **Step 4: Verify the CI baseline changes** + +Run: + +```bash +git diff --check +``` + +Expected: no output + +- [ ] **Step 5: Commit** + +```bash +git add .github/workflows/ci.yml docs/验证清单.md +git commit -m "ci: align validation baseline with local workflow" +``` + +--- + +### Task 5: Define Refactor Boundaries Before Any File Moves + +**Files:** +- Create: `docs/重构边界清单.md` +- Inspect: `vocata-server/src`, `vocata-web/src`, `vocata-admin/src` + +- [ ] **Step 1: Map each module to directory responsibilities** + +Create `docs/重构边界清单.md` with this table skeleton: + +```markdown +# 重构边界清单 + +## vocata-server + +| 目录 | 职责 | 第一阶段是否允许改动 | +| --- | --- | --- | +| `controller` | API 入口 | 仅小修 | +| `service` | 业务编排 | 仅小修 | +| `config` | 配置与装配 | 允许 | +| `common` | 通用能力 | 允许 | + +## vocata-web + +| 目录 | 职责 | 第一阶段是否允许改动 | +| --- | --- | --- | +| `src/api` | 接口调用 | 允许 | +| `src/views` | 页面 | 仅小修 | +| `src/types` | 类型 | 允许 | +| `src/utils` | 工具 | 允许 | + +## vocata-admin + +| 目录 | 职责 | 第一阶段是否允许改动 | +| --- | --- | --- | +| `src/api` | 接口调用 | 允许 | +| `src/views` | 页面 | 仅小修 | +| `src/types` | 类型 | 允许 | +| `src/utils` | 工具 | 允许 | +``` + +- [ ] **Step 2: Add explicit first-phase constraints** + +Append this section: + +```markdown +## 第一阶段允许改动 + +- 配置命名统一 +- 文档收口 +- 验证命令固定 +- 死代码删除 +- 明显重复类型和工具抽取 + +## 第一阶段禁止改动 + +- 大规模搬迁目录 +- 一次性重写页面 +- 一次性替换 AI provider 结构 +- 数据库大规模重设计 +``` + +- [ ] **Step 3: Verify boundaries doc is concrete enough** + +Run: + +```bash +rg -n "允许改动|禁止改动|vocata-server|vocata-web|vocata-admin" docs/重构边界清单.md +git diff --check +``` + +Expected: +- all three modules are covered +- allowed/forbidden sections exist +- no formatting errors + +- [ ] **Step 4: Commit** + +```bash +git add docs/重构边界清单.md +git commit -m "docs: define phase-one refactor boundaries" +``` + +--- + +### Task 6: Prepare The First Week Execution Queue + +**Files:** +- Modify: `docs/开发工作流.md`, `docs/验证清单.md`, `docs/重构边界清单.md` +- Create: optional `docs/第一周执行清单.md` + +- [ ] **Step 1: Convert the approved design into a one-week checklist** + +Create `docs/第一周执行清单.md` with this structure: + +```markdown +# 第一周执行清单 + +## Day 1 +- 冻结基线 +- 记录命令 + +## Day 2 +- 收口环境说明 +- 校正 `.env.example` + +## Day 3 +- 固定验证清单 +- 收窄 CI 范围 + +## Day 4 +- 完成开发工作流和提交规范 + +## Day 5 +- 完成重构边界清单 +``` + +- [ ] **Step 2: Cross-link the preparation docs** + +Add a short “相关文档” section to: +- `docs/开发工作流.md` +- `docs/验证清单.md` +- `docs/重构边界清单.md` + +Pattern: + +```markdown +## 相关文档 + +- `docs/开发环境说明.md` +- `docs/部署环境说明.md` +- `docs/提交规范.md` +``` + +- [ ] **Step 3: Final verification before execution starts** + +Run: + +```bash +git diff --check +git status --short +``` + +Expected: +- only the intended docs/CI files are changed +- no accidental edits to business code + +- [ ] **Step 4: Commit** + +```bash +git add docs/第一周执行清单.md docs/开发工作流.md docs/验证清单.md docs/重构边界清单.md +git commit -m "docs: add first-week refactor preparation checklist" +``` + +--- + +## Spec Coverage Check + +- 配置与环境治理:Task 2, Task 3 +- 测试与验证基线:Task 1, Task 4 +- 结构边界定义:Task 5 +- 个人开发流程固定:Task 3, Task 6 +- 最小改动提交:all tasks use docs/ci/config-only commits and avoid mixed business changes +- 三端统一治理、分端落地:Task 5 documents boundaries; subsequent refactor entry starts only after these tasks complete + +## Placeholder Scan + +No placeholder markers or undefined follow-up wording are left in the plan. Each task names exact files, commands, expected results, and commit messages. + +## Type / Name Consistency Check + +- Uses `STAGING_SSH_KEY`, `STAGING_HOST`, `STAGING_USER` consistently with the current staging workflow +- Uses `lint`, `type-check`, `build` consistently with current frontend `package.json` +- Uses `mvn -Dmaven.repo.local=/tmp/juhao_m2repo` consistently with the current local Maven setup diff --git a/docs/superpowers/specs/2026-04-01-refactor-preparation-design.md b/docs/superpowers/specs/2026-04-01-refactor-preparation-design.md new file mode 100644 index 0000000..38c9f5d --- /dev/null +++ b/docs/superpowers/specs/2026-04-01-refactor-preparation-design.md @@ -0,0 +1,313 @@ +# 重构前准备设计 + +- 日期:2026-04-01 +- 适用范围:`vocata-server`、`vocata-web`、`vocata-admin` +- 目标类型:个人开发版 +- 总体策略:方案一,统一治理先行 + +## 1. 背景与目标 + +当前仓库已经具备基本可运行能力,但在正式开始重构前,存在三个直接风险: + +1. 配置与环境入口不够收敛,开发、Docker、staging 的认知成本较高。 +2. 测试和验证护栏偏弱,改动后缺少足够的回归安全感。 +3. 三个模块的结构边界还不够清晰,容易在重构时演变为大范围混改。 + +本设计的目标不是立即重写模块结构,而是在不破坏现有可运行状态的前提下,先建立一套个人长期维护可执行、成本可控、最小改动提交的重构前准备体系。 + +## 2. 工作原则 + +### 2.1 先治理,再重构 + +先处理环境、配置、测试、提交方式,再进入业务结构重构。第一阶段不允许大规模搬迁业务代码。 + +### 2.2 最小改动提交 + +每个提交只处理一类问题,不混合配置、结构、测试、业务逻辑。 + +允许的提交形态: + +- 一个提交只改环境和配置说明 +- 一个提交只改 CI 或验证命令 +- 一个提交只补测试基线 +- 一个提交只做小范围结构清理 + +禁止的提交形态: + +- 一个提交同时改环境、目录结构和业务逻辑 +- 借重构名义顺手改需求 +- 未验证直接推送到 `develop` + +### 2.3 先建护栏,再做迁移 + +没有固定验证命令、没有 staging 可回归、没有清晰目录边界之前,不进行大规模结构迁移。 + +### 2.4 三端统一治理,分端落地 + +规则统一覆盖 `server`、`web`、`admin`,但实施顺序必须可控: + +1. 先全仓统一治理基础规则 +2. 再按 `vocata-server -> vocata-web -> vocata-admin` 的顺序进入模块重构 + +### 2.5 可运行优先于漂亮 + +任何治理动作都不能破坏: + +- 本地开发启动 +- Docker 开发启动 +- staging 部署 +- 核心接口可用性 + +## 3. 方案选择 + +本次采用“统一治理先行,然后分模块进入重构”的方案。 + +### 3.1 选型理由 + +- 这是当前风险最低的路径。 +- 能直接命中“配置混乱、测试缺失、结构不清”三个问题。 +- 适合个人开发阶段,维护成本最低。 +- 能保留现有可运行状态,不把项目推进到不可回退的大爆炸重构。 + +### 3.2 明确不采用的路径 + +不采用“三端同时大规模改造”的方式,也不采用“先只后端重构、前端继续放置”的方式。 + +## 4. 准备项清单 + +### 4.1 配置与环境治理 + +目标:解决配置和环境混乱。 + +准备项: + +- 统一开发环境的真实入口和说明方式 +- 明确根目录 `.env` / `.env.example` 的职责 +- 明确服务器部署 `.env` 的职责 +- 明确后端 `application*.yml` 的分层关系 +- 明确前端 API 地址的单一命名方案 +- 区分“启动必需配置”和“功能可选配置” +- 清理历史残留、重复命名和误导性说明 + +预期产出: + +- `docs/开发环境说明.md` +- `docs/部署环境说明.md` +- `.env.example` +- 配置命名约定说明 + +### 4.2 测试与验证基线 + +目标:解决重构时没有安全感。 + +准备项: + +- 固定三端最小验证命令 +- 保证前端 `lint + type-check + build` 稳定可跑 +- 后端建立最小测试/验证基线 +- 明确本地验证和 CI 验证的对应关系 +- 缩小 CI 的误触发范围 + +预期产出: + +- `docs/验证清单.md` +- 后端最小测试入口 +- 前端固定验证命令 +- CI 验证范围说明 + +### 4.3 结构边界定义 + +目标:在不大迁移的前提下定义“未来怎么改”。 + +准备项: + +- 给三个模块定义目录职责 +- 标注第一阶段允许改动区 +- 标注第一阶段禁动区 +- 清理死代码、重复类型、明显错误命名 +- 统一公共工具和公共类型的归属原则 + +预期产出: + +- `docs/重构边界清单.md` +- 目录职责表 +- 允许改动 / 禁止改动列表 + +### 4.4 个人开发流程固定 + +目标:降低长期维护摩擦。 + +准备项: + +- 固定 `feature/* -> develop` 的分支流 +- 固定 commit type 语义 +- 固定提交前检查项 +- 明确“禁止混改”的规则 + +预期产出: + +- `docs/开发工作流.md` +- `docs/提交规范.md` + +## 5. 分阶段执行 + +### 阶段 0:冻结基线 + +目标:把当前可运行状态固化成起点。 + +动作: + +- 记录当前启动命令 +- 记录当前部署命令 +- 记录当前验证命令 +- 记录已知问题,但先不顺手修 + +完成标志: + +- 本地前后端可启动 +- Docker 开发环境可启动 +- staging 可部署 + +### 阶段 1:配置与环境治理 + +目标:收口配置入口和环境认知。 + +动作: + +- 统一环境变量命名 +- 收口 `.env` / `.env.example` +- 明确开发与部署环境边界 +- 收敛配置文档 + +推荐提交类型: + +- `docs:` +- `chore:` + +### 阶段 2:测试与验证基线 + +目标:让每次改动都有可重复验证护栏。 + +动作: + +- 固定三端验证命令 +- 后端补最小验证基线 +- 前端稳定 `lint/type-check/build` +- 调整 CI 检查范围 + +推荐提交类型: + +- `test:` +- `ci:` +- `chore:` + +### 阶段 3:结构规则定义 + +目标:先定义边界,再动结构。 + +动作: + +- 补目录职责说明 +- 明确允许和禁动范围 +- 只做小规模清理,不做大迁移 + +推荐提交类型: + +- `docs:` +- `refactor:` + +### 阶段 4:进入模块重构 + +进入顺序: + +1. `vocata-server` +2. `vocata-web` +3. `vocata-admin` + +理由: + +- 后端先稳定接口、配置和数据边界 +- 用户端比管理端更接近核心主链路 +- 管理端最后调整风险最低 + +## 6. 第一周待办 + +### Day 1 + +- 冻结当前基线 +- 记录启动 / 部署 / 验证命令 +- 清点现有配置入口 + +### Day 2 + +- 收口 `.env` / `.env.example` +- 统一环境变量命名 +- 补 `开发环境说明` + +### Day 3 + +- 固定三端验证命令 +- 清理 CI 误触发 +- 补 `验证清单` + +### Day 4 + +- 写 `开发工作流` 和 `提交规范` +- 明确 feature 分支规则 +- 固定提交前检查项 + +### Day 5 + +- 写 `重构边界清单` +- 标记三端第一阶段可动区 / 禁动区 +- 为第二周的测试与结构治理做进入准备 + +## 7. 提交规范建议 + +推荐使用: + +- `docs:` 文档和说明 +- `chore:` 配置、脚本、环境、CI +- `fix:` 明确缺陷修复 +- `refactor:` 不改变行为的结构整理 +- `test:` 测试和验证基线 + +示例: + +- `docs: record refactor preparation baseline` +- `chore: normalize environment configuration` +- `ci: narrow staging validation scope` +- `test: add backend baseline checks` +- `refactor: normalize shared utility boundaries` + +## 8. 风险与规避 + +### 风险 1:一开始就搬目录 + +规避方式:先定义职责,再迁移;第一阶段只允许小清理。 + +### 风险 2:配置收口后仍不清楚谁生效 + +规避方式:保留单一真入口,并把优先级写入文档。 + +### 风险 3:测试目标过大导致进度停滞 + +规避方式:先建立最小护栏,不追求一次性高覆盖。 + +### 风险 4:三端并行重构导致回归难定位 + +规避方式:统一治理先行,模块重构串行推进。 + +## 9. 验收标准 + +当以下条件全部满足时,才进入正式模块重构: + +- 开发、Docker、staging 三套运行路径都有文档和固定入口 +- 三端验证命令固定下来 +- CI 不再因为无关改动误炸全部模块 +- 三端目录职责和禁动边界已经写清 +- 个人分支流和提交规则已经固定 + +## 10. 下一步 + +设计确认后,下一步应输出可直接执行的实施计划,并按“最小改动提交”顺序推进。 diff --git "a/docs/\345\274\200\345\217\221\345\267\245\344\275\234\346\265\201.md" "b/docs/\345\274\200\345\217\221\345\267\245\344\275\234\346\265\201.md" new file mode 100644 index 0000000..5fa9fec --- /dev/null +++ "b/docs/\345\274\200\345\217\221\345\267\245\344\275\234\346\265\201.md" @@ -0,0 +1,62 @@ +# 开发工作流 + +这份文档只约束个人开发节奏。目标是减少混改、减少回滚成本,并让每次提交都能单独解释清楚。 + +## 分支流 + +- 先从 `develop` 拉出个人分支。 +- 分支命名优先使用 `feature/-`、`fix/-`、`docs/` 这类可读命名。 +- 所有个人开发分支都要通过 PR 合回 `develop`。 +- 不要在 `develop` 上直接做功能开发,也不要把临时修补长期留在主线。 + +## 合并纪律 + +- 一个分支只解决一件事。 +- 一个 PR 只承载一个主题。 +- 先把依赖的 `develop` 变更同步到当前分支,再继续写代码。 +- 如果发现改动开始跨模块扩散,先拆分分支,再继续提交。 +- 不要把“顺手修一下”的内容塞进当前 PR,除非它和主问题是同一个根因。 + +## 推送前检查 + +提交前至少跑一次: + +- `git diff --check` +- 相关时也要补跑 Docker 和 staging 验证,而不是只跑本地单模块命令。 + +按修改范围补跑对应检查,只跑你实际改到的模块: + +- 后端改动:`./scripts/validate-backend.sh` +- 用户端前端改动:`./scripts/validate-web.sh` +- 管理后台前端改动:`./scripts/validate-admin.sh` +- Docker 或环境变量相关:`./scripts/validate-docker.sh` +- staging 部署后回归:在 staging 主机仓库目录执行 `./scripts/validate-staging-host.sh` + +如果本次改动只碰文档,也不要跳过 `git diff --check`。 +如果两个前端都改了,就两边都跑。 + +## 禁止的混改 + +- 不要把业务代码、文档、配置、CI 改动混在一个提交里。 +- 不要把多个无关模块的修补塞进同一个提交里。 +- 不要把格式化、重构和功能变化混成一个提交,除非无法拆分。 +- 不要在同一个 commit 里同时改 staging 部署逻辑和业务逻辑。 +- 不要用“清理一下”当理由把大范围变更藏进小提交。 + +## 实际做法 + +如果一个改动看起来说不清楚,就先拆: + +1. 先提交最小可验证的改动。 +2. 再提交后续修补。 +3. 最后再合并到 `develop`。 + +这样做的标准很简单:每个提交都能用一句话说清楚它解决了什么问题。 + +## 相关文档 + +- [`docs/开发环境说明.md`](开发环境说明.md) +- [`docs/部署环境说明.md`](部署环境说明.md) +- [`docs/提交规范.md`](提交规范.md) +- [`docs/验证清单.md`](验证清单.md) +- [`docs/重构边界清单.md`](重构边界清单.md) diff --git "a/docs/\345\274\200\345\217\221\347\216\257\345\242\203\350\257\264\346\230\216.md" "b/docs/\345\274\200\345\217\221\347\216\257\345\242\203\350\257\264\346\230\216.md" new file mode 100644 index 0000000..85f5a41 --- /dev/null +++ "b/docs/\345\274\200\345\217\221\347\216\257\345\242\203\350\257\264\346\230\216.md" @@ -0,0 +1,62 @@ +# 开发环境说明 + +这份文档是本地启动的单一入口。环境变量、启动顺序和 provider 选择都以这里为准;`docs/Docker开发环境.md` 只保留 Docker 编排约定。 + +## 启动入口 + +- 根目录 `.env` +- `docker-compose.yml` +- `vocata-server/src/main/resources/application.yml` +- `vocata-web/package.json` +- `vocata-admin/package.json` + +## 核心栈启动必需配置 + +这些配置用于拉起本地开发的基础容器和基础联调: + +- PostgreSQL:`POSTGRES_DB`、`POSTGRES_USER`、`POSTGRES_PASSWORD`、`DB_HOST`、`DB_PORT`、`DB_NAME`、`DB_USERNAME`、`DB_PASSWORD` +- Redis:`REDIS_HOST`、`REDIS_PORT`、`REDIS_PASSWORD`、`REDIS_DATABASE` +- Docker 端口映射:`POSTGRES_PORT`、`REDIS_EXPOSE_PORT`、`SERVER_PORT`、`WEB_PORT`、`ADMIN_PORT` +- 后端 profile:`SPRING_PROFILES_ACTIVE` +- 前端构建注入:`VITE_APP_URL`、`WEB_BUILD_MODE`、`ADMIN_BUILD_MODE` + +## 需要功能配置的场景 + +这些配置只在你要使用对应功能时需要补齐;不要把它们和核心栈启动混为一谈。 + +- AI/对话默认 provider:`AI_LLM_PROVIDER`、`AI_STT_PROVIDER`、`AI_TTS_PROVIDER` +- 七牛对象存储:`QINIU_ACCESS_KEY`、`QINIU_SECRET_KEY`、`QINIU_BUCKET`、`QINIU_DOMAIN`、`QINIU_REGION`、`QINIU_KEY_PREFIX` +- 七牛 AI/STT:`QINIU_AI_API_KEY`、`QINIU_AI_BASE_URL`、`QINIU_AI_MODEL`、`QINIU_AI_TIMEOUT`、`QINIU_STT_ENDPOINT`、`QINIU_STT_MODEL` +- 科大讯飞 TTS:`XUNFEI_TTS_APP_ID`、`XUNFEI_TTS_API_KEY`、`XUNFEI_TTS_SECRET_KEY` +- 邮件:`MAIL_USERNAME`、`MAIL_PASSWORD` +- Gemini:`GEMINI_API_KEY`、`GEMINI_BASE_URL`、`GEMINI_MODEL`、`GEMINI_TIMEOUT` +- OpenAI:`OPENAI_API_KEY`、`OPENAI_BASE_URL`、`OPENAI_MODEL`、`OPENAI_TIMEOUT` +- SiliconFlow:`SILICONFLOW_API_KEY`、`SILICONFLOW_BASE_URL`、`SILICONFLOW_AI_MODEL`、`SILICONFLOW_TIMEOUT` +- 可选工具:`PGADMIN_DEFAULT_EMAIL`、`PGADMIN_DEFAULT_PASSWORD` + +说明: + +- 当前代码里 AI provider 默认仍是 `qiniu/qiniu/xunfei`,所以如果你要把完整后端功能都跑起来,对应的第三方配置仍然要准备好。 +- 文件上传、AI 调用和语音相关功能依赖各自的 provider 配置;如果只是看数据库、Redis 或前端静态页面,不一定需要把所有第三方账号都补全。 + +## 启动顺序 + +1. 准备 `.env` +2. 启动基础依赖:`docker compose up -d postgres redis` +3. 启动后端和前端容器:`docker compose up -d --build` +4. 需要数据库管理或邮件调试时,再启用 tools profile + +如果只改前端页面,也可以直接在各自目录运行本地开发命令: + +- 用户端:`cd vocata-web && npm run dev` +- 管理端:`cd vocata-admin && npm run dev` + +## 常见问题 + +- 端口冲突:核心栈默认会占用 `55433`、`6380`、`9009`、`3000`、`3001`,冲突时优先改 `.env`,不要改容器内端口。 +- tools profile 端口:`5050`、`1025`、`8025` 只会在执行 `docker compose --profile tools up -d` 时占用。 +- `.env` 没同步:`docker-compose.yml` 读取的是根目录 `.env`,改完模板后要同步到实际文件。 +- 完整后端起不来:先看 AI、七牛和语音相关配置,再看 `docker compose logs vocata-server`。 +- 前端请求地址不对:`VITE_APP_URL` 是构建期变量,改了以后需要重新构建前端容器。 +- 数据库数据不一致:当前开发数据库使用 `pgvector/pgvector:pg17`,恢复旧卷或备份前先确认版本和扩展兼容。 +- 邮件不通:只有在你要验证邮箱相关功能时,才需要检查 `MAIL_USERNAME` 和 `MAIL_PASSWORD`。 diff --git "a/docs/\346\217\220\344\272\244\350\247\204\350\214\203.md" "b/docs/\346\217\220\344\272\244\350\247\204\350\214\203.md" new file mode 100644 index 0000000..8dcaf84 --- /dev/null +++ "b/docs/\346\217\220\344\272\244\350\247\204\350\214\203.md" @@ -0,0 +1,50 @@ +# 提交规范 + +这个仓库的长期工作目标是按约定式提交写提交信息。PR 模板里要求使用这套前缀,个人开发也应保持一致。 + +## 格式 + +```text +type(scope?): subject +``` + +- `type` 用小写。 +- `scope` 可选,写模块或领域名。 +- `subject` 写清楚变更结果,保持简短。 +- 一条提交只写一件事。 + +## 可用类型 + +- `feat`:新增功能或可见能力 +- `fix`:修复缺陷 +- `docs`:只改文档 +- `refactor`:重构实现,但不改变行为 +- `test`:新增或调整测试 +- `chore`:杂项维护、脚本、依赖、构建配置 +- `style`:纯格式调整,不改业务逻辑 + +## 真实示例 + +- `docs: add deployment environment guide` +- `docs: normalize development workflow` +- `chore: normalize environment variable naming` +- `fix(staging): keep fallback .env copy path` +- `refactor(aiChat): optimize websocket connection management` +- `refactor: remove duplicate frontend utility types` +- `feat(core): improve AI role creation flow` +- `test(server): add health check coverage` +- `chore(ci): narrow frontend validation scope` +- `chore(deps): update lockfile` +- `style(web): align form spacing` + +## 约束 + +- 不要用 `update`、`misc`、`wip` 这类泛化前缀。 +- 不要把多个主题拼成一个标题。 +- 不要让提交主题依赖上下文才能看懂。 +- 如果同一批改动里既有功能又有文档,先拆成两个提交。 + +## 推荐习惯 + +- 提交前先看 `git diff --stat`,确认自己只改了预期范围。 +- 如果一句提交说明写不完,通常说明这次提交太大了。 diff --git "a/docs/\347\254\254\344\270\200\345\221\250\346\211\247\350\241\214\346\270\205\345\215\225.md" "b/docs/\347\254\254\344\270\200\345\221\250\346\211\247\350\241\214\346\270\205\345\215\225.md" new file mode 100644 index 0000000..c512de6 --- /dev/null +++ "b/docs/\347\254\254\344\270\200\345\221\250\346\211\247\350\241\214\346\270\205\345\215\225.md" @@ -0,0 +1,42 @@ +# 第一周执行清单 + +这份清单只服务于重构前准备阶段,目标是先把配置、验证、流程和边界收口,再进入模块级重构。 + +## Day 1 + +- 冻结当前开发与 staging 基线 +- 记录本地启动、部署、验证命令 +- 标记已知问题,但不顺手扩修 + +## Day 2 + +- 收口开发环境说明 +- 校正 `.env.example` 注释和命名 +- 确认 Docker 开发文档只保留编排内容 + +## Day 3 + +- 固定验证清单 +- 收窄 CI 校验范围 +- 确认后端、用户端、管理端的最小验证入口 + +## Day 4 + +- 完成开发工作流说明 +- 完成提交规范说明 +- 明确个人分支、提交、合并纪律 + +## Day 5 + +- 完成重构边界清单 +- 明确第一阶段允许改动与禁止改动 +- 为后续模块级重构准备单独实施计划 + +## 相关文档 + +- [`docs/开发环境说明.md`](开发环境说明.md) +- [`docs/部署环境说明.md`](部署环境说明.md) +- [`docs/开发工作流.md`](开发工作流.md) +- [`docs/提交规范.md`](提交规范.md) +- [`docs/验证清单.md`](验证清单.md) +- [`docs/重构边界清单.md`](重构边界清单.md) 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..115cdc5 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" @@ -1,123 +1,6 @@ -# VocaTa 项目运行指南 +# 部署文档 -本文档说明如何在本地或通过容器编排运行 VocaTa 项目(后端服务、客户端前端、管理后台)。 +> 当前有效部署说明统一见 [docs/部署环境说明.md](./部署环境说明.md)。 +> 本文仅保留历史背景或补充说明,不再作为执行入口。 -## 1. 运行方式概览 -- **Docker Compose 一键启动**:适合快速体验或准备集成环境,自动拉起数据库、缓存、后端和两个前端。 -- **本地开发模式**:分别启动后端(Java + Maven)及前端项目(Vue 3 + Vite),适合调试与开发。 -- **生产部署示例**:项目根目录提供 `docker-compose.prod.yml` 可作为上线部署基础模板。 - -## 2. 环境准备 -### 2.1 通用要求 -- 操作系统:macOS / Linux / Windows(支持 Docker 或 Java 开发环境) -- Git(可选,用于克隆仓库) - -### 2.2 本地开发依赖项 -- JDK 17 及以上 -- Maven 3.6 及以上 -- Node.js 20.19 或 >= 22.12(前端 `package.json` 中的 `engines` 要求) -- npm(Node.js 自带) -- PostgreSQL 12 及以上 -- Redis 6 及以上 - -### 2.3 容器运行依赖项 -- Docker 24+ -- Docker Compose Plugin(`docker compose` 命令) - -## 3. 使用 Docker Compose 快速启动 -1. 确保 Docker Desktop 或 Docker Engine 正在运行。 -2. 在项目根目录执行: - ```bash - docker compose up -d --build - ``` - 该命令会按 `docker-compose.yml` 构建并启动以下服务: - - PostgreSQL(端口 `5432`) - - Redis(端口 `6379`) - - vocata-server(端口 `9009`,REST API `http://localhost:9009/api`) - - vocata-web(端口 `3000`,客户端 Web 入口) - - vocata-admin(端口 `3001`,管理后台入口) - - pgAdmin(端口 `5050`,数据库管理工具) - - MailHog(SMTP `1025` / Web UI `8025`,用于邮件调试) -3. 查看容器日志(可选): - ```bash - docker compose logs -f vocata-server - ``` -4. 停止并清理容器: - ```bash - docker compose down - ``` - 如需保留数据,可保留默认定义的 `postgres_data`、`redis_data` 卷;若需要重新初始化数据,可加上 `-v` 删除数据卷。 - -## 4. 本地开发模式 -### 4.1 准备数据库与缓存 -1. 启动 PostgreSQL 与 Redis,并记录连接信息。 -2. 初始化数据库(以 README 示例为准): - ```sql - CREATE DATABASE vocata_dev; - CREATE USER vocata_dev WITH PASSWORD 'vocata_dev'; - GRANT ALL PRIVILEGES ON DATABASE vocata_dev TO vocata_dev; - ``` - -### 4.2 配置后端(`vocata-server`) -1. 拷贝本地配置模板: - ```bash - cp src/main/resources/application-local.yml.template src/main/resources/application-local.yml - ``` -2. 根据实际环境修改数据库、Redis、邮件及第三方 AI 服务配置。若使用环境变量,也可直接设置 `DB_HOST`、`DB_USERNAME` 等变量,`application.yml` 中提供了占位符。 -3. 安装依赖并运行: - ```bash - mvn clean install - mvn spring-boot:run -Dspring-boot.run.profiles=local - ``` - 或打包后运行: - ```bash - mvn package - java -jar target/vocata-server-*.jar --spring.profiles.active=local - ``` -4. 服务默认监听 `http://localhost:9009`,健康检查接口为 `/api/health`。 - -### 4.3 启动客户端前端(`vocata-web`) -1. 安装依赖: - ```bash - npm install - ``` -2. 启动开发服务器: - ```bash - npm run dev - ``` -3. 默认访问地址为 `http://localhost:5173`(Vite 默认端口)。如需连接本地后端,请在 `.env` 或启动命令中设置 `VITE_API_BASE_URL=http://localhost:9009/api`。 -4. 生产构建: - ```bash - npm run build - npm run preview # 可选,预览构建结果 - ``` - -### 4.4 启动管理后台前端(`vocata-admin`) -流程与客户端一致: -```bash -cd vocata-admin -npm install -npm run dev -``` -同样可通过环境变量 `VITE_API_BASE_URL` 指向后端 API。 - -### 4.5 常用调试技巧 -- 后端实时日志位于 `vocata-server/logs` 目录,可根据需要调整 `application.yml` 中的日志级别。 -- 如需模拟邮件发送,可使用 MailHog(`http://localhost:8025`)。 -- 若前后端跨域问题,可检查 `vocata-server` 的 `WebConfig` 或前端代理配置。 - -## 5. 生产部署参考 -- 使用 `docker-compose.prod.yml`,提前构建或拉取镜像(如 `ghcr.io/leivik/vocata-server:latest`)。 -- 通过环境变量注入数据库、Redis、对象存储和 AI 服务密钥。 -- 建议在生产环境添加反向代理(Nginx 等)以及 HTTPS 终端。日志策略与健康检查已在 Compose 中示例配置。 - -## 6. 常见问题与排查 -- **数据库连接失败**:确认数据库容器是否就绪、账号密码与 `application-local.yml` 一致。 -- **Redis 认证错误**:本地默认无密码,如在服务器上启用密码,需要同步更新配置。 -- **前端无法请求 API**:检查 `VITE_API_BASE_URL` 是否指向正确地址/协议,并留意浏览器 CORS 报错。 -- **端口冲突**:修改对应服务的监听端口(Docker Compose 中的 `ports` 映射或 Vite 启动参数)。 -- **构建失败**:确保 Node.js 和 Maven 版本符合要求,必要时删除 `node_modules` / `target` 重装。 - -## 7. 下一步 -- 参考 `vocata-server/README.md` 获取 API 说明与模块概览。 -- 前端更多配置可查看各目录下的 `vite.config.ts` 与 `src` 代码。 +如果你要做本地 Docker 开发,请继续看 [Docker开发环境.md](./Docker开发环境.md)。 diff --git "a/docs/\351\203\250\347\275\262\347\216\257\345\242\203\350\257\264\346\230\216.md" "b/docs/\351\203\250\347\275\262\347\216\257\345\242\203\350\257\264\346\230\216.md" new file mode 100644 index 0000000..923ae5e --- /dev/null +++ "b/docs/\351\203\250\347\275\262\347\216\257\345\242\203\350\257\264\346\230\216.md" @@ -0,0 +1,72 @@ +# 部署环境说明 + +这份文档是当前 staging 部署的执行入口。仓库里和部署相关的路径、回滚方式、排障入口都以这里记录的实际行为为准。 + +## 服务器目录布局 + +当前 staging 服务器使用的根目录是: + +- `/home/deploy/deploy/vocata` + +部署工作流实际使用的代码目录是: + +- `/home/deploy/deploy/vocata/repo` + +环境变量文件放在: + +- `/home/deploy/deploy/vocata/.env` + +历史兼容位置是: + +- `/home/deploy/deploy/vocata/data/vocata-staging.env` + +如果根目录 `.env` 不存在,`cd-staging.yml` 会先尝试从旧路径复制一份,再继续部署。 + +## 部署路径 + +staging 的部署入口是 `.github/workflows/cd-staging.yml`。`develop` 分支 push 会自动触发;手动 `workflow_dispatch` 会按触发时选定的 ref,也就是当前 `github.ref_name`,执行部署。 + +当前实际动作是: + +1. 在 `/home/deploy/deploy/vocata/repo` 拉取或更新当前触发的 ref +2. 使用 `/home/deploy/deploy/vocata/.env` 作为 `docker compose` 的环境文件 +3. 执行 `docker compose --env-file "$ENV_FILE" up -d --build --wait` +4. 部署后用 `curl -fsS http://127.0.0.1:9009/api/health` 做健康检查 + +对外访问地址以 workflow 输出为准: + +- 用户端:`http://:3000` +- 管理端:`http://:3001` +- API:`http://:9009/api` + +## 回滚路径 + +staging 没有独立的回滚 workflow。当前可执行的回滚路径是: + +1. 回退到上一个 commit +2. 重新执行 staging workflow + +如果需要手动处理,实际做法还是回到 `/home/deploy/deploy/vocata/repo`,把代码切回上一个已知可用的 commit 或稳定状态,然后重新执行同一套 `docker compose --env-file "$ENV_FILE" up -d --build`。 + +如果这次部署是因为 `.env` 变更出问题,优先恢复 `/home/deploy/deploy/vocata/.env`,不要改容器内部配置。 + +## 排障入口 + +先看这几个位置,顺序和 workflow 的执行顺序一致: + +- GitHub Actions 的 `Deploy to Staging` 运行日志 +- GitHub Actions 的步骤摘要和 SSH action 输出 +- 服务器上的 `docker compose ps` +- 服务器上的 `docker compose logs --tail=100 vocata-server` +- 服务器上的 `docker compose logs --tail=60 vocata-web` +- 服务器上的 `docker compose logs --tail=60 vocata-admin` +- 健康检查:`curl -fsS http://127.0.0.1:9009/api/health` + +如果问题和密钥或部署用户有关,直接对照: + +- [GitHub-Staging-Secrets.md](./GitHub-Staging-Secrets.md) +- `.github/workflows/cd-staging.yml` + +## 关联工作流 + +- `.github/workflows/cd-staging.yml` diff --git "a/docs/\351\207\215\346\236\204\350\276\271\347\225\214\346\270\205\345\215\225.md" "b/docs/\351\207\215\346\236\204\350\276\271\347\225\214\346\270\205\345\215\225.md" new file mode 100644 index 0000000..5021f39 --- /dev/null +++ "b/docs/\351\207\215\346\236\204\350\276\271\347\225\214\346\270\205\345\215\225.md" @@ -0,0 +1,115 @@ +# 重构边界清单 + +这份清单只定义第一阶段的改动边界,不承担详细设计职责。目标是先限制重构范围,再进入模块级重构。 + +## vocata-server + +| 目录或入口 | 职责 | 第一阶段是否允许改动 | +| --- | --- | --- | +| `VocataApplication.java` | Spring Boot 启动入口 | 仅小修 | +| `src/main/resources/application*.yml` | 后端配置入口、provider 默认值、环境分层 | 允许 | +| `pom.xml` | 后端依赖和构建配置 | 允许 | +| `admin` | 管理端接口、DTO、服务 | 仅小修 | +| `ai` | LLM/STT/TTS、AI 接口、provider 装配 | 仅小修 | +| `auth` | 登录、注册、鉴权相关接口和服务 | 仅小修 | +| `character` | 角色管理、标签、角色生成 | 仅小修 | +| `common` | 通用配置、异常、返回体、工具 | 允许 | +| `config` | 全局配置入口 | 允许 | +| `conversation` | 对话、消息、标题生成 | 仅小修 | +| `file` | 文件上传、对象存储配置 | 允许 | +| `user` | 用户信息、收藏等用户域能力 | 仅小修 | +| `voice` | 声音配置、语音实体和服务 | 仅小修 | + +## vocata-web + +| 目录或入口 | 职责 | 第一阶段是否允许改动 | +| --- | --- | --- | +| `src/main.ts` | 前端启动入口 | 允许 | +| `src/App.vue` | 应用根组件 | 仅小修 | +| `package.json` | 前端脚本和依赖入口 | 允许 | +| `src/api` | 用户端接口请求与模块封装 | 允许 | +| `src/assets` | 图片、样式资源 | 仅小修 | +| `src/layouts` | 页面布局骨架 | 仅小修 | +| `src/router` | 路由入口与导航规则 | 允许 | +| `src/store` | Pinia 状态管理 | 允许 | +| `src/types` | 前端类型定义 | 允许 | +| `src/utils` | 通用工具、AI 聊天辅助逻辑 | 允许 | +| `src/views` | 页面与页面级组件 | 仅小修 | + +## vocata-admin + +| 目录或入口 | 职责 | 第一阶段是否允许改动 | +| --- | --- | --- | +| `src/main.ts` | 后台启动入口 | 允许 | +| `src/App.vue` | 后台根组件 | 仅小修 | +| `package.json` | 后台脚本和依赖入口 | 允许 | +| `src/api` | 管理后台接口请求与模块封装 | 允许 | +| `src/assets` | 图片、样式资源 | 仅小修 | +| `src/layouts` | 后台布局与导航骨架 | 仅小修 | +| `src/router` | 路由入口 | 允许 | +| `src/store` | 后台状态管理 | 允许 | +| `src/types` | 后台类型定义 | 允许 | +| `src/utils` | 通用工具 | 允许 | +| `src/views` | 后台页面 | 仅小修 | + +## 仓库级配置与编排 + +| 文件或入口 | 职责 | 第一阶段是否允许改动 | +| --- | --- | --- | +| `docker-compose.yml` | 开发编排主入口 | 允许 | +| `docker-compose.test.yml` | 测试环境编排 | 允许 | +| `docker-compose.prod.yml` | 生产环境编排 | 仅小修 | +| `.github/workflows/ci.yml` | CI 校验范围和命令入口 | 允许 | +| `.github/workflows/cd-staging.yml` | staging 部署入口 | 允许 | +| `.env.example` | 开发环境模板 | 允许 | + +## vocata-web 构建与校验面 + +| 文件或入口 | 职责 | 第一阶段是否允许改动 | +| --- | --- | --- | +| `vite.config.ts` | 用户端构建和 dev server 配置 | 允许 | +| `tsconfig.json` | 用户端 TypeScript 编译边界 | 允许 | +| `eslint.config.ts` | 用户端 lint 规则入口 | 允许 | + +## vocata-admin 构建与校验面 + +| 文件或入口 | 职责 | 第一阶段是否允许改动 | +| --- | --- | --- | +| `vite.config.ts` | 管理端构建和 dev server 配置 | 允许 | +| `tsconfig.json` | 管理端 TypeScript 编译边界 | 允许 | +| `eslint.config.ts` | 管理端 lint 规则入口 | 允许 | + +## 第一阶段允许改动 + +- 配置命名统一和配置说明收口 +- 文档补齐和入口整理 +- 验证命令固定、CI 范围收窄 +- 死代码删除 +- 明显重复类型、重复工具、小范围公共抽取 +- 小范围命名修正和注释修正 +- 只影响单模块的小修补 + +## 第一阶段禁止改动 + +- 大规模搬迁目录 +- 一次性重写整个页面或整个模块 +- 一次性替换 AI provider 总体结构 +- 大范围调整数据库结构 +- 跨前后端同时推进的大爆炸式重构 +- 把配置治理、结构整理和业务需求混在一个提交里 + +## 使用方式 + +- 看到“允许”不代表可以放大改,只代表第一阶段可以在这个区域内做最小范围治理。 +- 看到“仅小修”表示可以修明显问题,但不能把该目录当成第一阶段重构主战场。 +- 入口文件、配置文件和构建文件如果标为“允许”,也只表示它们是第一阶段可治理面,不表示可以顺手做结构重写。 +- 真正进入模块重构前,先以这份边界清单作为约束,再单独出该模块的实施计划。 +- 未列出的目录、入口文件和构建文件,默认不在第一阶段改动范围内;需要改动时,先单独补充边界说明或调整计划。 + +## 相关文档 + +- [`docs/开发环境说明.md`](开发环境说明.md) +- [`docs/部署环境说明.md`](部署环境说明.md) +- [`docs/提交规范.md`](提交规范.md) +- [`docs/开发工作流.md`](开发工作流.md) +- [`docs/验证清单.md`](验证清单.md) diff --git "a/docs/\351\252\214\350\257\201\346\270\205\345\215\225.md" "b/docs/\351\252\214\350\257\201\346\270\205\345\215\225.md" new file mode 100644 index 0000000..c4dde2b --- /dev/null +++ "b/docs/\351\252\214\350\257\201\346\270\205\345\215\225.md" @@ -0,0 +1,56 @@ +# 验证清单 + +以下入口按当前仓库的实际脚本和工作流整理,用于验证当前可执行基线、两个前端构建链路、Docker 编排,以及 staging 部署后的基础可用性。 + +## 推荐入口 + +```bash +./scripts/validate-backend.sh +./scripts/validate-web.sh +./scripts/validate-admin.sh +./scripts/validate-docker.sh +``` + +说明:以上脚本只是对当前基线命令做薄封装,不改变实际校验内容。默认本地直接从仓库根目录执行。 + +## 后端当前基线 + +```bash +./scripts/validate-backend.sh +``` + +说明:后端脚本依次执行 Maven 打包基线和 smoke test,默认使用 `/tmp/juhao_m2repo` 作为本地 Maven 仓库;如需覆盖,可临时传入 `MAVEN_REPO_LOCAL=/path/to/m2repo`。 + +## 用户端 + +```bash +./scripts/validate-web.sh +``` + +## 管理端 + +```bash +./scripts/validate-admin.sh +``` + +## Docker 开发环境 + +```bash +./scripts/validate-docker.sh +``` + +## Staging 部署后检查 + +```bash +./scripts/validate-staging-host.sh +``` + +说明:`validate-staging-host.sh` 需要在 staging 主机上的仓库目录执行。默认会推导上一级目录的 `.env`,也可以显式传入 `APP_DIR=/path/to/app` 或 `ENV_FILE=/path/to/.env`。 + +## 相关文档 + +- [`docs/开发环境说明.md`](开发环境说明.md) +- [`docs/部署环境说明.md`](部署环境说明.md) +- [`docs/提交规范.md`](提交规范.md) +- [`docs/开发工作流.md`](开发工作流.md) +- [`docs/重构边界清单.md`](重构边界清单.md) diff --git a/scripts/validate-admin.sh b/scripts/validate-admin.sh new file mode 100755 index 0000000..bb116d3 --- /dev/null +++ b/scripts/validate-admin.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" + +cd "$REPO_DIR/vocata-admin" + +echo "==> admin lint" +npx eslint . + +echo "==> admin type-check" +npm run type-check + +echo "==> admin build" +npm run build diff --git a/scripts/validate-backend.sh b/scripts/validate-backend.sh new file mode 100755 index 0000000..e7f045e --- /dev/null +++ b/scripts/validate-backend.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +MAVEN_REPO_LOCAL="${MAVEN_REPO_LOCAL:-/tmp/juhao_m2repo}" + +cd "$REPO_DIR/vocata-server" + +echo "==> backend package baseline" +mvn -Dmaven.repo.local="$MAVEN_REPO_LOCAL" -Dmaven.test.skip=true package + +echo "==> backend smoke baseline" +mvn -Dmaven.repo.local="$MAVEN_REPO_LOCAL" test diff --git a/scripts/validate-docker.sh b/scripts/validate-docker.sh new file mode 100755 index 0000000..fa885be --- /dev/null +++ b/scripts/validate-docker.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" + +cd "$REPO_DIR" + +echo "==> docker compose config" +docker compose config -q + +echo "==> docker compose ps" +docker compose ps diff --git a/scripts/validate-staging-host.sh b/scripts/validate-staging-host.sh new file mode 100755 index 0000000..8ca9c5e --- /dev/null +++ b/scripts/validate-staging-host.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +APP_DIR="${APP_DIR:-$(cd "$REPO_DIR/.." && pwd)}" +ENV_FILE="${ENV_FILE:-$APP_DIR/.env}" + +cd "$REPO_DIR" + +echo "==> staging compose status" +docker compose --env-file "$ENV_FILE" ps + +echo "==> staging backend health" +curl -fsS http://127.0.0.1:9009/api/health + +echo "==> staging character list smoke" +curl -fsS "http://127.0.0.1:9009/api/open/character/list?pageNum=1&pageSize=2" diff --git a/scripts/validate-web.sh b/scripts/validate-web.sh new file mode 100755 index 0000000..b2a2fd4 --- /dev/null +++ b/scripts/validate-web.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" + +cd "$REPO_DIR/vocata-web" + +echo "==> web lint" +npx eslint . + +echo "==> web type-check" +npm run type-check + +echo "==> web build" +npm run build 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-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-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-server/src/main/resources/application.yml b/vocata-server/src/main/resources/application.yml index e39247b..fa92a28 100644 --- a/vocata-server/src/main/resources/application.yml +++ b/vocata-server/src/main/resources/application.yml @@ -40,7 +40,7 @@ spring: host: ${REDIS_HOST:localhost} port: ${REDIS_PORT:6379} password: ${REDIS_PASSWORD:} - database: 0 + database: ${REDIS_DATABASE:0} timeout: 10000ms lettuce: pool: diff --git a/vocata-server/src/test/java/com/vocata/ai/test/SiliconFlowAiTest.java b/vocata-server/src/test/java/com/vocata/ai/test/SiliconFlowAiExamples.java similarity index 99% rename from vocata-server/src/test/java/com/vocata/ai/test/SiliconFlowAiTest.java rename to vocata-server/src/test/java/com/vocata/ai/test/SiliconFlowAiExamples.java index 721dbbe..694cbe1 100644 --- a/vocata-server/src/test/java/com/vocata/ai/test/SiliconFlowAiTest.java +++ b/vocata-server/src/test/java/com/vocata/ai/test/SiliconFlowAiExamples.java @@ -18,7 +18,7 @@ */ @SpringBootTest @ActiveProfiles("local") -public class SiliconFlowAiTest { +public class SiliconFlowAiExamples { /** * 测试案例1:基本的单轮对话 @@ -326,4 +326,4 @@ public void testRealWorldUsage() { System.out.println("\n\n✅ 真实场景测试完成"); System.out.println(String.format("回答长度: %d 字符", fullResponse.length())); } -} \ No newline at end of file +} diff --git a/vocata-server/src/test/java/com/vocata/ai/test/SiliconFlowApiUsageExample.java b/vocata-server/src/test/java/com/vocata/ai/test/SiliconFlowApiUsageExamples.java similarity index 99% rename from vocata-server/src/test/java/com/vocata/ai/test/SiliconFlowApiUsageExample.java rename to vocata-server/src/test/java/com/vocata/ai/test/SiliconFlowApiUsageExamples.java index 4a71cb6..aa94833 100644 --- a/vocata-server/src/test/java/com/vocata/ai/test/SiliconFlowApiUsageExample.java +++ b/vocata-server/src/test/java/com/vocata/ai/test/SiliconFlowApiUsageExamples.java @@ -15,7 +15,7 @@ */ @SpringBootTest @ActiveProfiles("local") -public class SiliconFlowApiUsageExample { +public class SiliconFlowApiUsageExamples { private final ObjectMapper objectMapper = new ObjectMapper(); @@ -320,4 +320,4 @@ public void printUsageGuide() { System.out.println("✅ 使用指南完成"); } -} \ No newline at end of file +} diff --git a/vocata-server/src/test/java/com/vocata/common/controller/HealthControllerWebMvcTest.java b/vocata-server/src/test/java/com/vocata/common/controller/HealthControllerWebMvcTest.java new file mode 100644 index 0000000..7dacc87 --- /dev/null +++ b/vocata-server/src/test/java/com/vocata/common/controller/HealthControllerWebMvcTest.java @@ -0,0 +1,27 @@ +package com.vocata.common.controller; + +import org.junit.jupiter.api.Test; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +class HealthControllerWebMvcTest { + + @Test + void healthEndpointReturnsStableSuccessPayload() throws Exception { + MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new HealthController()).build(); + + mockMvc.perform(get("/api/health")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.message").value("操作成功")) + .andExpect(jsonPath("$.data.status").value("UP")) + .andExpect(jsonPath("$.data.service").value("VocaTa API")) + .andExpect(jsonPath("$.data.version").value("1.0.0")) + .andExpect(jsonPath("$.timestamp").isNumber()) + .andExpect(jsonPath("$.data.timestamp").exists()); + } +} 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;"] 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 @@